diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 00000000000..20908052bab --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,95 @@ +version: '{build}' +clone_folder: c:\pillow +init: +- ECHO %PYTHON% +#- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) +# Uncomment previous line to get RDP access during the build. + +environment: + EXECUTABLE: python.exe + TEST_OPTIONS: + DEPLOY: YES + matrix: + - PYTHON: C:/Python310 + ARCHITECTURE: x86 + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 + - PYTHON: C:/Python37-x64 + ARCHITECTURE: x64 + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 + + +install: +- '%PYTHON%\%EXECUTABLE% --version' +- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip +- 7z x pillow-depends.zip -oc:\ +- mv c:\pillow-depends-main c:\pillow-depends +- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images +- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ +- ..\pillow-depends\gs1000w32.exe /S +- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH% +- cd c:\pillow\winbuild\ +- ps: | + c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ + c:\pillow\winbuild\build\build_dep_all.cmd + $host.SetShouldExit(0) +- path C:\pillow\winbuild\build\bin;%PATH% + +build_script: +- ps: | + c:\pillow\winbuild\build\build_pillow.cmd install + $host.SetShouldExit(0) +- cd c:\pillow +- '%PYTHON%\%EXECUTABLE% selftest.py --installed' + +test_script: +- cd c:\pillow +- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout' +- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% +- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' +- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' +#- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest? + +after_test: +- python -m pip install codecov +- codecov --file coverage.xml --name %PYTHON% --flags AppVeyor + +matrix: + fast_finish: true + +cache: +- '%LOCALAPPDATA%\pip\Cache' + +artifacts: +- path: pillow\dist\*.egg + name: egg +- path: pillow\dist\*.wheel + name: wheel + +before_deploy: + - cd c:\pillow + - '%PYTHON%\%EXECUTABLE% -m pip install wheel' + - cd c:\pillow\winbuild\ + - c:\pillow\winbuild\build\build_pillow.cmd bdist_wheel + - cd c:\pillow + - ps: Get-ChildItem .\dist\*.* | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } + +deploy: + provider: S3 + region: us-west-2 + access_key_id: AKIAIRAXC62ZNTVQJMOQ + secret_access_key: + secure: Hwb6klTqtBeMgxAjRoDltiiqpuH8xbwD4UooDzBSiCWXjuFj1lyl4kHgHwTCCGqi + bucket: pillow-nightly + folder: win/$(APPVEYOR_BUILD_NUMBER)/ + artifact: /.*egg|wheel/ + on: + APPVEYOR_REPO_NAME: python-pillow/Pillow + branch: main + deploy: YES + + +# Uncomment the following lines to get RDP access after the build/test and block for +# up to the timeout limit (~1hr) +# +#on_finish: +#- ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/.ci/after_success.sh b/.ci/after_success.sh new file mode 100755 index 00000000000..23a6fcd4d45 --- /dev/null +++ b/.ci/after_success.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# gather the coverage data +python3 -m pip install codecov +if [[ $MATRIX_DOCKER ]]; then + python3 -m coverage xml --ignore-errors +else + python3 -m coverage xml +fi diff --git a/.ci/build.sh b/.ci/build.sh new file mode 100755 index 00000000000..e678f68ec85 --- /dev/null +++ b/.ci/build.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +python3 -m coverage erase +if [ $(uname) == "Darwin" ]; then + export CPPFLAGS="-I/usr/local/miniconda/include"; +fi +make clean +make install-coverage diff --git a/.ci/install.sh b/.ci/install.sh new file mode 100755 index 00000000000..518b66acc23 --- /dev/null +++ b/.ci/install.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +aptget_update() +{ + if [ ! -z $1 ]; then + echo "" + echo "Retrying apt-get update..." + echo "" + fi + output=`sudo apt-get update 2>&1` + echo "$output" + if [[ $output == *[WE]:\ * ]]; then + return 1 + fi +} +if [[ $(uname) != CYGWIN* ]]; then + aptget_update || aptget_update retry || aptget_update retry +fi + +set -e + +if [[ $(uname) != CYGWIN* ]]; then + sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ + ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ + cmake meson imagemagick libharfbuzz-dev libfribidi-dev +fi + +python3 -m pip install --upgrade pip +python3 -m pip install --upgrade wheel +PYTHONOPTIMIZE=0 python3 -m pip install cffi +python3 -m pip install coverage +python3 -m pip install defusedxml +python3 -m pip install olefile +python3 -m pip install -U pytest +python3 -m pip install -U pytest-cov +python3 -m pip install -U pytest-timeout +python3 -m pip install pyroma + +if [[ $(uname) != CYGWIN* ]]; then + python3 -m pip install numpy + + # PyQt6 doesn't support PyPy3 + if [[ $GHA_PYTHON_VERSION == 3.* ]]; then + sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 + python3 -m pip install pyqt6 + fi + + # webp + pushd depends && ./install_webp.sh && popd + + # libimagequant + pushd depends && ./install_imagequant.sh && popd + + # raqm + pushd depends && ./install_raqm.sh && popd + + # extra test images + pushd depends && ./install_extra_test_images.sh && popd +else + cd depends && ./install_extra_test_images.sh && cd .. +fi diff --git a/.ci/test.sh b/.ci/test.sh new file mode 100755 index 00000000000..8ff7c5f6483 --- /dev/null +++ b/.ci/test.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e + +python3 -c "from PIL import Image" + +python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests $REVERSE diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000000..be32e6d1a8a --- /dev/null +++ b/.clang-format @@ -0,0 +1,20 @@ +# A clang-format style that approximates Python's PEP 7 +# Useful for IDE integration +BasedOnStyle: Google +AlwaysBreakAfterReturnType: All +AllowShortIfStatementsOnASingleLine: false +AlignAfterOpenBracket: AlwaysBreak +BinPackArguments: false +BinPackParameters: false +BreakBeforeBraces: Attach +ColumnLimit: 88 +DerivePointerAlignment: false +IndentWidth: 4 +Language: Cpp +PointerAlignment: Right +ReflowComments: true +SortIncludes: false +SpaceBeforeParens: ControlStatements +SpacesInParentheses: false +TabWidth: 4 +UseTab: Never diff --git a/.coveragerc b/.coveragerc index ea79190ae0e..f71b6b1a281 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,5 +10,11 @@ exclude_lines = if 0: if __name__ == .__main__.: # Don't complain about debug code - if Image.DEBUG: if DEBUG: + +[run] +omit = + Tests/32bit_segfault_check.py + Tests/bench_cffi_access.py + Tests/check_*.py + Tests/createfontdatachunk.py diff --git a/.editorconfig b/.editorconfig index 798ab753c77..d74549fe2ac 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,7 +16,6 @@ trim_trailing_whitespace = true [*.yml] # Two-space indentation indent_size = 2 -indent_style = space # Tab indentation (no size specified) [Makefile] diff --git a/.gitattributes b/.gitattributes index 983b58729d1..2cf25ab1ac8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ +*.eps binary *.ppm binary *.container binary diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5e467c4b138..ba2b7d8ed26 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -4,24 +4,27 @@ Bug fixes, feature additions, tests, documentation and more can be contributed v ## Bug fixes, feature additions, etc. -Please send a pull request to the master branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new) or irc://irc.freenode.net#pil +Please send a pull request to the `main` branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [discussions](https://github.com/python-pillow/Pillow/discussions/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil - Fork the Pillow repository. -- Create a branch from master. +- Create a branch from `main`. - Develop bug fixes, features, tests, etc. -- Run the test suite on both Python 2.x and 3.x. You can enable [Travis CI](https://travis-ci.org/profile/) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Coveralls](https://coveralls.io/repos/new) to see if the changed code is covered by tests. -- Create a pull request to pull the changes from your branch to the Pillow master. +- Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests. +- Create a pull request to pull the changes from your branch to the Pillow `main`. ### Guidelines - Separate code commits from reformatting commits. - Provide tests for any newly added code. -- Follow PEP8. -- When committing only documentation changes please include [ci skip] in the commit message to avoid running tests on Travis-CI and AppVeyor. +- Follow PEP 8. +- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor. +- Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests. ## Reporting Issues -When reporting issues, please include code that reproduces the issue and whenever possible, an image that demonstrates the issue. The best reproductions are self-contained scripts with minimal dependencies. +When reporting issues, please include code that reproduces the issue and whenever possible, an image that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive. + +The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as plone, Django, or buildout, try to replicate the issue just using Pillow. ### Provide details @@ -32,6 +35,4 @@ When reporting issues, please include code that reproduces the issue and wheneve ## Security vulnerabilities -To report sensitive vulnerability information, email security@python-pillow.org. - -If your organisation/employer is a distributor of Pillow and would like advance notification of security-related bugs, please let us know your preferred contact method. +Please see our [security policy](https://github.com/python-pillow/Pillow/blob/main/.github/SECURITY.md). diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000000..e0e6804bfe4 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +tidelift: "pypi/Pillow" diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 6c91b6427da..00000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,15 +0,0 @@ -### What did you do? - -### What did you expect to happen? - -### What actually happened? - -### What versions of Pillow and Python are you using? - -Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive. - -The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as plone, Django, or buildout, try to replicate the issue just using Pillow. - -```python -code goes here -``` diff --git a/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md b/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md new file mode 100644 index 00000000000..115f6135dfb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ISSUE_REPORT.md @@ -0,0 +1,59 @@ +--- +name: Issue report +about: Create a report to help us improve Pillow +--- + + + +### What did you do? + +### What did you expect to happen? + +### What actually happened? + +### What are your OS, Python and Pillow versions? + +* OS: +* Python: +* Pillow: + + + +```python +code goes here +``` diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000000..c6369fdef21 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,5 @@ +# Security policy + +To report sensitive vulnerability information, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. + +If your organisation/employer is a distributor of Pillow and would like advance notification of security-related bugs, please let us know your preferred contact method. diff --git a/.github/mergify.yml b/.github/mergify.yml new file mode 100644 index 00000000000..8dfa07f4ec5 --- /dev/null +++ b/.github/mergify.yml @@ -0,0 +1,15 @@ +pull_request_rules: + - name: Automatic merge + conditions: + - "#approved-reviews-by>=1" + - label=automerge + - status-success=Lint + - status-success=Test Successful + - status-success=Docker Test Successful + - status-success=Windows Test Successful + - status-success=MinGW Test Successful + - status-success=Cygwin Test Successful + - status-success=continuous-integration/appveyor/pr + actions: + merge: + method: merge diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000000..4d855469a12 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,26 @@ +name-template: "$NEXT_MINOR_VERSION" +tag-template: "$NEXT_MINOR_VERSION" +change-template: '- $TITLE #$NUMBER [@$AUTHOR]' + +categories: + - title: "Dependencies" + label: "Dependency" + - title: "Deprecations" + label: "Deprecation" + - title: "Documentation" + label: "Documentation" + - title: "Removals" + label: "Removal" + - title: "Testing" + label: "Testing" + +exclude-labels: + - "changelog: skip" + +template: | + + https://pillow.readthedocs.io/en/stable/releasenotes/$NEXT_MINOR_VERSION.html + + ## Changes + + $CHANGES diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 00000000000..d1d82433553 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ], + "labels": [ + "Dependency" + ], + "packageRules": [ + { + "groupName": "github-actions", + "matchManagers": ["github-actions"], + "separateMajorMinor": "false" + } + ], + "schedule": ["on the 3rd day of the month"] +} diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml new file mode 100644 index 00000000000..db030704607 --- /dev/null +++ b/.github/workflows/cifuzz.yml @@ -0,0 +1,56 @@ +name: CIFuzz + +on: + push: + paths: + - "**.c" + - "**.h" + pull_request: + paths: + - "**.c" + - "**.h" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + Fuzzing: + runs-on: ubuntu-latest + steps: + - name: Build Fuzzers + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + with: + oss-fuzz-project-name: 'pillow' + language: python + dry-run: false + - name: Run Fuzzers + id: run + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + with: + oss-fuzz-project-name: 'pillow' + fuzz-seconds: 600 + language: python + dry-run: false + - name: Upload New Crash + uses: actions/upload-artifact@v3 + if: failure() && steps.build.outcome == 'success' + with: + name: artifacts + path: ./out/artifacts + - name: Upload Legacy Crash + uses: actions/upload-artifact@v3 + if: steps.run.outcome == 'success' + with: + name: crash + path: ./out/crash* + - name: Fail on legacy crash + if: success() + run: | + [ ! -e out/crash-* ] + echo No legacy crash detected diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000000..6195f973b05 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,48 @@ +name: Lint + +on: [push, pull_request, workflow_dispatch] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + + runs-on: ubuntu-latest + + name: Lint + + steps: + - uses: actions/checkout@v3 + + - name: pre-commit cache + uses: actions/cache@v3 + with: + path: ~/.cache/pre-commit + key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} + restore-keys: | + lint-pre-commit- + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + cache: pip + cache-dependency-path: "setup.py" + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Install dependencies + run: | + python3 -m pip install -U pip + python3 -m pip install -U tox + + - name: Lint + run: tox -e lint + env: + PRE_COMMIT_COLOR: always diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh new file mode 100755 index 00000000000..65f2b81d543 --- /dev/null +++ b/.github/workflows/macos-install.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -e + +brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype openblas libraqm + +PYTHONOPTIMIZE=0 python3 -m pip install cffi +python3 -m pip install coverage +python3 -m pip install defusedxml +python3 -m pip install olefile +python3 -m pip install -U pytest +python3 -m pip install -U pytest-cov +python3 -m pip install -U pytest-timeout +python3 -m pip install pyroma + +echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg +python3 -m pip install numpy + +# extra test images +pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 00000000000..9e2fdc09604 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,28 @@ +name: Release drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + update_release_draft: + permissions: + contents: write # for release-drafter/release-drafter to create a github release + pull-requests: write # for release-drafter/release-drafter to add label to PR + if: github.repository == 'python-pillow/Pillow' + runs-on: ubuntu-latest + steps: + # Drafts your next release notes as pull requests are merged into "main" + - uses: release-drafter/release-drafter@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000000..ffac91ceca7 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,31 @@ +name: Close stale issues + +on: + schedule: + - cron: "10 0 * * *" + workflow_dispatch: + +permissions: + issues: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + stale: + if: github.repository_owner == 'python-pillow' + + runs-on: ubuntu-latest + + steps: + - name: "Check issues" + uses: actions/stale@v6 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + only-labels: "Awaiting OP Action" + close-issue-message: "Closing this issue as no feedback has been received." + days-before-stale: 7 + days-before-issue-close: 0 + days-before-pr-close: -1 + labels-to-remove-when-unstale: "Awaiting OP Action" diff --git a/.github/workflows/system-info.py b/.github/workflows/system-info.py new file mode 100644 index 00000000000..8e840319a7d --- /dev/null +++ b/.github/workflows/system-info.py @@ -0,0 +1,25 @@ +""" +Print out some handy system info like Travis CI does. + +This sort of info is missing from GitHub Actions. + +Requested here: +https://github.com/actions/virtual-environments/issues/79 +""" +import os +import platform +import sys + +print("Build system information") +print() + +print("sys.version\t\t", sys.version.split("\n")) +print("os.name\t\t\t", os.name) +print("sys.platform\t\t", sys.platform) +print("platform.system()\t", platform.system()) +print("platform.machine()\t", platform.machine()) +print("platform.platform()\t", platform.platform()) +print("platform.version()\t", platform.version()) +print("platform.uname()\t", platform.uname()) +if sys.platform == "darwin": + print("platform.mac_ver()\t", platform.mac_ver()) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml new file mode 100644 index 00000000000..5b9ab0edab3 --- /dev/null +++ b/.github/workflows/test-cygwin.yml @@ -0,0 +1,116 @@ +name: Test Cygwin + +on: [push, pull_request, workflow_dispatch] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + python-minor-version: [7, 8, 9] + + timeout-minutes: 40 + + name: Python 3.${{ matrix.python-minor-version }} + + steps: + - name: Fix line endings + run: | + git config --global core.autocrlf input + + - name: Checkout Pillow + uses: actions/checkout@v3 + + - name: Install Cygwin + uses: cygwin/cygwin-install-action@v2 + with: + platform: x86_64 + packages: > + ImageMagick gcc-g++ ghostscript jpeg libfreetype-devel + libimagequant-devel libjpeg-devel liblapack-devel + liblcms2-devel libopenjp2-devel libraqm-devel + libtiff-devel libwebp-devel libxcb-devel libxcb-xinerama0 + make netpbm perl + python3${{ matrix.python-minor-version }}-cffi + python3${{ matrix.python-minor-version }}-cython + python3${{ matrix.python-minor-version }}-devel + python3${{ matrix.python-minor-version }}-numpy + python3${{ matrix.python-minor-version }}-sip + python3${{ matrix.python-minor-version }}-tkinter + qt5-devel-tools subversion xorg-server-extra zlib-devel + + - name: Add Lapack to PATH + uses: egor-tensin/cleanup-path@v2 + with: + dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' + + - name: pip cache + uses: actions/cache@v3 + with: + path: 'C:\cygwin\home\runneradmin\.cache\pip' + key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }} + restore-keys: | + ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}- + + - name: Build system information + run: | + dash.exe -c "python3 .github/workflows/system-info.py" + + - name: Install dependencies + run: | + bash.exe .ci/install.sh + + - name: Install a different NumPy + shell: dash.exe -l "{0}" + run: | + python3 -m pip install -U 'numpy!=1.21.*' + + - name: Build + shell: bash.exe -eo pipefail -o igncr "{0}" + run: | + .ci/build.sh + + - name: Test + run: | + bash.exe xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh + + - name: Prepare to upload errors + if: failure() + run: | + dash.exe -c "mkdir -p Tests/errors" + + - name: Upload errors + uses: actions/upload-artifact@v3 + if: failure() + with: + name: errors + path: Tests/errors + + - name: After success + run: | + bash.exe .ci/after_success.sh + + - name: Upload coverage + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: GHA_Cygwin + name: Cygwin Python 3.${{ matrix.python-minor-version }} + + success: + permissions: + contents: none + needs: build + runs-on: ubuntu-latest + name: Cygwin Test Successful + steps: + - name: Success + run: echo Cygwin Test Successful diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml new file mode 100644 index 00000000000..c68d43935e2 --- /dev/null +++ b/.github/workflows/test-docker.yml @@ -0,0 +1,99 @@ +name: Test Docker + +on: [push, pull_request, workflow_dispatch] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + docker: [ + # Run slower jobs first to give them a headstart and reduce waiting time + ubuntu-22.04-jammy-arm64v8, + ubuntu-22.04-jammy-ppc64le, + ubuntu-22.04-jammy-s390x, + # Then run the remainder + alpine, + amazon-2-amd64, + arch, + centos-7-amd64, + centos-stream-8-amd64, + centos-stream-9-amd64, + debian-10-buster-x86, + debian-11-bullseye-x86, + fedora-35-amd64, + fedora-36-amd64, + gentoo, + ubuntu-18.04-bionic-amd64, + ubuntu-20.04-focal-amd64, + ubuntu-22.04-jammy-amd64, + ] + dockerTag: [main] + include: + - docker: "ubuntu-22.04-jammy-arm64v8" + qemu-arch: "aarch64" + - docker: "ubuntu-22.04-jammy-ppc64le" + qemu-arch: "ppc64le" + - docker: "ubuntu-22.04-jammy-s390x" + qemu-arch: "s390x" + + name: ${{ matrix.docker }} + + steps: + - uses: actions/checkout@v3 + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Set up QEMU + if: "matrix.qemu-arch" + run: | + docker run --rm --privileged aptman/qus -s -- -p ${{ matrix.qemu-arch }} + + - name: Docker pull + run: | + docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + + - name: Docker build + run: | + # The Pillow user in the docker container is UID 1000 + sudo chown -R 1000 $GITHUB_WORKSPACE + docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + sudo chown -R runner $GITHUB_WORKSPACE + + - name: After success + run: | + PATH="$PATH:~/.local/bin" + docker start pillow_container + pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'` + docker stop pillow_container + sudo mkdir -p $pil_path + sudo cp src/PIL/*.py $pil_path + .ci/after_success.sh + env: + MATRIX_DOCKER: ${{ matrix.docker }} + + - name: Upload coverage + uses: codecov/codecov-action@v3 + with: + flags: GHA_Docker + name: ${{ matrix.docker }} + + success: + permissions: + contents: none + needs: build + runs-on: ubuntu-latest + name: Docker Test Successful + steps: + - name: Success + run: echo Docker Test Successful diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml new file mode 100644 index 00000000000..ccf6e193a6d --- /dev/null +++ b/.github/workflows/test-mingw.yml @@ -0,0 +1,94 @@ +name: Test MinGW + +on: [push, pull_request, workflow_dispatch] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + mingw: ["MINGW32", "MINGW64"] + include: + - mingw: "MINGW32" + name: "MSYS2 MinGW 32-bit" + package: "mingw-w64-i686" + - mingw: "MINGW64" + name: "MSYS2 MinGW 64-bit" + package: "mingw-w64-x86_64" + + defaults: + run: + shell: bash.exe --login -eo pipefail "{0}" + env: + MSYSTEM: ${{ matrix.mingw }} + CHERE_INVOKING: 1 + + timeout-minutes: 30 + name: ${{ matrix.name }} + + steps: + - name: Checkout Pillow + uses: actions/checkout@v3 + + - name: Set up shell + run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH + shell: pwsh + + - name: Install dependencies + run: | + pacman -S --noconfirm \ + ${{ matrix.package }}-python3-cffi \ + ${{ matrix.package }}-python3-numpy \ + ${{ matrix.package }}-python3-olefile \ + ${{ matrix.package }}-python3-pip \ + ${{ matrix.package }}-python-pyqt6 \ + ${{ matrix.package }}-python3-setuptools \ + ${{ matrix.package }}-freetype \ + ${{ matrix.package }}-gcc \ + ${{ matrix.package }}-ghostscript \ + ${{ matrix.package }}-lcms2 \ + ${{ matrix.package }}-libimagequant \ + ${{ matrix.package }}-libjpeg-turbo \ + ${{ matrix.package }}-libraqm \ + ${{ matrix.package }}-libtiff \ + ${{ matrix.package }}-libwebp \ + ${{ matrix.package }}-openjpeg2 \ + subversion + + python3 -m pip install pyroma pytest pytest-cov pytest-timeout + + pushd depends && ./install_extra_test_images.sh && popd + + - name: Build Pillow + run: CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" . + + - name: Test Pillow + run: | + python3 selftest.py --installed + python3 -c "from PIL import Image" + python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests + + - name: Upload coverage + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: GHA_Windows + name: ${{ matrix.name }} + + success: + permissions: + contents: none + needs: build + runs-on: ubuntu-latest + name: MinGW Test Successful + steps: + - name: Success + run: echo MinGW Test Successful diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml new file mode 100644 index 00000000000..219189cf208 --- /dev/null +++ b/.github/workflows/test-valgrind.yml @@ -0,0 +1,52 @@ +name: Test Valgrind + +# like the docker tests, but running valgrind only on *.c/*.h changes. + +on: + push: + paths: + - "**.c" + - "**.h" + pull_request: + paths: + - "**.c" + - "**.h" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + docker: [ + ubuntu-22.04-jammy-amd64-valgrind, + ] + dockerTag: [main] + + name: ${{ matrix.docker }} + + steps: + - uses: actions/checkout@v3 + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Docker pull + run: | + docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + + - name: Build and Run Valgrind + run: | + # The Pillow user in the docker container is UID 1000 + sudo chown -R 1000 $GITHUB_WORKSPACE + docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + sudo chown -R runner $GITHUB_WORKSPACE diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml new file mode 100644 index 00000000000..6b7f62c237e --- /dev/null +++ b/.github/workflows/test-windows.yml @@ -0,0 +1,243 @@ +name: Test Windows + +on: [push, pull_request, workflow_dispatch] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + 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" + architecture: "x64" + - python-version: "pypy-3.8" + architecture: "x64" + + timeout-minutes: 30 + + name: Python ${{ matrix.python-version }} ${{ matrix.architecture }} + + steps: + - name: Checkout Pillow + uses: actions/checkout@v3 + + - name: Checkout cached dependencies + uses: actions/checkout@v3 + with: + repository: python-pillow/pillow-depends + path: winbuild\depends + + # sets env: pythonLocation + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + architecture: ${{ matrix.architecture }} + cache: pip + cache-dependency-path: ".github/workflows/test-windows.yml" + + - name: Print build system information + run: python3 .github/workflows/system-info.py + + - name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml + run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml + + - name: Install dependencies + id: install + run: | + 7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\" + echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH + + winbuild\depends\gs1000w32.exe /S + echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH + + 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 "vs=$a" >> $env:GITHUB_OUTPUT } + shell: pwsh + + - name: Cache build + id: build-cache + uses: actions/cache@v3 + with: + path: winbuild\build + key: + ${{ hashFiles('winbuild\build_prepare.py') }}-${{ hashFiles('.github\workflows\test-windows.yml') }}-${{ env.pythonLocation }}-${{ steps.install.outputs.vs }} + + - name: Prepare build + if: steps.build-cache.outputs.cache-hit != 'true' + run: | + & python.exe winbuild\build_prepare.py -v --python=$env:pythonLocation --srcdir + shell: pwsh + + - name: Build dependencies / libjpeg-turbo + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libjpeg.cmd" + + - name: Build dependencies / zlib + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_zlib.cmd" + + - name: Build dependencies / xz + if: steps.build-cache.outputs.cache-hit != 'true' + 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" + + - name: Build dependencies / LCMS2 + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_lcms2.cmd" + + - name: Build dependencies / OpenJPEG + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_openjpeg.cmd" + + # GPL licensed + - name: Build dependencies / libimagequant + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libimagequant.cmd" + + # Raqm dependencies + - name: Build dependencies / HarfBuzz + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_harfbuzz.cmd" + + # Raqm dependencies + - name: Build dependencies / FriBidi + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_fribidi.cmd" + + # trim ~150MB x 9 + - name: Optimize build cache + if: steps.build-cache.outputs.cache-hit != 'true' + run: rmdir /S /Q winbuild\build\src + shell: cmd + + - name: Build Pillow + run: | + $FLAGS="" + if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS="--disable-imagequant" } + & winbuild\build\build_pillow.cmd $FLAGS install + & $env:pythonLocation\python.exe selftest.py --installed + shell: pwsh + + # skip PyPy for speed + - name: Enable heap verification + if: "!contains(matrix.python-version, 'pypy')" + run: | + & reg.exe add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f + + - name: Test Pillow + run: | + path %GITHUB_WORKSPACE%\\winbuild\\build\\bin;%PATH% + python.exe -m pytest -vx -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests + shell: cmd + + - name: Prepare to upload errors + if: failure() + run: | + mkdir -p Tests/errors + shell: bash + + - name: Upload errors + uses: actions/upload-artifact@v3 + if: failure() + with: + name: errors + path: Tests/errors + + - name: After success + run: | + .ci/after_success.sh + shell: pwsh + + - name: Upload coverage + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: GHA_Windows + name: ${{ runner.os }} Python ${{ matrix.python-version }} ${{ matrix.architecture }} + + - name: Build wheel + id: wheel + if: "github.event_name != 'pull_request'" + run: | + 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 + + - 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.10" + uses: actions/upload-artifact@v3 + with: + name: fribidi + path: fribidi\* + + success: + permissions: + contents: none + needs: build + runs-on: ubuntu-latest + name: Windows Test Successful + steps: + - name: Success + run: echo Windows Test Successful diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000000..645384c02d8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,122 @@ +name: Test + +on: [push, pull_request, workflow_dispatch] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + + strategy: + fail-fast: false + matrix: + os: [ + "macos-latest", + "ubuntu-latest", + ] + python-version: [ + "pypy-3.8", + "pypy-3.7", + "3.11", + "3.10", + "3.9", + "3.8", + "3.7", + ] + include: + - python-version: "3.7" + PYTHONOPTIMIZE: 1 + REVERSE: "--reverse" + - python-version: "3.8" + PYTHONOPTIMIZE: 2 + + runs-on: ${{ matrix.os }} + name: ${{ matrix.os }} Python ${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: ".ci/*.sh" + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Install Linux dependencies + if: startsWith(matrix.os, 'ubuntu') + run: | + .ci/install.sh + env: + GHA_PYTHON_VERSION: ${{ matrix.python-version }} + + - name: Install macOS dependencies + if: startsWith(matrix.os, 'macOS') + run: | + .github/workflows/macos-install.sh + env: + GHA_PYTHON_VERSION: ${{ matrix.python-version }} + + - name: Build + run: | + .ci/build.sh + + - name: Test + run: | + if [ $REVERSE ]; then + python3 -m pip install pytest-reverse + fi + if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then + xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh + else + .ci/test.sh + fi + env: + PYTHONOPTIMIZE: ${{ matrix.PYTHONOPTIMIZE }} + REVERSE: ${{ matrix.REVERSE }} + + - name: Prepare to upload errors + if: failure() + run: | + mkdir -p Tests/errors + + - name: Upload errors + uses: actions/upload-artifact@v3 + if: failure() + with: + name: errors + path: Tests/errors + + - name: Docs + if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10 + run: | + make doccheck + + - name: After success + run: | + .ci/after_success.sh + + - name: Upload coverage + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }} + name: ${{ matrix.os }} Python ${{ matrix.python-version }} + + success: + permissions: + contents: none + needs: build + runs-on: ubuntu-latest + name: Test Successful + steps: + - name: Success + run: echo Test Successful diff --git a/.github/workflows/tidelift.yml b/.github/workflows/tidelift.yml new file mode 100644 index 00000000000..69f9e547602 --- /dev/null +++ b/.github/workflows/tidelift.yml @@ -0,0 +1,36 @@ +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/.gitignore b/.gitignore index 242f50845a2..790404535f7 100644 --- a/.gitignore +++ b/.gitignore @@ -32,7 +32,7 @@ htmlcov/ .tox/ .coverage .cache -nosetests.xml +.pytest_cache coverage.xml # Test files @@ -56,6 +56,9 @@ test_images # Sphinx documentation docs/_build/ +# viewdoc output +.long-description.html + # Vim cruft .*.swp @@ -64,6 +67,9 @@ docs/_build/ \#*# .#* +#VS Code +.vscode + #Komodo *.komodoproject @@ -75,6 +81,14 @@ docs/_build/ # Extra test images installed from pillow-depends/test_images Tests/images/README.md +Tests/images/crash_1.tif +Tests/images/crash_2.tif +Tests/images/crash-81154a65438ba5aaeca73fd502fa4850fbde60f8.tif +Tests/images/string_dimension.tiff +Tests/images/jpeg2000 Tests/images/msp Tests/images/picins Tests/images/sunraster + +# pyinstaller +*.spec diff --git a/.landscape.yaml b/.landscape.yaml deleted file mode 100644 index ddd9cef3260..00000000000 --- a/.landscape.yaml +++ /dev/null @@ -1,3 +0,0 @@ -strictness: medium -test-warnings: yes -max-line-length: 80 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000000..f81bcb956fa --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,52 @@ +repos: + - repo: https://github.com/psf/black + rev: 22.8.0 + hooks: + - id: black + 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 + hooks: + - id: isort + + - repo: https://github.com/asottile/yesqa + rev: v1.4.0 + hooks: + - id: yesqa + + - repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.3.1 + hooks: + - id: remove-tabs + exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) + + - repo: https://github.com/PyCQA/flake8 + rev: 5.0.4 + hooks: + - id: flake8 + additional_dependencies: [flake8-2020, flake8-implicit-str-concat] + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.9.0 + hooks: + - id: python-check-blanket-noqa + - id: rst-backticks + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-merge-conflict + - id: check-json + - id: check-yaml + + - repo: https://github.com/sphinx-contrib/sphinx-lint + rev: v0.6.1 + hooks: + - id: sphinx-lint + +ci: + autoupdate_schedule: monthly diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000000..0f581ebba90 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,8 @@ +version: 2 + +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1b8d854bcee..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,88 +0,0 @@ -language: python - -notifications: - irc: "chat.freenode.net#pil" - -# Run slow PyPy* first, to give them a headstart and reduce waiting time. -# Run latest 3.x and 2.x next, to get quick compatibility results. -# Then run the remainder, with fastest Docker jobs last. - -matrix: - fast_finish: true - include: - - python: "pypy" - - python: "pypy3" - - python: '3.6' - - python: '2.7' - - python: "2.7_with_system_site_packages" # For PyQt4 - - python: '3.5' - - python: '3.4' - - python: '3.7-dev' - - env: DOCKER="alpine" DOCKER_TAG="pytest" - - env: DOCKER="arch" DOCKER_TAG="pytest" # contains PyQt5 - - env: DOCKER="ubuntu-trusty-x86" DOCKER_TAG="pytest" - - env: DOCKER="ubuntu-xenial-amd64" DOCKER_TAG="pytest" - - env: DOCKER="debian-stretch-x86" DOCKER_TAG="pytest" - - env: DOCKER="centos-6-amd64" DOCKER_TAG="pytest" - - env: DOCKER="centos-7-amd64" DOCKER_TAG="pytest" - - env: DOCKER="amazon-1-amd64" DOCKER_TAG="pytest" - - env: DOCKER="amazon-2-amd64" DOCKER_TAG="pytest" - - env: DOCKER="fedora-26-amd64" DOCKER_TAG="pytest" - - env: DOCKER="fedora-27-amd64" DOCKER_TAG="pytest" - -dist: trusty - -sudo: required - -services: - - docker - -install: - - if [ "$DOCKER" == "" ]; then .travis/install.sh; fi - -before_install: - - if [ "$DOCKER" ]; then docker pull pythonpillow/$DOCKER:$DOCKER_TAG; fi - -before_script: -# Qt needs a display for some of the tests, and it's only run on the system site packages install - - "export DISPLAY=:99.0" - - "sh -e /etc/init.d/xvfb start" - -script: - - | - if [ "$DOCKER" == "" ]; then - .travis/script.sh - else - # the Pillow user in the docker container is UID 1000 - sudo chown -R 1000 $TRAVIS_BUILD_DIR - docker run -v $TRAVIS_BUILD_DIR:/Pillow pythonpillow/$DOCKER:$DOCKER_TAG - fi - -after_success: - - .travis/after_success.sh - -after_failure: - - | - if [ "$TRAVIS_REPO_SLUG" = "python-pillow/Pillow" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then - curl -Lo travis_after_all.py https://raw.github.com/dmakhno/travis_after_all/master/travis_after_all.py - python travis_after_all.py - export $(cat .to_export_back) - if [ "$BUILD_LEADER" = "YES" ]; then - if [ "$BUILD_AGGREGATE_STATUS" = "others_failed" ]; then - echo "All jobs failed" - else - echo "Some jobs failed" - fi - fi - fi - -after_script: - - | - if [ "$TRAVIS_REPO_SLUG" = "python-pillow/Pillow" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then - echo leader=$BUILD_LEADER status=$BUILD_AGGREGATE_STATUS - fi - -env: - global: - # travis encrypt AUTH_TOKEN= - secure: "Vzm7aG1Qv0SDQcqiPzZMedNLn5ZmpL7IzF0DYnqcD+/l+zmKU22SnJBcX0uVXumo+r7eZfpsShpqfcdsZvMlvmQnwz+Y6AGKQru9tCKZbTMnuRjWKKXekC+tr8Xt9CKvRVtte5PyXW31paxUI3/e+fQGBwoFjEEC+6EpEOjeRfE=" diff --git a/.travis/after_success.sh b/.travis/after_success.sh deleted file mode 100755 index a18c095c959..00000000000 --- a/.travis/after_success.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash - -# gather the coverage data -sudo apt-get -qq install lcov -lcov --capture --directory . -b . --output-file coverage.info -# filter to remove system headers -lcov --remove coverage.info '/usr/*' -o coverage.filtered.info -# convert to json -gem install coveralls-lcov -coveralls-lcov -v -n coverage.filtered.info > coverage.c.json - -coverage report -pip install codecov -pip install coveralls-merge -coveralls-merge coverage.c.json -codecov - -if [ "$DOCKER" == "" ]; then - pip install pyflakes pycodestyle - pyflakes *.py | tee >(wc -l) - pyflakes src/PIL/*.py | tee >(wc -l) - pyflakes Tests/*.py | tee >(wc -l) - pycodestyle --statistics --count src/PIL/*.py - pycodestyle --statistics --count Tests/*.py -fi - -if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ] && [ "$DOCKER" == "" ]; then - # Coverage and quality reports on just the latest diff. - # (Installation is very slow on Py3, so just do it for Py2.) - depends/diffcover-install.sh - depends/diffcover-run.sh -fi - -# after_all - -if [ "$TRAVIS_REPO_SLUG" = "python-pillow/Pillow" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then - curl -Lo travis_after_all.py https://raw.github.com/dmakhno/travis_after_all/master/travis_after_all.py - python travis_after_all.py - export $(cat .to_export_back) - if [ "$BUILD_LEADER" = "YES" ]; then - if [ "$BUILD_AGGREGATE_STATUS" = "others_succeeded" ]; then - echo "All jobs succeeded! Triggering macOS build..." - # Trigger a macOS build at the pillow-wheels repo - ./build_children.sh - else - echo "Some jobs failed" - fi - fi -fi diff --git a/.travis/install.sh b/.travis/install.sh deleted file mode 100755 index cad0e3c3203..00000000000 --- a/.travis/install.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -set -e - -sudo apt-get update -sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-tk\ - python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick\ - libharfbuzz-dev libfribidi-dev - -pip install cffi -pip install check-manifest -pip install coverage -pip install olefile -pip install -U pytest -pip install -U pytest-cov -pip install pyroma -pip install test-image-results - -# docs only on Python 2.7 -if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then pip install -r requirements.txt ; fi - -# clean checkout for manifest -mkdir /tmp/check-manifest && cp -a . /tmp/check-manifest - -# webp -pushd depends && ./install_webp.sh && popd - -# openjpeg -pushd depends && ./install_openjpeg.sh && popd - -# libimagequant -pushd depends && ./install_imagequant.sh && popd - -# extra test images -pushd depends && ./install_extra_test_images.sh && popd - diff --git a/.travis/script.sh b/.travis/script.sh deleted file mode 100755 index d6e02f01dd4..00000000000 --- a/.travis/script.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -set -e - -coverage erase -make clean -make install-coverage - -python selftest.py -python -m pytest -vx --cov PIL --cov-report term Tests - -pushd /tmp/check-manifest && check-manifest --ignore ".coveragerc,.editorconfig,*.yml,*.yaml,tox.ini" && popd - -# Docs -if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then make doccheck; fi diff --git a/CHANGES.rst b/CHANGES.rst index 9a33a67b9ac..6f2ba569e0c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,2339 @@ Changelog (Pillow) ================== +9.3.0 (2022-10-29) +------------------ + +- Limit SAMPLESPERPIXEL to avoid runtime DOS #6700 + [wiredfool] + +- Initialize libtiff buffer when saving #6699 + [radarhere] + +- Inline fname2char to fix memory leak #6329 + [nulano] + +- Fix memory leaks related to text features #6330 + [nulano] + +- Use double quotes for version check on old CPython on Windows #6695 + [hugovk] + +- Remove backup implementation of Round for Windows platforms #6693 + [cgohlke] + +- Fixed set_variation_by_name offset #6445 + [radarhere] + +- Fix malloc in _imagingft.c:font_setvaraxes #6690 + [cgohlke] + +- Release Python GIL when converting images using matrix operations #6418 + [hmaarrfk] + +- Added ExifTags enums #6630 + [radarhere] + +- Do not modify previous frame when calculating delta in PNG #6683 + [radarhere] + +- Added support for reading BMP images with RLE4 compression #6674 + [npjg, radarhere] + +- Decode JPEG compressed BLP1 data in original mode #6678 + [radarhere] + +- Added GPS TIFF tag info #6661 + [radarhere] + +- Added conversion between RGB/RGBA/RGBX and LAB #6647 + [radarhere] + +- Do not attempt normalization if mode is already normal #6644 + [radarhere] + +- Fixed seeking to an L frame in a GIF #6576 + [radarhere] + +- Consider all frames when selecting mode for PNG save_all #6610 + [radarhere] + +- Don't reassign crc on ChunkStream close #6627 + [wiredfool, radarhere] + +- Raise a warning if NumPy failed to raise an error during conversion #6594 + [radarhere] + +- Show all frames in ImageShow #6611 + [radarhere] + +- Allow FLI palette chunk to not be first #6626 + [radarhere] + +- If first GIF frame has transparency for RGB_ALWAYS loading strategy, use RGBA mode #6592 + [radarhere] + +- Round box position to integer when pasting embedded color #6517 + [radarhere, nulano] + +- Removed EXIF prefix when saving WebP #6582 + [radarhere] + +- Pad IM palette to 768 bytes when saving #6579 + [radarhere] + +- Added DDS BC6H reading #6449 + [ShadelessFox, REDxEYE, radarhere] + +- Added support for opening WhiteIsZero 16-bit integer TIFF images #6642 + [JayWiz, radarhere] + +- Raise an error when allocating translucent color to RGB palette #6654 + [jsbueno, radarhere] + +- Added reading of TIFF child images #6569 + [radarhere] + +- Improved ImageOps palette handling #6596 + [PososikTeam, radarhere] + +- Defer parsing of palette into colors #6567 + [radarhere] + +- Apply transparency to P images in ImageTk.PhotoImage #6559 + [radarhere] + +- Use rounding in ImageOps contain() and pad() #6522 + [bibinhashley, radarhere] + +- Fixed GIF remapping to palette with duplicate entries #6548 + [radarhere] + +- Allow remap_palette() to return an image with less than 256 palette entries #6543 + [radarhere] + +- Corrected BMP and TGA palette size when saving #6500 + [radarhere] + +- Do not call load() before draft() in Image.thumbnail #6539 + [radarhere] + +- Copy palette when converting from P to PA #6497 + [radarhere] + +- Allow RGB and RGBA values for PA image putpixel #6504 + [radarhere] + +- Removed support for tkinter in PyPy before Python 3.6 #6551 + [nulano] + +- Do not use CCITTFaxDecode filter if libtiff is not available #6518 + [radarhere] + +- Fallback to not using mmap if buffer is not large enough #6510 + [radarhere] + +- Fixed writing bytes as ASCII tag #6493 + [radarhere] + +- Open 1 bit EPS in mode 1 #6499 + [radarhere] + +- Removed support for tkinter before Python 1.5.2 #6549 + [radarhere] + +- Allow default ImageDraw font to be set #6484 + [radarhere, hugovk] + +- Save 1 mode PDF using CCITTFaxDecode filter #6470 + [radarhere] + +- Added support for RGBA PSD images #6481 + [radarhere] + +- Parse orientation from XMP tag contents #6463 + [bigcat88, radarhere] + +- Added support for reading ATI1/ATI2 (BC4/BC5) DDS images #6457 + [REDxEYE, radarhere] + +- Do not clear GIF tile when checking number of frames #6455 + [radarhere] + +- Support saving multiple MPO frames #6444 + [radarhere] + +- Do not double quote Pillow version for setuptools >= 60 #6450 + [radarhere] + +- Added ABGR BMP mask mode #6436 + [radarhere] + +- Fixed PSDraw rectangle #6429 + [radarhere] + +- Raise ValueError if PNG sRGB chunk is truncated #6431 + [radarhere] + +- Handle missing Python executable in ImageShow on macOS #6416 + [bryant1410, radarhere] + +9.2.0 (2022-07-01) +------------------ + +- Deprecate ImageFont.getsize and related functions #6381 + [nulano, radarhere] + +- Fixed null check for fribidi_version_info in FriBiDi shim #6376 + [nulano] + +- Added GIF decompression bomb check #6402 + [radarhere] + +- Handle PCF fonts files with less than 256 characters #6386 + [dawidcrivelli, radarhere] + +- Improved GIF optimize condition #6378 + [raygard, radarhere] + +- Reverted to __array_interface__ with the release of NumPy 1.23 #6394 + [radarhere] + +- Pad PCX palette to 768 bytes when saving #6391 + [radarhere] + +- Fixed bug with rounding pixels to palette colors #6377 + [btrekkie, radarhere] + +- Use gnome-screenshot on Linux if available #6361 + [radarhere, nulano] + +- Fixed loading L mode BMP RLE8 images #6384 + [radarhere] + +- Fixed incorrect operator in ImageCms error #6370 + [LostBenjamin, hugovk, radarhere] + +- Limit FPX tile size to avoid extending outside image #6368 + [radarhere] + +- Added support for decoding plain PPM formats #5242 + [Piolie, radarhere] + +- Added apply_transparency() #6352 + [radarhere] + +- Fixed behaviour change from endian fix #6197 + [radarhere] + +- Allow remapping P images with RGBA palettes #6350 + [radarhere] + +- Fixed drawing translucent 1px high polygons #6278 + [radarhere] + +- Pad COLORMAP to 768 items when saving TIFF #6232 + [radarhere] + +- Fix P -> PA conversion #6337 + [RedShy, radarhere] + +- Once exif data is parsed, do not reload unless it changes #6335 + [radarhere] + +- Only try to connect discontiguous corners at the end of edges #6303 + [radarhere] + +- Improve transparency handling when saving GIF images #6176 + [radarhere] + +- Do not update GIF frame position until local image is found #6219 + [radarhere] + +- Netscape GIF extension belongs after the global color table #6211 + [radarhere] + +- Only write GIF comments at the beginning of the file #6300 + [raygard, radarhere] + +- Separate multiple GIF comment blocks with newlines #6294 + [raygard, radarhere] + +- Always use GIF89a for comments #6292 + [raygard, radarhere] + +- Ignore compression value from BMP info dictionary when saving as TIFF #6231 + [radarhere] + +- If font is file-like object, do not re-read from object to get variant #6234 + [radarhere] + +- Raise ValueError when trying to access internal fp after close #6213 + [radarhere] + +- Support more affine expression forms in im.point() #6254 + [benrg, radarhere] + +- Populate Python palette in fromarray() #6283 + [radarhere] + +- Raise ValueError if PNG chunks are truncated #6253 + [radarhere] + +- Use durations from each frame by default when saving GIFs #6265 + [radarhere] + +- Adjust BITSPERSAMPLE to match SAMPLESPERPIXEL when opening TIFFs #6270 + [radarhere] + +- Search pkgconf system libs/cflags #6138 + [jameshilliard, radarhere] + +- Raise ValueError for invalid PPM maxval #6242 + [radarhere] + +- Corrected screencapture argument in ImageGrab.grab() #6244 + [axt-one] + +- Deprecate support for Qt 5 (PyQt5 and PySide2) #6237 + [hugovk, radarhere] + +- Increase wait time of temporary file deletion on Windows #6224 + [AlexTedeschi] + +- Deprecate FreeTypeFont.getmask2 fill parameter #6220 + [nulano, radarhere, hugovk] + +- Round lut values where necessary #6188 + [radarhere] + +- Load before getting size in resize() #6190 + [radarhere] + +- Load image before performing size calculations in thumbnail() #6186 + [radarhere] + +- Deprecated PhotoImage.paste() box parameter #6178 + [radarhere] + +9.1.1 (2022-05-17) +------------------ + +- When reading past the end of a TGA scan line, reduce bytes left. CVE-2022-30595 + [radarhere] + +- Do not open images with zero or negative height #6269 + [radarhere] + +9.1.0 (2022-04-01) +------------------ + +- Add support for multiple component transformation to JPEG2000 #5500 + [scaramallion, radarhere, hugovk] + +- Fix loading FriBiDi on Alpine #6165 + [nulano] + +- Added setting for converting GIF P frames to RGB #6150 + [radarhere] + +- Allow 1 mode images to be inverted #6034 + [radarhere] + +- Raise ValueError when trying to save empty JPEG #6159 + [radarhere] + +- Always save TIFF with contiguous planar configuration #5973 + [radarhere] + +- Connected discontiguous polygon corners #5980 + [radarhere] + +- Ensure Tkinter hook is activated for getimage() #6032 + [radarhere] + +- Use screencapture arguments to crop on macOS #6152 + [radarhere] + +- Do not mark L mode JPEG as 1 bit in PDF #6151 + [radarhere] + +- Added support for reading I;16R TIFF images #6132 + [radarhere] + +- If an error occurs after creating a file, remove the file #6134 + [radarhere] + +- Fixed calling DisplayViewer or XVViewer without a title #6136 + [radarhere] + +- Retain RGBA transparency when saving multiple GIF frames #6128 + [radarhere] + +- Save additional ICO frames with other bit depths if supplied #6122 + [radarhere] + +- Handle EXIF data truncated to just the header #6124 + [radarhere] + +- Added support for reading BMP images with RLE8 compression #6102 + [radarhere] + +- Support Python distributions where _tkinter is compiled in #6006 + [lukegb] + +- Added support for PPM arbitrary maxval #6119 + [radarhere] + +- Added BigTIFF reading #6097 + [radarhere] + +- When converting, clip I;16 to be unsigned, not signed #6112 + [radarhere] + +- Fixed loading L mode GIF with transparency #6086 + [radarhere] + +- Improved handling of PPM header #5121 + [Piolie, radarhere] + +- Reset size when seeking away from "Large Thumbnail" MPO frame #6101 + [radarhere] + +- Replace requirements.txt with extras #6072 + [hugovk, radarhere] + +- Added PyEncoder and support BLP saving #6069 + [radarhere] + +- Handle TGA images with packets that cross scan lines #6087 + [radarhere] + +- Added FITS reading #6056 + [radarhere, hugovk] + +- Added rawmode argument to Image.getpalette() #6061 + [radarhere] + +- Fixed BUFR, GRIB and HDF5 stub saving #6071 + [radarhere] + +- Do not automatically remove temporary ImageShow files on Unix #6045 + [radarhere] + +- Correctly read JPEG compressed BLP images #4685 + [Meithal, radarhere] + +- Merged _MODE_CONV typ into ImageMode as typestr #6057 + [radarhere] + +- Consider palette size when converting and in getpalette() #6060 + [radarhere] + +- Added enums #5954 + [radarhere] + +- Ensure image is opaque after converting P to PA with RGB palette #6052 + [radarhere] + +- Attach RGBA palettes from putpalette() when suitable #6054 + [radarhere] + +- Added get_photoshop_blocks() to parse Photoshop TIFF tag #6030 + [radarhere] + +- Drop excess values in BITSPERSAMPLE #6041 + [mikhail-iurkov] + +- Added unpacker from RGBA;15 to RGB #6031 + [radarhere] + +- Enable arm64 for MSVC on Windows #5811 + [gaborkertesz-linaro, gaborkertesz] + +- Keep IPython/Jupyter text/plain output stable #5891 + [shamrin, radarhere] + +- Raise an error when performing a negative crop #5972 + [radarhere, hugovk] + +- Deprecated show_file "file" argument in favour of "path" #5959 + [radarhere] + +- Fixed SPIDER images for use with Bio-formats library #5956 + [radarhere] + +- Ensure duplicated file pointer is closed #5946 + [radarhere] + +- Added specific error if path coordinate type is incorrect #5942 + [radarhere] + +- Return an empty bytestring from tobytes() for an empty image #5938 + [radarhere] + +- Remove readonly from Image.__eq__ #5930 + [hugovk] + +9.0.1 (2022-02-03) +------------------ + +- In show_file, use os.remove to remove temporary images. CVE-2022-24303 #6010 + [radarhere, hugovk] + +- Restrict builtins within lambdas for ImageMath.eval. CVE-2022-22817 #6009 + [radarhere] + +9.0.0 (2022-01-02) +------------------ + +- Restrict builtins for ImageMath.eval(). CVE-2022-22817 #5923 + [radarhere] + +- Ensure JpegImagePlugin stops at the end of a truncated file #5921 + [radarhere] + +- Fixed ImagePath.Path array handling. CVE-2022-22815, CVE-2022-22816 #5920 + [radarhere] + +- Remove consecutive duplicate tiles that only differ by their offset #5919 + [radarhere] + +- Improved I;16 operations on big endian #5901 + [radarhere] + +- Limit quantized palette to number of colors #5879 + [radarhere] + +- Fixed palette index for zeroed color in FASTOCTREE quantize #5869 + [radarhere] + +- When saving RGBA to GIF, make use of first transparent palette entry #5859 + [radarhere] + +- Pass SAMPLEFORMAT to libtiff #5848 + [radarhere] + +- Added rounding when converting P and PA #5824 + [radarhere] + +- Improved putdata() documentation and data handling #5910 + [radarhere] + +- Exclude carriage return in PDF regex to help prevent ReDoS #5912 + [hugovk] + +- Fixed freeing pointer in ImageDraw.Outline.transform #5909 + [radarhere] + +- Added ImageShow support for xdg-open #5897 + [m-shinder, radarhere] + +- Support 16-bit grayscale ImageQt conversion #5856 + [cmbruns, radarhere] + +- Convert subsequent GIF frames to RGB or RGBA #5857 + [radarhere] + +- Do not prematurely return in ImageFile when saving to stdout #5665 + [infmagic2047, radarhere] + +- Added support for top right and bottom right TGA orientations #5829 + [radarhere] + +- Corrected ICNS file length in header #5845 + [radarhere] + +- Block tile TIFF tags when saving #5839 + [radarhere] + +- Added line width argument to polygon #5694 + [radarhere] + +- Do not redeclare class each time when converting to NumPy #5844 + [radarhere] + +- Only prevent repeated polygon pixels when drawing with transparency #5835 + [radarhere] + +- Add support for pickling TrueType fonts #5826 + [hugovk, radarhere] + +- Only prefer command line tools SDK on macOS over default MacOSX SDK #5828 + [radarhere] + +- Drop support for soon-EOL Python 3.6 #5768 + [hugovk, nulano, radarhere] + +- Fix compilation on 64-bit Termux #5793 + [landfillbaby] + +- Use title for display in ImageShow #5788 + [radarhere] + +- Remove support for FreeType 2.7 and older #5777 + [hugovk, radarhere] + +- Fix for PyQt6 #5775 + [hugovk, radarhere] + +- Removed deprecated PILLOW_VERSION, Image.show command parameter, Image._showxv and ImageFile.raise_ioerror #5776 + [radarhere] + +8.4.0 (2021-10-15) +------------------ + +- Prefer global transparency in GIF when replacing with background color #5756 + [radarhere] + +- Added "exif" keyword argument to TIFF saving #5575 + [radarhere] + +- Copy Python palette to new image in quantize() #5696 + [radarhere] + +- Read ICO AND mask from end #5667 + [radarhere] + +- Actually check the framesize in FliDecode.c #5659 + [wiredfool] + +- Determine JPEG2000 mode purely from ihdr header box #5654 + [radarhere] + +- Fixed using info dictionary when writing multiple APNG frames #5611 + [radarhere] + +- Allow saving 1 and L mode TIFF with PhotometricInterpretation 0 #5655 + [radarhere] + +- For GIF save_all with palette, do not include palette with each frame #5603 + [radarhere] + +- Keep transparency when converting from P to LA or PA #5606 + [radarhere] + +- Copy palette to new image in transform() #5647 + [radarhere] + +- Added "transparency" argument to EpsImagePlugin load() #5620 + [radarhere] + +- Corrected pathlib.Path detection when saving #5633 + [radarhere] + +- Added WalImageFile class #5618 + [radarhere] + +- Consider I;16 pixel size when drawing text #5598 + [radarhere] + +- If default conversion from P is RGB with transparency, convert to RGBA #5594 + [radarhere] + +- Speed up rotating square images by 90 or 270 degrees #5646 + [radarhere] + +- Add support for reading DPI information from JPEG2000 images + [rogermb, radarhere] + +- Catch TypeError from corrupted DPI value in EXIF #5639 + [homm, radarhere] + +- Do not close file pointer when saving SGI images #5645 + [farizrahman4u, radarhere] + +- Deprecate ImagePalette size parameter #5641 + [radarhere, hugovk] + +- Prefer command line tools SDK on macOS #5624 + [radarhere] + +- Added tags when saving YCbCr TIFF #5597 + [radarhere] + +- PSD layer count may be negative #5613 + [radarhere] + +- Fixed ImageOps expand with tuple border on P image #5615 + [radarhere] + +- Fixed error saving APNG with duplicate frames and different duration times #5609 + [thak1411, radarhere] + +8.3.2 (2021-09-02) +------------------ + +- CVE-2021-23437 Raise ValueError if color specifier is too long + [hugovk, radarhere] + +- Fix 6-byte OOB read in FliDecode + [wiredfool] + +- Add support for Python 3.10 #5569, #5570 + [hugovk, radarhere] + +- Ensure TIFF ``RowsPerStrip`` is multiple of 8 for JPEG compression #5588 + [kmilos, radarhere] + +- Updates for ``ImagePalette`` channel order #5599 + [radarhere] + +- Hide FriBiDi shim symbols to avoid conflict with real FriBiDi library #5651 + [nulano] + +8.3.1 (2021-07-06) +------------------ + +- Catch OSError when checking if fp is sys.stdout #5585 + [radarhere] + +- Handle removing orientation from alternate types of EXIF data #5584 + [radarhere] + +- Make Image.__array__ take optional dtype argument #5572 + [t-vi, radarhere] + +8.3.0 (2021-07-01) +------------------ + +- Use snprintf instead of sprintf. CVE-2021-34552 #5567 + [radarhere] + +- Limit TIFF strip size when saving with LibTIFF #5514 + [kmilos] + +- Allow ICNS save on all operating systems #4526 + [baletu, radarhere, newpanjing, hugovk] + +- De-zigzag JPEG's DQT when loading; deprecate convert_dict_qtables #4989 + [gofr, radarhere] + +- Replaced xml.etree.ElementTree #5565 + [radarhere] + +- Moved CVE image to pillow-depends #5561 + [radarhere] + +- Added tag data for IFD groups #5554 + [radarhere] + +- Improved ImagePalette #5552 + [radarhere] + +- Add DDS saving #5402 + [radarhere] + +- Improved getxmp() #5455 + [radarhere] + +- Convert to float for comparison with float in IFDRational __eq__ #5412 + [radarhere] + +- Allow getexif() to access TIFF tag_v2 data #5416 + [radarhere] + +- Read FITS image mode and size #5405 + [radarhere] + +- Merge parallel horizontal edges in ImagingDrawPolygon #5347 + [radarhere, hrdrq] + +- Use transparency behind first GIF frame and when disposing to background #5557 + [radarhere, zewt] + +- Avoid unstable nature of qsort in Quant.c #5367 + [radarhere] + +- Copy palette to new images in ImageOps expand #5551 + [radarhere] + +- Ensure palette string matches RGB mode #5549 + [radarhere] + +- Do not modify EXIF of original image instance in exif_transpose() #5547 + [radarhere] + +- Fixed default numresolution for small JPEG2000 images #5540 + [radarhere] + +- Added DDS BC5 reading #5501 + [radarhere] + +- Raise an error if ImageDraw.textbbox is used without a TrueType font #5510 + [radarhere] + +- Added ICO saving in BMP format #5513 + [radarhere] + +- Ensure PNG seeks to end of previous chunk at start of load_end #5493 + [radarhere] + +- Do not allow TIFF to seek to a past frame #5473 + [radarhere] + +- Avoid race condition when displaying images with eog #5507 + [mconst] + +- Added specific error messages when ink has incorrect number of bands #5504 + [radarhere] + +- Allow converting an image to a numpy array to raise errors #5379 + [radarhere] + +- Removed DPI rounding from BMP, JPEG, PNG and WMF loading #5476, #5470 + [radarhere] + +- Remove spikes when drawing thin pieslices #5460 + [xtsm] + +- Updated default value for SAMPLESPERPIXEL TIFF tag #5452 + [radarhere] + +- Removed TIFF DPI rounding #5446 + [radarhere, hugovk] + +- Include code in WebP error #5471 + [radarhere] + +- Do not alter pixels outside mask when drawing text on an image with transparency #5434 + [radarhere] + +- Reset handle when seeking backwards in TIFF #5443 + [radarhere] + +- Replace sys.stdout with sys.stdout.buffer when saving #5437 + [radarhere] + +- Fixed UNDEFINED TIFF tag of length 0 being changed in roundtrip #5426 + [radarhere] + +- Fixed bug when checking FreeType2 version if it is not installed #5445 + [radarhere] + +- Do not round dimensions when saving PDF #5459 + [radarhere] + +- Added ImageOps contain() #5417 + [radarhere, hugovk] + +- Changed WebP default "method" value to 4 #5450 + [radarhere] + +- Switched to saving 1-bit PDFs with DCTDecode #5430 + [radarhere] + +- Use bpp from ICO header #5429 + [radarhere] + +- Corrected JPEG APP14 transform value #5408 + [radarhere] + +- Changed TIFF tag 33723 length to 1 #5425 + [radarhere] + +- Changed ImageMorph incorrect mode errors to ValueError #5414 + [radarhere] + +- Add EXIF tags specified in EXIF 2.32 #5419 + [gladiusglad] + +- Treat previous contents of first GIF frame as transparent #5391 + [radarhere] + +- For special image modes, revert default resize resampling to NEAREST #5411 + [radarhere] + +- JPEG2000: Support decoding subsampled RGB and YCbCr images #4996 + [nulano, radarhere] + +- Stop decoding BC1 punchthrough alpha in BC2&3 #4144 + [jansol] + +- Use zero if GIF background color index is missing #5390 + [radarhere] + +- Fixed ensuring that GIF previous frame was loaded #5386 + [radarhere] + +- Valgrind fixes #5397 + [wiredfool] + +- Round down the radius in rounded_rectangle #5382 + [radarhere] + +- Fixed reading uncompressed RGB data from DDS #5383 + [radarhere] + +8.2.0 (2021-04-01) +------------------ + +- Added getxmp() method #5144 + [UrielMaD, radarhere] + +- Add ImageShow support for GraphicsMagick #5349 + [latosha-maltba, radarhere] + +- Do not load transparent pixels from subsequent GIF frames #5333 + [zewt, radarhere] + +- Use LZW encoding when saving GIF images #5291 + [raygard] + +- Set all transparent colors to be equal in quantize() #5282 + [radarhere] + +- Allow PixelAccess to use Python __int__ when parsing x and y #5206 + [radarhere] + +- Removed Image._MODEINFO #5316 + [radarhere] + +- Add preserve_tone option to autocontrast #5350 + [elejke, radarhere] + +- Fixed linear_gradient and radial_gradient I and F modes #5274 + [radarhere] + +- Add support for reading TIFFs with PlanarConfiguration=2 #5364 + [kkopachev, wiredfool, nulano] + +- Deprecated categories #5351 + [radarhere] + +- Do not premultiply alpha when resizing with Image.NEAREST resampling #5304 + [nulano] + +- Dynamically link FriBiDi instead of Raqm #5062 + [nulano] + +- Allow fewer PNG palette entries than the bit depth maximum when saving #5330 + [radarhere] + +- Use duration from info dictionary when saving WebP #5338 + [radarhere] + +- Stop flattening EXIF IFD into getexif() #4947 + [radarhere, kkopachev] + +- Replaced tiff_deflate with tiff_adobe_deflate compression when saving TIFF images #5343 + [radarhere] + +- Save ICC profile from TIFF encoderinfo #5321 + [radarhere] + +- Moved RGB fix inside ImageQt class #5268 + [radarhere] + +- Allow alpha_composite destination to be negative #5313 + [radarhere] + +- Ensure file is closed if it is opened by ImageQt.ImageQt #5260 + [radarhere] + +- Added ImageDraw rounded_rectangle method #5208 + [radarhere] + +- Added IPythonViewer #5289 + [radarhere, Kipkurui-mutai] + +- Only draw each rectangle outline pixel once #5183 + [radarhere] + +- Use mmap instead of built-in Win32 mapper #5224 + [radarhere, cgohlke] + +- Handle PCX images with an odd stride #5214 + [radarhere] + +- Only read different sizes for "Large Thumbnail" MPO frames #5168 + [radarhere] + +- Added PyQt6 support #5258 + [radarhere] + +- Changed Image.open formats parameter to be case-insensitive #5250 + [Piolie, radarhere] + +- Deprecate Tk/Tcl 8.4, to be removed in Pillow 10 (2023-07-01) #5216 + [radarhere] + +- Added tk version to pilinfo #5226 + [radarhere, nulano] + +- Support for ignoring tests when running valgrind #5150 + [wiredfool, radarhere, hugovk] + +- OSS-Fuzz support #5189 + [wiredfool, radarhere] + +8.1.2 (2021-03-06) +------------------ + +- Fix Memory DOS in BLP (CVE-2021-27921), ICNS (CVE-2021-27922) and ICO (CVE-2021-27923) Image Plugins + [wiredfool] + +8.1.1 (2021-03-01) +------------------ + +- Use more specific regex chars to prevent ReDoS. CVE-2021-25292 + [hugovk] + +- Fix OOB Read in TiffDecode.c, and check the tile validity before reading. CVE-2021-25291 + [wiredfool] + +- Fix negative size read in TiffDecode.c. CVE-2021-25290 + [wiredfool] + +- Fix OOB read in SgiRleDecode.c. CVE-2021-25293 + [wiredfool] + +- Incorrect error code checking in TiffDecode.c. CVE-2021-25289 + [wiredfool] + +- PyModule_AddObject fix for Python 3.10 #5194 + [radarhere] + +8.1.0 (2021-01-02) +------------------ + +- Fix TIFF OOB Write error. CVE-2020-35654 #5175 + [wiredfool] + +- Fix for Read Overflow in PCX Decoding. CVE-2020-35653 #5174 + [wiredfool, radarhere] + +- Fix for SGI Decode buffer overrun. CVE-2020-35655 #5173 + [wiredfool, radarhere] + +- Fix OOB Read when saving GIF of xsize=1 #5149 + [wiredfool] + +- Makefile updates #5159 + [wiredfool, radarhere] + +- Add support for PySide6 #5161 + [hugovk] + +- Use disposal settings from previous frame in APNG #5126 + [radarhere] + +- Added exception explaining that _repr_png_ saves to PNG #5139 + [radarhere] + +- Use previous disposal method in GIF load_end #5125 + [radarhere] + +- Allow putpalette to accept 1024 integers to include alpha values #5089 + [radarhere] + +- Fix OOB Read when writing TIFF with custom Metadata #5148 + [wiredfool] + +- Added append_images support for ICO #4568 + [ziplantil, radarhere] + +- Block TIFFTAG_SUBIFD #5120 + [radarhere] + +- Fixed dereferencing potential null pointers #5108, #5111 + [cgohlke, radarhere] + +- Deprecate FreeType 2.7 #5098 + [hugovk, radarhere] + +- Moved warning to end of execution #4965 + [radarhere] + +- Removed unused fromstring and tostring C methods #5026 + [radarhere] + +- init() if one of the formats is unrecognised #5037 + [radarhere] + +- Moved string_dimension CVE image to pillow-depends #4993 + [radarhere] + +- Support raw rgba8888 for DDS #4760 + [qiankanglai] + +8.0.1 (2020-10-22) +------------------ + +- Update FreeType used in binary wheels to 2.10.4 to fix CVE-2020-15999. + [radarhere] + +- Moved string_dimension image to pillow-depends #4993 + [radarhere] + +8.0.0 (2020-10-15) +------------------ + +- Drop support for EOL Python 3.5 #4746, #4794 + [hugovk, radarhere, nulano] + +- Drop support for PyPy3 < 7.2.0 #4964 + [nulano] + +- Remove ImageCms.CmsProfile attributes deprecated since 3.2.0 #4768 + [hugovk, radarhere] + +- Remove long-deprecated Image.py functions #4798 + [hugovk, nulano, radarhere] + +- Add support for 16-bit precision JPEG quantization values #4918 + [gofr] + +- Added reading of IFD tag type #4979 + [radarhere] + +- Initialize offset memory for PyImagingPhotoPut #4806 + [nqbit] + +- Fix TiffDecode comparison warnings #4756 + [nulano] + +- Docs: Add dark mode #4968 + [hugovk, nulano] + +- Added macOS SDK install path to library and include directories #4974 + [radarhere, fxcoudert] + +- Imaging.h: prevent confusion with system #4923 + [ax3l, ,radarhere] + +- Avoid using pkg_resources in PIL.features.pilinfo #4975 + [nulano] + +- Add getlength and getbbox functions for TrueType fonts #4959 + [nulano, radarhere, hugovk] + +- Allow tuples with one item to give single color value in getink #4927 + [radarhere, nulano] + +- Add support for CBDT and COLR fonts #4955 + [nulano, hugovk] + +- Removed OSError in favour of DecompressionBombError for BMP #4966 + [radarhere] + +- Implemented another ellipse drawing algorithm #4523 + [xtsm, radarhere] + +- Removed unused JpegImagePlugin._fixup_dict function #4957 + [radarhere] + +- Added reading and writing of private PNG chunks #4292 + [radarhere] + +- Implement anchor for TrueType fonts #4930 + [nulano, hugovk] + +- Fixed bug in Exif __delitem__ #4942 + [radarhere] + +- Fix crash in ImageTk.PhotoImage on MinGW 64-bit #4946 + [nulano] + +- Moved CVE images to pillow-depends #4929 + [radarhere] + +- Refactor font_getsize and font_render #4910 + [nulano] + +- Fixed loading profile with non-ASCII path on Windows #4914 + [radarhere] + +- Fixed effect_spread bug for zero distance #4908 + [radarhere, hugovk] + +- Added formats parameter to Image.open #4837 + [nulano, radarhere] + +- Added regular_polygon draw method #4846 + [comhar] + +- Raise proper TypeError in putpixel #4882 + [nulano, hugovk] + +- Added writing of subIFDs #4862 + [radarhere] + +- Fix IFDRational __eq__ bug #4888 + [luphord, radarhere] + +- Fixed duplicate variable name #4885 + [liZe, radarhere] + +- Added homebrew zlib include directory #4842 + [radarhere] + +- Corrected inverted PDF CMYK colors #4866 + [radarhere] + +- Do not try to close file pointer if file pointer is empty #4823 + [radarhere] + +- ImageOps.autocontrast: add mask parameter #4843 + [navneeth, hugovk] + +- Read EXIF data tEXt chunk into info as bytes instead of string #4828 + [radarhere] + +- Replaced distutils with setuptools #4797, #4809, #4814, #4817, #4829, #4890 + [hugovk, radarhere] + +- Add MIME type to PsdImagePlugin #4788 + [samamorgan] + +- Allow ImageOps.autocontrast to specify low and high cutoffs separately #4749 + [millionhz, radarhere] + +7.2.0 (2020-07-01) +------------------ + +- Do not convert I;16 images when showing PNGs #4744 + [radarhere] + +- Fixed ICNS file pointer saving #4741 + [radarhere] + +- Fixed loading non-RGBA mode APNGs with dispose background #4742 + [radarhere] + +- Deprecated _showxv #4714 + [radarhere] + +- Deprecate Image.show(command="...") #4646 + [nulano, hugovk, radarhere] + +- Updated JPEG magic number #4707 + [Cykooz, radarhere] + +- Change STRIPBYTECOUNTS to LONG if necessary when saving #4626 + [radarhere, hugovk] + +- Write JFIF header when saving JPEG #4639 + [radarhere] + +- Replaced tiff_jpeg with jpeg compression when saving TIFF images #4627 + [radarhere] + +- Writing TIFF tags: improved BYTE, added UNDEFINED #4605 + [radarhere] + +- Consider transparency when pasting text on an RGBA image #4566 + [radarhere] + +- Added method argument to single frame WebP saving #4547 + [radarhere] + +- Use ImageFileDirectory_v2 in Image.Exif #4637 + [radarhere] + +- Corrected reading EXIF metadata without prefix #4677 + [radarhere] + +- Fixed drawing a jointed line with a sequence of numeric values #4580 + [radarhere] + +- Added support for 1-D NumPy arrays #4608 + [radarhere] + +- Parse orientation from XMP tags #4560 + [radarhere] + +- Speed up text layout by not rendering glyphs #4652 + [nulano] + +- Fixed ZeroDivisionError in Image.thumbnail #4625 + [radarhere] + +- Replaced TiffImagePlugin DEBUG with logging #4550 + [radarhere] + +- Fix repeatedly loading .gbr #4620 + [ElinksFr, radarhere] + +- JPEG: Truncate icclist instead of setting to None #4613 + [homm] + +- Fixes default offset for Exif #4594 + [rodrigob, radarhere] + +- Fixed bug when unpickling TIFF images #4565 + [radarhere] + +- Fix pickling WebP #4561 + [hugovk, radarhere] + +- Replace IOError and WindowsError aliases with OSError #4536 + [hugovk, radarhere] + +7.1.2 (2020-04-25) +------------------ + +- Raise an EOFError when seeking too far in PNG #4528 + [radarhere] + +7.1.1 (2020-04-02) +------------------ + +- Fix regression seeking and telling PNGs #4512 #4514 + [hugovk, radarhere] + +7.1.0 (2020-04-01) +------------------ + +- Fix multiple OOB reads in FLI decoding #4503 + [wiredfool] + +- Fix buffer overflow in SGI-RLE decoding #4504 + [wiredfool, hugovk] + +- Fix bounds overflow in JPEG 2000 decoding #4505 + [wiredfool] + +- Fix bounds overflow in PCX decoding #4506 + [wiredfool] + +- Fix 2 buffer overflows in TIFF decoding #4507 + [wiredfool] + +- Add APNG support #4243 + [pmrowla, radarhere, hugovk] + +- ImageGrab.grab() for Linux with XCB #4260 + [nulano, radarhere] + +- Added three new channel operations #4230 + [dwastberg, radarhere] + +- Prevent masking of Image reduce method in Jpeg2KImagePlugin #4474 + [radarhere, homm] + +- Added reading of earlier ImageMagick PNG EXIF data #4471 + [radarhere] + +- Fixed endian handling for I;16 getextrema #4457 + [radarhere] + +- Release buffer if function returns prematurely #4381 + [radarhere] + +- Add JPEG comment to info dictionary #4455 + [radarhere] + +- Fix size calculation of Image.thumbnail() #4404 + [orlnub123] + +- Fixed stroke on FreeType < 2.9 #4401 + [radarhere] + +- If present, only use alpha channel for bounding box #4454 + [radarhere] + +- Warn if an unknown feature is passed to features.check() #4438 + [jdufresne] + +- Fix Name field length when saving IM images #4424 + [hugovk, radarhere] + +- Allow saving of zero quality JPEG images #4440 + [radarhere] + +- Allow explicit zero width to hide outline #4334 + [radarhere] + +- Change ContainerIO return type to match file object mode #4297 + [jdufresne, radarhere] + +- Only draw each polygon pixel once #4333 + [radarhere] + +- Add support for shooting situation Exif IFD tags #4398 + [alexagv] + +- Handle multiple and malformed JPEG APP13 markers #4370 + [homm] + +- Depends: Update libwebp to 1.1.0 #4342, libjpeg to 9d #4352 + [radarhere] + +7.0.0 (2020-01-02) +------------------ + +- Drop support for EOL Python 2.7 #4109 + [hugovk, radarhere, jdufresne] + +- Fix rounding error on RGB to L conversion #4320 + [homm] + +- Exif writing fixes: Rational boundaries and signed/unsigned types #3980 + [kkopachev, radarhere] + +- Allow loading of WMF images at a given DPI #4311 + [radarhere] + +- Added reduce operation #4251 + [homm] + +- Raise ValueError for io.StringIO in Image.open #4302 + [radarhere, hugovk] + +- Fix thumbnail geometry when DCT scaling is used #4231 + [homm, radarhere] + +- Use default DPI when exif provides invalid x_resolution #4147 + [beipang2, radarhere] + +- Change default resize resampling filter from NEAREST to BICUBIC #4255 + [homm] + +- Fixed black lines on upscaled images with the BOX filter #4278 + [homm] + +- Better thumbnail aspect ratio preservation #4256 + [homm] + +- Add La mode packing and unpacking #4248 + [homm] + +- Include tests in coverage reports #4173 + [hugovk] + +- Handle broken Photoshop data #4239 + [radarhere] + +- Raise a specific exception if no data is found for an MPO frame #4240 + [radarhere] + +- Fix Unicode support for PyPy #4145 + [nulano] + +- Added UnidentifiedImageError #4182 + [radarhere, hugovk] + +- Remove deprecated __version__ from plugins #4197 + [hugovk, radarhere] + +- Fixed freeing unallocated pointer when resizing with height too large #4116 + [radarhere] + +- Copy info in Image.transform #4128 + [radarhere] + +- Corrected DdsImagePlugin setting info gamma #4171 + [radarhere] + +- Depends: Update libtiff to 4.1.0 #4195, Tk Tcl to 8.6.10 #4229, libimagequant to 2.12.6 #4318 + [radarhere] + +- Improve handling of file resources #3577 + [jdufresne] + +- Removed CI testing of Fedora 29 #4165 + [hugovk] + +- Added pypy3 to tox envlist #4137 + [jdufresne] + +- Drop support for EOL PyQt4 and PySide #4108 + [hugovk, radarhere] + +- Removed deprecated setting of TIFF image sizes #4114 + [radarhere] + +- Removed deprecated PILLOW_VERSION #4107 + [hugovk] + +- Changed default frombuffer raw decoder args #1730 + [radarhere] + +6.2.2 (2020-01-02) +------------------ + +- This is the last Pillow release to support Python 2.7 #3642 + +- Overflow checks for realloc for tiff decoding. CVE-2020-5310 + [wiredfool, radarhere] + +- Catch SGI buffer overrun. CVE-2020-5311 + [radarhere] + +- Catch PCX P mode buffer overrun. CVE-2020-5312 + [radarhere] + +- Catch FLI buffer overrun. CVE-2020-5313 + [radarhere] + +- Raise an error for an invalid number of bands in FPX image. CVE-2019-19911 + [wiredfool, radarhere] + +6.2.1 (2019-10-21) +------------------ + +- Add support for Python 3.8 #4141 + [hugovk] + +6.2.0 (2019-10-01) +------------------ + +- Catch buffer overruns #4104 + [radarhere] + +- Initialize rows_per_strip when RowsPerStrip tag is missing #4034 + [cgohlke, radarhere] + +- Raise error if TIFF dimension is a string #4103 + [radarhere] + +- Added decompression bomb checks #4102 + [radarhere] + +- Fix ImageGrab.grab DPI scaling on Windows 10 version 1607+ #4000 + [nulano, radarhere] + +- Corrected negative seeks #4101 + [radarhere] + +- Added argument to capture all screens on Windows #3950 + [nulano, radarhere] + +- Updated warning to specify when Image.frombuffer defaults will change #4086 + [radarhere] + +- Changed WindowsViewer format to PNG #4080 + [radarhere] + +- Use TIFF orientation #4063 + [radarhere] + +- Raise the same error if a truncated image is loaded a second time #3965 + [radarhere] + +- Lazily use ImageFileDirectory_v1 values from Exif #4031 + [radarhere] + +- Improved HSV conversion #4004 + [radarhere] + +- Added text stroking #3978 + [radarhere, hugovk] + +- No more deprecated bdist_wininst .exe installers #4029 + [hugovk] + +- Do not allow floodfill to extend into negative coordinates #4017 + [radarhere] + +- Fixed arc drawing bug for a non-whole number of degrees #4014 + [radarhere] + +- Fix bug when merging identical images to GIF with a list of durations #4003 + [djy0, radarhere] + +- Fix bug in TIFF loading of BufferedReader #3998 + [chadawagner] + +- Added fallback for finding ld on MinGW Cygwin #4019 + [radarhere] + +- Remove indirect dependencies from requirements.txt #3976 + [hugovk] + +- Depends: Update libwebp to 1.0.3 #3983, libimagequant to 2.12.5 #3993, freetype to 2.10.1 #3991 + [radarhere] + +- Change overflow check to use PY_SSIZE_T_MAX #3964 + [radarhere] + +- Report reason for pytest skips #3942 + [hugovk] + +6.1.0 (2019-07-01) +------------------ + +- Deprecate Image.__del__ #3929 + [jdufresne] + +- Tiff: Add support for JPEG quality #3886 + [olt] + +- Respect the PKG_CONFIG environment variable when building #3928 + [chewi] + +- Use explicit memcpy() to avoid unaligned memory accesses #3225 + [DerDakon] + +- Improve encoding of TIFF tags #3861 + [olt] + +- Update Py_UNICODE to Py_UCS4 #3780 + [nulano] + +- Consider I;16 pixel size when drawing #3899 + [radarhere] + +- Add TIFFTAG_SAMPLEFORMAT to blocklist #3926 + [cgohlke, radarhere] + +- Create GIF deltas from background colour of GIF frames if disposal mode is 2 #3708 + [sircinnamon, radarhere] + +- Added ImageSequence all_frames #3778 + [radarhere] + +- Use unsigned int to store TIFF IFD offsets #3923 + [cgohlke] + +- Include CPPFLAGS when searching for libraries #3819 + [jefferyto] + +- Updated TIFF tile descriptors to match current decoding functionality #3795 + [dmnisson] + +- Added an ``image.entropy()`` method (second revision) #3608 + [fish2000] + +- Pass the correct types to PyArg_ParseTuple #3880 + [QuLogic] + +- Fixed crash when loading non-font bytes #3912 + [radarhere] + +- Fix SPARC memory alignment issues in Pack/Unpack functions #3858 + [kulikjak] + +- Added CMYK;16B and CMYK;16N unpackers #3913 + [radarhere] + +- Fixed bugs in calculating text size #3864 + [radarhere] + +- Add __main__.py to output basic format and support information #3870 + [jdufresne] + +- Added variation font support #3802 + [radarhere] + +- Do not down-convert if image is LA when showing with PNG format #3869 + [radarhere] + +- Improve handling of PSD frames #3759 + [radarhere] + +- Improved ICO and ICNS loading #3897 + [radarhere] + +- Changed Preview application path so that it is no longer static #3896 + [radarhere] + +- Corrected ttb text positioning #3856 + [radarhere] + +- Handle unexpected ICO image sizes #3836 + [radarhere] + +- Fixed bits value for RGB;16N unpackers #3837 + [kkopachev] + +- Travis CI: Add Fedora 30, remove Fedora 28 #3821 + [hugovk] + +- Added reading of CMYK;16L TIFF images #3817 + [radarhere] + +- Fixed dimensions of 1-bit PDFs #3827 + [radarhere] + +- Fixed opening mmap image through Path on Windows #3825 + [radarhere] + +- Fixed ImageDraw arc gaps #3824 + [radarhere] + +- Expand GIF to include frames with extents outside the image size #3822 + [radarhere] + +- Fixed ImageTk getimage #3814 + [radarhere] + +- Fixed bug in decoding large images #3791 + [radarhere] + +- Fixed reading APP13 marker without Photoshop data #3771 + [radarhere] + +- Added option to include layered windows in ImageGrab.grab on Windows #3808 + [radarhere] + +- Detect libimagequant when installed by pacman on MingW #3812 + [radarhere] + +- Fixed raqm layout bug #3787 + [radarhere] + +- Fixed loading font with non-Unicode path on Windows #3785 + [radarhere] + +- Travis CI: Upgrade PyPy from 6.0.0 to 7.1.1 #3783 + [hugovk, johnthagen] + +- Depends: Updated openjpeg to 2.3.1 #3794, raqm to 0.7.0 #3877, libimagequant to 2.12.3 #3889 + [radarhere] + +- Fix numpy bool bug #3790 + [radarhere] + +6.0.0 (2019-04-01) +------------------ + +- Python 2.7 support will be removed in Pillow 7.0.0 #3682 + [hugovk] + +- Add EXIF class #3625 + [radarhere] + +- Add ImageOps exif_transpose method #3687 + [radarhere] + +- Added warnings to deprecated CMSProfile attributes #3615 + [hugovk] + +- Documented reading TIFF multiframe images #3720 + [akuchling] + +- Improved speed of opening an MPO file #3658 + [Glandos] + +- Update palette in quantize #3721 + [radarhere] + +- Improvements to TIFF is_animated and n_frames #3714 + [radarhere] + +- Fixed incompatible pointer type warnings #3754 + [radarhere] + +- Improvements to PA and LA conversion and palette operations #3728 + [radarhere] + +- Consistent DPI rounding #3709 + [radarhere] + +- Change size of MPO image to match frame #3588 + [radarhere] + +- Read Photoshop resolution data #3701 + [radarhere] + +- Ensure image is mutable before saving #3724 + [radarhere] + +- Correct remap_palette documentation #3740 + [radarhere] + +- Promote P images to PA in putalpha #3726 + [radarhere] + +- Allow RGB and RGBA values for new P images #3719 + [radarhere] + +- Fixed TIFF bug when seeking backwards and then forwards #3713 + [radarhere] + +- Cache EXIF information #3498 + [Glandos] + +- Added transparency for all PNG greyscale modes #3744 + [radarhere] + +- Fix deprecation warnings in Python 3.8 #3749 + [radarhere] + +- Fixed GIF bug when rewinding to a non-zero frame #3716 + [radarhere] + +- Only close original fp in __del__ and __exit__ if original fp is exclusive #3683 + [radarhere] + +- Fix BytesWarning in Tests/test_numpy.py #3725 + [jdufresne] + +- Add missing MIME types and extensions #3520 + [pirate486743186] + +- Add I;16 PNG save #3566 + [radarhere] + +- Add support for BMP RGBA bitfield compression #3705 + [radarhere] + +- Added ability to set language for text rendering #3693 + [iwsfutcmd] + +- Only close exclusive fp on Image __exit__ #3698 + [radarhere] + +- Changed EPS subprocess stdout from devnull to None #3635 + [radarhere] + +- Add reading old-JPEG compressed TIFFs #3489 + [kkopachev] + +- Add EXIF support for PNG #3674 + [radarhere] + +- Add option to set dither param on quantize #3699 + [glasnt] + +- Add reading of DDS uncompressed RGB data #3673 + [radarhere] + +- Correct length of Tiff BYTE tags #3672 + [radarhere] + +- Add DIB saving and loading through Image open #3691 + [radarhere] + +- Removed deprecated VERSION #3624 + [hugovk] + +- Fix 'BytesWarning: Comparison between bytes and string' in PdfDict #3580 + [jdufresne] + +- Do not resize in Image.thumbnail if already the destination size #3632 + [radarhere] + +- Replace .seek() magic numbers with io.SEEK_* constants #3572 + [jdufresne] + +- Make ContainerIO.isatty() return a bool, not int #3568 + [jdufresne] + +- Add support to all transpose operations for I;16 modes #3563, #3741 + [radarhere] + +- Deprecate support for PyQt4 and PySide #3655 + [hugovk, radarhere] + +- Add TIFF compression codecs: LZMA, Zstd, WebP #3555 + [cgohlke] + +- Fixed pickling of iTXt class with protocol > 1 #3537 + [radarhere] + +- _util.isPath returns True for pathlib.Path objects #3616 + [wbadart] + +- Remove unnecessary unittest.main() boilerplate from test files #3631 + [jdufresne] + +- Exif: Seek to IFD offset #3584 + [radarhere] + +- Deprecate PIL.*ImagePlugin.__version__ attributes #3628 + [jdufresne] + +- Docs: Add note about ImageDraw operations that exceed image bounds #3620 + [radarhere] + +- Allow for unknown PNG chunks after image data #3558 + [radarhere] + +- Changed EPS subprocess stdin from devnull to None #3611 + [radarhere] + +- Fix possible integer overflow #3609 + [cgohlke] + +- Catch BaseException for resource cleanup handlers #3574 + [jdufresne] + +- Improve pytest configuration to allow specific tests as CLI args #3579 + [jdufresne] + +- Drop support for Python 3.4 #3596 + [hugovk] + +- Remove deprecated PIL.OleFileIO #3598 + [hugovk] + +- Remove deprecated ImageOps undocumented functions #3599 + [hugovk] + +- Depends: Update libwebp to 1.0.2 #3602 + [radarhere] + +- Detect MIME types #3525 + [radarhere] + +5.4.1 (2019-01-06) +------------------ + +- File closing: Only close __fp if not fp #3540 + [radarhere] + +- Fix build for Termux #3529 + [pslacerda] + +- PNG: Detect MIME types #3525 + [radarhere] + +- PNG: Handle IDAT chunks after image end #3532 + [radarhere] + +5.4.0 (2019-01-01) +------------------ + +- Docs: Improved ImageChops documentation #3522 + [radarhere] + +- Allow RGB and RGBA values for P image putpixel #3519 + [radarhere] + +- Add APNG extension to PNG plugin #3501 + [pirate486743186, radarhere] + +- Lookup ld.so.cache instead of hardcoding search paths #3245 + [pslacerda] + +- Added custom string TIFF tags #3513 + [radarhere] + +- Improve setup.py configuration #3395 + [diorcety] + +- Read textual chunks located after IDAT chunks for PNG #3506 + [radarhere] + +- Performance: Don't try to hash value if enum is empty #3503 + [Glandos] + +- Added custom int and float TIFF tags #3350 + [radarhere] + +- Fixes for issues reported by static code analysis #3393 + [frenzymadness] + +- GIF: Wait until mode is normalized to copy im.info into encoderinfo #3187 + [radarhere] + +- Docs: Add page of deprecations and removals #3486 + [hugovk] + +- Travis CI: Upgrade PyPy from 5.8.0 to 6.0 #3488 + [hugovk] + +- Travis CI: Allow lint job to fail #3467 + [hugovk] + +- Resolve __fp when closing and deleting #3261 + [radarhere] + +- Close exclusive fp before discarding #3461 + [radarhere] + +- Updated open files documentation #3490 + [radarhere] + +- Added libjpeg_turbo to check_feature #3493 + [radarhere] + +- Change color table index background to tuple when saving as WebP #3471 + [radarhere] + +- Allow arbitrary number of comment extension subblocks #3479 + [radarhere] + +- Ensure previous FLI frame is loaded before seeking to the next #3478 + [radarhere] + +- ImageShow improvements #3450 + [radarhere] + +- Depends: Update libimagequant to 2.12.2 #3442, libtiff to 4.0.10 #3458, libwebp to 1.0.1 #3468, Tk Tcl to 8.6.9 #3465 + [radarhere] + +- Check quality_layers type #3464 + [radarhere] + +- Add context manager, __del__ and close methods to TarIO #3455 + [radarhere] + +- Test: Do not play sound when running screencapture command #3454 + [radarhere] + +- Close exclusive fp on open exception #3456 + [radarhere] + +- Only close existing fp in WebP if fp is exclusive #3418 + [radarhere] + +- Docs: Re-add the downloads badge #3443 + [hugovk] + +- Added negative index to PixelAccess #3406 + [Nazime] + +- Change tuple background to global color table index when saving as GIF #3385 + [radarhere] + +- Test: Improved ImageGrab tests #3424 + [radarhere] + +- Flake8 fixes #3422, #3440 + [radarhere, hugovk] + +- Only ask for YCbCr->RGB libtiff conversion for jpeg-compressed tiffs #3417 + [kkopachev] + +- Optimise ImageOps.fit by combining resize and crop #3409 + [homm] + +5.3.0 (2018-10-01) +------------------ + +- Changed Image size property to be read-only by default #3203 + [radarhere] + +- Add warnings if image file identification fails due to lack of WebP support #3169 + [radarhere, hugovk] + +- Hide the Ghostscript progress dialog popup on Windows #3378 + [hugovk] + +- Adding support to reading tiled and YcbCr jpeg tiffs through libtiff #3227 + [kkopachev] + +- Fixed None as TIFF compression argument #3310 + [radarhere] + +- Changed GIF seek to remove previous info items #3324 + [radarhere] + +- Improved PDF document info #3274 + [radarhere] + +- Add line width parameter to rectangle and ellipse-based shapes #3094 + [hugovk, radarhere] + +- Fixed decompression bomb check in _crop #3313 + [dinkolubina, hugovk] + +- Added support to ImageDraw.floodfill for non-RGB colors #3377 + [radarhere] + +- Tests: Avoid catching unexpected exceptions in tests #2203 + [jdufresne] + +- Use TextIOWrapper.detach() instead of NoCloseStream #2214 + [jdufresne] + +- Added transparency to matrix conversion #3205 + [radarhere] + +- Added ImageOps pad method #3364 + [radarhere] + +- Give correct extrema for I;16 format images #3359 + [bz2] + +- Added PySide2 #3279 + [radarhere] + +- Corrected TIFF tags #3369 + [radarhere] + +- CI: Install CFFI and pycparser without any PYTHONOPTIMIZE #3374 + [hugovk] + +- Read/Save RGB webp as RGB (instead of RGBX) #3298 + [kkopachev] + +- ImageDraw: Add line joints #3250 + [radarhere] + +- Improved performance of ImageDraw floodfill method #3294 + [yo1995] + +- Fix builds with --parallel #3272 + [hsoft] + +- Add more raw Tiff modes (RGBaX, RGBaXX, RGBAX, RGBAXX) #3335 + [homm] + +- Close existing WebP fp before setting new fp #3341 + [radarhere] + +- Add orientation, compression and id_section as TGA save keyword arguments #3327 + [radarhere] + +- Convert int values of RATIONAL TIFF tags to floats #3338 + [radarhere, wiredfool] + +- Fix code for PYTHONOPTIMIZE #3233 + [hugovk] + +- Changed ImageFilter.Kernel to subclass ImageFilter.BuiltinFilter, instead of the other way around #3273 + [radarhere] + +- Remove unused draw.draw_line, draw.draw_point and font.getabc methods #3232 + [hugovk] + +- Tests: Added ImageFilter tests #3295 + [radarhere] + +- Tests: Added ImageChops tests #3230 + [hugovk, radarhere] + +- AppVeyor: Download lib if not present in pillow-depends #3316 + [radarhere] + +- Travis CI: Add Python 3.7 and Xenial #3234 + [hugovk] + +- Docs: Added documentation for NumPy conversion #3301 + [radarhere] + +- Depends: Update libimagequant to 2.12.1 #3281 + [radarhere] + +- Add three-color support to ImageOps.colorize #3242 + [tsennott] + +- Tests: Add LA to TGA test modes #3222 + [danpla] + +- Skip outline if the draw operation fills with the same colour #2922 + [radarhere] + +- Flake8 fixes #3173, #3380 + [radarhere] + +- Avoid deprecated 'U' mode when opening files #2187 + [jdufresne] + +5.2.0 (2018-07-01) +------------------ + +- Fixed saving a multiframe image as a single frame PDF #3137 + [radarhere] + +- If a Qt version is already imported, attempt to use it first #3143 + [radarhere] + +- Fix transform fill color for alpha images #3147 + [fozcode] + +- TGA: Add support for writing RLE data #3186 + [danpla] + +- TGA: Read and write LA data #3178 + [danpla] + +- QuantOctree.c: Remove erroneous attempt to average over an empty range #3196 + [tkoeppe] + +- Changed ICNS format tests to pass on OS X 10.11 #3202 + [radarhere] + +- Fixed bug in ImageDraw.multiline_textsize() #3114 + [tianyu139] + +- Added getsize_multiline support for PIL.ImageFont #3113 + [tianyu139] + +- Added ImageFile get_format_mimetype method #3190 + [radarhere] + +- Changed mmap file pointer to use context manager #3216 + [radarhere] + +- Changed ellipse point calculations to be more evenly distributed #3142 + [radarhere] + +- Only extract first Exif segment #2946 + [hugovk] + +- Tests: Test ImageDraw2, WalImageFile #3135, #2989 + [hugovk] + +- Remove unnecessary '#if 0' code #3075 + [hugovk] + +- Tests: Added GD tests #1817 + [radarhere] + +- Fix collections ABCs DeprecationWarning in Python 3.7 #3123 + [hugovk] + +- unpack_from is faster than unpack of slice #3201 + [landfillbaby] + +- Docs: Add coordinate system links and file handling links in documentation #3204, #3214 + [radarhere] + +- Tests: TestFilePng: Fix test_save_l_transparency() #3182 + [danpla] + +- Docs: Correct argument name #3171 + [radarhere] + +- Docs: Update CMake download URL #3166 + [radarhere] + +- Docs: Improve Image.transform documentation #3164 + [radarhere] + +- Fix transform fillcolor argument when image mode is RGBA or LA #3163 + [radarhere] + +- Tests: More specific Exception testing #3158 + [radarhere] + +- Add getrgb HSB/HSV color strings #3148 + [radarhere] + +- Allow float values in getrgb HSL color string #3146 + [radarhere] + +- AppVeyor: Upgrade to Python 2.7.15 and 3.4.4 #3140 + [radarhere] + +- AppVeyor: Upgrade to PyPy 6.0.0 #3133 + [hugovk] + +- Deprecate PILLOW_VERSION and VERSION #3090 + [hugovk] + +- Support Python 3.7 #3076 + [hugovk] + +- Depends: Update freetype to 2.9.1, libjpeg to 9c, libwebp to 1.0.0 #3121, #3136, #3108 + [radarhere] + +- Build macOS wheels with Xcode 6.4, supporting older macOS versions #3068 + [wiredfool] + +- Fix _i2f compilation on some GCC versions #3067 + [homm] + +- Changed encoderinfo to have priority over info when saving GIF images #3086 + [radarhere] + +- Rename PIL.version to PIL._version and remove it from module #3083 + [homm] + +- Enable background colour parameter on rotate #3057 + [storesource] + +- Remove unnecessary ``#if 1`` directive #3072 + [jdufresne] + +- Remove unused Python class, Path #3070 + [jdufresne] + +- Fix dereferencing type-punned pointer will break strict-aliasing #3069 + [jdufresne] + +5.1.0 (2018-04-02) +------------------ + +- Close fp before return in ImagingSavePPM #3061 + [kathryndavies] + +- Added documentation for ICNS append_images #3051 + [radarhere] + +- Docs: Move intro text below its header #3021 + [hugovk] + +- CI: Rename appveyor.yml as .appveyor.yml #2978 + [hugovk] + +- Fix TypeError for JPEG2000 parser feed #3042 + [hugovk] + +- Certain corrupted jpegs can result in no data read #3023 + [kkopachev] + +- Add support for BLP file format #3007 + [jleclanche] + +- Simplify version checks #2998 + [hugovk] + +- Fix "invalid escape sequence" warning on Python 3.6+ #2996 + [timgraham] + +- Allow append_images to set .icns scaled images #3005 + [radarhere] + +- Support appending to existing PDFs #2965 + [vashek] + +- Fix and improve efficient saving of ICNS on macOS #3004 + [radarhere] + +- Build: Enable pip cache in AppVeyor build #3009 + [thijstriemstra] + +- Trim trailing whitespace #2985 + [Metallicow] + +- Docs: Correct reference to Image.new method #3000 + [radarhere] + +- Rearrange ImageFilter classes into alphabetical order #2990 + [radarhere] + +- Test: Remove duplicate line #2983 + [radarhere] + +- Build: Update AppVeyor PyPy version #3003 + [radarhere] + +- Tiff: Open 8 bit Tiffs with 5 or 6 channels, discarding extra channels #2938 + [homm] + +- Readme: Added Twitter badge #2930 + [hugovk] + +- Removed __main__ code from ImageCms #2942 + [radarhere] + +- Test: Changed assert statements to unittest calls #2961 + [radarhere] + +- Depends: Update libimagequant to 2.11.10, raqm to 0.5.0, freetype to 2.9 #3036, #3017, #2957 + [radarhere] + +- Remove _imaging.crc32 in favor of builtin Python crc32 implementation #2935 + [wiredfool] + +- Move Tk directory to src directory #2928 + [hugovk] + +- Enable pip cache in Travis CI #2933 + [jdufresne] + +- Remove unused and duplicate imports #2927 + [radarhere] + +- Docs: Changed documentation references to 2.x to 2.7 #2921 + [radarhere] + +- Fix memory leak when opening webp files #2974 + [wiredfool] + +- Setup: Fix "TypeError: 'NoneType' object is not iterable" for PPC and CRUX #2951 + [hugovk] + +- Setup: Add libdirs for ppc64le and armv7l #2968 + [nehaljwani] + 5.0.0 (2018-01-01) ------------------ @@ -16,13 +2349,13 @@ Changelog (Pillow) - Dynamically link libraqm #2753 [wiredfool] - + - Removed scripts directory #2901 [wiredfool] - + - TIFF: Run all compressed tiffs through libtiff decoder #2899 [wiredfool] - + - GIF: Add disposal option when saving GIFs #2902 [linnil1, wiredfool] @@ -39,7 +2372,7 @@ Changelog (Pillow) [wiredfool] - Test: avoid random failure in test_effect_noise #2894 - [hugovk] + [hugovk] - Increased epsilon for test_file_eps.py:test_showpage due to Arch update. #2896 [wiredfool] @@ -83,7 +2416,7 @@ Changelog (Pillow) - Add eog support for Ubuntu Image Viewer #2864 [NafisFaysal] -- Test: Test on 3.7-dev on Travis.ci #2870 +- Test: Test on 3.7-dev on Travis CI #2870 [hugovk] - Dependencies: Update libtiff to 4.0.9 #2871 @@ -122,7 +2455,7 @@ Changelog (Pillow) - GIF: Permit LZW code lengths up to 12 bits in GIF decode #2813 [wiredfool] -- Fix unterminiated string and unchecked exception in _font_text_asBytes. #2825 +- Fix unterminated string and unchecked exception in _font_text_asBytes. #2825 [wiredfool] - PPM: Use fixed list of whitespace, rather relying on locale, fixes #272. #2831 @@ -212,7 +2545,7 @@ Changelog (Pillow) - Fixed doc syntax in ImageDraw #2752 [radarhere] -- Fixed support for building on Windows/msys2. Added Appveyor CI coverage for python3 on msys2 #2476 +- Fixed support for building on Windows/msys2. Added Appveyor CI coverage for python3 on msys2 #2746 [wiredfool] - Fix ValueError in Exif/Tiff IFD #2719 @@ -284,7 +2617,7 @@ Changelog (Pillow) - Use RGBX rawmode for RGB JPEG images where possible #1989 [homm] -- Remove palettes from non-palette modes in _new #2702 +- Remove palettes from non-palette modes in _new #2704 [wiredfool] - Delete transparency info when convert'ing RGB/L to RGBA #2633 @@ -404,7 +2737,7 @@ Changelog (Pillow) - Doc: Clarified Image.save:append_images documentation #2604 [radarhere] -- CI: Amazon Linux and Centos6 docker images added to TravisCI #2585 +- CI: Amazon Linux and Centos6 docker images added to Travis CI #2585 [wiredfool] - Image.alpha_composite added #2595 @@ -434,7 +2767,7 @@ Changelog (Pillow) - Add decompression bomb check to Image.crop #2410 [wiredfool] -- ImageFile: Ensure that the `err_code` variable is initialized in case of exception. #2363 +- ImageFile: Ensure that the ``err_code`` variable is initialized in case of exception. #2363 [alexkiro] - Tiff: Support append_images for saving multipage TIFFs #2406 @@ -473,7 +2806,7 @@ Changelog (Pillow) - Update Feature Detection #2520 [wiredfool] -- CI: Update pypy on TravisCI #2573 +- CI: Update pypy on Travis CI #2573 [hugovk] - ImageMorph: Fix wrong expected size of MRLs read from disk #2561 @@ -575,7 +2908,7 @@ Changelog (Pillow) - Doc: Reordered operating systems in Compatibility Matrix #2436 [radarhere] -- Test: Additional tests for BurfStub, Eps, Container, GribStub, IPTC, Wmf, XVThumb, ImageDraw, ImageMorph ImageShow #2425 +- Test: Additional tests for BufrStub, Eps, Container, GribStub, IPTC, Wmf, XVThumb, ImageDraw, ImageMorph, ImageShow #2425 [radarhere] - Health fixes #2437 @@ -671,7 +3004,7 @@ Changelog (Pillow) - Removed PIL 1.0 era TK readme that concerns Windows 95/NT #2360 [wiredfool] -- Prevent `nose -v` printing docstrings #2369 +- Prevent ``nose -v`` printing docstrings #2369 [hugovk] - Replaced absolute PIL imports with relative imports #2349 @@ -707,10 +3040,10 @@ Changelog (Pillow) - Add center and translate option to Image.rotate. #2328 [lambdafu] -- Test: Relax WMF test condition, fixes #2323 +- Test: Relax WMF test condition, fixes #2323. #2327 [wiredfool] -- Allow 0 size images, Fixes #2259, Reverts to pre-3.4 behavior. +- Allow 0 size images, Fixes #2259, Reverts to pre-3.4 behavior. #2262 [wiredfool] - SGI: Save uncompressed SGI/BW/RGB/RGBA files #2325 @@ -760,7 +3093,7 @@ Changelog (Pillow) - Test: Faster assert_image_similar #2279 [homm] -- Removed depreciated internal "stretch" method #2276 +- Removed deprecated internal "stretch" method #2276 [homm] - Removed the handles_eof flag in decode.c #2223 @@ -1041,10 +3374,10 @@ Changelog (Pillow) 3.3.2 (2016-10-03) ------------------ -- Fix negative image sizes in Storage.c #2105 +- Fix negative image sizes in Storage.c #2146 [wiredfool] -- Fix integer overflow in map.c #2105 +- Fix integer overflow in map.c #2146 [wiredfool] 3.3.1 (2016-08-18) @@ -1116,7 +3449,7 @@ Changelog (Pillow) - Changed depends/install_*.sh urls to point to github pillow-depends repo #1983 [wiredfool] -- Allow ICC profile from `encoderinfo` while saving PNGs #1909 +- Allow ICC profile from ``encoderinfo`` while saving PNGs #1909 [homm] - Fix integer overflow on ILP32 systems (32-bit Linux). #1975 @@ -1125,7 +3458,7 @@ Changelog (Pillow) - Change function declaration to match Tcl_CmdProc type #1966 [homm] -- Integer overflow checks on all calls to *alloc #1781 +- Integer overflow checks on all calls to \*alloc #1781 [wiredfool] - Change equals method on Image so it short circuits #1967 @@ -1182,7 +3515,7 @@ Changelog (Pillow) - Skip tests that require libtiff if it is not installed #1893 (fixes #1866) [wiredfool] -- Skip test when icc profile is not available, fixes #1887 +- Skip test when icc profile is not available, fixes #1887. #1892 [doko42] - Make deprecated functions raise NotImplementedError instead of Exception. #1862, #1890 @@ -1559,7 +3892,7 @@ Changelog (Pillow) - Added PDF multipage saving #1445 [radarhere] -- Removed deprecated code, Image.tostring, Image.fromstring, Image.offset, ImageDraw.setink, ImageDraw.setfill, ImageFileIO, ImageFont.FreeTypeFont and ImageFont.truetype `file` kwarg, ImagePalette private _make functions, ImageWin.fromstring and ImageWin.tostring #1343 +- Removed deprecated code, Image.tostring, Image.fromstring, Image.offset, ImageDraw.setink, ImageDraw.setfill, ImageFileIO, ImageFont.FreeTypeFont and ImageFont.truetype ``file`` kwarg, ImagePalette private _make functions, ImageWin.fromstring and ImageWin.tostring #1343 [radarhere] - Load more broken images #1428 @@ -1787,7 +4120,7 @@ Changelog (Pillow) 2.8.1 (2015-04-02) ------------------ -- Bug fix: Catch struct.error on invalid JPEG, fixes #1163 +- Bug fix: Catch struct.error on invalid JPEG, fixes #1163. #1165 [wiredfool, hugovk] 2.8.0 (2015-04-01) @@ -1922,7 +4255,7 @@ Changelog (Pillow) - Updated manifest #957 [wiredfool] -- Fix PyPy 2.4 regression #952 +- Fix PyPy 2.4 regression #958 [wiredfool] - Webp Metadata Skip Test comments #954 @@ -1964,7 +4297,7 @@ Changelog (Pillow) - Use redistributable ICC profiles for testing, skip if not available #923 [wiredfool] -- Additional documentation for JPEG info and save options #890 +- Additional documentation for JPEG info and save options #922 [wiredfool] - Fix JPEG Encoding memory leak when exif or qtables were specified #921 @@ -2051,7 +4384,7 @@ Changelog (Pillow) - Doc cleanup [wiredfool] -- Fix `ImageStat` docs #796 +- Fix ``ImageStat`` docs #796 [akx] - Added docs for ExifTags #794 @@ -2488,7 +4821,7 @@ Changelog (Pillow) - Add RGBA support to ImageColor #309 [yoavweiss] -- Test for `str`, not `"utf-8"` #306 (fixes #304) +- Test for ``str``, not ``"utf-8"`` #306 (fixes #304) [mjpieters] - Fix missing import os in _util.py #303 @@ -2594,7 +4927,7 @@ Changelog (Pillow) - Partial work to add a wrapper for WebPGetFeatures to correctly support #220 (fixes #204) -- Significant performance improvement of `alpha_composite` function #156 +- Significant performance improvement of ``alpha_composite`` function #156 [homm] - Support explicitly disabling features via --disable-* options #240 @@ -2756,15 +5089,15 @@ Changelog (Pillow) 1.0 (07/30/2010) ---------------- -- Remove support for ``import Image``, etc. from the standard namespace. ``from PIL import Image`` etc. now required. -- Forked PIL based on `Hanno Schlichting's re-packaging `_ +- Remove support for ``import Image``. ``from PIL import Image`` now required. +- Forked PIL based on `Chris McDonough and Hanno Schlichting's setuptools compatible re-packaging `_ [aclark4life] Pre-fork --------- +======== 0.2b5-1.1.7 -+++++++++++ +----------- :: @@ -2798,1740 +5131,1787 @@ Pre-fork Ka-Ping Yee, and many others (if your name should be on this list, let me know.) - *** Changes from release 1.1.6 to 1.1.7 *** - - This section may not be fully complete. For changes since this file - was last updated, see the repository revision history: - - https://bitbucket.org/effbot/pil-2009-raclette/commits/all - - (1.1.7 final) - - + Set GIF loop info property to the number of iterations if a NETSCAPE - loop extension is present, instead of always setting it to 1 (from - Valentino Volonghi). - - (1.1.7c1 released) - - + Improved PNG compression (from Alexey Borzenkov). - - + Read interlaced PNG files (from Conrado Porto Lopes Gouvêa) - - + Added various TGA improvements from Alexey Borzenkov, including - support for specifying image orientation. - - + Bumped block threshold to 16 megabytes, made size estimation a bit - more accurate. This speeds up allocation of large images. - - + Fixed rounding error in ImagingDrawWideLine. - - "gormish" writes: ImagingDrawWideLine() in Draw.c has a bug in every - version I've seen, which leads to different width lines depending on - the order of the points in the line. This is especially bad at some - angles where a 'width=2' line can completely disappear. - - + Added support for RGBA mode to the SGI module (based on code by - Karsten Hiddemann). - - + Handle repeated IPTC tags (adapted from a patch by Eric Bruning). +1.1.6 to 1.1.7 +-------------- - Eric writes: According to the specification, some IPTC tags can be - repeated, e.g., tag 2:25 (keywords). PIL 1.1.6 only retained the last - instance of that tag. Below is a patch to store all tags. If there are - multiple tag instances, they are stored in a (python) list. Single tag - instances remain as strings. +This section may not be fully complete. For changes since this file +was last updated, see the repository revision history: +http://svn.effbot.org/public/pil/ - + Fixed potential crash in ImageFilter for small target images - (reported by Zac Burns and Daniel Fetchinson). +1.1.7 final +----------- - + Use BMP instead of JPEG as temporary show format on Mac OS X. +- Set GIF loop info property to the number of iterations if a NETSCAPE + loop extension is present, instead of always setting it to 1 (from + Valentino Volonghi). - + Fixed putpixel/new for I;16 with colors > 255. +1.1.7c1 +------- - + Added integer power support to ImagingMath. +- Improved PNG compression (from Alexey Borzenkov). - + Added limited support for I;16L mode (explicit little endian). +- Read interlaced PNG files (from Conrado Porto Lopes Gouvêa) - + Moved WMF support into Image.core; enable WMF rendering by default - if renderer is available. +- Added various TGA improvements from Alexey Borzenkov, including + support for specifying image orientation. - + Mark the ARG plugin as obsolete. +- Bumped block threshold to 16 megabytes, made size estimation a bit + more accurate. This speeds up allocation of large images. - + Added version query mechanism to ImageCms and ImageFont, for - debugging. +- Fixed rounding error in ImagingDrawWideLine. - + Added (experimental) ImageCms function for fetching the ICC profile - for the current display (currently Windows only). + "gormish" writes: ImagingDrawWideLine() in Draw.c has a bug in every + version I've seen, which leads to different width lines depending on + the order of the points in the line. This is especially bad at some + angles where a 'width=2' line can completely disappear. - Added HWND/HDC support to ImageCms.get_display_profile(). +- Added support for RGBA mode to the SGI module (based on code by + Karsten Hiddemann). - + Added WMF renderer (Windows only). +- Handle repeated IPTC tags (adapted from a patch by Eric Bruning). - + Added ImagePointHandler and ImageTransformHandler mixins; made - ImageCmsTransform work with im.point. + Eric writes: According to the specification, some IPTC tags can be + repeated, e.g., tag 2:25 (keywords). PIL 1.1.6 only retained the last + instance of that tag. Below is a patch to store all tags. If there are + multiple tag instances, they are stored in a (python) list. Single tag + instances remain as strings. - + Fixed potential endless loop in the XVThumbnail reader (from Nikolai - Ugelvik). +- Fixed potential crash in ImageFilter for small target images + (reported by Zac Burns and Daniel Fetchinson). - + Added Kevin Cazabon's pyCMS package. +- Use BMP instead of JPEG as temporary show format on Mac OS X. - The C code has been moved to _imagingcms.c, the Python interface - module is installed as PIL.ImageCMS. +- Fixed putpixel/new for I;16 with colors > 255. - Added support for in-memory ICC profiles. +- Added integer power support to ImagingMath. - Unified buildTransform and buildTransformFromOpenProfiles. +- Added limited support for I;16L mode (explicit little endian). - The profile can now be either a filename, a profile object, or a - file-like object containing an in-memory profile. +- Moved WMF support into Image.core; enable WMF rendering by default + if renderer is available. - Additional fixes from Florian Böch: +- Mark the ARG plugin as obsolete. - Very nice - it just needs LCMS flags support so we can use black - point compensation and softproofing :) See attached patches. They - also fix a naming issue which could cause confusion - display - profile (ImageCms wording) actually means proof profile (lcms - wording), so I changed variable names and docstrings where - applicable. Patches are tested under Python 2.6. +- Added version query mechanism to ImageCms and ImageFont, for + debugging. - + Improved support for layer names in PSD files (from Sylvain Baubeau) +- Added (experimental) ImageCms function for fetching the ICC profile + for the current display (currently Windows only). - Sylvain writes: I needed to be able to retrieve the names of the - layers in a PSD files. But PsdImagePlugin.py didn't do the job so I - wrote this very small patch. + Added HWND/HDC support to ImageCms.get_display_profile(). - + Improved RGBA support for ImageTk for 8.4 and newer (from Con - Radchenko). +- Added WMF renderer (Windows only). - This replaces the slow run-length based encoding model with true - compositing at the Tk level. +- Added ImagePointHandler and ImageTransformHandler mixins; made + ImageCmsTransform work with im.point. - + Added support for 16- and 32-bit images to McIdas loader. +- Fixed potential endless loop in the XVThumbnail reader (from Nikolai + Ugelvik). - Based on file samples and stand-alone reader code provided by Craig - Swank. +- Added Kevin Cazabon's pyCMS package. - + Added ImagePalette support to putpalette. + The C code has been moved to _imagingcms.c, the Python interface + module is installed as PIL.ImageCMS. - + Fixed problem with incremental parsing of PNG files. + Added support for in-memory ICC profiles. - + Make selftest.py report non-zero status on failure (from Mark - Sienkiewicz) + Unified buildTransform and buildTransformFromOpenProfiles. - + Add big endian save support and multipage infrastructure to the TIFF - writer (from Sebastian Haase). + The profile can now be either a filename, a profile object, or a + file-like object containing an in-memory profile. - + Handle files with GPS IFD but no basic EXIF IFD (reported by Kurt - Schwehr). + Additional fixes from Florian Böch: - + Added zTXT support (from Andrew Kuchling via Lowell Alleman). + Very nice - it just needs LCMS flags support so we can use black + point compensation and softproofing :) See attached patches. They + also fix a naming issue which could cause confusion - display + profile (ImageCms wording) actually means proof profile (lcms + wording), so I changed variable names and docstrings where + applicable. Patches are tested under Python 2.6. - + Fixed potential infinite loop bug in ImageFont (from Guilherme Polo). +- Improved support for layer names in PSD files (from Sylvain Baubeau) - + Added sample ICC profiles (from Kevin Cazabon) + Sylvain writes: I needed to be able to retrieve the names of the + layers in a PSD files. But PsdImagePlugin.py didn't do the job so I + wrote this very small patch. - + Fixed array interface for I, F, and RGBA/RGBX images. +- Improved RGBA support for ImageTk for 8.4 and newer (from Con + Radchenko). - + Added Chroma subsampling support for JPEG (from Justin Huff). + This replaces the slow run-length based encoding model with true + compositing at the Tk level. - Justin writes: Attached is a patch (against PIL 1.1.6) to provide - control over the chroma subsampling done by the JPEG encoder. This - is often useful for reducing compression artifacts around edges of - clipart and text. +- Added support for 16- and 32-bit images to McIdas loader. - + Added USM/Gaussian Blur code from Kevin Cazabon. + Based on file samples and stand-alone reader code provided by Craig + Swank. - + Fixed bug w. uninitialized image data when cropping outside the - source image. +- Added ImagePalette support to putpalette. - + Use ImageShow to implement the Image.show method. +- Fixed problem with incremental parsing of PNG files. - Most notably, this picks the 'display' utility when available. It - also allows application code to register new display utilities via - the ImageShow registry. +- Make selftest.py report non-zero status on failure (from Mark + Sienkiewicz) - + Release the GIL in the PNG compressor (from Michael van Tellingen). +- Add big endian save support and multipage infrastructure to the TIFF + writer (from Sebastian Haase). - + Revised JPEG CMYK handling. +- Handle files with GPS IFD but no basic EXIF IFD (reported by Kurt + Schwehr). - Always assume Adobe behaviour, both when reading and writing (based on - a patch by Kevin Cazabon, and test data by Tim V. and Charlie Clark, and - additional debugging by Michael van Tellingen). +- Added zTXT support (from Andrew Kuchling via Lowell Alleman). - + Support for preserving ICC profiles (by Florian Böch via Tim Hatch). +- Fixed potential infinite loop bug in ImageFont (from Guilherme Polo). - Florian writes: +- Added sample ICC profiles (from Kevin Cazabon) - It's a beta, so still needs some testing, but should allow you to: - - retain embedded ICC profiles when saving from/to JPEG, PNG, TIFF. - Existing code doesn't need to be changed. - - access embedded profiles in JPEG, PNG, PSD, TIFF. +- Fixed array interface for I, F, and RGBA/RGBX images. - It also includes patches for TIFF to retain IPTC, Photoshop and XMP - metadata when saving as TIFF again, read/write TIFF resolution - information correctly, and to correct inverted CMYK JPEG files. +- Added Chroma subsampling support for JPEG (from Justin Huff). - + Fixed potential memory leak in median cut quantizer (from Evgeny Salmin). + Justin writes: Attached is a patch (against PIL 1.1.6) to provide + control over the chroma subsampling done by the JPEG encoder. This + is often useful for reducing compression artifacts around edges of + clipart and text. - + Fixed OverflowError when reading upside-down BMP images. +- Added USM/Gaussian Blur code from Kevin Cazabon. - + Added resolution save option for PDF files. +- Fixed bug w. uninitialized image data when cropping outside the + source image. - Andreas Kostyrka writes: I've included a patched PdfImagePlugin.py - based on 1.1.6 as included in Ubuntu, that supports a "resolution" - save option. Not great, but it makes the PDF saving more useful by - allowing PDFs that are not exactly 72dpi. +- Use ImageShow to implement the Image.show method. - + Look for Tcl/Tk include files in version-specific include directory - (from Encolpe Degoute). + Most notably, this picks the 'display' utility when available. It + also allows application code to register new display utilities via + the ImageShow registry. - + Fixed grayscale rounding error in ImageColor.getcolor (from Tim - Hatch). +- Release the GIL in the PNG compressor (from Michael van Tellingen). - + Fixed calculation of mean value in ImageEnhance.Contrast (reported - by "roop" and Scott David Daniels). +- Revised JPEG CMYK handling. - + Fixed truetype positioning when first character has a negative left - bearing (from Ned Batchelder): + Always assume Adobe behaviour, both when reading and writing (based on + a patch by Kevin Cazabon, and test data by Tim V. and Charlie Clark, and + additional debugging by Michael van Tellingen). - Ned writes: In PIL 1.1.6, ImageDraw.text will position the string - incorrectly if the first character has a negative left bearing. To - see the problem, show a string like "///" in an italic font. The - first slash will be clipped at the left, and the string will be - mis-positioned. +- Support for preserving ICC profiles (by Florian Böch via Tim Hatch). - + Fixed resolution unit bug in tiff reader/writer (based on code by - Florian Höch, Gary Bloom, and others). + Florian writes: - + Added simple transparency support for RGB images (reported by - Sebastian Spaeth). + It's a beta, so still needs some testing, but should allow you to: - + Added support for Unicode filenames in ImageFont.truetype (from Donn - Ingle). + - retain embedded ICC profiles when saving from/to JPEG, PNG, TIFF. + Existing code doesn't need to be changed. + - access embedded profiles in JPEG, PNG, PSD, TIFF. - + Fixed potential crash in ImageFont.getname method (from Donn Ingle). + It also includes patches for TIFF to retain IPTC, Photoshop and XMP + metadata when saving as TIFF again, read/write TIFF resolution + information correctly, and to correct inverted CMYK JPEG files. - + Fixed encoding issue in PIL/WalImageFile (from Santiago M. Mola). +- Fixed potential memory leak in median cut quantizer (from Evgeny Salmin). - *** Changes from release 1.1.5 to 1.1.6 *** +- Fixed OverflowError when reading upside-down BMP images. - (1.1.6 released) +- Added resolution save option for PDF files. - + Fixed some 64-bit compatibility warnings for Python 2.5. + Andreas Kostyrka writes: I've included a patched PdfImagePlugin.py + based on 1.1.6 as included in Ubuntu, that supports a "resolution" + save option. Not great, but it makes the PDF saving more useful by + allowing PDFs that are not exactly 72dpi. - + Added threading support for the Sane driver (from Abel Deuring). +- Look for Tcl/Tk include files in version-specific include directory + (from Encolpe Degoute). - (1.1.6b2 released) +- Fixed grayscale rounding error in ImageColor.getcolor (from Tim + Hatch). - + Added experimental "floodfill" function to the ImageDraw module - (based on code by Eric Raymond). +- Fixed calculation of mean value in ImageEnhance.Contrast (reported + by "roop" and Scott David Daniels). - + The default arguments for "frombuffer" doesn't match "fromstring" - and the documentation; this is a bug, and will most likely be fixed - in a future version. In this release, PIL prints a warning message - instead. To silence the warning, change any calls of the form - "frombuffer(mode, size, data)" to +- Fixed truetype positioning when first character has a negative left + bearing (from Ned Batchelder): - frombuffer(mode, size, data, "raw", mode, 0, 1) + Ned writes: In PIL 1.1.6, ImageDraw.text will position the string + incorrectly if the first character has a negative left bearing. To + see the problem, show a string like "///" in an italic font. The + first slash will be clipped at the left, and the string will be + mis-positioned. - + Added "fromarray" function, which takes an object implementing the - NumPy array interface and creates a PIL Image from it. (from Travis - Oliphant). +- Fixed resolution unit bug in tiff reader/writer (based on code by + Florian Höch, Gary Bloom, and others). - + Added NumPy array interface support (__array_interface__) to the - Image class (based on code by Travis Oliphant). +- Added simple transparency support for RGB images (reported by + Sebastian Spaeth). - This allows you to easily convert between PIL image memories and - NumPy arrays: +- Added support for Unicode filenames in ImageFont.truetype (from Donn + Ingle). - import numpy, Image +- Fixed potential crash in ImageFont.getname method (from Donn Ingle). - im = Image.open('hopper.jpg') +- Fixed encoding issue in PIL/WalImageFile (from Santiago M. Mola). - a = numpy.asarray(im) # a is readonly +1.1.6 +----- - im = Image.fromarray(a) +- Fixed some 64-bit compatibility warnings for Python 2.5. - + Fixed CMYK polarity for JPEG images, by treating all images as - "Adobe CMYK" images. (thanks to Cesare Leonardi and Kevin Cazabon - for samples, debugging, and patches). +- Added threading support for the Sane driver (from Abel Deuring). - (1.1.6b1 released) +1.1.6b2 +------- - + Added 'expand' option to the Image 'rotate' method. If true, the - output image is made large enough to hold the entire rotated image. +- Added experimental "floodfill" function to the ImageDraw module + (based on code by Eric Raymond). - + Changed the ImageDraw 'line' method to always draw the last pixel in - a polyline, independent of line angle. +- The default arguments for "frombuffer" doesn't match "fromstring" + and the documentation; this is a bug, and will most likely be fixed + in a future version. In this release, PIL prints a warning message + instead. To silence the warning, change any calls of the form + "frombuffer(mode, size, data)" to:: - + Fixed bearing calculation and clipping in the ImageFont truetype - renderer; this could lead to clipped text, or crashes in the low- - level _imagingft module. (based on input from Adam Twardoch and - others). + frombuffer(mode, size, data, "raw", mode, 0, 1) - + Added ImageQt wrapper module, for converting PIL Image objects to - QImage objects in an efficient way. +- Added "fromarray" function, which takes an object implementing the + NumPy array interface and creates a PIL Image from it. (from Travis + Oliphant). - + Fixed 'getmodebands' to return the number of bands also for "PA" - and "LA" modes. Added 'getmodebandnames' helper that return the - band names. +- Added NumPy array interface support (__array_interface__) to the + Image class (based on code by Travis Oliphant). - (1.1.6a2 released) + This allows you to easily convert between PIL image memories and + NumPy arrays:: - + Added float/double support to the TIFF loader (from Russell - Nelson). + import numpy, Image + im = Image.open('hopper.jpg') + a = numpy.asarray(im) # a is readonly + im = Image.fromarray(a) - + Fixed broken use of realloc() in path.c (from Jan Matejek) +- Fixed CMYK polarity for JPEG images, by treating all images as + "Adobe CMYK" images. (thanks to Cesare Leonardi and Kevin Cazabon + for samples, debugging, and patches). - + Added save support for Spider images (from William Baxter). +1.1.6b1 +------- - + Fixed broken 'paste' and 'resize' operations in pildriver - (from Bill Janssen). +- Added 'expand' option to the Image 'rotate' method. If true, the + output image is made large enough to hold the entire rotated image. - + Added support for duplex scanning to the Sane interface (Abel - Deuring). +- Changed the ImageDraw 'line' method to always draw the last pixel in + a polyline, independent of line angle. - (1.1.6a1 released) +- Fixed bearing calculation and clipping in the ImageFont truetype + renderer; this could lead to clipped text, or crashes in the low- + level _imagingft module. (based on input from Adam Twardoch and + others). - + Fixed a memory leak in "convert(mode)", when converting from - L to P. +- Added ImageQt wrapper module, for converting PIL Image objects to + QImage objects in an efficient way. - + Added pixel access object. The "load" method now returns a - access object that can be used to directly get and set pixel - values, using ordinary [x, y] notation: +- Fixed 'getmodebands' to return the number of bands also for "PA" + and "LA" modes. Added 'getmodebandnames' helper that return the + band names. - pixel = im.load() - v = pixel[x, y] - pixel[x, y] = v +1.1.6a2 +------- - If you're accessing more than a few pixels, this is a lot - faster than using getpixel/putpixel. +- Added float/double support to the TIFF loader (from Russell + Nelson). - + Fixed building on Cygwin (from Miki Tebeka). +- Fixed broken use of realloc() in path.c (from Jan Matejek) - + Fixed "point(callable)" on unloaded images (reported by Håkan - Karlsson). +- Added save support for Spider images (from William Baxter). - + Fixed size bug in ImageWin.ImageWindow constructor (from Victor - Reijs) +- Fixed broken 'paste' and 'resize' operations in pildriver + (from Bill Janssen). - + Fixed ImageMath float() and int() operations for Python 2.4 - (reported by Don Rozenberg). +- Added support for duplex scanning to the Sane interface (Abel + Deuring). - + Fixed "RuntimeError: encoder error -8 in tostring" problem for - wide "RGB", "I", and "F" images. +1.1.6a1 +------- - + Fixed line width calculation. +- Fixed a memory leak in "convert(mode)", when converting from + L to P. - (1.1.6a0 released) +- Added pixel access object. The "load" method now returns a + access object that can be used to directly get and set pixel + values, using ordinary [x, y] notation:: - + Fixed byte order issue in Image.paste(ink) (from Ka-Ping Yee). + pixel = im.load() + v = pixel[x, y] + pixel[x, y] = v - + Fixed off-by-0.5 errors in the ANTIALIAS code (based on input - from Douglas Bagnall). + If you're accessing more than a few pixels, this is a lot + faster than using getpixel/putpixel. - + Added buffer interface support to the Path constructor. If - a buffer is provided, it is assumed to contain a flat array - of float coordinates (e.g. array.array('f', seq)). +- Fixed building on Cygwin (from Miki Tebeka). - + Added new ImageMath module. +- Fixed "point(callable)" on unloaded images (reported by Håkan + Karlsson). - + Fixed ImageOps.equalize when used with a small number of distinct - values (reported by David Kirtley). +- Fixed size bug in ImageWin.ImageWindow constructor (from Victor + Reijs) - + Fixed potential integer division in PSDraw.image (from Eric Etheridge). +- Fixed ImageMath float() and int() operations for Python 2.4 + (reported by Don Rozenberg). - *** Changes from release 1.1 to 1.1.5 *** +- Fixed "RuntimeError: encoder error -8 in tostring" problem for + wide "RGB", "I", and "F" images. - (1.1.5c2 and 1.1.5 final released) +- Fixed line width calculation. - + Added experimental PERSPECTIVE transform method (from Jeff Breiden- - bach). +1.1.6a0 +------- - (1.1.5c1 released) +- Fixed byte order issue in Image.paste(ink) (from Ka-Ping Yee). - + Make sure "thumbnail" never generates zero-wide or zero-high images - (reported by Gene Skonicki) +- Fixed off-by-0.5 errors in the ANTIALIAS code (based on input + from Douglas Bagnall). - + Fixed a "getcolors" bug that could result in a zero count for some - colors (reported by Richard Oudkerk). +- Added buffer interface support to the Path constructor. If + a buffer is provided, it is assumed to contain a flat array + of float coordinates (e.g. array.array('f', seq)). - + Changed default "convert" palette to avoid "rounding errors" when - round-tripping white source pixels (reported by Henryk Gerlach and - Jeff Epler). +- Added new ImageMath module. - (1.1.5b3 released) +- Fixed ImageOps.equalize when used with a small number of distinct + values (reported by David Kirtley). - + Don't crash in "quantize" method if the number of colors requested - is larger than 256. This release raises a ValueError exception; - future versions may return a mode "RGB" image instead (reported - by Richard Oudkerk). +- Fixed potential integer division in PSDraw.image (from Eric Etheridge). - + Added WBMP read/write support (based on code by Duncan Booth). +1.1.5c2 and 1.1.5 final +----------------------- - (1.1.5b2 released) +- Added experimental PERSPECTIVE transform method (from Jeff Breiden- + bach). - + Added DPI read/write support to the PNG codec. The decoder sets - the info["dpi"] attribute for PNG files with appropriate resolution - settings. The encoder uses the "dpi" option (based on code by Niki - Spahiev). +1.1.5c1 +------- - + Added limited support for "point" mappings from mode "I" to mode "L". - Only 16-bit values are supported (other values are clipped), the lookup - table must contain exactly 65536 entries, and the mode argument must be - set to "L". +- Make sure "thumbnail" never generates zero-wide or zero-high images + (reported by Gene Skonicki) - + Added support for Mac OS X icns files (based on code by Bob Ippolito). +- Fixed a "getcolors" bug that could result in a zero count for some + colors (reported by Richard Oudkerk). - + Added "ModeFilter" support to the ImageFilter module. +- Changed default "convert" palette to avoid "rounding errors" when + round-tripping white source pixels (reported by Henryk Gerlach and + Jeff Epler). - + Added support for Spider images (from William Baxter). See the - comments in PIL/SpiderImagePlugin.py for more information on this - format. +1.1.5b3 +------- - (1.1.5b1 released) +- Don't crash in "quantize" method if the number of colors requested + is larger than 256. This release raises a ValueError exception; + future versions may return a mode "RGB" image instead (reported + by Richard Oudkerk). - + Added new Sane release (from Ralph Heinkel). See the Sane/README - and Sane/CHANGES files for more information. +- Added WBMP read/write support (based on code by Duncan Booth). - + Added experimental PngInfo chunk container to the PngImageFile - module. This can be used to add arbitrary chunks to a PNG file. - Create a PngInfo instance, use "add" or "add_text" to add chunks, - and pass the instance as the "pnginfo" option when saving the - file. +1.1.5b2 +------- - + Added "getpalette" method. This returns the palette as a list, - or None if the image has no palette. To modify the palette, use - "getpalette" to fetch the current palette, modify the list, and - put it back using "putpalette". +- Added DPI read/write support to the PNG codec. The decoder sets + the info["dpi"] attribute for PNG files with appropriate resolution + settings. The encoder uses the "dpi" option (based on code by Niki + Spahiev). - + Added optional flattening to the ImagePath "tolist" method. - tolist() or tolist(0) returns a list of 2-tuples, as before. - tolist(1) returns a flattened list instead. +- Added limited support for "point" mappings from mode "I" to mode "L". + Only 16-bit values are supported (other values are clipped), the lookup + table must contain exactly 65536 entries, and the mode argument must be + set to "L". - (1.1.5a5 released) +- Added support for Mac OS X icns files (based on code by Bob Ippolito). - + Fixed BILINEAR/BICUBIC/ANTIALIAS filtering for mode "LA". +- Added "ModeFilter" support to the ImageFilter module. - + Added "getcolors()" method. This is similar to the existing histo- - gram method, but looks at color values instead of individual layers, - and returns an unsorted list of (count, color) tuples. +- Added support for Spider images (from William Baxter). See the + comments in PIL/SpiderImagePlugin.py for more information on this + format. - By default, the method returns None if finds more than 256 colors. - If you need to look for more colors, you can pass in a limit (this - is used to allocate internal tables, so you probably don't want to - pass in too large values). +1.1.5b1 +------- - + Build improvements: Fixed building under AIX, improved detection of - FreeType2 and Mac OS X framework libraries, and more. Many thanks - to everyone who helped test the new "setup.py" script! +- Added new Sane release (from Ralph Heinkel). See the Sane/README + and Sane/CHANGES files for more information. - (1.1.5a4 released) +- Added experimental PngInfo chunk container to the PngImageFile + module. This can be used to add arbitrary chunks to a PNG file. + Create a PngInfo instance, use "add" or "add_text" to add chunks, + and pass the instance as the "pnginfo" option when saving the + file. - + The "save" method now looks for a file format driver before - creating the file. +- Added "getpalette" method. This returns the palette as a list, + or None if the image has no palette. To modify the palette, use + "getpalette" to fetch the current palette, modify the list, and + put it back using "putpalette". - + Don't use antialiased truetype fonts when drawing in mode "P", "I", - and "F" images. +- Added optional flattening to the ImagePath "tolist" method. + tolist() or tolist(0) returns a list of 2-tuples, as before. + tolist(1) returns a flattened list instead. - + Rewrote the "setup.py" file. The new version scans for available - support libraries, and configures both the libImaging core library - and the bindings in one step. +1.1.5a5 +------- - To use specific versions of the libraries, edit the ROOT variables - in the setup.py file. +- Fixed BILINEAR/BICUBIC/ANTIALIAS filtering for mode "LA". - + Removed threaded "show" viewer; use the old "show" implementation - instead (Windows). +- Added "getcolors()" method. This is similar to the existing histo- + gram method, but looks at color values instead of individual layers, + and returns an unsorted list of (count, color) tuples. - + Added deprecation warnings to Image.offset, ImageDraw.setink, and - ImageDraw.setfill. + By default, the method returns None if finds more than 256 colors. + If you need to look for more colors, you can pass in a limit (this + is used to allocate internal tables, so you probably don't want to + pass in too large values). - + Added width option to ImageDraw.line(). The current implementation - works best for straight lines; it does not support line joins, so - polylines won't look good. +- Build improvements: Fixed building under AIX, improved detection of + FreeType2 and Mac OS X framework libraries, and more. Many thanks + to everyone who helped test the new "setup.py" script! - + ImageDraw.Draw is now a factory function instead of a class. If - you need to create custom draw classes, inherit from the ImageDraw - class. All other code should use the factory function. +1.1.5a4 +------- - + Fixed loading of certain PCX files (problem reported by Greg - Hamilton, who also provided samples). +- The "save" method now looks for a file format driver before + creating the file. - + Changed _imagingft.c to require FreeType 2.1 or newer. The - module can still be built with earlier versions; see comments - in _imagingft.c for details. +- Don't use antialiased truetype fonts when drawing in mode "P", "I", + and "F" images. - (1.1.5a3 released) +- Rewrote the "setup.py" file. The new version scans for available + support libraries, and configures both the libImaging core library + and the bindings in one step. - + Added 'getim' method, which returns a PyCObject wrapping an - Imaging pointer. The description string is set to IMAGING_MAGIC. - See Imaging.h for pointer and string declarations. + To use specific versions of the libraries, edit the ROOT variables + in the setup.py file. - + Fixed reading of TIFF JPEG images (problem reported by Ulrik - Svensson). +- Removed threaded "show" viewer; use the old "show" implementation + instead (Windows). - + Made ImageColor work under Python 1.5.2 +- Added deprecation warnings to Image.offset, ImageDraw.setink, and + ImageDraw.setfill. - + Fixed division by zero "equalize" on very small images (from - Douglas Bagnall). +- Added width option to ImageDraw.line(). The current implementation + works best for straight lines; it does not support line joins, so + polylines won't look good. - (1.1.5a2 released) +- ImageDraw.Draw is now a factory function instead of a class. If + you need to create custom draw classes, inherit from the ImageDraw + class. All other code should use the factory function. - + The "paste" method now supports the alternative "paste(im, mask)" - syntax (in this case, the box defaults to im's bounding box). +- Fixed loading of certain PCX files (problem reported by Greg + Hamilton, who also provided samples). - + The "ImageFile.Parser" class now works also for PNG files with - more than one IDAT block. +- Changed _imagingft.c to require FreeType 2.1 or newer. The + module can still be built with earlier versions; see comments + in _imagingft.c for details. - + Added DPI read/write to the TIFF codec, and fixed writing of - rational values. The decoder sets the info["dpi"] attribute - for TIFF files with appropriate resolution settings. The - encoder uses the "dpi" option. +1.1.5a3 +------- - + Disable interlacing for small (or narrow) GIF images, to - work around what appears to be a hard-to-find bug in PIL's - GIF encoder. +- Added 'getim' method, which returns a PyCObject wrapping an + Imaging pointer. The description string is set to IMAGING_MAGIC. + See Imaging.h for pointer and string declarations. - + Fixed writing of mode "P" PDF images. Made mode "1" PDF - images smaller. +- Fixed reading of TIFF JPEG images (problem reported by Ulrik + Svensson). - + Made the XBM reader a bit more robust; the file may now start - with a few whitespace characters. +- Made ImageColor work under Python 1.5.2 - + Added support for enhanced metafiles to the WMF driver. The - separate PILWMF kit lets you render both placeable WMF files - and EMF files as raster images. See +- Fixed division by zero "equalize" on very small images (from + Douglas Bagnall). - http://effbot.org/downloads#pilwmf +1.1.5a2 +------- - (1.1.5a1 released) +- The "paste" method now supports the alternative "paste(im, mask)" + syntax (in this case, the box defaults to im's bounding box). - + Replaced broken WMF driver with a WMF stub plugin (see below). +- The "ImageFile.Parser" class now works also for PNG files with + more than one IDAT block. - + Fixed writing of mode "1", "L", and "CMYK" PDF images (based on - input from Nicholas Riley and others). +- Added DPI read/write to the TIFF codec, and fixed writing of + rational values. The decoder sets the info["dpi"] attribute + for TIFF files with appropriate resolution settings. The + encoder uses the "dpi" option. - + Fixed adaptive palette conversion for zero-width or zero-height - images (from Chris Cogdon) +- Disable interlacing for small (or narrow) GIF images, to + work around what appears to be a hard-to-find bug in PIL's + GIF encoder. - + Fixed reading of PNG images from QuickTime 6 (from Paul Pharr) +- Fixed writing of mode "P" PDF images. Made mode "1" PDF + images smaller. - + Added support for StubImageFile plugins, including stub plugins - for BUFR, FITS, GRIB, and HDF5 files. A stub plugin can identify - a given file format, but relies on application code to open and - save files in that format. +- Made the XBM reader a bit more robust; the file may now start + with a few whitespace characters. - + Added optional "encoding" argument to the ImageFont.truetype - factory. This argument can be used to specify non-Unicode character - maps for fonts that support that. For example, to draw text using - the Microsoft Symbol font, use: +- Added support for enhanced metafiles to the WMF driver. The + separate PILWMF kit lets you render both placeable WMF files + and EMF files as raster images. See + http://effbot.org/downloads#pilwmf - font = ImageFont.truetype("symbol.ttf", 16, encoding="symb") - draw.text((0, 0), unichr(0xF000 + 0xAA)) +1.1.5a1 +------- - (note that the symbol font uses characters in the 0xF000-0xF0FF - range) +- Replaced broken WMF driver with a WMF stub plugin (see below). - Common encodings are "unic" (Unicode), "symb" (Microsoft Symbol), - "ADOB" (Adobe Standard), "ADBE" (Adobe Expert), and "armn" (Apple - Roman). See the FreeType documentation for more information. +- Fixed writing of mode "1", "L", and "CMYK" PDF images (based on + input from Nicholas Riley and others). - + Made "putalpha" a bit more robust; you can now attach an alpha - layer to a plain "L" or "RGB" image, and you can also specify - constant alphas instead of alpha layers (using integers or colour - names). +- Fixed adaptive palette conversion for zero-width or zero-height + images (from Chris Cogdon) - + Added experimental "LA" mode support. +- Fixed reading of PNG images from QuickTime 6 (from Paul Pharr) - An "LA" image is an "L" image with an attached transparency layer. - Note that support for "LA" is not complete; some operations may - fail or produce unexpected results. +- Added support for StubImageFile plugins, including stub plugins + for BUFR, FITS, GRIB, and HDF5 files. A stub plugin can identify + a given file format, but relies on application code to open and + save files in that format. - + Added "RankFilter", "MinFilter", "MedianFilter", and "MaxFilter" - classes to the ImageFilter module. +- Added optional "encoding" argument to the ImageFont.truetype + factory. This argument can be used to specify non-Unicode character + maps for fonts that support that. For example, to draw text using + the Microsoft Symbol font, use:: - + Improved support for applications using multiple threads; PIL - now releases the global interpreter lock for many CPU-intensive - operations (based on work by Kevin Cazabon). + font = ImageFont.truetype("symbol.ttf", 16, encoding="symb") + draw.text((0, 0), unichr(0xF000 + 0xAA)) - + Ignore Unicode characters in the PCF loader (from Andres Polit) + (note that the symbol font uses characters in the 0xF000-0xF0FF + range) - + Fixed typo in OleFileIO.loadfat, which could affect loading of - FlashPix and Image Composer images (Daniel Haertle) + Common encodings are "unic" (Unicode), "symb" (Microsoft Symbol), + "ADOB" (Adobe Standard), "ADBE" (Adobe Expert), and "armn" (Apple + Roman). See the FreeType documentation for more information. - + Fixed building on platforms that have Freetype but don't have - Tcl/Tk (Jack Jansen, Luciano Nocera, Piet van Oostrum and others) +- Made "putalpha" a bit more robust; you can now attach an alpha + layer to a plain "L" or "RGB" image, and you can also specify + constant alphas instead of alpha layers (using integers or colour + names). - + Added EXIF GPSInfo read support for JPEG files. To extract - GPSInfo information, open the file, extract the exif dictionary, - and check for the key 0x8825 (GPSInfo). If present, it contains - a dictionary mapping GPS keys to GPS values. For a list of keys, - see the EXIF specification. +- Added experimental "LA" mode support. - The "ExifTags" module contains a GPSTAGS dictionary mapping GPS - tags to tag names. + An "LA" image is an "L" image with an attached transparency layer. + Note that support for "LA" is not complete; some operations may + fail or produce unexpected results. - + Added DPI read support to the PCX and DCX codecs (info["dpi"]). +- Added "RankFilter", "MinFilter", "MedianFilter", and "MaxFilter" + classes to the ImageFilter module. - + The "show" methods now uses a built-in image viewer on Windows. - This viewer creates an instance of the ImageWindow class (see - below) and keeps it running in a separate thread. NOTE: This - was disabled in 1.1.5a4. +- Improved support for applications using multiple threads; PIL + now releases the global interpreter lock for many CPU-intensive + operations (based on work by Kevin Cazabon). - + Added experimental "Window" and "ImageWindow" classes to the - ImageWin module. These classes allow you to create a WCK-style - toplevel window, and use it to display raster data. +- Ignore Unicode characters in the PCF loader (from Andres Polit) - + Fixed some Python 1.5.2 issues (to build under 1.5.2, use the - Makefile.pre.in/Setup.in approach) +- Fixed typo in OleFileIO.loadfat, which could affect loading of + FlashPix and Image Composer images (Daniel Haertle) - + Added support for the TIFF FillOrder tag. PIL can read mode "1", - "L", "P" and "RGB" images with non-standard FillOrder (based on - input from Jeff Breidenbach). +- Fixed building on platforms that have Freetype but don't have + Tcl/Tk (Jack Jansen, Luciano Nocera, Piet van Oostrum and others) - (1.1.4 final released) +- Added EXIF GPSInfo read support for JPEG files. To extract + GPSInfo information, open the file, extract the exif dictionary, + and check for the key 0x8825 (GPSInfo). If present, it contains + a dictionary mapping GPS keys to GPS values. For a list of keys, + see the EXIF specification. - + Fixed ImageTk build problem on Unix. + The "ExifTags" module contains a GPSTAGS dictionary mapping GPS + tags to tag names. - (1.1.4b2 released) +- Added DPI read support to the PCX and DCX codecs (info["dpi"]). - + Improved building on Mac OS X (from Jack Jansen). +- The "show" methods now uses a built-in image viewer on Windows. + This viewer creates an instance of the ImageWindow class (see + below) and keeps it running in a separate thread. NOTE: This + was disabled in 1.1.5a4. - + Improved building on Windows with MinGW (from Klamer Shutte). +- Added experimental "Window" and "ImageWindow" classes to the + ImageWin module. These classes allow you to create a WCK-style + toplevel window, and use it to display raster data. - + If no font is specified, ImageDraw now uses the embedded default - font. Use the "load" or "truetype" methods to load a real font. +- Fixed some Python 1.5.2 issues (to build under 1.5.2, use the + Makefile.pre.in/Setup.in approach) - + Added embedded default font to the ImageFont module (currently - an 8-pixel Courier font, taken from the X window distribution). +- Added support for the TIFF FillOrder tag. PIL can read mode "1", + "L", "P" and "RGB" images with non-standard FillOrder (based on + input from Jeff Breidenbach). - (1.1.4b1 released) +1.1.4 final +----------- - + Added experimental EXIF support for JPEG files. To extract EXIF - information from a JPEG file, open the file as usual, and call the - "_getexif" method. If successful, this method returns a dictionary - mapping EXIF TIFF tags to values. If the file does not contain EXIF - data, the "_getexif" method returns None. +- Fixed ImageTk build problem on Unix. - The "ExifTags" module contains a dictionary mapping tags to tag - names. +1.1.4b2 +------- - This interface will most likely change in future versions. +- Improved building on Mac OS X (from Jack Jansen). - + Fixed a bug when using the "transparency" option with the GIF - writer. +- Improved building on Windows with MinGW (from Klamer Shutte). - + Added limited support for "bitfield compression" in BMP files - and DIB buffers, for 15-bit, 16-bit, and 32-bit images. This - also fixes a problem with ImageGrab module when copying screen- - dumps from the clipboard on 15/16/32-bit displays. +- If no font is specified, ImageDraw now uses the embedded default + font. Use the "load" or "truetype" methods to load a real font. - + Added experimental WAL (Quake 2 textures) loader. To use this - loader, import WalImageFile and call the "open" method in that - module. +- Added embedded default font to the ImageFont module (currently + an 8-pixel Courier font, taken from the X window distribution). - (1.1.4a4 released) +1.1.4b1 +------- - + Added updated SANE driver (Andrew Kuchling, Abel Deuring) +- Added experimental EXIF support for JPEG files. To extract EXIF + information from a JPEG file, open the file as usual, and call the + "_getexif" method. If successful, this method returns a dictionary + mapping EXIF TIFF tags to values. If the file does not contain EXIF + data, the "_getexif" method returns None. - + Use Python's "mmap" module on non-Windows platforms to read some - uncompressed formats using memory mapping. Also added a "frombuffer" - function that allows you to access the contents of an existing string - or buffer object as if it were an image object. + The "ExifTags" module contains a dictionary mapping tags to tag + names. - + Fixed a memory leak that could appear when processing mode "P" - images (from Pier Paolo Glave) + This interface will most likely change in future versions. - + Ignore Unicode characters in the BDF loader (from Graham Dumpleton) +- Fixed a bug when using the "transparency" option with the GIF + writer. - (1.1.4a3 released; windows only) +- Added limited support for "bitfield compression" in BMP files + and DIB buffers, for 15-bit, 16-bit, and 32-bit images. This + also fixes a problem with ImageGrab module when copying screen- + dumps from the clipboard on 15/16/32-bit displays. - + Added experimental RGBA-on-RGB drawing support. To use RGBA - colours on an RGB image, pass "RGBA" as the second string to - the ImageDraw.Draw constructor. +- Added experimental WAL (Quake 2 textures) loader. To use this + loader, import WalImageFile and call the "open" method in that + module. - + Added support for non-ASCII strings (Latin-1) and Unicode - to the truetype font renderer. +1.1.4a4 +------- - + The ImageWin "Dib" object can now be constructed directly from - an image object. +- Added updated SANE driver (Andrew Kuchling, Abel Deuring) - + The ImageWin module now allows you use window handles as well - as device contexts. To use a window handle, wrap the handle in - an ImageWin.HWND object, and pass in this object instead of the - device context. +- Use Python's "mmap" module on non-Windows platforms to read some + uncompressed formats using memory mapping. Also added a "frombuffer" + function that allows you to access the contents of an existing string + or buffer object as if it were an image object. - (1.1.4a2 released) +- Fixed a memory leak that could appear when processing mode "P" + images (from Pier Paolo Glave) - + Improved support for 16-bit unsigned integer images (mode "I;16"). - This includes TIFF reader support, and support for "getextrema" - and "point" (from Klamer Shutte). +- Ignore Unicode characters in the BDF loader (from Graham Dumpleton) - + Made the BdfFontFile reader a bit more robust (from Kevin Cazabon - and Dmitry Vasiliev) +1.1.4a3 released; Windows only +------------------------------ - + Changed TIFF writer to always write Compression tag, even when - using the default compression (from Greg Couch). +- Added experimental RGBA-on-RGB drawing support. To use RGBA + colours on an RGB image, pass "RGBA" as the second string to + the ImageDraw.Draw constructor. - + Added "show" support for Mac OS X (from Dan Wolfe). +- Added support for non-ASCII strings (Latin-1) and Unicode + to the truetype font renderer. - + Added clipboard support to the "ImageGrab" module (Windows only). - The "grabclipboard" function returns an Image object, a list of - filenames (not in 1.1.4), or None if neither was found. +- The ImageWin "Dib" object can now be constructed directly from + an image object. - (1.1.4a1 released) +- The ImageWin module now allows you use window handles as well + as device contexts. To use a window handle, wrap the handle in + an ImageWin.HWND object, and pass in this object instead of the + device context. - + Improved support for drawing RGB data in palette images. You can - now use RGB tuples or colour names (see below) when drawing in a - mode "P" image. The drawing layer automatically assigns color - indexes, as long as you don't use more than 256 unique colours. +1.1.4a2 +------- - + Moved self test from MiniTest/test.py to ./selftest.py. +- Improved support for 16-bit unsigned integer images (mode "I;16"). + This includes TIFF reader support, and support for "getextrema" + and "point" (from Klamer Shutte). - + Added support for CSS3-style color strings to most places that - accept colour codes/tuples. This includes the "ImageDraw" module, - the Image "new" function, and the Image "paste" method. +- Made the BdfFontFile reader a bit more robust (from Kevin Cazabon + and Dmitry Vasiliev) - Colour strings can use one of the following formats: "#f00", - "#ff0000", "rgb(255,0,0)", "rgb(100%,0%,0%)", "hsl(0, 100%, 50%)", - or "red" (most X11-style colour names are supported). See the - documentation for the "ImageColor" module for more information. +- Changed TIFF writer to always write Compression tag, even when + using the default compression (from Greg Couch). - + Fixed DCX decoder (based on input from Larry Bates) +- Added "show" support for Mac OS X (from Dan Wolfe). - + Added "IptcImagePlugin.getiptcinfo" helper to extract IPTC/NAA - newsphoto properties from JPEG, TIFF, or IPTC files. +- Added clipboard support to the "ImageGrab" module (Windows only). + The "grabclipboard" function returns an Image object, a list of + filenames (not in 1.1.4), or None if neither was found. - + Support for TrueType/OpenType fonts has been added to - the standard distribution. You need the freetype 2.0 - library. +1.1.4a1 +------- - + Made the PCX reader a bit more robust when reading 2-bit - and 4-bit PCX images with odd image sizes. +- Improved support for drawing RGB data in palette images. You can + now use RGB tuples or colour names (see below) when drawing in a + mode "P" image. The drawing layer automatically assigns color + indexes, as long as you don't use more than 256 unique colours. - + Added "Kernel" class to the ImageFilter module. This class - allows you to filter images with user-defined 3x3 and 5x5 - convolution kernels. +- Moved self test from MiniTest/test.py to ./selftest.py. - + Added "putdata" support for mode "I", "F" and "RGB". +- Added support for CSS3-style color strings to most places that + accept colour codes/tuples. This includes the "ImageDraw" module, + the Image "new" function, and the Image "paste" method. - + The GIF writer now supports the transparency option (from - Denis Benoit). + Colour strings can use one of the following formats: "#f00", + "#ff0000", "rgb(255,0,0)", "rgb(100%,0%,0%)", "hsl(0, 100%, 50%)", + or "red" (most X11-style colour names are supported). See the + documentation for the "ImageColor" module for more information. - + A HTML version of the module documentation is now shipped - with the source code distribution. You'll find the files in - the Doc subdirectory. +- Fixed DCX decoder (based on input from Larry Bates) - + Added support for Palm pixmaps (from Bill Janssen). This - change was listed for 1.1.3, but the "PalmImagePlugin" driver - didn't make it into the distribution. +- Added "IptcImagePlugin.getiptcinfo" helper to extract IPTC/NAA + newsphoto properties from JPEG, TIFF, or IPTC files. - + Improved decoder error messages. +- Support for TrueType/OpenType fonts has been added to + the standard distribution. You need the freetype 2.0 + library. - (1.1.3 final released) +- Made the PCX reader a bit more robust when reading 2-bit + and 4-bit PCX images with odd image sizes. - + Made setup.py look for old versions of zlib. For some back- - ground, see: http://www.gzip.org/zlib/advisory-2002-03-11.txt +- Added "Kernel" class to the ImageFilter module. This class + allows you to filter images with user-defined 3x3 and 5x5 + convolution kernels. - (1.1.3c2 released) +- Added "putdata" support for mode "I", "F" and "RGB". - + Added setup.py file (tested on Unix and Windows). You still - need to build libImaging/imaging.lib in the traditional way, - but the setup.py script takes care of the rest. +- The GIF writer now supports the transparency option (from + Denis Benoit). - The old Setup.in/Makefile.pre.in build method is still - supported. +- A HTML version of the module documentation is now shipped + with the source code distribution. You'll find the files in + the Doc subdirectory. - + Fixed segmentation violation in ANTIALIAS filter (an internal - buffer wasn't properly allocated). +- Added support for Palm pixmaps (from Bill Janssen). This + change was listed for 1.1.3, but the "PalmImagePlugin" driver + didn't make it into the distribution. - (1.1.3c1 released) +- Improved decoder error messages. - + Added ANTIALIAS downsampling filter for high-quality "resize" - and "thumbnail" operations. Also added filter option to the - "thumbnail" operation; the default value is NEAREST, but this - will most likely change in future versions. +1.1.3 final +----------- - + Fixed plugin loader to be more robust if the __file__ - variable isn't set. +- Made setup.py look for old versions of zlib. For some back- + ground, see: https://zlib.net/advisory-2002-03-11.txt - + Added seek/tell support (for layers) to the PhotoShop - loader. Layer 0 is the main image. +1.1.3c2 +------- - + Added new (but experimental) "ImageOps" module, which provides - shortcuts for commonly used operations on entire images. +- Added setup.py file (tested on Unix and Windows). You still + need to build libImaging/imaging.lib in the traditional way, + but the setup.py script takes care of the rest. - + Don't mess up when loading PNG images if the decoder leaves - data in the output buffer. This could cause internal errors - on some PNG images, with some versions of ZLIB. (Bug report - and patch provided by Bernhard Herzog.) + The old Setup.in/Makefile.pre.in build method is still + supported. - + Don't mess up on Unicode filenames. +- Fixed segmentation violation in ANTIALIAS filter (an internal + buffer wasn't properly allocated). - + Don't mess up when drawing on big endian platforms. +1.1.3c1 +------- - + Made the TIFF loader a bit more robust; it can now read some - more slightly broken TIFF files (based on input from Ted Wright, - Bob Klimek, and D. Alan Stewart) +- Added ANTIALIAS downsampling filter for high-quality "resize" + and "thumbnail" operations. Also added filter option to the + "thumbnail" operation; the default value is NEAREST, but this + will most likely change in future versions. - + Added OS/2 EMX build files (from Andrew MacIntyre) +- Fixed plugin loader to be more robust if the __file__ + variable isn't set. - + Change "ImageFont" to reject image files if they don't have the - right mode. Older versions could leak memory for "P" images. - (Bug reported by Markus Gritsch). +- Added seek/tell support (for layers) to the PhotoShop + loader. Layer 0 is the main image. - + Renamed some internal functions to avoid potential build - problem on Mac OS X. +- Added new (but experimental) "ImageOps" module, which provides + shortcuts for commonly used operations on entire images. - + Added DL_EXPORT where relevant (for Cygwin, based on input - from Robert Yodlowski) +- Don't mess up when loading PNG images if the decoder leaves + data in the output buffer. This could cause internal errors + on some PNG images, with some versions of ZLIB. (Bug report + and patch provided by Bernhard Herzog.) - + (re)moved bogus __init__ call in BdfFontFile (bug spotted - by Fred Clare) +- Don't mess up on Unicode filenames. - + Added "ImageGrab" support (Windows only) +- Don't mess up when drawing on big endian platforms. - + Added support for XBM hotspots (based on code contributed by - Bernhard Herzog). +- Made the TIFF loader a bit more robust; it can now read some + more slightly broken TIFF files (based on input from Ted Wright, + Bob Klimek, and D. Alan Stewart) - + Added write support for more TIFF tags, namely the Artist, - Copyright, DateTime, ResolutionUnit, Software, XResolution and - YResolution tags (from Greg Couch) +- Added OS/2 EMX build files (from Andrew MacIntyre) - + Added TransposedFont wrapper to ImageFont module +- Change "ImageFont" to reject image files if they don't have the + right mode. Older versions could leak memory for "P" images. + (Bug reported by Markus Gritsch). - + Added "optimize" flag to GIF encoder. If optimize is present - and non-zero, PIL will work harder to create a small file. +- Renamed some internal functions to avoid potential build + problem on Mac OS X. - + Raise "EOFError" (not IndexError) when reading beyond the - end of a TIFF sequence. +- Added DL_EXPORT where relevant (for Cygwin, based on input + from Robert Yodlowski) - + Support rewind ("seek(0)") for GIF and TIFF sequences. +- (re)moved bogus __init__ call in BdfFontFile (bug spotted + by Fred Clare) - + Load grayscale GIF images as mode "L" +- Added "ImageGrab" support (Windows only) - + Added DPI read/write support to the JPEG codec. The decoder - sets the info["dpi"] attribute for JPEG files with JFIF dpi - settings. The encoder uses the "dpi" option: +- Added support for XBM hotspots (based on code contributed by + Bernhard Herzog). - im = Image.open("file.jpg") - dpi = im.info["dpi"] # raises KeyError if DPI not known - im.save("out.jpg", dpi=dpi) +- Added write support for more TIFF tags, namely the Artist, + Copyright, DateTime, ResolutionUnit, Software, XResolution and + YResolution tags (from Greg Couch) - Note that PIL doesn't always preserve the "info" attribute - for normal image operations. +- Added TransposedFont wrapper to ImageFont module - (1.1.2c1 and 1.1.2 final released) +- Added "optimize" flag to GIF encoder. If optimize is present + and non-zero, PIL will work harder to create a small file. - + Adapted to Python 2.1. Among other things, all uses of the - "regex" module have been replaced with "re". +- Raise "EOFError" (not IndexError) when reading beyond the + end of a TIFF sequence. - + Fixed attribute error when reading large PNG files (this bug - was introduced in maintenance code released after the 1.1.1 - release) +- Support rewind ("seek(0)") for GIF and TIFF sequences. - + Ignore non-string objects in sys.path +- Load grayscale GIF images as mode "L" - + Fixed Image.transform(EXTENT) for negative xoffsets +- Added DPI read/write support to the JPEG codec. The decoder + sets the info["dpi"] attribute for JPEG files with JFIF dpi + settings. The encoder uses the "dpi" option:: - + Fixed loading of image plugins if PIL is installed as a package. - (The plugin loader now always looks in the directory where the - Image.py module itself is found, even if that directory isn't on - the standard search path) + im = Image.open("file.jpg") + dpi = im.info["dpi"] # raises KeyError if DPI not known + im.save("out.jpg", dpi=dpi) - + The Png plugin has been added to the list of preloaded standard - formats + Note that PIL doesn't always preserve the "info" attribute + for normal image operations. - + Fixed bitmap/text drawing in fill mode. +1.1.2c1 and 1.1.2 final +----------------------- - + Fixed "getextrema" to work also for multiband images. +- Adapted to Python 2.1. Among other things, all uses of the + "regex" module have been replaced with "re". - + Added transparency support for L and P images to the PNG codec. +- Fixed attribute error when reading large PNG files (this bug + was introduced in maintenance code released after the 1.1.1 + release) - + Improved support for read-only images. The "load" method now - sets the "readonly" attribute for memory-mapped images. Operations - that modifies an image in place (such as "paste" and drawing operations) - creates an in-memory copy of the image, if necessary. (before this - change, any attempt to modify a memory-mapped image resulted in a - core dump...) +- Ignore non-string objects in sys.path - + Added special cases for lists everywhere PIL expects a sequence. - This should speed up things like "putdata" and drawing operations. +- Fixed Image.transform(EXTENT) for negative xoffsets - + The Image.offset method is deprecated. Use the ImageChops.offset - function instead. +- Fixed loading of image plugins if PIL is installed as a package. + (The plugin loader now always looks in the directory where the + Image.py module itself is found, even if that directory isn't on + the standard search path) - + Changed ImageChops operators to copy palette and info dictionary - from the first image argument. +- The Png plugin has been added to the list of preloaded standard + formats - (1.1.1 released) +- Fixed bitmap/text drawing in fill mode. - + Additional fixes for Python 1.6/2.0, including TIFF "save" bug. +- Fixed "getextrema" to work also for multiband images. - + Changed "init" to properly load plugins when PIL is used as a - package. +- Added transparency support for L and P images to the PNG codec. - + Fixed broken "show" method (on Unix) +- Improved support for read-only images. The "load" method now + sets the "readonly" attribute for memory-mapped images. Operations + that modifies an image in place (such as "paste" and drawing operations) + creates an in-memory copy of the image, if necessary. (before this + change, any attempt to modify a memory-mapped image resulted in a + core dump...) - *** Changes from release 1.0 to 1.1 *** +- Added special cases for lists everywhere PIL expects a sequence. + This should speed up things like "putdata" and drawing operations. - + Adapted to Python 1.6 ("append" and other method changes) +- The Image.offset method is deprecated. Use the ImageChops.offset + function instead. - + Fixed Image.paste when pasting with solid colour and matte - layers ("L" or "RGBA" masks) (bug reported by Robert Kern) +- Changed ImageChops operators to copy palette and info dictionary + from the first image argument. - + To make it easier to distribute prebuilt versions of PIL, - the tkinit binding stuff has been moved to a separate - extension module, named "_imagingtk". +1.1.1 +----- - *** Changes from release 0.3b2 to 1.0 final *** +- Additional fixes for Python 1.6/2.0, including TIFF "save" bug. - + If there's no 16-bit integer (like on a Cray T3E), set - INT16 to the smallest integer available. Most of the - library works just fine anyway (from Bill Crutchfield) +- Changed "init" to properly load plugins when PIL is used as a + package. - + Tweaks to make drawing work on big-endian platforms. +- Fixed broken "show" method (on Unix) - (1.0c2 released) +1.0 to 1.1 +---------- - + If PIL is built with the WITH_TKINTER flag, ImageTk can - automatically hook into a standard Tkinter build. You - no longer need to build your own Tkinter to use the - ImageTk module. +- Adapted to Python 1.6 ("append" and other method changes) - The old way still works, though. For more information, - see Tk/install.txt. +- Fixed Image.paste when pasting with solid colour and matte + layers ("L" or "RGBA" masks) (bug reported by Robert Kern) - + Some tweaks to ImageTk to support multiple Tk interpreters - (from Greg Couch). +- To make it easier to distribute prebuilt versions of PIL, + the tkinit binding stuff has been moved to a separate + extension module, named "_imagingtk". - + ImageFont "load_path" now scans directory mentioned in .pth - files (from Richard Jones). - - (1.0c1 released) - - + The TIFF plugin has been rewritten. The new plugin fully - supports all major PIL image modes (including F and I). - - + The ImageFile module now includes a Parser class, which can - be used to incrementally decode an image file (while down- - loading it from the net, for example). See the handbook for - details. - - + "show" now converts non-standard modes to "L" or "RGB" (as - appropriate), rather than writing weird things to disk for - "xv" to choke upon. (bug reported by Les Schaffer). - - (1.0b2 released) - - + Major speedups for rotate, transform(EXTENT), and transform(AFFINE) - when using nearest neighbour resampling. - - + Modified ImageDraw to be compatible with the Arrow graphics - interface. See the handbook for details. - - + PIL now automatically loads file codecs when used as a package - (from The Dragon De Monsyne). Also included an __init__.py file - in the standard distribution. - - + The GIF encoder has been modified to produce much smaller files. - - PIL now uses a run-length encoding method to encode GIF files. - On a random selection of GIF images grabbed from the web, this - version makes the images about twice as large as the original - LZW files, where the earlier version made them over 5 times - larger. YMMV, of course. - - + Added PCX write support (works with "1", "P", "L", and "RGB") - - + Added "bitmap" and "textsize" methods to ImageDraw. - - + Improved font rendering code. Fixed a bug or two, and moved - most of the time critical stuff to C. - - + Removed "bdf2pil.py". Use "pilfont.py" instead! - - + Improved 16-bit support (still experimental, though). +0.3b2 to 1.0 final +------------------ - The following methods now support "I;16" and "I;16B" images: - "getpixel", "copy", "convert" (to and from mode "I"), "resize", - "rotate", and "transform" with nearest neighbour filters, and - "save" using the IM format. The "new" and "open" functions - also work as expected. On Windows, 16-bit files are memory - mapped. +- If there's no 16-bit integer (like on a Cray T3E), set + INT16 to the smallest integer available. Most of the + library works just fine anyway (from Bill Crutchfield) - NOTE: ALL other operations are still UNDEFINED on 16-bit images. +- Tweaks to make drawing work on big-endian platforms. - + The "paste" method now supports constant sources. +1.0c2 +----- - Just pass a colour value (a number or a tuple, depending on - the target image mode) instead of the source image. +- If PIL is built with the WITH_TKINTER flag, ImageTk can + automatically hook into a standard Tkinter build. You + no longer need to build your own Tkinter to use the + ImageTk module. - This was in fact implemented in an inefficient way in - earlier versions (the "paste" method generated a temporary - source image if you passed it a colour instead of an image). - In this version, this is handled on the C level instead. + The old way still works, though. For more information, + see Tk/install.txt. - + Added experimental "RGBa" mode support. +- Some tweaks to ImageTk to support multiple Tk interpreters + (from Greg Couch). - An "RGBa" image is an RGBA image where the colour components - have have been premultiplied with the alpha value. PIL allows - you to convert an RGBA image to an RGBa image, and to paste - RGBa images on top of RGB images. Since this saves a bunch - of multiplications and shifts, it is typically about twice - as fast an ordinary RGBA paste. +- ImageFont "load_path" now scans directory mentioned in .pth + files (from Richard Jones). - + Eliminated extra conversion step when pasting "RGBA" or "RGBa" - images on top of "RGB" images. +1.0c1 +----- - + Fixed Image.BICUBIC resampling for "RGB" images. +- The TIFF plugin has been rewritten. The new plugin fully + supports all major PIL image modes (including F and I). - + Fixed PCX image file handler to properly read 8-bit PCX - files (bug introduced in 1.0b1, reported by Bernhard - Herzog) +- The ImageFile module now includes a Parser class, which can + be used to incrementally decode an image file (while down- + loading it from the net, for example). See the handbook for + details. - + Fixed PSDraw "image" method to restore the coordinate - system. +- "show" now converts non-standard modes to "L" or "RGB" (as + appropriate), rather than writing weird things to disk for + "xv" to choke upon. (bug reported by Les Schaffer). - + Fixed "blend" problem when applied to images that was - not already loaded (reported by Edward C. Jones) +1.0b2 +----- - + Fixed -f option to "pilconvert.py" (from Anthony Baxter) +- Major speedups for rotate, transform(EXTENT), and transform(AFFINE) + when using nearest neighbour resampling. - (1.0b1 released) +- Modified ImageDraw to be compatible with the Arrow graphics + interface. See the handbook for details. - + Added Toby J. Sargeant's quantization package. To enable - quantization, use the "palette" option to "convert": +- PIL now automatically loads file codecs when used as a package + (from The Dragon De Monsyne). Also included an __init__.py file + in the standard distribution. - imOut = im.convert("P", palette=Image.ADAPTIVE) +- The GIF encoder has been modified to produce much smaller files. - This can be used with "L", "P", and "RGB" images. In this - version, dithering cannot be used with adaptive palettes. + PIL now uses a run-length encoding method to encode GIF files. + On a random selection of GIF images grabbed from the web, this + version makes the images about twice as large as the original + LZW files, where the earlier version made them over 5 times + larger. YMMV, of course. - Note: ADAPTIVE currently maps to median cut quantization - with 256 colours. The quantization package also contains - a maximum coverage quantizer, which will be supported by - future versions of PIL. +- Added PCX write support (works with "1", "P", "L", and "RGB") - + Added Eric S. Raymond's "pildriver" image calculator to the - distribution. See the docstring for more information. +- Added "bitmap" and "textsize" methods to ImageDraw. - + The "offset" method no longer dumps core if given positive - offsets (from Charles Waldman). +- Improved font rendering code. Fixed a bug or two, and moved + most of the time critical stuff to C. - + Fixed a resource leak that could cause ImageWin to run out of - GDI resources (from Roger Burnham). +- Removed "bdf2pil.py". Use "pilfont.py" instead! - + Added "arc", "chord", and "pieslice" methods to ImageDraw (inspired - by code contributed by Richard Jones). +- Improved 16-bit support (still experimental, though). - + Added experimental 16-bit support, via modes "I;16" (little endian - data) and "I;16B" (big endian). Only a few methods properly support - such images (see above). + The following methods now support "I;16" and "I;16B" images: + "getpixel", "copy", "convert" (to and from mode "I"), "resize", + "rotate", and "transform" with nearest neighbour filters, and + "save" using the IM format. The "new" and "open" functions + also work as expected. On Windows, 16-bit files are memory + mapped. - + Added XV thumbnail file handler (from Gene Cash). + NOTE: ALL other operations are still UNDEFINED on 16-bit images. - + Fixed BMP image file handler to handle palette images with small - palettes (from Rob Hooft). +- The "paste" method now supports constant sources. - + Fixed Sun raster file handler for palette images (from Charles - Waldman). + Just pass a colour value (a number or a tuple, depending on + the target image mode) instead of the source image. - + Improved various internal error messages. + This was in fact implemented in an inefficient way in + earlier versions (the "paste" method generated a temporary + source image if you passed it a colour instead of an image). + In this version, this is handled on the C level instead. - + Fixed Path constructor to handle arbitrary sequence objects. This - also affects the ImageDraw class (from Richard Jones). +- Added experimental "RGBa" mode support. - + Fixed a bug in JpegDecode that caused PIL to report "decoder error - -2" for some progressive JPEG files (reported by Magnus Källström, - who also provided samples). + An "RGBa" image is an RGBA image where the colour components + have have been premultiplied with the alpha value. PIL allows + you to convert an RGBA image to an RGBa image, and to paste + RGBa images on top of RGB images. Since this saves a bunch + of multiplications and shifts, it is typically about twice + as fast an ordinary RGBA paste. - + Fixed a bug in JpegImagePlugin that caused PIL to hang when loading - JPEG files using 16-bit quantization tables. +- Eliminated extra conversion step when pasting "RGBA" or "RGBa" + images on top of "RGB" images. - + The Image "transform" method now supports Image.QUAD transforms. - The data argument is an 8-tuple giving the upper left, lower - left, lower right, and upper right corner of the source quadri- - lateral. Also added Image.MESH transform which takes a list - of quadrilaterals. +- Fixed Image.BICUBIC resampling for "RGB" images. - + The Image "resize", "rotate", and "transform" methods now support - Image.BILINEAR (2x2) and Image.BICUBIC (4x4) resampling filters. - Filters can be used with all transform methods. +- Fixed PCX image file handler to properly read 8-bit PCX + files (bug introduced in 1.0b1, reported by Bernhard + Herzog) - + The ImageDraw "rectangle" method now includes both the right - and the bottom edges when drawing filled rectangles. +- Fixed PSDraw "image" method to restore the coordinate + system. - + The TGA decoder now works properly for runlength encoded images - which have more than one byte per pixel. +- Fixed "blend" problem when applied to images that was + not already loaded (reported by Edward C. Jones) - + "getbands" on an YCbCr image now returns ("Y", "Cb", "Cr") +- Fixed -f option to "pilconvert.py" (from Anthony Baxter) - + Some file drivers didn't handle the optional "modify" argument - to the load method. This resulted in exceptions when you used - "paste" (and other methods that modify an image in place) on a - newly opened file. +1.0b1 +----- - *** Changes from release 0.2 (b5) to 0.3 (b2) *** +- Added Toby J. Sargeant's quantization package. To enable + quantization, use the "palette" option to "convert":: - (0.3b2 released) + imOut = im.convert("P", palette=Image.ADAPTIVE) - The test suite includes 825 individual tests. + This can be used with "L", "P", and "RGB" images. In this + version, dithering cannot be used with adaptive palettes. - + An Image "getbands" method has been added. It returns a tuple - containing the individual band names for this image. To figure - out how many bands an image has, use "len(im.getbands())". + Note: ADAPTIVE currently maps to median cut quantization + with 256 colours. The quantization package also contains + a maximum coverage quantizer, which will be supported by + future versions of PIL. - + An Image "putpixel" method has been added. +- Added Eric S. Raymond's "pildriver" image calculator to the + distribution. See the docstring for more information. - + The Image "point" method can now be used to convert "L" images - to any other format, via a lookup table. That table should - contain 256 values for each band in the output image. +- The "offset" method no longer dumps core if given positive + offsets (from Charles Waldman). - + Some file drivers (including FLI/FLC, GIF, and IM) accidentally - overwrote the offset method with an internal attribute. All - drivers have been updated to use private attributes where - possible. +- Fixed a resource leak that could cause ImageWin to run out of + GDI resources (from Roger Burnham). - + The Image "histogram" method now works for "I" and "F" images. - For these modes, PIL divides the range between the min and - max values used in the image into 256 bins. You can also - pass in your own min and max values via the "extrema" option: +- Added "arc", "chord", and "pieslice" methods to ImageDraw (inspired + by code contributed by Richard Jones). - h = im.histogram(extrema=(0, 255)) +- Added experimental 16-bit support, via modes "I;16" (little endian + data) and "I;16B" (big endian). Only a few methods properly support + such images (see above). - + An Image "getextrema" method has been added. It returns the - min and max values used in the image. In this release, this - works for single band images only. +- Added XV thumbnail file handler (from Gene Cash). - + Changed the PNG driver to load and save mode "I" images as - 16-bit images. When saving, values outside the range 0..65535 - are clipped. +- Fixed BMP image file handler to handle palette images with small + palettes (from Rob Hooft). - + Fixed ImageFont.py to work with the new "pilfont" compiler. +- Fixed Sun raster file handler for palette images (from Charles + Waldman). - + Added JPEG "save" and "draft" support for mode "YCbCr" images. - Note that if you save an "YCbCr" image as a JPEG file and read - it back, it is read as an RGB file. To get around this, you - can use the "draft" method: +- Improved various internal error messages. - im = Image.open("color.jpg") - im.draft("YCbCr", im.size) +- Fixed Path constructor to handle arbitrary sequence objects. This + also affects the ImageDraw class (from Richard Jones). - + Read "RGBA" TGA images. Also fixed the orientation bug; all - images should now come out the right way. +- Fixed a bug in JpegDecode that caused PIL to report "decoder error + -2" for some progressive JPEG files (reported by Magnus Källström, + who also provided samples). - + Changed mode name (and internal representation) from "YCrCb" - to "YCbCr" (!) - *** WARNING: MAY BREAK EXISTING CODE *** +- Fixed a bug in JpegImagePlugin that caused PIL to hang when loading + JPEG files using 16-bit quantization tables. - (0.3b1 released) +- The Image "transform" method now supports Image.QUAD transforms. + The data argument is an 8-tuple giving the upper left, lower + left, lower right, and upper right corner of the source quadri- + lateral. Also added Image.MESH transform which takes a list + of quadrilaterals. - The test suite includes 750 individual tests. +- The Image "resize", "rotate", and "transform" methods now support + Image.BILINEAR (2x2) and Image.BICUBIC (4x4) resampling filters. + Filters can be used with all transform methods. - + The "pilfont" package is now included in the standard PIL - distribution. The pilfont utility can be used to convert - X BDF and PCF raster font files to a format understood by - the ImageFont module. +- The ImageDraw "rectangle" method now includes both the right + and the bottom edges when drawing filled rectangles. - + GIF files are now interlaced by default. To write a - non-interlaced file, pass interlace=0 to the "save" - method. +- The TGA decoder now works properly for runlength encoded images + which have more than one byte per pixel. - + The default string format has changed for the "fromstring" - and "tostring" methods. - *** WARNING: MAY BREAK EXISTING CODE *** +- "getbands" on an YCbCr image now returns ("Y", "Cb", "Cr") - NOTE: If no extra arguments are given, the first line in - the string buffer is the top line of the image, instead of - the bottom line. For RGB images, the string now contains - 3 bytes per pixel instead of 4. These changes were made - to make the methods compatible with the "fromstring" - factory function. +- Some file drivers didn't handle the optional "modify" argument + to the load method. This resulted in exceptions when you used + "paste" (and other methods that modify an image in place) on a + newly opened file. - To get the old behaviour, use the following syntax: +0.3b2 +----- - data = im.tostring("raw", "RGBX", 0, -1) - im.fromstring(data, "raw", "RGBX", 0, -1) +The test suite includes 825 individual tests. - + "new" no longer gives a MemoryError if the width or height - is zero (this only happened on platforms where malloc(0) - or calloc(0) returns NULL). +- An Image "getbands" method has been added. It returns a tuple + containing the individual band names for this image. To figure + out how many bands an image has, use "len(im.getbands())". - + "new" now adds a default palette object to "P" images. +- An Image "putpixel" method has been added. - + You can now convert directly between all modes supported by - PIL. When converting colour images to "P", PIL defaults to - a "web" palette and dithering. When converting greyscale - images to "1", PIL uses a thresholding and dithering. +- The Image "point" method can now be used to convert "L" images + to any other format, via a lookup table. That table should + contain 256 values for each band in the output image. - + Added a "dither" option to "convert". By default, "convert" - uses floyd-steinberg error diffusion for "P" and "1" targets, - so this option is only used to *disable* dithering. Allowed - values are NONE (no dithering) or FLOYDSTEINBERG (default). +- Some file drivers (including FLI/FLC, GIF, and IM) accidentally + overwrote the offset method with an internal attribute. All + drivers have been updated to use private attributes where + possible. - imOut = im.convert("P", dither=Image.NONE) +- The Image "histogram" method now works for "I" and "F" images. + For these modes, PIL divides the range between the min and + max values used in the image into 256 bins. You can also + pass in your own min and max values via the "extrema" option:: - + Added a full set of "I" decoders. You can use "fromstring" - (and file decoders) to read any standard integer type as an - "I" image. + h = im.histogram(extrema=(0, 255)) - + Added some support for "YCbCr" images (creation, conversion - from/to "L" and "RGB", IM YCC load/save) +- An Image "getextrema" method has been added. It returns the + min and max values used in the image. In this release, this + works for single band images only. - + "getpixel" now works properly with fractional coordinates. +- Changed the PNG driver to load and save mode "I" images as + 16-bit images. When saving, values outside the range 0..65535 + are clipped. - + ImageDraw "setink" now works with "I", "F", "RGB", "RGBA", - "RGBX", "CMYK", and "YCbCr" images. +- Fixed ImageFont.py to work with the new "pilfont" compiler. - + ImImagePlugin no longer attaches palettes to "RGB" images. +- Added JPEG "save" and "draft" support for mode "YCbCr" images. + Note that if you save an "YCbCr" image as a JPEG file and read + it back, it is read as an RGB file. To get around this, you + can use the "draft" method:: - + Various minor fixes. + im = Image.open("color.jpg") + im.draft("YCbCr", im.size) - (0.3a4 released) +- Read "RGBA" TGA images. Also fixed the orientation bug; all + images should now come out the right way. - + Added experimental IPTC/NAA support. +- Changed mode name (and internal representation) from "YCrCb" + to "YCbCr" (!) + **WARNING: MAY BREAK EXISTING CODE** - + Eliminated AttributeError exceptions after "crop" (from - Skip Montanaro) +0.3b1 +----- - + Reads some uncompressed formats via memory mapping (this - is currently supported on Win32 only) +The test suite includes 750 individual tests. - + Fixed some last minute glitches in the last alpha release - (Types instead of types in Image.py, version numbers, etc.) +- The "pilfont" package is now included in the standard PIL + distribution. The pilfont utility can be used to convert + X BDF and PCF raster font files to a format understood by + the ImageFont module. - + Eliminated some more bogus compiler warnings. +- GIF files are now interlaced by default. To write a + non-interlaced file, pass interlace=0 to the "save" + method. - + Various fixes to make PIL compile and run smoother on Macs - (from Jack Jansen). +- The default string format has changed for the "fromstring" + and "tostring" methods. + **WARNING: MAY BREAK EXISTING CODE** - + Fixed "fromstring" and "tostring" for mode "I" images. + NOTE: If no extra arguments are given, the first line in + the string buffer is the top line of the image, instead of + the bottom line. For RGB images, the string now contains + 3 bytes per pixel instead of 4. These changes were made + to make the methods compatible with the "fromstring" + factory function. - (0.3a3 released) + To get the old behaviour, use the following syntax:: - The test suite includes 530 individual tests. + data = im.tostring("raw", "RGBX", 0, -1) + im.fromstring(data, "raw", "RGBX", 0, -1) - + Eliminated unexpected side-effect in "paste" with matte. "paste" - now works properly also if compiled with "gcc". +- "new" no longer gives a MemoryError if the width or height + is zero (this only happened on platforms where malloc(0) + or calloc(0) returns NULL). - + Adapted to Python 1.5 (build issues only) +- "new" now adds a default palette object to "P" images. - + Fixed the ImageDraw "point" method to draw also the last - point (!). +- You can now convert directly between all modes supported by + PIL. When converting colour images to "P", PIL defaults to + a "web" palette and dithering. When converting greyscale + images to "1", PIL uses a thresholding and dithering. - + Added "I" and "RGBX" support to Image.new. +- Added a "dither" option to "convert". By default, "convert" + uses floyd-steinberg error diffusion for "P" and "1" targets, + so this option is only used to *disable* dithering. Allowed + values are NONE (no dithering) or FLOYDSTEINBERG (default). + :: - + The plugin path is now properly prepended to the module search - path when a plugin module is imported. + imOut = im.convert("P", dither=Image.NONE) - + Added "draw" method to the ImageWin.Dib class. This is used by - Topaz to print images on Windows printers. +- Added a full set of "I" decoders. You can use "fromstring" + (and file decoders) to read any standard integer type as an + "I" image. - + "convert" now supports conversions from "P" to "1" and "F". +- Added some support for "YCbCr" images (creation, conversion + from/to "L" and "RGB", IM YCC load/save) - + "paste" can now take a colour instead of an image as the first argument. - The colour must match the colour argument given to the new function, and - match the mode of the target image. +- "getpixel" now works properly with fractional coordinates. - + Fixed "paste" to allow a mask also for mode "F" images. +- ImageDraw "setink" now works with "I", "F", "RGB", "RGBA", + "RGBX", "CMYK", and "YCbCr" images. - + The BMP driver now saves mode "1" images. When loading images, the mode - is set to "L" for 8-bit files with greyscale palettes, and to "P" for - other 8-bit files. +- ImImagePlugin no longer attaches palettes to "RGB" images. - + The IM driver now reads and saves "1" images (file modes "0 1" or "L 1"). +- Various minor fixes. - + The JPEG and GIF drivers now saves "1" images. For JPEG, the image - is saved as 8-bit greyscale (it will load as mode "L"). For GIF, the - image will be loaded as a "P" image. +0.3a4 +----- - + Fixed a potential buffer overrun in the GIF encoder. +- Added experimental IPTC/NAA support. - (0.3a2 released) +- Eliminated AttributeError exceptions after "crop" (from + Skip Montanaro) - The test suite includes 400 individual tests. +- Reads some uncompressed formats via memory mapping (this + is currently supported on Win32 only) - + Improvements to the test suite revealed a number of minor bugs, which - are all fixed. Note that crop/paste, 32-bit ImageDraw, and ImageFont - are still weak spots in this release. +- Fixed some last minute glitches in the last alpha release + (Types instead of types in Image.py, version numbers, etc.) - + Added "putpalette" method to the Image class. You can use this - to add or modify the palette for "P" and "L" images. If a palette - is added to an "L" image, it is automatically converted to a "P" - image. +- Eliminated some more bogus compiler warnings. - + Fixed ImageDraw to properly handle 32-bit image memories - ("RGB", "RGBA", "CMYK", "F") +- Various fixes to make PIL compile and run smoother on Macs + (from Jack Jansen). - + Fixed "fromstring" and "tostring" not to mess up the mode attribute - in default mode. +- Fixed "fromstring" and "tostring" for mode "I" images. - + Changed ImPlatform.h to work on CRAY's (don't have one at home, so I - haven't tried it). The previous version assumed that either "short" - or "int" were 16-bit wide. PIL still won't compile on platforms where - neither "short", "int" nor "long" are 32-bit wide. +0.3a3 +----- - + Added file= and data= keyword arguments to PhotoImage and BitmapImage. - This allows you to use them as drop-in replacements for the corre- - sponding Tkinter classes. +The test suite includes 530 individual tests. - + Removed bogus references to the crack coder (ImagingCrack). +- Eliminated unexpected side-effect in "paste" with matte. "paste" + now works properly also if compiled with "gcc". - (0.3a1 released) +- Adapted to Python 1.5 (build issues only) - + Make sure image is loaded in "tostring". +- Fixed the ImageDraw "point" method to draw also the last + point (!). - + Added floating point packer (native 32-bit floats only). +- Added "I" and "RGBX" support to Image.new. - *** Changes from release 0.1b1 to 0.2 (b5) *** +- The plugin path is now properly prepended to the module search + path when a plugin module is imported. - + Modified "fromstring" and "tostring" methods to use file codecs. - Also added "fromstring" factory method to create an image directly - from data in a string. +- Added "draw" method to the ImageWin.Dib class. This is used by + Topaz to print images on Windows printers. - + Added support for 32-bit floating point images (mode "F"). You - can convert between "L" and "F" images, and apply a subset of the - available image processing methods on the "F" image. You can also - read virtually any data format into a floating point image memory; - see the section on "Decoding Floating Point Data" in the handbook - for more information. +- "convert" now supports conversions from "P" to "1" and "F". - (0.2b5 released; on windows only) +- "paste" can now take a colour instead of an image as the first argument. + The colour must match the colour argument given to the new function, and + match the mode of the target image. - + Fixed the tobitmap() method to work properly for small bitmaps. +- Fixed "paste" to allow a mask also for mode "F" images. - + Added RMS and standard deviation to the ImageStat.Stat class. Also - modified the constructor to take an optional feature mask, and also - to accept either an image or a list containing the histogram data. +- The BMP driver now saves mode "1" images. When loading images, the mode + is set to "L" for 8-bit files with greyscale palettes, and to "P" for + other 8-bit files. - + The BitmapImage code in ImageTk can now use a special bitmap - decoder, which has to be patched into Tk. See the "Tk/pilbitmap.txt" - file for details. If not installed, bitmaps are transferred to Tk as - XBM strings. +- The IM driver now reads and saves "1" images (file modes "0 1" or "L 1"). - + The PhotoImage code in ImageTk now uses a Tcl command ("PyImagingPaste") - instead of a special image type. This gives somewhat better performance, - and also allows PIL to support transparency. - *** WARNING: TKAPPINIT MUST BE MODIFIED *** +- The JPEG and GIF drivers now saves "1" images. For JPEG, the image + is saved as 8-bit greyscale (it will load as mode "L"). For GIF, the + image will be loaded as a "P" image. - + ImageTk now honours the alpha layer in RGBA images. Only fully - transparent pixels are made transparent (that is, the alpha layer - is treated as a mask). To treat the alpha laters as a matte, you - must paste the image on the background before handing it over to - ImageTk. +- Fixed a potential buffer overrun in the GIF encoder. - + Added McIdas reader (supports 8-bit images only). +0.3a2 +----- - + PIL now preloads drivers for BMP, GIF, JPEG, PPM, and TIFF. As - long as you only load and save these formats, you don't have to - wait for a full scan for drivers. To force scanning, call the - Image.init() function. +The test suite includes 400 individual tests. - + The "seek" and "tell" methods are now always available, also for - single-frame images. +- Improvements to the test suite revealed a number of minor bugs, which + are all fixed. Note that crop/paste, 32-bit ImageDraw, and ImageFont + are still weak spots in this release. - + Added optional mask argument to histogram method. The mask may - be an "1" or "L" image with the same size as the original image. - Only pixels where the mask is non-zero are included in the - histogram. +- Added "putpalette" method to the Image class. You can use this + to add or modify the palette for "P" and "L" images. If a palette + is added to an "L" image, it is automatically converted to a "P" + image. - + The "paste" method now allows you to specify only the lower left - corner (a 2-tuple), instead of the full region (a 4-tuple). +- Fixed ImageDraw to properly handle 32-bit image memories + ("RGB", "RGBA", "CMYK", "F") - + Reverted to old plugin scanning model; now scans all directory - names in the path when looking for plugins. +- Fixed "fromstring" and "tostring" not to mess up the mode attribute + in default mode. - + Added PIXAR raster support. Only uncompressed ("dumped") RGB - images can currently be read (based on information provided - by Greg Coats). +- Changed ImPlatform.h to work on CRAY's (don't have one at home, so I + haven't tried it). The previous version assumed that either "short" + or "int" were 16-bit wide. PIL still won't compile on platforms where + neither "short", "int" nor "long" are 32-bit wide. - + Added FlashPix (FPX) read support. Reads all pixel formats, but - only the highest resolution is read, and the viewing transform is - currently ignored. +- Added file= and data= keyword arguments to PhotoImage and BitmapImage. + This allows you to use them as drop-in replacements for the corre- + sponding Tkinter classes. - + Made PNG encoding somewhat more efficient in "optimize" mode; a - bug in 0.2b4 didn't enable all predictor filters when optimized - storage were requested. +- Removed bogus references to the crack coder (ImagingCrack). - + Added Microsoft Image Composer (MIC) read support. When opened, - the first sprite in the file is loaded. You can use the seek method - to load additional sprites from the file. +0.3a1 +----- - + Properly reads "P" and "CMYK" PSD images. +- Make sure image is loaded in "tostring". - + "pilconvert" no longer optimizes by default; use the -o option to - make the file as small as possible (at the expense of speed); use - the -q option to set the quality when compressing to JPEG. +- Added floating point packer (native 32-bit floats only). - + Fixed "crop" not to drop the palette for "P" images. +0.1b1 to 0.2 (b5) +----------------- - + Added and verified FLC support. +- Modified "fromstring" and "tostring" methods to use file codecs. + Also added "fromstring" factory method to create an image directly + from data in a string. - + Paste with "L" or "RGBA" alpha is now several times faster on most - platforms. +- Added support for 32-bit floating point images (mode "F"). You + can convert between "L" and "F" images, and apply a subset of the + available image processing methods on the "F" image. You can also + read virtually any data format into a floating point image memory; + see the section on "Decoding Floating Point Data" in the handbook + for more information. - + Changed Image.new() to initialize the image to black, as described - in the handbook. To get an uninitialized image, use None as the - colour. +0.2b5 released; on windows only +------------------------------- - + Fixed the PDF encoder to produce a valid header; Acrobat no longer - complains when you load PDF images created by PIL. +- Fixed the tobitmap() method to work properly for small bitmaps. - + PIL only scans fully-qualified directory names in the path when - looking for plugins. - *** WARNING: MAY BREAK EXISTING CODE *** +- Added RMS and standard deviation to the ImageStat.Stat class. Also + modified the constructor to take an optional feature mask, and also + to accept either an image or a list containing the histogram data. - + Faster implementation of "save" used when filename is given, - or when file object has "fileno" and "flush" methods. +- The BitmapImage code in ImageTk can now use a special bitmap + decoder, which has to be patched into Tk. See the "Tk/pilbitmap.txt" + file for details. If not installed, bitmaps are transferred to Tk as + XBM strings. - + Don't crash in "crop" if region extends outside the source image. +- The PhotoImage code in ImageTk now uses a Tcl command ("PyImagingPaste") + instead of a special image type. This gives somewhat better performance, + and also allows PIL to support transparency. + **WARNING: TKAPPINIT MUST BE MODIFIED** - + Eliminated a massive memory leak in the "save" function. +- ImageTk now honours the alpha layer in RGBA images. Only fully + transparent pixels are made transparent (that is, the alpha layer + is treated as a mask). To treat the alpha laters as a matte, you + must paste the image on the background before handing it over to + ImageTk. - + The GIF decoder doesn't crash if the code size is set to an illegal - value. This could happen since another bug didn't handle local - palettes properly if they didn't have the same size as the - global palette (not very common). +- Added McIdas reader (supports 8-bit images only). - + Added predictor support (TIFF 6.0 section 14) to the TIFF decoder. +- PIL now preloads drivers for BMP, GIF, JPEG, PPM, and TIFF. As + long as you only load and save these formats, you don't have to + wait for a full scan for drivers. To force scanning, call the + Image.init() function. - + Fixed palette and padding problems in BMP driver. Now properly - writes "1", "L", "P" and "RGB" images. +- The "seek" and "tell" methods are now always available, also for + single-frame images. - + Fixed getpixel()/getdata() to return correct pixel values. +- Added optional mask argument to histogram method. The mask may + be an "1" or "L" image with the same size as the original image. + Only pixels where the mask is non-zero are included in the + histogram. - + Added PSD (PhotoShop) read support. Reads both uncompressed - and compressed images of most types. +- The "paste" method now allows you to specify only the lower left + corner (a 2-tuple), instead of the full region (a 4-tuple). - + Added GIF write support (writes "uncompressed" GIF files only, - due to unresolvable licensing issues). The "gifmaker.py" script - can be used to create GIF animations. +- Reverted to old plugin scanning model; now scans all directory + names in the path when looking for plugins. - + Reads 8-bit "L" and "P" TGA images. Also reads 16-bit "RGB" - images. +- Added PIXAR raster support. Only uncompressed ("dumped") RGB + images can currently be read (based on information provided + by Greg Coats). - + Added FLI read support. This driver has only been tested - on a few FLI samples. +- Added FlashPix (FPX) read support. Reads all pixel formats, but + only the highest resolution is read, and the viewing transform is + currently ignored. - + Reads 2-bit and 4-bit PCX images. +- Made PNG encoding somewhat more efficient in "optimize" mode; a + bug in 0.2b4 didn't enable all predictor filters when optimized + storage were requested. - + Added MSP read and write support. Both version 1 and 2 can be - read, but only version 1 (uncompressed) files are written. +- Added Microsoft Image Composer (MIC) read support. When opened, + the first sprite in the file is loaded. You can use the seek method + to load additional sprites from the file. - + Fixed a bug in the FLI/FLC identification code that caused the - driver to raise an exception when parsing valid FLI/FLC files. +- Properly reads "P" and "CMYK" PSD images. - + Improved performance when loading file format plugins, and when - opening files. +- "pilconvert" no longer optimizes by default; use the -o option to + make the file as small as possible (at the expense of speed); use + the -q option to set the quality when compressing to JPEG. - + Added GIF animation support, via the "seek" and "tell" methods. - You can use "player.py" to play an animated GIF file. +- Fixed "crop" not to drop the palette for "P" images. - + Removed MNG support, since the spec is changing faster than I - can change the code. I've added support for the experimental - ARG format instead. Contact me for more information on this - format. +- Added and verified FLC support. - + Added keyword options to the "save" method. The following options - are currently supported: +- Paste with "L" or "RGBA" alpha is now several times faster on most + platforms. - format option description - -------------------------------------------------------- - JPEG optimize minimize output file at the - expense of compression speed. +- Changed Image.new() to initialize the image to black, as described + in the handbook. To get an uninitialized image, use None as the + colour. - JPEG progressive enable progressive output. the - option value is ignored. +- Fixed the PDF encoder to produce a valid header; Acrobat no longer + complains when you load PDF images created by PIL. - JPEG quality set compression quality (1-100). - the default value is 75. +- PIL only scans fully-qualified directory names in the path when + looking for plugins. + **WARNING: MAY BREAK EXISTING CODE** - JPEG smooth smooth dithered images. value - is strength (1-100). default is - off (0). +- Faster implementation of "save" used when filename is given, + or when file object has "fileno" and "flush" methods. - PNG optimize minimize output file at the - expense of compression speed. +- Don't crash in "crop" if region extends outside the source image. - Expect more options in future releases. Also note that - file writers silently ignore unknown options. +- Eliminated a massive memory leak in the "save" function. - + Plugged memory leaks in the PNG and TIFF decoders. +- The GIF decoder doesn't crash if the code size is set to an illegal + value. This could happen since another bug didn't handle local + palettes properly if they didn't have the same size as the + global palette (not very common). - + Added PNG write support. +- Added predictor support (TIFF 6.0 section 14) to the TIFF decoder. - + (internal) RGB unpackers and converters now set the pad byte - to 255 (full opacity). +- Fixed palette and padding problems in BMP driver. Now properly + writes "1", "L", "P" and "RGB" images. - + Properly handles the "transparency" property for GIF, PNG - and XPM files. +- Fixed getpixel()/getdata() to return correct pixel values. - + Added a "putalpha" method, allowing you to attach a "1" or "L" - image as the alpha layer to an "RGBA" image. +- Added PSD (PhotoShop) read support. Reads both uncompressed + and compressed images of most types. - + Various improvements to the sample scripts: +- Added GIF write support (writes "uncompressed" GIF files only, + due to unresolvable licensing issues). The "gifmaker.py" script + can be used to create GIF animations. - "pilconvert" Carries out some extra tricks in order to make - the resulting file as small as possible. +- Reads 8-bit "L" and "P" TGA images. Also reads 16-bit "RGB" + images. - "explode" (NEW) Split an image sequence into individual frames. +- Added FLI read support. This driver has only been tested + on a few FLI samples. - "gifmaker" (NEW) Convert a sequence file into a GIF animation. - Note that the GIF encoder create "uncompressed" GIF - files, so animations created by this script are - rather large (typically 2-5 times the compressed - sizes). +- Reads 2-bit and 4-bit PCX images. - "image2py" (NEW) Convert a single image to a python module. See - comments in this script for details. +- Added MSP read and write support. Both version 1 and 2 can be + read, but only version 1 (uncompressed) files are written. - "player" If multiple images are given on the command line, - they are interpreted as frames in a sequence. The - script assumes that they all have the same size. - Also note that this script now can play FLI/FLC - and GIF animations. +- Fixed a bug in the FLI/FLC identification code that caused the + driver to raise an exception when parsing valid FLI/FLC files. - This player can also execute embedded Python - animation applets (ARG format only). +- Improved performance when loading file format plugins, and when + opening files. - "viewer" Transparent images ("P" with transparency property, - and "RGBA") are superimposed on the standard Tk back- - ground. +- Added GIF animation support, via the "seek" and "tell" methods. + You can use "player.py" to play an animated GIF file. - + Fixed colour argument to "new". For multilayer images, pass a - tuple: (Red, Green, Blue), (Red, Green, Blue, Alpha), or (Cyan, - Magenta, Yellow, Black). +- Removed MNG support, since the spec is changing faster than I + can change the code. I've added support for the experimental + ARG format instead. Contact me for more information on this + format. - + Added XPM (X pixmap) read support. +- Added keyword options to the "save" method. The following options + are currently supported: - (0.2b3 released) + .. list-table:: + :widths: 25 25 50 + :header-rows: 1 - + Added MNG (multi-image network graphics) read support. "Ming" - is a proposed animation standard, based on the PNG file format. + * - Format + - Option + - Description + * - JPEG + - optimize + - Minimize output file at the expense of compression speed. + * - JPEG + - progressive + - Enable progressive output. The option value is ignored. + * - JPEG + - quality + - Set compression quality (1-100). The default value is 75. + * - JPEG + - smooth + - Smooth dithered images. Value is strength (1-100). Default is off (0). + * - PNG + - optimize + - Minimize output file at the expense of compression speed. - You can use the "player" sample script to display some flavours - of this format. The MNG standard is still under development, - as is this driver. More information, including sample files, - can be found at + Expect more options in future releases. Also note that + file writers silently ignore unknown options. - + Added a "verify" method to images loaded from file. This method - scans the file for errors, without actually decoding the image - data, and raises a suitable exception if it finds any problems. - Currently implemented for PNG and MNG files only. +- Plugged memory leaks in the PNG and TIFF decoders. - + Added support for interlaced GIF images. +- Added PNG write support. - + Added PNG read support -- if linked with the ZLIB compression library, - PIL reads all kinds of PNG images, except interlaced files. +- (internal) RGB unpackers and converters now set the pad byte + to 255 (full opacity). - + Improved PNG identification support -- doesn't mess up on unknown - chunks, identifies all possible PNG modes, and verifies checksum - on PNG header chunks. +- Properly handles the "transparency" property for GIF, PNG + and XPM files. - + Added an experimental reader for placable Windows Meta Files (WMF). - This reader is still very incomplete, but it illustrates how PIL's - drawing capabilities can be used to render vector and metafile - formats. +- Added a "putalpha" method, allowing you to attach a "1" or "L" + image as the alpha layer to an "RGBA" image. - + Added restricted drivers for images from Image Tools (greyscale - only) and LabEye/IFUNC (common interchange modes only). +- Various improvements to the sample scripts: - + Some minor improvements to the sample scripts provided in the - "Scripts" directory. + .. list-table:: + :widths: 25 75 - + The test images have been moved to the "Images" directory. + * - pilconvert + - Carries out some extra tricks in order to make + the resulting file as small as possible. + * - explode + - (NEW) Split an image sequence into individual frames. + * - gifmaker + - (NEW) Convert a sequence file into a GIF animation. + Note that the GIF encoder create "uncompressed" GIF + files, so animations created by this script are + rather large (typically 2-5 times the compressed + sizes). + * - image2py + - (NEW) Convert a single image to a python module. See + comments in this script for details. + * - player + - If multiple images are given on the command line, + they are interpreted as frames in a sequence. The + script assumes that they all have the same size. + Also note that this script now can play FLI/FLC + and GIF animations. + + This player can also execute embedded Python + animation applets (ARG format only). + * - viewer + - Transparent images ("P" with transparency property, + and "RGBA") are superimposed on the standard Tk background. + +- Fixed colour argument to "new". For multilayer images, pass a + tuple: (Red, Green, Blue), (Red, Green, Blue, Alpha), or (Cyan, + Magenta, Yellow, Black). + +- Added XPM (X pixmap) read support. + +0.2b3 +----- + +- Added MNG (multi-image network graphics) read support. "Ming" + is a proposed animation standard, based on the PNG file format. + + You can use the "player" sample script to display some flavours + of this format. The MNG standard is still under development, + as is this driver. More information, including sample files, + can be found at + +- Added a "verify" method to images loaded from file. This method + scans the file for errors, without actually decoding the image + data, and raises a suitable exception if it finds any problems. + Currently implemented for PNG and MNG files only. + +- Added support for interlaced GIF images. + +- Added PNG read support -- if linked with the ZLIB compression library, + PIL reads all kinds of PNG images, except interlaced files. + +- Improved PNG identification support -- doesn't mess up on unknown + chunks, identifies all possible PNG modes, and verifies checksum + on PNG header chunks. + +- Added an experimental reader for placable Windows Meta Files (WMF). + This reader is still very incomplete, but it illustrates how PIL's + drawing capabilities can be used to render vector and metafile + formats. + +- Added restricted drivers for images from Image Tools (greyscale + only) and LabEye/IFUNC (common interchange modes only). + +- Some minor improvements to the sample scripts provided in the + "Scripts" directory. + +- The test images have been moved to the "Images" directory. - (0.2b2 released) - (0.2b1 released; Windows only) +0.2b2 released. 0.2b1 released for Windows only +----------------------------------------------- - + Fixed filling of complex polygons. The ImageDraw "line" and - "polygon" methods also accept Path objects. +- Fixed filling of complex polygons. The ImageDraw "line" and + "polygon" methods also accept Path objects. - + The ImageTk "PhotoImage" object can now be constructed directly - from an image. You can also pass the object itself to Tkinter, - instead of using the "image" attribute. Finally, using "paste" - on a displayed image automatically updates the display. +- The ImageTk "PhotoImage" object can now be constructed directly + from an image. You can also pass the object itself to Tkinter, + instead of using the "image" attribute. Finally, using "paste" + on a displayed image automatically updates the display. - + The ImageTk "BitmapImage" object allows you to create transparent - overlays from 1-bit images. You can pass the object itself to - Tkinter. The constructor takes the same arguments as the Tkinter - BitmapImage class; use the "foreground" option to set the colour - of the overlay. +- The ImageTk "BitmapImage" object allows you to create transparent + overlays from 1-bit images. You can pass the object itself to + Tkinter. The constructor takes the same arguments as the Tkinter + BitmapImage class; use the "foreground" option to set the colour + of the overlay. - + Added a "putdata" method to the Image class. This can be used to - load a 1-layer image with data from a sequence object or a string. - An optional floating point scale and offset can be used to adjust - the data to fit into the 8-bit pixel range. Also see the "getdata" - method. +- Added a "putdata" method to the Image class. This can be used to + load a 1-layer image with data from a sequence object or a string. + An optional floating point scale and offset can be used to adjust + the data to fit into the 8-bit pixel range. Also see the "getdata" + method. - + Added the EXTENT method to the Image "transform" method. This can - be used to quickly crop, stretch, shrink, or mirror a subregion - from another image. +- Added the EXTENT method to the Image "transform" method. This can + be used to quickly crop, stretch, shrink, or mirror a subregion + from another image. - + Adapted to Python 1.4. +- Adapted to Python 1.4. - + Added a project makefile for Visual C++ 4.x. This allows you to - easily build a dynamically linked version of PIL for Windows 95 - and NT. +- Added a project makefile for Visual C++ 4.x. This allows you to + easily build a dynamically linked version of PIL for Windows 95 + and NT. - + A Tk "booster" patch for Windows is available. It gives dramatic - performance improvements for some displays. Has been tested with - Tk 4.2 only, but is likely to work with Tk 4.1 as well. See the Tk - subdirectory for details. +- A Tk "booster" patch for Windows is available. It gives dramatic + performance improvements for some displays. Has been tested with + Tk 4.2 only, but is likely to work with Tk 4.1 as well. See the Tk + subdirectory for details. - + You can now save 1-bit images in the XBM format. In addition, the - Image class now provides a "tobitmap" method which returns a string - containing an XBM representation of the image. Quite handy to use - with Tk. +- You can now save 1-bit images in the XBM format. In addition, the + Image class now provides a "tobitmap" method which returns a string + containing an XBM representation of the image. Quite handy to use + with Tk. - + More conversions, including "RGB" to "1" and more. +- More conversions, including "RGB" to "1" and more. - (0.2a1 released) +0.2a1 +----- - + Where earlier versions accepted lists, this version accepts arbitrary - Python sequences (including strings, in some cases). A few resource - leaks were plugged in the process. +- Where earlier versions accepted lists, this version accepts arbitrary + Python sequences (including strings, in some cases). A few resource + leaks were plugged in the process. - + The Image "paste" method now allows the box to extend outside - the target image. The size of the box, the image to be pasted, - and the optional mask must still match. +- The Image "paste" method now allows the box to extend outside + the target image. The size of the box, the image to be pasted, + and the optional mask must still match. - + The ImageDraw module now supports filled polygons, outlined and - filled ellipses, and text. Font support is rudimentary, though. +- The ImageDraw module now supports filled polygons, outlined and + filled ellipses, and text. Font support is rudimentary, though. - + The Image "point" method now takes an optional mode argument, - allowing you to convert the image while translating it. Currently, - this can only be used to convert "L" or "P" images to "1" images - (creating thresholded images or "matte" masks). +- The Image "point" method now takes an optional mode argument, + allowing you to convert the image while translating it. Currently, + this can only be used to convert "L" or "P" images to "1" images + (creating thresholded images or "matte" masks). - + An Image "getpixel" method has been added. For single band images, - it returns the pixel value at a given position as an integer. - For n-band images, it returns an n-tuple of integers. +- An Image "getpixel" method has been added. For single band images, + it returns the pixel value at a given position as an integer. + For n-band images, it returns an n-tuple of integers. - + An Image "getdata" method has been added. It returns a sequence - object representing the image as a 1-dimensional array. Only len() - and [] can be used with this sequence. This method returns a - reference to the existing image data, so changes in the image - will be immediately reflected in the sequence object. +- An Image "getdata" method has been added. It returns a sequence + object representing the image as a 1-dimensional array. Only len() + and [] can be used with this sequence. This method returns a + reference to the existing image data, so changes in the image + will be immediately reflected in the sequence object. - + Fixed alignment problems in the Windows BMP writer. +- Fixed alignment problems in the Windows BMP writer. - + If converting an "RGB" image to "RGB" or "L", you can give a second - argument containing a colour conversion matrix. +- If converting an "RGB" image to "RGB" or "L", you can give a second + argument containing a colour conversion matrix. - + An Image "getbbox" method has been added. It returns the bounding - box of data in an image, considering the value 0 as background. +- An Image "getbbox" method has been added. It returns the bounding + box of data in an image, considering the value 0 as background. - + An Image "offset" method has been added. It returns a new image - where the contents of the image have been offset the given distance - in X and/or Y direction. Data wraps between edges. +- An Image "offset" method has been added. It returns a new image + where the contents of the image have been offset the given distance + in X and/or Y direction. Data wraps between edges. - + Saves PDF images. The driver creates a binary PDF 1.1 files, using - JPEG compression for "L", "RGB", and "CMYK" images, and hex encoding - (same as for PostScript) for other formats. +- Saves PDF images. The driver creates a binary PDF 1.1 files, using + JPEG compression for "L", "RGB", and "CMYK" images, and hex encoding + (same as for PostScript) for other formats. - + The "paste" method now accepts "1" masks. Zero means transparent, - any other pixel value means opaque. This is faster than using an - "L" transparency mask. +- The "paste" method now accepts "1" masks. Zero means transparent, + any other pixel value means opaque. This is faster than using an + "L" transparency mask. - + Properly writes EPS files (and properly prints images to postscript - printers as well). +- Properly writes EPS files (and properly prints images to PostScript + printers as well). - + Reads 4-bit BMP files, as well as 4 and 8-bit Windows ICO and CUR - files. Cursor animations are not supported. +- Reads 4-bit BMP files, as well as 4 and 8-bit Windows ICO and CUR + files. Cursor animations are not supported. - + Fixed alignment problems in the Sun raster loader. +- Fixed alignment problems in the Sun raster loader. - + Added "draft" and "thumbnail" methods. The draft method is used - to optimize loading of JPEG and PCD files, the thumbnail method is - used to create a thumbnail representation of an image. +- Added "draft" and "thumbnail" methods. The draft method is used + to optimize loading of JPEG and PCD files, the thumbnail method is + used to create a thumbnail representation of an image. - + Added Windows display support, via the ImageWin class (see the - handbook for details). +- Added Windows display support, via the ImageWin class (see the + handbook for details). - + Added raster conversion for EPS files. This requires GNU or Aladdin - Ghostscript, and probably works on UNIX only. +- Added raster conversion for EPS files. This requires GNU or Aladdin + Ghostscript, and probably works on UNIX only. - + Reads PhotoCD (PCD) images. The base resolution (768x512) can be - read from a PhotoCD file. +- Reads PhotoCD (PCD) images. The base resolution (768x512) can be + read from a PhotoCD file. - + Eliminated some compiler warnings. Bindings now compile cleanly in C++ - mode. Note that the Imaging library itself must be compiled in C mode. +- Eliminated some compiler warnings. Bindings now compile cleanly in C++ + mode. Note that the Imaging library itself must be compiled in C mode. - + Added "bdf2pil.py", which converts BDF fonts into images with associated - metrics. This is definitely work in progress. For info, see description - in script for details. +- Added "bdf2pil.py", which converts BDF fonts into images with associated + metrics. This is definitely work in progress. For info, see description + in script for details. - + Fixed a bug in the "ImageEnhance.py" module. +- Fixed a bug in the "ImageEnhance.py" module. - + Fixed a bug in the netpbm save hack in "GifImagePlugin.py" +- Fixed a bug in the netpbm save hack in "GifImagePlugin.py" - + Fixed 90 and 270 degree rotation of rectangular images. +- Fixed 90 and 270 degree rotation of rectangular images. - + Properly reads 8-bit TIFF palette-color images. +- Properly reads 8-bit TIFF palette-color images. - + Reads plane separated RGB and CMYK TIFF images. +- Reads plane separated RGB and CMYK TIFF images. - + Added driver debug mode. This is enabled by setting Image.DEBUG - to a non-zero value. Try the -D option to "pilfile.py" and see what - happens. +- Added driver debug mode. This is enabled by setting Image.DEBUG + to a non-zero value. Try the -D option to "pilfile.py" and see what + happens. - + Don't crash on "atend" constructs in PostScript files. +- Don't crash on "atend" constructs in PostScript files. - + Only the Image module imports _imaging directly. Other modules - should refer to the binding module as "Image.core". +- Only the Image module imports _imaging directly. Other modules + should refer to the binding module as "Image.core". - *** Changes from release 0.0 to 0.1 (b1) *** +0.0 to 0.1 (b1) +--------------- - + A handbook is available (distributed separately). +- A handbook is available (distributed separately). - + The coordinate system is changed so that (0,0) is now located - in the upper left corner. This is in compliancy with ISO 12087 - and 90% of all other image processing and graphics libraries. +- The coordinate system is changed so that (0,0) is now located + in the upper left corner. This is in compliancy with ISO 12087 + and 90% of all other image processing and graphics libraries. - + Modes "1" (bilevel) and "P" (palette) have been introduced. Note - that bilevel images are stored with one byte per pixel. +- Modes "1" (bilevel) and "P" (palette) have been introduced. Note + that bilevel images are stored with one byte per pixel. - + The Image "crop" and "paste" methods now accepts None as the - box argument, to refer to the full image (self, that is). +- The Image "crop" and "paste" methods now accepts None as the + box argument, to refer to the full image (self, that is). - + The Image "crop" method now works properly. +- The Image "crop" method now works properly. - + The Image "point" method is now available. You can use either a - lookup table or a function taking one argument. +- The Image "point" method is now available. You can use either a + lookup table or a function taking one argument. - + The Image join function has been renamed to "merge". +- The Image join function has been renamed to "merge". - + An Image "composite" function has been added. It is identical - to copy() followed by paste(mask). +- An Image "composite" function has been added. It is identical + to copy() followed by paste(mask). - + An Image "eval" function has been added. It is currently identical - to point(function); that is, only a single image can be processed. +- An Image "eval" function has been added. It is currently identical + to point(function); that is, only a single image can be processed. - + A set of channel operations has been added. See the "ImageChops" - module, test_chops.py, and the handbook for details. +- A set of channel operations has been added. See the "ImageChops" + module, test_chops.py, and the handbook for details. - + Added the "pilconvert" utility, which converts image files. Note - that the number of output formats are still quite restricted. +- Added the "pilconvert" utility, which converts image files. Note + that the number of output formats are still quite restricted. - + Added the "pilfile" utility, which quickly identifies image files - (without loading them, in most cases). +- Added the "pilfile" utility, which quickly identifies image files + (without loading them, in most cases). - + Added the "pilprint" utility, which prints image files to Postscript - printers. +- Added the "pilprint" utility, which prints image files to PostScript + printers. - + Added a rudimentary version of the "pilview" utility, which is - simple image viewer based on Tk. Only File/Exit and Image/Next - works properly. +- Added a rudimentary version of the "pilview" utility, which is + simple image viewer based on Tk. Only File/Exit and Image/Next + works properly. - + An interface to Tk has been added. See "Lib/ImageTk.py" and README - for details. +- An interface to Tk has been added. See "Lib/ImageTk.py" and README + for details. - + An interface to Jack Jansen's Img library has been added (thanks to - Jack). This allows you to read images through the Img extensions file - format handlers. See the file "Lib/ImgExtImagePlugin.py" for details. +- An interface to Jack Jansen's Img library has been added (thanks to + Jack). This allows you to read images through the Img extensions file + format handlers. See the file "Lib/ImgExtImagePlugin.py" for details. - + Postscript printing is provided through the PSDraw module. See the - handbook for details. +- PostScript printing is provided through the PSDraw module. See the + handbook for details. diff --git a/LICENSE b/LICENSE index 80456a75386..40aabc3239f 100644 --- a/LICENSE +++ b/LICENSE @@ -5,12 +5,26 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2018 by Alex Clark and contributors + Copyright © 2010-2022 by Alex Clark and contributors -Like PIL, Pillow is licensed under the open source PIL Software License: +Like PIL, Pillow is licensed under the open source HPND License: -By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply with the following terms and conditions: +By obtaining, using, and/or copying this software and/or its associated +documentation, you agree that you have read, understood, and will comply +with the following terms and conditions: -Permission to use, copy, modify, and distribute this software and its associated documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. +Permission to use, copy, modify, and distribute this software and its +associated documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appears in all copies, and that +both that copyright notice and this permission notice appear in supporting +documentation, and that the name of Secret Labs AB or the author not be +used in advertising or publicity pertaining to distribution of the software +without specific, written prior permission. -SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. +IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, +INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index 865e51697db..08f6dfc0877 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,32 +1,32 @@ - include *.c include *.h include *.in +include *.lock include *.md include *.py include *.rst include *.sh include *.txt +include *.yaml include LICENSE include Makefile +include Pipfile +include tox.ini graft Tests graft src -graft Tk graft depends graft winbuild graft docs -prune docs/_static # build/src control detritus +exclude .appveyor.yml +exclude .clang-format exclude .coveragerc -exclude codecov.yml exclude .editorconfig -exclude .landscape.yaml -exclude .travis -exclude .travis/* -exclude appveyor.yml -exclude build_children.sh -exclude tox.ini +exclude .readthedocs.yml +exclude codecov.yml +exclude renovate.json global-exclude .git* global-exclude *.pyc global-exclude *.so +prune .ci diff --git a/Makefile b/Makefile index 1e888ee3512..8f2862948a8 100644 --- a/Makefile +++ b/Makefile @@ -1,106 +1,120 @@ -# https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html -.PHONY: clean coverage doc docserve help inplace install install-req release-test sdist test upload upload-test -.DEFAULT_GOAL := release-test +.DEFAULT_GOAL := help +.PHONY: clean clean: - python setup.py clean + python3 setup.py clean rm src/PIL/*.so || true rm -r build || true find . -name __pycache__ | xargs rm -r || true -BRANCHES=`git branch -a | grep -v HEAD | grep -v master | grep remote` -co: - -for i in $(BRANCHES) ; do \ - git checkout -t $$i ; \ - done - +.PHONY: coverage coverage: - python selftest.py - python setup.py test + python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest + python3 -m pytest -qq rm -r htmlcov || true - coverage report + python3 -c "import coverage" > /dev/null 2>&1 || python3 -m pip install coverage + python3 -m coverage report +.PHONY: doc doc: + python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . $(MAKE) -C docs html +.PHONY: doccheck doccheck: - $(MAKE) -C docs html + $(MAKE) doc # Don't make our tests rely on the links in the docs being up every single build. # We don't control them. But do check, and update them to the target of their redirects. $(MAKE) -C docs linkcheck || true +.PHONY: docserve docserve: - cd docs/_build/html && python -mSimpleHTTPServer 2> /dev/null& + cd docs/_build/html && python3 -m http.server 2> /dev/null& +.PHONY: help help: @echo "Welcome to Pillow development. Please use \`make \` where is one of" @echo " clean remove build products" @echo " coverage run coverage test (in progress)" - @echo " doc make html docs" - @echo " docserve run an http server on the docs directory" + @echo " doc make HTML docs" + @echo " docserve run an HTTP server on the docs directory" @echo " html to make standalone HTML files" @echo " inplace make inplace extension" @echo " install make and install" @echo " install-coverage make and install with C coverage" - @echo " install-req install documentation and test dependencies" - @echo " install-venv install in virtualenv" + @echo " lint run the lint checks" + @echo " lint-fix run Black and isort to (mostly) fix lint issues" @echo " release-test run code and package tests before release" - @echo " test run tests on installed pillow" - @echo " upload build and upload sdists to PyPI" - @echo " upload-test build and upload sdists to test.pythonpackages.com" + @echo " test run tests on installed Pillow" +.PHONY: inplace inplace: clean - python setup.py develop build_ext --inplace + python3 -m pip install -e --global-option="build_ext" --global-option="--inplace" . +.PHONY: install install: - python setup.py install - python selftest.py + python3 -m pip install . + python3 selftest.py +.PHONY: install-coverage install-coverage: - CFLAGS="-coverage" python setup.py build_ext install - python selftest.py + CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip install --global-option="build_ext" . + python3 selftest.py +.PHONY: debug debug: # make a debug version if we don't have a -dbg python. Leaves in symbols # for our stuff, kills optimization, and redirects to dev null so we # see any build failures. make clean > /dev/null - CFLAGS='-g -O0' python setup.py build_ext install > /dev/null - -install-req: - pip install -r requirements.txt - -install-venv: - virtualenv . - bin/pip install -r requirements.txt + CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /dev/null +.PHONY: release-test release-test: - $(MAKE) install-req - python setup.py develop - python selftest.py - python -m pytest Tests - python setup.py install - python -m pytest -qq - check-manifest - pyroma . - viewdoc - + python3 -m pip install -e .[tests] + python3 selftest.py + python3 -m pytest Tests + python3 -m pip install . + -rm dist/*.egg + -rmdir dist + python3 -m pytest -qq + python3 -m check_manifest + python3 -m pyroma . + $(MAKE) readme + +.PHONY: sdist sdist: - python setup.py sdist --format=gztar + python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build + python3 -m build --sdist + python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine + python3 -m twine check --strict dist/* +.PHONY: test test: - pytest -qq + python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest + python3 -m pytest -qq -# https://docs.python.org/2/distutils/packageindex.html#the-pypirc-file -upload-test: -# [test] -# username: -# password: -# repository = http://test.pythonpackages.com - python setup.py sdist --format=gztar upload -r test - -upload: - python setup.py sdist --format=gztar upload +.PHONY: valgrind +valgrind: + python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind + PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ + --log-file=/tmp/valgrind-output \ + python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output +.PHONY: readme readme: - viewdoc + python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 + python3 -m markdown2 README.md > .long-description.html && open .long-description.html + + +.PHONY: lint +lint: + python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox + python3 -m tox -e lint + +.PHONY: lint-fix +lint-fix: + python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black + python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort + python3 -m black --target-version py37 . + python3 -m isort . diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000000..1e611a63ce7 --- /dev/null +++ b/Pipfile @@ -0,0 +1,22 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +black = "*" +check-manifest = "*" +coverage = "*" +defusedxml = "*" +packaging = "*" +markdown2 = "*" +olefile = "*" +pyroma = "*" +pytest = "*" +pytest-cov = "*" +pytest-timeout = "*" + +[dev-packages] + +[requires] +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 00000000000..600b19050f5 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,324 @@ +{ + "_meta": { + "hash": { + "sha256": "e5cad23bf4187647d53b613a64dc4792b7064bf86b08dfb5737580e32943f54d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "attrs": { + "hashes": [ + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.2.0" + }, + "black": { + "hashes": [ + "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3", + "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f" + ], + "index": "pypi", + "version": "==21.12b0" + }, + "build": { + "hashes": [ + "sha256:1aaadcd69338252ade4f7ec1265e1a19184bf916d84c9b7df095f423948cb89f", + "sha256:21b7ebbd1b22499c4dac536abc7606696ea4d909fd755e00f09f3c0f2c05e3c8" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "certifi": { + "hashes": [ + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" + ], + "version": "==2021.10.8" + }, + "charset-normalizer": { + "hashes": [ + "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", + "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c" + ], + "markers": "python_version >= '3'", + "version": "==2.0.9" + }, + "check-manifest": { + "hashes": [ + "sha256:365c94d65de4c927d9d8b505371d08ee19f9f369c86b9ac3db97c2754c827c95", + "sha256:56dadd260a9c7d550b159796d2894b6d0bcc176a94cbc426d9bb93e5e48d12ce" + ], + "index": "pypi", + "version": "==0.47" + }, + "click": { + "hashes": [ + "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", + "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" + ], + "markers": "python_version >= '3.6'", + "version": "==8.0.3" + }, + "coverage": { + "hashes": [ + "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0", + "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd", + "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884", + "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48", + "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76", + "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0", + "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64", + "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685", + "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47", + "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d", + "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840", + "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f", + "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971", + "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c", + "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a", + "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de", + "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17", + "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4", + "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521", + "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57", + "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b", + "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282", + "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644", + "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475", + "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d", + "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da", + "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953", + "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2", + "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e", + "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c", + "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc", + "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64", + "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74", + "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617", + "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3", + "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d", + "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa", + "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739", + "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8", + "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8", + "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781", + "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58", + "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9", + "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c", + "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd", + "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e", + "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49" + ], + "index": "pypi", + "version": "==6.2" + }, + "defusedxml": { + "hashes": [ + "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", + "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" + ], + "index": "pypi", + "version": "==0.7.1" + }, + "docutils": { + "hashes": [ + "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c", + "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.18.1" + }, + "idna": { + "hashes": [ + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + ], + "markers": "python_version >= '3'", + "version": "==3.3" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "markdown2": { + "hashes": [ + "sha256:8f4ac8d9a124ab408c67361090ed512deda746c04362c36c2ec16190c720c2b0", + "sha256:91113caf23aa662570fe21984f08fe74f814695c0a0ea8e863a8b4c4f63f9f6e" + ], + "index": "pypi", + "version": "==2.4.2" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "olefile": { + "hashes": [ + "sha256:133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964" + ], + "index": "pypi", + "version": "==0.46" + }, + "packaging": { + "hashes": [ + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + ], + "index": "pypi", + "version": "==21.3" + }, + "pathspec": { + "hashes": [ + "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", + "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" + ], + "version": "==0.9.0" + }, + "pep517": { + "hashes": [ + "sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0", + "sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161" + ], + "version": "==0.12.0" + }, + "platformdirs": { + "hashes": [ + "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", + "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" + ], + "markers": "python_version >= '3.6'", + "version": "==2.4.0" + }, + "pluggy": { + "hashes": [ + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" + ], + "markers": "python_version >= '3.6'", + "version": "==1.0.0" + }, + "py": { + "hashes": [ + "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", + "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.11.0" + }, + "pygments": { + "hashes": [ + "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", + "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" + ], + "markers": "python_version >= '3.5'", + "version": "==2.10.0" + }, + "pyparsing": { + "hashes": [ + "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", + "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.6" + }, + "pyroma": { + "hashes": [ + "sha256:0fba67322913026091590e68e0d9e0d4fbd6420fcf34d315b2ad6985ab104d65", + "sha256:f8c181e0d5d292f11791afc18f7d0218a83c85cf64d6f8fb1571ce9d29a24e4a" + ], + "index": "pypi", + "version": "==3.2" + }, + "pytest": { + "hashes": [ + "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", + "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" + ], + "index": "pypi", + "version": "==6.2.5" + }, + "pytest-cov": { + "hashes": [ + "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", + "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" + ], + "index": "pypi", + "version": "==3.0.0" + }, + "pytest-timeout": { + "hashes": [ + "sha256:e6f98b54dafde8d70e4088467ff621260b641eb64895c4195b6e5c8f45638112", + "sha256:fe9c3d5006c053bb9e062d60f641e6a76d6707aedb645350af9593e376fcc717" + ], + "index": "pypi", + "version": "==2.0.2" + }, + "requests": { + "hashes": [ + "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", + "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==2.26.0" + }, + "setuptools": { + "hashes": [ + "sha256:5ec2bbb534ed160b261acbbdd1b463eb3cf52a8d223d96a8ab9981f63798e85c", + "sha256:75fd345a47ce3d79595b27bf57e6f49c2ca7904f3c7ce75f8a87012046c86b0b" + ], + "markers": "python_version >= '3.7'", + "version": "==60.0.0" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==0.10.2" + }, + "tomli": { + "hashes": [ + "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f", + "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c" + ], + "markers": "python_version >= '3.6'", + "version": "==1.2.3" + }, + "typing-extensions": { + "hashes": [ + "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", + "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" + ], + "markers": "python_version >= '3.6'", + "version": "==4.0.1" + }, + "urllib3": { + "hashes": [ + "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", + "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.7" + } + }, + "develop": {} +} diff --git a/README.md b/README.md new file mode 100644 index 00000000000..e7c0ebc5aa5 --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +

+ Pillow logo +

+ +# Pillow + +## Python Imaging Library (Fork) + +Pillow is the friendly PIL fork by [Alex Clark and +Contributors](https://github.com/python-pillow/Pillow/graphs/contributors). +PIL is the Python Imaging Library by Fredrik Lundh and Contributors. +As of 2019, Pillow development is +[supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise). + + + + + + + + + + + + + + + + + + +
docs + Documentation Status +
tests + GitHub Actions build status (Lint) + GitHub Actions build status (Test Linux and macOS) + GitHub Actions build status (Test Windows) + GitHub Actions build status (Test MinGW) + GitHub Actions build status (Test Cygwin) + GitHub Actions build status (Test Docker) + AppVeyor CI build status (Windows) + GitHub Actions wheels build status (Wheels) + Travis CI wheels build status (aarch64) + Code coverage + Tidelift Align +
package + Zenodo + Tidelift + Newest PyPI version + Number of PyPI downloads + OpenSSF Best Practices +
social + Join the chat at https://gitter.im/python-pillow/Pillow + Follow on https://twitter.com/PythonPillow +
+ +## Overview + +The Python Imaging Library adds image processing capabilities to your Python interpreter. + +This library provides extensive file format support, an efficient internal representation, and fairly powerful image processing capabilities. + +The core image library is designed for fast access to data stored in a few basic pixel formats. It should provide a solid foundation for a general image processing tool. + +## More Information + +- [Documentation](https://pillow.readthedocs.io/) + - [Installation](https://pillow.readthedocs.io/en/latest/installation.html) + - [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html) +- [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md) + - [Issues](https://github.com/python-pillow/Pillow/issues) + - [Pull requests](https://github.com/python-pillow/Pillow/pulls) +- [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html) +- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) + - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork) + +## Report a Vulnerability + +To report a security vulnerability, please follow the procedure described in the [Tidelift security policy](https://tidelift.com/docs/security). diff --git a/README.rst b/README.rst deleted file mode 100644 index e217ba29d5e..00000000000 --- a/README.rst +++ /dev/null @@ -1,73 +0,0 @@ -Pillow -====== - -Python Imaging Library (Fork) ------------------------------ - -Pillow is the friendly PIL fork by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. - -.. start-badges - -.. list-table:: - :stub-columns: 1 - - * - docs - - |docs| - * - tests - - | |linux| |macos| |windows| |coverage| - * - package - - |zenodo| |version| - * - chat - - |gitter| - -.. |docs| image:: https://readthedocs.org/projects/pillow/badge/?version=latest - :target: https://pillow.readthedocs.io/?badge=latest - :alt: Documentation Status - -.. |linux| image:: https://img.shields.io/travis/python-pillow/Pillow/master.svg?label=Linux%20build - :target: https://travis-ci.org/python-pillow/Pillow - :alt: Travis CI build status (Linux) - -.. |macos| image:: https://img.shields.io/travis/python-pillow/pillow-wheels/latest.svg?label=macOS%20build - :target: https://travis-ci.org/python-pillow/pillow-wheels - :alt: Travis CI build status (macOS) - -.. |windows| image:: https://img.shields.io/appveyor/ci/python-pillow/Pillow/master.svg?label=Windows%20build - :target: https://ci.appveyor.com/project/python-pillow/Pillow - :alt: AppVeyor CI build status (Windows) - -.. |coverage| image:: https://coveralls.io/repos/python-pillow/Pillow/badge.svg?branch=master&service=github - :target: https://coveralls.io/github/python-pillow/Pillow?branch=master - :alt: Code coverage - -.. |zenodo| image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg - :target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow - -.. |version| image:: https://img.shields.io/pypi/v/pillow.svg - :target: https://pypi.python.org/pypi/Pillow/ - :alt: Latest PyPI version - -.. |gitter| image:: https://badges.gitter.im/python-pillow/Pillow.svg - :target: https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge - :alt: Join the chat at https://gitter.im/python-pillow/Pillow - -.. end-badges - - - -More Information ----------------- - -- `Documentation `_ - - - `Installation `_ - - `Handbook `_ - -- `Contribute `_ - - - `Issues `_ - - `Pull requests `_ - -- `Changelog `_ - - - `Pre-fork `_ diff --git a/RELEASING.md b/RELEASING.md index d72401bd58c..b0506748470 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,106 +1,127 @@ # Release Checklist +See https://pillow.readthedocs.io/en/stable/releasenotes/versioning.html for +information about how the version numbers line up with releases. + ## Main Release -Released quarterly on the first day of January, April, July, October. +Released quarterly on January 2nd, April 1st, July 1st and October 15th. -* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/1174 -* [ ] Develop and prepare release in ``master`` branch. -* [ ] Check [Travis CI](https://travis-ci.org/python-pillow/Pillow) and [AppVeyor CI](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in ``master`` branch. -* [ ] Check that all of the wheel builds [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels) pass the tests in TravisCI. -* [ ] In compliance with https://www.python.org/dev/peps/pep-0440/, update version identifier in `PIL/version.py` +* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 +* [ ] Develop and prepare release in `main` branch. +* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch. +* [ ] Check that all of the wheel builds [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels) pass the tests in Travis CI and GitHub Actions. +* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Update `CHANGES.rst`. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Create branch and tag for release e.g.: -``` - $ git branch 2.9.x - $ git tag 2.9.0 - $ git push --all - $ git push --tags -``` -* [ ] Create source distributions e.g.: -``` - $ make sdist -``` -* [ ] Create [binary distributions](#binary-distributions) -* [ ] Upload all binaries and source distributions with ``twine upload dist/Pillow-4.1.0-*`` -* [ ] Manually hide old versions on PyPI such that only the latest major release is visible when viewing https://pypi.python.org/pypi/Pillow (https://pypi.python.org/pypi?:action=pkg_edit&name=Pillow) + ```bash + git branch 5.2.x + git tag 5.2.0 + git push --all + git push --tags + ``` +* [ ] Create and check source distribution: + ```bash + make sdist + ``` +* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) +* [ ] Check and upload all binaries and source distributions e.g.: + ```bash + python3 -m twine check --strict dist/* + python3 -m twine upload dist/Pillow-5.2.0* + ``` +* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) +* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` ## Point Release Released as needed for security, installation or critical bug fixes. -* [ ] Make necessary changes in ``master`` branch. +* [ ] Make necessary changes in `main` branch. * [ ] Update `CHANGES.rst`. -* [ ] Cherry pick individual commits from ``master`` branch to release branch e.g. ``2.9.x``. -* [ ] Check [Travis CI](https://travis-ci.org/python-pillow/Pillow) to confirm passing tests in release branch e.g. ``2.9.x``. -* [ ] Checkout release branch e.g.: -``` - git checkout -t remotes/origin/2.9.x -``` -* [ ] In compliance with https://www.python.org/dev/peps/pep-0440/, update version identifier in `PIL/version.py` +* [ ] Check out release branch e.g.: + ```bash + git checkout -t remotes/origin/5.2.x + ``` +* [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`. + + + +* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in release branch e.g. `5.2.x`. +* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Run pre-release check via `make release-test`. * [ ] Create tag for release e.g.: -``` - $ git tag 2.9.1 - $ git push --tags -``` -* [ ] Create source distributions e.g.: -``` - $ make sdist -``` -* [ ] Create [binary distributions](#binary-distributions) + ```bash + git tag 5.2.1 + git push + git push --tags + ``` +* [ ] Create and check source distribution: + ```bash + make sdist + ``` +* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) +* [ ] Check and upload all binaries and source distributions e.g.: + ```bash + python3 -m twine check --strict dist/* + python3 -m twine upload dist/Pillow-5.2.1* + ``` +* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) ## Embargoed Release Released as needed privately to individual vendors for critical security-related bug fixes. * [ ] Prepare patch for all versions that will get a fix. Test against local installations. -* [ ] Commit against master, cherry pick to affected release branches. +* [ ] Commit against `main`, cherry pick to affected release branches. * [ ] Run local test matrix on each release & Python version. * [ ] Privately send to distros. * [ ] Run pre-release check via `make release-test` * [ ] Amend any commits with the CVE # * [ ] On release date, tag and push to GitHub. -``` - git checkout 2.5.x - git tag 2.5.3 - git push origin 2.5.x - git push origin --tags -``` -* [ ] Create source distributions e.g.: -``` - $ make sdist -``` -* [ ] Create [binary distributions](#binary-distributions) + ```bash + git checkout 2.5.x + git tag 2.5.3 + git push origin 2.5.x + git push origin --tags + ``` +* [ ] Create and check source distribution: + ```bash + make sdist + ``` +* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) +* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) ## Binary Distributions ### Windows -* [ ] Contact @cgohlke for Windows binaries via release ticket e.g. https://github.com/python-pillow/Pillow/issues/1174. -* [ ] Download and extract tarball from @cgohlke and ``twine upload *``. +* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) + and copy into `dist/` ### Mac and Linux * [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels): -``` - $ git checkout https://github.com/python-pillow/pillow-wheels - $ cd pillow-wheels - $ git submodule init - $ git submodule update - $ cd Pillow - $ git fetch --all - $ git checkout [[release tag]] - $ cd .. - $ git commit -m "Pillow -> 2.9.0" Pillow - $ git push -``` -* [ ] Download distributions from the [Pillow Wheel Builder container](http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com/). - + ```bash + git clone https://github.com/python-pillow/pillow-wheels + cd pillow-wheels + ./update-pillow-tag.sh [[release tag]] + ``` +* [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases) + and copy into `dist/` ## Publicize Release -* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) e.g. https://twitter.com/aclark4life/status/583366798302691328. +* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 ## Documentation -* [ ] Make sure the default version for Read the Docs is the latest release version, e.g. ``3.1.x`` rather than ``latest``: https://readthedocs.org/projects/pillow/versions/ +* [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes + +## Docker Images + +* [ ] Update Pillow in the Docker Images repository + ```bash + git clone https://github.com/python-pillow/docker-images + cd docker-images + ./update-pillow-tag.sh [[release tag]] + ``` diff --git a/Tests/32bit_segfault_check.py b/Tests/32bit_segfault_check.py index a601f762e85..2ff7f908f6d 100755 --- a/Tests/32bit_segfault_check.py +++ b/Tests/32bit_segfault_check.py @@ -1,8 +1,8 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 -from PIL import Image import sys +from PIL import Image if sys.maxsize < 2**32: - im = Image.new('L', (999999, 999999), 0) + im = Image.new("L", (999999, 999999), 0) diff --git a/Tests/README.rst b/Tests/README.rst index 44f6f4792f1..2d014e5a46e 100644 --- a/Tests/README.rst +++ b/Tests/README.rst @@ -1,40 +1,32 @@ Pillow Tests ============ -Test scripts are named ``test_xxx.py`` and use the ``unittest`` module. A base class and helper functions can be found in ``helper.py``. +Test scripts are named ``test_xxx.py``. Helper classes and functions can be found in ``helper.py``. Dependencies ------------ +------------ Install:: - pip install pytest pytest-cov + python3 -m pip install pytest pytest-cov pytest-timeout Execution --------- -**If Pillow has been built in-place** - To run an individual test:: - python Tests/test_image.py + pytest Tests/test_image.py + +Or:: + + pytest -k test_image.py Run all the tests from the root of the Pillow source distribution:: - pytest -vx Tests + pytest Or with coverage:: - pytest -vx --cov PIL --cov-report term Tests + pytest --cov PIL --cov Tests --cov-report term coverage html open htmlcov/index.html - -**If Pillow has been installed** - -To run an individual test:: - - pytest -k Tests/test_image.py - -Run all the tests from the root of the Pillow source distribution:: - - pytest diff --git a/Tests/__init__.py b/Tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index 30001716811..87cad699d3b 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -1,10 +1,10 @@ -from helper import unittest, PillowTestCase, hopper - -# Not running this test by default. No DOS against Travis CI. +import time from PIL import PyAccess -import time +from .helper import hopper + +# Not running this test by default. No DOS against CI. def iterate_get(size, access): @@ -26,33 +26,33 @@ def timer(func, label, *args): starttime = time.time() for x in range(iterations): func(*args) - if time.time()-starttime > 10: - print("%s: breaking at %s iterations, %.6f per iteration" % ( - label, x+1, (time.time()-starttime)/(x+1.0))) + if time.time() - starttime > 10: + print( + "{}: breaking at {} iterations, {:.6f} per iteration".format( + label, x + 1, (time.time() - starttime) / (x + 1.0) + ) + ) break - if x == iterations-1: + if x == iterations - 1: endtime = time.time() - print("%s: %.4f s %.6f per iteration" % ( - label, endtime-starttime, (endtime-starttime)/(x+1.0))) - - -class BenchCffiAccess(PillowTestCase): - - def test_direct(self): - im = hopper() - im.load() - # im = Image.new( "RGB", (2000, 2000), (1, 3, 2)) - caccess = im.im.pixel_access(False) - access = PyAccess.new(im, False) - - self.assertEqual(caccess[(0, 0)], access[(0, 0)]) - - print("Size: %sx%s" % im.size) - timer(iterate_get, 'PyAccess - get', im.size, access) - timer(iterate_set, 'PyAccess - set', im.size, access) - timer(iterate_get, 'C-api - get', im.size, caccess) - timer(iterate_set, 'C-api - set', im.size, caccess) - - -if __name__ == '__main__': - unittest.main() + print( + "{}: {:.4f} s {:.6f} per iteration".format( + label, endtime - starttime, (endtime - starttime) / (x + 1.0) + ) + ) + + +def test_direct(): + im = hopper() + im.load() + # im = Image.new( "RGB", (2000, 2000), (1, 3, 2)) + caccess = im.im.pixel_access(False) + access = PyAccess.new(im, False) + + assert caccess[(0, 0)] == access[(0, 0)] + + print("Size: %sx%s" % im.size) + timer(iterate_get, "PyAccess - get", im.size, access) + timer(iterate_set, "PyAccess - set", im.size, access) + timer(iterate_get, "C-api - get", im.size, caccess) + timer(iterate_set, "C-api - set", im.size, caccess) diff --git a/Tests/bench_get.py b/Tests/bench_get.py deleted file mode 100644 index 51f3a6aa228..00000000000 --- a/Tests/bench_get.py +++ /dev/null @@ -1,21 +0,0 @@ -import helper -import timeit - -import sys -sys.path.insert(0, ".") - - -def bench(mode): - im = helper.hopper(mode) - get = im.im.getpixel - xy = 50, 50 # position shouldn't really matter - t0 = timeit.default_timer() - for _ in range(1000000): - get(xy) - print(mode, timeit.default_timer() - t0, "us") - -bench("L") -bench("I") -bench("I;16") -bench("F") -bench("RGB") diff --git a/Tests/check_fli_oob.py b/Tests/check_fli_oob.py new file mode 100644 index 00000000000..7b3d4d7ee9d --- /dev/null +++ b/Tests/check_fli_oob.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +from PIL import Image + +repro_ss2 = ( + "images/fli_oob/06r/06r00.fli", + "images/fli_oob/06r/others/06r01.fli", + "images/fli_oob/06r/others/06r02.fli", + "images/fli_oob/06r/others/06r03.fli", + "images/fli_oob/06r/others/06r04.fli", +) + +repro_lc = ( + "images/fli_oob/05r/05r00.fli", + "images/fli_oob/05r/others/05r03.fli", + "images/fli_oob/05r/others/05r06.fli", + "images/fli_oob/05r/others/05r05.fli", + "images/fli_oob/05r/others/05r01.fli", + "images/fli_oob/05r/others/05r04.fli", + "images/fli_oob/05r/others/05r02.fli", + "images/fli_oob/05r/others/05r07.fli", + "images/fli_oob/patch0/000000", + "images/fli_oob/patch0/000001", + "images/fli_oob/patch0/000002", + "images/fli_oob/patch0/000003", +) + + +repro_advance = ( + "images/fli_oob/03r/03r00.fli", + "images/fli_oob/03r/others/03r01.fli", + "images/fli_oob/03r/others/03r09.fli", + "images/fli_oob/03r/others/03r11.fli", + "images/fli_oob/03r/others/03r05.fli", + "images/fli_oob/03r/others/03r10.fli", + "images/fli_oob/03r/others/03r06.fli", + "images/fli_oob/03r/others/03r08.fli", + "images/fli_oob/03r/others/03r03.fli", + "images/fli_oob/03r/others/03r07.fli", + "images/fli_oob/03r/others/03r02.fli", + "images/fli_oob/03r/others/03r04.fli", +) + +repro_brun = ( + "images/fli_oob/04r/initial.fli", + "images/fli_oob/04r/others/04r02.fli", + "images/fli_oob/04r/others/04r05.fli", + "images/fli_oob/04r/others/04r04.fli", + "images/fli_oob/04r/others/04r03.fli", + "images/fli_oob/04r/others/04r01.fli", + "images/fli_oob/04r/04r00.fli", +) + +repro_copy = ( + "images/fli_oob/02r/others/02r02.fli", + "images/fli_oob/02r/others/02r04.fli", + "images/fli_oob/02r/others/02r03.fli", + "images/fli_oob/02r/others/02r01.fli", + "images/fli_oob/02r/02r00.fli", +) + + +for path in repro_ss2 + repro_lc + repro_advance + repro_brun + repro_copy: + with Image.open(path) as im: + try: + im.load() + except Exception as msg: + print(msg) diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index 9b370da3ca0..08a55d349d5 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -1,16 +1,10 @@ -from helper import unittest, PillowTestCase from PIL import Image TEST_FILE = "Tests/images/fli_overflow.fli" -class TestFliOverflow(PillowTestCase): - def test_fli_overflow(self): +def test_fli_overflow(): - # this should not crash with a malloc error or access violation - im = Image.open(TEST_FILE) + # this should not crash with a malloc error or access violation + with Image.open(TEST_FILE) as im: im.load() - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/check_icns_dos.py b/Tests/check_icns_dos.py index e56709bbb2f..a34bee45c51 100644 --- a/Tests/check_icns_dos.py +++ b/Tests/check_icns_dos.py @@ -1,11 +1,9 @@ # Tests potential DOS of IcnsImagePlugin with 0 length block. # Run from anywhere that PIL is importable. -from PIL import Image from io import BytesIO -if bytes is str: - Image.open(BytesIO(bytes('icns\x00\x00\x00\x10hang\x00\x00\x00\x00'))) -else: - Image.open(BytesIO(bytes('icns\x00\x00\x00\x10hang\x00\x00\x00\x00', - 'latin-1'))) +from PIL import Image + +with Image.open(BytesIO(b"icns\x00\x00\x00\x10hang\x00\x00\x00\x00")): + pass diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index a31cd2180a4..d07082aba9f 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -1,44 +1,45 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 +import pytest -from __future__ import division -from helper import unittest, PillowTestCase -import sys from PIL import Image +from .helper import is_win32 + min_iterations = 100 max_iterations = 10000 +pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") + + +def _get_mem_usage(): + from resource import RUSAGE_SELF, getpagesize, getrusage + + mem = getrusage(RUSAGE_SELF).ru_maxrss + return mem * getpagesize() / 1024 / 1024 + + +def _test_leak(min_iterations, max_iterations, fn, *args, **kwargs): + mem_limit = None + for i in range(max_iterations): + fn(*args, **kwargs) + mem = _get_mem_usage() + if i < min_iterations: + mem_limit = mem + 1 + continue + msg = f"memory usage limit exceeded after {i + 1} iterations" + assert mem <= mem_limit, msg + + +def test_leak_putdata(): + im = Image.new("RGB", (25, 25)) + _test_leak(min_iterations, max_iterations, im.putdata, im.getdata()) + -@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") -class TestImagingLeaks(PillowTestCase): - - def _get_mem_usage(self): - from resource import getpagesize, getrusage, RUSAGE_SELF - mem = getrusage(RUSAGE_SELF).ru_maxrss - return mem * getpagesize() / 1024 / 1024 - - def _test_leak(self, min_iterations, max_iterations, fn, *args, **kwargs): - mem_limit = None - for i in range(max_iterations): - fn(*args, **kwargs) - mem = self._get_mem_usage() - if i < min_iterations: - mem_limit = mem + 1 - continue - self.assertLessEqual(mem, mem_limit, - msg='memory usage limit exceeded after %d iterations' - % (i + 1)) - - def test_leak_putdata(self): - im = Image.new('RGB', (25, 25)) - self._test_leak(min_iterations, max_iterations, - im.putdata, im.getdata()) - - def test_leak_getlist(self): - im = Image.new('P', (25, 25)) - self._test_leak(min_iterations, max_iterations, - # Pass a new list at each iteration. - lambda: im.point(range(256))) - -if __name__ == '__main__': - unittest.main() +def test_leak_getlist(): + im = Image.new("P", (25, 25)) + _test_leak( + min_iterations, + max_iterations, + # Pass a new list at each iteration. + lambda: im.point(range(256)), + ) diff --git a/Tests/check_j2k_dos.py b/Tests/check_j2k_dos.py index 9f06888a31b..71dcea4f39f 100644 --- a/Tests/check_j2k_dos.py +++ b/Tests/check_j2k_dos.py @@ -1,13 +1,11 @@ # Tests potential DOS of Jpeg2kImagePlugin with 0 length block. # Run from anywhere that PIL is importable. -from PIL import Image from io import BytesIO -if bytes is str: - Image.open(BytesIO(bytes( - '\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang'))) -else: - Image.open(BytesIO(bytes( - '\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang', - 'latin-1'))) +from PIL import Image + +with Image.open( + BytesIO(b"\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang") +): + pass diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py index 8e9c4ca20ef..afe5836f3fd 100755 --- a/Tests/check_j2k_leaks.py +++ b/Tests/check_j2k_leaks.py @@ -1,42 +1,42 @@ -from helper import unittest, PillowTestCase -import sys -from PIL import Image from io import BytesIO +import pytest + +from PIL import Image + +from .helper import is_win32, skip_unless_feature + # Limits for testing the leak -mem_limit = 1024*1048576 -stack_size = 8*1048576 -iterations = int((mem_limit/stack_size)*2) -codecs = dir(Image.core) +mem_limit = 1024 * 1048576 +stack_size = 8 * 1048576 +iterations = int((mem_limit / stack_size) * 2) test_file = "Tests/images/rgb_trns_ycbc.jp2" +pytestmark = [ + pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"), + skip_unless_feature("jpg_2000"), +] + + +def test_leak_load(): + from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit + + setrlimit(RLIMIT_STACK, (stack_size, stack_size)) + setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) + for _ in range(iterations): + with Image.open(test_file) as im: + im.load() + + +def test_leak_save(): + from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit -@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") -class TestJpegLeaks(PillowTestCase): - def setUp(self): - if "jpeg2k_encoder" not in codecs or "jpeg2k_decoder" not in codecs: - self.skipTest('JPEG 2000 support not available') - - def test_leak_load(self): - from resource import setrlimit, RLIMIT_AS, RLIMIT_STACK - setrlimit(RLIMIT_STACK, (stack_size, stack_size)) - setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) - for _ in range(iterations): - with Image.open(test_file) as im: - im.load() - - def test_leak_save(self): - from resource import setrlimit, RLIMIT_AS, RLIMIT_STACK - setrlimit(RLIMIT_STACK, (stack_size, stack_size)) - setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) - for _ in range(iterations): - with Image.open(test_file) as im: - im.load() - test_output = BytesIO() - im.save(test_output, "JPEG2000") - test_output.seek(0) - test_output.read() - - -if __name__ == '__main__': - unittest.main() + setrlimit(RLIMIT_STACK, (stack_size, stack_size)) + setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) + for _ in range(iterations): + with Image.open(test_file) as im: + im.load() + test_output = BytesIO() + im.save(test_output, "JPEG2000") + test_output.seek(0) + test_output.read() diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index bec4ea69486..b16412898f0 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -1,14 +1,10 @@ -from PIL import Image -from helper import unittest, PillowTestCase - +import pytest -class TestJ2kEncodeOverflow(PillowTestCase): - def test_j2k_overflow(self): +from PIL import Image - im = Image.new('RGBA', (1024, 131584)) - target = self.tempfile('temp.jpc') - with self.assertRaises(IOError): - im.save(target) -if __name__ == '__main__': - unittest.main() +def test_j2k_overflow(tmp_path): + im = Image.new("RGBA", (1024, 131584)) + target = str(tmp_path / "temp.jpc") + with pytest.raises(OSError): + im.save(target) diff --git a/Tests/check_jp2_overflow.py b/Tests/check_jp2_overflow.py new file mode 100755 index 00000000000..0210505f5fe --- /dev/null +++ b/Tests/check_jp2_overflow.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +# Reproductions/tests for OOB read errors in FliDecode.c + +# When run in python, all of these images should fail for +# one reason or another, either as a buffer overrun, +# unrecognized datastream, or truncated image file. +# There shouldn't be any segfaults. +# +# if run like +# `valgrind --tool=memcheck python check_jp2_overflow.py 2>&1 | grep Decode.c` +# the output should be empty. There may be python issues +# in the valgrind especially if run in a debug python +# version. + + +from PIL import Image + +repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2") + +for path in repro: + with Image.open(path) as im: + try: + im.load() + except Exception as msg: + print(msg) diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index 7df2dfcc46d..ab8d7771992 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -1,6 +1,8 @@ -from helper import unittest, PillowTestCase, hopper from io import BytesIO -import sys + +import pytest + +from .helper import hopper, is_win32 iterations = 5000 @@ -9,16 +11,14 @@ When run on a system without the jpeg leak fixes, the valgrind runs look like this. -NOSE_PROCESSES=0 NOSE_TIMEOUT=600 valgrind --tool=massif \ - python test-installed.py -s -v Tests/check_jpeg_leaks.py +valgrind --tool=massif python test-installed.py -s -v Tests/check_jpeg_leaks.py """ -@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") -class TestJpegLeaks(PillowTestCase): +pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") - """ +""" pre patch: MB @@ -74,134 +74,140 @@ class TestJpegLeaks(PillowTestCase): """ - def test_qtables_leak(self): - im = hopper('RGB') - - standard_l_qtable = [int(s) for s in """ - 16 11 10 16 24 40 51 61 - 12 12 14 19 26 58 60 55 - 14 13 16 24 40 57 69 56 - 14 17 22 29 51 87 80 62 - 18 22 37 56 68 109 103 77 - 24 35 55 64 81 104 113 92 - 49 64 78 87 103 121 120 101 - 72 92 95 98 112 100 103 99 - """.split(None)] - - standard_chrominance_qtable = [int(s) for s in """ - 17 18 24 47 99 99 99 99 - 18 21 26 66 99 99 99 99 - 24 26 56 99 99 99 99 99 - 47 66 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - """.split(None)] - - qtables = [standard_l_qtable, - standard_chrominance_qtable] - - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG", qtables=qtables) - - def test_exif_leak(self): - """ -pre patch: - - MB -177.1^ # - | @@@# - | :@@@@@@# - | ::::@@@@@@# - | ::::::::@@@@@@# - | @@::::: ::::@@@@@@# - | @@@@ ::::: ::::@@@@@@# - | @@@@@@@ ::::: ::::@@@@@@# - | @@::@@@@@@@ ::::: ::::@@@@@@# - | @@@@ : @@@@@@@ ::::: ::::@@@@@@# - | @@@@@@ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @@@@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @::@@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | ::::@: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | :@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | ::@@::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @@::: @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @::@ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | :::@: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - | @@@:: @: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# - 0 +----------------------------------------------------------------------->Gi - 0 11.37 - - -post patch: - - MB -21.06^ ::::::::::::::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | ##::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @@@@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: - 0 +----------------------------------------------------------------------->Gi - 0 11.33 - -""" - im = hopper('RGB') - exif = b'12345678'*4096 - - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG", exif=exif) - def test_base_save(self): - """ -base case: - MB -20.99^ ::::: :::::::::::::::::::::::::::::::::::::::::::@::: - | ##: : ::::::@::::::: :::: :::: : : : : : : :::::::::::: :::@::: - | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@# : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@@ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@@@@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: - 0 +----------------------------------------------------------------------->Gi - 0 7.882 -""" - im = hopper('RGB') +def test_qtables_leak(): + im = hopper("RGB") + + standard_l_qtable = [ + int(s) + for s in """ + 16 11 10 16 24 40 51 61 + 12 12 14 19 26 58 60 55 + 14 13 16 24 40 57 69 56 + 14 17 22 29 51 87 80 62 + 18 22 37 56 68 109 103 77 + 24 35 55 64 81 104 113 92 + 49 64 78 87 103 121 120 101 + 72 92 95 98 112 100 103 99 + """.split( + None + ) + ] + + standard_chrominance_qtable = [ + int(s) + for s in """ + 17 18 24 47 99 99 99 99 + 18 21 26 66 99 99 99 99 + 24 26 56 99 99 99 99 99 + 47 66 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + """.split( + None + ) + ] + + qtables = [standard_l_qtable, standard_chrominance_qtable] + + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", qtables=qtables) + + +def test_exif_leak(): + """ + pre patch: + + MB + 177.1^ # + | @@@# + | :@@@@@@# + | ::::@@@@@@# + | ::::::::@@@@@@# + | @@::::: ::::@@@@@@# + | @@@@ ::::: ::::@@@@@@# + | @@@@@@@ ::::: ::::@@@@@@# + | @@::@@@@@@@ ::::: ::::@@@@@@# + | @@@@ : @@@@@@@ ::::: ::::@@@@@@# + | @@@@@@ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @@@@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @::@@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | ::::@: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | :@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | ::@@::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @@::: @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @::@ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | :::@: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @@@:: @: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + 0 +----------------------------------------------------------------------->Gi + 0 11.37 + + + post patch: + + MB + 21.06^ ::::::::::::::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | ##::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + 0 +----------------------------------------------------------------------->Gi + 0 11.33 + """ + im = hopper("RGB") + exif = b"12345678" * 4096 - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG") + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", exif=exif) -if __name__ == '__main__': - unittest.main() +def test_base_save(): + """ + base case: + MB + 20.99^ ::::: :::::::::::::::::::::::::::::::::::::::::::@::: + | ##: : ::::::@::::::: :::: :::: : : : : : : :::::::::::: :::@::: + | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@# : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@@ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@@@@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + 0 +----------------------------------------------------------------------->Gi + 0 7.882""" + im = hopper("RGB") + + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG") diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index ef0cd1f80b7..d98f4a694ef 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,6 +1,8 @@ import sys -from helper import unittest, PillowTestCase +import pytest + +from PIL import Image # This test is not run automatically. # @@ -11,27 +13,36 @@ # Raspberry Pis). It does succeed on a 3gb Ubuntu 12.04x64 VM on Python # 2.7 and 3.2. -from PIL import Image + +try: + import numpy +except ImportError: + numpy = None + YDIM = 32769 XDIM = 48000 -@unittest.skipIf(sys.maxsize <= 2**32, "requires 64-bit system") -class LargeMemoryTest(PillowTestCase): +pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") + + +def _write_png(tmp_path, xdim, ydim): + f = str(tmp_path / "temp.png") + im = Image.new("L", (xdim, ydim), 0) + im.save(f) + - def _write_png(self, xdim, ydim): - f = self.tempfile('temp.png') - im = Image.new('L', (xdim, ydim), (0)) - im.save(f) +def test_large(tmp_path): + """succeeded prepatch""" + _write_png(tmp_path, XDIM, YDIM) - def test_large(self): - """ succeeded prepatch""" - self._write_png(XDIM, YDIM) - def test_2gpx(self): - """failed prepatch""" - self._write_png(XDIM, XDIM) +def test_2gpx(tmp_path): + """failed prepatch""" + _write_png(tmp_path, XDIM, XDIM) -if __name__ == '__main__': - unittest.main() +@pytest.mark.skipif(numpy is None, reason="Numpy is not installed") +def test_size_greater_than_int(): + arr = numpy.ndarray(shape=(16394, 16394)) + Image.fromarray(arr) diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index e48d9836797..24cb1f722bf 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,6 +1,8 @@ import sys -from helper import unittest, PillowTestCase +import pytest + +from PIL import Image # This test is not run automatically. # @@ -10,34 +12,29 @@ # on any 32-bit machine, as well as any smallish things (like # Raspberry Pis). -from PIL import Image -try: - import numpy as np -except ImportError: - raise unittest.SkipTest("numpy not installed") + +np = pytest.importorskip("numpy", reason="NumPy not installed") YDIM = 32769 XDIM = 48000 -@unittest.skipIf(sys.maxsize <= 2**32, "requires 64-bit system") -class LargeMemoryNumpyTest(PillowTestCase): +pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") + - def _write_png(self, xdim, ydim): - dtype = np.uint8 - a = np.zeros((xdim, ydim), dtype=dtype) - f = self.tempfile('temp.png') - im = Image.fromarray(a, 'L') - im.save(f) +def _write_png(tmp_path, xdim, ydim): + dtype = np.uint8 + a = np.zeros((xdim, ydim), dtype=dtype) + f = str(tmp_path / "temp.png") + im = Image.fromarray(a, "L") + im.save(f) - def test_large(self): - """ succeeded prepatch""" - self._write_png(XDIM, YDIM) - def test_2gpx(self): - """failed prepatch""" - self._write_png(XDIM, XDIM) +def test_large(tmp_path): + """succeeded prepatch""" + _write_png(tmp_path, XDIM, YDIM) -if __name__ == '__main__': - unittest.main() +def test_2gpx(tmp_path): + """failed prepatch""" + _write_png(tmp_path, XDIM, XDIM) diff --git a/Tests/check_libtiff_segfault.py b/Tests/check_libtiff_segfault.py index 6611648a56f..bd7f407e4ab 100644 --- a/Tests/check_libtiff_segfault.py +++ b/Tests/check_libtiff_segfault.py @@ -1,19 +1,15 @@ -from helper import unittest, PillowTestCase +import pytest + from PIL import Image TEST_FILE = "Tests/images/libtiff_segfault.tif" -class TestLibtiffSegfault(PillowTestCase): - def test_segfault(self): - """ This test should not segfault. It will on Pillow <= 3.1.0 and - libtiff >= 4.0.0 - """ +def test_libtiff_segfault(): + """This test should not segfault. It will on Pillow <= 3.1.0 and + libtiff >= 4.0.0 + """ - with self.assertRaises(IOError): - im = Image.open(TEST_FILE) + with pytest.raises(OSError): + with Image.open(TEST_FILE) as im: im.load() - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index 8998f8c0f0b..d8d645189e6 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -1,64 +1,61 @@ -from helper import unittest, PillowTestCase -from PIL import Image, PngImagePlugin, ImageFile -from io import BytesIO import zlib +from io import BytesIO + +from PIL import Image, ImageFile, PngImagePlugin TEST_FILE = "Tests/images/png_decompression_dos.png" -class TestPngDos(PillowTestCase): - def test_ignore_dos_text(self): - ImageFile.LOAD_TRUNCATED_IMAGES = True +def test_ignore_dos_text(): + ImageFile.LOAD_TRUNCATED_IMAGES = True + + try: + im = Image.open(TEST_FILE) + im.load() + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False - try: - im = Image.open(TEST_FILE) - im.load() - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + for s in im.text.values(): + assert len(s) < 1024 * 1024, "Text chunk larger than 1M" - for s in im.text.values(): - self.assertLess(len(s), 1024*1024, "Text chunk larger than 1M") + for s in im.info.values(): + assert len(s) < 1024 * 1024, "Text chunk larger than 1M" - for s in im.info.values(): - self.assertLess(len(s), 1024*1024, "Text chunk larger than 1M") - def test_dos_text(self): +def test_dos_text(): - try: - im = Image.open(TEST_FILE) - im.load() - except ValueError as msg: - self.assertTrue(msg, "Decompressed Data Too Large") - return + try: + im = Image.open(TEST_FILE) + im.load() + except ValueError as msg: + assert msg, "Decompressed Data Too Large" + return - for s in im.text.values(): - self.assertLess(len(s), 1024*1024, "Text chunk larger than 1M") + for s in im.text.values(): + assert len(s) < 1024 * 1024, "Text chunk larger than 1M" - def test_dos_total_memory(self): - im = Image.new('L', (1, 1)) - compressed_data = zlib.compress('a'*1024*1023) - info = PngImagePlugin.PngInfo() +def test_dos_total_memory(): + im = Image.new("L", (1, 1)) + compressed_data = zlib.compress(b"a" * 1024 * 1023) - for x in range(64): - info.add_text('t%s' % x, compressed_data, zip=True) - info.add_itxt('i%s' % x, compressed_data, zip=True) + info = PngImagePlugin.PngInfo() - b = BytesIO() - im.save(b, 'PNG', pnginfo=info) - b.seek(0) + for x in range(64): + info.add_text(f"t{x}", compressed_data, zip=True) + info.add_itxt(f"i{x}", compressed_data, zip=True) - try: - im2 = Image.open(b) - except ValueError as msg: - self.assertIn("Too much memory", msg) - return + b = BytesIO() + im.save(b, "PNG", pnginfo=info) + b.seek(0) - total_len = 0 - for txt in im2.text.values(): - total_len += len(txt) - self.assertLess(total_len, 64*1024*1024, - "Total text chunks greater than 64M") + try: + im2 = Image.open(b) + except ValueError as msg: + assert "Too much memory" in msg + return -if __name__ == '__main__': - unittest.main() + total_len = 0 + for txt in im2.text.values(): + total_len += len(txt) + assert total_len < 64 * 1024 * 1024, "Total text chunks greater than 64M" diff --git a/Tests/check_webp_leaks.py b/Tests/check_webp_leaks.py deleted file mode 100644 index 0f54f382d33..00000000000 --- a/Tests/check_webp_leaks.py +++ /dev/null @@ -1,38 +0,0 @@ -from __future__ import division -from helper import unittest, PillowTestCase -import sys -from PIL import Image -from io import BytesIO - -# Limits for testing the leak -mem_limit = 16 # max increase in MB -iterations = 5000 -test_file = "Tests/images/hopper.webp" - - -@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") -class TestWebPLeaks(PillowTestCase): - - def setUp(self): - try: - from PIL import _webp - except ImportError: - self.skipTest('WebP support not installed') - - def _get_mem_usage(self): - from resource import getpagesize, getrusage, RUSAGE_SELF - mem = getrusage(RUSAGE_SELF).ru_maxrss - return mem * getpagesize() / 1024 / 1024 - - def test_leak_load(self): - with open(test_file, 'rb') as f: - im_data = f.read() - start_mem = self._get_mem_usage() - for _ in range(iterations): - with Image.open(BytesIO(im_data)) as im: - im.load() - mem = (self._get_mem_usage() - start_mem) - self.assertLess(mem, mem_limit, msg='memory usage limit exceeded') - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/conftest.py b/Tests/conftest.py new file mode 100644 index 00000000000..66da7593c2e --- /dev/null +++ b/Tests/conftest.py @@ -0,0 +1,31 @@ +import io + + +def pytest_report_header(config): + try: + from PIL import features + + with io.StringIO() as out: + features.pilinfo(out=out, supported_formats=False) + return out.getvalue() + except Exception as e: + return f"pytest_report_header failed: {e}" + + +def pytest_configure(config): + config.addinivalue_line( + "markers", + "pil_noop_mark: A conditional mark where nothing special happens", + ) + + # We're marking some tests to ignore valgrind errors and XFAIL them. + # Ensure that the mark is defined + # even in cases where pytest-valgrind isn't installed + try: + config.addinivalue_line( + "markers", + "valgrind_known_error: Tests that have known issues with valgrind", + ) + except Exception: + # valgrind is already installed + pass diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py index 720fd0067af..e318eb73217 100755 --- a/Tests/createfontdatachunk.py +++ b/Tests/createfontdatachunk.py @@ -1,16 +1,16 @@ -#!/usr/bin/env python -from __future__ import print_function +#!/usr/bin/env python3 import base64 import os -import sys if __name__ == "__main__": # create font data chunk for embedding font = "Tests/images/courB08" print(" f._load_pilfont_data(") - print(" # %s" % os.path.basename(font)) + print(f" # {os.path.basename(font)}") print(" BytesIO(base64.decodestring(b'''") - base64.encode(open(font + ".pil", "rb"), sys.stdout) + with open(font + ".pil", "rb") as fp: + print(base64.b64encode(fp.read()).decode()) print("''')), Image.open(BytesIO(base64.decodestring(b'''") - base64.encode(open(font + ".pbm", "rb"), sys.stdout) + with open(font + ".pbm", "rb") as fp: + print(base64.b64encode(fp.read()).decode()) print("'''))))") diff --git a/Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf b/Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf new file mode 100644 index 00000000000..c065f59a9da Binary files /dev/null and b/Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf differ diff --git a/Tests/fonts/AdobeVFPrototype.ttf b/Tests/fonts/AdobeVFPrototype.ttf new file mode 100644 index 00000000000..64f5ea8e1ed Binary files /dev/null and b/Tests/fonts/AdobeVFPrototype.ttf differ diff --git a/Tests/fonts/ArefRuqaa-Regular.ttf b/Tests/fonts/ArefRuqaa-Regular.ttf new file mode 100644 index 00000000000..940cb58f4dc Binary files /dev/null and b/Tests/fonts/ArefRuqaa-Regular.ttf differ diff --git a/Tests/fonts/BungeeColor-Regular_colr_Windows.ttf b/Tests/fonts/BungeeColor-Regular_colr_Windows.ttf new file mode 100644 index 00000000000..d8eabb3b6a3 Binary files /dev/null and b/Tests/fonts/BungeeColor-Regular_colr_Windows.ttf differ diff --git a/Tests/fonts/DejaVuSans-bitmap.ttf b/Tests/fonts/DejaVuSans-bitmap.ttf deleted file mode 100644 index 702cce37de2..00000000000 Binary files a/Tests/fonts/DejaVuSans-bitmap.ttf and /dev/null differ diff --git a/Tests/fonts/DejaVuSans/DejaVuSans-24-1-stripped.ttf b/Tests/fonts/DejaVuSans/DejaVuSans-24-1-stripped.ttf new file mode 100644 index 00000000000..8eaf1ee0811 Binary files /dev/null and b/Tests/fonts/DejaVuSans/DejaVuSans-24-1-stripped.ttf differ diff --git a/Tests/fonts/DejaVuSans/DejaVuSans-24-2-stripped.ttf b/Tests/fonts/DejaVuSans/DejaVuSans-24-2-stripped.ttf new file mode 100644 index 00000000000..23366725106 Binary files /dev/null and b/Tests/fonts/DejaVuSans/DejaVuSans-24-2-stripped.ttf differ diff --git a/Tests/fonts/DejaVuSans/DejaVuSans-24-4-stripped.ttf b/Tests/fonts/DejaVuSans/DejaVuSans-24-4-stripped.ttf new file mode 100644 index 00000000000..9accc9ebcaf Binary files /dev/null and b/Tests/fonts/DejaVuSans/DejaVuSans-24-4-stripped.ttf differ diff --git a/Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf b/Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf new file mode 100644 index 00000000000..0f93442678d Binary files /dev/null and b/Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf differ diff --git a/Tests/fonts/DejaVuSans.ttf b/Tests/fonts/DejaVuSans/DejaVuSans.ttf similarity index 100% rename from Tests/fonts/DejaVuSans.ttf rename to Tests/fonts/DejaVuSans/DejaVuSans.ttf diff --git a/Tests/fonts/DejaVuSans/LICENSE.txt b/Tests/fonts/DejaVuSans/LICENSE.txt new file mode 100644 index 00000000000..30516578fb2 --- /dev/null +++ b/Tests/fonts/DejaVuSans/LICENSE.txt @@ -0,0 +1,40 @@ +DejaVuSans-24-{1,2,4,8}-stripped.ttf are based on DejaVuSans.ttf converted using FontForge to add bitmap strikes and keep only the ASCII range. + +DejaVu Fonts — License +Fonts are © Bitstream (see below). DejaVu changes are in public domain. Explanation of copyright is on Gnome page on Bitstream Vera fonts. Glyphs imported from Arev fonts are © Tavmjung Bah (see below) + +Bitstream Vera Fonts Copyright +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: + +The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera". + +This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names. + +The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org. + +Arev Fonts Copyright +Original text + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the modifications to the Bitstream Vera Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: + +The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Tavmjong Bah" or the word "Arev". + +This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Tavmjong Bah Arev" names. + +The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from Tavmjong Bah. For further information, contact: tavmjong @ free . fr. \ No newline at end of file diff --git a/Tests/fonts/KhmerOSBattambang-Regular.ttf b/Tests/fonts/KhmerOSBattambang-Regular.ttf new file mode 100755 index 00000000000..b812c0af1bc Binary files /dev/null and b/Tests/fonts/KhmerOSBattambang-Regular.ttf differ diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt index ee9daee592c..da559b3d3f2 100644 --- a/Tests/fonts/LICENSE.txt +++ b/Tests/fonts/LICENSE.txt @@ -1,13 +1,27 @@ -NotoNastaliqUrdu-Regular.ttf: +NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts +NotoSans-Regular.ttf, from https://www.google.com/get/noto/ +NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/ +NotoColorEmoji.ttf, from https://github.com/googlefonts/noto-emoji +AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype +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 -(from https://github.com/googlei18n/noto-fonts) +All of the above fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to. -All Noto 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. +FreeMono.ttf is licensed under GPLv3, with the GPL font exception. +OpenSansCondensed-LightItalic.tt, from https://fonts.google.com/specimen/Open+Sans, under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) -10x20-ISO8859-1.pcf +chromacheck-sbix.woff, from https://github.com/RoelN/ChromaCheck, under The MIT License (MIT), Copyright (c) 2018 Roel Nieskens, https://pixelambacht.nl Copyright (c) 2018 Google LLC -(from https://packages.ubuntu.com/xenial/xfonts-base) +KhmerOSBattambang-Regular.ttf is licensed under LGPL-2.1 or later. + +FreeMono.ttf is licensed under GPLv3. + +10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base "Public domain font. Share and enjoy." diff --git a/Tests/fonts/NotoColorEmoji.ttf b/Tests/fonts/NotoColorEmoji.ttf new file mode 100644 index 00000000000..ef7b725758c Binary files /dev/null and b/Tests/fonts/NotoColorEmoji.ttf differ diff --git a/Tests/fonts/NotoSans-Regular.ttf b/Tests/fonts/NotoSans-Regular.ttf new file mode 100644 index 00000000000..a1b8994edea Binary files /dev/null and b/Tests/fonts/NotoSans-Regular.ttf differ diff --git a/Tests/fonts/NotoSansJP-Regular.otf b/Tests/fonts/NotoSansJP-Regular.otf new file mode 100644 index 00000000000..fbccd9f16a0 Binary files /dev/null and b/Tests/fonts/NotoSansJP-Regular.otf differ diff --git a/Tests/fonts/NotoSansSymbols-Regular.ttf b/Tests/fonts/NotoSansSymbols-Regular.ttf new file mode 100644 index 00000000000..92accef72d4 Binary files /dev/null and b/Tests/fonts/NotoSansSymbols-Regular.ttf differ diff --git a/Tests/fonts/OpenSans.woff2 b/Tests/fonts/OpenSans.woff2 new file mode 100644 index 00000000000..15339ea9ccd Binary files /dev/null and b/Tests/fonts/OpenSans.woff2 differ diff --git a/Tests/fonts/OpenSansCondensed-LightItalic.ttf b/Tests/fonts/OpenSansCondensed-LightItalic.ttf new file mode 100644 index 00000000000..b4ee4951f33 Binary files /dev/null and b/Tests/fonts/OpenSansCondensed-LightItalic.ttf differ diff --git a/Tests/fonts/TINY5x3GX.ttf b/Tests/fonts/TINY5x3GX.ttf new file mode 100755 index 00000000000..bd6e208dece Binary files /dev/null and b/Tests/fonts/TINY5x3GX.ttf differ diff --git a/Tests/fonts/chromacheck-sbix.woff b/Tests/fonts/chromacheck-sbix.woff new file mode 100644 index 00000000000..518d4b7ea6c Binary files /dev/null and b/Tests/fonts/chromacheck-sbix.woff differ diff --git a/Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf b/Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf new file mode 100644 index 00000000000..79013251524 Binary files /dev/null and b/Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf differ diff --git a/Tests/fonts/ter-x20b-cp1250.pbm b/Tests/fonts/ter-x20b-cp1250.pbm new file mode 100644 index 00000000000..fe7e2c4dc03 Binary files /dev/null and b/Tests/fonts/ter-x20b-cp1250.pbm differ diff --git a/Tests/fonts/ter-x20b-cp1250.pil b/Tests/fonts/ter-x20b-cp1250.pil new file mode 100644 index 00000000000..4da49e5fd41 Binary files /dev/null and b/Tests/fonts/ter-x20b-cp1250.pil differ diff --git a/Tests/fonts/ter-x20b-iso8859-1.pbm b/Tests/fonts/ter-x20b-iso8859-1.pbm new file mode 100644 index 00000000000..ffd840ae94e Binary files /dev/null and b/Tests/fonts/ter-x20b-iso8859-1.pbm differ diff --git a/Tests/fonts/ter-x20b-iso8859-1.pil b/Tests/fonts/ter-x20b-iso8859-1.pil new file mode 100644 index 00000000000..14d6e8be7b2 Binary files /dev/null and b/Tests/fonts/ter-x20b-iso8859-1.pil differ diff --git a/Tests/fonts/ter-x20b-iso8859-2.pbm b/Tests/fonts/ter-x20b-iso8859-2.pbm new file mode 100644 index 00000000000..ad5b3af8d75 Binary files /dev/null and b/Tests/fonts/ter-x20b-iso8859-2.pbm differ diff --git a/Tests/fonts/ter-x20b-iso8859-2.pil b/Tests/fonts/ter-x20b-iso8859-2.pil new file mode 100644 index 00000000000..14d6e8be7b2 Binary files /dev/null and b/Tests/fonts/ter-x20b-iso8859-2.pil differ diff --git a/Tests/fonts/ter-x20b.pcf b/Tests/fonts/ter-x20b.pcf new file mode 100644 index 00000000000..962bcca6af4 Binary files /dev/null and b/Tests/fonts/ter-x20b.pcf differ diff --git a/Tests/helper.py b/Tests/helper.py index fdeb00c0cd2..0d1d03ac8c1 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -1,280 +1,236 @@ """ Helper functions. """ -from __future__ import print_function + +import logging +import os +import shutil import sys +import sysconfig import tempfile -import os -import unittest +from io import BytesIO -from PIL import Image, ImageMath +import pytest +from packaging.version import parse as parse_version + +from PIL import Image, ImageMath, features -import logging logger = logging.getLogger(__name__) HAS_UPLOADER = False -if os.environ.get('SHOW_ERRORS', None): - # local img.show for errors. - HAS_UPLOADER=True +if os.environ.get("SHOW_ERRORS", None): + # local img.show for errors. + HAS_UPLOADER = True + class test_image_results: - @classmethod - def upload(self, a, b): + @staticmethod + def upload(a, b): a.show() b.show() + +elif "GITHUB_ACTIONS" in os.environ: + HAS_UPLOADER = True + + class test_image_results: + @staticmethod + def upload(a, b): + dir_errors = os.path.join(os.path.dirname(__file__), "errors") + os.makedirs(dir_errors, exist_ok=True) + tmpdir = tempfile.mkdtemp(dir=dir_errors) + a.save(os.path.join(tmpdir, "a.png")) + b.save(os.path.join(tmpdir, "b.png")) + return tmpdir + else: try: import test_image_results + HAS_UPLOADER = True except ImportError: pass - def convert_to_comparable(a, b): new_a, new_b = a, b - if a.mode == 'P': - new_a = Image.new('L', a.size) - new_b = Image.new('L', b.size) + if a.mode == "P": + new_a = Image.new("L", a.size) + new_b = Image.new("L", b.size) new_a.putdata(a.getdata()) new_b.putdata(b.getdata()) - elif a.mode == 'I;16': - new_a = a.convert('I') - new_b = b.convert('I') + elif a.mode == "I;16": + new_a = a.convert("I") + new_b = b.convert("I") return new_a, new_b -class PillowTestCase(unittest.TestCase): +def assert_deep_equal(a, b, msg=None): + try: + assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" + except Exception: + assert a == b, msg - def __init__(self, *args, **kwargs): - unittest.TestCase.__init__(self, *args, **kwargs) - # holds last result object passed to run method: - self.currentResult = None - # Nicer output for --verbose - def __str__(self): - return self.__class__.__name__ + "." + self._testMethodName +def assert_image(im, mode, size, msg=None): + if mode is not None: + assert im.mode == mode, ( + msg or f"got mode {repr(im.mode)}, expected {repr(mode)}" + ) - def run(self, result=None): - self.currentResult = result # remember result for use later - unittest.TestCase.run(self, result) # call superclass run method + if size is not None: + assert im.size == size, ( + msg or f"got size {repr(im.size)}, expected {repr(size)}" + ) - def delete_tempfile(self, path): - try: - ok = self.currentResult.wasSuccessful() - except AttributeError: # for pytest - ok = True - if ok: - # only clean out tempfiles if test passed +def assert_image_equal(a, b, msg=None): + assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" + assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" + if a.tobytes() != b.tobytes(): + if HAS_UPLOADER: try: - os.remove(path) - except OSError: - pass # report? - else: - print("=== orphaned temp file: %s" % path) - - def assert_deep_equal(self, a, b, msg=None): - try: - self.assertEqual( - len(a), len(b), - msg or "got length %s, expected %s" % (len(a), len(b))) - self.assertTrue( - all(x == y for x, y in zip(a, b)), - msg or "got %s, expected %s" % (a, b)) - except: - self.assertEqual(a, b, msg) - - def assert_image(self, im, mode, size, msg=None): - if mode is not None: - self.assertEqual( - im.mode, mode, - msg or "got mode %r, expected %r" % (im.mode, mode)) - - if size is not None: - self.assertEqual( - im.size, size, - msg or "got size %r, expected %r" % (im.size, size)) - - def assert_image_equal(self, a, b, msg=None): - self.assertEqual( - a.mode, b.mode, - msg or "got mode %r, expected %r" % (a.mode, b.mode)) - self.assertEqual( - a.size, b.size, - msg or "got size %r, expected %r" % (a.size, b.size)) - if a.tobytes() != b.tobytes(): - if HAS_UPLOADER: - try: - url = test_image_results.upload(a, b) - logger.error("Url for test images: %s" % url) - except Exception as msg: - pass - - self.fail(msg or "got different content") - - def assert_image_equal_tofile(self, a, filename, msg=None, mode=None): - with Image.open(filename) as img: - if mode: - img = img.convert(mode) - self.assert_image_equal(a, img, msg) - - def assert_image_similar(self, a, b, epsilon, msg=None): - epsilon = float(epsilon) - self.assertEqual( - a.mode, b.mode, - msg or "got mode %r, expected %r" % (a.mode, b.mode)) - self.assertEqual( - a.size, b.size, - msg or "got size %r, expected %r" % (a.size, b.size)) - - a, b = convert_to_comparable(a, b) - - diff = 0 - for ach, bch in zip(a.split(), b.split()): - chdiff = ImageMath.eval("abs(a - b)", a=ach, b=bch).convert('L') - diff += sum(i * num for i, num in enumerate(chdiff.histogram())) - - ave_diff = float(diff)/(a.size[0]*a.size[1]) - try: - self.assertGreaterEqual( - epsilon, ave_diff, - (msg or '') + - " average pixel value difference %.4f > epsilon %.4f" % ( - ave_diff, epsilon)) - except Exception as e: - if HAS_UPLOADER: - try: - url = test_image_results.upload(a, b) - logger.error("Url for test images: %s" % url) - except: - pass - raise e - - def assert_image_similar_tofile(self, a, filename, epsilon, msg=None, mode=None): - with Image.open(filename) as img: - if mode: - img = img.convert(mode) - self.assert_image_similar(a, img, epsilon, msg) - - def assert_warning(self, warn_class, func, *args, **kwargs): - import warnings - - result = None - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. - warnings.simplefilter("always") - - # Hopefully trigger a warning. - result = func(*args, **kwargs) - - # Verify some things. - if warn_class is None: - self.assertEqual(len(w), 0, - "Expected no warnings, got %s" % - list(v.category for v in w)) - else: - self.assertGreaterEqual(len(w), 1) - found = False - for v in w: - if issubclass(v.category, warn_class): - found = True - break - self.assertTrue(found) - return result + url = test_image_results.upload(a, b) + logger.error(f"Url for test images: {url}") + except Exception: + pass + + assert False, msg or "got different content" + + +def assert_image_equal_tofile(a, filename, msg=None, mode=None): + with Image.open(filename) as img: + if mode: + img = img.convert(mode) + assert_image_equal(a, img, msg) + + +def assert_image_similar(a, b, epsilon, msg=None): + assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" + assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" + + a, b = convert_to_comparable(a, b) + + diff = 0 + for ach, bch in zip(a.split(), b.split()): + chdiff = ImageMath.eval("abs(a - b)", a=ach, b=bch).convert("L") + diff += sum(i * num for i, num in enumerate(chdiff.histogram())) + + ave_diff = diff / (a.size[0] * a.size[1]) + try: + assert epsilon >= ave_diff, ( + (msg or "") + + f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}" + ) + except Exception as e: + if HAS_UPLOADER: + try: + url = test_image_results.upload(a, b) + logger.error(f"Url for test images: {url}") + except Exception: + pass + raise e + + +def assert_image_similar_tofile(a, filename, epsilon, msg=None, mode=None): + with Image.open(filename) as img: + if mode: + img = img.convert(mode) + assert_image_similar(a, img, epsilon, msg) + + +def assert_all_same(items, msg=None): + assert items.count(items[0]) == len(items), msg + - def assert_all_same(self, items, msg=None): - self.assertTrue(items.count(items[0]) == len(items), msg) - - def assert_not_all_same(self, items, msg=None): - self.assertFalse(items.count(items[0]) == len(items), msg) - - def skipKnownBadTest(self, msg=None, platform=None, - travis=None, interpreter=None): - # Skip if platform/travis matches, and - # PILLOW_RUN_KNOWN_BAD is not true in the environment. - if os.environ.get('PILLOW_RUN_KNOWN_BAD', False): - print(os.environ.get('PILLOW_RUN_KNOWN_BAD', False)) - return - - skip = True - if platform is not None: - skip = sys.platform.startswith(platform) - if travis is not None: - skip = skip and (travis == bool(os.environ.get('TRAVIS', False))) - if interpreter is not None: - skip = skip and (interpreter == 'pypy' and - hasattr(sys, 'pypy_version_info')) - if skip: - self.skipTest(msg or "Known Bad Test") - - def tempfile(self, template): - assert template[:5] in ("temp.", "temp_") - fd, path = tempfile.mkstemp(template[4:], template[:4]) - os.close(fd) - - self.addCleanup(self.delete_tempfile, path) - return path - - def open_withImagemagick(self, f): - if not imagemagick_available(): - raise IOError() - - outfile = self.tempfile("temp.png") - if command_succeeds([IMCONVERT, f, outfile]): - return Image.open(outfile) - raise IOError() - - -@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") -class PillowLeakTestCase(PillowTestCase): - # requires unix/osx +def assert_not_all_same(items, msg=None): + assert items.count(items[0]) != len(items), msg + + +def assert_tuple_approx_equal(actuals, targets, threshold, msg): + """Tests if actuals has values within threshold from targets""" + value = True + for i, target in enumerate(targets): + value *= target - threshold <= actuals[i] <= target + threshold + + assert value, msg + ": " + repr(actuals) + " != " + repr(targets) + + +def skip_unless_feature(feature): + reason = f"{feature} not available" + return pytest.mark.skipif(not features.check(feature), reason=reason) + + +def skip_unless_feature_version(feature, version_required, reason=None): + if not features.check(feature): + return pytest.mark.skip(f"{feature} not available") + if reason is None: + reason = f"{feature} is older than {version_required}" + version_required = parse_version(version_required) + version_available = parse_version(features.version(feature)) + return pytest.mark.skipif(version_available < version_required, reason=reason) + + +def mark_if_feature_version(mark, feature, version_blacklist, reason=None): + if not features.check(feature): + return pytest.mark.pil_noop_mark() + if reason is None: + reason = f"{feature} is {version_blacklist}" + version_required = parse_version(version_blacklist) + version_available = parse_version(features.version(feature)) + if ( + version_available.major == version_required.major + and version_available.minor == version_required.minor + ): + return mark(reason=reason) + return pytest.mark.pil_noop_mark() + + +@pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS") +class PillowLeakTestCase: + # requires unix/macOS iterations = 100 # count mem_limit = 512 # k def _get_mem_usage(self): """ Gets the RUSAGE memory usage, returns in K. Encapsulates the difference - between OSX and Linux rss reporting + between macOS and Linux rss reporting - :returns; memory usage in kilobytes + :returns: memory usage in kilobytes """ - from resource import getrusage, RUSAGE_SELF + from resource import RUSAGE_SELF, getrusage + mem = getrusage(RUSAGE_SELF).ru_maxrss - if sys.platform == 'darwin': + if sys.platform == "darwin": # man 2 getrusage: - # ru_maxrss the maximum resident set size utilized (in bytes). - return mem / 1024 # Kb - else: - # linux - # man 2 getrusage - # ru_maxrss (since Linux 2.6.32) - # This is the maximum resident set size used (in kilobytes). - return mem # Kb + # ru_maxrss + # This is the maximum resident set size utilized (in bytes). + return mem / 1024 # Kb + # linux + # man 2 getrusage + # ru_maxrss (since Linux 2.6.32) + # This is the maximum resident set size used (in kilobytes). + return mem # Kb def _test_leak(self, core): start_mem = self._get_mem_usage() for cycle in range(self.iterations): core() - mem = (self._get_mem_usage() - start_mem) - self.assertLess(mem, self.mem_limit, - msg='memory usage limit exceeded in iteration %d' % cycle) + mem = self._get_mem_usage() - start_mem + msg = f"memory usage limit exceeded in iteration {cycle}" + assert mem < self.mem_limit, msg # helpers -py3 = (sys.version_info >= (3, 0)) - def fromstring(data): - from io import BytesIO return Image.open(BytesIO(data)) def tostring(im, string_format, **options): - from io import BytesIO out = BytesIO() im.save(out, string_format, **options) return out.getvalue() @@ -301,58 +257,73 @@ def hopper(mode=None, cache={}): return im.copy() -def command_succeeds(cmd): - """ - Runs the command, which must be a list of strings. Returns True if the - command succeeds, or False if an OSError was raised by subprocess.Popen. - """ - import subprocess - with open(os.devnull, 'wb') as f: - try: - subprocess.call(cmd, stdout=f, stderr=subprocess.STDOUT) - except OSError: - return False - return True - - def djpeg_available(): - return command_succeeds(['djpeg', '-version']) + return bool(shutil.which("djpeg")) def cjpeg_available(): - return command_succeeds(['cjpeg', '-version']) + return bool(shutil.which("cjpeg")) def netpbm_available(): - return (command_succeeds(["ppmquant", "--version"]) and - command_succeeds(["ppmtogif", "--version"])) + return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) + +def magick_command(): + if sys.platform == "win32": + magickhome = os.environ.get("MAGICK_HOME", "") + if magickhome: + imagemagick = [os.path.join(magickhome, "convert.exe")] + graphicsmagick = [os.path.join(magickhome, "gm.exe"), "convert"] + else: + imagemagick = None + graphicsmagick = None + else: + imagemagick = ["convert"] + graphicsmagick = ["gm", "convert"] -def imagemagick_available(): - return IMCONVERT and command_succeeds([IMCONVERT, '-version']) + if imagemagick and shutil.which(imagemagick[0]): + return imagemagick + if graphicsmagick and shutil.which(graphicsmagick[0]): + return graphicsmagick def on_appveyor(): - return 'APPVEYOR' in os.environ + return "APPVEYOR" in os.environ -if sys.platform == 'win32': - IMCONVERT = os.environ.get('MAGICK_HOME', '') - if IMCONVERT: - IMCONVERT = os.path.join(IMCONVERT, 'convert.exe') -else: - IMCONVERT = 'convert' +def on_github_actions(): + return "GITHUB_ACTIONS" in os.environ + + +def on_ci(): + # GitHub Actions and AppVeyor have "CI" + return "CI" in os.environ + + +def is_big_endian(): + return sys.byteorder == "big" + + +def is_ppc64le(): + import platform + + return platform.machine() == "ppc64le" + + +def is_win32(): + return sys.platform.startswith("win32") + + +def is_pypy(): + return hasattr(sys, "pypy_translation_info") -def distro(): - if os.path.exists('/etc/os-release'): - with open('/etc/os-release', 'r') as f: - for line in f: - if 'ID=' in line: - return line.strip().split('=')[1] +def is_mingw(): + return sysconfig.get_platform() == "mingw" -class cached_property(object): +class CachedProperty: def __init__(self, func): self.func = func diff --git a/Tests/images/00r0_gray_l.jp2 b/Tests/images/00r0_gray_l.jp2 new file mode 100644 index 00000000000..28612238a9c Binary files /dev/null and b/Tests/images/00r0_gray_l.jp2 differ diff --git a/Tests/images/00r1_graya_la.jp2 b/Tests/images/00r1_graya_la.jp2 new file mode 100644 index 00000000000..f3f840a08e3 Binary files /dev/null and b/Tests/images/00r1_graya_la.jp2 differ diff --git a/Tests/images/01r_00.pcx b/Tests/images/01r_00.pcx new file mode 100644 index 00000000000..f40777ac582 Binary files /dev/null and b/Tests/images/01r_00.pcx differ diff --git a/Tests/images/1.eps b/Tests/images/1.eps new file mode 100644 index 00000000000..727dc9b7f04 Binary files /dev/null and b/Tests/images/1.eps differ diff --git a/Tests/images/16_bit_noise.tif b/Tests/images/16_bit_noise.tif new file mode 100644 index 00000000000..19180638efa Binary files /dev/null and b/Tests/images/16_bit_noise.tif differ diff --git a/Tests/images/16bit.r.tif b/Tests/images/16bit.r.tif new file mode 100644 index 00000000000..0f3996e95eb Binary files /dev/null and b/Tests/images/16bit.r.tif differ diff --git a/Tests/images/1_trns.png b/Tests/images/1_trns.png new file mode 100644 index 00000000000..c9a271b4066 Binary files /dev/null and b/Tests/images/1_trns.png differ diff --git a/Tests/images/200x32_p_bl_raw_origin.tga b/Tests/images/200x32_p_bl_raw_origin.tga new file mode 100644 index 00000000000..329f0ca4d9e Binary files /dev/null and b/Tests/images/200x32_p_bl_raw_origin.tga differ diff --git a/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds b/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds new file mode 100644 index 00000000000..9b4d8e21f64 Binary files /dev/null and b/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds differ diff --git a/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.png b/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.png new file mode 100644 index 00000000000..57177fe2bb8 Binary files /dev/null and b/Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.png differ diff --git a/Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds b/Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds new file mode 100644 index 00000000000..1da9293de9b Binary files /dev/null and b/Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds differ diff --git a/Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.png b/Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.png new file mode 100644 index 00000000000..57177fe2bb8 Binary files /dev/null and b/Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.png differ diff --git a/Tests/images/a_fli.png b/Tests/images/a_fli.png new file mode 100644 index 00000000000..93c3f1b1266 Binary files /dev/null and b/Tests/images/a_fli.png differ diff --git a/Tests/images/apng/blend_op_over.png b/Tests/images/apng/blend_op_over.png new file mode 100644 index 00000000000..3fe0f4ca789 Binary files /dev/null and b/Tests/images/apng/blend_op_over.png differ diff --git a/Tests/images/apng/blend_op_over_near_transparent.png b/Tests/images/apng/blend_op_over_near_transparent.png new file mode 100644 index 00000000000..3ee5fe3bf27 Binary files /dev/null and b/Tests/images/apng/blend_op_over_near_transparent.png differ diff --git a/Tests/images/apng/blend_op_source_near_transparent.png b/Tests/images/apng/blend_op_source_near_transparent.png new file mode 100644 index 00000000000..1af30f81f7e Binary files /dev/null and b/Tests/images/apng/blend_op_source_near_transparent.png differ diff --git a/Tests/images/apng/blend_op_source_solid.png b/Tests/images/apng/blend_op_source_solid.png new file mode 100644 index 00000000000..d90c54967b6 Binary files /dev/null and b/Tests/images/apng/blend_op_source_solid.png differ diff --git a/Tests/images/apng/blend_op_source_transparent.png b/Tests/images/apng/blend_op_source_transparent.png new file mode 100644 index 00000000000..0f290fd7fdb Binary files /dev/null and b/Tests/images/apng/blend_op_source_transparent.png differ diff --git a/Tests/images/apng/chunk_actl_after_idat.png b/Tests/images/apng/chunk_actl_after_idat.png new file mode 100644 index 00000000000..296a29d4c11 Binary files /dev/null and b/Tests/images/apng/chunk_actl_after_idat.png differ diff --git a/Tests/images/apng/chunk_multi_actl.png b/Tests/images/apng/chunk_multi_actl.png new file mode 100644 index 00000000000..213f8854969 Binary files /dev/null and b/Tests/images/apng/chunk_multi_actl.png differ diff --git a/Tests/images/apng/chunk_no_actl.png b/Tests/images/apng/chunk_no_actl.png new file mode 100644 index 00000000000..5b68c7b4409 Binary files /dev/null and b/Tests/images/apng/chunk_no_actl.png differ diff --git a/Tests/images/apng/chunk_no_fctl.png b/Tests/images/apng/chunk_no_fctl.png new file mode 100644 index 00000000000..58ca904abdc Binary files /dev/null and b/Tests/images/apng/chunk_no_fctl.png differ diff --git a/Tests/images/apng/chunk_no_fdat.png b/Tests/images/apng/chunk_no_fdat.png new file mode 100644 index 00000000000..af42766b5ed Binary files /dev/null and b/Tests/images/apng/chunk_no_fdat.png differ diff --git a/Tests/images/apng/chunk_repeat_fctl.png b/Tests/images/apng/chunk_repeat_fctl.png new file mode 100644 index 00000000000..a5779855fc6 Binary files /dev/null and b/Tests/images/apng/chunk_repeat_fctl.png differ diff --git a/Tests/images/apng/delay.png b/Tests/images/apng/delay.png new file mode 100644 index 00000000000..64cceaae83a Binary files /dev/null and b/Tests/images/apng/delay.png differ diff --git a/Tests/images/apng/delay_round.png b/Tests/images/apng/delay_round.png new file mode 100644 index 00000000000..3f082665c99 Binary files /dev/null and b/Tests/images/apng/delay_round.png differ diff --git a/Tests/images/apng/delay_short_max.png b/Tests/images/apng/delay_short_max.png new file mode 100644 index 00000000000..99d53b71812 Binary files /dev/null and b/Tests/images/apng/delay_short_max.png differ diff --git a/Tests/images/apng/delay_zero_denom.png b/Tests/images/apng/delay_zero_denom.png new file mode 100644 index 00000000000..bad60c767fb Binary files /dev/null and b/Tests/images/apng/delay_zero_denom.png differ diff --git a/Tests/images/apng/delay_zero_numer.png b/Tests/images/apng/delay_zero_numer.png new file mode 100644 index 00000000000..a029a959b5d Binary files /dev/null and b/Tests/images/apng/delay_zero_numer.png differ diff --git a/Tests/images/apng/dispose_op_background.png b/Tests/images/apng/dispose_op_background.png new file mode 100644 index 00000000000..b63ebc0b35d Binary files /dev/null and b/Tests/images/apng/dispose_op_background.png differ diff --git a/Tests/images/apng/dispose_op_background_before_region.png b/Tests/images/apng/dispose_op_background_before_region.png new file mode 100644 index 00000000000..427b829a025 Binary files /dev/null and b/Tests/images/apng/dispose_op_background_before_region.png differ diff --git a/Tests/images/apng/dispose_op_background_final.png b/Tests/images/apng/dispose_op_background_final.png new file mode 100644 index 00000000000..77694ff1d15 Binary files /dev/null and b/Tests/images/apng/dispose_op_background_final.png differ diff --git a/Tests/images/apng/dispose_op_background_p_mode.png b/Tests/images/apng/dispose_op_background_p_mode.png new file mode 100644 index 00000000000..e5fb4784d26 Binary files /dev/null and b/Tests/images/apng/dispose_op_background_p_mode.png differ diff --git a/Tests/images/apng/dispose_op_background_region.png b/Tests/images/apng/dispose_op_background_region.png new file mode 100644 index 00000000000..05948d44aed Binary files /dev/null and b/Tests/images/apng/dispose_op_background_region.png differ diff --git a/Tests/images/apng/dispose_op_none.png b/Tests/images/apng/dispose_op_none.png new file mode 100644 index 00000000000..3094c1d23d6 Binary files /dev/null and b/Tests/images/apng/dispose_op_none.png differ diff --git a/Tests/images/apng/dispose_op_none_region.png b/Tests/images/apng/dispose_op_none_region.png new file mode 100644 index 00000000000..4e1dbf77e45 Binary files /dev/null and b/Tests/images/apng/dispose_op_none_region.png differ diff --git a/Tests/images/apng/dispose_op_previous.png b/Tests/images/apng/dispose_op_previous.png new file mode 100644 index 00000000000..1c15f132fe7 Binary files /dev/null and b/Tests/images/apng/dispose_op_previous.png differ diff --git a/Tests/images/apng/dispose_op_previous_final.png b/Tests/images/apng/dispose_op_previous_final.png new file mode 100644 index 00000000000..858f6f0382b Binary files /dev/null and b/Tests/images/apng/dispose_op_previous_final.png differ diff --git a/Tests/images/apng/dispose_op_previous_first.png b/Tests/images/apng/dispose_op_previous_first.png new file mode 100644 index 00000000000..3f9b3cfae76 Binary files /dev/null and b/Tests/images/apng/dispose_op_previous_first.png differ diff --git a/Tests/images/apng/dispose_op_previous_frame.png b/Tests/images/apng/dispose_op_previous_frame.png new file mode 100644 index 00000000000..14168da8992 Binary files /dev/null and b/Tests/images/apng/dispose_op_previous_frame.png differ diff --git a/Tests/images/apng/dispose_op_previous_region.png b/Tests/images/apng/dispose_op_previous_region.png new file mode 100644 index 00000000000..f326afa5c27 Binary files /dev/null and b/Tests/images/apng/dispose_op_previous_region.png differ diff --git a/Tests/images/apng/fctl_actl.png b/Tests/images/apng/fctl_actl.png new file mode 100644 index 00000000000..d0418ddd75f Binary files /dev/null and b/Tests/images/apng/fctl_actl.png differ diff --git a/Tests/images/apng/mode_16bit.png b/Tests/images/apng/mode_16bit.png new file mode 100644 index 00000000000..1210e373797 Binary files /dev/null and b/Tests/images/apng/mode_16bit.png differ diff --git a/Tests/images/apng/mode_greyscale.png b/Tests/images/apng/mode_greyscale.png new file mode 100644 index 00000000000..29ed7d1ea12 Binary files /dev/null and b/Tests/images/apng/mode_greyscale.png differ diff --git a/Tests/images/apng/mode_greyscale_alpha.png b/Tests/images/apng/mode_greyscale_alpha.png new file mode 100644 index 00000000000..f9307f63504 Binary files /dev/null and b/Tests/images/apng/mode_greyscale_alpha.png differ diff --git a/Tests/images/apng/mode_palette.png b/Tests/images/apng/mode_palette.png new file mode 100644 index 00000000000..11ccfb6cba0 Binary files /dev/null and b/Tests/images/apng/mode_palette.png differ diff --git a/Tests/images/apng/mode_palette_1bit_alpha.png b/Tests/images/apng/mode_palette_1bit_alpha.png new file mode 100644 index 00000000000..e95425ac194 Binary files /dev/null and b/Tests/images/apng/mode_palette_1bit_alpha.png differ diff --git a/Tests/images/apng/mode_palette_alpha.png b/Tests/images/apng/mode_palette_alpha.png new file mode 100644 index 00000000000..f3c4c9f9e6d Binary files /dev/null and b/Tests/images/apng/mode_palette_alpha.png differ diff --git a/Tests/images/apng/num_plays.png b/Tests/images/apng/num_plays.png new file mode 100644 index 00000000000..4d76802e4cc Binary files /dev/null and b/Tests/images/apng/num_plays.png differ diff --git a/Tests/images/apng/num_plays_1.png b/Tests/images/apng/num_plays_1.png new file mode 100644 index 00000000000..fb25394305f Binary files /dev/null and b/Tests/images/apng/num_plays_1.png differ diff --git a/Tests/images/apng/sequence_fdat_fctl.png b/Tests/images/apng/sequence_fdat_fctl.png new file mode 100644 index 00000000000..29ac75e1675 Binary files /dev/null and b/Tests/images/apng/sequence_fdat_fctl.png differ diff --git a/Tests/images/apng/sequence_gap.png b/Tests/images/apng/sequence_gap.png new file mode 100644 index 00000000000..25dd9bcd868 Binary files /dev/null and b/Tests/images/apng/sequence_gap.png differ diff --git a/Tests/images/apng/sequence_reorder.png b/Tests/images/apng/sequence_reorder.png new file mode 100644 index 00000000000..dc78e9bb13b Binary files /dev/null and b/Tests/images/apng/sequence_reorder.png differ diff --git a/Tests/images/apng/sequence_reorder_chunk.png b/Tests/images/apng/sequence_reorder_chunk.png new file mode 100644 index 00000000000..5d951ffe2a5 Binary files /dev/null and b/Tests/images/apng/sequence_reorder_chunk.png differ diff --git a/Tests/images/apng/sequence_repeat.png b/Tests/images/apng/sequence_repeat.png new file mode 100644 index 00000000000..d5cf83f9f98 Binary files /dev/null and b/Tests/images/apng/sequence_repeat.png differ diff --git a/Tests/images/apng/sequence_repeat_chunk.png b/Tests/images/apng/sequence_repeat_chunk.png new file mode 100644 index 00000000000..27d1d3eb5a4 Binary files /dev/null and b/Tests/images/apng/sequence_repeat_chunk.png differ diff --git a/Tests/images/apng/sequence_start.png b/Tests/images/apng/sequence_start.png new file mode 100644 index 00000000000..5e040743a1d Binary files /dev/null and b/Tests/images/apng/sequence_start.png differ diff --git a/Tests/images/apng/single_frame.png b/Tests/images/apng/single_frame.png new file mode 100644 index 00000000000..0cd5bea856b Binary files /dev/null and b/Tests/images/apng/single_frame.png differ diff --git a/Tests/images/apng/single_frame_default.png b/Tests/images/apng/single_frame_default.png new file mode 100644 index 00000000000..db7581fbdfc Binary files /dev/null and b/Tests/images/apng/single_frame_default.png differ diff --git a/Tests/images/apng/split_fdat.png b/Tests/images/apng/split_fdat.png new file mode 100644 index 00000000000..2dc58b929a4 Binary files /dev/null and b/Tests/images/apng/split_fdat.png differ diff --git a/Tests/images/apng/split_fdat_zero_chunk.png b/Tests/images/apng/split_fdat_zero_chunk.png new file mode 100644 index 00000000000..14a76d9d618 Binary files /dev/null and b/Tests/images/apng/split_fdat_zero_chunk.png differ diff --git a/Tests/images/apng/syntax_num_frames_high.png b/Tests/images/apng/syntax_num_frames_high.png new file mode 100644 index 00000000000..bba9cdfd580 Binary files /dev/null and b/Tests/images/apng/syntax_num_frames_high.png differ diff --git a/Tests/images/apng/syntax_num_frames_invalid.png b/Tests/images/apng/syntax_num_frames_invalid.png new file mode 100644 index 00000000000..ca7b13ab8ab Binary files /dev/null and b/Tests/images/apng/syntax_num_frames_invalid.png differ diff --git a/Tests/images/apng/syntax_num_frames_low.png b/Tests/images/apng/syntax_num_frames_low.png new file mode 100644 index 00000000000..6f895f91d75 Binary files /dev/null and b/Tests/images/apng/syntax_num_frames_low.png differ diff --git a/Tests/images/apng/syntax_num_frames_zero.png b/Tests/images/apng/syntax_num_frames_zero.png new file mode 100644 index 00000000000..0cb7ea36e3e Binary files /dev/null and b/Tests/images/apng/syntax_num_frames_zero.png differ diff --git a/Tests/images/apng/syntax_num_frames_zero_default.png b/Tests/images/apng/syntax_num_frames_zero_default.png new file mode 100644 index 00000000000..89f2b75e257 Binary files /dev/null and b/Tests/images/apng/syntax_num_frames_zero_default.png differ diff --git a/Tests/images/app13-multiple.jpg b/Tests/images/app13-multiple.jpg new file mode 100644 index 00000000000..8341383a0e6 Binary files /dev/null and b/Tests/images/app13-multiple.jpg differ diff --git a/Tests/images/app13.jpg b/Tests/images/app13.jpg new file mode 100644 index 00000000000..b02d71b40be Binary files /dev/null and b/Tests/images/app13.jpg differ diff --git a/Tests/images/argb-32bpp_MipMaps-1.dds b/Tests/images/argb-32bpp_MipMaps-1.dds new file mode 100644 index 00000000000..d1d1998b1b3 Binary files /dev/null and b/Tests/images/argb-32bpp_MipMaps-1.dds differ diff --git a/Tests/images/argb-32bpp_MipMaps-1.png b/Tests/images/argb-32bpp_MipMaps-1.png new file mode 100644 index 00000000000..3570ccf355e Binary files /dev/null and b/Tests/images/argb-32bpp_MipMaps-1.png differ diff --git a/Tests/images/ati1.dds b/Tests/images/ati1.dds new file mode 100644 index 00000000000..747e4b1b98a Binary files /dev/null and b/Tests/images/ati1.dds differ diff --git a/Tests/images/ati1.png b/Tests/images/ati1.png new file mode 100644 index 00000000000..790d7d7dbb2 Binary files /dev/null and b/Tests/images/ati1.png differ diff --git a/Tests/images/ati2.dds b/Tests/images/ati2.dds new file mode 100644 index 00000000000..3ac5f7956ea Binary files /dev/null and b/Tests/images/ati2.dds differ diff --git a/Tests/images/balloon.jpf b/Tests/images/balloon.jpf new file mode 100644 index 00000000000..767eab5dde6 Binary files /dev/null and b/Tests/images/balloon.jpf differ diff --git a/Tests/images/balloon_eciRGBv2_aware.jp2 b/Tests/images/balloon_eciRGBv2_aware.jp2 new file mode 100644 index 00000000000..18fd1e1723d Binary files /dev/null and b/Tests/images/balloon_eciRGBv2_aware.jp2 differ diff --git a/Tests/images/bc5_snorm.dds b/Tests/images/bc5_snorm.dds new file mode 100644 index 00000000000..7458c67c6ad Binary files /dev/null and b/Tests/images/bc5_snorm.dds differ diff --git a/Tests/images/bc5_typeless.dds b/Tests/images/bc5_typeless.dds new file mode 100644 index 00000000000..b5bae52bb95 Binary files /dev/null and b/Tests/images/bc5_typeless.dds differ diff --git a/Tests/images/bc5_unorm.dds b/Tests/images/bc5_unorm.dds new file mode 100644 index 00000000000..a04a026eb1f Binary files /dev/null and b/Tests/images/bc5_unorm.dds differ diff --git a/Tests/images/bc5_unorm.png b/Tests/images/bc5_unorm.png new file mode 100644 index 00000000000..05279ddfbe6 Binary files /dev/null and b/Tests/images/bc5_unorm.png differ diff --git a/Tests/images/bc5s.dds b/Tests/images/bc5s.dds new file mode 100644 index 00000000000..0b999eed320 Binary files /dev/null and b/Tests/images/bc5s.dds differ diff --git a/Tests/images/bc5s.png b/Tests/images/bc5s.png new file mode 100644 index 00000000000..39d7811bf2e Binary files /dev/null and b/Tests/images/bc5s.png differ diff --git a/Tests/images/bc6h.dds b/Tests/images/bc6h.dds new file mode 100644 index 00000000000..77993a0c1ea Binary files /dev/null and b/Tests/images/bc6h.dds differ diff --git a/Tests/images/bc6h.png b/Tests/images/bc6h.png new file mode 100644 index 00000000000..609f114890c Binary files /dev/null and b/Tests/images/bc6h.png differ diff --git a/Tests/images/bc6h_sf.dds b/Tests/images/bc6h_sf.dds new file mode 100644 index 00000000000..2ab1b195b19 Binary files /dev/null and b/Tests/images/bc6h_sf.dds differ diff --git a/Tests/images/bc6h_sf.png b/Tests/images/bc6h_sf.png new file mode 100644 index 00000000000..6a3b73d5fc9 Binary files /dev/null and b/Tests/images/bc6h_sf.png differ diff --git a/Tests/images/bitmap_font_1_basic.png b/Tests/images/bitmap_font_1_basic.png new file mode 100644 index 00000000000..01a05606c0a Binary files /dev/null and b/Tests/images/bitmap_font_1_basic.png differ diff --git a/Tests/images/bitmap_font_1_raqm.png b/Tests/images/bitmap_font_1_raqm.png new file mode 100644 index 00000000000..560efb68598 Binary files /dev/null and b/Tests/images/bitmap_font_1_raqm.png differ diff --git a/Tests/images/bitmap_font_2_basic.png b/Tests/images/bitmap_font_2_basic.png new file mode 100644 index 00000000000..44d137dd67c Binary files /dev/null and b/Tests/images/bitmap_font_2_basic.png differ diff --git a/Tests/images/bitmap_font_2_raqm.png b/Tests/images/bitmap_font_2_raqm.png new file mode 100644 index 00000000000..7a40bd6c239 Binary files /dev/null and b/Tests/images/bitmap_font_2_raqm.png differ diff --git a/Tests/images/bitmap_font_4_basic.png b/Tests/images/bitmap_font_4_basic.png new file mode 100644 index 00000000000..e79d86aa886 Binary files /dev/null and b/Tests/images/bitmap_font_4_basic.png differ diff --git a/Tests/images/bitmap_font_4_raqm.png b/Tests/images/bitmap_font_4_raqm.png new file mode 100644 index 00000000000..d98a3bc3ee1 Binary files /dev/null and b/Tests/images/bitmap_font_4_raqm.png differ diff --git a/Tests/images/bitmap_font_8_basic.png b/Tests/images/bitmap_font_8_basic.png new file mode 100644 index 00000000000..15a7c980914 Binary files /dev/null and b/Tests/images/bitmap_font_8_basic.png differ diff --git a/Tests/images/bitmap_font_8_raqm.png b/Tests/images/bitmap_font_8_raqm.png new file mode 100644 index 00000000000..1ad088c9362 Binary files /dev/null and b/Tests/images/bitmap_font_8_raqm.png differ diff --git a/Tests/images/bitmap_font_stroke_basic.png b/Tests/images/bitmap_font_stroke_basic.png new file mode 100644 index 00000000000..86b2d09f66e Binary files /dev/null and b/Tests/images/bitmap_font_stroke_basic.png differ diff --git a/Tests/images/bitmap_font_stroke_raqm.png b/Tests/images/bitmap_font_stroke_raqm.png new file mode 100644 index 00000000000..08029ce34be Binary files /dev/null and b/Tests/images/bitmap_font_stroke_raqm.png differ diff --git a/Tests/images/black_and_white.ico b/Tests/images/black_and_white.ico new file mode 100644 index 00000000000..f98d7ac8e8d Binary files /dev/null and b/Tests/images/black_and_white.ico differ diff --git a/Tests/images/blp/blp1_jpeg.blp b/Tests/images/blp/blp1_jpeg.blp new file mode 100644 index 00000000000..bdf7146ed41 Binary files /dev/null and b/Tests/images/blp/blp1_jpeg.blp differ diff --git a/Tests/images/blp/blp1_jpeg.png b/Tests/images/blp/blp1_jpeg.png new file mode 100644 index 00000000000..be151f205ed Binary files /dev/null and b/Tests/images/blp/blp1_jpeg.png differ diff --git a/Tests/images/blp/blp1_jpeg2.blp b/Tests/images/blp/blp1_jpeg2.blp new file mode 100644 index 00000000000..890180e9b47 Binary files /dev/null and b/Tests/images/blp/blp1_jpeg2.blp differ diff --git a/Tests/images/blp/blp2_dxt1.blp b/Tests/images/blp/blp2_dxt1.blp new file mode 100644 index 00000000000..73c0c91b51a Binary files /dev/null and b/Tests/images/blp/blp2_dxt1.blp differ diff --git a/Tests/images/blp/blp2_dxt1.png b/Tests/images/blp/blp2_dxt1.png new file mode 100644 index 00000000000..f2a24618a9a Binary files /dev/null and b/Tests/images/blp/blp2_dxt1.png differ diff --git a/Tests/images/blp/blp2_dxt1a.blp b/Tests/images/blp/blp2_dxt1a.blp new file mode 100644 index 00000000000..5bedc27d656 Binary files /dev/null and b/Tests/images/blp/blp2_dxt1a.blp differ diff --git a/Tests/images/blp/blp2_dxt1a.png b/Tests/images/blp/blp2_dxt1a.png new file mode 100644 index 00000000000..d2cdea807ce Binary files /dev/null and b/Tests/images/blp/blp2_dxt1a.png differ diff --git a/Tests/images/blp/blp2_raw.blp b/Tests/images/blp/blp2_raw.blp new file mode 100644 index 00000000000..813d4bfae61 Binary files /dev/null and b/Tests/images/blp/blp2_raw.blp differ diff --git a/Tests/images/blp/blp2_raw.png b/Tests/images/blp/blp2_raw.png new file mode 100644 index 00000000000..c77a3c04816 Binary files /dev/null and b/Tests/images/blp/blp2_raw.png differ diff --git a/Tests/images/broken_exif_dpi.jpg b/Tests/images/broken_exif_dpi.jpg new file mode 100644 index 00000000000..2c88b94630b Binary files /dev/null and b/Tests/images/broken_exif_dpi.jpg differ diff --git a/Tests/images/bw_gradient.imt b/Tests/images/bw_gradient.imt new file mode 100644 index 00000000000..d765cf95fac Binary files /dev/null and b/Tests/images/bw_gradient.imt differ diff --git a/Tests/images/bw_gradient.png b/Tests/images/bw_gradient.png new file mode 100644 index 00000000000..79c921486f8 Binary files /dev/null and b/Tests/images/bw_gradient.png differ diff --git a/Tests/images/cbdt_notocoloremoji.png b/Tests/images/cbdt_notocoloremoji.png new file mode 100644 index 00000000000..1da12fba115 Binary files /dev/null and b/Tests/images/cbdt_notocoloremoji.png differ diff --git a/Tests/images/cbdt_notocoloremoji_mask.png b/Tests/images/cbdt_notocoloremoji_mask.png new file mode 100644 index 00000000000..6d036a0b6ba Binary files /dev/null and b/Tests/images/cbdt_notocoloremoji_mask.png differ diff --git a/Tests/images/child_ifd.tiff b/Tests/images/child_ifd.tiff new file mode 100644 index 00000000000..700185d88ae Binary files /dev/null and b/Tests/images/child_ifd.tiff differ diff --git a/Tests/images/child_ifd_jpeg.tiff b/Tests/images/child_ifd_jpeg.tiff new file mode 100644 index 00000000000..f5e3d129d5d Binary files /dev/null and b/Tests/images/child_ifd_jpeg.tiff differ diff --git a/Tests/images/chromacheck-sbix.png b/Tests/images/chromacheck-sbix.png new file mode 100644 index 00000000000..b906ef133a4 Binary files /dev/null and b/Tests/images/chromacheck-sbix.png differ diff --git a/Tests/images/chromacheck-sbix_mask.png b/Tests/images/chromacheck-sbix_mask.png new file mode 100644 index 00000000000..4b68ff91bec Binary files /dev/null and b/Tests/images/chromacheck-sbix_mask.png differ diff --git a/Tests/images/colr_bungee.png b/Tests/images/colr_bungee.png new file mode 100644 index 00000000000..b10a60be057 Binary files /dev/null and b/Tests/images/colr_bungee.png differ diff --git a/Tests/images/colr_bungee_mask.png b/Tests/images/colr_bungee_mask.png new file mode 100644 index 00000000000..f13e1767749 Binary files /dev/null and b/Tests/images/colr_bungee_mask.png differ diff --git a/Tests/images/combined_larger_than_size.psd b/Tests/images/combined_larger_than_size.psd new file mode 100644 index 00000000000..2e6caef39ee Binary files /dev/null and b/Tests/images/combined_larger_than_size.psd differ diff --git a/Tests/images/comment_after_last_frame.gif b/Tests/images/comment_after_last_frame.gif new file mode 100644 index 00000000000..9f5c7b8da47 Binary files /dev/null and b/Tests/images/comment_after_last_frame.gif differ diff --git a/Tests/images/comment_after_only_frame.gif b/Tests/images/comment_after_only_frame.gif new file mode 100644 index 00000000000..8188b684732 Binary files /dev/null and b/Tests/images/comment_after_only_frame.gif differ diff --git a/Tests/images/crash-0c7e0e8e11ce787078f00b5b0ca409a167f070e0.tif b/Tests/images/crash-0c7e0e8e11ce787078f00b5b0ca409a167f070e0.tif new file mode 100644 index 00000000000..5275075e917 Binary files /dev/null and b/Tests/images/crash-0c7e0e8e11ce787078f00b5b0ca409a167f070e0.tif differ diff --git a/Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif b/Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif new file mode 100644 index 00000000000..6e4e9b9caa5 Binary files /dev/null and b/Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif differ diff --git a/Tests/images/crash-0e16d3bfb83be87356d026d66919deaefca44dac.tif b/Tests/images/crash-0e16d3bfb83be87356d026d66919deaefca44dac.tif new file mode 100644 index 00000000000..f59aab21afe Binary files /dev/null and b/Tests/images/crash-0e16d3bfb83be87356d026d66919deaefca44dac.tif differ diff --git a/Tests/images/crash-1152ec2d1a1a71395b6f2ce6721c38924d025bf3.tif b/Tests/images/crash-1152ec2d1a1a71395b6f2ce6721c38924d025bf3.tif new file mode 100644 index 00000000000..c8d6e2aada3 Binary files /dev/null and b/Tests/images/crash-1152ec2d1a1a71395b6f2ce6721c38924d025bf3.tif differ diff --git a/Tests/images/crash-1185209cf7655b5aed8ae5e77784dfdd18ab59e9.tif b/Tests/images/crash-1185209cf7655b5aed8ae5e77784dfdd18ab59e9.tif new file mode 100644 index 00000000000..ecf7db38f2e Binary files /dev/null and b/Tests/images/crash-1185209cf7655b5aed8ae5e77784dfdd18ab59e9.tif differ diff --git a/Tests/images/crash-2020-10-test.tif b/Tests/images/crash-2020-10-test.tif new file mode 100644 index 00000000000..958cdde2209 Binary files /dev/null and b/Tests/images/crash-2020-10-test.tif differ diff --git a/Tests/images/crash-338516dbd2f0e83caddb8ce256c22db3bd6dc40f.tif b/Tests/images/crash-338516dbd2f0e83caddb8ce256c22db3bd6dc40f.tif new file mode 100644 index 00000000000..344d62b277a Binary files /dev/null and b/Tests/images/crash-338516dbd2f0e83caddb8ce256c22db3bd6dc40f.tif differ diff --git a/Tests/images/crash-465703f71a0f0094873a3e0e82c9f798161171b8.sgi b/Tests/images/crash-465703f71a0f0094873a3e0e82c9f798161171b8.sgi new file mode 100644 index 00000000000..81ae1182391 Binary files /dev/null and b/Tests/images/crash-465703f71a0f0094873a3e0e82c9f798161171b8.sgi differ diff --git a/Tests/images/crash-4f085cc12ece8cde18758d42608bed6a2a2cfb1c.tif b/Tests/images/crash-4f085cc12ece8cde18758d42608bed6a2a2cfb1c.tif new file mode 100644 index 00000000000..18197c15f1a Binary files /dev/null and b/Tests/images/crash-4f085cc12ece8cde18758d42608bed6a2a2cfb1c.tif differ diff --git a/Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k b/Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k new file mode 100644 index 00000000000..c9bd7fc0a8d Binary files /dev/null and b/Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k differ diff --git a/Tests/images/crash-5762152299364352.fli b/Tests/images/crash-5762152299364352.fli new file mode 100644 index 00000000000..944fe0b56c7 Binary files /dev/null and b/Tests/images/crash-5762152299364352.fli differ diff --git a/Tests/images/crash-63b1dffefc8c075ddc606c0a2f5fdc15ece78863.tif b/Tests/images/crash-63b1dffefc8c075ddc606c0a2f5fdc15ece78863.tif new file mode 100644 index 00000000000..b89203f75c4 Binary files /dev/null and b/Tests/images/crash-63b1dffefc8c075ddc606c0a2f5fdc15ece78863.tif differ diff --git a/Tests/images/crash-64834657ee604b8797bf99eac6a194c124a9a8ba.sgi b/Tests/images/crash-64834657ee604b8797bf99eac6a194c124a9a8ba.sgi new file mode 100644 index 00000000000..f31d810e4c3 Binary files /dev/null and b/Tests/images/crash-64834657ee604b8797bf99eac6a194c124a9a8ba.sgi differ diff --git a/Tests/images/crash-6b7f2244da6d0ae297ee0754a424213444e92778.sgi b/Tests/images/crash-6b7f2244da6d0ae297ee0754a424213444e92778.sgi new file mode 100644 index 00000000000..74396935b9a Binary files /dev/null and b/Tests/images/crash-6b7f2244da6d0ae297ee0754a424213444e92778.sgi differ diff --git a/Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif b/Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif new file mode 100644 index 00000000000..053e4e4e952 Binary files /dev/null and b/Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif differ diff --git a/Tests/images/crash-754d9c7ec485ffb76a90eeaab191ef69a2a3a3cd.sgi b/Tests/images/crash-754d9c7ec485ffb76a90eeaab191ef69a2a3a3cd.sgi new file mode 100644 index 00000000000..8e093bdfd72 Binary files /dev/null and b/Tests/images/crash-754d9c7ec485ffb76a90eeaab191ef69a2a3a3cd.sgi differ diff --git a/Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k b/Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k new file mode 100644 index 00000000000..fd2f4dd3677 Binary files /dev/null and b/Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k differ diff --git a/Tests/images/crash-86214e58da443d2b80820cff9677a38a33dcbbca.tif b/Tests/images/crash-86214e58da443d2b80820cff9677a38a33dcbbca.tif new file mode 100644 index 00000000000..34e4f6014af Binary files /dev/null and b/Tests/images/crash-86214e58da443d2b80820cff9677a38a33dcbbca.tif differ diff --git a/Tests/images/crash-abcf1c97b8fe42a6c68f1fb0b978530c98d57ced.sgi b/Tests/images/crash-abcf1c97b8fe42a6c68f1fb0b978530c98d57ced.sgi new file mode 100644 index 00000000000..790cb37449e Binary files /dev/null and b/Tests/images/crash-abcf1c97b8fe42a6c68f1fb0b978530c98d57ced.sgi differ diff --git a/Tests/images/crash-b82e64d4f3f76d7465b6af535283029eda211259.sgi b/Tests/images/crash-b82e64d4f3f76d7465b6af535283029eda211259.sgi new file mode 100644 index 00000000000..8b7d8776591 Binary files /dev/null and b/Tests/images/crash-b82e64d4f3f76d7465b6af535283029eda211259.sgi differ diff --git a/Tests/images/crash-c1b2595b8b0b92cc5f38b6635e98e3a119ade807.sgi b/Tests/images/crash-c1b2595b8b0b92cc5f38b6635e98e3a119ade807.sgi new file mode 100644 index 00000000000..e9d2ca1a6f2 Binary files /dev/null and b/Tests/images/crash-c1b2595b8b0b92cc5f38b6635e98e3a119ade807.sgi differ diff --git a/Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k b/Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k new file mode 100644 index 00000000000..c3ad0d6330a Binary files /dev/null and b/Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k differ diff --git a/Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k b/Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k new file mode 100644 index 00000000000..3aadfc37727 Binary files /dev/null and b/Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k differ diff --git a/Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi b/Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi new file mode 100644 index 00000000000..b02aacea9c3 Binary files /dev/null and b/Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi differ diff --git a/Tests/images/crash-f46f5b2f43c370fe65706c11449f567ecc345e74.tif b/Tests/images/crash-f46f5b2f43c370fe65706c11449f567ecc345e74.tif new file mode 100644 index 00000000000..c6774d4591a Binary files /dev/null and b/Tests/images/crash-f46f5b2f43c370fe65706c11449f567ecc345e74.tif differ diff --git a/Tests/images/cross_scan_line.png b/Tests/images/cross_scan_line.png new file mode 100644 index 00000000000..64b68ed338d Binary files /dev/null and b/Tests/images/cross_scan_line.png differ diff --git a/Tests/images/cross_scan_line.tga b/Tests/images/cross_scan_line.tga new file mode 100644 index 00000000000..5ef8c8154d4 Binary files /dev/null and b/Tests/images/cross_scan_line.tga differ diff --git a/Tests/images/cross_scan_line_truncated.tga b/Tests/images/cross_scan_line_truncated.tga new file mode 100644 index 00000000000..cec4357e3ac Binary files /dev/null and b/Tests/images/cross_scan_line_truncated.tga differ diff --git a/Tests/images/decompression_bomb.gif b/Tests/images/decompression_bomb.gif new file mode 100644 index 00000000000..3ca21b60a97 Binary files /dev/null and b/Tests/images/decompression_bomb.gif differ diff --git a/Tests/images/decompression_bomb.ico b/Tests/images/decompression_bomb.ico new file mode 100644 index 00000000000..2ecfa8586e8 Binary files /dev/null and b/Tests/images/decompression_bomb.ico differ diff --git a/Tests/images/decompression_bomb_extents.gif b/Tests/images/decompression_bomb_extents.gif new file mode 100644 index 00000000000..0d5ff03f525 Binary files /dev/null and b/Tests/images/decompression_bomb_extents.gif differ diff --git a/Tests/images/different_transparency.gif b/Tests/images/different_transparency.gif new file mode 100644 index 00000000000..2d36bef9e36 Binary files /dev/null and b/Tests/images/different_transparency.gif differ diff --git a/Tests/images/different_transparency_merged.png b/Tests/images/different_transparency_merged.png new file mode 100644 index 00000000000..3438f62a6f4 Binary files /dev/null and b/Tests/images/different_transparency_merged.png differ diff --git a/Tests/images/dispose_bgnd_rgba.gif b/Tests/images/dispose_bgnd_rgba.gif new file mode 100644 index 00000000000..c18a0ba71f1 Binary files /dev/null and b/Tests/images/dispose_bgnd_rgba.gif differ diff --git a/Tests/images/dispose_bgnd_transparency.gif b/Tests/images/dispose_bgnd_transparency.gif new file mode 100644 index 00000000000..7c626fe72c0 Binary files /dev/null and b/Tests/images/dispose_bgnd_transparency.gif differ diff --git a/Tests/images/dispose_none_load_end.gif b/Tests/images/dispose_none_load_end.gif new file mode 100644 index 00000000000..3c94eb1b6e0 Binary files /dev/null and b/Tests/images/dispose_none_load_end.gif differ diff --git a/Tests/images/dispose_none_load_end_second.png b/Tests/images/dispose_none_load_end_second.png new file mode 100644 index 00000000000..dc01ccbdd0c Binary files /dev/null and b/Tests/images/dispose_none_load_end_second.png differ diff --git a/Tests/images/dispose_prev_first_frame.gif b/Tests/images/dispose_prev_first_frame.gif new file mode 100644 index 00000000000..4c19dd1ed43 Binary files /dev/null and b/Tests/images/dispose_prev_first_frame.gif differ diff --git a/Tests/images/dispose_prev_first_frame_seeked.png b/Tests/images/dispose_prev_first_frame_seeked.png new file mode 100644 index 00000000000..85a3753e16d Binary files /dev/null and b/Tests/images/dispose_prev_first_frame_seeked.png differ diff --git a/Tests/images/drawing_wmf_ref_144.png b/Tests/images/drawing_wmf_ref_144.png new file mode 100644 index 00000000000..20ed9ce597b Binary files /dev/null and b/Tests/images/drawing_wmf_ref_144.png differ diff --git a/Tests/images/duplicate_number_of_loops.gif b/Tests/images/duplicate_number_of_loops.gif new file mode 100644 index 00000000000..ac315ee99f3 Binary files /dev/null and b/Tests/images/duplicate_number_of_loops.gif differ diff --git a/Tests/images/dxt5-colorblock-alpha-issue-4142.dds b/Tests/images/dxt5-colorblock-alpha-issue-4142.dds new file mode 100644 index 00000000000..905527eada4 Binary files /dev/null and b/Tests/images/dxt5-colorblock-alpha-issue-4142.dds differ diff --git a/Tests/images/empty_gps_ifd.jpg b/Tests/images/empty_gps_ifd.jpg new file mode 100644 index 00000000000..28f180b8743 Binary files /dev/null and b/Tests/images/empty_gps_ifd.jpg differ diff --git a/Tests/images/exif-ifd-offset.jpg b/Tests/images/exif-ifd-offset.jpg new file mode 100644 index 00000000000..e5dfc6807a1 Binary files /dev/null and b/Tests/images/exif-ifd-offset.jpg differ diff --git a/Tests/images/exif.png b/Tests/images/exif.png new file mode 100644 index 00000000000..0388b6b8a1c Binary files /dev/null and b/Tests/images/exif.png differ diff --git a/Tests/images/exif_imagemagick.png b/Tests/images/exif_imagemagick.png new file mode 100644 index 00000000000..6f59224c854 Binary files /dev/null and b/Tests/images/exif_imagemagick.png differ diff --git a/Tests/images/exif_imagemagick_orientation.png b/Tests/images/exif_imagemagick_orientation.png new file mode 100644 index 00000000000..819a0703f83 Binary files /dev/null and b/Tests/images/exif_imagemagick_orientation.png differ diff --git a/Tests/images/exif_text.png b/Tests/images/exif_text.png new file mode 100644 index 00000000000..e2d8dc0fffa Binary files /dev/null and b/Tests/images/exif_text.png differ diff --git a/Tests/images/expected_to_read.jp2 b/Tests/images/expected_to_read.jp2 new file mode 100644 index 00000000000..d8029a0d3a0 Binary files /dev/null and b/Tests/images/expected_to_read.jp2 differ diff --git a/Tests/images/first_frame_transparency.gif b/Tests/images/first_frame_transparency.gif new file mode 100644 index 00000000000..86dc0de64a9 Binary files /dev/null and b/Tests/images/first_frame_transparency.gif differ diff --git a/Tests/images/fli_oob/02r/02r00.fli b/Tests/images/fli_oob/02r/02r00.fli new file mode 100644 index 00000000000..eac0e4304f2 Binary files /dev/null and b/Tests/images/fli_oob/02r/02r00.fli differ diff --git a/Tests/images/fli_oob/02r/notes b/Tests/images/fli_oob/02r/notes new file mode 100644 index 00000000000..49f92b19bed --- /dev/null +++ b/Tests/images/fli_oob/02r/notes @@ -0,0 +1 @@ +Is this because a file-originating field is being interpreted as a *signed* int32, allowing it to provide negative values for 'advance'? diff --git a/Tests/images/fli_oob/02r/others/02r01.fli b/Tests/images/fli_oob/02r/others/02r01.fli new file mode 100644 index 00000000000..3a5864c84c5 Binary files /dev/null and b/Tests/images/fli_oob/02r/others/02r01.fli differ diff --git a/Tests/images/fli_oob/02r/others/02r02.fli b/Tests/images/fli_oob/02r/others/02r02.fli new file mode 100644 index 00000000000..2b3d15b55ae Binary files /dev/null and b/Tests/images/fli_oob/02r/others/02r02.fli differ diff --git a/Tests/images/fli_oob/02r/others/02r03.fli b/Tests/images/fli_oob/02r/others/02r03.fli new file mode 100644 index 00000000000..a631721321a Binary files /dev/null and b/Tests/images/fli_oob/02r/others/02r03.fli differ diff --git a/Tests/images/fli_oob/02r/others/02r04.fli b/Tests/images/fli_oob/02r/others/02r04.fli new file mode 100644 index 00000000000..4c17cbb3dee Binary files /dev/null and b/Tests/images/fli_oob/02r/others/02r04.fli differ diff --git a/Tests/images/fli_oob/02r/reproducing b/Tests/images/fli_oob/02r/reproducing new file mode 100644 index 00000000000..3286d94f1c7 --- /dev/null +++ b/Tests/images/fli_oob/02r/reproducing @@ -0,0 +1 @@ +Image.open(...).seek(212) diff --git a/Tests/images/fli_oob/03r/03r00.fli b/Tests/images/fli_oob/03r/03r00.fli new file mode 100644 index 00000000000..7972880cecd Binary files /dev/null and b/Tests/images/fli_oob/03r/03r00.fli differ diff --git a/Tests/images/fli_oob/03r/notes b/Tests/images/fli_oob/03r/notes new file mode 100644 index 00000000000..d75605cea64 --- /dev/null +++ b/Tests/images/fli_oob/03r/notes @@ -0,0 +1 @@ +ridiculous bytes value passed to ImagingFliDecode diff --git a/Tests/images/fli_oob/03r/others/03r01.fli b/Tests/images/fli_oob/03r/others/03r01.fli new file mode 100644 index 00000000000..1102c69ca3b Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r01.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r02.fli b/Tests/images/fli_oob/03r/others/03r02.fli new file mode 100644 index 00000000000..d30326fe0b0 Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r02.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r03.fli b/Tests/images/fli_oob/03r/others/03r03.fli new file mode 100644 index 00000000000..7f3db178e60 Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r03.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r04.fli b/Tests/images/fli_oob/03r/others/03r04.fli new file mode 100644 index 00000000000..f05375e843b Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r04.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r05.fli b/Tests/images/fli_oob/03r/others/03r05.fli new file mode 100644 index 00000000000..03794432419 Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r05.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r06.fli b/Tests/images/fli_oob/03r/others/03r06.fli new file mode 100644 index 00000000000..1527cbf91a0 Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r06.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r07.fli b/Tests/images/fli_oob/03r/others/03r07.fli new file mode 100644 index 00000000000..c9dea41351d Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r07.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r08.fli b/Tests/images/fli_oob/03r/others/03r08.fli new file mode 100644 index 00000000000..698101443c5 Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r08.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r09.fli b/Tests/images/fli_oob/03r/others/03r09.fli new file mode 100644 index 00000000000..12058480a44 Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r09.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r10.fli b/Tests/images/fli_oob/03r/others/03r10.fli new file mode 100644 index 00000000000..448b0a812d7 Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r10.fli differ diff --git a/Tests/images/fli_oob/03r/others/03r11.fli b/Tests/images/fli_oob/03r/others/03r11.fli new file mode 100644 index 00000000000..db1b5fe5870 Binary files /dev/null and b/Tests/images/fli_oob/03r/others/03r11.fli differ diff --git a/Tests/images/fli_oob/03r/reproducing b/Tests/images/fli_oob/03r/reproducing new file mode 100644 index 00000000000..145b8b074a2 --- /dev/null +++ b/Tests/images/fli_oob/03r/reproducing @@ -0,0 +1 @@ +im = Image.open(d); im.seek(0); im.getdata() diff --git a/Tests/images/fli_oob/04r/04r00.fli b/Tests/images/fli_oob/04r/04r00.fli new file mode 100644 index 00000000000..c4e416f3903 Binary files /dev/null and b/Tests/images/fli_oob/04r/04r00.fli differ diff --git a/Tests/images/fli_oob/04r/initial.fli b/Tests/images/fli_oob/04r/initial.fli new file mode 100644 index 00000000000..5a8659f7c0b Binary files /dev/null and b/Tests/images/fli_oob/04r/initial.fli differ diff --git a/Tests/images/fli_oob/04r/notes b/Tests/images/fli_oob/04r/notes new file mode 100644 index 00000000000..7922e0ba895 --- /dev/null +++ b/Tests/images/fli_oob/04r/notes @@ -0,0 +1 @@ +failure to check input buffer (`data`) boundaries in BRUN chunk diff --git a/Tests/images/fli_oob/04r/others/04r01.fli b/Tests/images/fli_oob/04r/others/04r01.fli new file mode 100644 index 00000000000..af968970b65 Binary files /dev/null and b/Tests/images/fli_oob/04r/others/04r01.fli differ diff --git a/Tests/images/fli_oob/04r/others/04r02.fli b/Tests/images/fli_oob/04r/others/04r02.fli new file mode 100644 index 00000000000..ae027fc1180 Binary files /dev/null and b/Tests/images/fli_oob/04r/others/04r02.fli differ diff --git a/Tests/images/fli_oob/04r/others/04r03.fli b/Tests/images/fli_oob/04r/others/04r03.fli new file mode 100644 index 00000000000..ab92f4b6a83 Binary files /dev/null and b/Tests/images/fli_oob/04r/others/04r03.fli differ diff --git a/Tests/images/fli_oob/04r/others/04r04.fli b/Tests/images/fli_oob/04r/others/04r04.fli new file mode 100644 index 00000000000..533ffa027e8 Binary files /dev/null and b/Tests/images/fli_oob/04r/others/04r04.fli differ diff --git a/Tests/images/fli_oob/04r/others/04r05.fli b/Tests/images/fli_oob/04r/others/04r05.fli new file mode 100644 index 00000000000..b07ef6496af Binary files /dev/null and b/Tests/images/fli_oob/04r/others/04r05.fli differ diff --git a/Tests/images/fli_oob/04r/reproducing b/Tests/images/fli_oob/04r/reproducing new file mode 100644 index 00000000000..145b8b074a2 --- /dev/null +++ b/Tests/images/fli_oob/04r/reproducing @@ -0,0 +1 @@ +im = Image.open(d); im.seek(0); im.getdata() diff --git a/Tests/images/fli_oob/05r/05r00.fli b/Tests/images/fli_oob/05r/05r00.fli new file mode 100644 index 00000000000..dff5a01e804 Binary files /dev/null and b/Tests/images/fli_oob/05r/05r00.fli differ diff --git a/Tests/images/fli_oob/05r/notes b/Tests/images/fli_oob/05r/notes new file mode 100644 index 00000000000..bec9db779a7 --- /dev/null +++ b/Tests/images/fli_oob/05r/notes @@ -0,0 +1 @@ +failure to check input buffer (`data`) boundaries in LC chunk diff --git a/Tests/images/fli_oob/05r/others/05r01.fli b/Tests/images/fli_oob/05r/others/05r01.fli new file mode 100644 index 00000000000..1ad3444fec3 Binary files /dev/null and b/Tests/images/fli_oob/05r/others/05r01.fli differ diff --git a/Tests/images/fli_oob/05r/others/05r02.fli b/Tests/images/fli_oob/05r/others/05r02.fli new file mode 100644 index 00000000000..cd6429884c4 Binary files /dev/null and b/Tests/images/fli_oob/05r/others/05r02.fli differ diff --git a/Tests/images/fli_oob/05r/others/05r03.fli b/Tests/images/fli_oob/05r/others/05r03.fli new file mode 100644 index 00000000000..2a4be914cb8 Binary files /dev/null and b/Tests/images/fli_oob/05r/others/05r03.fli differ diff --git a/Tests/images/fli_oob/05r/others/05r04.fli b/Tests/images/fli_oob/05r/others/05r04.fli new file mode 100644 index 00000000000..0b547c7e2ec Binary files /dev/null and b/Tests/images/fli_oob/05r/others/05r04.fli differ diff --git a/Tests/images/fli_oob/05r/others/05r05.fli b/Tests/images/fli_oob/05r/others/05r05.fli new file mode 100644 index 00000000000..0bf7752300e Binary files /dev/null and b/Tests/images/fli_oob/05r/others/05r05.fli differ diff --git a/Tests/images/fli_oob/05r/others/05r06.fli b/Tests/images/fli_oob/05r/others/05r06.fli new file mode 100644 index 00000000000..c35b8e232f9 Binary files /dev/null and b/Tests/images/fli_oob/05r/others/05r06.fli differ diff --git a/Tests/images/fli_oob/05r/others/05r07.fli b/Tests/images/fli_oob/05r/others/05r07.fli new file mode 100644 index 00000000000..b99ce01b307 Binary files /dev/null and b/Tests/images/fli_oob/05r/others/05r07.fli differ diff --git a/Tests/images/fli_oob/05r/reproducing b/Tests/images/fli_oob/05r/reproducing new file mode 100644 index 00000000000..145b8b074a2 --- /dev/null +++ b/Tests/images/fli_oob/05r/reproducing @@ -0,0 +1 @@ +im = Image.open(d); im.seek(0); im.getdata() diff --git a/Tests/images/fli_oob/06r/06r00.fli b/Tests/images/fli_oob/06r/06r00.fli new file mode 100644 index 00000000000..9189d6ed03f Binary files /dev/null and b/Tests/images/fli_oob/06r/06r00.fli differ diff --git a/Tests/images/fli_oob/06r/notes b/Tests/images/fli_oob/06r/notes new file mode 100644 index 00000000000..397ad4748a3 --- /dev/null +++ b/Tests/images/fli_oob/06r/notes @@ -0,0 +1 @@ +failure to check input buffer (`data`) boundaries in SS2 chunk diff --git a/Tests/images/fli_oob/06r/others/06r01.fli b/Tests/images/fli_oob/06r/others/06r01.fli new file mode 100644 index 00000000000..24a99dacc4f Binary files /dev/null and b/Tests/images/fli_oob/06r/others/06r01.fli differ diff --git a/Tests/images/fli_oob/06r/others/06r02.fli b/Tests/images/fli_oob/06r/others/06r02.fli new file mode 100644 index 00000000000..02067a32c10 Binary files /dev/null and b/Tests/images/fli_oob/06r/others/06r02.fli differ diff --git a/Tests/images/fli_oob/06r/others/06r03.fli b/Tests/images/fli_oob/06r/others/06r03.fli new file mode 100644 index 00000000000..649668c0ad9 Binary files /dev/null and b/Tests/images/fli_oob/06r/others/06r03.fli differ diff --git a/Tests/images/fli_oob/06r/others/06r04.fli b/Tests/images/fli_oob/06r/others/06r04.fli new file mode 100644 index 00000000000..bff28ccfcea Binary files /dev/null and b/Tests/images/fli_oob/06r/others/06r04.fli differ diff --git a/Tests/images/fli_oob/06r/reproducing b/Tests/images/fli_oob/06r/reproducing new file mode 100644 index 00000000000..145b8b074a2 --- /dev/null +++ b/Tests/images/fli_oob/06r/reproducing @@ -0,0 +1 @@ +im = Image.open(d); im.seek(0); im.getdata() diff --git a/Tests/images/fli_oob/patch0/000000 b/Tests/images/fli_oob/patch0/000000 new file mode 100644 index 00000000000..e074e4a76c0 Binary files /dev/null and b/Tests/images/fli_oob/patch0/000000 differ diff --git a/Tests/images/fli_oob/patch0/000001 b/Tests/images/fli_oob/patch0/000001 new file mode 100644 index 00000000000..6cfd7f6478f Binary files /dev/null and b/Tests/images/fli_oob/patch0/000001 differ diff --git a/Tests/images/fli_oob/patch0/000002 b/Tests/images/fli_oob/patch0/000002 new file mode 100644 index 00000000000..ff5a6b63b19 Binary files /dev/null and b/Tests/images/fli_oob/patch0/000002 differ diff --git a/Tests/images/fli_oob/patch0/000003 b/Tests/images/fli_oob/patch0/000003 new file mode 100644 index 00000000000..12c15b43ea7 Binary files /dev/null and b/Tests/images/fli_oob/patch0/000003 differ diff --git a/Tests/images/fli_overrun.bin b/Tests/images/fli_overrun.bin new file mode 100644 index 00000000000..e1e8c590179 Binary files /dev/null and b/Tests/images/fli_overrun.bin differ diff --git a/Tests/images/fli_overrun2.bin b/Tests/images/fli_overrun2.bin new file mode 100644 index 00000000000..4afdb6f8909 Binary files /dev/null and b/Tests/images/fli_overrun2.bin differ diff --git a/Tests/images/fujifilm.mpo b/Tests/images/fujifilm.mpo new file mode 100644 index 00000000000..ff0deb8a676 Binary files /dev/null and b/Tests/images/fujifilm.mpo differ diff --git a/Tests/images/g4_orientation_1.tif b/Tests/images/g4_orientation_1.tif new file mode 100755 index 00000000000..8ab0f1d0d02 Binary files /dev/null and b/Tests/images/g4_orientation_1.tif differ diff --git a/Tests/images/g4_orientation_2.tif b/Tests/images/g4_orientation_2.tif new file mode 100755 index 00000000000..4ab0856411f Binary files /dev/null and b/Tests/images/g4_orientation_2.tif differ diff --git a/Tests/images/g4_orientation_3.tif b/Tests/images/g4_orientation_3.tif new file mode 100755 index 00000000000..ca0d0fe29f2 Binary files /dev/null and b/Tests/images/g4_orientation_3.tif differ diff --git a/Tests/images/g4_orientation_4.tif b/Tests/images/g4_orientation_4.tif new file mode 100755 index 00000000000..166381fb73f Binary files /dev/null and b/Tests/images/g4_orientation_4.tif differ diff --git a/Tests/images/g4_orientation_5.tif b/Tests/images/g4_orientation_5.tif new file mode 100755 index 00000000000..9fecaad65c3 Binary files /dev/null and b/Tests/images/g4_orientation_5.tif differ diff --git a/Tests/images/g4_orientation_6.tif b/Tests/images/g4_orientation_6.tif new file mode 100755 index 00000000000..6abc001ebc1 Binary files /dev/null and b/Tests/images/g4_orientation_6.tif differ diff --git a/Tests/images/g4_orientation_7.tif b/Tests/images/g4_orientation_7.tif new file mode 100755 index 00000000000..0babc91083f Binary files /dev/null and b/Tests/images/g4_orientation_7.tif differ diff --git a/Tests/images/g4_orientation_8.tif b/Tests/images/g4_orientation_8.tif new file mode 100755 index 00000000000..3216a372577 Binary files /dev/null and b/Tests/images/g4_orientation_8.tif differ diff --git a/Tests/images/hopper.dds b/Tests/images/hopper.dds new file mode 100644 index 00000000000..8b9af9ed9a4 Binary files /dev/null and b/Tests/images/hopper.dds differ diff --git a/Tests/images/hopper.fits b/Tests/images/hopper.fits index 85afa4ac167..7f28f75e5a8 100644 Binary files a/Tests/images/hopper.fits and b/Tests/images/hopper.fits differ diff --git a/Tests/images/hopper.gd b/Tests/images/hopper.gd new file mode 100644 index 00000000000..82d2408f93e Binary files /dev/null and b/Tests/images/hopper.gd differ diff --git a/Tests/images/hopper.pnm b/Tests/images/hopper.pnm new file mode 100644 index 00000000000..52368b2e234 Binary files /dev/null and b/Tests/images/hopper.pnm differ diff --git a/Tests/images/hopper.wal b/Tests/images/hopper.wal new file mode 100644 index 00000000000..f6260c6b33b Binary files /dev/null and b/Tests/images/hopper.wal differ diff --git a/Tests/images/hopper_16bit.pgm b/Tests/images/hopper_16bit.pgm new file mode 100644 index 00000000000..e482493dd40 Binary files /dev/null and b/Tests/images/hopper_16bit.pgm differ diff --git a/Tests/images/hopper_16bit_plain.pgm b/Tests/images/hopper_16bit_plain.pgm new file mode 100644 index 00000000000..a48ab55441a --- /dev/null +++ b/Tests/images/hopper_16bit_plain.pgm @@ -0,0 +1,4 @@ +P2 +128 128 +65535 +6425 5654 3598 6682 7453 7453 4626 5654 8738 8995 6939 5911 7710 7710 6682 7710 9252 9509 8224 6168 5654 5911 6168 7967 8481 7710 5397 7967 5654 28270 58853 41634 46517 42405 47031 30069 5654 10023 9252 4626 13107 44461 18247 5140 9766 31097 52685 60395 63479 62194 61680 61423 60395 50629 11822 14906 26471 23387 23901 24158 25700 25186 26985 26214 25700 22873 25700 24158 23901 25700 25700 25186 24929 24415 24415 24415 24415 24672 25443 25957 25700 25443 24929 24929 25700 25700 25957 26214 25700 25700 25700 25443 25186 25443 25443 25186 25186 25186 25186 25443 25443 25443 25443 25443 25443 25443 25186 25186 25443 25700 25957 25957 25443 25186 25443 26214 27756 28784 29555 29555 29812 29812 29812 29555 28784 28527 28527 28784 6425 5654 4112 6939 7196 7196 4369 5140 8224 8481 6425 5397 7453 7453 6939 8224 8738 9509 7967 5654 5397 5397 5911 7453 6425 8224 6682 6939 7710 33667 45489 23644 44718 15163 12336 22616 16191 7967 5140 6939 14392 43176 18761 7453 4369 7967 20560 37265 51657 61937 62451 61937 60652 52685 10794 9766 23644 24415 25443 21331 18761 20817 23644 25186 25186 25957 25186 25186 26471 24672 23644 25443 25443 24415 24415 26214 26471 24929 24415 24929 25700 25443 25186 25443 25957 25957 26471 26214 26214 25957 26214 25700 25443 25443 25443 25186 25700 25186 25186 25186 25186 25443 25443 25700 25700 25700 25443 25443 25186 25186 25186 25443 26214 25957 25700 25957 27242 28013 29298 29555 30069 30069 30326 29812 29298 29041 29041 29298 6425 6168 4369 7196 7196 6939 3855 4883 7967 8224 5911 5140 7196 7453 6682 8224 7967 8995 7967 5911 5654 5911 6168 7453 8995 7453 5654 7196 8738 30583 25186 15934 43433 17219 7453 7710 6168 8481 11565 5397 18504 44461 14649 4626 7196 8995 6168 10023 13364 51657 60652 61680 60909 57568 35466 7453 9252 10537 9509 7453 13364 9766 10537 13364 11565 16448 21331 26214 25700 29041 24929 27756 25443 25443 25186 25186 24929 24929 25957 27242 25957 25700 25700 25957 26214 26214 26214 26214 26985 26471 26471 26214 26214 25700 25443 25443 25957 25700 25186 25186 25186 25443 25700 25957 25700 25443 25443 25443 25443 25186 25186 25186 26471 26214 25957 25957 26471 27499 28784 30069 30069 30326 30326 30069 29555 29298 29555 29812 5911 5654 4626 7453 7453 6682 3598 4883 8224 8738 5911 5397 7196 7710 6682 8481 7967 9509 7967 5911 5654 6168 6425 7196 9766 6939 5397 7453 7453 17990 10537 20817 46003 15420 6168 6425 7710 6939 9252 5397 14392 45232 15934 5654 5654 7710 7967 8481 7967 48830 63222 37265 32896 44204 14649 6682 10023 7453 6682 20303 32382 31354 19018 13621 12079 12079 12079 10537 20560 24672 27499 23901 25186 25700 25186 24929 26471 28013 26471 23644 26214 26214 26214 26471 26728 26728 26471 25957 26728 26471 26471 26214 26214 25957 25957 25957 25957 25700 25700 25443 25443 25700 25957 25957 25186 25443 25443 25443 25700 25700 25443 25443 25700 25957 25957 25700 25957 26985 28527 29812 29298 29555 29555 29298 29298 29298 29555 29812 5397 5654 4883 7196 7196 6425 3855 5397 8481 8738 6425 5397 7196 7710 6939 8224 7967 9252 8481 5911 5654 6168 6425 6939 5911 7967 7196 5911 5654 9252 5397 21845 42662 12336 7453 6168 9252 9252 7967 4883 22616 43433 8481 6682 7453 6168 8995 8224 11565 52685 51914 12079 6939 4626 8224 7967 10280 39064 35723 18761 34952 61166 29041 8224 31354 43176 20303 12079 10794 13621 24672 28527 25957 26471 26985 26728 25957 25957 26728 27499 26471 26471 26728 26728 27242 26985 26471 26214 26214 26214 26471 26471 26214 26214 26214 26214 26214 26214 26214 26214 26214 26214 26214 25957 25957 25957 25957 25957 25957 25957 25957 25700 25186 25700 26214 25957 25957 26471 27499 28527 28013 28270 28527 28527 28527 28527 28784 29041 5397 5654 5140 7710 7196 6425 3855 5654 8481 8738 6425 5397 7453 7453 6425 7710 7710 9252 8224 5397 4883 5654 5911 6425 6682 8738 6682 5397 6682 6425 5654 21331 45232 13107 8738 7710 8738 6939 5654 8738 23130 44975 10280 7710 5911 6425 7967 5140 12079 55512 21845 11051 5140 5397 6682 22616 60138 64250 64764 53970 35723 65021 43690 40863 63993 65021 62451 43690 10537 12079 13878 24415 27499 24672 24415 27756 28270 25957 24929 26471 27242 26728 26985 27242 27242 27242 26728 26214 25700 25700 25957 25957 26471 26214 26471 26471 26214 26471 26471 26728 26471 26471 26214 25957 26728 26471 26214 25957 25700 25700 25700 25700 25700 25957 25957 25957 25957 26214 26471 26985 27242 27242 27499 27756 27756 28013 28270 28527 5140 6168 5654 8224 7196 6168 3855 5397 7710 8224 6168 5654 7710 7967 6682 7967 7710 9252 7967 5397 4626 5654 5654 6168 8995 7967 5397 5911 6425 6168 6682 21588 42919 13878 6425 6425 9766 9509 7453 4883 21588 43433 10280 5140 5140 9509 7967 10280 11822 47288 42148 38036 7196 5911 31868 63736 57054 40349 59881 62708 65278 63993 62194 65021 63222 53713 52685 59110 49858 12850 11051 34952 49601 39578 30069 26471 26728 26985 27242 27499 27242 26985 26985 26985 26985 26985 26728 26471 25700 25957 25957 25957 26471 26214 26214 26214 26728 26728 26728 26471 26471 26471 26471 26471 26985 26471 26214 25700 25443 25443 25700 25957 26214 25957 25700 25957 26214 26214 26214 25957 26471 26985 27499 27756 27756 27756 28013 28270 5654 6425 5654 8481 7453 6168 3341 5140 6939 7710 6168 5397 7710 7967 6682 7967 8224 10023 8481 5140 4626 5397 5911 6168 6168 6939 6425 6682 4626 7196 6939 19789 43690 15420 5397 7967 9509 6682 7710 7196 28013 40349 7710 5911 7967 7967 7453 27756 55512 64507 65278 34695 6168 16962 31354 22102 31097 24415 19018 26214 42662 57825 57054 42148 25957 22616 33410 31611 18761 25957 8995 13107 60395 64764 63222 51657 36751 27756 26471 27499 27499 27242 27242 26728 26985 26985 26985 26471 25957 25957 25957 25957 26214 26214 26214 26214 27242 26985 26471 26214 26214 26471 26728 26728 26471 26214 25700 25186 25186 25443 25957 26214 26214 25957 25443 25700 26214 26728 26728 26728 26728 27242 27499 27756 28013 28013 28270 28270 5911 6939 29555 10280 6168 5654 4112 4369 7196 8995 5654 4626 10794 8738 5911 7453 6939 10023 6682 6168 4626 5654 4369 5397 7453 7196 7453 5140 4883 6425 7710 20303 44204 13621 6682 8224 8738 8995 7710 6682 25186 41634 7967 8995 6425 11308 49858 63479 65535 64250 65278 26985 7710 7196 7453 34952 48830 62194 59624 36751 59881 50886 54484 51400 37779 62965 55769 54484 21331 6682 12079 9509 47288 65278 63736 64764 64507 53713 32639 27242 28270 27756 25700 29298 24672 28784 27499 26214 26471 26471 26728 26471 26728 26471 26471 26728 26985 26471 26214 26471 26471 26214 26471 26985 25957 25957 25700 25443 25443 25700 26214 26728 25700 26214 26728 26471 26214 25957 26214 26471 26471 26471 26471 26471 26728 26985 27499 27499 5654 9509 52428 23130 9252 3855 5397 4369 9509 6682 3341 5140 10280 5654 7453 7710 9252 8738 6682 6682 5397 4112 4626 6939 7196 6939 6168 6168 7967 6939 7196 19018 43690 13621 6425 8481 8995 8738 7196 6168 25700 41634 8224 3598 20046 58339 63736 65021 62965 65021 63993 22102 6682 8481 8481 30840 47802 56026 57311 62965 64250 62965 64507 64250 63993 58596 58339 54484 22359 13364 7196 8481 37779 65021 65021 62194 65021 64764 60652 46774 29555 25700 27242 28270 27756 27242 25186 28013 26728 26471 26471 26471 26471 26728 26471 26728 26471 26214 25957 26471 26471 26214 26471 26985 26214 26214 25957 25957 25700 25957 26471 26728 25957 25957 26214 26214 25957 25957 25957 25957 26471 26471 26471 26471 26728 26985 27499 27499 4112 8995 54741 57311 11308 6425 4112 8995 6425 7710 6168 6682 6939 11051 6168 6682 6682 7196 8481 5654 3855 5911 7710 4369 8481 8224 6168 5397 7196 6425 7967 19018 43690 14392 6939 8481 8995 8224 6939 5654 29041 41634 7967 23130 64250 65278 63993 63736 63736 62194 56797 16191 10280 6425 7967 13878 54741 52428 54741 58082 56283 56026 53456 56026 59624 56540 46003 55769 18247 10023 13364 12336 39321 65278 63993 65278 64764 65278 65278 63736 53970 32125 27242 28784 28270 24929 29812 25186 26214 26214 26214 26214 26214 26471 26214 26471 26214 25957 25957 26214 26214 26214 26471 26985 26471 26471 26471 26214 26214 26214 26728 26985 26728 26471 26471 26471 26471 26471 26471 26214 26728 26471 26728 26728 26985 27242 27499 27756 5140 10023 50886 61680 32896 20817 44204 23387 6939 7453 5397 5911 8481 32125 9766 6168 8738 8738 8738 6425 3084 4369 4369 5911 6939 7453 7453 5911 5140 6168 7967 18504 43690 15420 6939 8224 9509 8224 7196 6168 30583 38036 15163 62965 62194 64764 63222 57825 44461 44461 53199 15163 3084 5911 5654 9766 27756 55769 52171 63222 64250 64250 64507 62965 64250 63479 53456 42148 10794 5911 7453 6682 29812 60652 64764 65021 62451 65021 64764 65021 65021 56540 29555 26985 26985 28527 25957 26728 25700 25957 25700 25700 25700 25700 25700 25700 26214 25700 25700 26214 26471 26214 26214 26728 26728 26728 26728 26471 26471 26471 26728 26985 27242 26728 26471 26471 26728 26728 26471 25957 26728 26728 26985 26985 26985 27242 27499 27756 6682 7453 50115 64507 60652 58853 50115 18761 7967 6682 5397 6682 9252 46774 24158 8224 6168 8738 6682 6168 6168 7710 1799 5911 5397 6425 6425 7196 5654 6682 6425 16448 44204 16705 6425 7453 8995 7710 7453 6682 28013 40092 54998 63222 64764 61937 40606 35209 29298 15420 8481 6168 5397 8224 6682 9766 47288 56540 39321 54998 63736 65021 65278 65535 63736 61423 39835 55769 27242 10280 8481 10023 15163 32125 48830 55769 64507 64764 63222 64507 62451 64507 48830 26985 26471 26471 26985 26214 25700 25700 25700 25957 25700 25700 25700 25700 26214 25443 25700 25957 26214 25957 25957 26471 26214 26471 26471 26471 26214 25957 26214 26471 26728 26214 25957 25700 26214 26214 25957 25700 26728 26728 26728 26728 26985 27242 27756 28013 8995 34438 58339 61937 61166 60652 41634 9766 6939 7710 7196 6425 7967 50886 42148 6939 8738 9252 24415 26214 6168 3598 6939 4626 7710 14649 5140 6682 6425 6682 6168 16448 43690 17733 6168 6939 8995 7196 7453 6682 25700 50372 61166 63479 58853 31354 25700 12079 4112 5397 3598 5911 3855 4112 8224 6425 33667 42148 51914 58339 57825 64764 63222 64250 54741 53970 49087 51400 23644 8481 12079 7967 8995 6168 8481 25443 43433 56283 62965 64764 64764 65021 61680 32896 29298 25700 25443 28013 26214 26214 26214 26471 26214 26214 26214 26471 25957 25700 25700 26214 26214 25957 25957 26471 25957 26214 26471 26471 26214 25957 25957 25957 26728 26471 26214 25957 26214 26214 26214 26214 26471 26471 26728 26728 27242 27499 28270 28270 43947 59110 61937 64507 62451 59110 29555 4112 8738 8738 4626 5140 10537 54484 54741 21845 20560 48830 53456 20817 5654 6168 5140 4883 12593 35466 9766 5397 6168 5911 7453 16191 43176 18247 6168 6939 9252 6939 7453 6682 24929 56026 62451 60395 27242 13878 3598 4626 3341 3084 23901 28784 8224 6682 4369 10023 32125 61166 59624 53713 41634 58339 63993 63479 49344 54484 60395 60909 22359 10537 10023 8224 8995 26728 8738 10280 6939 20817 39578 49344 63736 61166 59624 53713 23901 29298 27499 26728 26471 26471 26471 26471 26471 26471 26471 26471 26214 25957 25957 26471 26471 25957 25957 26214 25957 26214 26471 26471 26214 25957 25700 25957 26985 26985 26728 26471 26471 26471 26728 26985 26471 26728 26471 26728 27242 27756 28527 28784 46003 57568 61937 63479 63736 63736 44204 15934 6939 8738 5911 9509 11822 51400 59110 57568 61680 59110 41377 7710 7710 5140 3084 5654 13878 54741 18247 6168 6939 6168 8738 13621 43176 18504 6168 7196 9766 7196 7710 6939 23130 58596 59881 34695 6682 3084 5140 27242 25186 11822 8481 38293 7196 16191 19789 9766 25186 48573 49601 49087 54227 39835 24672 27499 43433 48316 49601 52685 25443 11822 16191 8995 19275 35466 7196 25700 28527 6939 8738 19532 41891 61166 60909 57825 31611 26471 27242 27499 26471 26728 26471 26471 26471 26471 26728 26471 26471 25957 25957 26471 26471 25957 25957 26214 25957 26214 26471 26471 26214 25957 25700 25700 26471 26471 26728 26471 25957 25957 26471 26985 26214 26214 26471 26728 27242 27756 28527 28784 6939 12850 30840 63479 62708 52428 49344 23130 7710 9252 6425 19532 42662 60909 60138 59624 60652 55255 14906 8481 6425 7453 2056 5397 17990 58853 37008 6682 14649 30326 12336 14906 43947 20046 5911 5911 9252 8481 6425 7453 21588 47545 36237 6425 5911 6682 6939 44975 52428 49344 47031 37522 49601 57054 60138 54998 55255 31097 25186 9252 8224 12593 9766 9509 9766 12593 21074 45746 49858 59367 62194 44718 47288 35466 55512 56026 45232 13621 24929 14906 12079 42148 62708 57054 40863 27242 28013 26985 27242 25957 27499 25957 27756 26214 26471 26471 26985 26471 26214 26214 26471 26728 26471 26214 26471 26214 26214 25957 25957 25957 26214 26471 26728 26985 27242 27499 27242 27242 27499 27499 27756 27499 27499 27499 27756 28013 28527 28784 6682 6939 21588 62708 49087 16448 14649 10794 6939 13364 37779 56283 62965 60909 62451 61680 60138 27242 8224 7710 6939 2570 8738 4883 19275 58339 53456 36751 50115 32639 8224 10537 43176 20817 6425 5911 9252 8481 6425 6425 5654 1285 4112 4626 2570 6425 5654 11822 34952 38550 14135 44975 58339 46003 44975 57568 61423 60395 62451 49344 29298 20046 7967 13878 27242 50629 63222 60138 61166 58596 51400 60909 59367 35980 47545 51657 48573 57311 51914 11822 5911 11051 48573 47288 28013 13878 10794 24158 27242 26214 26985 25700 27499 26471 26214 26214 26985 26214 25957 25957 26214 26214 26214 25957 26471 26471 26214 26214 26214 26471 26728 26985 27242 27499 27756 27756 27499 27499 28013 28270 28013 28013 28013 28013 28270 28527 28784 29041 4112 9509 20303 60395 32639 7196 7196 5654 7710 10537 24415 45489 56797 63736 62451 61423 57825 43690 11308 8481 4112 4626 6168 7196 28270 60909 56540 46260 44461 13107 6939 12593 41891 22102 6425 5911 8738 8481 5654 4883 6425 4112 4112 4883 3855 4112 2570 5654 3341 8995 15420 27242 13364 8738 46260 50629 44204 37265 25700 19275 16191 20046 12850 19532 17990 20817 37008 40863 54741 55769 33667 19789 28527 28784 4626 9509 25700 49344 44461 40863 41891 12593 13878 7453 4626 3855 7196 12079 26728 26985 26985 25700 27756 26728 26985 26728 26985 26728 26214 26214 26471 26471 26214 26214 26728 26728 26471 26471 26728 26985 27242 27499 27242 27499 27756 28013 27756 27756 28013 28270 28013 28013 28270 28527 28527 28784 29041 29041 5140 8481 15934 53199 16705 7967 7453 3598 5397 9766 5140 9509 19018 50629 63479 63222 61937 59110 27756 7710 6939 12079 24158 35980 53456 60909 50372 46774 24158 5397 5911 9252 40863 23130 5654 6425 8481 6682 4883 3598 1799 4626 4626 3855 1799 3598 5654 4883 6682 3341 2827 6682 14392 7967 7967 8481 8224 8738 7710 5140 7710 6682 4883 9252 7453 6168 11565 8738 11822 12593 15677 17990 44461 46260 48316 16448 6425 6168 24415 35209 14392 8738 6425 4626 6682 2827 6168 3598 23644 27242 26985 25957 27242 26728 27242 27242 27499 27499 27242 27242 27242 26985 26985 26728 26985 26985 26985 26985 26985 27242 27242 27499 27242 27499 27756 27756 27756 27756 28013 28270 27756 28013 28270 28527 28784 28784 29041 29041 7453 8738 15420 38036 8224 7967 7196 4883 4883 6168 6425 4883 9766 50372 61166 29812 33410 50629 52428 14906 6682 12336 21845 38550 51657 56797 56283 45489 11308 6168 6682 9766 40092 23901 4626 6939 7710 4883 3341 3341 4369 3084 3598 5911 4883 3084 4369 771 5397 4112 3598 6939 4112 6682 6168 3341 5140 4626 6682 5654 9252 6168 7710 6168 7967 7453 5911 7196 9509 6168 6425 9509 25186 33410 56797 54227 20560 8481 5654 3855 3341 4369 3855 2313 4112 3598 1542 4626 19018 25957 25957 26214 26471 25957 26728 27499 27756 28013 28013 28013 28013 27756 27499 27499 27499 27499 27499 27242 27242 27242 27242 27242 27756 27756 28013 28013 27756 28013 28270 28527 28013 28270 28527 28784 29041 29041 29041 29041 3598 7967 11051 15420 6682 8738 6682 4626 7453 7453 7967 8995 6939 51400 48316 11308 7967 9766 23644 21845 5140 6168 5140 8224 33153 57311 57825 47802 28527 7196 6939 7453 38807 25700 4883 6682 6425 3598 3084 3341 2570 4626 2827 1799 5911 3084 3341 6168 2827 4626 3598 1799 4883 3855 3598 8481 8224 5654 4883 5140 3598 3855 5911 4112 5654 2827 5140 7196 4369 7710 6939 7196 10794 23644 12336 7196 4626 6168 2056 4369 3084 2056 3341 4626 2570 4369 2827 5397 15934 25957 26214 26985 26985 26471 26985 28270 28013 28270 28527 28527 28270 28013 27756 27756 28013 28013 27756 27756 27499 27499 27242 27242 28270 28270 28270 28270 28013 28013 28527 28784 28527 28784 28784 29041 29298 29555 29555 29555 6939 8738 7196 8224 12336 9252 5654 5654 6682 7967 2827 7967 10280 51914 26214 8995 8481 11308 9509 10280 6682 3341 4626 3598 25186 61166 49344 48830 50115 13107 3598 8224 37522 28527 5397 5140 4369 3084 4369 2827 4369 3341 4883 2827 3855 4112 4626 1028 3598 1799 4369 4369 514 5140 3855 1285 2827 3855 4883 6682 4369 5140 4883 4883 4369 5140 6168 5140 5654 7196 2570 4112 3084 2313 4626 2056 4112 2570 0 3598 3084 3084 3341 3855 1028 2056 4883 4112 15934 27756 26985 28013 27756 27499 28013 28270 28013 28270 28784 28784 28270 28013 28013 28270 28527 28527 28270 28013 28013 28013 27756 27756 28527 28527 28527 28270 28013 28013 28270 28527 28527 28527 28784 28784 29041 29298 29812 29812 5140 10537 6682 7453 10023 10280 7710 3855 4883 6682 6168 5654 11565 42405 8481 9509 7967 8995 8224 6939 5397 5397 3855 4626 32382 57311 20560 10023 37265 32382 6939 5911 36494 30326 6425 4626 2827 3084 5140 3084 3855 2313 4883 3084 4626 4112 3598 3855 3084 3341 1799 4112 4883 257 4369 2313 3341 2827 2827 2056 2827 3341 2313 3084 3855 2827 2313 3084 1799 1799 4112 2827 3084 4369 1285 2313 3084 1799 6168 2313 3598 1028 4112 2570 4626 4369 2570 3341 16448 28270 27242 28013 27756 28013 27499 27756 28270 28527 29041 29041 28527 28270 28270 28527 28527 28527 28270 28270 28270 28270 28270 28270 28270 28270 28270 28013 27499 27499 28013 28270 28527 28270 28527 28527 28784 29041 29555 29812 5911 8224 7196 8738 9509 10280 6939 5140 7196 6168 4626 7710 11308 14649 9252 6939 8995 8738 8738 7710 5654 3855 3855 4626 37779 42662 6425 3598 7453 13107 7196 5140 33410 30583 5654 3598 3341 4112 4112 3855 3598 4112 3341 3084 3598 3084 2827 3341 2056 3084 3341 3084 2827 3341 3084 1799 2570 3341 2827 2570 3598 3084 2313 2827 3598 3084 2827 2827 3084 3084 2827 2570 2570 3084 3084 2570 2570 3341 3084 2056 3341 2570 3084 3341 3341 4369 4369 2827 16448 28784 27756 28013 26728 29041 27756 29812 28784 28784 29041 29041 28527 28270 28270 28270 28270 28527 28784 28784 28784 28784 28527 28527 28270 28270 28013 28013 28270 28784 28784 29041 29041 28527 28013 28013 28527 28784 28527 28527 6168 8224 7196 8738 9252 10023 6425 4369 5654 6939 6168 8224 8224 10023 7196 8224 8224 8481 8738 7967 5911 4369 4112 4626 33924 16448 5654 6425 4112 4369 5140 6939 31868 31611 4883 3598 3084 4112 3855 3598 2313 4626 4112 2570 2570 3855 3855 4626 3855 3855 3341 2827 2313 2570 3598 4883 3084 3598 3341 3084 3598 3341 2313 2570 2313 2570 2570 2570 3084 3341 3598 3341 2313 3341 3598 3084 2313 2313 2570 2827 2056 1028 1799 2570 2056 2570 3855 4369 14392 28270 28527 29041 28013 29555 28013 29812 29041 29041 29298 29298 29041 28784 28784 28784 29041 29041 29298 29298 29041 29041 28784 28784 29041 28784 28527 28527 28527 28784 28784 29041 29041 28527 28013 28013 28270 28527 28527 28527 5911 8224 7453 8995 8995 9766 5911 4112 5397 6682 6425 7453 6168 6168 6168 9509 7967 8224 8738 8224 6168 4626 4112 4626 14906 5911 9252 3341 2570 5911 5654 5397 30583 33924 4112 4112 3084 3855 3598 3598 5397 4112 2313 2827 4883 4112 1542 1542 2827 2056 2570 4369 4112 2827 2313 3084 2827 2827 2827 2827 3084 3341 2827 3084 3598 3598 3598 3341 3084 2827 2827 2570 3598 3084 3084 3341 3598 3598 3341 3598 3598 2313 3084 4369 3598 2570 3084 3084 12336 28527 29041 29812 28527 29812 28270 29812 29298 29298 29555 29812 29555 29298 29298 29298 29555 29555 29812 29812 29555 29298 29298 29041 29555 29555 29298 29041 28784 29041 29041 29298 28784 28527 28270 28013 28270 28527 28527 28527 5911 8224 7196 8995 8995 9509 5654 3598 6682 6425 4626 6425 7453 6425 7453 9509 8481 8481 8738 7967 6168 4112 3855 4369 4369 8224 7196 4626 5397 6168 3598 6425 30069 36237 4112 4626 3084 3855 3341 3341 3341 3598 3084 2827 3084 2570 2827 4883 4626 3341 2313 2827 3341 3341 3598 3855 4369 3598 3855 4112 3598 3598 4112 4369 3855 4112 4112 4369 4883 4883 5397 5654 4626 3855 3341 3341 3084 2570 2570 3084 2827 1542 2056 3084 3084 3855 4112 3341 10794 29298 29298 29812 28270 29298 28784 30583 29812 29812 30069 30069 29812 29812 29812 29812 30069 30069 30069 30069 30069 29812 29812 29555 29812 29812 29555 29298 29298 29298 29298 29298 28784 28527 28527 28270 28527 28527 28784 29041 5911 7710 7196 8738 8738 9766 5654 3855 7196 6425 4112 6168 8738 6682 8481 9252 8995 8995 8481 7196 5397 3855 3598 4369 7196 8224 5140 7196 3084 3341 3855 6168 29812 35980 4112 4369 2827 3598 3084 3341 1285 3598 4369 3598 3855 4112 2313 2056 3341 5654 8224 9509 11051 13107 14135 14392 12850 10794 10280 9766 7453 6939 7967 7453 9766 10280 12079 13878 16448 18761 20560 21588 22616 20560 16962 12850 8224 4626 3341 3084 3341 3084 3084 3084 2056 3341 4369 3341 7967 29041 29555 29555 28784 29555 29298 30840 29812 30069 30326 30326 30069 29812 30069 30326 29812 29812 29812 29812 29812 29812 30069 30069 29812 29812 29555 29555 29555 29555 29555 29812 29041 29041 28784 28784 28784 28784 29298 29298 5911 7967 6939 8224 8481 9509 5911 3855 5911 6682 5140 6168 8738 5911 8738 8738 9252 8995 8481 7453 5140 3855 3598 4369 6682 6682 8481 4626 2313 5397 5397 4883 29812 33410 3855 3855 3084 3341 3598 3598 4112 2827 2056 3084 3598 2570 4626 9509 19532 25443 31611 34181 34695 34952 35209 34952 34181 31354 31097 30069 26214 24672 25443 24672 25957 25957 27756 30069 32896 35723 37265 38036 35723 36237 37008 36494 33667 24929 12850 3598 2313 2056 3598 3855 2570 2570 3598 2827 5140 28527 29298 30326 30069 30326 29812 30583 30326 30326 30583 30326 30326 30069 30326 30326 30069 29812 29812 29812 29812 30069 30326 30326 30069 29812 29812 29812 29812 30069 30069 30069 29298 29298 29298 29298 29041 29298 29555 30069 6168 8224 6939 8224 8224 9252 5911 4369 4883 6939 5654 5911 8481 5654 9252 7967 8995 8481 8224 7453 5654 3598 3598 4626 5911 4626 7453 5654 6425 5397 2570 6682 31868 31611 4883 3084 2570 3341 3341 3084 4112 3084 2313 3084 4626 9766 22873 37008 40349 42148 43690 43690 43947 44204 45232 46003 46003 43690 44461 44718 40606 39064 40092 39064 40863 40606 41120 42148 44461 45489 45232 44461 43433 42405 41377 41377 43176 41120 33153 24672 9509 4369 1542 2827 3084 3084 3598 4369 4369 29298 29555 30583 30326 30583 30326 30326 30840 30840 30840 30840 30326 30326 30583 30583 30326 30326 30069 30069 29812 30069 30069 30069 30326 30326 30326 30326 30326 30326 30326 30326 29812 30069 29812 29555 29555 29555 30069 30583 6682 8481 6939 7967 8224 9252 5911 4112 4369 6939 5140 5397 8481 6425 10023 7967 8224 8224 8224 7453 5911 3855 3598 4369 6425 7196 6425 6939 3855 4626 4883 4883 33667 30840 5911 3341 3084 3341 2827 2827 2570 5911 6425 7967 19018 33667 42148 43433 43947 42148 41891 44975 48573 49858 50629 50886 50115 47802 50115 51914 48830 48059 49344 48316 49087 48316 48573 49858 50886 51400 50372 48830 45232 46003 45746 43690 43690 44204 42148 38550 32896 18247 5911 3855 3855 3341 3855 4883 5397 30583 29812 29812 29812 30069 30326 30583 31097 31097 31097 30840 30583 30583 30583 30840 30840 30583 30326 30069 30069 30069 30069 30069 30583 30583 30840 30326 30583 30583 30326 30326 30069 30326 30326 30326 29812 29812 30326 30840 19275 9766 7453 9766 7453 9766 6682 4369 4626 7453 6425 6682 5911 9252 9252 7710 8481 7967 7710 7967 6168 3598 3598 4626 5397 8224 5911 5397 6425 3598 3855 6168 31868 33410 3341 3084 2570 3598 4112 3855 10794 20817 17990 22359 35209 41634 45232 46774 44975 45746 46517 47288 47802 49344 50629 51657 52685 51657 49858 50629 50372 50372 53199 52685 49344 52685 52685 51400 49087 53970 53970 51914 51914 46774 47802 43947 46517 43947 44204 39578 39578 32382 24158 15934 3341 3084 4369 3341 6682 30840 30326 29298 30840 30583 31354 30583 31097 30840 30583 30583 30840 30840 30583 30326 30583 30326 30069 30326 30326 30326 30326 30069 30326 30326 30326 30326 30326 30326 30326 30326 30583 30583 30326 30583 31097 31354 31868 32125 48573 8481 8738 9252 6682 16705 10794 5140 5911 6425 3855 7453 6425 6939 7196 7710 7710 7710 7710 7967 6168 3855 3855 4626 6168 8995 7196 6168 6168 4369 4626 4883 30583 36237 3341 3084 4883 1542 1028 9252 22873 23644 21588 29555 39835 43690 45746 44975 46517 46517 47802 48830 50115 51657 52428 52685 51143 50886 51143 52685 51400 49858 50886 50115 52428 52171 51400 51657 50629 49601 48573 48830 49087 51914 45489 43433 44461 43947 42919 40092 40092 36237 35209 24158 14906 4112 4626 2570 7710 30326 29812 29298 30583 29555 30583 31097 31354 31097 30840 30840 31097 31097 31097 30840 31097 30840 30583 30583 30840 30840 30583 30326 30583 30583 30840 30840 30840 30840 30840 30840 30840 30840 30840 31097 31097 31354 31611 31611 60138 38550 7710 12336 34952 45232 8481 2827 7196 8995 7453 7453 6425 8481 7196 7453 6425 6939 7967 7967 6168 3855 3855 4626 5911 7967 7196 6682 5911 5140 6168 4883 31611 34181 3084 4626 3084 3855 8738 17990 21331 19789 25700 35466 40092 42662 45489 45232 48316 49087 50115 50372 49087 48059 47031 46774 49601 50372 50886 52428 50886 48830 50115 49601 47288 48830 48316 45746 45489 41377 37522 35466 37522 37265 40606 40863 38807 40349 40606 41377 39835 37779 37265 33410 23387 7967 2827 5140 10537 31354 31097 30583 31611 30326 31611 32382 31611 31354 31097 31354 31611 31611 31611 31354 31611 31354 31097 31097 31354 31097 30840 30840 31097 31354 31354 31611 31611 31611 31611 31611 31097 31097 31354 31611 31611 31611 31354 31354 59624 57825 25957 51914 59367 24672 7196 3598 18247 30583 6168 7453 6939 7196 7453 7453 5911 6939 7967 7710 5911 3598 3598 4883 6939 7967 7453 7453 6168 5397 6425 4883 32382 34695 3598 3855 514 4112 10537 16191 19789 22616 31868 36494 36751 40092 42662 42919 39835 39064 37522 35466 34181 34695 37265 39578 46774 49344 50629 51143 49858 49601 51914 50115 44718 47288 46517 41120 39835 34695 33153 34438 31097 34952 41120 41634 46774 41634 39321 39835 40092 38807 35209 34695 30840 17990 5654 3341 12336 31868 31611 30069 30840 30840 30840 31097 31611 31611 31354 31611 31868 31868 31868 31611 31868 31611 31354 31611 31611 31611 31611 31354 31868 31868 31868 31868 31868 31868 31611 31611 31354 31354 31611 31611 31868 31868 31868 31868 57568 61680 61423 58339 47802 10537 4883 5140 19789 55769 13621 9509 6682 4369 11051 8738 6425 6939 7710 6939 5397 3855 3855 4626 7196 7453 6939 7453 6168 4112 5397 4883 30069 36494 4369 3084 3598 5654 9766 17990 24158 26985 32639 35209 38036 41891 43176 43176 42919 40092 37008 34952 35209 36494 39321 41891 42919 47545 49601 49344 48316 49858 52428 49344 48830 45232 41891 40092 40092 30069 24158 25186 24158 27242 32896 38550 45232 42405 45746 37779 37265 40606 39321 29555 30840 19532 11051 6168 14649 32896 32896 30583 31097 32382 32382 31097 31868 31611 31611 31611 31868 32125 31868 31868 31868 31611 31611 31611 32125 32125 32125 31868 32382 32382 32125 32125 31868 31611 31354 31354 31868 31611 31611 31611 31868 32382 32639 32896 61423 58596 57825 57054 24415 7710 9252 2827 24672 58082 39835 9252 7967 25700 39578 12336 6939 7453 7453 6939 5397 3855 3598 4112 4883 6168 5140 5140 4369 3341 4883 5654 30840 32639 4369 4883 4112 7710 18504 26985 26728 29041 31868 36751 40863 40349 39321 39835 39835 35466 30069 28270 28270 28784 29555 29812 38807 43947 46260 45489 44461 47288 51143 49344 48316 45746 41891 37779 33667 25700 22359 23644 20560 15934 17733 16448 14649 16448 16191 19018 19789 24158 24158 25700 29298 21331 10537 7967 19018 33924 33410 31354 31611 33667 32896 31868 31868 31868 31611 31868 32125 32125 32125 31868 32125 31868 31868 32125 32382 32382 32382 32125 32639 32639 32382 32382 32125 31868 31611 31611 32125 32125 31868 31868 32125 32639 33153 33410 57825 60138 57825 59110 22873 9252 3084 3855 20303 58082 60909 27242 44718 58082 33924 7453 6939 7196 7453 6682 5911 4883 4112 3598 4626 8738 6939 3855 3341 3855 5140 5140 34181 30069 5140 4369 1799 8738 26214 28784 24929 26985 30069 38036 40349 37008 38293 38036 37265 32896 28013 25700 25700 27499 30583 33924 31354 38036 43433 45232 44461 46517 51400 51914 50115 45489 38293 34181 30840 25957 16448 9252 14906 21331 26214 23901 23644 20560 11051 11051 29041 23901 11565 17990 22616 23901 11051 10794 23130 34181 32382 30840 31097 31868 30840 31611 32382 32125 31868 32125 32125 32125 32125 31868 32639 32639 32382 32382 32382 32639 32382 32125 32639 32639 32639 32639 32639 32382 32382 32125 32382 32382 32382 32382 32639 32896 33153 33153 59367 62708 62194 58082 54484 14135 8224 4369 25443 55769 61423 60395 54998 48316 10794 5397 6168 6939 7453 7196 6425 5140 3855 3341 8481 14649 11565 5140 3855 4369 5140 3855 32639 32382 6168 2827 6425 14906 29555 29298 25700 23644 20817 23130 18504 12336 12593 7453 6939 7967 9766 10537 9509 8738 11822 15163 23130 32639 43176 50115 50115 49601 52171 52942 50886 46517 38550 30326 18504 17990 26985 38550 31097 34181 36237 38293 31097 38036 59367 27756 7196 12079 10794 13364 18761 21845 16448 15934 29555 37265 34181 33667 33667 32382 31354 33667 32639 32382 32125 32125 32382 32382 32125 31868 33410 33153 32896 32639 32639 32639 32382 32125 32639 32896 32896 32896 32896 32896 32896 32896 32382 32639 32639 32896 32896 32896 32896 32896 45746 60395 52942 56026 60138 43433 6939 14906 50886 59110 58082 57311 54998 24415 9766 5397 6682 6939 6682 8224 3855 19275 13107 12593 36494 41891 7710 5654 2827 4626 4112 4883 31354 31097 7967 4883 8995 25957 34181 27756 21845 16705 7710 13107 15677 13107 7967 30069 48830 44461 36494 31868 32125 38293 38807 24672 20817 16448 29812 38293 49344 49858 47288 46260 42148 36237 27756 31097 45232 35209 30069 13107 9766 11051 15677 19789 30840 31097 39835 51400 15934 12336 22873 17990 17733 23644 27756 24415 33924 32382 34695 33667 33153 33924 32382 32382 32639 32639 32639 32639 32382 32382 32382 32382 32896 32896 33153 33410 33410 33153 33153 33153 32896 32896 32896 33153 33153 33153 32896 32896 33153 32896 32896 32896 32896 32896 33153 33153 37779 54998 17990 14649 24929 39064 23644 14649 52171 56797 56283 56026 48830 10794 7453 6168 6168 7196 6682 7967 5397 18504 28013 33410 46774 20303 4369 2570 4626 4369 3855 4883 31868 30069 7967 13621 19275 30583 31354 24158 17476 14649 4626 7196 4112 10537 32382 33153 34952 25186 15163 12336 11565 10280 23130 39321 35466 33667 35723 36751 43176 43690 43433 38036 36751 31354 32382 32896 30840 28013 12079 20303 7967 13364 11822 10794 16191 22616 20046 18504 20303 17990 15420 10280 10537 20817 31354 38293 44461 40349 32639 34952 34181 33153 33667 34952 33153 33153 33153 32896 32896 32896 32896 32896 33410 33410 33667 33667 33667 33410 33410 33153 33153 33153 33153 33410 33153 33153 33153 32896 32639 32896 32896 33153 33153 33410 33667 33667 35723 48059 9252 9766 5140 8481 7453 6939 31868 55769 59881 59110 55512 29041 8995 7196 6939 8738 6939 6682 5140 12336 28013 28784 28270 8738 4626 3598 3341 4112 5397 4369 32382 34438 7453 13621 29812 33924 27499 18761 12079 12593 14906 13364 7196 25957 29041 30840 22873 12850 10280 8995 9766 8995 14392 19275 30583 29298 28784 37779 38550 34181 35980 34695 33924 27242 36494 34695 28013 14906 20303 12079 3855 9509 9766 11051 11822 18761 20303 23387 31097 25700 20560 20817 23130 32639 36494 27756 34181 43176 33924 35466 32125 32896 35466 33667 33667 33667 33667 33667 33667 33667 33410 33410 33924 33924 34181 34181 33924 33924 33667 33410 33667 33667 33667 33667 33410 33410 33410 33153 32896 33153 33410 33667 33667 33667 33667 33667 30326 28527 6682 8224 7967 6425 3341 3598 10537 51143 59110 52685 59110 51657 11822 3855 5654 8481 7453 6168 6939 15934 30583 26728 9509 2570 5397 5911 2313 4112 6168 4626 29041 35980 8738 14649 39578 34952 29555 27499 18247 21331 15163 16962 12593 22873 17990 25700 12593 11051 14392 6425 4369 11565 22616 26728 17219 21588 23644 35980 32896 42919 49601 49087 42405 37779 39321 37779 26985 32125 28527 24929 22616 20560 19018 20046 22616 23901 27499 32382 39064 35466 37779 32896 28784 27756 31097 17990 15934 37008 35466 34438 31868 32896 35723 31868 34181 34181 34181 34181 34181 34181 34181 34181 34438 34438 34438 34438 34438 34181 33924 33667 33667 33667 33924 33924 33924 33924 33667 33667 33667 33924 34181 34181 33924 33667 33410 33153 19789 15934 10280 5140 5911 8738 4626 3855 7196 51400 37522 11051 26471 46003 29812 5911 6682 8224 7967 5397 4369 16191 29555 31868 25957 2056 3598 4369 5140 4112 4626 4883 29041 34695 8738 20817 35723 22359 21588 27242 28013 28270 26985 32896 27499 21331 20303 19018 17733 16705 18761 19275 25957 25700 25957 33153 35209 38550 38807 39835 25700 45489 52942 52942 47802 31868 45489 43176 39835 34952 32382 37779 34695 33153 30583 28527 30326 34695 36237 34181 36494 38293 42662 34181 30840 29298 35209 37265 15677 31354 33153 33153 35209 33410 33924 34438 34438 34438 34438 34438 34438 34438 34438 34438 34695 34695 34695 34695 34438 34181 33924 33667 33924 33924 34181 34438 34438 34438 34438 34438 34181 34438 34438 34438 34181 33924 33667 33410 10023 6168 5911 8224 8738 8481 3598 3084 10280 52171 20303 7196 8995 8224 13878 6939 6939 6168 7453 7453 6168 15420 22873 30069 38550 21074 3598 4112 4883 5140 4369 4112 30840 33153 6425 23901 28013 20817 27756 29041 29298 26985 37779 39321 29555 28270 19532 18247 15934 18247 26214 37522 43947 41377 36751 39064 37522 44718 49344 43433 25186 43947 50886 53199 51657 43176 43690 46774 39321 35723 44975 43176 46774 49601 46774 41891 35466 42405 43433 39835 40606 40349 40606 33667 30326 38550 40863 36237 21588 31097 33924 32639 35466 33924 33153 36237 34695 34695 34695 34695 34695 34695 34695 34695 34952 34952 34952 34695 34695 34438 34181 33924 34181 34181 34438 34438 34695 34695 34695 34695 33924 34181 34181 34438 34438 34438 34438 34438 5140 6682 7196 8224 6939 6168 3341 3855 11565 38293 5140 7967 9509 5140 6168 4112 7967 7196 7196 6939 5140 5911 4883 8224 22102 27756 5911 6425 3084 5397 4883 4369 32125 34695 5654 16448 21331 17733 31354 28270 26728 28013 35723 41891 33410 33410 25186 22359 22616 28527 35723 46003 46260 46774 42919 38036 34438 46517 51143 38036 31611 48316 52685 50115 52171 46260 34952 48059 49087 41120 36237 44204 47802 45489 42662 39835 35466 40606 43690 40863 42662 41377 41891 37779 31868 41120 48316 39578 22359 34181 35209 33410 33667 34695 34181 34952 34695 34695 34952 34952 34952 34952 34952 35209 34952 34952 34952 34952 34952 34695 34438 34181 34181 34181 34695 34695 34695 34695 34438 34438 34181 34181 34181 34181 34438 34438 34695 34695 6939 5397 6682 5397 7967 7453 4112 3855 8481 14649 6939 8995 7453 7453 6425 6168 5654 8481 7710 5911 5911 3855 2827 3855 5140 7710 6682 7196 4112 3598 4369 4883 28270 33924 7196 13107 26214 16448 35466 38036 32639 23644 33667 38036 35723 34181 35209 30583 26214 34695 33924 40092 37522 33667 30326 35466 50629 48830 44461 30840 40606 50372 52942 52685 50372 52685 36751 43690 46260 52428 48316 39578 38807 33924 36237 32896 34181 35980 44461 43176 38550 38807 38807 37265 35980 30583 41634 41891 24415 35980 33924 35209 33153 35466 34952 34438 34952 34952 34952 34952 35209 35209 35209 35209 35209 35209 35209 35209 35209 34952 34695 34438 34695 34695 34952 34695 34695 34438 34181 34181 34695 34695 34181 34181 34181 34438 34438 34438 5654 6168 5911 7196 8224 6682 4112 3598 6168 5654 6682 7710 7453 6425 5911 6682 6682 8224 6939 6682 4883 3341 4112 2827 3598 4626 5140 6168 3598 4883 4626 4369 28784 34438 5654 7967 31354 19018 40092 37265 29812 24672 35209 36237 38550 35466 38293 31868 25186 33667 34181 34438 35723 37265 45489 52685 52428 49344 43176 31354 47288 50115 53970 52942 53199 53713 45746 38293 49601 51400 51400 53970 51400 48316 43433 39578 40349 44718 45746 43176 40092 38807 38036 37008 29041 19018 33924 41377 32125 32639 34695 35466 33924 34181 35209 35980 35466 35209 34952 34952 35209 35466 35209 35209 35209 34952 35209 35723 35466 34952 34438 34695 35209 35209 35209 34952 34695 34438 34181 34181 34438 34438 34438 34438 34438 34438 34438 34438 6168 6425 6425 7196 8224 6425 3855 3084 5397 5397 6168 7453 7196 5911 5397 5911 6939 8224 7196 6425 5140 3855 4369 3598 2827 5654 4112 4112 6682 4883 4883 5140 30840 36237 3084 12593 30583 25957 41891 37522 24929 24672 33667 34181 38807 39321 39064 31097 31611 37008 42919 48059 51400 53456 54741 52942 51143 53713 38293 41377 49344 49858 52685 54484 53970 51914 53713 40863 44204 49858 52171 51400 51400 51914 49087 46003 46260 46517 43176 40349 41120 39321 31868 41120 34695 20817 28527 41120 37779 36494 36494 35723 33667 33667 34952 34438 35466 35209 34952 34952 35209 35466 35209 35209 35723 35209 35209 35466 35466 34952 34952 35209 35209 35209 35209 34952 34695 34695 34438 34438 34695 34695 34438 34438 34438 34695 34695 34695 5911 6168 6425 6939 7967 5911 3598 3084 5654 5911 6425 7710 7453 6425 5654 6425 6939 7967 6682 5911 4883 3855 4626 3855 4626 4883 4112 4112 6682 2827 4883 5911 28527 35466 14649 40092 49087 33924 40092 39578 24415 31097 37008 32125 34952 38807 38550 34695 25957 31611 47288 54484 53456 54227 55512 54227 54227 50372 32639 47545 50886 52685 52428 54484 53970 52171 52685 47031 32896 45232 50115 50372 52171 51657 46774 43176 44204 43176 40349 39064 39064 34438 35466 41377 45232 29041 31868 36237 38036 35723 35209 34695 34438 35466 36237 35466 35466 35209 35209 35209 35209 35466 35466 35209 35980 35466 35209 35466 35466 35209 35209 35723 35209 35209 34952 34952 34952 34952 34952 34952 34695 34695 34952 34952 34695 34695 34695 34695 5654 5911 6168 6682 7710 5911 3855 3598 5397 5911 6682 7710 7710 6939 6682 6682 6682 7710 6168 5397 4369 3341 4626 3598 5397 3341 5654 4626 4883 4626 4369 3084 21588 43433 38293 46774 50629 43176 46260 34952 33153 42405 42148 31611 30326 35466 36751 38293 37008 32639 42405 51914 53199 53713 51657 49344 46003 31354 30840 44975 48316 52428 52685 51143 53456 53970 54227 50115 30840 25700 38550 43433 44204 43176 43433 45746 48573 47802 42919 37779 35466 34695 37522 42662 47288 40863 32125 36237 37008 35466 35209 35209 34952 35209 35209 34181 35723 35466 35209 35209 35466 35466 35466 35466 35723 35209 35209 35466 35723 35209 35466 35723 35209 35209 35209 34952 35209 35209 35466 35466 34952 34952 34952 34952 34952 34952 34952 34952 5397 5911 5911 6425 7196 5654 3598 3598 4626 5397 6168 7196 7453 6939 6939 7196 6939 7710 6168 5140 4369 3341 4112 3341 3855 4112 6425 4626 4369 6939 3341 8995 33924 42662 40349 46517 50629 42919 43947 39835 41120 47802 43947 32896 30583 34181 33924 37265 41891 33924 35209 41377 47802 48573 40092 34181 26728 25443 41634 46260 48573 51914 53713 51914 52942 52171 50886 52171 46774 32639 28784 30069 25957 26471 34181 42919 44975 45746 42405 34952 35723 36237 36494 41634 46774 37522 30840 39578 35980 35209 35466 35980 35209 35466 35723 34952 35723 35466 35209 35209 35466 35723 35466 35466 35466 35209 35209 35980 35980 35466 35209 35466 35209 35209 35209 35209 35209 35209 35466 35466 34952 34952 34952 34952 35209 35209 35466 35466 6168 6425 6168 6425 6682 5140 3341 3598 4626 5140 6425 7196 7196 7453 7196 7196 6682 7453 6168 5397 4626 4112 4626 3341 3855 5140 4883 5397 4626 2570 6425 31868 44461 44204 38550 48573 51914 44204 41891 43947 41120 46260 44718 36494 32639 33153 32896 36237 42662 39835 33410 24929 23901 27242 27242 26985 18761 37522 48059 45489 47288 49858 52428 51143 51914 52171 49858 49344 45489 42405 29812 41634 44718 43433 46260 47031 42148 40606 40349 36494 37522 35209 39578 43690 43690 32125 40092 38550 35209 34438 34952 35466 34695 35466 36494 36237 35980 35723 35466 35466 35723 35723 35723 35466 35466 35209 35466 35980 35980 35466 35209 35209 35209 35209 35209 35209 35209 35466 35466 35466 35209 35209 35209 35209 35209 35209 35723 35723 6168 6425 6425 6168 6682 5140 3084 3341 4883 5654 6682 7196 7453 7196 7196 6939 6425 6939 5654 5397 4883 4369 5140 3598 4626 4883 4112 5654 4112 4369 22102 48059 47031 45232 36751 49344 50372 39064 39321 45489 41891 43947 44718 37522 32896 32125 34181 37008 37008 42405 41891 38293 38807 42148 40092 32382 24929 43690 40606 38550 43433 45232 49087 48316 46003 43947 44975 38550 38550 37522 24415 31611 45232 47545 47802 44975 41120 39064 39064 39578 37522 35466 39064 43690 38036 40863 43690 38550 36751 35723 35723 35466 34181 34695 34952 34181 35980 35723 35466 35466 35723 35723 35723 35723 35980 35466 35723 35980 35980 35466 35209 35466 35466 35209 35209 35209 35209 35466 35466 35466 35209 35209 35209 35466 35466 35466 35466 35466 5397 6168 5911 6425 6682 5140 3341 3855 4369 5397 6168 6682 6682 6425 6168 6168 5397 6425 4883 5397 4883 4369 5140 3598 3598 4626 4883 3855 4112 15677 41120 44975 44204 45489 37008 49344 51143 34952 37779 42148 47545 43690 42405 35466 32382 33924 37008 37008 34695 39321 40863 43433 46003 47288 40349 24415 20046 26471 20817 22873 30840 32382 37008 38293 38550 37008 28527 25700 14135 20560 13107 22102 37265 43690 43690 41120 42405 41120 39064 41891 38807 35723 38036 30069 41891 46003 37265 41891 34952 34181 34952 35723 35723 36494 36494 35466 35980 35723 35466 35466 35723 35980 35723 35723 36494 35980 35723 35980 35723 35466 35466 35980 35466 35209 35209 35209 35209 35466 35466 35466 35466 35466 35466 35466 35466 35466 35723 35723 3855 7710 6168 4883 10023 4626 3084 4626 6425 2570 6682 8481 6682 6168 4626 6682 6168 7967 6425 4369 4369 5140 5140 2313 3855 3598 5140 5397 18761 38293 44461 45489 44461 44204 38036 49601 48059 35209 38293 43947 38550 38036 25186 32382 34695 33924 37265 40349 36751 38036 41120 41377 43176 38550 25957 20046 39835 32125 15677 15420 16962 19532 23130 23130 21588 20046 24158 19018 22616 38293 25443 11565 26214 35723 38550 36751 40863 38550 40092 42405 39835 39835 35980 32639 32896 34438 42148 39064 35980 35209 35209 35466 35466 35209 35723 36237 35466 35980 36237 35980 35980 35723 35980 36237 36751 36494 35980 35723 35466 35209 35209 34952 35209 34952 34952 34952 35466 35466 35466 35209 35466 35466 35466 35466 35723 35980 36237 36237 7196 5654 4626 9509 5654 5397 4369 771 5140 6425 5140 5397 6939 5397 5140 5140 5397 5654 4369 4369 4369 4626 5140 5140 3084 5140 5654 24929 41891 41891 45232 45232 44461 44204 38293 49087 47545 38550 39064 43690 41891 39321 42405 45232 38550 33667 36494 39578 41120 36751 42405 40606 38807 34181 19532 31868 43947 42662 40863 41120 39578 30840 19275 10280 15677 29041 36751 38550 37522 40349 33153 14906 13107 27756 34438 36494 37779 37779 41120 41377 38807 42148 48830 43947 45489 40863 45489 38293 35466 34952 34952 35466 35723 35466 35466 35723 35466 35980 36237 35980 35980 35723 35980 36237 36494 36237 35980 35466 35209 35209 35209 35209 34952 34695 34695 34695 34952 35209 35209 34952 35209 35209 35466 35466 35723 35723 35980 35980 5397 6682 9252 14906 6939 1799 4883 5397 3084 6168 7710 8224 7967 5911 5397 5397 5397 4883 4112 4112 5140 4883 3855 4626 3084 7967 36494 45489 39578 45232 45746 44718 44204 43690 39064 48830 47288 41377 37522 40863 46260 44461 48059 48830 45489 35466 35980 40606 42148 38036 38807 39835 32896 22359 25700 40349 42148 40863 42919 40606 43176 41377 37265 32639 34438 40092 37779 39835 38293 35723 40349 30840 12336 16962 26214 29041 33667 37522 40863 41891 40092 41120 46003 47802 41120 46517 43947 35980 35209 35209 35209 35723 35980 35723 35723 35980 35466 35980 35980 35980 35723 35723 35980 35980 35980 35723 35466 35209 35209 35209 35209 35209 35209 34952 34695 34695 34952 35209 34952 34952 34952 34952 35209 35209 35466 35466 35723 35980 16962 9509 32382 20046 6682 5911 2570 3084 8224 3341 5654 6425 5654 6425 5140 4369 5140 5911 5140 3855 5397 4883 2827 3855 8481 38807 44975 43690 45232 42662 45489 43690 43947 42662 39064 48830 47031 42148 32382 34438 47031 49858 47545 51143 45232 35980 38036 40863 42405 39321 37008 35723 24672 16448 37522 40863 41891 42919 44461 42405 43433 44718 44975 41891 44975 41120 39578 39835 41891 39835 41891 38550 21845 11565 22102 26985 36237 38550 37265 39064 38293 42662 48830 44718 44204 43947 37522 34695 35466 34952 35209 35980 36237 35723 35723 35980 35466 35980 35980 35980 35723 35723 35723 35980 35723 35466 35466 35209 35209 35209 35209 35209 35466 35209 34952 34952 35209 35209 35209 34952 34952 34952 34952 34952 35209 35466 35466 35723 42405 39835 35209 7710 4883 3855 4112 2827 3855 6168 5654 5911 5911 4112 4883 5911 5140 5140 5654 4112 4626 3855 5397 13878 33924 40092 44975 44461 44461 45232 45489 45489 44975 42405 39578 49858 47802 41377 27499 27499 42405 48059 47288 48573 31097 34438 40349 37779 44461 38293 39835 29298 17476 24929 38036 41634 43176 46517 44975 46774 45232 47802 51143 49344 49344 46517 46517 41120 42662 43690 42662 40092 37522 21845 18761 26214 33410 37008 39321 42662 39064 37779 49087 47288 40092 39578 37779 34952 34952 34695 34952 35980 35980 35723 35466 35723 35723 35980 35980 35980 35723 35723 35723 35980 35466 35466 35209 35209 35209 35209 35466 35466 35466 35209 35209 35209 35466 35466 35209 35209 34952 34952 34952 35209 35209 35466 35466 35466 51914 42148 26471 7196 4112 6682 2570 3855 10794 19275 8995 6425 8224 2827 5397 6168 5140 4112 5654 5140 4626 4112 11051 28527 40606 41377 44718 47288 43176 43176 47545 46517 45232 42662 39578 49858 49087 40606 25443 22102 28013 39578 43176 31354 8481 23901 36237 43176 43433 41120 39578 27242 19532 34695 35209 43690 43176 46003 43690 48316 44975 47031 51143 49601 49344 48316 47031 46260 46517 44204 42919 40349 39321 33924 20046 34438 38550 41377 42148 38807 38807 25186 30326 39064 34952 41377 39578 34695 34952 34438 34695 35209 35466 34952 35209 35466 35723 35723 35980 35980 35723 35723 35723 35980 35723 35723 35466 35466 35466 35466 35723 35723 35209 34952 34952 34952 35209 35209 34952 34695 34952 34952 34952 34952 35209 35466 35209 35466 47545 43947 12850 5911 6939 3341 3084 15677 32125 22616 7196 3084 6682 5140 4369 3084 4883 4883 5911 4626 5654 6682 12850 30069 35209 44204 44461 45489 43690 43690 49087 43690 43947 43176 40092 48573 49601 40863 27242 20560 25443 29555 29812 15420 6425 21588 34181 40349 40863 44461 41377 31868 27756 36494 37008 40349 38293 34952 32639 34181 32639 35209 42662 44718 40092 36751 33153 36237 36751 35723 37265 41120 37265 39578 27499 38036 42919 43433 43176 39064 34438 19532 19789 27756 46260 41891 36494 34438 35209 34952 34695 35209 35209 34952 35209 35723 35209 35723 35980 35723 35723 35466 35723 35980 35980 35980 35723 35723 35723 35723 35980 35980 35209 34952 34952 34952 34952 34952 34952 34695 34952 34952 34952 34952 35209 35466 35466 35723 50372 43947 16962 6168 3855 5140 3084 19018 29041 6425 7196 6939 3855 6168 3084 6168 3855 5140 6168 2570 4883 8224 10280 20817 32382 42148 44975 45489 44718 43433 45746 46517 43176 43176 40092 47288 49601 41377 29555 20560 21588 26728 37008 27242 11308 11565 35209 40863 41377 43433 48316 36494 33924 35209 37522 33924 23644 17476 19532 19789 20046 19789 24158 26214 20303 20046 20560 19789 17219 19275 19018 25700 30069 36237 40349 42919 48573 42662 38807 39835 38293 17476 23130 36751 39321 35980 38036 34695 35723 35209 35466 35466 35466 35209 35466 35980 35209 35723 35980 35723 35723 35466 35723 35980 36237 36237 35980 35723 35723 35980 35980 35980 35466 35209 35209 35209 35209 35209 35209 34952 34952 34952 34952 34952 35209 35466 35466 35723 57311 51143 34695 6939 5140 4369 2313 18761 13364 8481 6168 5654 7196 3341 3855 5397 4883 5654 5654 3598 3855 9252 12850 11822 30840 38550 45489 45746 46260 43433 46517 46003 43690 43176 39835 47288 47031 34695 28784 21074 17219 15677 13878 15420 11308 10280 29812 37522 40863 43947 46774 46517 33667 32125 25443 20560 17219 23387 25957 27242 30326 28784 23901 22359 25443 33667 41634 39835 37265 40092 39321 36494 33410 37779 38807 47545 45489 44461 40606 40349 33667 20817 24929 35466 42662 42919 35466 36494 35209 35466 35466 35980 35980 35723 35723 35723 35980 35723 35723 35723 35723 35723 35723 35723 36751 36237 35723 35466 35723 35723 35466 35209 35723 35723 35723 35723 35209 34952 34952 34695 34438 34695 34952 35209 35209 35209 35723 35980 38550 43176 44204 16448 2827 5911 514 25443 15420 4369 6682 5397 5140 5140 4369 5397 5140 5397 5397 4112 4626 8995 12079 11565 24158 39578 44975 45232 45489 44204 46517 46003 43947 42405 40092 48059 44204 30069 28270 28270 15677 7967 3855 8224 10794 11565 26985 37265 39321 39064 45232 46774 40349 34952 36237 35209 37265 37779 38807 42405 45489 43947 43176 45746 45746 42405 44204 45489 43947 41120 38550 38807 41377 35466 44204 46003 44975 40349 42919 39321 30583 21074 27499 36494 44718 41891 36494 35209 35723 35723 35723 35980 35723 35723 35723 35723 36237 35980 35723 35723 35723 35723 35723 35723 35980 35723 35723 35466 35209 35209 35466 35723 35723 35723 35723 35723 35209 34952 34952 34952 34695 34952 35209 35466 35466 35466 35723 35980 20303 3084 23387 29555 6425 3084 6168 38293 24415 2827 5654 4883 3598 5911 3598 5397 5397 4883 4626 4369 5140 8738 11565 11051 16448 37779 45232 45746 45232 44461 46003 44975 43176 42405 40349 47288 42148 28013 29555 33153 29812 16448 8481 8481 10537 9766 19789 33667 36751 39578 46003 43433 40606 34695 41634 38036 37265 39064 39321 38807 42405 46517 45746 42405 43176 41120 41634 39321 35980 35980 38807 43433 42662 35466 42919 47288 40349 40349 39578 36751 24158 22616 31097 37265 41891 36494 36237 36237 36237 35980 35980 35723 35723 35980 36237 35980 36237 35980 35980 35723 35723 35723 35723 35723 35209 35466 35723 35466 34952 34695 35209 35723 35723 35723 35723 35723 35209 35209 34952 34952 35209 35209 35466 35466 35466 35466 35723 35980 7196 5654 4626 7196 4626 4626 3598 29298 33667 7453 5654 5397 4883 5397 2570 5911 5140 4883 4369 3341 4369 8738 11822 10537 11565 30840 45746 46260 45232 44975 45746 44461 42919 42919 39835 46003 42148 30583 31611 32125 37522 36237 30840 15934 7453 6939 14392 30583 37008 37522 41377 45232 42148 33410 40349 41377 37779 35723 34952 34695 32896 31097 29555 27499 25443 26728 30069 31354 34952 41634 42919 41120 40092 35980 41377 40349 40606 36494 37265 32382 22616 25700 31354 35723 37265 33153 36494 36751 36751 36237 36237 35980 35980 35980 36237 36237 36494 36237 36237 35980 35980 35723 35723 35723 35466 35466 35466 35466 34952 34952 35209 35466 35723 35723 35723 35723 35466 35209 35209 34952 35209 35209 35466 35466 35209 35209 35466 35723 4883 6939 4626 6168 4626 3855 2313 6168 28013 11051 6425 5397 6939 4626 3598 6682 4883 5140 4883 3341 4369 10023 12593 10537 11308 21331 42662 46003 44975 44975 45746 43690 42662 42148 38807 45489 43176 32382 32125 30840 35723 33410 37779 38036 29812 13878 7196 24929 33667 36751 36494 42662 37779 36237 39321 40092 42662 40863 39321 35466 29812 25957 25957 26471 26985 24415 26471 29555 34181 38807 38036 35980 40863 39835 38807 38550 37779 34952 36751 26985 26214 30326 29041 34695 35723 36494 37008 35723 36494 36494 36494 36494 36494 36237 36494 36237 36751 36494 36237 36237 35980 35980 35980 35980 36494 35980 35466 35466 35466 35466 35466 35209 35723 35723 35723 35723 35466 35209 35209 34952 34952 34952 35209 35209 34952 34952 34952 35209 5911 4369 5911 4626 2827 4626 4883 4626 10280 8481 6168 4112 6425 4369 5140 5397 4883 4883 4883 4626 6425 11308 13107 10537 12336 14392 34181 43690 44204 44461 45746 42662 43433 41634 38036 45746 43176 30840 31097 31868 36237 33667 34181 34438 33410 25700 14392 22359 31097 38036 35209 36494 32639 40349 41377 37008 35209 37779 37522 36237 38807 43176 44718 42919 41634 39064 39321 37008 34181 34695 37008 40606 41634 41120 37779 41891 34181 35723 36237 21074 28013 33153 30840 35466 35209 37265 36494 35723 36494 36751 37008 37008 37008 36751 36494 36237 37008 36494 36494 36237 36237 36237 36237 36237 37008 36237 35723 35466 35980 35980 35723 35209 35980 35980 35980 35723 35466 35209 34952 34952 34695 34952 34952 34952 34695 34695 34952 35209 6939 3084 5140 7453 5911 2827 514 3341 1799 4883 5911 3341 4883 4112 4883 3855 4626 3855 5397 7710 10023 12336 12336 11051 12336 12850 22359 40092 42662 43690 45232 42662 43176 42148 39064 45489 42148 30583 31868 32639 32896 32382 33153 33153 31097 24672 14649 21845 35209 33410 33153 37008 38807 40092 41377 43176 42919 40863 39835 43947 49858 49858 48316 48830 49087 47545 48830 48059 46774 46774 46260 46003 40606 37008 39064 35466 37008 32896 37779 22359 27756 34438 34952 36751 35209 36494 35466 37008 36751 37008 36751 36751 36751 37008 36751 37008 36494 36494 36494 36494 36237 36494 36494 36494 36751 36494 35980 35980 35980 35980 35980 35980 36237 36237 35980 35980 35466 35209 34952 34695 34695 34952 35209 35209 34952 34952 35209 35466 4369 6682 4883 5654 2056 3084 4112 3598 5140 4112 5911 4112 4883 4112 5140 4112 4626 2827 5397 10794 13107 12850 11822 11565 11565 13621 13621 37265 41377 43690 44975 42148 41891 43947 40863 44718 41120 32382 33410 31354 31868 33924 33924 33410 33667 30840 18247 17990 37265 31611 34181 35209 40863 37265 38807 40606 37265 41634 41634 42405 47802 50372 50115 50886 49858 46260 47288 49344 49344 47802 44975 45489 38807 37008 34438 29298 35209 34695 37008 32125 29812 34952 36751 35723 36494 37008 36494 37265 37265 36751 36751 37008 36751 37008 37265 37522 36494 36494 36494 36494 36494 36494 36494 36751 35980 36237 36494 36237 35723 35723 35980 36494 36494 36237 36237 35980 35466 34952 34695 34695 34952 35209 35209 35466 35209 35209 35466 35723 4626 5397 5654 4883 3855 3084 2827 3341 5140 4626 4369 4626 4626 4112 4112 4626 3598 6425 10794 13107 13107 11822 11308 11308 11565 13107 14649 23644 40863 41120 42405 38293 40092 48316 37779 39835 38807 31097 35980 31868 32382 33667 33153 34952 32639 34181 15677 9766 34695 37522 31097 33667 35723 37522 34181 38807 40349 41377 40606 39578 43433 43690 47545 48316 44204 44718 47288 49858 49601 45746 41891 40863 34181 31097 27756 32639 32382 31097 35466 32896 33410 35209 36494 37265 37265 36751 37522 36751 37522 36494 35980 36494 36751 36751 36751 37522 37008 36751 36751 36751 36751 36751 36751 36751 36237 36237 36237 36237 36237 36237 36237 36237 36751 36494 36494 36237 35980 35723 35723 35466 35466 35466 35466 35466 35466 35466 35466 35466 4883 5397 5654 5140 3855 3084 3084 3598 3598 6168 6425 3855 3341 4369 4883 4883 6425 8995 12079 13621 13107 12336 11565 11051 11565 12850 14906 15677 34438 41120 43690 34952 39064 50115 38036 35209 33667 30326 35723 31097 36237 29298 32639 37008 33667 33153 16448 19789 42919 36751 28270 29298 33153 35466 31354 35723 40092 39835 37779 37008 40863 40606 42405 41377 38550 41891 43176 42405 42405 42148 39064 34695 30326 21588 24415 20303 24415 37522 34695 12593 36751 33410 34695 35466 37265 37779 35466 36751 36237 35723 36237 36494 37008 36494 37265 37779 36751 36751 36751 36751 36751 36751 36751 36751 36494 36494 36237 36237 36237 36237 36237 36494 36751 36494 36494 36237 35723 35723 35466 35466 35466 35466 35466 35466 35466 35466 35466 35466 4883 5397 5397 5140 3855 3341 3598 3598 4369 7196 5911 3855 4883 5654 5140 5397 10280 11308 13364 14135 13621 12593 11565 11051 11822 13878 15163 11565 24929 40606 42919 35723 37779 50372 37265 32125 30069 28527 34695 31354 34438 33667 33410 39578 31097 23387 7196 44975 44204 39321 31868 22873 20560 23901 24158 28270 35980 38036 37779 37008 37522 34695 34438 32639 33924 37522 38293 36494 35466 35723 32125 26471 20303 16962 13621 17476 32639 42405 32125 4112 14649 31097 36237 36237 36751 37265 38807 36237 35980 36237 36494 36751 36494 36237 36494 37522 36751 36751 36751 36751 36751 36751 36751 36751 37008 36751 36494 36237 36237 36237 36494 36494 36751 36751 36494 36237 35723 35466 35466 35209 35466 35466 35466 35466 35466 35466 35466 35466 4883 5397 5397 5140 3855 3341 3598 4112 4112 5397 4369 3855 4883 4369 5140 8481 12593 13107 13621 14135 13621 12850 11822 11051 12079 14135 13878 13107 16705 37522 40092 38550 37522 49087 36751 32125 30069 26985 32896 33410 36751 30583 34952 36494 28013 5911 8738 59110 37779 40606 37008 24672 13621 10280 13621 17733 25443 30326 31868 30326 27242 23130 23901 25443 28013 27756 26728 25957 25700 24672 22616 19789 14135 10023 13364 31097 37779 41120 32125 2827 5654 9252 27499 36494 35209 36494 36494 38036 36751 37008 37008 37008 37008 36751 36751 36751 36751 36751 36751 36751 36751 36751 36751 36751 37265 37008 36494 36237 35980 36237 36237 36494 36751 36751 36494 36237 35723 35466 35466 35209 35723 35723 35723 35723 35723 35723 35723 35723 5140 5397 5397 4626 3855 3598 3598 4369 4112 5654 5140 4369 4369 5397 8995 14392 13364 13364 13878 13878 13621 12850 11822 11051 11308 13107 12850 13107 12850 32382 38807 36751 39064 49087 38550 31868 29812 25957 33667 33410 35466 29812 35980 31868 5397 2570 29812 50629 30326 39321 36751 33153 22359 8995 10023 11565 15677 18761 16962 14392 11308 10537 13364 16191 16962 13878 10794 10280 12850 15420 17476 19018 15934 14906 29555 37779 38807 41634 28784 3084 4883 3598 5140 23644 38036 37779 37008 34695 37265 37008 37008 37265 37779 37779 37522 37265 37008 37008 37008 37008 37008 37008 37008 37008 37265 37008 36751 36237 36237 36237 36237 36237 36751 36751 36494 36494 35980 35723 35466 35466 35723 35723 35723 35723 35723 35723 35723 35723 5140 5654 5654 4883 3598 3084 3598 4369 4883 5397 5140 4112 5397 10023 14135 14649 12850 13107 13621 13878 13107 12593 11822 10794 11308 12336 13364 11051 13364 26214 40092 31868 41120 50372 41891 29041 26985 25443 34695 29555 32639 31868 25957 6682 3084 2313 41634 44975 22873 40349 36751 36494 30840 18247 17476 16191 15420 14649 7967 6425 5397 5911 5911 6168 6168 5911 4626 5654 11051 18504 22102 22102 19275 27499 37265 35723 40863 41120 30069 4369 2313 3855 3855 4626 21331 37008 37779 40092 37522 37779 38036 37522 37522 37779 37008 36494 37265 37008 37008 37008 37008 37008 37008 37008 37265 37265 37008 36751 36494 36237 36237 36237 36751 36751 36751 36494 36237 35980 35980 35723 35980 35980 35980 35980 35980 35980 35980 35980 5140 5397 5140 4626 3598 3084 3341 4112 4626 4369 6425 8224 8738 12593 14392 11308 12079 13107 13878 14135 13621 12336 11308 10794 11051 12593 13364 12079 14135 19789 40349 28527 42405 50629 44204 26471 24929 23644 33924 23644 30583 12593 3341 4883 3598 3341 48830 46260 16191 42148 38293 34695 32125 26214 23901 23901 22102 20303 12336 10023 7710 6939 4883 3341 3855 6168 8738 11822 18247 25957 27242 24672 24158 30326 34181 37779 37265 42405 35723 1799 5397 2313 3341 3855 1542 13364 33410 37522 36237 37522 38550 38293 37522 37265 36494 35980 37008 37008 37008 37008 37008 37008 37008 37008 37522 37522 37522 37265 37008 36751 36237 36237 36751 36751 36751 36751 36237 36237 36237 36237 35980 35980 35980 35980 35980 35980 35980 35980 5654 5654 5140 4369 3084 2570 3341 4112 4883 7196 15163 19018 14392 13107 14906 13878 12079 12850 14392 14649 13621 12079 10794 10280 10280 13364 11565 14906 12336 14649 38293 28013 42662 49344 44461 25957 24415 21588 32125 19532 4883 3855 2827 2570 4369 1542 51657 53713 13878 41120 38550 35209 34181 29298 22873 27499 26471 26471 19018 14649 8481 6939 6168 7453 6682 8995 11822 15677 22102 27242 26728 23644 26985 35980 34695 37779 38293 53713 31611 3341 3084 4626 3855 3341 3855 2827 6682 17990 32896 35723 38293 38293 37522 37522 37522 37779 37008 37008 37008 37008 37265 37008 37265 37008 38036 37779 37779 37779 37522 37008 36494 36237 36494 36494 36751 36751 36494 36494 36494 36494 36237 36237 36237 36237 35980 35980 35980 36237 5397 6168 6425 2313 4112 3084 3341 3084 7453 28270 43433 35723 13621 15420 14649 12593 13107 13878 14649 14649 13364 12079 10537 10280 11308 12593 11565 12079 14392 14392 35466 28784 40863 48830 42405 25957 16705 10280 4369 3855 3084 3598 2827 2313 3598 4112 53713 56797 17219 35723 39321 35466 33924 28784 26471 26985 27756 26471 21845 17219 15163 13364 10537 8995 9509 11051 14135 17733 25700 28784 23901 24672 30326 36751 33410 39321 46517 58082 30326 3598 3084 3341 3855 3598 3598 3341 3855 3855 2570 10280 24158 36494 39835 37265 36237 38807 37522 37008 36237 37265 37779 37522 36751 36237 37265 37779 37779 37522 36751 36751 37008 37008 37008 36751 36751 36751 36751 36751 36751 37008 35980 35723 35723 35723 35980 36237 36237 36237 5397 4626 5397 3855 5397 3598 2313 10023 34438 45489 43176 34695 23130 14649 14649 12850 10794 12079 13878 14649 14392 12850 11308 10280 9252 12850 13364 12336 11565 12593 33667 32382 31354 29555 17733 5397 2827 2827 1799 3084 2827 3341 3341 3084 4626 4112 53456 57568 17219 30583 41634 34695 31354 31097 29041 25443 27756 25700 21074 19532 17219 15163 13364 14906 17476 17219 19275 23644 28013 26728 22873 27499 33924 33667 38036 39578 59110 59110 25186 3341 3084 3341 3341 3341 3084 3084 3598 3341 4112 5140 4369 6939 19789 35209 39835 35466 37522 37522 37265 37008 36751 37008 38036 39321 38036 38036 38036 37779 37522 36751 36751 36751 37008 36751 37265 37265 37265 37265 36751 37008 36494 36494 36237 35980 35980 36237 35980 36237 4883 3855 5911 4626 3598 2570 5397 28013 44204 48830 41634 35723 36494 19789 14135 13878 12336 13364 14135 14135 13107 11565 10023 8995 11565 12079 11308 12850 14906 14392 19275 12593 3855 4369 2570 1799 3341 4626 3598 3341 3598 3598 3341 3598 4626 2827 52171 58596 10794 24672 38550 40092 34181 30326 26728 29555 28013 24929 20560 21588 18761 16705 15934 21074 21845 20046 21588 25957 28784 26471 24672 30326 32382 35723 38807 49344 60909 58596 21331 3084 2570 2827 3084 3084 2827 2827 3084 3341 4112 2056 3855 7196 4883 3598 15420 31868 38036 37265 36751 38293 39064 38550 37265 36494 37522 36751 36751 37522 37522 37265 37522 38293 36494 36751 37265 37522 37522 37265 37008 36494 37008 37008 36494 35980 35980 36494 35980 35723 5911 4369 5397 4369 2313 3341 7967 42405 45232 45489 41634 35980 43433 30583 18247 15677 12079 12850 13621 14135 13364 12336 10794 10023 8481 12336 13878 11565 6682 4369 4626 3598 3341 3598 3598 2827 1799 2313 3341 3855 3855 3341 3084 3341 4112 2313 49344 60395 16191 19018 39835 39321 33410 33667 30326 30583 29041 26214 21588 22873 20046 18761 18761 25186 24929 23901 23901 27242 30069 29555 29041 32382 33667 35466 42405 59881 57311 58339 14392 3341 2570 2827 3084 3084 2570 2570 2827 3084 4369 5140 5140 4369 4112 5140 5911 5911 12079 25957 37008 36751 34952 37265 39064 37522 39321 38036 38036 38550 37779 36237 35980 36751 36751 36751 37008 37522 37265 37008 36751 36494 37008 36494 36237 35980 35980 36237 35980 35980 6425 4369 3855 4112 3855 5654 7453 42662 46517 47031 43176 38807 46003 41377 27499 13107 12593 13364 14135 14135 12593 10794 7967 6682 7453 6425 4883 3598 3598 4626 2570 1799 2313 2570 3855 4369 3855 4369 4369 2570 3341 2570 2827 4112 4369 3084 43690 60138 47802 12079 32639 39835 35723 31354 31354 32639 31097 28784 24672 24158 22102 21588 22873 27756 27756 26214 26214 28784 30069 28784 29812 32896 36751 31868 52685 60909 59624 57568 4883 4369 3084 3341 3084 3084 2570 2570 3084 3341 4112 2827 3598 5654 6425 5397 4112 4626 6168 2313 7196 23901 37008 39578 37779 37522 37008 36751 37522 38807 38293 37265 36751 37522 37008 37008 37008 36751 36751 36751 36751 37008 36494 36494 35723 35723 36237 36237 36237 36237 4883 4883 4626 4112 4112 5140 7710 42148 43433 48573 43433 43433 47031 48573 43947 19018 13621 12850 10794 8738 6939 5140 3341 2313 2827 2313 2827 2570 2827 3855 2827 3341 3855 3598 3598 2827 2056 3341 4112 2056 2570 2827 3341 4369 4112 2313 32639 55769 61166 31868 22359 36237 34695 32125 33410 30326 32382 29555 26471 23901 24158 23387 25700 29298 26728 25186 26214 31354 30840 26214 29298 34438 30069 41377 59367 58339 60909 48316 2056 4369 3341 3084 3084 2827 2570 2570 3084 3341 3598 4369 5140 5140 4626 4369 5140 6425 4883 5911 5654 4369 6682 15677 27499 35980 38293 38550 38807 38293 37008 36494 37522 38036 37522 37265 37265 36751 36751 36494 36494 36751 36237 35980 35980 35723 35980 36237 35980 35980 4112 5140 5140 3598 3084 2827 9252 43947 45232 48573 45232 44718 47545 51914 52685 25443 7967 6939 4883 3598 2827 3341 3598 3598 4626 2827 3084 2313 1285 1542 3341 3341 771 2570 4369 4369 3855 3855 3855 3855 3084 3341 2827 3855 2827 2313 24415 53199 61166 54227 18247 30840 37008 31097 30069 32639 32896 29041 26471 23387 25700 23644 26728 29555 26985 26471 27499 32896 31611 27242 29812 33410 31611 56026 59367 59110 59110 30840 4112 3598 3341 3084 3341 3084 2570 2570 3084 3341 3341 3598 4626 5911 5911 5140 4883 5140 4626 5397 5397 5140 4369 4883 6168 7196 12593 19532 28784 34952 38036 38807 38036 36751 37522 37265 37522 37265 36751 36494 36494 36237 36751 36237 35723 35466 35980 35980 35723 35723 4883 4626 4112 3341 4369 2827 9509 41634 47802 45489 48059 45746 49858 51914 43176 13107 3855 3598 3341 3855 4112 3855 3341 2570 4112 2313 3598 4112 4626 3084 5140 2056 4112 3855 2570 2570 3598 2827 2313 3341 3341 3341 2827 3341 3084 5140 22873 56283 59624 62194 30840 17990 34695 36494 31868 30840 33667 28527 26471 23387 27499 23901 27242 29812 28270 28784 28270 31097 29555 25957 27242 25700 47288 59881 59881 59624 59881 17219 2827 3598 3341 3341 3084 2827 2313 2313 2570 3084 3598 5140 5397 4369 4883 6425 6168 4369 5911 5654 4883 4626 5911 6682 5911 4626 5911 6682 6425 6168 10537 20817 32382 38807 37522 37522 37779 37522 37522 37008 36237 35723 37008 36751 35980 35723 35980 35723 35723 35466 5654 4112 4883 3855 4112 2827 8995 40863 47288 47545 47802 45746 42148 33667 7967 4369 3598 3598 3341 3598 3341 3341 3341 3084 3341 3341 3598 3598 3341 3084 3084 3084 4112 3598 3341 3084 3084 3084 3084 2570 4369 3341 3341 2827 3855 2827 19789 57311 57311 63222 57311 16705 27499 34181 34952 30326 32382 32125 26214 24672 24158 18247 28784 26985 26471 24929 29041 31611 26985 25700 19275 38807 58339 62451 59881 59110 57825 5911 3855 2570 2827 2827 3084 3084 2570 2570 2827 3341 3084 3598 4369 5140 5397 5654 5911 5911 5654 5654 5911 5911 5654 5397 5654 5397 6168 6168 6682 6168 4626 4112 6682 10280 26728 37779 37779 36494 36751 38036 35466 37265 36237 36237 36237 36237 35723 35980 35980 35723 4883 4883 4883 4883 4626 2313 6682 41377 44975 49858 41634 7453 4626 4883 514 5397 3598 3598 3855 3855 3598 3598 3341 3341 3598 3341 3598 3598 3341 3084 3341 3084 3084 3084 2827 2827 3598 3341 3341 3341 2827 2570 4112 4112 4112 3855 14392 58853 57825 61166 62965 53970 23387 31868 32639 33924 32639 34181 27242 23130 25700 17476 21588 22873 22102 24415 24929 33153 27242 17733 30840 58596 61680 61166 63222 59110 38036 3855 2313 3341 2827 3084 3341 3084 2570 2570 3341 3598 3855 4369 4626 5140 5140 5397 5397 5654 5654 6168 6168 5911 5654 5911 5911 5911 6939 5911 5397 6425 6168 5397 5654 6168 6168 8995 26728 38293 36751 36237 37008 35466 36494 36237 36494 36237 35980 35466 35723 35723 4369 5397 3598 4626 4883 3084 5397 42919 50372 44461 7710 5397 2827 3598 4883 3598 3855 3855 4112 4112 3598 3598 3855 3598 3341 3598 3598 3341 3598 3084 3084 2827 2827 3084 3084 2827 3341 3598 3598 3598 3855 3855 4112 4369 2827 4369 8481 57311 60652 61166 65021 61937 56540 26728 34695 36237 34952 33667 28784 25186 25700 17219 21845 25443 21588 19275 30326 29812 23387 25700 58853 60652 64507 61423 62451 58596 15677 2313 2313 3341 3084 3341 3341 3341 2827 2827 3341 3598 4369 4626 4369 4883 4883 5140 5397 5654 5397 5654 5911 5911 5654 5654 5654 5911 6939 5397 5140 6425 6939 5911 4883 4369 4883 5654 7967 16191 34181 37779 36751 36494 36237 36494 36494 36237 35723 35209 35723 35723 5140 5397 3084 4112 4369 4112 4369 42148 47288 20046 3341 5911 1799 3341 3084 3084 3855 4112 3855 4112 3855 3855 3598 3598 3084 3084 3341 3598 3084 3084 3341 2827 3341 3084 3084 3341 3341 3341 3084 3084 3855 3855 3855 3855 2570 4112 5140 42919 57568 63736 63222 65021 63993 50886 25443 37008 41634 35723 31868 29298 24158 16962 25700 23901 21588 23130 33153 30069 21588 58082 61937 64250 64764 63736 60652 51143 4883 2570 3084 3084 3598 3341 3598 3341 3341 3341 3855 4112 3598 4112 4626 4883 5140 5397 5654 5140 5397 5654 5654 5654 5397 5397 5654 5911 6682 6168 6168 5911 5654 5140 5397 6425 6939 5140 4626 5911 5397 12593 32125 37008 35980 36237 36237 35723 35723 35209 35466 35466 5654 5140 3855 4112 3341 3855 3598 40606 44204 3855 7710 1285 2056 6168 3855 4112 3855 3855 4369 4369 4112 4112 3855 3598 3084 3084 3341 3084 3341 3341 2827 3084 3341 3341 3341 3084 3341 3341 3341 3341 2313 3084 4112 4112 4369 3598 3598 20303 56283 58339 65278 64764 63993 63736 42919 27756 39321 38550 37008 33924 27756 22359 31354 23644 21588 34952 31354 21588 58596 64250 63736 65021 64250 65278 62451 32382 4112 3855 3855 3598 3855 3598 3341 3598 3341 3598 4112 4112 3598 4112 4883 5140 5397 5397 5397 4883 5140 5397 5654 5654 5654 5397 5397 5654 5654 5911 5911 5654 5397 5140 5911 6939 6425 7710 4883 4883 4883 5397 15163 37779 35980 36237 36237 35980 35723 34952 34952 35466 10794 4369 4626 5397 2827 3598 4369 39064 23901 3341 1542 6168 4112 2570 3855 4883 3855 4112 3855 4112 3855 4112 3598 3598 3341 3341 3341 3084 3084 3341 3084 2827 2827 2827 3084 3084 3341 3341 3598 3855 3341 3598 3855 3598 4883 3855 5140 6425 47802 57311 61680 64507 65278 64507 63736 38293 31868 39835 43176 38807 35209 30326 36237 30069 32639 34181 26471 59110 64507 63222 65021 65021 64764 65021 60395 12079 5140 4369 3598 4369 4112 4112 3341 3598 3598 3855 4112 4112 4883 5397 5397 5911 5911 5397 5397 5140 5654 5654 5911 5911 5397 5654 5911 5911 5911 5397 5397 5654 5654 5397 5140 4883 4112 3855 7710 5911 4369 4626 4112 31097 36237 36494 36237 35980 35466 34952 34952 35466 26471 7453 4369 5140 3341 3341 4369 34695 11051 4626 3855 2056 4369 4626 4626 2313 3598 3855 3598 3855 3598 3598 3341 3341 3341 3341 3084 3084 3084 3084 2827 2827 2827 3341 3598 3341 3341 3084 3341 3341 4369 4369 3598 2827 4112 3598 5911 4369 31097 58082 61680 65278 64507 65278 63993 62451 36751 33924 39835 38293 37522 34438 35466 33410 34181 38293 58853 63479 63479 65278 65278 63993 65278 63993 43433 3598 4883 4369 4626 4369 4883 4369 3598 3598 4112 4112 4626 4626 5654 5654 5140 5140 5397 5140 5397 5140 5654 5911 5911 5654 5654 5654 5654 5911 5140 5140 5140 5140 5654 5397 4626 3855 6939 3855 7453 4112 3341 5911 4883 17733 36237 36237 36237 36237 35466 35209 35209 35209 43690 12850 3855 4369 3598 3341 3084 28784 3341 1799 4626 4883 3855 3598 4112 4626 3598 3598 3598 3855 3598 3598 3341 3341 3341 3341 3341 3341 3341 2827 3084 2827 3598 3598 3598 3341 2827 2827 2827 3084 2056 3084 3084 3084 3855 3341 4369 5911 20817 58853 58853 65278 65021 63993 65021 64250 60395 35980 41120 47545 52171 53456 52171 50115 48316 39835 61166 64764 65021 64507 64507 65278 65278 63479 23130 5140 4883 3598 5911 4112 4883 4369 3598 3855 4112 4369 4369 4626 5140 4883 4369 4369 4369 4883 5397 5654 5397 5654 5911 5397 5397 5140 5397 5397 4883 5397 5140 4883 4369 4626 4883 5397 4626 5654 4626 4626 7710 3084 5397 7710 36237 36237 36237 35980 35723 35466 34952 35209 47802 36237 4883 4369 3598 3598 5397 13621 3598 3855 4112 4369 4369 4112 3598 3598 4369 3855 3855 3598 3341 3341 3341 3598 3084 3084 3341 3341 3341 3341 2827 2570 3084 3341 3598 3598 3598 3341 3084 3084 3084 3084 3084 3598 3598 3855 4626 4883 9509 54998 59624 62965 64507 64764 65278 64764 63993 62708 61166 58853 59624 57054 47288 41120 12593 5911 15420 57568 64764 65278 65278 64507 63222 54741 5911 5654 5911 4883 3341 5140 4369 4626 4626 4626 4626 4626 4883 4883 5654 5654 5654 5397 5140 4883 4883 4883 5397 5397 5140 5397 5654 5654 5140 4626 4883 4626 4626 4626 4883 4626 4626 4369 3855 4626 4883 4883 4626 4112 3341 3598 32896 35466 35466 37008 35209 36494 34695 35980 47031 42919 18247 2827 3855 3341 4883 7710 3598 3598 3855 4112 4112 3855 3855 3598 3855 3855 3855 3598 3598 3598 3598 3598 3341 3341 3341 3341 3341 3341 3084 3084 3084 3341 3598 3855 3341 3084 3084 3084 3341 3341 3598 3341 3341 3341 3855 4112 6425 43690 60909 62194 62965 65021 63993 65021 63993 34952 20303 18761 12850 8738 7453 6168 6168 6425 3855 9766 52685 63993 65278 64507 65278 29812 5654 3855 4369 4883 4369 5140 4883 4883 4883 4883 5140 5397 5140 5140 5140 5140 5140 5397 5397 5397 5654 5654 5654 5397 5397 5654 5654 5654 5140 4883 5911 5654 5397 5140 4883 4626 4369 4112 3855 4112 4883 4883 4626 3855 3598 3598 21588 34695 37265 34952 35980 35723 35209 36751 45489 45746 34181 12079 3598 4626 6168 4112 3598 3341 3598 3855 3855 3598 3341 3598 3598 3855 3598 3598 3598 3341 3341 3341 3855 3855 3598 3341 3341 3341 3084 3084 2827 3084 3598 3598 3341 3084 3341 3341 3084 3341 3598 3341 3341 3341 3341 3598 5654 30326 57825 62451 65021 63479 65278 63993 28527 5911 1799 7453 4626 4369 6168 4626 4112 5140 7967 4626 11051 55512 65021 62708 57825 10023 6168 4626 4369 5654 5140 5140 5140 5140 5140 5140 5397 5397 5140 5140 5397 5397 5654 5654 5654 5654 5911 5654 6168 5911 5911 6168 6168 6168 5911 5654 6425 6168 5654 5140 4883 4369 3855 3598 3855 4369 4626 4626 4369 3855 3855 4112 10794 37522 37522 34952 36237 34952 36237 35980 45232 43176 44204 30583 4626 5654 5654 4112 3598 3341 3598 3855 3855 3855 3341 3598 3598 3855 3855 3855 3598 3598 3598 3341 4112 4112 3855 3341 3341 3341 3341 3341 3084 3341 3598 3598 3084 3084 3084 3341 2570 2827 3341 3341 3341 3341 3598 3598 4626 21074 57568 64507 65021 64250 63736 26728 4883 3855 6682 6939 5911 6939 5140 3855 6939 3598 2570 2827 7710 12336 58339 63993 34952 6168 5397 6168 5140 5397 5140 4883 5140 5140 5140 5140 5397 5140 4883 4883 6168 6168 6168 5911 5654 5397 5397 5397 5911 5911 5911 6168 6168 6168 5911 5654 5654 5397 5140 4883 4626 4369 4112 3855 4112 4369 4369 4369 4112 3598 3855 3855 5654 35980 34695 36751 35723 35209 37522 34952 46517 41891 47288 40863 11565 5140 3598 4369 3855 3855 3598 3855 3855 3855 3598 3855 3598 3855 3855 3855 3855 3855 3598 3598 4369 4112 3855 3598 3341 3341 3341 3341 3341 3341 3598 3598 3084 2827 2827 3084 2570 2827 3084 3341 3341 3341 3598 3598 5140 9252 51657 60909 63736 65278 25443 3341 4112 5911 5397 2827 4112 5397 4112 6939 5140 5140 5911 5397 4883 3855 21331 58853 12336 5397 4626 5397 4883 4369 5397 4369 5397 5397 5397 5140 5397 5397 5140 5140 6425 6425 6168 6168 5911 5397 5397 5140 5140 5397 5397 5397 5397 5397 5397 5397 5397 5140 4883 4626 4883 4626 4369 4112 4369 4112 4369 4112 3855 3341 3598 3598 3598 23130 35466 36751 34952 35980 37265 35723 46003 42662 44975 38550 21845 5911 2056 4112 3855 3855 3598 3598 3855 3855 3598 3855 3341 3855 3855 4112 4112 3855 3855 3598 4112 3855 3855 3598 3341 3341 3341 3341 3341 3598 3598 3341 2827 2570 2827 2827 2827 3084 3341 3341 3084 3084 3341 3341 3598 6939 32125 61680 65278 24929 5140 5654 4883 2827 3855 3855 3598 4369 3855 4883 4112 4369 3598 4112 2570 8481 3341 17219 5911 3341 6939 4883 5140 4626 5911 4626 5654 5654 5397 5397 5654 5654 5397 5397 5911 5911 5911 6168 5911 5654 5397 5397 5140 5397 5654 5654 5397 5397 5654 5654 5654 5397 5140 5140 4883 4883 4626 4369 4112 4112 4369 4112 3855 3341 3598 3598 3855 8995 37522 35980 36237 37008 35209 37779 45232 42919 42919 37522 29555 5654 3855 2827 3855 3855 3598 3855 3598 3598 3598 3855 3341 3598 3855 3855 4112 3855 3598 3598 3598 3598 3598 3598 3598 3341 3341 3341 3084 3341 3341 3341 2827 2827 2827 3084 3084 3084 3341 3084 3084 3084 3598 3855 2827 7196 17219 61166 30069 4626 2827 2827 3855 2570 5397 4883 2313 3855 4369 3341 4626 5140 5654 4112 7710 2313 5140 6168 6939 3855 7967 5654 5140 5654 5397 4883 5397 5397 5397 5397 5654 5654 5397 5397 5654 5911 5911 5911 5654 5397 5140 4883 5397 5654 5911 5654 5397 5397 5911 6168 5654 5397 5140 4883 5140 4883 4626 4369 3598 3855 4369 3855 3855 3341 3598 3341 4369 3855 30583 36237 36751 36494 35209 38036 46003 42919 42662 41377 31097 4883 5654 1542 3598 3341 3341 3598 3341 3341 3341 3598 3084 3341 3598 3855 3855 3855 3598 3341 3341 3341 3598 3598 3598 3598 3341 3084 2827 3084 3341 3084 2827 2827 3084 3341 2570 2827 2827 3084 3084 3598 4112 4626 5397 2570 8224 26985 3855 4883 4626 2570 3598 3084 4369 3598 2827 3341 3084 4883 4112 4112 5397 4883 3084 7710 5140 5911 4626 5654 5140 5397 3598 5911 4112 5140 4883 4883 5140 5140 5397 5397 5397 5397 6168 6168 6168 5911 5654 4883 4626 4369 4369 4883 5140 5140 4626 4626 5140 5397 4883 4626 4626 4626 4883 4883 4883 4626 3341 3598 4112 4112 3855 3341 3341 3341 2570 4369 18247 36751 36237 35466 37522 36751 46260 42148 43690 39835 32382 7710 3084 3855 3341 3084 2827 3341 3084 3598 3855 4369 3341 3341 3341 3341 3341 3598 3598 3855 3855 3598 3341 3341 3598 3598 3341 3084 3341 3341 3084 3084 2827 2827 3084 3084 3598 3598 3341 3084 3084 3341 3598 4112 4112 4112 7196 3855 4112 6425 2570 4112 5911 4626 5140 4112 5911 2827 5397 10023 7453 8738 11308 5397 4883 4626 4883 5911 5397 5140 4883 5140 5654 5654 5397 5140 5397 5397 5397 5397 5654 5911 5654 5911 5911 5654 5397 5654 5911 5911 5911 5654 4883 4883 4626 4112 4112 4369 5140 5654 5397 4883 4626 4369 4369 4626 4626 4626 3598 3598 3598 3855 3855 3855 3855 4112 4369 3855 8738 37008 36751 35980 37522 36494 46774 43176 43433 39835 32639 6682 2570 3598 3341 3084 3341 3598 3341 3341 3341 3598 3341 3598 3598 3341 3598 3598 3855 3855 3598 3341 3341 3341 3598 3598 3341 3084 3341 3341 3341 3084 2827 3084 3084 3084 3341 3341 3341 3084 2827 2827 3341 3855 4369 4112 6425 4369 2827 3341 2056 4626 3084 3855 3341 32639 43433 50886 54998 52428 53970 51400 47288 40349 7196 4112 4883 4112 4883 4883 4626 4883 5397 5654 5397 5140 5140 4883 4626 4369 4626 4626 4883 4883 5140 5140 5140 5654 6168 6168 6168 5654 4883 4883 4626 4112 4112 4369 5140 5654 5654 5140 4626 4369 4369 4369 4112 4369 3855 4112 3598 3598 3341 3341 3341 3341 3084 2827 5654 31354 35980 36751 37008 37265 46260 43690 42405 39578 33153 5140 2570 3341 3341 3341 3598 3598 3341 3084 2827 2827 3341 3598 3598 3598 3598 3855 3855 3855 3598 3341 3084 3084 3341 3598 3598 3341 3598 3598 3341 3341 3084 3084 3341 3341 3341 3341 3341 3084 2827 2827 3341 3598 3084 4369 5397 4369 3855 5911 6425 4112 6425 5397 26471 52171 61423 64507 65278 65278 64507 64507 61937 57054 23130 6425 4369 3855 4369 4369 4626 4883 5140 5397 5654 5397 5140 4883 4369 3855 3855 4112 4112 4369 4626 4883 5140 5911 6168 6168 5911 5397 4883 4883 4626 4112 4112 4369 4883 5397 5654 5140 4626 4369 4112 4112 3855 3598 4369 4369 3855 3855 3598 3341 3084 3084 2570 2827 2570 22616 35466 37008 36494 37779 45489 43947 41634 39578 32382 3855 3341 3341 3341 3598 3341 3341 3084 3084 2827 2827 3598 3598 3598 3598 3598 3855 3855 3855 3341 3084 3084 3084 3341 3598 3598 3341 3598 3598 3598 3598 3341 3341 3341 3341 3598 3855 3598 3341 3084 3084 3341 3855 3855 4626 4112 4369 4112 5140 6682 3341 4369 7196 44718 56540 65278 65278 63479 65278 63993 63736 60652 56797 29555 6168 3855 5397 4369 4369 4626 4883 5140 5140 5654 5654 5397 5140 4626 4112 4369 4369 4369 4369 4626 5140 5397 5911 5911 5654 5140 4626 4626 4626 4369 4112 3855 3855 4369 4883 5140 4883 4369 4112 4112 4112 3855 3598 4369 3855 3855 3598 3341 3084 2827 2827 3341 3084 2313 14135 36751 37265 36494 37522 45489 44204 41634 39835 29298 3341 3855 3598 3598 3598 3084 2827 2827 3084 3341 3341 3341 3341 3341 3598 3598 3598 3598 3598 3598 3341 3084 3084 3341 3341 3598 3341 3598 3598 3598 3598 3341 3341 3341 3341 3598 3598 3598 3598 3084 3084 3341 3855 4369 3598 3341 5911 5654 4112 6168 7196 5397 26214 51657 63736 65278 65021 65278 64507 65021 64764 61166 57825 16191 4369 4626 4112 4369 4626 5140 5397 5140 5397 5397 5654 5140 5140 5140 5140 5397 5140 4883 4883 5397 5654 5911 5911 5397 4626 4112 3855 4626 4626 4369 3855 3598 3598 4112 4369 4883 4369 4112 4112 4112 4112 3855 3598 3598 3855 3341 3341 3084 3341 3084 3084 3855 3084 3598 7967 37008 37008 36751 37008 45746 43690 42148 40092 24415 3084 3598 3341 3341 3598 3084 2827 2827 3084 3598 3855 3341 3341 3341 3598 3598 3598 3598 3598 4369 3855 3341 3084 3341 3341 3341 3341 3598 3598 3598 3598 3341 3341 3341 3341 3084 3341 3341 3341 3084 3084 3084 3598 3084 4112 4369 4626 6682 6425 4883 4883 13364 41891 56797 65021 64764 63993 65278 65021 63736 61423 59110 43947 5397 4626 6682 3341 4883 5140 5654 5654 5397 5397 5397 5654 5140 5140 5397 5654 5911 5654 5397 5140 6168 6168 5911 5397 4626 3855 3598 3598 4626 4626 4369 4112 3598 3598 4112 4369 4369 4112 4112 4112 4112 4112 3855 3855 3598 3598 3341 3084 3084 3341 3084 3341 3598 2570 3855 4112 34438 37265 37265 37008 45489 42919 42405 39835 19532 3341 3084 3341 3341 3341 3598 3598 3084 3341 3598 3598 3341 3341 3598 3598 3855 3598 3598 3598 4883 4369 3855 3341 3341 3341 3084 3084 3341 3341 3598 3598 3341 3341 3084 3084 3084 3084 3341 3341 3084 3084 3341 3598 3598 5140 5397 3341 4883 4883 4112 10794 37779 52942 63736 64507 65278 65278 65278 64250 65278 60395 53713 19275 5140 5397 5911 5397 5397 5654 6168 5911 5397 5140 5140 5397 5654 5654 5654 5654 5911 5911 5654 5654 6168 6168 5654 4883 3855 3598 3855 4112 5140 4883 4883 4369 4112 3855 4112 4369 4369 4369 4112 3855 3855 3855 3598 3341 3341 3598 3341 3084 3084 3084 3341 3341 3341 3084 3341 3598 29298 37522 37265 37522 44975 41891 42405 39835 16191 3598 3598 3855 3598 3341 3598 3598 3598 3341 3084 2827 3341 3598 3598 3855 3855 3855 3855 3598 5140 4626 3855 3598 3341 3084 3084 2827 3341 3341 3598 3598 3341 3341 3084 3084 3084 3341 3598 3598 3341 3341 3598 3855 4369 2313 5140 6682 6939 3855 11822 37522 52685 59881 64764 65021 63736 65021 64764 65278 61937 52428 44718 5397 6425 7196 4112 6425 5654 6168 6425 6168 5397 5397 5397 5654 6425 6425 6168 5911 6168 5911 5911 6168 5911 5911 5397 4369 3598 3341 3855 4369 5397 5654 5140 4626 4626 4369 4626 4883 4883 4626 4369 3855 3855 3598 3341 3084 3341 3341 3341 3341 2827 3084 3084 2827 3855 3855 2827 4112 25443 38036 36751 37779 43176 42919 42405 40863 13107 3084 4112 2570 4112 3855 3341 3084 3084 3084 3084 3341 2827 2313 4883 1028 6168 3855 2570 4112 4112 3084 2827 4626 1799 3341 3341 2313 1799 5397 3598 2827 3341 3598 4626 3084 4112 4112 3855 3598 3341 3341 3598 3855 4883 2827 6425 5911 6168 5397 39321 49344 58596 64507 63993 65021 65021 65021 64507 63736 54227 48573 15677 6682 6939 5911 4626 6425 6939 6425 5911 5911 5140 4369 4369 5140 6168 3855 6168 5397 4883 6168 5140 4883 5654 4369 4883 2827 5654 3341 4883 4883 5140 3341 5140 3598 5140 5654 4883 4626 4883 4112 4369 4112 4112 4112 2827 3341 3855 3855 3855 3598 3341 3341 3341 3341 4369 3084 3341 4626 20560 37779 38807 37522 43690 42919 41891 40863 10023 2570 4112 3084 3855 3598 3341 3341 3084 3341 3084 3598 5654 2313 3084 6682 2827 2827 7967 3084 4369 4112 4112 5654 2570 3855 4112 4112 2570 1799 2827 3341 5654 5911 1028 6168 3855 3855 3855 3341 3084 3084 3341 3598 3598 4883 4626 4369 6168 8481 32639 50115 57054 65021 65021 64764 65021 65021 62451 55255 40606 29041 8995 5140 5654 6682 5140 5911 6425 6168 5397 5654 5397 4626 4626 5140 5911 4369 5654 7196 6682 5911 6682 6682 5397 5140 4112 3598 2056 3855 4883 4883 6425 3598 7196 2570 3598 3341 4369 4883 4369 3341 3341 3855 4369 4369 3855 4112 3598 3855 3598 3598 3084 3598 3341 3598 3855 3341 3855 4112 17219 38293 38807 38036 44718 41891 41120 40606 6425 2313 4112 3855 3598 3084 3084 3341 3084 3341 3084 3084 1799 3598 33667 35466 35980 31354 28527 23644 21588 18504 13621 10537 5140 4626 3341 2570 4883 3855 4626 3598 2056 5397 2313 3598 3855 3855 3598 3341 3084 2827 3084 3341 2827 6168 2827 4112 5140 7710 19532 47031 55769 62194 63993 65021 65021 63479 53713 38550 29041 12079 5397 5911 5654 7453 5654 5654 6168 5654 5654 5911 5911 5140 4883 5397 4883 6682 4883 5140 4883 5140 5140 3598 4369 5654 5654 6425 3084 5654 5397 4883 3084 5397 4626 4883 6168 5654 3341 3598 4626 4112 3855 4112 3855 2827 2570 3084 3084 3341 3341 3341 3084 3598 3341 3598 3855 4112 4112 4112 12850 39064 38550 39064 44461 41634 40863 39321 3855 2827 3855 3855 3084 3084 3341 3598 3341 3341 3084 3084 7453 2056 34181 47802 65021 65021 62965 37008 37779 39578 41377 45746 46774 47545 43433 40349 34438 32896 25700 19789 3855 771 6425 4369 3855 3855 3598 3341 3084 3084 3084 3341 3341 4883 2827 5654 5140 5140 7967 38807 49601 60395 64764 63993 61680 56026 40092 26214 21588 6425 6425 6939 6168 6425 5911 5397 5397 5654 5911 5911 5911 5397 5397 5397 3598 6939 5911 5654 6168 5654 6682 19275 14906 16448 17476 17476 17219 16962 16962 17476 18504 28784 5140 4369 3855 4626 2570 5140 3341 3598 3084 4626 4369 2827 3598 4112 2827 3084 3341 3084 3341 3598 3341 3855 3598 4369 4112 3341 8995 38550 38036 38550 43433 41377 41634 35723 3341 3341 3341 3341 2827 3084 3341 3598 3341 3341 3084 3084 2570 8224 36494 50372 63993 64507 53713 31354 37522 38807 38550 41120 42662 46517 46774 47545 45746 46003 43433 43947 24672 4626 1542 2313 3855 3855 3855 3598 3341 3341 3855 3855 4112 3341 4626 6425 6168 5911 4883 25700 46260 56026 62965 60395 54741 39578 27242 31097 12593 5140 6168 5911 6168 4112 5397 5654 5140 5654 5911 6168 5911 5654 5654 5140 7967 4626 3598 5140 7453 4112 4369 35980 21588 22873 21331 21331 23130 22873 22359 25443 22873 45489 7710 4369 4883 5140 4626 4626 5140 4626 2827 4369 4112 2056 3084 2570 3084 3341 3341 3341 3598 3598 3855 3855 3341 4112 3341 2827 6168 34952 37265 37779 43176 41377 42148 29298 3598 4112 3084 3341 3341 3084 3341 3341 3084 3341 3084 3084 1799 10794 43176 47802 54741 58082 51914 41377 36751 42405 44975 47031 46003 46774 45746 47031 46774 44461 43433 44975 39321 20046 4626 3598 3598 3855 4112 3855 3598 3598 4112 4369 4883 3341 5140 4626 5654 6939 5654 11051 45746 49858 57311 56283 43176 33410 35980 34952 5654 5140 5911 5140 6682 3855 5397 5397 5397 5911 5911 5911 5911 5911 5397 5140 4626 5397 7710 4369 5140 5654 5397 39321 21845 22359 22616 22616 23130 23130 23387 23901 25186 44975 10537 4883 4626 2570 5654 3341 2570 3855 3341 5911 5140 1799 3598 3598 3341 3341 3341 3598 3855 3855 4112 3855 4112 4112 4626 3855 5397 30583 37779 38036 42919 42405 42148 22102 4112 3855 2827 3341 3598 3598 3598 3341 3084 3341 3084 3341 5654 14906 43947 54227 52942 49601 57054 38293 35723 43690 48316 51400 51143 51400 49601 50886 50629 49087 48059 46517 48830 38807 20817 2056 3341 3855 4112 4112 3855 3855 4369 4626 4883 4369 4626 4626 5140 5654 6425 4369 34438 51143 55769 47288 34695 39835 49858 20303 4883 5397 5911 5140 5911 5397 5397 5397 5654 6425 6425 5911 5397 5654 5397 4883 6168 4883 4626 6939 23644 40092 26985 28270 25186 21331 30840 25186 25957 22359 29812 20817 32896 35980 23387 32382 26471 23644 19789 17733 19018 25700 28013 26985 16191 3855 3084 3855 3341 3855 3855 3855 4369 4369 3855 3855 4626 4112 5911 4112 4883 24672 38293 39321 42919 42662 42148 17476 4369 3598 2827 3855 3855 3598 3598 3341 3084 3084 3084 3341 3855 19532 33153 44718 57568 55769 38293 31354 34695 39578 40092 41377 42148 44204 43947 46003 45746 44718 43947 42919 43947 42148 39064 8481 3341 3598 3855 4112 3855 3855 4369 4626 3855 4626 3598 6682 5911 3855 7196 6425 18761 50372 48059 39835 35466 35723 40092 4112 5397 3598 5140 4112 4369 6425 5140 4883 6168 6682 6425 5654 5397 5911 5397 4883 4369 6425 11051 21074 42662 62451 44975 27499 35723 25957 44461 29555 33924 25443 42405 25186 47545 28270 27756 49601 34695 32896 22616 24929 20817 34695 42662 40092 22102 3341 2056 4626 3855 4112 3855 4112 4369 4369 4112 3855 3855 3855 5654 3855 4112 20046 37779 39321 44204 41634 42405 10023 3598 4369 3084 3084 3084 3084 3341 3341 3084 2827 3084 3341 4112 13621 23901 28784 28270 28270 31868 36237 36494 37779 39578 40863 41891 42662 44204 45489 42662 43433 41891 41891 41634 40349 39578 8224 3084 3341 3598 3855 3598 3341 3341 3084 4112 4369 4369 4369 4626 4626 4883 5140 4112 46260 44718 29298 20046 36751 10794 3598 5140 4369 3598 3855 4626 5397 5397 5654 7196 5654 4626 5397 6168 5397 5140 5911 5654 12079 20046 26728 47802 61166 44975 26728 35209 25700 46774 32125 36751 30326 43176 27499 47288 30840 26985 45232 36237 32639 24415 23901 22359 30069 38036 41377 21588 4369 3598 4112 3855 4112 4369 4112 4369 3855 4112 4369 4369 4626 5140 3598 5140 15420 37779 39321 43690 41891 40092 7967 2827 4112 3084 3598 3598 3598 3855 3598 3598 3341 3855 3855 2313 3341 4112 3084 1799 2570 5397 8224 10023 12079 14906 18247 21845 25186 28527 30326 34181 36751 37522 38550 39064 39321 35723 7710 3084 3341 3341 3341 3341 3341 3598 3598 4112 4112 4112 4369 4369 4112 4369 4626 5911 23130 36494 17990 14906 24415 3084 4883 5140 4626 4369 4112 4626 4883 4883 5397 4883 4626 5397 6168 5140 4369 4883 6425 13107 22359 26471 25186 22873 30326 22359 23644 30326 39578 35723 30840 32125 38550 27242 28784 34952 28013 22359 25443 22102 24672 21845 19275 20560 20817 22102 30840 9766 2313 2827 4369 3855 4112 4626 4883 4369 4112 4112 3855 4626 4369 4883 4626 4883 11822 38550 39578 42919 42148 35466 5397 2570 4112 2827 3855 3084 3341 3341 3341 3084 3084 3598 3598 3855 3341 3084 3341 3598 3598 3598 3341 4112 4112 3855 3341 2827 2570 3084 3341 4112 3598 4883 8224 8995 11308 13621 3855 3084 3084 3084 2827 3084 3341 4112 4369 4112 4112 3855 3855 4112 3855 3855 4112 4626 6682 21074 15163 15934 8481 3084 4369 4112 3855 4112 4369 4626 4883 5397 5654 5911 5397 5397 5140 4883 6168 10280 14906 31868 44975 45232 39835 20046 20046 20817 34952 41891 48573 19018 37522 46260 58596 16962 23387 44204 38807 29555 22616 22359 22102 21074 20817 21588 20046 21074 34695 7967 4369 3341 5140 3855 3855 4626 4883 4369 4112 4112 4112 4369 4112 4369 5911 4369 7453 39321 39321 42148 42148 30840 3084 2313 4112 2570 3855 2827 2827 3084 2827 3084 2827 3341 3341 2827 3084 3341 3855 3855 3855 3598 3598 3598 3855 3855 3598 3341 3084 3598 3855 3598 2056 3341 4369 1799 2056 3084 3084 3084 3084 2827 2827 3084 3341 3855 3855 4369 4112 3855 3598 3855 3855 3855 3598 3084 3855 7710 16191 14392 2056 5654 3598 3341 3855 4112 4112 4112 4626 5397 6168 6682 5911 5397 4626 5654 8738 14649 19532 35980 46774 43690 38807 21074 20303 20817 29812 41377 49344 25186 38036 40092 53970 23130 28527 42919 36237 27756 22873 24415 21331 19789 21331 22616 21845 23901 33410 9766 4112 2056 2827 3855 4369 4369 4369 4369 4369 3855 4112 4369 4626 4369 5911 4112 4626 40092 38807 41377 41377 25957 2827 3084 4112 2313 3598 3341 3341 3598 3341 3341 3084 3598 3598 3855 4369 4112 3341 2570 2313 2827 3341 3084 3341 3598 3598 3341 3084 3084 3084 3084 3598 5140 2827 3084 6425 4112 2313 3084 3084 3084 3084 3084 3084 3084 3084 4112 4112 3598 3855 3341 3341 3341 3341 4369 3341 4626 9766 6425 4626 3084 5140 4369 4112 4112 3855 4112 4369 4883 5654 5397 5140 4626 5140 6168 7710 10280 12079 17476 21331 16962 15934 9766 10537 10280 11565 14906 16191 7196 9766 9252 17733 8995 9766 12593 10023 6939 6168 6168 6682 5654 4883 6425 4883 5654 9509 3341 2313 4369 5140 4112 4369 4369 4112 3855 4112 4112 4369 4112 4626 4626 4883 4369 4626 38293 38550 41634 40092 21074 3341 3598 4112 2570 3341 3084 3084 3341 3084 3084 3084 3084 3598 3598 3598 3855 4369 4883 4626 4112 3598 3855 4112 4369 4626 4626 4626 4369 4112 4626 3855 5397 2056 2313 3855 2827 3084 3341 3341 3084 3084 3084 3084 2827 2827 3598 3598 3855 3855 3598 3598 3598 4112 4369 3855 4883 3855 2570 5654 2827 3855 3598 3855 3855 4112 4369 5140 5654 6168 5140 4626 4369 4626 5654 5911 5140 4883 5397 5654 4883 6425 6168 5911 6682 7196 6682 5654 6425 7710 7453 6168 5654 6682 6168 5654 5911 8224 4112 5654 5397 4883 6425 4369 4112 1542 4112 2313 4369 3855 4369 4369 4112 3855 3855 4112 4112 4626 4112 4626 4883 3855 4626 4626 32639 38807 42662 38807 17476 3855 3855 3341 3084 3341 2827 2570 3084 2570 2827 2827 2827 3341 4883 3598 2313 1799 2570 3341 3598 3598 3341 3341 3341 3341 3341 3084 2827 2570 2313 514 4626 4112 5140 1799 2827 4626 3341 3084 3084 2827 2827 2827 2827 3084 3341 3341 3598 3855 3855 3855 4112 4626 3598 5140 3855 4369 3341 4112 5140 3084 3598 3855 4369 4626 5397 5654 5911 5911 5140 4883 4883 4369 5140 5140 5654 5654 6682 5654 7196 8224 6682 5654 4883 6168 6939 8224 6939 4883 5911 8481 7967 4112 5654 5397 5654 7710 5397 4626 5140 6425 4112 5140 6425 4112 4626 3084 3341 3084 4112 4112 4112 4112 4112 4112 4112 4369 4626 3855 4883 3341 4883 4112 24929 39578 43433 38036 15420 4112 3855 2827 3598 3341 3084 3084 3341 3084 3084 3341 3341 3598 3341 3598 3855 3855 3855 4112 3855 3855 3855 3598 3598 3598 3598 3598 3598 3598 4369 2056 4369 1542 4626 3341 5397 3084 3598 3084 2827 2570 2570 2827 3084 3341 3341 3341 3598 3855 3855 4112 4369 4626 5397 2570 3084 5397 2056 3855 4883 4626 5397 5140 5140 5911 5911 5397 4626 3855 3598 4369 5140 5140 5140 5140 5911 6168 5911 5397 5911 5911 4369 7196 5911 6168 5140 5911 5654 8738 6682 5140 5140 6682 5654 6939 5911 3341 5397 5911 4883 4369 4883 4883 4883 4626 2313 4369 3855 5140 4112 4112 4626 4369 4112 4369 4112 4112 4883 2827 4883 3341 5140 3341 19275 40349 \ No newline at end of file diff --git a/Tests/images/hopper_16bit_qtables.jpg b/Tests/images/hopper_16bit_qtables.jpg new file mode 100644 index 00000000000..88abef943b9 Binary files /dev/null and b/Tests/images/hopper_16bit_qtables.jpg differ diff --git a/Tests/images/hopper_1bit.pbm b/Tests/images/hopper_1bit.pbm new file mode 100644 index 00000000000..216a94ffa89 Binary files /dev/null and b/Tests/images/hopper_1bit.pbm differ diff --git a/Tests/images/hopper_1bit_plain.pbm b/Tests/images/hopper_1bit_plain.pbm new file mode 100644 index 00000000000..9f5c6e501ff --- /dev/null +++ b/Tests/images/hopper_1bit_plain.pbm @@ -0,0 +1,14 @@ +P1 +128 128 +111111111111111111111111111111000001 +111110111100000000111111111111111111 +111111111111111111111111111111111111 +111111111111111111111111111111111111 +111111111111100101111111101111100000 +001111111111111111111111111111111111 +111111111111111111111111111111111111 +111111111111111111111111111111111111 +011111111011111110000001111111111111 +111111111111111111111111111111111111 +111111111111111111111111111111111111 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 0 0 1 0 0 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 1 1 0 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 1 1 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 1 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 1 1 0 0 1 1 1 1 1 1 0 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 0 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 1 0 1 1 1 1 1 0 0 0 0 0 1 1 0 0 0 0 1 1 1 1 1 0 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 0 0 1 0 0 0 1 1 1 1 0 0 0 0 0 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 0 1 1 0 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 0 0 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 0 0 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 0 1 1 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 1 0 0 0 1 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 0 0 0 1 1 0 0 1 1 1 1 0 0 0 0 0 0 0 1 1 0 0 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 0 1 0 0 0 0 1 1 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 0 0 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 0 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 1 1 0 0 0 0 0 0 0 1 1 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 1 0 0 0 0 0 1 1 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 0 0 1 1 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 0 0 1 1 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 0 0 0 0 0 1 1 0 1 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 1 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 1 1 1 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0 1 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 0 1 1 0 0 0 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 0 1 1 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 0 1 0 1 0 0 0 0 1 1 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 0 1 0 0 0 0 1 1 1 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 0 0 0 1 0 0 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 0 0 0 1 0 1 1 1 1 0 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 0 0 0 1 1 1 0 1 0 1 1 1 1 1 0 0 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 0 0 0 1 1 1 0 1 1 1 1 1 1 1 0 0 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 0 0 0 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 0 0 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 0 1 1 0 0 1 1 1 1 1 1 1 1 1 1 0 1 1 1 0 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 0 0 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 0 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 0 0 0 1 1 1 1 1 1 1 1 0 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 0 0 0 0 1 1 1 1 1 0 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 1 0 0 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 0 1 0 1 0 1 0 1 0 1 1 0 0 0 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 0 1 0 1 0 1 0 1 0 1 1 0 0 0 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 0 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 0 0 0 1 0 0 0 1 1 0 0 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 0 0 1 0 0 0 1 1 0 0 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 \ No newline at end of file diff --git a/Tests/images/hopper_8bit.pgm b/Tests/images/hopper_8bit.pgm new file mode 100644 index 00000000000..23d7ada964b Binary files /dev/null and b/Tests/images/hopper_8bit.pgm differ diff --git a/Tests/images/hopper_8bit.ppm b/Tests/images/hopper_8bit.ppm new file mode 100644 index 00000000000..a63406cf460 Binary files /dev/null and b/Tests/images/hopper_8bit.ppm differ diff --git a/Tests/images/hopper_8bit_plain.pgm b/Tests/images/hopper_8bit_plain.pgm new file mode 100644 index 00000000000..37d152c73f3 --- /dev/null +++ b/Tests/images/hopper_8bit_plain.pgm @@ -0,0 +1,4 @@ +P2 +128 128 +255 +25 22 14 26 29 29 18 22 34 35 27 23 30 30 26 30 36 37 32 24 22 23 24 31 33 30 21 31 22 110 229 162 181 165 183 117 22 39 36 18 51 173 71 20 38 121 205 235 247 242 240 239 235 197 46 58 103 91 93 94 100 98 105 102 100 89 100 94 93 100 100 98 97 95 95 95 95 96 99 101 100 99 97 97 100 100 101 102 100 100 100 99 98 99 99 98 98 98 98 99 99 99 99 99 99 99 98 98 99 100 101 101 99 98 99 102 108 112 115 115 116 116 116 115 112 111 111 112 25 22 16 27 28 28 17 20 32 33 25 21 29 29 27 32 34 37 31 22 21 21 23 29 25 32 26 27 30 131 177 92 174 59 48 88 63 31 20 27 56 168 73 29 17 31 80 145 201 241 243 241 236 205 42 38 92 95 99 83 73 81 92 98 98 101 98 98 103 96 92 99 99 95 95 102 103 97 95 97 100 99 98 99 101 101 103 102 102 101 102 100 99 99 99 98 100 98 98 98 98 99 99 100 100 100 99 99 98 98 98 99 102 101 100 101 106 109 114 115 117 117 118 116 114 113 113 114 25 24 17 28 28 27 15 19 31 32 23 20 28 29 26 32 31 35 31 23 22 23 24 29 35 29 22 28 34 119 98 62 169 67 29 30 24 33 45 21 72 173 57 18 28 35 24 39 52 201 236 240 237 224 138 29 36 41 37 29 52 38 41 52 45 64 83 102 100 113 97 108 99 99 98 98 97 97 101 106 101 100 100 101 102 102 102 102 105 103 103 102 102 100 99 99 101 100 98 98 98 99 100 101 100 99 99 99 99 98 98 98 103 102 101 101 103 107 112 117 117 118 118 117 115 114 115 116 23 22 18 29 29 26 14 19 32 34 23 21 28 30 26 33 31 37 31 23 22 24 25 28 38 27 21 29 29 70 41 81 179 60 24 25 30 27 36 21 56 176 62 22 22 30 31 33 31 190 246 145 128 172 57 26 39 29 26 79 126 122 74 53 47 47 47 41 80 96 107 93 98 100 98 97 103 109 103 92 102 102 102 103 104 104 103 101 104 103 103 102 102 101 101 101 101 100 100 99 99 100 101 101 98 99 99 99 100 100 99 99 100 101 101 100 101 105 111 116 114 115 115 114 114 114 115 116 21 22 19 28 28 25 15 21 33 34 25 21 28 30 27 32 31 36 33 23 22 24 25 27 23 31 28 23 22 36 21 85 166 48 29 24 36 36 31 19 88 169 33 26 29 24 35 32 45 205 202 47 27 18 32 31 40 152 139 73 136 238 113 32 122 168 79 47 42 53 96 111 101 103 105 104 101 101 104 107 103 103 104 104 106 105 103 102 102 102 103 103 102 102 102 102 102 102 102 102 102 102 102 101 101 101 101 101 101 101 101 100 98 100 102 101 101 103 107 111 109 110 111 111 111 111 112 113 21 22 20 30 28 25 15 22 33 34 25 21 29 29 25 30 30 36 32 21 19 22 23 25 26 34 26 21 26 25 22 83 176 51 34 30 34 27 22 34 90 175 40 30 23 25 31 20 47 216 85 43 20 21 26 88 234 250 252 210 139 253 170 159 249 253 243 170 41 47 54 95 107 96 95 108 110 101 97 103 106 104 105 106 106 106 104 102 100 100 101 101 103 102 103 103 102 103 103 104 103 103 102 101 104 103 102 101 100 100 100 100 100 101 101 101 101 102 103 105 106 106 107 108 108 109 110 111 20 24 22 32 28 24 15 21 30 32 24 22 30 31 26 31 30 36 31 21 18 22 22 24 35 31 21 23 25 24 26 84 167 54 25 25 38 37 29 19 84 169 40 20 20 37 31 40 46 184 164 148 28 23 124 248 222 157 233 244 254 249 242 253 246 209 205 230 194 50 43 136 193 154 117 103 104 105 106 107 106 105 105 105 105 105 104 103 100 101 101 101 103 102 102 102 104 104 104 103 103 103 103 103 105 103 102 100 99 99 100 101 102 101 100 101 102 102 102 101 103 105 107 108 108 108 109 110 22 25 22 33 29 24 13 20 27 30 24 21 30 31 26 31 32 39 33 20 18 21 23 24 24 27 25 26 18 28 27 77 170 60 21 31 37 26 30 28 109 157 30 23 31 31 29 108 216 251 254 135 24 66 122 86 121 95 74 102 166 225 222 164 101 88 130 123 73 101 35 51 235 252 246 201 143 108 103 107 107 106 106 104 105 105 105 103 101 101 101 101 102 102 102 102 106 105 103 102 102 103 104 104 103 102 100 98 98 99 101 102 102 101 99 100 102 104 104 104 104 106 107 108 109 109 110 110 23 27 115 40 24 22 16 17 28 35 22 18 42 34 23 29 27 39 26 24 18 22 17 21 29 28 29 20 19 25 30 79 172 53 26 32 34 35 30 26 98 162 31 35 25 44 194 247 255 250 254 105 30 28 29 136 190 242 232 143 233 198 212 200 147 245 217 212 83 26 47 37 184 254 248 252 251 209 127 106 110 108 100 114 96 112 107 102 103 103 104 103 104 103 103 104 105 103 102 103 103 102 103 105 101 101 100 99 99 100 102 104 100 102 104 103 102 101 102 103 103 103 103 103 104 105 107 107 22 37 204 90 36 15 21 17 37 26 13 20 40 22 29 30 36 34 26 26 21 16 18 27 28 27 24 24 31 27 28 74 170 53 25 33 35 34 28 24 100 162 32 14 78 227 248 253 245 253 249 86 26 33 33 120 186 218 223 245 250 245 251 250 249 228 227 212 87 52 28 33 147 253 253 242 253 252 236 182 115 100 106 110 108 106 98 109 104 103 103 103 103 104 103 104 103 102 101 103 103 102 103 105 102 102 101 101 100 101 103 104 101 101 102 102 101 101 101 101 103 103 103 103 104 105 107 107 16 35 213 223 44 25 16 35 25 30 24 26 27 43 24 26 26 28 33 22 15 23 30 17 33 32 24 21 28 25 31 74 170 56 27 33 35 32 27 22 113 162 31 90 250 254 249 248 248 242 221 63 40 25 31 54 213 204 213 226 219 218 208 218 232 220 179 217 71 39 52 48 153 254 249 254 252 254 254 248 210 125 106 112 110 97 116 98 102 102 102 102 102 103 102 103 102 101 101 102 102 102 103 105 103 103 103 102 102 102 104 105 104 103 103 103 103 103 103 102 104 103 104 104 105 106 107 108 20 39 198 240 128 81 172 91 27 29 21 23 33 125 38 24 34 34 34 25 12 17 17 23 27 29 29 23 20 24 31 72 170 60 27 32 37 32 28 24 119 148 59 245 242 252 246 225 173 173 207 59 12 23 22 38 108 217 203 246 250 250 251 245 250 247 208 164 42 23 29 26 116 236 252 253 243 253 252 253 253 220 115 105 105 111 101 104 100 101 100 100 100 100 100 100 102 100 100 102 103 102 102 104 104 104 104 103 103 103 104 105 106 104 103 103 104 104 103 101 104 104 105 105 105 106 107 108 26 29 195 251 236 229 195 73 31 26 21 26 36 182 94 32 24 34 26 24 24 30 7 23 21 25 25 28 22 26 25 64 172 65 25 29 35 30 29 26 109 156 214 246 252 241 158 137 114 60 33 24 21 32 26 38 184 220 153 214 248 253 254 255 248 239 155 217 106 40 33 39 59 125 190 217 251 252 246 251 243 251 190 105 103 103 105 102 100 100 100 101 100 100 100 100 102 99 100 101 102 101 101 103 102 103 103 103 102 101 102 103 104 102 101 100 102 102 101 100 104 104 104 104 105 106 108 109 35 134 227 241 238 236 162 38 27 30 28 25 31 198 164 27 34 36 95 102 24 14 27 18 30 57 20 26 25 26 24 64 170 69 24 27 35 28 29 26 100 196 238 247 229 122 100 47 16 21 14 23 15 16 32 25 131 164 202 227 225 252 246 250 213 210 191 200 92 33 47 31 35 24 33 99 169 219 245 252 252 253 240 128 114 100 99 109 102 102 102 103 102 102 102 103 101 100 100 102 102 101 101 103 101 102 103 103 102 101 101 101 104 103 102 101 102 102 102 102 103 103 104 104 106 107 110 110 171 230 241 251 243 230 115 16 34 34 18 20 41 212 213 85 80 190 208 81 22 24 20 19 49 138 38 21 24 23 29 63 168 71 24 27 36 27 29 26 97 218 243 235 106 54 14 18 13 12 93 112 32 26 17 39 125 238 232 209 162 227 249 247 192 212 235 237 87 41 39 32 35 104 34 40 27 81 154 192 248 238 232 209 93 114 107 104 103 103 103 103 103 103 103 103 102 101 101 103 103 101 101 102 101 102 103 103 102 101 100 101 105 105 104 103 103 103 104 105 103 104 103 104 106 108 111 112 179 224 241 247 248 248 172 62 27 34 23 37 46 200 230 224 240 230 161 30 30 20 12 22 54 213 71 24 27 24 34 53 168 72 24 28 38 28 30 27 90 228 233 135 26 12 20 106 98 46 33 149 28 63 77 38 98 189 193 191 211 155 96 107 169 188 193 205 99 46 63 35 75 138 28 100 111 27 34 76 163 238 237 225 123 103 106 107 103 104 103 103 103 103 104 103 103 101 101 103 103 101 101 102 101 102 103 103 102 101 100 100 103 103 104 103 101 101 103 105 102 102 103 104 106 108 111 112 27 50 120 247 244 204 192 90 30 36 25 76 166 237 234 232 236 215 58 33 25 29 8 21 70 229 144 26 57 118 48 58 171 78 23 23 36 33 25 29 84 185 141 25 23 26 27 175 204 192 183 146 193 222 234 214 215 121 98 36 32 49 38 37 38 49 82 178 194 231 242 174 184 138 216 218 176 53 97 58 47 164 244 222 159 106 109 105 106 101 107 101 108 102 103 103 105 103 102 102 103 104 103 102 103 102 102 101 101 101 102 103 104 105 106 107 106 106 107 107 108 107 107 107 108 109 111 112 26 27 84 244 191 64 57 42 27 52 147 219 245 237 243 240 234 106 32 30 27 10 34 19 75 227 208 143 195 127 32 41 168 81 25 23 36 33 25 25 22 5 16 18 10 25 22 46 136 150 55 175 227 179 175 224 239 235 243 192 114 78 31 54 106 197 246 234 238 228 200 237 231 140 185 201 189 223 202 46 23 43 189 184 109 54 42 94 106 102 105 100 107 103 102 102 105 102 101 101 102 102 102 101 103 103 102 102 102 103 104 105 106 107 108 108 107 107 109 110 109 109 109 109 110 111 112 113 16 37 79 235 127 28 28 22 30 41 95 177 221 248 243 239 225 170 44 33 16 18 24 28 110 237 220 180 173 51 27 49 163 86 25 23 34 33 22 19 25 16 16 19 15 16 10 22 13 35 60 106 52 34 180 197 172 145 100 75 63 78 50 76 70 81 144 159 213 217 131 77 111 112 18 37 100 192 173 159 163 49 54 29 18 15 28 47 104 105 105 100 108 104 105 104 105 104 102 102 103 103 102 102 104 104 103 103 104 105 106 107 106 107 108 109 108 108 109 110 109 109 110 111 111 112 113 113 20 33 62 207 65 31 29 14 21 38 20 37 74 197 247 246 241 230 108 30 27 47 94 140 208 237 196 182 94 21 23 36 159 90 22 25 33 26 19 14 7 18 18 15 7 14 22 19 26 13 11 26 56 31 31 33 32 34 30 20 30 26 19 36 29 24 45 34 46 49 61 70 173 180 188 64 25 24 95 137 56 34 25 18 26 11 24 14 92 106 105 101 106 104 106 106 107 107 106 106 106 105 105 104 105 105 105 105 105 106 106 107 106 107 108 108 108 108 109 110 108 109 110 111 112 112 113 113 29 34 60 148 32 31 28 19 19 24 25 19 38 196 238 116 130 197 204 58 26 48 85 150 201 221 219 177 44 24 26 38 156 93 18 27 30 19 13 13 17 12 14 23 19 12 17 3 21 16 14 27 16 26 24 13 20 18 26 22 36 24 30 24 31 29 23 28 37 24 25 37 98 130 221 211 80 33 22 15 13 17 15 9 16 14 6 18 74 101 101 102 103 101 104 107 108 109 109 109 109 108 107 107 107 107 107 106 106 106 106 106 108 108 109 109 108 109 110 111 109 110 111 112 113 113 113 113 14 31 43 60 26 34 26 18 29 29 31 35 27 200 188 44 31 38 92 85 20 24 20 32 129 223 225 186 111 28 27 29 151 100 19 26 25 14 12 13 10 18 11 7 23 12 13 24 11 18 14 7 19 15 14 33 32 22 19 20 14 15 23 16 22 11 20 28 17 30 27 28 42 92 48 28 18 24 8 17 12 8 13 18 10 17 11 21 62 101 102 105 105 103 105 110 109 110 111 111 110 109 108 108 109 109 108 108 107 107 106 106 110 110 110 110 109 109 111 112 111 112 112 113 114 115 115 115 27 34 28 32 48 36 22 22 26 31 11 31 40 202 102 35 33 44 37 40 26 13 18 14 98 238 192 190 195 51 14 32 146 111 21 20 17 12 17 11 17 13 19 11 15 16 18 4 14 7 17 17 2 20 15 5 11 15 19 26 17 20 19 19 17 20 24 20 22 28 10 16 12 9 18 8 16 10 0 14 12 12 13 15 4 8 19 16 62 108 105 109 108 107 109 110 109 110 112 112 110 109 109 110 111 111 110 109 109 109 108 108 111 111 111 110 109 109 110 111 111 111 112 112 113 114 116 116 20 41 26 29 39 40 30 15 19 26 24 22 45 165 33 37 31 35 32 27 21 21 15 18 126 223 80 39 145 126 27 23 142 118 25 18 11 12 20 12 15 9 19 12 18 16 14 15 12 13 7 16 19 1 17 9 13 11 11 8 11 13 9 12 15 11 9 12 7 7 16 11 12 17 5 9 12 7 24 9 14 4 16 10 18 17 10 13 64 110 106 109 108 109 107 108 110 111 113 113 111 110 110 111 111 111 110 110 110 110 110 110 110 110 110 109 107 107 109 110 111 110 111 111 112 113 115 116 23 32 28 34 37 40 27 20 28 24 18 30 44 57 36 27 35 34 34 30 22 15 15 18 147 166 25 14 29 51 28 20 130 119 22 14 13 16 16 15 14 16 13 12 14 12 11 13 8 12 13 12 11 13 12 7 10 13 11 10 14 12 9 11 14 12 11 11 12 12 11 10 10 12 12 10 10 13 12 8 13 10 12 13 13 17 17 11 64 112 108 109 104 113 108 116 112 112 113 113 111 110 110 110 110 111 112 112 112 112 111 111 110 110 109 109 110 112 112 113 113 111 109 109 111 112 111 111 24 32 28 34 36 39 25 17 22 27 24 32 32 39 28 32 32 33 34 31 23 17 16 18 132 64 22 25 16 17 20 27 124 123 19 14 12 16 15 14 9 18 16 10 10 15 15 18 15 15 13 11 9 10 14 19 12 14 13 12 14 13 9 10 9 10 10 10 12 13 14 13 9 13 14 12 9 9 10 11 8 4 7 10 8 10 15 17 56 110 111 113 109 115 109 116 113 113 114 114 113 112 112 112 113 113 114 114 113 113 112 112 113 112 111 111 111 112 112 113 113 111 109 109 110 111 111 111 23 32 29 35 35 38 23 16 21 26 25 29 24 24 24 37 31 32 34 32 24 18 16 18 58 23 36 13 10 23 22 21 119 132 16 16 12 15 14 14 21 16 9 11 19 16 6 6 11 8 10 17 16 11 9 12 11 11 11 11 12 13 11 12 14 14 14 13 12 11 11 10 14 12 12 13 14 14 13 14 14 9 12 17 14 10 12 12 48 111 113 116 111 116 110 116 114 114 115 116 115 114 114 114 115 115 116 116 115 114 114 113 115 115 114 113 112 113 113 114 112 111 110 109 110 111 111 111 23 32 28 35 35 37 22 14 26 25 18 25 29 25 29 37 33 33 34 31 24 16 15 17 17 32 28 18 21 24 14 25 117 141 16 18 12 15 13 13 13 14 12 11 12 10 11 19 18 13 9 11 13 13 14 15 17 14 15 16 14 14 16 17 15 16 16 17 19 19 21 22 18 15 13 13 12 10 10 12 11 6 8 12 12 15 16 13 42 114 114 116 110 114 112 119 116 116 117 117 116 116 116 116 117 117 117 117 117 116 116 115 116 116 115 114 114 114 114 114 112 111 111 110 111 111 112 113 23 30 28 34 34 38 22 15 28 25 16 24 34 26 33 36 35 35 33 28 21 15 14 17 28 32 20 28 12 13 15 24 116 140 16 17 11 14 12 13 5 14 17 14 15 16 9 8 13 22 32 37 43 51 55 56 50 42 40 38 29 27 31 29 38 40 47 54 64 73 80 84 88 80 66 50 32 18 13 12 13 12 12 12 8 13 17 13 31 113 115 115 112 115 114 120 116 117 118 118 117 116 117 118 116 116 116 116 116 116 117 117 116 116 115 115 115 115 115 116 113 113 112 112 112 112 114 114 23 31 27 32 33 37 23 15 23 26 20 24 34 23 34 34 36 35 33 29 20 15 14 17 26 26 33 18 9 21 21 19 116 130 15 15 12 13 14 14 16 11 8 12 14 10 18 37 76 99 123 133 135 136 137 136 133 122 121 117 102 96 99 96 101 101 108 117 128 139 145 148 139 141 144 142 131 97 50 14 9 8 14 15 10 10 14 11 20 111 114 118 117 118 116 119 118 118 119 118 118 117 118 118 117 116 116 116 116 117 118 118 117 116 116 116 116 117 117 117 114 114 114 114 113 114 115 117 24 32 27 32 32 36 23 17 19 27 22 23 33 22 36 31 35 33 32 29 22 14 14 18 23 18 29 22 25 21 10 26 124 123 19 12 10 13 13 12 16 12 9 12 18 38 89 144 157 164 170 170 171 172 176 179 179 170 173 174 158 152 156 152 159 158 160 164 173 177 176 173 169 165 161 161 168 160 129 96 37 17 6 11 12 12 14 17 17 114 115 119 118 119 118 118 120 120 120 120 118 118 119 119 118 118 117 117 116 117 117 117 118 118 118 118 118 118 118 118 116 117 116 115 115 115 117 119 26 33 27 31 32 36 23 16 17 27 20 21 33 25 39 31 32 32 32 29 23 15 14 17 25 28 25 27 15 18 19 19 131 120 23 13 12 13 11 11 10 23 25 31 74 131 164 169 171 164 163 175 189 194 197 198 195 186 195 202 190 187 192 188 191 188 189 194 198 200 196 190 176 179 178 170 170 172 164 150 128 71 23 15 15 13 15 19 21 119 116 116 116 117 118 119 121 121 121 120 119 119 119 120 120 119 118 117 117 117 117 117 119 119 120 118 119 119 118 118 117 118 118 118 116 116 118 120 75 38 29 38 29 38 26 17 18 29 25 26 23 36 36 30 33 31 30 31 24 14 14 18 21 32 23 21 25 14 15 24 124 130 13 12 10 14 16 15 42 81 70 87 137 162 176 182 175 178 181 184 186 192 197 201 205 201 194 197 196 196 207 205 192 205 205 200 191 210 210 202 202 182 186 171 181 171 172 154 154 126 94 62 13 12 17 13 26 120 118 114 120 119 122 119 121 120 119 119 120 120 119 118 119 118 117 118 118 118 118 117 118 118 118 118 118 118 118 118 119 119 118 119 121 122 124 125 189 33 34 36 26 65 42 20 23 25 15 29 25 27 28 30 30 30 30 31 24 15 15 18 24 35 28 24 24 17 18 19 119 141 13 12 19 6 4 36 89 92 84 115 155 170 178 175 181 181 186 190 195 201 204 205 199 198 199 205 200 194 198 195 204 203 200 201 197 193 189 190 191 202 177 169 173 171 167 156 156 141 137 94 58 16 18 10 30 118 116 114 119 115 119 121 122 121 120 120 121 121 121 120 121 120 119 119 120 120 119 118 119 119 120 120 120 120 120 120 120 120 120 121 121 122 123 123 234 150 30 48 136 176 33 11 28 35 29 29 25 33 28 29 25 27 31 31 24 15 15 18 23 31 28 26 23 20 24 19 123 133 12 18 12 15 34 70 83 77 100 138 156 166 177 176 188 191 195 196 191 187 183 182 193 196 198 204 198 190 195 193 184 190 188 178 177 161 146 138 146 145 158 159 151 157 158 161 155 147 145 130 91 31 11 20 41 122 121 119 123 118 123 126 123 122 121 122 123 123 123 122 123 122 121 121 122 121 120 120 121 122 122 123 123 123 123 123 121 121 122 123 123 123 122 122 232 225 101 202 231 96 28 14 71 119 24 29 27 28 29 29 23 27 31 30 23 14 14 19 27 31 29 29 24 21 25 19 126 135 14 15 2 16 41 63 77 88 124 142 143 156 166 167 155 152 146 138 133 135 145 154 182 192 197 199 194 193 202 195 174 184 181 160 155 135 129 134 121 136 160 162 182 162 153 155 156 151 137 135 120 70 22 13 48 124 123 117 120 120 120 121 123 123 122 123 124 124 124 123 124 123 122 123 123 123 123 122 124 124 124 124 124 124 123 123 122 122 123 123 124 124 124 124 224 240 239 227 186 41 19 20 77 217 53 37 26 17 43 34 25 27 30 27 21 15 15 18 28 29 27 29 24 16 21 19 117 142 17 12 14 22 38 70 94 105 127 137 148 163 168 168 167 156 144 136 137 142 153 163 167 185 193 192 188 194 204 192 190 176 163 156 156 117 94 98 94 106 128 150 176 165 178 147 145 158 153 115 120 76 43 24 57 128 128 119 121 126 126 121 124 123 123 123 124 125 124 124 124 123 123 123 125 125 125 124 126 126 125 125 124 123 122 122 124 123 123 123 124 126 127 128 239 228 225 222 95 30 36 11 96 226 155 36 31 100 154 48 27 29 29 27 21 15 14 16 19 24 20 20 17 13 19 22 120 127 17 19 16 30 72 105 104 113 124 143 159 157 153 155 155 138 117 110 110 112 115 116 151 171 180 177 173 184 199 192 188 178 163 147 131 100 87 92 80 62 69 64 57 64 63 74 77 94 94 100 114 83 41 31 74 132 130 122 123 131 128 124 124 124 123 124 125 125 125 124 125 124 124 125 126 126 126 125 127 127 126 126 125 124 123 123 125 125 124 124 125 127 129 130 225 234 225 230 89 36 12 15 79 226 237 106 174 226 132 29 27 28 29 26 23 19 16 14 18 34 27 15 13 15 20 20 133 117 20 17 7 34 102 112 97 105 117 148 157 144 149 148 145 128 109 100 100 107 119 132 122 148 169 176 173 181 200 202 195 177 149 133 120 101 64 36 58 83 102 93 92 80 43 43 113 93 45 70 88 93 43 42 90 133 126 120 121 124 120 123 126 125 124 125 125 125 125 124 127 127 126 126 126 127 126 125 127 127 127 127 127 126 126 125 126 126 126 126 127 128 129 129 231 244 242 226 212 55 32 17 99 217 239 235 214 188 42 21 24 27 29 28 25 20 15 13 33 57 45 20 15 17 20 15 127 126 24 11 25 58 115 114 100 92 81 90 72 48 49 29 27 31 38 41 37 34 46 59 90 127 168 195 195 193 203 206 198 181 150 118 72 70 105 150 121 133 141 149 121 148 231 108 28 47 42 52 73 85 64 62 115 145 133 131 131 126 122 131 127 126 125 125 126 126 125 124 130 129 128 127 127 127 126 125 127 128 128 128 128 128 128 128 126 127 127 128 128 128 128 128 178 235 206 218 234 169 27 58 198 230 226 223 214 95 38 21 26 27 26 32 15 75 51 49 142 163 30 22 11 18 16 19 122 121 31 19 35 101 133 108 85 65 30 51 61 51 31 117 190 173 142 124 125 149 151 96 81 64 116 149 192 194 184 180 164 141 108 121 176 137 117 51 38 43 61 77 120 121 155 200 62 48 89 70 69 92 108 95 132 126 135 131 129 132 126 126 127 127 127 127 126 126 126 126 128 128 129 130 130 129 129 129 128 128 128 129 129 129 128 128 129 128 128 128 128 128 129 129 147 214 70 57 97 152 92 57 203 221 219 218 190 42 29 24 24 28 26 31 21 72 109 130 182 79 17 10 18 17 15 19 124 117 31 53 75 119 122 94 68 57 18 28 16 41 126 129 136 98 59 48 45 40 90 153 138 131 139 143 168 170 169 148 143 122 126 128 120 109 47 79 31 52 46 42 63 88 78 72 79 70 60 40 41 81 122 149 173 157 127 136 133 129 131 136 129 129 129 128 128 128 128 128 130 130 131 131 131 130 130 129 129 129 129 130 129 129 129 128 127 128 128 129 129 130 131 131 139 187 36 38 20 33 29 27 124 217 233 230 216 113 35 28 27 34 27 26 20 48 109 112 110 34 18 14 13 16 21 17 126 134 29 53 116 132 107 73 47 49 58 52 28 101 113 120 89 50 40 35 38 35 56 75 119 114 112 147 150 133 140 135 132 106 142 135 109 58 79 47 15 37 38 43 46 73 79 91 121 100 80 81 90 127 142 108 133 168 132 138 125 128 138 131 131 131 131 131 131 131 130 130 132 132 133 133 132 132 131 130 131 131 131 131 130 130 130 129 128 129 130 131 131 131 131 131 118 111 26 32 31 25 13 14 41 199 230 205 230 201 46 15 22 33 29 24 27 62 119 104 37 10 21 23 9 16 24 18 113 140 34 57 154 136 115 107 71 83 59 66 49 89 70 100 49 43 56 25 17 45 88 104 67 84 92 140 128 167 193 191 165 147 153 147 105 125 111 97 88 80 74 78 88 93 107 126 152 138 147 128 112 108 121 70 62 144 138 134 124 128 139 124 133 133 133 133 133 133 133 133 134 134 134 134 134 133 132 131 131 131 132 132 132 132 131 131 131 132 133 133 132 131 130 129 77 62 40 20 23 34 18 15 28 200 146 43 103 179 116 23 26 32 31 21 17 63 115 124 101 8 14 17 20 16 18 19 113 135 34 81 139 87 84 106 109 110 105 128 107 83 79 74 69 65 73 75 101 100 101 129 137 150 151 155 100 177 206 206 186 124 177 168 155 136 126 147 135 129 119 111 118 135 141 133 142 149 166 133 120 114 137 145 61 122 129 129 137 130 132 134 134 134 134 134 134 134 134 134 135 135 135 135 134 133 132 131 132 132 133 134 134 134 134 134 133 134 134 134 133 132 131 130 39 24 23 32 34 33 14 12 40 203 79 28 35 32 54 27 27 24 29 29 24 60 89 117 150 82 14 16 19 20 17 16 120 129 25 93 109 81 108 113 114 105 147 153 115 110 76 71 62 71 102 146 171 161 143 152 146 174 192 169 98 171 198 207 201 168 170 182 153 139 175 168 182 193 182 163 138 165 169 155 158 157 158 131 118 150 159 141 84 121 132 127 138 132 129 141 135 135 135 135 135 135 135 135 136 136 136 135 135 134 133 132 133 133 134 134 135 135 135 135 132 133 133 134 134 134 134 134 20 26 28 32 27 24 13 15 45 149 20 31 37 20 24 16 31 28 28 27 20 23 19 32 86 108 23 25 12 21 19 17 125 135 22 64 83 69 122 110 104 109 139 163 130 130 98 87 88 111 139 179 180 182 167 148 134 181 199 148 123 188 205 195 203 180 136 187 191 160 141 172 186 177 166 155 138 158 170 159 166 161 163 147 124 160 188 154 87 133 137 130 131 135 133 136 135 135 136 136 136 136 136 137 136 136 136 136 136 135 134 133 133 133 135 135 135 135 134 134 133 133 133 133 134 134 135 135 27 21 26 21 31 29 16 15 33 57 27 35 29 29 25 24 22 33 30 23 23 15 11 15 20 30 26 28 16 14 17 19 110 132 28 51 102 64 138 148 127 92 131 148 139 133 137 119 102 135 132 156 146 131 118 138 197 190 173 120 158 196 206 205 196 205 143 170 180 204 188 154 151 132 141 128 133 140 173 168 150 151 151 145 140 119 162 163 95 140 132 137 129 138 136 134 136 136 136 136 137 137 137 137 137 137 137 137 137 136 135 134 135 135 136 135 135 134 133 133 135 135 133 133 133 134 134 134 22 24 23 28 32 26 16 14 24 22 26 30 29 25 23 26 26 32 27 26 19 13 16 11 14 18 20 24 14 19 18 17 112 134 22 31 122 74 156 145 116 96 137 141 150 138 149 124 98 131 133 134 139 145 177 205 204 192 168 122 184 195 210 206 207 209 178 149 193 200 200 210 200 188 169 154 157 174 178 168 156 151 148 144 113 74 132 161 125 127 135 138 132 133 137 140 138 137 136 136 137 138 137 137 137 136 137 139 138 136 134 135 137 137 137 136 135 134 133 133 134 134 134 134 134 134 134 134 24 25 25 28 32 25 15 12 21 21 24 29 28 23 21 23 27 32 28 25 20 15 17 14 11 22 16 16 26 19 19 20 120 141 12 49 119 101 163 146 97 96 131 133 151 153 152 121 123 144 167 187 200 208 213 206 199 209 149 161 192 194 205 212 210 202 209 159 172 194 203 200 200 202 191 179 180 181 168 157 160 153 124 160 135 81 111 160 147 142 142 139 131 131 136 134 138 137 136 136 137 138 137 137 139 137 137 138 138 136 136 137 137 137 137 136 135 135 134 134 135 135 134 134 134 135 135 135 23 24 25 27 31 23 14 12 22 23 25 30 29 25 22 25 27 31 26 23 19 15 18 15 18 19 16 16 26 11 19 23 111 138 57 156 191 132 156 154 95 121 144 125 136 151 150 135 101 123 184 212 208 211 216 211 211 196 127 185 198 205 204 212 210 203 205 183 128 176 195 196 203 201 182 168 172 168 157 152 152 134 138 161 176 113 124 141 148 139 137 135 134 138 141 138 138 137 137 137 137 138 138 137 140 138 137 138 138 137 137 139 137 137 136 136 136 136 136 136 135 135 136 136 135 135 135 135 22 23 24 26 30 23 15 14 21 23 26 30 30 27 26 26 26 30 24 21 17 13 18 14 21 13 22 18 19 18 17 12 84 169 149 182 197 168 180 136 129 165 164 123 118 138 143 149 144 127 165 202 207 209 201 192 179 122 120 175 188 204 205 199 208 210 211 195 120 100 150 169 172 168 169 178 189 186 167 147 138 135 146 166 184 159 125 141 144 138 137 137 136 137 137 133 139 138 137 137 138 138 138 138 139 137 137 138 139 137 138 139 137 137 137 136 137 137 138 138 136 136 136 136 136 136 136 136 21 23 23 25 28 22 14 14 18 21 24 28 29 27 27 28 27 30 24 20 17 13 16 13 15 16 25 18 17 27 13 35 132 166 157 181 197 167 171 155 160 186 171 128 119 133 132 145 163 132 137 161 186 189 156 133 104 99 162 180 189 202 209 202 206 203 198 203 182 127 112 117 101 103 133 167 175 178 165 136 139 141 142 162 182 146 120 154 140 137 138 140 137 138 139 136 139 138 137 137 138 139 138 138 138 137 137 140 140 138 137 138 137 137 137 137 137 137 138 138 136 136 136 136 137 137 138 138 24 25 24 25 26 20 13 14 18 20 25 28 28 29 28 28 26 29 24 21 18 16 18 13 15 20 19 21 18 10 25 124 173 172 150 189 202 172 163 171 160 180 174 142 127 129 128 141 166 155 130 97 93 106 106 105 73 146 187 177 184 194 204 199 202 203 194 192 177 165 116 162 174 169 180 183 164 158 157 142 146 137 154 170 170 125 156 150 137 134 136 138 135 138 142 141 140 139 138 138 139 139 139 138 138 137 138 140 140 138 137 137 137 137 137 137 137 138 138 138 137 137 137 137 137 137 139 139 24 25 25 24 26 20 12 13 19 22 26 28 29 28 28 27 25 27 22 21 19 17 20 14 18 19 16 22 16 17 86 187 183 176 143 192 196 152 153 177 163 171 174 146 128 125 133 144 144 165 163 149 151 164 156 126 97 170 158 150 169 176 191 188 179 171 175 150 150 146 95 123 176 185 186 175 160 152 152 154 146 138 152 170 148 159 170 150 143 139 139 138 133 135 136 133 140 139 138 138 139 139 139 139 140 138 139 140 140 138 137 138 138 137 137 137 137 138 138 138 137 137 137 138 138 138 138 138 21 24 23 25 26 20 13 15 17 21 24 26 26 25 24 24 21 25 19 21 19 17 20 14 14 18 19 15 16 61 160 175 172 177 144 192 199 136 147 164 185 170 165 138 126 132 144 144 135 153 159 169 179 184 157 95 78 103 81 89 120 126 144 149 150 144 111 100 55 80 51 86 145 170 170 160 165 160 152 163 151 139 148 117 163 179 145 163 136 133 136 139 139 142 142 138 140 139 138 138 139 140 139 139 142 140 139 140 139 138 138 140 138 137 137 137 137 138 138 138 138 138 138 138 138 138 139 139 15 30 24 19 39 18 12 18 25 10 26 33 26 24 18 26 24 31 25 17 17 20 20 9 15 14 20 21 73 149 173 177 173 172 148 193 187 137 149 171 150 148 98 126 135 132 145 157 143 148 160 161 168 150 101 78 155 125 61 60 66 76 90 90 84 78 94 74 88 149 99 45 102 139 150 143 159 150 156 165 155 155 140 127 128 134 164 152 140 137 137 138 138 137 139 141 138 140 141 140 140 139 140 141 143 142 140 139 138 137 137 136 137 136 136 136 138 138 138 137 138 138 138 138 139 140 141 141 28 22 18 37 22 21 17 3 20 25 20 21 27 21 20 20 21 22 17 17 17 18 20 20 12 20 22 97 163 163 176 176 173 172 149 191 185 150 152 170 163 153 165 176 150 131 142 154 160 143 165 158 151 133 76 124 171 166 159 160 154 120 75 40 61 113 143 150 146 157 129 58 51 108 134 142 147 147 160 161 151 164 190 171 177 159 177 149 138 136 136 138 139 138 138 139 138 140 141 140 140 139 140 141 142 141 140 138 137 137 137 137 136 135 135 135 136 137 137 136 137 137 138 138 139 139 140 140 21 26 36 58 27 7 19 21 12 24 30 32 31 23 21 21 21 19 16 16 20 19 15 18 12 31 142 177 154 176 178 174 172 170 152 190 184 161 146 159 180 173 187 190 177 138 140 158 164 148 151 155 128 87 100 157 164 159 167 158 168 161 145 127 134 156 147 155 149 139 157 120 48 66 102 113 131 146 159 163 156 160 179 186 160 181 171 140 137 137 137 139 140 139 139 140 138 140 140 140 139 139 140 140 140 139 138 137 137 137 137 137 137 136 135 135 136 137 136 136 136 136 137 137 138 138 139 140 66 37 126 78 26 23 10 12 32 13 22 25 22 25 20 17 20 23 20 15 21 19 11 15 33 151 175 170 176 166 177 170 171 166 152 190 183 164 126 134 183 194 185 199 176 140 148 159 165 153 144 139 96 64 146 159 163 167 173 165 169 174 175 163 175 160 154 155 163 155 163 150 85 45 86 105 141 150 145 152 149 166 190 174 172 171 146 135 138 136 137 140 141 139 139 140 138 140 140 140 139 139 139 140 139 138 138 137 137 137 137 137 138 137 136 136 137 137 137 136 136 136 136 136 137 138 138 139 165 155 137 30 19 15 16 11 15 24 22 23 23 16 19 23 20 20 22 16 18 15 21 54 132 156 175 173 173 176 177 177 175 165 154 194 186 161 107 107 165 187 184 189 121 134 157 147 173 149 155 114 68 97 148 162 168 181 175 182 176 186 199 192 192 181 181 160 166 170 166 156 146 85 73 102 130 144 153 166 152 147 191 184 156 154 147 136 136 135 136 140 140 139 138 139 139 140 140 140 139 139 139 140 138 138 137 137 137 137 138 138 138 137 137 137 138 138 137 137 136 136 136 137 137 138 138 138 202 164 103 28 16 26 10 15 42 75 35 25 32 11 21 24 20 16 22 20 18 16 43 111 158 161 174 184 168 168 185 181 176 166 154 194 191 158 99 86 109 154 168 122 33 93 141 168 169 160 154 106 76 135 137 170 168 179 170 188 175 183 199 193 192 188 183 180 181 172 167 157 153 132 78 134 150 161 164 151 151 98 118 152 136 161 154 135 136 134 135 137 138 136 137 138 139 139 140 140 139 139 139 140 139 139 138 138 138 138 139 139 137 136 136 136 137 137 136 135 136 136 136 136 137 138 137 138 185 171 50 23 27 13 12 61 125 88 28 12 26 20 17 12 19 19 23 18 22 26 50 117 137 172 173 177 170 170 191 170 171 168 156 189 193 159 106 80 99 115 116 60 25 84 133 157 159 173 161 124 108 142 144 157 149 136 127 133 127 137 166 174 156 143 129 141 143 139 145 160 145 154 107 148 167 169 168 152 134 76 77 108 180 163 142 134 137 136 135 137 137 136 137 139 137 139 140 139 139 138 139 140 140 140 139 139 139 139 140 140 137 136 136 136 136 136 136 135 136 136 136 136 137 138 138 139 196 171 66 24 15 20 12 74 113 25 28 27 15 24 12 24 15 20 24 10 19 32 40 81 126 164 175 177 174 169 178 181 168 168 156 184 193 161 115 80 84 104 144 106 44 45 137 159 161 169 188 142 132 137 146 132 92 68 76 77 78 77 94 102 79 78 80 77 67 75 74 100 117 141 157 167 189 166 151 155 149 68 90 143 153 140 148 135 139 137 138 138 138 137 138 140 137 139 140 139 139 138 139 140 141 141 140 139 139 140 140 140 138 137 137 137 137 137 137 136 136 136 136 136 137 138 138 139 223 199 135 27 20 17 9 73 52 33 24 22 28 13 15 21 19 22 22 14 15 36 50 46 120 150 177 178 180 169 181 179 170 168 155 184 183 135 112 82 67 61 54 60 44 40 116 146 159 171 182 181 131 125 99 80 67 91 101 106 118 112 93 87 99 131 162 155 145 156 153 142 130 147 151 185 177 173 158 157 131 81 97 138 166 167 138 142 137 138 138 140 140 139 139 139 140 139 139 139 139 139 139 139 143 141 139 138 139 139 138 137 139 139 139 139 137 136 136 135 134 135 136 137 137 137 139 140 150 168 172 64 11 23 2 99 60 17 26 21 20 20 17 21 20 21 21 16 18 35 47 45 94 154 175 176 177 172 181 179 171 165 156 187 172 117 110 110 61 31 15 32 42 45 105 145 153 152 176 182 157 136 141 137 145 147 151 165 177 171 168 178 178 165 172 177 171 160 150 151 161 138 172 179 175 157 167 153 119 82 107 142 174 163 142 137 139 139 139 140 139 139 139 139 141 140 139 139 139 139 139 139 140 139 139 138 137 137 138 139 139 139 139 139 137 136 136 136 135 136 137 138 138 138 139 140 79 12 91 115 25 12 24 149 95 11 22 19 14 23 14 21 21 19 18 17 20 34 45 43 64 147 176 178 176 173 179 175 168 165 157 184 164 109 115 129 116 64 33 33 41 38 77 131 143 154 179 169 158 135 162 148 145 152 153 151 165 181 178 165 168 160 162 153 140 140 151 169 166 138 167 184 157 157 154 143 94 88 121 145 163 142 141 141 141 140 140 139 139 140 141 140 141 140 140 139 139 139 139 139 137 138 139 138 136 135 137 139 139 139 139 139 137 137 136 136 137 137 138 138 138 138 139 140 28 22 18 28 18 18 14 114 131 29 22 21 19 21 10 23 20 19 17 13 17 34 46 41 45 120 178 180 176 175 178 173 167 167 155 179 164 119 123 125 146 141 120 62 29 27 56 119 144 146 161 176 164 130 157 161 147 139 136 135 128 121 115 107 99 104 117 122 136 162 167 160 156 140 161 157 158 142 145 126 88 100 122 139 145 129 142 143 143 141 141 140 140 140 141 141 142 141 141 140 140 139 139 139 138 138 138 138 136 136 137 138 139 139 139 139 138 137 137 136 137 137 138 138 137 137 138 139 19 27 18 24 18 15 9 24 109 43 25 21 27 18 14 26 19 20 19 13 17 39 49 41 44 83 166 179 175 175 178 170 166 164 151 177 168 126 125 120 139 130 147 148 116 54 28 97 131 143 142 166 147 141 153 156 166 159 153 138 116 101 101 103 105 95 103 115 133 151 148 140 159 155 151 150 147 136 143 105 102 118 113 135 139 142 144 139 142 142 142 142 142 141 142 141 143 142 141 141 140 140 140 140 142 140 138 138 138 138 138 137 139 139 139 139 138 137 137 136 136 136 137 137 136 136 136 137 23 17 23 18 11 18 19 18 40 33 24 16 25 17 20 21 19 19 19 18 25 44 51 41 48 56 133 170 172 173 178 166 169 162 148 178 168 120 121 124 141 131 133 134 130 100 56 87 121 148 137 142 127 157 161 144 137 147 146 141 151 168 174 167 162 152 153 144 133 135 144 158 162 160 147 163 133 139 141 82 109 129 120 138 137 145 142 139 142 143 144 144 144 143 142 141 144 142 142 141 141 141 141 141 144 141 139 138 140 140 139 137 140 140 140 139 138 137 136 136 135 136 136 136 135 135 136 137 27 12 20 29 23 11 2 13 7 19 23 13 19 16 19 15 18 15 21 30 39 48 48 43 48 50 87 156 166 170 176 166 168 164 152 177 164 119 124 127 128 126 129 129 121 96 57 85 137 130 129 144 151 156 161 168 167 159 155 171 194 194 188 190 191 185 190 187 182 182 180 179 158 144 152 138 144 128 147 87 108 134 136 143 137 142 138 144 143 144 143 143 143 144 143 144 142 142 142 142 141 142 142 142 143 142 140 140 140 140 140 140 141 141 140 140 138 137 136 135 135 136 137 137 136 136 137 138 17 26 19 22 8 12 16 14 20 16 23 16 19 16 20 16 18 11 21 42 51 50 46 45 45 53 53 145 161 170 175 164 163 171 159 174 160 126 130 122 124 132 132 130 131 120 71 70 145 123 133 137 159 145 151 158 145 162 162 165 186 196 195 198 194 180 184 192 192 186 175 177 151 144 134 114 137 135 144 125 116 136 143 139 142 144 142 145 145 143 143 144 143 144 145 146 142 142 142 142 142 142 142 143 140 141 142 141 139 139 140 142 142 141 141 140 138 136 135 135 136 137 137 138 137 137 138 139 18 21 22 19 15 12 11 13 20 18 17 18 18 16 16 18 14 25 42 51 51 46 44 44 45 51 57 92 159 160 165 149 156 188 147 155 151 121 140 124 126 131 129 136 127 133 61 38 135 146 121 131 139 146 133 151 157 161 158 154 169 170 185 188 172 174 184 194 193 178 163 159 133 121 108 127 126 121 138 128 130 137 142 145 145 143 146 143 146 142 140 142 143 143 143 146 144 143 143 143 143 143 143 143 141 141 141 141 141 141 141 141 143 142 142 141 140 139 139 138 138 138 138 138 138 138 138 138 19 21 22 20 15 12 12 14 14 24 25 15 13 17 19 19 25 35 47 53 51 48 45 43 45 50 58 61 134 160 170 136 152 195 148 137 131 118 139 121 141 114 127 144 131 129 64 77 167 143 110 114 129 138 122 139 156 155 147 144 159 158 165 161 150 163 168 165 165 164 152 135 118 84 95 79 95 146 135 49 143 130 135 138 145 147 138 143 141 139 141 142 144 142 145 147 143 143 143 143 143 143 143 143 142 142 141 141 141 141 141 142 143 142 142 141 139 139 138 138 138 138 138 138 138 138 138 138 19 21 21 20 15 13 14 14 17 28 23 15 19 22 20 21 40 44 52 55 53 49 45 43 46 54 59 45 97 158 167 139 147 196 145 125 117 111 135 122 134 131 130 154 121 91 28 175 172 153 124 89 80 93 94 110 140 148 147 144 146 135 134 127 132 146 149 142 138 139 125 103 79 66 53 68 127 165 125 16 57 121 141 141 143 145 151 141 140 141 142 143 142 141 142 146 143 143 143 143 143 143 143 143 144 143 142 141 141 141 142 142 143 143 142 141 139 138 138 137 138 138 138 138 138 138 138 138 19 21 21 20 15 13 14 16 16 21 17 15 19 17 20 33 49 51 53 55 53 50 46 43 47 55 54 51 65 146 156 150 146 191 143 125 117 105 128 130 143 119 136 142 109 23 34 230 147 158 144 96 53 40 53 69 99 118 124 118 106 90 93 99 109 108 104 101 100 96 88 77 55 39 52 121 147 160 125 11 22 36 107 142 137 142 142 148 143 144 144 144 144 143 143 143 143 143 143 143 143 143 143 143 145 144 142 141 140 141 141 142 143 143 142 141 139 138 138 137 139 139 139 139 139 139 139 139 20 21 21 18 15 14 14 17 16 22 20 17 17 21 35 56 52 52 54 54 53 50 46 43 44 51 50 51 50 126 151 143 152 191 150 124 116 101 131 130 138 116 140 124 21 10 116 197 118 153 143 129 87 35 39 45 61 73 66 56 44 41 52 63 66 54 42 40 50 60 68 74 62 58 115 147 151 162 112 12 19 14 20 92 148 147 144 135 145 144 144 145 147 147 146 145 144 144 144 144 144 144 144 144 145 144 143 141 141 141 141 141 143 143 142 142 140 139 138 138 139 139 139 139 139 139 139 139 20 22 22 19 14 12 14 17 19 21 20 16 21 39 55 57 50 51 53 54 51 49 46 42 44 48 52 43 52 102 156 124 160 196 163 113 105 99 135 115 127 124 101 26 12 9 162 175 89 157 143 142 120 71 68 63 60 57 31 25 21 23 23 24 24 23 18 22 43 72 86 86 75 107 145 139 159 160 117 17 9 15 15 18 83 144 147 156 146 147 148 146 146 147 144 142 145 144 144 144 144 144 144 144 145 145 144 143 142 141 141 141 143 143 143 142 141 140 140 139 140 140 140 140 140 140 140 140 20 21 20 18 14 12 13 16 18 17 25 32 34 49 56 44 47 51 54 55 53 48 44 42 43 49 52 47 55 77 157 111 165 197 172 103 97 92 132 92 119 49 13 19 14 13 190 180 63 164 149 135 125 102 93 93 86 79 48 39 30 27 19 13 15 24 34 46 71 101 106 96 94 118 133 147 145 165 139 7 21 9 13 15 6 52 130 146 141 146 150 149 146 145 142 140 144 144 144 144 144 144 144 144 146 146 146 145 144 143 141 141 143 143 143 143 141 141 141 141 140 140 140 140 140 140 140 140 22 22 20 17 12 10 13 16 19 28 59 74 56 51 58 54 47 50 56 57 53 47 42 40 40 52 45 58 48 57 149 109 166 192 173 101 95 84 125 76 19 15 11 10 17 6 201 209 54 160 150 137 133 114 89 107 103 103 74 57 33 27 24 29 26 35 46 61 86 106 104 92 105 140 135 147 149 209 123 13 12 18 15 13 15 11 26 70 128 139 149 149 146 146 146 147 144 144 144 144 145 144 145 144 148 147 147 147 146 144 142 141 142 142 143 143 142 142 142 142 141 141 141 141 140 140 140 141 21 24 25 9 16 12 13 12 29 110 169 139 53 60 57 49 51 54 57 57 52 47 41 40 44 49 45 47 56 56 138 112 159 190 165 101 65 40 17 15 12 14 11 9 14 16 209 221 67 139 153 138 132 112 103 105 108 103 85 67 59 52 41 35 37 43 55 69 100 112 93 96 118 143 130 153 181 226 118 14 12 13 15 14 14 13 15 15 10 40 94 142 155 145 141 151 146 144 141 145 147 146 143 141 145 147 147 146 143 143 144 144 144 143 143 143 143 143 143 144 140 139 139 139 140 141 141 141 21 18 21 15 21 14 9 39 134 177 168 135 90 57 57 50 42 47 54 57 56 50 44 40 36 50 52 48 45 49 131 126 122 115 69 21 11 11 7 12 11 13 13 12 18 16 208 224 67 119 162 135 122 121 113 99 108 100 82 76 67 59 52 58 68 67 75 92 109 104 89 107 132 131 148 154 230 230 98 13 12 13 13 13 12 12 14 13 16 20 17 27 77 137 155 138 146 146 145 144 143 144 148 153 148 148 148 147 146 143 143 143 144 143 145 145 145 145 143 144 142 142 141 140 140 141 140 141 19 15 23 18 14 10 21 109 172 190 162 139 142 77 55 54 48 52 55 55 51 45 39 35 45 47 44 50 58 56 75 49 15 17 10 7 13 18 14 13 14 14 13 14 18 11 203 228 42 96 150 156 133 118 104 115 109 97 80 84 73 65 62 82 85 78 84 101 112 103 96 118 126 139 151 192 237 228 83 12 10 11 12 12 11 11 12 13 16 8 15 28 19 14 60 124 148 145 143 149 152 150 145 142 146 143 143 146 146 145 146 149 142 143 145 146 146 145 144 142 144 144 142 140 140 142 140 139 23 17 21 17 9 13 31 165 176 177 162 140 169 119 71 61 47 50 53 55 52 48 42 39 33 48 54 45 26 17 18 14 13 14 14 11 7 9 13 15 15 13 12 13 16 9 192 235 63 74 155 153 130 131 118 119 113 102 84 89 78 73 73 98 97 93 93 106 117 115 113 126 131 138 165 233 223 227 56 13 10 11 12 12 10 10 11 12 17 20 20 17 16 20 23 23 47 101 144 143 136 145 152 146 153 148 148 150 147 141 140 143 143 143 144 146 145 144 143 142 144 142 141 140 140 141 140 140 25 17 15 16 15 22 29 166 181 183 168 151 179 161 107 51 49 52 55 55 49 42 31 26 29 25 19 14 14 18 10 7 9 10 15 17 15 17 17 10 13 10 11 16 17 12 170 234 186 47 127 155 139 122 122 127 121 112 96 94 86 84 89 108 108 102 102 112 117 112 116 128 143 124 205 237 232 224 19 17 12 13 12 12 10 10 12 13 16 11 14 22 25 21 16 18 24 9 28 93 144 154 147 146 144 143 146 151 149 145 143 146 144 144 144 143 143 143 143 144 142 142 139 139 141 141 141 141 19 19 18 16 16 20 30 164 169 189 169 169 183 189 171 74 53 50 42 34 27 20 13 9 11 9 11 10 11 15 11 13 15 14 14 11 8 13 16 8 10 11 13 17 16 9 127 217 238 124 87 141 135 125 130 118 126 115 103 93 94 91 100 114 104 98 102 122 120 102 114 134 117 161 231 227 237 188 8 17 13 12 12 11 10 10 12 13 14 17 20 20 18 17 20 25 19 23 22 17 26 61 107 140 149 150 151 149 144 142 146 148 146 145 145 143 143 142 142 143 141 140 140 139 140 141 140 140 16 20 20 14 12 11 36 171 176 189 176 174 185 202 205 99 31 27 19 14 11 13 14 14 18 11 12 9 5 6 13 13 3 10 17 17 15 15 15 15 12 13 11 15 11 9 95 207 238 211 71 120 144 121 117 127 128 113 103 91 100 92 104 115 105 103 107 128 123 106 116 130 123 218 231 230 230 120 16 14 13 12 13 12 10 10 12 13 13 14 18 23 23 20 19 20 18 21 21 20 17 19 24 28 49 76 112 136 148 151 148 143 146 145 146 145 143 142 142 141 143 141 139 138 140 140 139 139 19 18 16 13 17 11 37 162 186 177 187 178 194 202 168 51 15 14 13 15 16 15 13 10 16 9 14 16 18 12 20 8 16 15 10 10 14 11 9 13 13 13 11 13 12 20 89 219 232 242 120 70 135 142 124 120 131 111 103 91 107 93 106 116 110 112 110 121 115 101 106 100 184 233 233 232 233 67 11 14 13 13 12 11 9 9 10 12 14 20 21 17 19 25 24 17 23 22 19 18 23 26 23 18 23 26 25 24 41 81 126 151 146 146 147 146 146 144 141 139 144 143 140 139 140 139 139 138 22 16 19 15 16 11 35 159 184 185 186 178 164 131 31 17 14 14 13 14 13 13 13 12 13 13 14 14 13 12 12 12 16 14 13 12 12 12 12 10 17 13 13 11 15 11 77 223 223 246 223 65 107 133 136 118 126 125 102 96 94 71 112 105 103 97 113 123 105 100 75 151 227 243 233 230 225 23 15 10 11 11 12 12 10 10 11 13 12 14 17 20 21 22 23 23 22 22 23 23 22 21 22 21 24 24 26 24 18 16 26 40 104 147 147 142 143 148 138 145 141 141 141 141 139 140 140 139 19 19 19 19 18 9 26 161 175 194 162 29 18 19 2 21 14 14 15 15 14 14 13 13 14 13 14 14 13 12 13 12 12 12 11 11 14 13 13 13 11 10 16 16 16 15 56 229 225 238 245 210 91 124 127 132 127 133 106 90 100 68 84 89 86 95 97 129 106 69 120 228 240 238 246 230 148 15 9 13 11 12 13 12 10 10 13 14 15 17 18 20 20 21 21 22 22 24 24 23 22 23 23 23 27 23 21 25 24 21 22 24 24 35 104 149 143 141 144 138 142 141 142 141 140 138 139 139 17 21 14 18 19 12 21 167 196 173 30 21 11 14 19 14 15 15 16 16 14 14 15 14 13 14 14 13 14 12 12 11 11 12 12 11 13 14 14 14 15 15 16 17 11 17 33 223 236 238 253 241 220 104 135 141 136 131 112 98 100 67 85 99 84 75 118 116 91 100 229 236 251 239 243 228 61 9 9 13 12 13 13 13 11 11 13 14 17 18 17 19 19 20 21 22 21 22 23 23 22 22 22 23 27 21 20 25 27 23 19 17 19 22 31 63 133 147 143 142 141 142 142 141 139 137 139 139 20 21 12 16 17 16 17 164 184 78 13 23 7 13 12 12 15 16 15 16 15 15 14 14 12 12 13 14 12 12 13 11 13 12 12 13 13 13 12 12 15 15 15 15 10 16 20 167 224 248 246 253 249 198 99 144 162 139 124 114 94 66 100 93 84 90 129 117 84 226 241 250 252 248 236 199 19 10 12 12 14 13 14 13 13 13 15 16 14 16 18 19 20 21 22 20 21 22 22 22 21 21 22 23 26 24 24 23 22 20 21 25 27 20 18 23 21 49 125 144 140 141 141 139 139 137 138 138 22 20 15 16 13 15 14 158 172 15 30 5 8 24 15 16 15 15 17 17 16 16 15 14 12 12 13 12 13 13 11 12 13 13 13 12 13 13 13 13 9 12 16 16 17 14 14 79 219 227 254 252 249 248 167 108 153 150 144 132 108 87 122 92 84 136 122 84 228 250 248 253 250 254 243 126 16 15 15 14 15 14 13 14 13 14 16 16 14 16 19 20 21 21 21 19 20 21 22 22 22 21 21 22 22 23 23 22 21 20 23 27 25 30 19 19 19 21 59 147 140 141 141 140 139 136 136 138 42 17 18 21 11 14 17 152 93 13 6 24 16 10 15 19 15 16 15 16 15 16 14 14 13 13 13 12 12 13 12 11 11 11 12 12 13 13 14 15 13 14 15 14 19 15 20 25 186 223 240 251 254 251 248 149 124 155 168 151 137 118 141 117 127 133 103 230 251 246 253 253 252 253 235 47 20 17 14 17 16 16 13 14 14 15 16 16 19 21 21 23 23 21 21 20 22 22 23 23 21 22 23 23 23 21 21 22 22 21 20 19 16 15 30 23 17 18 16 121 141 142 141 140 138 136 136 138 103 29 17 20 13 13 17 135 43 18 15 8 17 18 18 9 14 15 14 15 14 14 13 13 13 13 12 12 12 12 11 11 11 13 14 13 13 12 13 13 17 17 14 11 16 14 23 17 121 226 240 254 251 254 249 243 143 132 155 149 146 134 138 130 133 149 229 247 247 254 254 249 254 249 169 14 19 17 18 17 19 17 14 14 16 16 18 18 22 22 20 20 21 20 21 20 22 23 23 22 22 22 22 23 20 20 20 20 22 21 18 15 27 15 29 16 13 23 19 69 141 141 141 141 138 137 137 137 170 50 15 17 14 13 12 112 13 7 18 19 15 14 16 18 14 14 14 15 14 14 13 13 13 13 13 13 13 11 12 11 14 14 14 13 11 11 11 12 8 12 12 12 15 13 17 23 81 229 229 254 253 249 253 250 235 140 160 185 203 208 203 195 188 155 238 252 253 251 251 254 254 247 90 20 19 14 23 16 19 17 14 15 16 17 17 18 20 19 17 17 17 19 21 22 21 22 23 21 21 20 21 21 19 21 20 19 17 18 19 21 18 22 18 18 30 12 21 30 141 141 141 140 139 138 136 137 186 141 19 17 14 14 21 53 14 15 16 17 17 16 14 14 17 15 15 14 13 13 13 14 12 12 13 13 13 13 11 10 12 13 14 14 14 13 12 12 12 12 12 14 14 15 18 19 37 214 232 245 251 252 254 252 249 244 238 229 232 222 184 160 49 23 60 224 252 254 254 251 246 213 23 22 23 19 13 20 17 18 18 18 18 18 19 19 22 22 22 21 20 19 19 19 21 21 20 21 22 22 20 18 19 18 18 18 19 18 18 17 15 18 19 19 18 16 13 14 128 138 138 144 137 142 135 140 183 167 71 11 15 13 19 30 14 14 15 16 16 15 15 14 15 15 15 14 14 14 14 14 13 13 13 13 13 13 12 12 12 13 14 15 13 12 12 12 13 13 14 13 13 13 15 16 25 170 237 242 245 253 249 253 249 136 79 73 50 34 29 24 24 25 15 38 205 249 254 251 254 116 22 15 17 19 17 20 19 19 19 19 20 21 20 20 20 20 20 21 21 21 22 22 22 21 21 22 22 22 20 19 23 22 21 20 19 18 17 16 15 16 19 19 18 15 14 14 84 135 145 136 140 139 137 143 177 178 133 47 14 18 24 16 14 13 14 15 15 14 13 14 14 15 14 14 14 13 13 13 15 15 14 13 13 13 12 12 11 12 14 14 13 12 13 13 12 13 14 13 13 13 13 14 22 118 225 243 253 247 254 249 111 23 7 29 18 17 24 18 16 20 31 18 43 216 253 244 225 39 24 18 17 22 20 20 20 20 20 20 21 21 20 20 21 21 22 22 22 22 23 22 24 23 23 24 24 24 23 22 25 24 22 20 19 17 15 14 15 17 18 18 17 15 15 16 42 146 146 136 141 136 141 140 176 168 172 119 18 22 22 16 14 13 14 15 15 15 13 14 14 15 15 15 14 14 14 13 16 16 15 13 13 13 13 13 12 13 14 14 12 12 12 13 10 11 13 13 13 13 14 14 18 82 224 251 253 250 248 104 19 15 26 27 23 27 20 15 27 14 10 11 30 48 227 249 136 24 21 24 20 21 20 19 20 20 20 20 21 20 19 19 24 24 24 23 22 21 21 21 23 23 23 24 24 24 23 22 22 21 20 19 18 17 16 15 16 17 17 17 16 14 15 15 22 140 135 143 139 137 146 136 181 163 184 159 45 20 14 17 15 15 14 15 15 15 14 15 14 15 15 15 15 15 14 14 17 16 15 14 13 13 13 13 13 13 14 14 12 11 11 12 10 11 12 13 13 13 14 14 20 36 201 237 248 254 99 13 16 23 21 11 16 21 16 27 20 20 23 21 19 15 83 229 48 21 18 21 19 17 21 17 21 21 21 20 21 21 20 20 25 25 24 24 23 21 21 20 20 21 21 21 21 21 21 21 21 20 19 18 19 18 17 16 17 16 17 16 15 13 14 14 14 90 138 143 136 140 145 139 179 166 175 150 85 23 8 16 15 15 14 14 15 15 14 15 13 15 15 16 16 15 15 14 16 15 15 14 13 13 13 13 13 14 14 13 11 10 11 11 11 12 13 13 12 12 13 13 14 27 125 240 254 97 20 22 19 11 15 15 14 17 15 19 16 17 14 16 10 33 13 67 23 13 27 19 20 18 23 18 22 22 21 21 22 22 21 21 23 23 23 24 23 22 21 21 20 21 22 22 21 21 22 22 22 21 20 20 19 19 18 17 16 16 17 16 15 13 14 14 15 35 146 140 141 144 137 147 176 167 167 146 115 22 15 11 15 15 14 15 14 14 14 15 13 14 15 15 16 15 14 14 14 14 14 14 14 13 13 13 12 13 13 13 11 11 11 12 12 12 13 12 12 12 14 15 11 28 67 238 117 18 11 11 15 10 21 19 9 15 17 13 18 20 22 16 30 9 20 24 27 15 31 22 20 22 21 19 21 21 21 21 22 22 21 21 22 23 23 23 22 21 20 19 21 22 23 22 21 21 23 24 22 21 20 19 20 19 18 17 14 15 17 15 15 13 14 13 17 15 119 141 143 142 137 148 179 167 166 161 121 19 22 6 14 13 13 14 13 13 13 14 12 13 14 15 15 15 14 13 13 13 14 14 14 14 13 12 11 12 13 12 11 11 12 13 10 11 11 12 12 14 16 18 21 10 32 105 15 19 18 10 14 12 17 14 11 13 12 19 16 16 21 19 12 30 20 23 18 22 20 21 14 23 16 20 19 19 20 20 21 21 21 21 24 24 24 23 22 19 18 17 17 19 20 20 18 18 20 21 19 18 18 18 19 19 19 18 13 14 16 16 15 13 13 13 10 17 71 143 141 138 146 143 180 164 170 155 126 30 12 15 13 12 11 13 12 14 15 17 13 13 13 13 13 14 14 15 15 14 13 13 14 14 13 12 13 13 12 12 11 11 12 12 14 14 13 12 12 13 14 16 16 16 28 15 16 25 10 16 23 18 20 16 23 11 21 39 29 34 44 21 19 18 19 23 21 20 19 20 22 22 21 20 21 21 21 21 22 23 22 23 23 22 21 22 23 23 23 22 19 19 18 16 16 17 20 22 21 19 18 17 17 18 18 18 14 14 14 15 15 15 15 16 17 15 34 144 143 140 146 142 182 168 169 155 127 26 10 14 13 12 13 14 13 13 13 14 13 14 14 13 14 14 15 15 14 13 13 13 14 14 13 12 13 13 13 12 11 12 12 12 13 13 13 12 11 11 13 15 17 16 25 17 11 13 8 18 12 15 13 127 169 198 214 204 210 200 184 157 28 16 19 16 19 19 18 19 21 22 21 20 20 19 18 17 18 18 19 19 20 20 20 22 24 24 24 22 19 19 18 16 16 17 20 22 22 20 18 17 17 17 16 17 15 16 14 14 13 13 13 13 12 11 22 122 140 143 144 145 180 170 165 154 129 20 10 13 13 13 14 14 13 12 11 11 13 14 14 14 14 15 15 15 14 13 12 12 13 14 14 13 14 14 13 13 12 12 13 13 13 13 13 12 11 11 13 14 12 17 21 17 15 23 25 16 25 21 103 203 239 251 254 254 251 251 241 222 90 25 17 15 17 17 18 19 20 21 22 21 20 19 17 15 15 16 16 17 18 19 20 23 24 24 23 21 19 19 18 16 16 17 19 21 22 20 18 17 16 16 15 14 17 17 15 15 14 13 12 12 10 11 10 88 138 144 142 147 177 171 162 154 126 15 13 13 13 14 13 13 12 12 11 11 14 14 14 14 14 15 15 15 13 12 12 12 13 14 14 13 14 14 14 14 13 13 13 13 14 15 14 13 12 12 13 15 15 18 16 17 16 20 26 13 17 28 174 220 254 254 247 254 249 248 236 221 115 24 15 21 17 17 18 19 20 20 22 22 21 20 18 16 17 17 17 17 18 20 21 23 23 22 20 18 18 18 17 16 15 15 17 19 20 19 17 16 16 16 15 14 17 15 15 14 13 12 11 11 13 12 9 55 143 145 142 146 177 172 162 155 114 13 15 14 14 14 12 11 11 12 13 13 13 13 13 14 14 14 14 14 14 13 12 12 13 13 14 13 14 14 14 14 13 13 13 13 14 14 14 14 12 12 13 15 17 14 13 23 22 16 24 28 21 102 201 248 254 253 254 251 253 252 238 225 63 17 18 16 17 18 20 21 20 21 21 22 20 20 20 20 21 20 19 19 21 22 23 23 21 18 16 15 18 18 17 15 14 14 16 17 19 17 16 16 16 16 15 14 14 15 13 13 12 13 12 12 15 12 14 31 144 144 143 144 178 170 164 156 95 12 14 13 13 14 12 11 11 12 14 15 13 13 13 14 14 14 14 14 17 15 13 12 13 13 13 13 14 14 14 14 13 13 13 13 12 13 13 13 12 12 12 14 12 16 17 18 26 25 19 19 52 163 221 253 252 249 254 253 248 239 230 171 21 18 26 13 19 20 22 22 21 21 21 22 20 20 21 22 23 22 21 20 24 24 23 21 18 15 14 14 18 18 17 16 14 14 16 17 17 16 16 16 16 16 15 15 14 14 13 12 12 13 12 13 14 10 15 16 134 145 145 144 177 167 165 155 76 13 12 13 13 13 14 14 12 13 14 14 13 13 14 14 15 14 14 14 19 17 15 13 13 13 12 12 13 13 14 14 13 13 12 12 12 12 13 13 12 12 13 14 14 20 21 13 19 19 16 42 147 206 248 251 254 254 254 250 254 235 209 75 20 21 23 21 21 22 24 23 21 20 20 21 22 22 22 22 23 23 22 22 24 24 22 19 15 14 15 16 20 19 19 17 16 15 16 17 17 17 16 15 15 15 14 13 13 14 13 12 12 12 13 13 13 12 13 14 114 146 145 146 175 163 165 155 63 14 14 15 14 13 14 14 14 13 12 11 13 14 14 15 15 15 15 14 20 18 15 14 13 12 12 11 13 13 14 14 13 13 12 12 12 13 14 14 13 13 14 15 17 9 20 26 27 15 46 146 205 233 252 253 248 253 252 254 241 204 174 21 25 28 16 25 22 24 25 24 21 21 21 22 25 25 24 23 24 23 23 24 23 23 21 17 14 13 15 17 21 22 20 18 18 17 18 19 19 18 17 15 15 14 13 12 13 13 13 13 11 12 12 11 15 15 11 16 99 148 143 147 168 167 165 159 51 12 16 10 16 15 13 12 12 12 12 13 11 9 19 4 24 15 10 16 16 12 11 18 7 13 13 9 7 21 14 11 13 14 18 12 16 16 15 14 13 13 14 15 19 11 25 23 24 21 153 192 228 251 249 253 253 253 251 248 211 189 61 26 27 23 18 25 27 25 23 23 20 17 17 20 24 15 24 21 19 24 20 19 22 17 19 11 22 13 19 19 20 13 20 14 20 22 19 18 19 16 17 16 16 16 11 13 15 15 15 14 13 13 13 13 17 12 13 18 80 147 151 146 170 167 163 159 39 10 16 12 15 14 13 13 12 13 12 14 22 9 12 26 11 11 31 12 17 16 16 22 10 15 16 16 10 7 11 13 22 23 4 24 15 15 15 13 12 12 13 14 14 19 18 17 24 33 127 195 222 253 253 252 253 253 243 215 158 113 35 20 22 26 20 23 25 24 21 22 21 18 18 20 23 17 22 28 26 23 26 26 21 20 16 14 8 15 19 19 25 14 28 10 14 13 17 19 17 13 13 15 17 17 15 16 14 15 14 14 12 14 13 14 15 13 15 16 67 149 151 148 174 163 160 158 25 9 16 15 14 12 12 13 12 13 12 12 7 14 131 138 140 122 111 92 84 72 53 41 20 18 13 10 19 15 18 14 8 21 9 14 15 15 14 13 12 11 12 13 11 24 11 16 20 30 76 183 217 242 249 253 253 247 209 150 113 47 21 23 22 29 22 22 24 22 22 23 23 20 19 21 19 26 19 20 19 20 20 14 17 22 22 25 12 22 21 19 12 21 18 19 24 22 13 14 18 16 15 16 15 11 10 12 12 13 13 13 12 14 13 14 15 16 16 16 50 152 150 152 173 162 159 153 15 11 15 15 12 12 13 14 13 13 12 12 29 8 133 186 253 253 245 144 147 154 161 178 182 185 169 157 134 128 100 77 15 3 25 17 15 15 14 13 12 12 12 13 13 19 11 22 20 20 31 151 193 235 252 249 240 218 156 102 84 25 25 27 24 25 23 21 21 22 23 23 23 21 21 21 14 27 23 22 24 22 26 75 58 64 68 68 67 66 66 68 72 112 20 17 15 18 10 20 13 14 12 18 17 11 14 16 11 12 13 12 13 14 13 15 14 17 16 13 35 150 148 150 169 161 162 139 13 13 13 13 11 12 13 14 13 13 12 12 10 32 142 196 249 251 209 122 146 151 150 160 166 181 182 185 178 179 169 171 96 18 6 9 15 15 15 14 13 13 15 15 16 13 18 25 24 23 19 100 180 218 245 235 213 154 106 121 49 20 24 23 24 16 21 22 20 22 23 24 23 22 22 20 31 18 14 20 29 16 17 140 84 89 83 83 90 89 87 99 89 177 30 17 19 20 18 18 20 18 11 17 16 8 12 10 12 13 13 13 14 14 15 15 13 16 13 11 24 136 145 147 168 161 164 114 14 16 12 13 13 12 13 13 12 13 12 12 7 42 168 186 213 226 202 161 143 165 175 183 179 182 178 183 182 173 169 175 153 78 18 14 14 15 16 15 14 14 16 17 19 13 20 18 22 27 22 43 178 194 223 219 168 130 140 136 22 20 23 20 26 15 21 21 21 23 23 23 23 23 21 20 18 21 30 17 20 22 21 153 85 87 88 88 90 90 91 93 98 175 41 19 18 10 22 13 10 15 13 23 20 7 14 14 13 13 13 14 15 15 16 15 16 16 18 15 21 119 147 148 167 165 164 86 16 15 11 13 14 14 14 13 12 13 12 13 22 58 171 211 206 193 222 149 139 170 188 200 199 200 193 198 197 191 187 181 190 151 81 8 13 15 16 16 15 15 17 18 19 17 18 18 20 22 25 17 134 199 217 184 135 155 194 79 19 21 23 20 23 21 21 21 22 25 25 23 21 22 21 19 24 19 18 27 92 156 105 110 98 83 120 98 101 87 116 81 128 140 91 126 103 92 77 69 74 100 109 105 63 15 12 15 13 15 15 15 17 17 15 15 18 16 23 16 19 96 149 153 167 166 164 68 17 14 11 15 15 14 14 13 12 12 12 13 15 76 129 174 224 217 149 122 135 154 156 161 164 172 171 179 178 174 171 167 171 164 152 33 13 14 15 16 15 15 17 18 15 18 14 26 23 15 28 25 73 196 187 155 138 139 156 16 21 14 20 16 17 25 20 19 24 26 25 22 21 23 21 19 17 25 43 82 166 243 175 107 139 101 173 115 132 99 165 98 185 110 108 193 135 128 88 97 81 135 166 156 86 13 8 18 15 16 15 16 17 17 16 15 15 15 22 15 16 78 147 153 172 162 165 39 14 17 12 12 12 12 13 13 12 11 12 13 16 53 93 112 110 110 124 141 142 147 154 159 163 166 172 177 166 169 163 163 162 157 154 32 12 13 14 15 14 13 13 12 16 17 17 17 18 18 19 20 16 180 174 114 78 143 42 14 20 17 14 15 18 21 21 22 28 22 18 21 24 21 20 23 22 47 78 104 186 238 175 104 137 100 182 125 143 118 168 107 184 120 105 176 141 127 95 93 87 117 148 161 84 17 14 16 15 16 17 16 17 15 16 17 17 18 20 14 20 60 147 153 170 163 156 31 11 16 12 14 14 14 15 14 14 13 15 15 9 13 16 12 7 10 21 32 39 47 58 71 85 98 111 118 133 143 146 150 152 153 139 30 12 13 13 13 13 13 14 14 16 16 16 17 17 16 17 18 23 90 142 70 58 95 12 19 20 18 17 16 18 19 19 21 19 18 21 24 20 17 19 25 51 87 103 98 89 118 87 92 118 154 139 120 125 150 106 112 136 109 87 99 86 96 85 75 80 81 86 120 38 9 11 17 15 16 18 19 17 16 16 15 18 17 19 18 19 46 150 154 167 164 138 21 10 16 11 15 12 13 13 13 12 12 14 14 15 13 12 13 14 14 14 13 16 16 15 13 11 10 12 13 16 14 19 32 35 44 53 15 12 12 12 11 12 13 16 17 16 16 15 15 16 15 15 16 18 26 82 59 62 33 12 17 16 15 16 17 18 19 21 22 23 21 21 20 19 24 40 58 124 175 176 155 78 78 81 136 163 189 74 146 180 228 66 91 172 151 115 88 87 86 82 81 84 78 82 135 31 17 13 20 15 15 18 19 17 16 16 16 17 16 17 23 17 29 153 153 164 164 120 12 9 16 10 15 11 11 12 11 12 11 13 13 11 12 13 15 15 15 14 14 14 15 15 14 13 12 14 15 14 8 13 17 7 8 12 12 12 12 11 11 12 13 15 15 17 16 15 14 15 15 15 14 12 15 30 63 56 8 22 14 13 15 16 16 16 18 21 24 26 23 21 18 22 34 57 76 140 182 170 151 82 79 81 116 161 192 98 148 156 210 90 111 167 141 108 89 95 83 77 83 88 85 93 130 38 16 8 11 15 17 17 17 17 17 15 16 17 18 17 23 16 18 156 151 161 161 101 11 12 16 9 14 13 13 14 13 13 12 14 14 15 17 16 13 10 9 11 13 12 13 14 14 13 12 12 12 12 14 20 11 12 25 16 9 12 12 12 12 12 12 12 12 16 16 14 15 13 13 13 13 17 13 18 38 25 18 12 20 17 16 16 15 16 17 19 22 21 20 18 20 24 30 40 47 68 83 66 62 38 41 40 45 58 63 28 38 36 69 35 38 49 39 27 24 24 26 22 19 25 19 22 37 13 9 17 20 16 17 17 16 15 16 16 17 16 18 18 19 17 18 149 150 162 156 82 13 14 16 10 13 12 12 13 12 12 12 12 14 14 14 15 17 19 18 16 14 15 16 17 18 18 18 17 16 18 15 21 8 9 15 11 12 13 13 12 12 12 12 11 11 14 14 15 15 14 14 14 16 17 15 19 15 10 22 11 15 14 15 15 16 17 20 22 24 20 18 17 18 22 23 20 19 21 22 19 25 24 23 26 28 26 22 25 30 29 24 22 26 24 22 23 32 16 22 21 19 25 17 16 6 16 9 17 15 17 17 16 15 15 16 16 18 16 18 19 15 18 18 127 151 166 151 68 15 15 13 12 13 11 10 12 10 11 11 11 13 19 14 9 7 10 13 14 14 13 13 13 13 13 12 11 10 9 2 18 16 20 7 11 18 13 12 12 11 11 11 11 12 13 13 14 15 15 15 16 18 14 20 15 17 13 16 20 12 14 15 17 18 21 22 23 23 20 19 19 17 20 20 22 22 26 22 28 32 26 22 19 24 27 32 27 19 23 33 31 16 22 21 22 30 21 18 20 25 16 20 25 16 18 12 13 12 16 16 16 16 16 16 16 17 18 15 19 13 19 16 97 154 169 148 60 16 15 11 14 13 12 12 13 12 12 13 13 14 13 14 15 15 15 16 15 15 15 14 14 14 14 14 14 14 17 8 17 6 18 13 21 12 14 12 11 10 10 11 12 13 13 13 14 15 15 16 17 18 21 10 12 21 8 15 19 18 21 20 20 23 23 21 18 15 14 17 20 20 20 20 23 24 23 21 23 23 17 28 23 24 20 23 22 34 26 20 20 26 22 27 23 13 21 23 19 17 19 19 19 18 9 17 15 20 16 16 18 17 16 17 16 16 19 11 19 13 20 13 75 157 \ No newline at end of file diff --git a/Tests/images/hopper_8bit_plain.ppm b/Tests/images/hopper_8bit_plain.ppm new file mode 100644 index 00000000000..c39a92a0bd6 --- /dev/null +++ b/Tests/images/hopper_8bit_plain.ppm @@ -0,0 +1,4 @@ +P3 +128 128 +255 +20 21 67 17 18 62 9 10 54 21 23 62 24 26 65 24 27 60 13 17 44 16 19 54 29 28 86 30 27 94 21 20 80 17 17 71 25 25 77 24 25 73 20 21 69 25 25 77 29 29 93 31 30 96 25 26 83 17 19 70 14 18 65 15 19 66 15 21 71 22 27 82 24 28 91 23 24 88 15 14 74 29 25 75 26 16 51 116 105 122 240 225 228 180 157 143 216 171 142 196 157 128 193 180 172 115 117 129 17 20 51 32 36 74 32 32 70 27 13 28 83 42 14 207 165 125 83 70 51 19 21 20 36 37 57 120 117 146 209 200 229 243 230 248 255 245 244 252 240 226 250 238 224 245 239 225 239 236 227 194 198 201 36 47 69 44 59 98 82 103 160 67 92 156 64 96 155 64 99 155 69 106 159 66 103 158 77 108 172 71 104 175 68 101 182 55 91 169 69 104 170 62 98 160 61 96 164 67 104 174 65 104 173 61 104 173 59 103 174 55 102 172 55 102 172 57 102 167 58 103 158 62 102 163 65 101 179 70 102 185 67 103 177 66 103 173 66 100 172 66 100 172 68 102 176 67 103 177 68 104 180 67 106 181 65 104 181 63 105 181 61 105 180 62 104 180 63 102 179 65 101 179 67 100 179 66 99 178 63 101 176 63 102 177 63 102 177 64 103 178 64 103 178 64 103 178 64 103 178 64 103 178 64 103 178 64 103 178 63 102 177 63 102 177 64 103 178 65 104 179 66 105 180 66 105 180 65 102 180 64 101 179 66 102 178 69 105 179 76 110 184 81 115 186 84 117 188 85 118 187 86 119 186 86 119 186 84 119 187 83 118 186 79 116 187 78 115 186 77 115 188 78 116 189 20 21 67 17 18 62 10 12 53 21 23 64 23 25 64 23 26 59 12 16 43 15 18 51 27 26 83 28 26 91 20 19 77 16 16 68 24 24 76 24 24 74 21 22 70 25 27 78 27 27 89 30 30 94 24 25 81 16 18 67 14 18 63 14 18 63 14 20 68 20 25 80 16 20 81 25 26 90 20 19 79 25 21 72 32 25 59 138 126 146 187 172 177 111 87 75 208 164 137 90 50 25 57 46 42 86 87 105 57 60 95 25 28 71 16 15 57 36 21 40 90 46 21 203 160 125 85 71 58 29 29 37 15 15 41 28 27 61 84 74 109 151 139 163 212 197 200 251 238 230 254 241 232 249 240 231 241 236 230 206 205 210 39 41 62 29 37 73 78 90 140 77 94 148 78 100 150 61 85 133 52 75 119 60 83 127 73 93 144 79 98 156 76 97 162 77 102 166 72 101 157 70 101 158 73 106 173 63 100 171 56 96 168 62 104 176 60 104 177 56 100 173 58 100 172 64 108 173 67 110 165 63 103 164 62 98 174 64 99 180 67 103 177 66 103 173 67 101 173 68 102 174 69 103 177 68 104 178 68 106 181 67 106 181 67 106 181 64 106 180 63 107 180 63 105 179 64 103 178 66 102 178 67 100 179 67 100 177 65 103 178 63 102 177 63 102 177 63 102 177 63 102 177 64 103 178 64 103 178 65 104 179 65 104 179 65 104 179 64 103 178 64 103 178 63 102 177 63 102 177 63 102 177 64 103 178 68 105 183 67 104 182 67 103 179 68 104 178 74 108 182 78 112 183 83 116 187 85 118 187 87 120 187 87 120 187 86 121 189 84 119 187 81 118 189 80 117 188 79 117 190 80 118 191 20 21 65 18 20 61 11 13 54 22 24 65 23 25 64 21 24 59 10 14 39 14 17 48 25 26 80 26 26 86 17 18 72 14 16 65 22 24 73 23 25 74 20 22 71 26 27 81 25 25 85 28 30 87 24 26 77 16 19 64 14 19 61 15 20 62 14 21 67 19 25 77 26 30 91 22 23 87 16 16 76 26 22 73 36 28 65 123 114 135 107 94 103 81 56 49 202 160 135 96 58 37 38 25 32 27 28 56 18 21 64 26 28 79 41 39 89 30 14 40 106 61 42 209 163 137 69 53 54 17 15 37 25 24 64 32 29 76 25 17 64 43 32 66 61 47 60 211 197 197 246 232 231 250 236 235 246 234 236 231 221 229 143 134 151 31 25 51 37 31 67 39 37 74 35 34 66 26 26 54 51 49 73 39 35 58 43 37 63 52 48 75 43 42 73 57 63 95 70 84 119 82 104 151 73 102 168 80 116 192 60 101 180 69 112 191 62 103 182 63 102 181 65 101 177 65 102 172 66 102 162 66 102 162 68 105 176 73 109 185 68 105 176 66 104 175 67 104 175 68 105 176 69 105 179 69 105 179 68 106 179 66 106 178 69 109 181 66 108 180 66 108 180 65 107 179 66 106 178 66 104 177 66 102 178 66 102 178 66 104 179 65 104 179 63 102 177 63 102 177 63 102 177 64 103 178 65 104 179 66 105 180 65 104 179 64 103 178 64 103 178 64 103 178 64 103 178 63 102 177 63 102 177 63 102 177 69 106 184 68 105 183 68 104 180 68 104 178 71 105 179 76 110 181 81 115 186 85 120 188 85 120 188 86 121 189 86 121 189 85 120 188 82 119 190 81 118 189 81 119 192 82 120 193 18 20 59 17 19 58 12 14 55 23 25 66 23 25 66 20 23 58 9 13 38 14 17 48 26 28 77 27 28 84 17 19 68 14 17 62 22 24 73 23 25 76 20 21 75 26 27 83 24 26 83 29 32 85 25 27 75 17 20 63 15 20 58 16 21 61 15 22 68 18 24 76 29 33 94 18 22 85 15 15 75 26 23 76 29 23 61 75 65 90 50 36 49 98 75 69 210 170 144 88 52 30 32 20 32 22 22 56 24 26 74 21 22 76 33 30 83 32 13 41 90 45 26 212 166 140 73 56 66 22 18 51 19 17 64 27 24 77 31 24 75 37 26 66 39 26 44 200 185 192 255 242 249 155 140 147 138 123 130 183 167 177 67 51 64 34 20 37 47 33 56 37 23 46 33 20 40 87 74 91 135 121 134 132 116 127 85 68 78 63 47 58 54 43 57 47 45 59 43 46 63 31 42 70 62 80 130 72 97 163 78 108 180 62 95 172 66 99 178 68 101 180 67 100 177 66 100 171 72 108 168 78 114 174 72 106 178 59 95 171 68 106 179 68 106 177 68 106 177 70 107 178 71 107 181 71 107 181 69 107 180 67 105 178 68 108 180 66 109 178 66 109 178 67 107 176 67 107 177 67 105 176 67 105 178 67 105 178 66 105 180 65 104 179 65 104 179 64 103 178 64 103 178 65 104 179 66 105 180 66 105 180 63 102 177 64 103 178 64 103 178 64 103 178 65 104 179 65 104 179 64 103 178 64 103 178 66 103 181 67 104 182 68 104 180 67 103 177 70 104 176 74 108 179 80 114 185 84 119 187 82 117 185 83 118 186 83 118 186 81 118 188 81 118 189 81 118 189 81 119 192 82 120 193 16 18 56 17 19 57 13 15 56 23 24 68 23 24 68 20 22 60 10 14 41 15 19 48 27 29 77 28 29 83 18 21 66 15 18 61 22 24 72 23 25 76 20 21 78 25 26 83 23 26 81 29 32 83 26 29 74 17 20 61 15 20 58 16 21 61 15 22 68 17 23 75 13 19 77 22 26 89 22 22 82 20 17 70 23 16 58 40 31 60 29 16 33 101 80 75 196 160 126 75 41 14 35 25 36 18 21 56 29 32 77 30 32 81 27 25 74 29 11 35 122 78 51 205 160 127 45 27 39 24 22 59 23 24 72 19 19 69 32 29 76 34 26 65 50 41 60 213 201 211 210 197 207 57 41 54 37 21 34 28 12 23 42 26 37 42 26 36 50 34 45 162 146 157 147 134 144 81 68 78 145 132 141 247 234 241 122 110 112 40 29 33 127 119 130 170 167 176 82 78 77 49 47 50 41 39 61 48 50 89 85 92 147 94 108 173 79 99 172 76 104 178 74 108 179 71 109 174 68 107 166 68 106 168 73 106 183 74 109 190 69 107 180 68 108 178 68 108 180 70 108 181 71 109 184 70 108 183 69 107 180 68 106 179 68 106 177 67 107 176 67 108 174 67 108 174 67 107 176 67 107 176 67 107 177 67 107 177 66 106 178 66 106 178 66 106 178 66 106 178 66 106 178 66 106 178 66 106 178 65 105 177 65 105 177 65 105 177 65 105 177 65 105 177 65 105 177 65 105 177 65 105 177 65 104 179 64 101 179 66 103 181 67 105 180 67 105 178 68 105 176 70 107 177 76 110 181 79 114 182 77 112 180 78 113 181 78 115 185 78 115 185 78 115 186 77 115 186 78 116 189 79 117 190 15 18 53 17 19 57 14 16 57 25 26 70 23 24 68 20 22 60 10 14 41 16 20 49 26 29 74 27 29 80 19 22 65 15 18 59 23 25 73 23 24 78 19 19 79 24 24 84 22 25 80 28 32 80 25 28 73 15 18 59 12 17 55 14 19 59 13 20 66 15 21 73 16 22 80 25 29 90 18 20 79 15 15 69 25 20 61 28 19 50 29 17 37 97 78 74 206 170 134 77 46 15 39 31 44 23 27 65 26 31 73 20 23 68 19 17 64 44 27 45 125 82 47 209 167 129 51 34 44 28 26 63 17 20 63 18 21 66 29 26 69 21 15 49 51 44 60 220 213 221 91 81 92 50 38 52 27 15 29 30 16 29 35 22 29 99 84 87 244 230 229 255 249 248 255 251 255 218 207 213 146 136 144 255 253 255 174 169 165 163 159 156 254 247 254 255 252 255 250 242 231 179 169 160 50 37 46 53 41 63 56 48 85 90 91 137 95 105 158 77 96 152 71 97 156 77 112 170 79 115 173 68 106 169 64 100 176 70 105 186 70 110 182 69 109 179 69 109 181 70 110 182 71 109 184 71 109 184 69 107 182 68 106 179 66 104 175 66 105 172 67 106 173 66 107 171 67 108 174 67 107 176 68 108 178 68 108 178 66 106 178 67 107 179 67 107 179 68 108 180 67 107 179 67 107 179 66 106 178 65 105 177 68 108 180 67 107 179 66 106 178 65 105 177 64 104 176 64 104 176 64 104 176 64 104 176 65 103 178 66 104 179 67 105 178 67 105 178 68 105 176 69 106 177 72 106 177 73 108 176 74 109 177 73 110 178 74 111 181 75 112 182 74 112 183 75 113 184 76 114 187 77 115 188 17 17 51 20 20 56 18 17 59 28 27 71 25 23 70 21 20 60 9 13 42 15 19 48 25 26 72 27 27 77 18 21 62 16 19 60 24 26 74 24 25 81 19 19 81 24 24 88 23 25 82 29 32 83 25 27 75 14 17 62 12 15 58 14 19 61 13 19 67 14 20 72 25 31 89 22 26 87 13 15 74 18 18 70 25 19 63 26 19 52 30 21 42 97 79 77 196 162 124 79 50 16 30 21 38 19 22 65 30 35 77 31 34 77 25 24 66 29 13 26 119 77 35 203 162 118 51 34 42 17 16 50 14 18 55 31 35 70 28 28 62 39 37 61 47 45 56 185 184 189 166 163 170 150 147 156 31 25 37 28 20 31 131 122 123 254 247 239 230 222 209 165 157 144 239 232 226 247 243 242 255 254 255 249 250 252 239 246 238 254 255 248 254 244 242 221 206 201 217 203 190 241 227 218 204 189 196 59 45 60 49 38 55 137 133 150 191 192 210 147 154 180 103 117 156 82 104 154 78 107 163 74 109 173 71 109 184 71 110 189 71 111 181 70 110 179 70 110 180 70 109 184 70 109 186 70 109 186 69 108 185 68 106 181 66 104 175 68 105 173 68 105 173 67 106 171 67 108 174 67 107 176 65 108 177 65 107 179 68 108 180 68 108 180 68 108 180 67 107 179 67 107 179 67 107 179 67 107 179 67 107 179 69 109 181 67 107 179 66 106 178 64 104 176 63 103 175 63 103 175 64 104 176 65 105 177 67 105 180 66 104 179 66 104 177 67 105 178 68 106 177 68 106 177 69 106 176 68 105 175 70 107 177 72 109 179 74 111 182 74 112 183 74 112 185 74 112 185 75 113 186 76 114 187 19 18 52 21 21 57 19 18 58 29 28 72 25 24 68 21 20 60 10 11 39 14 18 47 22 23 69 25 25 75 18 20 61 15 18 59 24 26 74 24 25 81 19 19 83 24 24 88 24 26 85 31 34 89 26 28 79 14 16 64 12 14 62 15 17 65 15 19 67 15 20 75 14 19 77 18 22 83 18 20 77 19 21 72 15 13 53 30 23 56 32 22 46 90 72 72 197 165 127 84 55 25 24 16 40 25 27 76 29 33 80 19 22 67 26 25 69 39 22 38 144 102 60 193 150 105 41 24 32 21 20 52 25 29 58 26 30 57 27 27 53 107 106 124 215 216 220 250 252 249 254 255 255 136 135 140 26 23 32 69 64 70 128 120 117 93 86 70 129 122 96 104 97 69 81 74 58 108 101 93 167 166 164 224 226 223 220 226 214 165 166 152 113 98 91 104 84 75 145 128 108 136 120 104 83 69 69 110 97 106 41 31 40 55 48 56 237 235 238 250 253 255 241 247 255 189 202 236 124 145 188 84 111 164 72 106 170 75 110 178 76 111 177 73 111 176 71 110 179 70 108 181 70 109 186 69 108 187 69 108 187 68 107 182 68 104 178 68 105 175 68 105 173 68 105 173 68 107 174 65 108 176 65 107 179 64 108 179 70 110 182 69 109 181 67 107 179 66 106 178 66 106 178 67 107 179 68 108 180 68 108 180 67 107 179 66 106 178 64 104 176 62 102 174 62 102 174 63 103 175 65 105 177 66 106 178 67 105 180 66 104 179 65 103 176 66 104 177 68 106 177 70 108 179 69 108 177 69 108 177 71 108 178 71 110 179 73 111 182 74 112 183 75 113 186 75 113 186 76 114 187 76 114 187 23 19 52 24 23 57 112 110 149 37 35 75 22 19 62 18 18 56 13 14 42 13 14 44 25 23 70 30 30 82 17 18 62 12 14 55 36 37 85 29 28 85 16 16 78 22 22 86 20 20 82 33 33 93 21 20 77 19 19 71 13 13 65 16 18 67 10 13 64 12 17 72 19 24 82 18 23 81 21 24 79 14 16 65 16 14 54 25 20 52 33 25 48 93 74 76 199 166 135 77 46 26 30 20 55 26 26 86 27 29 86 27 30 83 26 24 73 36 19 38 132 90 52 197 154 112 44 24 33 34 31 62 21 23 46 41 43 64 194 192 213 248 246 255 255 255 255 250 251 245 254 255 249 106 106 104 34 29 33 35 26 27 38 28 19 147 135 113 204 190 153 255 244 203 248 231 203 157 140 122 244 230 221 207 197 187 218 213 193 208 200 181 166 141 134 255 243 232 234 215 183 226 211 180 93 81 69 32 23 26 50 44 58 39 35 50 184 184 192 252 255 255 243 250 255 247 255 255 244 255 255 194 211 239 109 128 171 86 107 160 86 112 169 79 111 170 70 103 170 81 118 188 62 99 177 76 115 194 71 110 189 67 106 183 70 106 182 70 107 178 72 107 175 70 107 175 69 108 177 66 109 178 66 108 182 65 109 182 69 109 181 67 107 179 66 106 178 67 107 179 67 107 179 66 106 178 67 107 179 69 109 181 65 105 177 65 105 177 64 104 176 63 103 175 63 103 175 64 104 176 66 106 178 68 108 180 66 104 177 68 106 179 70 108 181 69 107 180 68 106 177 67 105 176 68 106 177 68 108 178 69 107 178 69 107 178 69 107 178 69 107 178 70 108 181 71 109 182 72 110 185 72 110 185 23 18 48 36 33 64 203 199 234 88 86 123 35 31 68 13 12 44 19 19 43 14 15 43 33 31 80 23 20 73 9 8 52 15 16 60 34 35 83 17 17 69 24 23 80 24 24 84 29 29 91 28 28 88 21 20 77 20 20 74 16 16 68 11 11 63 11 13 64 19 22 75 21 23 80 18 23 78 16 19 72 18 20 68 28 26 65 27 22 54 31 23 47 85 69 72 198 163 135 78 45 28 30 18 56 26 26 90 27 29 88 26 29 84 24 22 71 34 17 36 134 91 57 198 154 115 45 26 30 15 11 34 76 78 91 226 226 236 250 247 255 255 252 255 250 245 239 255 255 243 252 250 238 90 86 77 33 24 25 41 31 30 44 30 21 134 118 95 205 186 144 238 217 172 243 220 186 255 245 221 255 250 241 255 242 234 255 253 234 255 251 234 255 249 239 248 222 207 246 226 189 228 212 176 98 85 69 61 49 49 35 24 38 35 31 46 148 147 153 251 255 255 251 255 255 237 245 247 249 255 255 247 255 255 229 236 255 171 182 212 97 116 158 77 103 154 80 109 167 80 113 180 75 111 185 70 109 188 63 101 182 73 112 191 69 107 182 70 106 180 72 106 177 70 107 177 69 107 178 68 108 180 66 108 182 65 109 184 68 107 182 66 106 178 65 105 177 67 107 179 67 107 179 66 106 178 67 107 179 69 109 181 66 106 178 66 106 178 65 105 177 65 105 177 64 104 176 65 105 177 67 107 179 68 108 180 67 105 178 67 105 178 68 106 179 68 106 179 67 105 176 66 106 176 66 106 176 66 106 176 68 108 178 68 108 178 69 107 178 69 107 178 70 108 181 71 109 182 72 110 185 72 110 185 18 12 40 35 31 58 214 209 239 222 219 248 45 40 70 24 22 46 15 14 32 33 33 59 22 19 66 26 23 78 21 19 66 22 21 65 24 22 69 40 38 85 20 18 67 21 21 73 21 20 77 23 22 80 29 26 81 19 16 69 11 9 59 18 18 68 25 25 75 10 12 63 25 28 81 24 27 80 17 19 70 16 17 63 26 24 61 25 20 52 32 27 50 85 69 72 198 163 133 83 48 28 33 19 54 27 26 86 28 30 87 25 28 79 22 23 67 31 17 32 147 104 70 197 154 112 45 26 20 93 88 95 251 251 249 255 255 250 255 247 245 255 246 243 255 248 237 253 240 224 231 220 202 73 61 47 50 36 35 37 21 22 45 27 23 70 51 34 233 211 174 225 202 160 234 210 176 244 222 199 235 214 211 230 214 215 215 206 201 225 217 206 244 230 217 234 218 195 194 179 140 233 217 181 88 66 53 53 33 34 63 46 54 55 44 52 157 153 150 254 255 250 246 250 253 252 255 255 252 253 255 255 255 253 255 255 248 248 249 251 201 211 236 108 126 166 85 108 158 86 115 173 78 113 181 62 100 175 81 119 200 63 101 182 68 105 183 69 105 181 71 105 177 69 106 176 68 106 177 67 107 179 65 107 183 64 108 183 67 106 181 65 105 177 65 105 177 66 106 178 66 106 178 66 106 178 67 107 179 69 109 181 67 107 179 67 107 179 67 107 179 66 106 178 66 106 178 66 106 178 68 108 180 69 109 181 70 108 179 69 107 178 69 107 178 69 107 178 68 108 178 68 108 178 67 107 179 65 107 179 67 109 181 66 108 180 68 108 180 68 108 180 70 108 183 71 109 184 72 110 185 73 111 186 23 15 39 41 36 58 200 195 217 241 238 255 130 125 145 82 79 96 172 170 183 91 89 110 26 20 68 27 22 78 19 15 65 19 18 62 29 28 70 122 121 161 34 34 72 21 20 60 30 28 77 29 29 81 30 28 77 22 20 67 9 7 54 14 12 59 14 12 59 17 18 66 21 23 72 21 25 73 23 25 73 17 19 60 18 16 53 24 19 51 33 27 53 83 67 70 200 164 130 89 53 27 34 20 46 27 27 79 29 32 85 24 28 75 23 25 64 33 19 32 153 110 78 185 140 99 74 55 41 249 244 240 246 243 228 255 255 237 255 245 232 241 221 210 190 168 155 190 169 152 223 205 183 72 57 38 24 9 4 34 18 21 36 17 19 54 34 25 127 106 75 235 216 176 221 201 168 255 246 225 255 248 248 255 247 255 255 250 254 249 245 242 255 250 238 255 248 222 222 209 167 179 164 125 59 37 23 41 16 19 43 22 29 39 20 24 126 113 105 243 235 224 255 252 248 255 253 250 247 242 238 255 255 244 255 255 236 255 255 244 251 255 255 210 220 247 99 117 155 84 107 157 77 108 172 78 115 186 65 104 183 67 108 188 65 104 181 66 104 179 67 104 175 69 103 174 67 104 175 66 104 177 63 105 181 63 105 181 66 106 178 64 104 176 64 104 176 66 106 178 67 107 179 66 106 178 66 106 178 68 108 180 68 108 180 68 108 180 68 108 180 67 107 179 67 107 179 67 107 179 68 108 180 69 109 181 72 110 181 70 108 179 69 107 178 68 108 178 69 109 179 69 109 179 66 108 180 64 106 178 67 109 181 67 109 181 69 109 181 69 109 181 70 108 183 71 109 184 72 110 185 73 111 186 31 22 41 33 26 42 198 192 204 253 250 255 240 234 244 231 228 235 196 194 199 74 71 88 31 24 68 26 18 75 21 14 65 24 21 64 34 32 69 180 179 210 92 92 118 29 30 58 20 20 56 31 30 70 23 21 61 21 19 58 21 19 58 27 25 64 4 3 43 17 19 60 14 17 62 18 21 66 20 21 65 25 24 64 19 18 52 26 21 53 27 21 49 77 58 62 203 165 128 95 59 25 34 20 35 26 25 65 28 32 77 22 27 69 23 27 62 34 22 32 144 100 71 193 148 107 232 211 190 253 246 228 255 255 230 253 241 215 176 155 134 157 131 116 135 107 95 81 54 37 50 30 5 38 22 0 33 18 13 42 27 32 36 21 28 49 34 31 199 183 157 234 220 183 168 153 120 225 213 191 255 246 247 255 252 255 252 255 255 255 255 255 255 247 237 252 238 211 168 158 109 230 219 174 119 103 87 53 35 35 48 26 38 55 32 40 77 53 43 143 121 100 208 187 166 232 215 195 255 253 237 255 253 240 251 247 235 252 252 244 245 243 244 250 252 255 180 190 217 88 107 147 76 106 160 70 108 173 70 109 184 65 107 183 65 104 181 66 104 177 67 104 174 69 104 172 67 104 174 66 104 175 65 104 179 65 104 179 66 106 178 64 104 174 65 105 175 66 106 176 67 107 177 66 106 176 66 106 176 68 108 178 67 107 177 68 108 178 68 108 178 68 108 178 67 107 177 66 106 176 67 107 177 68 108 178 70 108 179 68 106 177 66 106 176 65 105 175 66 106 178 65 107 179 64 106 178 63 105 177 67 109 181 67 109 181 67 109 183 67 109 183 70 109 184 71 110 185 73 111 186 74 112 187 40 32 45 139 131 142 231 225 229 244 240 241 241 237 236 240 236 233 166 162 159 39 36 47 29 20 63 31 22 77 28 20 71 24 19 60 31 27 60 196 196 220 161 163 178 24 26 41 32 32 58 32 33 64 93 92 123 100 99 130 23 20 51 14 10 43 24 23 57 14 14 52 25 27 66 51 54 95 14 16 57 22 22 60 22 21 55 26 21 53 26 20 48 77 58 64 205 162 127 101 61 26 32 19 29 23 23 59 27 32 74 20 25 67 23 27 62 34 22 34 134 89 68 232 186 153 255 236 212 255 248 227 239 230 201 137 121 95 122 95 76 71 39 26 38 8 0 41 15 0 30 10 0 36 21 0 24 13 7 24 13 17 40 27 37 35 21 20 144 130 103 179 165 126 216 203 161 241 227 198 234 223 217 255 251 255 245 246 250 251 250 248 226 210 197 227 208 178 206 193 141 215 203 153 105 90 69 46 28 26 62 40 52 47 24 30 53 30 16 44 20 0 53 29 3 117 95 72 185 166 149 230 217 201 254 244 234 255 253 244 255 253 246 255 253 252 235 241 255 113 130 160 89 117 165 69 105 165 64 104 174 70 114 187 65 107 181 68 106 179 69 106 176 71 106 174 69 106 174 69 106 176 68 106 179 67 107 179 66 106 176 65 105 175 65 105 175 67 107 177 67 107 177 66 106 176 66 106 176 68 108 178 66 106 176 67 107 177 68 108 178 68 108 178 67 107 177 66 106 176 66 106 176 66 106 176 70 108 179 69 107 178 67 107 177 66 106 176 65 107 179 65 107 179 65 107 179 65 107 179 66 108 180 66 108 180 67 109 183 67 109 183 71 110 185 72 111 186 75 113 188 75 113 188 178 168 176 234 228 232 245 240 237 255 251 245 247 243 234 233 231 219 119 116 107 18 15 22 35 27 68 35 26 81 18 10 61 19 14 54 40 37 66 211 210 228 212 213 218 83 86 91 78 79 97 187 188 209 207 205 227 80 78 100 22 18 43 24 21 48 17 17 45 15 16 47 43 46 81 133 135 173 33 35 73 17 17 55 21 20 54 24 18 52 31 24 55 77 56 65 204 159 126 105 62 30 34 18 31 23 23 61 28 32 80 19 23 71 23 26 67 33 21 41 129 85 76 254 206 186 255 241 227 246 234 218 117 105 83 71 50 31 38 5 0 42 9 4 34 6 2 26 8 0 105 93 67 120 113 87 36 32 23 30 24 26 25 13 23 50 36 33 140 125 92 255 240 190 252 233 177 230 209 162 179 159 134 240 224 209 255 249 237 255 246 233 210 187 171 233 208 177 255 235 182 255 238 186 105 85 58 57 37 28 53 34 36 45 27 27 48 32 16 118 103 80 48 31 13 55 36 22 44 22 11 99 75 65 174 148 135 210 187 171 255 248 232 246 237 228 230 233 238 196 211 234 70 97 140 84 119 175 71 112 178 66 110 181 66 108 180 68 108 178 70 107 175 72 107 173 72 107 173 70 107 175 69 107 178 68 108 178 67 107 177 66 106 176 66 106 176 68 108 178 68 108 178 66 106 176 66 106 176 67 107 177 66 106 176 67 107 177 68 108 178 68 108 178 67 107 177 66 106 176 65 105 175 66 106 176 71 109 180 71 109 180 69 109 179 68 108 178 66 108 180 66 108 180 66 110 181 67 111 182 64 108 181 65 109 182 66 108 182 67 109 183 71 110 185 73 112 187 76 114 189 77 115 190 184 176 187 229 222 229 246 240 240 250 247 242 251 248 241 251 248 243 176 172 169 65 59 71 28 20 61 36 26 76 24 16 57 39 32 63 47 43 60 202 199 206 232 231 227 226 225 221 241 240 246 230 228 242 161 158 179 29 27 49 29 27 51 18 18 44 9 9 37 20 19 50 51 51 79 211 211 237 71 68 95 24 21 48 27 23 48 26 20 46 36 29 60 66 46 57 203 158 127 107 63 34 35 18 34 25 24 64 30 34 82 20 24 72 24 27 68 32 22 47 115 80 84 255 219 211 248 229 222 144 134 124 37 25 9 26 8 0 41 12 8 128 97 95 117 92 85 60 43 25 45 34 4 157 151 119 36 29 10 71 63 50 87 74 66 51 35 19 116 98 58 209 189 138 214 193 138 212 190 141 231 208 177 173 152 131 110 93 77 121 104 88 190 164 147 209 184 154 214 192 143 225 205 155 118 97 66 63 42 23 78 60 46 48 32 16 86 75 47 150 139 107 41 27 1 115 97 77 129 106 90 48 21 4 59 28 7 99 71 49 180 159 140 248 236 224 241 236 240 220 225 244 109 125 158 80 107 152 78 110 167 74 112 175 69 107 178 68 108 180 69 107 178 70 107 177 70 107 175 70 107 175 69 108 177 69 107 178 68 108 178 66 106 176 66 106 176 68 108 178 68 108 178 66 106 176 66 106 176 67 107 177 66 106 176 67 107 177 68 108 178 68 108 178 67 107 177 66 106 176 65 105 175 65 105 175 69 107 178 69 107 178 69 109 179 68 108 178 64 106 178 64 106 178 66 108 180 67 111 182 65 107 181 65 107 181 66 108 182 67 109 183 71 110 185 73 112 187 76 114 189 77 115 190 30 22 46 53 46 64 123 118 125 249 247 250 246 243 252 206 202 216 194 190 205 92 86 112 33 22 65 39 28 70 30 20 44 80 73 81 173 164 165 243 236 230 242 234 221 240 232 219 244 235 228 220 213 220 59 54 77 33 29 62 22 22 56 24 27 60 5 5 39 18 18 46 71 67 84 234 226 237 151 141 149 34 23 29 63 54 57 123 115 126 50 44 70 69 52 62 205 162 130 111 70 40 31 18 35 17 20 61 28 33 75 25 30 70 21 21 59 31 25 53 93 79 92 195 180 185 148 138 139 31 23 20 29 23 11 33 25 14 42 22 21 192 169 163 222 200 177 210 190 155 201 183 143 162 147 104 209 194 151 238 223 182 252 234 194 231 214 171 233 215 169 139 121 75 117 98 56 54 35 2 48 30 8 64 46 32 53 34 27 51 34 24 54 35 18 64 47 21 98 82 46 193 178 139 211 193 157 248 230 194 255 243 205 192 174 134 202 185 139 156 139 93 233 216 172 236 217 177 196 173 139 74 50 16 119 93 60 80 54 27 64 42 28 178 160 156 254 239 244 227 219 232 159 157 178 98 105 133 93 111 149 82 108 157 76 109 176 66 104 179 70 112 188 64 106 180 73 113 182 68 107 174 70 107 175 70 107 177 71 109 180 69 107 178 68 106 177 68 106 177 69 107 178 70 108 179 69 107 178 68 106 177 69 107 180 68 106 179 68 106 179 67 105 178 67 105 178 67 105 178 68 106 179 69 107 180 69 109 179 70 110 180 71 111 181 72 112 182 71 111 181 71 111 181 71 111 183 70 112 184 72 112 184 71 111 183 71 111 183 71 111 183 72 112 184 73 113 185 75 115 187 76 116 188 28 21 54 29 23 49 86 82 96 245 243 254 191 188 207 65 61 84 57 53 80 44 37 71 29 20 63 55 45 82 152 143 160 225 216 219 253 243 241 246 236 227 252 242 230 250 238 226 244 231 223 114 103 109 36 27 54 29 25 62 23 23 61 4 7 42 30 31 61 19 17 38 80 72 83 234 224 225 218 205 199 153 140 132 204 193 187 133 124 129 35 27 51 53 35 47 202 159 125 114 73 41 32 20 34 17 21 58 28 34 70 27 31 66 24 20 55 25 21 48 23 19 36 7 4 13 20 14 18 22 17 14 14 11 2 29 25 16 32 18 18 60 41 35 153 133 108 168 148 113 74 54 17 194 175 133 245 227 181 198 180 132 194 176 128 242 224 178 255 239 197 252 234 196 255 243 215 207 190 170 128 110 98 91 74 67 44 26 26 67 49 47 120 102 90 211 194 176 255 247 225 250 233 205 255 237 204 245 227 189 219 200 157 255 237 192 252 230 183 161 139 92 207 184 140 222 199 157 212 186 149 246 220 185 224 198 165 66 42 16 38 19 5 58 38 37 202 183 189 194 178 189 114 106 117 53 52 68 32 42 69 74 96 137 80 108 171 69 105 179 68 109 188 61 105 180 70 113 181 67 108 174 69 106 174 69 106 174 70 109 178 68 106 177 67 105 176 67 105 176 68 106 177 68 106 177 68 106 177 67 105 176 69 107 180 69 107 180 68 106 179 68 106 179 68 106 179 69 107 180 70 108 181 71 109 182 71 111 181 72 112 182 73 113 183 73 113 183 72 112 182 72 112 182 73 113 185 74 114 186 73 113 185 73 113 185 73 113 185 73 113 185 74 114 186 75 115 187 76 116 188 77 117 189 16 10 48 37 32 64 79 76 95 234 233 249 126 124 146 28 25 52 27 24 53 23 17 53 31 22 69 44 33 75 99 91 112 181 174 182 227 218 223 255 246 245 250 241 236 248 237 233 235 220 225 177 165 179 48 39 68 34 28 64 13 11 51 15 15 49 23 22 40 30 27 36 117 107 115 246 234 234 232 218 207 192 178 167 184 170 167 57 47 56 31 22 51 62 42 54 198 154 119 121 78 43 33 20 30 18 21 54 29 32 65 29 30 60 23 18 48 20 15 38 28 22 36 19 14 20 20 14 16 23 18 15 17 17 7 20 17 8 19 7 9 34 18 18 27 9 0 50 32 12 76 58 36 122 104 80 70 50 23 51 32 2 197 178 146 213 196 166 188 171 143 160 144 119 114 97 79 88 72 59 77 59 55 90 74 74 62 45 51 89 70 74 86 65 62 98 75 69 162 138 128 177 154 140 233 208 188 237 213 189 153 127 100 98 73 42 134 107 77 135 108 78 40 11 0 62 32 4 124 94 70 217 186 165 197 166 146 180 154 137 177 159 149 60 45 42 65 50 53 36 25 33 22 15 23 15 14 28 19 30 50 30 50 85 80 106 163 74 108 179 68 110 184 62 106 177 70 114 179 69 110 174 71 110 177 71 108 176 70 109 178 69 108 177 68 106 177 68 106 177 69 107 178 69 107 178 68 106 177 68 106 177 70 108 179 70 108 179 69 107 180 69 107 180 70 108 181 71 109 182 72 110 183 73 111 184 71 111 181 72 112 182 73 113 183 74 114 184 73 113 183 73 113 183 74 114 184 75 115 185 73 113 185 73 113 185 74 114 186 75 115 187 75 115 187 76 116 188 77 117 189 77 117 189 20 14 58 32 28 65 62 59 86 204 205 226 64 61 90 31 27 60 29 25 58 13 9 46 22 14 63 40 31 76 21 16 39 40 34 48 79 71 84 202 194 205 254 244 252 253 242 250 250 236 251 238 224 247 111 102 133 30 24 62 26 21 61 45 44 75 94 93 101 143 139 138 216 205 209 247 233 232 209 193 180 194 180 167 104 90 90 28 17 31 27 17 51 49 28 43 196 150 114 126 82 43 32 17 24 22 23 51 30 31 59 24 24 50 20 15 38 19 10 29 11 5 15 22 16 20 25 16 17 19 14 10 9 9 0 16 16 6 28 19 22 27 15 19 36 22 21 25 10 5 23 8 1 37 23 14 69 53 40 44 28 13 44 28 12 46 30 14 43 30 14 46 32 19 41 27 18 30 17 11 40 26 26 34 22 26 28 15 22 46 31 38 43 24 26 38 18 19 61 40 37 50 29 24 63 40 32 66 44 31 79 56 40 89 66 48 193 168 148 200 175 155 209 183 166 85 59 42 47 19 5 44 18 5 116 89 78 153 133 122 67 53 44 41 32 27 32 22 23 24 15 20 29 24 30 10 10 20 16 25 42 0 15 48 69 94 148 75 109 173 70 109 178 64 107 176 69 113 176 69 110 172 72 111 176 73 110 178 72 111 180 72 111 180 72 110 181 72 110 181 72 110 181 71 109 180 71 109 180 70 108 179 71 109 180 71 109 180 71 109 182 71 109 182 71 109 182 72 110 183 72 110 183 73 111 184 71 111 181 72 112 182 73 113 183 73 113 183 73 113 183 73 113 183 74 114 184 75 115 185 72 112 184 73 113 185 74 114 186 75 115 187 76 116 188 76 116 188 77 117 189 77 117 189 27 23 73 31 28 73 57 56 90 144 145 175 29 28 62 29 27 64 26 24 61 16 14 54 19 12 63 23 17 63 26 21 51 21 16 38 40 35 57 199 191 214 243 234 253 120 111 132 137 124 150 204 190 223 207 197 234 59 51 92 26 20 58 48 44 69 89 84 88 156 149 143 210 198 198 232 217 214 232 216 203 191 174 164 53 41 43 30 19 36 29 19 56 51 30 47 194 146 108 130 85 43 30 14 15 26 24 48 30 28 49 19 16 35 18 9 28 19 8 24 22 15 22 17 11 11 21 12 13 27 22 18 19 21 10 13 14 6 21 15 19 9 0 7 26 19 26 20 14 18 20 11 16 33 24 27 22 14 12 32 24 21 31 22 17 20 11 6 27 18 13 24 17 11 32 24 21 26 21 18 40 34 36 28 22 26 35 28 35 28 22 26 35 29 31 34 28 28 30 21 22 36 26 25 46 35 31 33 22 16 35 22 14 48 35 26 108 96 84 140 128 116 231 219 207 221 209 197 91 77 68 44 31 22 32 19 11 23 14 5 19 12 4 21 16 12 19 13 15 13 8 12 19 15 16 13 14 19 0 7 20 4 20 46 52 76 124 72 104 163 68 106 171 66 107 173 66 110 171 66 107 169 70 109 174 73 112 179 73 112 181 74 113 182 74 113 182 74 113 182 74 113 182 73 112 181 73 111 182 73 111 182 73 111 182 73 111 182 73 111 182 72 110 181 72 110 181 72 110 181 72 110 181 72 110 181 73 113 183 73 113 183 74 114 184 74 114 184 73 113 183 74 114 184 75 115 185 76 116 186 73 113 185 74 114 186 75 115 187 76 116 188 77 117 189 77 117 189 77 117 189 77 117 189 10 7 62 27 25 74 40 39 79 56 56 92 23 22 62 31 30 70 22 22 60 15 13 53 27 23 74 26 23 70 30 26 61 34 31 60 27 22 54 200 195 227 190 183 214 46 39 70 35 25 60 42 31 71 94 85 130 86 79 121 21 15 51 25 20 43 24 19 23 38 30 27 139 125 125 234 219 216 238 221 213 198 183 178 119 108 114 33 24 43 29 21 58 40 22 38 190 142 102 139 92 46 32 14 14 26 23 42 24 23 39 14 12 25 19 8 22 19 9 18 14 8 10 23 17 17 16 10 10 11 7 4 22 25 16 10 15 9 14 12 17 28 22 32 14 8 20 21 15 27 18 12 22 10 5 12 22 17 23 19 13 17 18 12 14 39 30 33 38 29 32 29 20 21 24 18 18 24 18 20 17 13 14 19 14 18 27 22 26 20 15 19 24 22 25 11 11 13 22 20 21 31 27 28 22 16 16 34 29 26 33 25 22 34 26 23 48 41 35 98 91 85 54 47 41 34 27 21 24 17 11 29 24 18 13 8 2 20 17 12 12 13 7 8 9 4 15 13 14 20 18 21 11 10 8 17 18 20 4 13 22 8 23 46 42 65 107 75 104 160 71 106 170 71 110 175 70 111 173 68 109 171 71 110 177 75 114 183 74 113 182 75 114 183 76 115 184 76 115 184 75 114 183 74 113 182 74 112 183 74 112 183 75 113 184 75 113 184 74 112 183 74 112 183 73 111 182 73 111 182 72 110 181 72 110 181 75 115 185 75 115 185 75 115 185 75 115 185 74 114 184 74 114 184 76 116 186 77 117 187 75 115 187 76 116 188 76 116 188 77 117 189 78 118 190 79 119 191 79 119 191 79 119 191 22 21 78 29 29 81 23 24 68 26 28 69 43 44 90 31 32 76 17 19 57 18 18 56 22 20 69 27 25 74 9 7 44 28 27 61 39 34 74 201 196 237 101 96 136 34 29 69 34 27 68 44 37 81 37 30 81 39 33 79 26 21 53 15 10 30 21 15 27 20 11 16 107 95 97 248 234 234 203 188 191 199 186 193 202 192 200 52 48 65 13 9 44 43 26 42 185 137 97 150 102 56 35 17 13 21 18 35 16 16 26 12 11 19 23 13 22 18 8 16 22 16 16 16 12 11 22 18 17 12 11 9 11 18 11 11 20 15 17 18 23 5 2 13 17 11 23 12 4 15 22 14 25 22 14 25 5 0 6 27 17 25 22 12 20 13 2 8 19 8 14 23 12 16 27 16 20 34 23 27 23 14 17 27 18 19 26 17 18 24 18 18 21 16 20 22 20 23 27 23 24 23 19 20 26 20 22 33 27 27 17 8 9 22 14 12 18 10 8 15 7 5 22 17 13 12 7 3 20 15 11 13 10 5 3 0 0 14 15 10 10 15 9 9 14 10 13 13 15 17 15 18 5 4 2 7 9 8 14 21 27 5 18 37 44 63 105 83 110 165 75 109 172 76 114 179 73 114 176 72 113 177 74 113 182 76 114 185 74 113 182 75 114 183 77 116 185 77 116 185 75 114 183 74 113 182 74 113 182 75 114 183 76 115 184 76 115 184 76 114 185 75 113 184 75 113 184 75 113 184 74 112 183 74 112 183 76 116 185 76 116 185 76 116 186 75 115 185 74 114 184 74 114 184 75 115 185 76 116 186 76 116 186 76 116 186 76 116 188 76 116 188 77 117 189 78 118 190 80 120 192 80 120 192 15 14 72 35 35 89 20 21 69 24 25 71 34 34 84 35 36 82 25 27 65 10 12 50 16 14 61 23 21 68 20 20 58 18 18 56 42 40 87 161 159 208 30 28 75 33 32 76 27 26 70 32 30 77 28 25 80 25 21 71 20 17 48 21 18 39 18 11 29 22 14 29 133 122 130 230 219 227 88 75 92 45 34 50 148 142 154 127 124 141 24 24 58 32 18 31 181 133 93 156 110 61 39 21 17 17 17 29 11 11 19 13 12 17 25 18 25 18 9 14 18 14 13 10 9 7 22 18 19 12 12 12 13 22 17 9 20 16 12 15 22 14 14 24 14 11 20 15 12 19 9 6 13 18 15 22 22 17 24 4 0 5 22 15 22 14 7 14 19 10 15 17 8 13 17 8 11 14 5 8 15 9 11 18 12 12 14 8 8 17 11 11 18 14 15 13 11 14 12 8 9 15 11 12 11 5 7 11 5 7 23 14 15 18 9 10 19 10 11 22 16 16 10 4 4 13 9 6 15 11 10 8 7 5 25 24 22 8 10 7 11 17 15 1 7 5 15 16 20 10 10 12 19 18 16 17 17 17 7 12 16 4 15 33 47 65 105 87 112 166 77 109 170 76 114 179 74 114 176 73 114 180 73 111 182 74 112 185 75 114 183 76 115 184 78 117 186 78 117 186 76 115 184 75 114 183 75 114 183 76 115 184 76 115 184 76 115 184 76 114 185 76 114 185 76 114 185 76 114 185 76 114 185 76 114 185 75 115 184 75 115 184 75 115 185 74 114 184 72 112 182 72 112 182 74 114 184 75 115 185 76 116 186 75 115 185 75 115 187 75 115 187 76 116 188 77 117 189 79 119 191 80 120 192 16 17 73 25 27 78 22 24 73 28 30 78 30 32 83 34 36 84 21 24 59 15 18 51 22 24 65 19 20 64 13 15 53 25 27 66 39 39 89 52 52 104 31 31 81 21 23 71 29 31 79 28 30 79 27 28 84 25 25 75 19 19 53 13 13 39 15 11 38 19 14 37 151 142 163 171 161 185 29 19 46 16 10 36 28 27 43 47 50 67 22 26 55 28 16 26 167 122 80 158 111 65 34 19 12 12 14 26 11 14 21 15 16 20 17 15 20 19 14 18 15 14 12 16 16 16 15 13 16 11 12 16 9 18 15 6 16 15 8 11 20 10 13 22 5 9 12 9 15 13 10 16 14 8 14 14 9 13 14 13 14 16 12 12 14 7 7 9 10 10 12 13 13 15 11 11 13 10 10 10 14 14 14 13 13 11 10 10 8 12 12 10 16 14 15 14 12 15 14 10 11 14 10 11 16 10 12 16 10 12 17 8 11 16 7 10 14 8 10 16 10 12 16 10 12 13 9 8 10 10 10 12 14 13 11 13 12 6 10 9 7 16 15 5 13 15 10 13 18 14 13 18 14 13 11 17 17 17 14 19 23 2 13 31 50 65 104 90 113 167 80 110 172 78 113 179 71 109 174 79 118 185 74 112 185 82 120 193 77 116 185 78 117 184 79 118 185 79 118 185 77 116 183 76 115 182 75 114 183 75 114 183 75 114 183 76 115 184 77 116 185 77 116 185 77 116 185 77 116 185 76 115 184 76 115 184 75 114 183 75 114 183 74 113 182 74 113 182 75 114 183 77 116 185 78 116 187 79 117 188 79 117 188 77 115 186 75 113 184 75 113 184 77 115 186 78 116 187 77 115 188 77 115 188 17 19 70 25 27 78 22 24 73 28 30 79 30 31 85 33 35 83 19 22 57 12 15 46 17 19 58 21 23 64 19 21 59 26 28 69 27 27 79 32 33 89 22 23 77 25 28 79 23 29 77 24 30 80 26 29 84 24 26 77 18 20 58 13 14 45 15 12 43 18 13 45 133 127 161 66 58 95 22 16 54 25 21 54 13 15 36 12 17 37 13 19 45 35 24 30 161 116 74 162 115 69 31 16 9 12 14 26 9 14 18 13 17 20 15 15 17 16 14 17 9 9 9 18 18 18 17 16 21 9 10 14 5 14 11 9 18 17 15 15 23 18 17 25 13 17 16 11 18 11 9 16 9 7 14 7 6 11 7 9 11 10 13 15 14 19 19 19 12 12 12 14 14 14 13 13 13 12 12 12 14 14 14 12 14 13 10 10 8 11 11 9 11 9 10 13 9 10 14 8 10 15 9 9 19 10 11 20 11 12 21 11 12 20 10 11 16 7 8 20 11 12 19 13 13 15 11 12 9 9 9 8 10 9 10 11 13 9 13 14 3 11 13 1 6 9 7 7 15 11 10 15 10 8 9 10 10 10 13 16 21 10 18 37 42 57 96 90 111 166 83 113 175 82 117 183 76 114 179 81 120 187 75 113 186 81 119 194 78 117 186 79 118 185 80 119 186 80 119 186 79 118 185 78 117 184 77 116 185 77 116 185 78 117 186 78 117 186 79 118 187 79 118 187 78 117 186 78 117 186 77 116 185 77 116 185 78 117 186 77 116 185 76 115 184 76 115 184 76 115 184 77 116 185 78 116 187 79 117 188 79 117 188 77 115 186 75 113 184 75 113 184 76 114 185 77 115 186 77 115 188 77 115 188 18 18 68 27 27 77 24 24 74 30 30 82 30 29 86 33 33 83 19 19 55 12 13 44 16 18 56 21 23 62 20 22 60 23 25 66 18 19 73 17 18 75 18 19 73 29 32 85 21 28 80 22 29 81 24 30 82 24 28 76 19 20 64 14 14 52 12 13 43 15 14 48 55 52 99 21 17 67 34 31 74 9 9 45 4 8 37 17 23 47 16 22 46 28 18 19 155 111 66 171 124 78 28 13 6 12 16 27 7 15 18 11 17 17 13 14 18 14 14 16 21 21 23 16 16 18 10 8 13 12 11 16 16 21 17 15 17 14 10 5 9 10 4 8 13 12 8 9 10 2 11 12 4 18 18 10 18 17 12 15 11 8 13 9 6 17 11 11 16 10 10 16 10 10 16 10 10 16 10 10 17 11 11 16 12 11 16 10 10 17 11 11 21 12 13 21 12 13 21 11 12 21 11 10 21 9 9 20 8 8 21 7 6 20 6 5 23 11 11 20 10 9 18 10 8 18 12 12 16 14 15 14 14 14 13 14 16 11 15 18 11 15 18 6 10 13 12 11 19 20 15 21 17 13 14 12 10 11 10 13 18 5 13 32 34 49 88 91 112 167 85 115 177 85 120 184 78 116 179 82 121 188 76 114 187 82 120 193 79 118 187 80 119 186 81 120 187 82 121 188 81 120 187 80 119 186 80 119 186 80 119 186 81 120 187 81 120 187 81 120 189 81 120 189 80 119 188 79 118 187 79 118 187 78 117 186 80 119 188 80 119 188 79 118 187 78 117 186 77 116 185 78 117 186 78 117 186 79 118 187 78 116 187 77 115 186 76 114 185 75 113 184 76 114 185 77 115 186 77 115 186 77 115 186 18 19 65 26 27 75 23 23 73 29 29 83 30 29 87 34 31 84 18 18 54 10 11 42 22 22 60 20 22 61 13 15 53 20 21 65 22 23 79 18 19 76 21 24 79 27 33 85 22 29 84 22 29 83 24 30 82 23 27 75 17 20 65 11 13 52 11 12 42 14 14 48 13 10 65 27 26 84 24 22 71 13 15 54 16 19 52 18 22 51 9 13 38 34 22 22 155 108 64 180 132 84 28 13 6 15 18 27 7 15 17 9 18 17 12 13 17 13 13 15 13 13 15 14 14 16 15 10 16 15 10 14 14 13 8 13 10 5 20 8 8 31 15 15 31 14 7 26 9 1 22 5 0 25 6 0 28 9 2 29 8 3 30 9 4 33 9 7 35 11 9 32 8 6 32 8 8 33 9 9 29 8 7 29 8 7 31 10 9 32 11 10 30 9 8 31 10 9 34 10 8 35 11 7 37 12 8 38 13 8 41 14 7 42 15 8 37 12 7 32 9 3 29 8 3 27 9 5 24 8 8 19 7 7 17 7 8 16 10 12 13 11 14 7 6 11 11 6 13 17 10 17 17 11 11 18 14 13 15 16 21 5 14 31 28 43 82 94 115 168 87 117 177 84 120 182 77 115 177 80 119 184 78 116 187 85 123 194 81 120 189 82 121 188 83 122 189 83 122 189 82 121 188 82 121 188 82 121 188 82 121 188 83 122 189 83 122 189 82 121 190 82 121 190 82 121 190 81 120 189 81 120 189 80 119 188 81 120 189 81 120 189 80 119 188 79 118 187 79 118 187 79 118 187 79 118 187 79 118 187 78 116 187 77 115 186 77 115 186 76 114 185 77 115 186 77 115 186 78 116 187 79 117 188 19 18 62 27 25 72 24 22 72 30 27 84 31 26 90 35 30 88 19 17 56 11 12 43 24 24 62 22 21 61 11 13 52 19 20 64 27 28 84 20 20 80 25 28 83 28 31 86 25 30 88 25 30 88 25 28 81 22 24 72 16 17 61 9 12 47 9 13 38 12 15 46 22 22 76 26 26 86 15 15 67 23 24 68 7 9 47 9 10 41 12 13 41 36 20 21 157 106 63 181 130 83 29 12 4 17 17 25 8 13 16 11 17 15 12 13 15 13 13 15 5 5 7 18 13 17 23 14 19 21 11 12 23 14 7 26 13 5 25 4 1 29 0 0 42 1 0 56 9 1 66 19 11 73 24 17 79 30 23 87 37 30 91 41 34 94 41 35 88 35 29 80 27 21 77 26 22 75 24 20 66 15 11 64 13 9 66 17 13 64 16 12 73 25 21 73 28 23 81 34 28 88 41 33 99 51 39 109 60 46 116 67 52 120 71 56 124 76 62 114 68 55 99 55 44 79 39 31 59 22 16 43 8 4 37 3 1 31 5 4 21 11 10 15 11 12 19 9 17 18 9 14 15 6 7 16 12 11 16 17 22 5 14 31 17 32 71 92 115 167 87 118 175 84 120 180 79 118 177 81 121 182 81 118 186 87 124 194 82 121 188 83 122 187 84 123 188 84 123 188 83 122 187 82 121 186 83 122 189 84 123 190 82 121 188 82 121 188 82 121 188 82 121 188 82 121 188 82 121 188 82 121 190 82 121 190 81 120 189 81 120 189 80 119 188 80 119 188 80 119 188 80 119 188 80 119 188 81 120 189 79 117 188 79 117 188 78 116 187 78 116 187 78 116 187 78 116 187 80 118 189 80 118 189 19 18 60 27 26 70 24 21 74 29 25 84 31 24 91 35 29 89 20 18 58 13 12 44 20 18 57 23 22 62 17 16 56 19 20 66 27 28 85 17 17 77 26 29 84 26 29 84 28 30 91 27 29 90 26 27 84 23 24 72 15 17 55 9 13 42 9 14 36 12 15 46 20 22 71 19 20 77 26 27 83 12 13 61 6 5 45 18 17 51 20 17 48 32 13 17 159 106 62 174 120 73 31 11 2 14 15 20 8 14 14 10 16 14 13 15 14 14 14 14 18 16 17 15 9 11 16 4 8 22 8 8 25 11 2 25 5 0 40 10 2 66 26 18 115 61 51 144 82 71 168 106 93 178 116 103 181 118 103 182 119 104 185 119 103 184 118 102 181 115 99 170 104 88 169 103 89 163 100 85 148 85 70 142 79 64 144 82 69 141 79 66 144 85 71 145 86 70 153 92 74 162 101 82 174 112 91 187 123 98 193 129 102 196 132 105 185 124 96 186 126 102 187 129 109 184 128 111 169 117 103 132 84 72 84 37 27 41 4 0 22 4 2 15 6 7 22 11 17 23 12 16 18 8 7 14 10 7 13 14 18 4 13 28 5 21 57 90 113 163 87 118 173 87 123 181 84 123 180 84 124 183 83 121 186 86 123 191 84 123 188 84 123 188 85 124 189 84 123 188 84 123 188 83 122 187 84 123 190 84 123 190 83 122 189 82 121 188 82 121 188 82 121 188 82 121 188 83 122 189 83 122 191 83 122 191 82 121 190 81 120 189 81 120 189 81 120 189 81 120 189 82 121 190 82 121 190 82 121 190 80 118 189 80 118 189 80 118 189 80 118 189 79 117 188 80 118 189 81 119 190 83 121 192 21 19 59 29 26 71 25 20 74 30 24 84 32 22 91 36 27 90 22 17 58 14 13 47 16 14 53 24 23 63 18 17 59 18 19 65 26 27 84 15 15 77 28 31 86 24 26 83 28 28 92 28 25 92 27 25 88 25 23 73 18 19 50 11 13 34 9 14 36 12 16 45 17 20 61 11 14 65 22 23 80 16 16 70 22 21 61 21 17 50 11 5 39 42 19 27 169 112 69 168 112 63 36 14 3 13 12 17 8 12 11 10 15 11 13 13 13 14 12 13 19 15 16 19 10 11 20 5 8 26 6 7 34 14 3 58 32 17 116 80 68 179 131 119 200 143 124 210 148 127 216 154 131 217 155 130 219 155 128 220 156 128 225 160 130 228 163 133 229 164 132 218 155 122 221 158 125 222 159 126 205 144 113 199 138 107 202 141 112 198 137 108 207 143 115 207 142 112 210 144 112 215 149 114 225 157 120 229 161 122 230 161 119 227 158 116 221 154 111 214 151 110 209 147 110 207 148 114 210 155 125 201 147 121 167 117 94 126 87 70 52 32 25 25 14 12 17 2 5 22 7 10 20 10 8 16 11 7 14 15 17 9 19 31 2 18 52 92 116 164 86 119 172 87 124 179 85 125 177 85 126 180 85 123 185 85 123 186 86 125 190 86 125 190 86 125 190 86 125 190 84 123 188 84 123 188 85 124 189 85 124 189 84 123 190 84 123 190 83 122 189 83 122 189 82 121 188 83 122 189 83 122 189 83 122 189 83 122 191 83 122 191 83 122 191 83 122 191 83 122 191 83 122 191 83 122 191 83 122 191 81 120 189 82 121 190 82 120 191 81 119 190 81 119 190 81 119 190 83 121 192 85 123 194 26 21 53 32 27 67 27 20 72 29 23 85 30 23 91 34 28 90 20 18 58 14 13 45 15 13 50 24 22 62 16 15 57 17 15 64 28 27 84 18 19 76 31 34 89 23 26 81 25 25 87 27 25 90 27 25 88 26 23 76 19 20 50 12 14 35 11 13 34 12 16 43 19 23 58 21 24 69 18 19 75 22 21 78 11 10 52 15 14 48 19 13 51 35 11 24 175 120 79 165 109 60 40 18 5 15 13 16 12 13 8 13 14 8 14 10 9 15 9 11 17 7 8 35 19 20 44 18 19 54 23 18 98 67 49 158 124 99 196 154 132 208 157 136 214 157 130 210 149 120 211 147 119 223 160 129 237 174 143 244 179 147 246 182 147 247 183 147 243 181 144 234 172 135 243 181 142 248 189 149 237 177 140 234 174 138 239 179 143 237 174 139 241 175 141 239 173 138 241 173 136 246 178 139 249 184 142 250 186 142 246 182 136 239 177 130 225 163 116 229 166 122 227 164 121 220 156 118 217 157 120 216 160 123 205 153 116 184 140 113 150 121 107 88 65 59 42 17 12 32 9 3 29 11 7 19 11 8 15 15 15 12 20 33 4 23 56 95 122 169 85 121 173 83 122 177 83 123 175 84 123 178 85 124 181 85 125 186 87 127 189 87 126 191 87 126 191 86 125 190 85 124 189 85 124 189 85 124 189 86 125 190 86 125 190 85 124 189 84 123 190 83 122 187 83 122 189 83 122 187 83 122 189 83 122 189 85 124 191 85 124 191 85 124 193 84 123 190 84 123 192 84 123 192 83 122 191 83 122 191 82 121 190 83 122 191 83 122 191 83 122 191 82 120 191 82 120 191 84 122 193 86 124 195 79 71 86 42 33 60 29 21 72 37 28 93 26 21 87 33 32 89 20 24 61 11 15 44 14 15 46 25 25 63 22 19 64 22 20 70 18 18 70 28 31 84 26 32 84 19 26 80 26 28 85 25 25 85 25 23 86 27 24 79 20 20 58 12 12 38 12 13 33 15 17 38 16 19 52 26 29 72 17 18 72 15 16 70 20 21 67 11 10 50 12 10 49 37 17 29 163 114 74 171 120 73 30 8 0 16 11 8 14 11 2 18 14 5 24 14 13 26 10 13 60 35 39 105 71 72 104 56 54 125 73 60 174 127 97 198 153 114 213 167 133 221 171 138 221 162 128 227 164 129 231 166 134 233 168 138 235 170 142 241 176 148 246 181 153 249 185 157 251 190 161 246 187 155 240 181 147 243 184 150 241 182 150 243 182 151 253 192 163 253 189 161 241 176 146 255 190 158 255 190 158 248 185 152 236 177 145 252 198 164 250 200 163 240 192 154 243 191 154 226 170 133 235 171 135 222 156 121 231 166 128 219 157 116 214 162 115 192 144 106 188 143 124 159 115 102 130 83 63 91 53 34 34 6 2 22 8 8 20 16 17 6 14 27 10 28 66 94 124 176 84 125 177 78 121 174 87 126 181 87 124 179 87 128 184 85 125 184 87 127 188 86 126 188 85 125 187 85 125 187 86 126 188 86 126 188 85 125 187 84 124 186 85 125 187 84 124 186 83 122 187 84 124 186 84 123 188 84 124 186 84 123 188 83 122 187 84 123 188 84 123 188 84 123 190 84 123 188 84 123 190 84 123 190 84 123 190 84 123 190 85 124 191 85 124 191 84 123 190 85 124 191 86 125 194 87 126 195 89 128 197 90 129 198 198 186 188 40 29 43 38 27 67 36 29 81 22 19 74 59 61 109 35 40 72 13 19 45 20 21 49 22 21 55 14 8 54 27 23 74 21 18 73 21 22 76 18 24 76 20 27 79 20 26 78 22 25 80 24 24 84 25 25 79 20 19 61 13 12 43 12 13 34 15 17 38 18 21 56 28 31 76 21 23 74 17 19 70 19 20 66 12 14 53 14 14 52 30 14 24 156 111 70 180 132 84 28 9 0 16 11 8 23 19 10 12 5 0 14 0 0 52 30 33 112 80 83 123 79 78 127 68 60 161 99 78 199 143 106 213 160 116 219 167 127 218 164 126 226 168 130 228 168 131 234 171 138 239 174 144 245 179 153 250 184 160 252 188 163 253 189 164 244 184 158 244 184 156 243 186 156 249 192 162 246 186 158 240 179 151 246 182 157 243 179 154 250 189 160 249 188 159 249 184 156 251 185 159 243 182 154 237 180 151 228 177 148 228 179 147 231 180 149 244 189 158 227 162 130 221 152 119 225 157 120 220 157 116 212 155 110 197 145 105 196 144 120 182 127 106 181 124 94 133 83 56 83 50 41 30 10 11 24 16 14 6 10 21 14 32 70 92 122 174 82 123 175 78 121 176 88 124 182 85 119 180 86 125 182 87 127 186 88 128 189 87 127 189 86 126 188 86 126 188 87 127 189 87 127 189 87 127 189 86 126 188 87 127 189 86 126 188 85 125 187 85 125 187 86 126 188 86 126 188 85 125 187 84 124 186 85 124 189 85 124 189 86 125 190 86 125 190 86 125 190 86 125 190 86 125 192 86 125 192 86 125 192 86 125 192 86 125 192 87 126 193 87 126 193 88 127 194 88 127 196 88 127 196 244 231 223 158 148 147 35 26 45 49 44 74 134 132 169 171 174 207 28 32 59 7 9 32 27 25 49 34 31 62 29 22 66 28 21 75 23 17 79 28 26 89 21 23 80 20 25 80 16 22 72 18 24 74 24 25 82 25 25 79 20 19 61 12 12 46 11 13 38 15 16 44 18 20 59 25 27 75 22 23 77 19 22 73 17 20 63 15 17 55 20 21 52 29 14 19 159 115 70 172 126 74 26 8 0 22 17 21 16 12 9 21 13 10 47 29 29 89 63 64 108 73 69 110 66 55 142 86 69 183 123 97 199 144 105 209 156 112 221 165 128 220 164 129 232 176 139 235 179 142 238 182 149 241 182 152 238 176 151 233 171 148 229 167 144 227 167 143 238 178 152 239 182 155 242 185 156 248 191 162 243 183 157 236 174 151 243 178 156 241 176 154 228 171 142 236 176 148 240 170 145 234 158 135 233 157 134 216 142 117 196 129 103 184 123 94 191 132 102 190 131 99 207 142 112 209 143 111 201 135 103 206 142 107 205 145 109 205 149 114 194 143 114 188 135 104 188 133 94 169 119 86 116 83 68 47 26 21 21 8 2 20 19 27 28 42 77 99 125 174 91 126 180 86 125 184 95 126 190 90 121 185 92 126 190 93 131 194 90 128 191 88 128 190 87 127 189 88 128 190 89 129 191 89 129 191 89 129 191 88 128 190 89 129 191 88 128 190 87 127 189 87 127 189 88 128 190 87 127 189 86 126 188 86 126 188 87 126 191 88 127 192 88 127 192 89 128 193 89 128 193 89 128 193 89 128 195 89 128 195 87 126 193 87 126 193 88 127 194 89 128 195 89 128 195 89 128 195 88 127 194 88 127 194 244 230 219 234 223 217 107 98 101 203 200 211 230 228 250 94 94 118 25 26 47 14 12 33 71 68 87 120 115 138 27 17 54 31 21 71 28 18 78 26 20 80 23 23 77 21 25 73 14 21 65 17 24 70 23 26 81 24 24 78 19 18 62 11 11 45 11 12 40 15 16 46 21 23 64 24 26 77 21 24 79 22 25 76 18 21 62 14 19 51 20 24 51 29 15 15 162 119 68 173 128 73 27 11 0 17 14 21 5 1 2 24 14 13 56 35 34 86 55 52 106 68 55 122 77 58 165 111 87 186 129 99 185 132 92 198 145 103 213 152 121 215 152 121 202 142 106 197 139 102 192 132 98 184 123 94 181 117 90 183 119 94 193 129 104 201 139 114 228 167 139 237 178 148 242 183 153 244 185 155 240 179 151 241 177 150 250 186 161 242 180 155 216 161 131 230 170 142 235 163 139 219 139 116 219 132 112 197 112 91 187 109 86 188 116 91 170 105 77 184 120 92 209 144 116 211 146 118 232 166 140 210 146 119 199 139 111 198 142 115 194 145 115 189 140 107 179 126 86 173 124 91 144 113 95 86 66 57 35 18 11 17 10 18 37 48 80 102 126 174 95 126 183 87 121 184 91 122 187 91 122 187 90 123 190 90 125 191 90 128 191 89 129 191 88 128 190 89 129 191 90 130 192 90 130 192 90 130 192 89 129 191 90 130 192 89 129 191 88 128 190 89 129 191 89 129 191 89 129 191 89 129 191 88 128 190 90 129 194 90 129 194 90 129 194 90 129 194 90 129 194 90 129 194 89 128 195 89 128 195 88 127 194 88 127 194 89 128 195 89 128 195 90 129 196 90 129 196 90 129 196 90 129 196 237 220 213 250 237 231 246 237 232 231 225 227 188 184 198 40 39 57 18 17 35 21 18 35 81 75 85 222 214 225 59 47 71 41 30 64 28 19 64 16 10 58 40 38 78 29 31 69 16 24 61 18 25 67 23 26 77 22 22 74 17 16 58 12 11 45 12 12 40 14 15 46 23 24 68 22 24 75 19 22 77 22 25 76 17 21 59 10 15 44 16 21 43 28 17 11 155 110 55 180 135 78 31 14 4 13 11 22 15 13 18 31 19 19 56 31 27 95 61 51 125 84 64 141 95 69 166 115 86 178 125 93 187 138 98 206 152 114 216 152 125 220 150 124 221 150 118 212 139 104 199 126 93 192 118 89 192 118 91 195 124 96 206 135 107 215 146 117 217 151 117 234 170 135 241 179 142 240 178 141 236 174 137 243 180 145 252 189 156 239 178 147 231 178 146 220 163 134 217 145 120 216 134 112 220 132 112 182 94 72 155 73 49 154 78 54 146 76 50 156 90 64 177 111 87 198 133 111 224 159 139 210 149 130 219 164 143 184 136 114 180 135 112 192 148 121 192 142 109 150 106 77 140 114 99 91 72 65 59 38 35 32 20 30 48 57 86 108 130 177 102 131 189 90 121 186 90 124 188 95 130 196 94 129 197 90 125 191 91 129 192 89 129 191 89 129 191 89 129 191 90 130 192 91 131 193 90 130 192 90 130 192 90 130 192 89 129 191 89 129 191 89 129 191 91 131 193 91 131 193 91 131 193 90 130 192 93 131 196 93 131 196 92 130 195 92 130 195 91 129 194 90 128 193 89 127 192 89 127 192 91 129 194 90 128 193 90 127 195 90 127 195 91 128 196 93 130 198 94 131 199 95 132 200 253 234 230 240 225 218 234 223 217 228 220 218 99 93 103 32 28 43 37 33 50 14 8 22 102 93 96 233 223 224 164 151 160 44 31 48 36 25 55 102 95 129 153 150 179 44 45 75 19 25 59 19 27 66 23 25 74 21 22 70 18 16 56 15 11 44 14 11 38 13 13 41 15 14 56 19 19 69 14 15 69 14 16 64 11 15 52 8 12 39 14 19 39 32 19 11 158 113 56 166 120 61 31 14 4 20 18 29 19 14 20 40 26 25 95 64 59 134 95 80 137 95 70 148 103 72 161 114 86 180 133 103 194 150 115 196 146 113 201 136 114 208 137 115 206 139 110 188 122 90 168 101 72 160 93 66 160 93 67 162 95 69 165 98 71 167 100 71 201 135 103 222 156 121 231 165 130 227 162 124 223 158 120 234 170 132 250 184 149 241 178 143 230 176 140 223 164 132 215 146 117 204 127 101 192 110 86 161 79 55 143 67 43 146 74 50 131 63 40 111 45 23 118 51 32 112 47 29 102 41 23 106 50 33 98 51 33 105 64 46 105 68 50 123 85 66 129 84 61 129 91 70 131 109 98 95 80 75 58 34 34 41 25 36 66 73 101 112 135 177 106 132 191 91 125 189 90 128 191 97 136 201 94 133 198 91 129 194 90 130 192 90 130 192 89 129 191 90 130 192 91 131 193 91 131 193 91 131 193 90 130 192 91 131 193 90 130 192 90 130 192 91 131 193 92 132 194 92 132 194 92 132 194 91 131 193 94 132 197 94 132 197 93 131 196 93 131 196 92 130 195 91 129 194 90 128 193 90 128 193 92 130 195 92 130 195 91 128 196 91 128 196 92 129 197 94 131 199 96 133 201 97 134 202 241 220 215 247 230 223 235 222 216 238 228 227 94 86 97 40 33 49 16 9 25 20 12 23 87 77 75 235 225 215 247 234 226 114 103 101 182 170 182 231 222 241 134 129 151 27 27 53 19 25 59 20 25 65 23 24 72 23 21 68 22 18 53 19 15 42 16 13 32 14 11 32 18 14 47 32 29 72 24 22 69 10 11 55 8 11 44 11 13 38 16 19 38 32 17 10 174 125 69 158 109 51 34 16 6 19 16 25 13 4 7 48 29 23 129 93 81 146 101 80 132 87 58 140 95 64 150 108 83 179 139 114 186 149 122 180 134 108 194 133 114 195 132 114 184 132 110 163 117 94 144 98 75 136 88 68 136 88 68 146 94 73 162 105 85 178 117 96 170 106 79 197 132 104 221 152 121 230 159 129 227 156 124 235 164 132 255 182 153 254 185 152 243 181 144 225 163 124 201 132 99 187 116 86 173 102 74 151 84 57 112 48 23 82 20 0 105 42 24 129 66 49 151 84 68 139 76 61 134 77 60 115 68 50 71 35 13 66 37 19 135 105 97 117 84 79 74 34 26 96 61 55 101 83 81 104 89 92 62 35 40 55 35 47 84 90 114 114 136 175 100 129 185 89 124 188 84 128 189 86 132 192 83 127 188 88 129 191 92 132 193 91 131 192 90 130 191 91 131 192 91 131 192 91 131 192 91 131 192 90 130 191 93 133 194 93 133 194 92 132 193 92 132 193 92 132 193 93 133 194 92 132 193 91 131 192 94 132 195 94 132 195 94 132 195 94 132 195 94 132 195 93 131 194 93 131 196 92 130 195 93 131 196 93 131 196 93 131 196 93 131 196 94 132 197 95 133 198 96 134 199 96 134 199 246 227 213 255 241 231 253 239 236 234 223 227 217 209 222 59 52 68 36 28 43 24 14 22 108 97 91 228 216 200 251 238 221 245 233 219 223 211 211 194 184 195 44 39 61 20 17 48 18 22 59 19 24 66 25 23 72 25 22 67 25 20 52 22 17 37 19 13 23 16 10 22 34 29 52 58 52 86 44 39 79 17 15 54 13 12 43 15 15 41 17 18 39 29 11 7 169 118 65 170 117 63 40 20 9 16 9 16 34 23 19 77 53 41 146 105 85 150 103 77 137 90 60 126 82 55 110 72 53 117 81 65 97 65 44 79 38 18 93 34 18 74 12 0 63 14 0 63 20 3 70 27 11 75 29 14 71 25 9 71 22 5 87 32 12 105 44 23 140 74 48 179 110 81 223 150 118 252 177 145 252 177 145 249 175 140 255 187 155 255 192 157 250 183 140 233 166 123 203 134 95 170 102 67 121 56 26 116 55 27 147 91 68 191 136 116 164 106 92 178 116 103 189 123 111 196 132 120 164 106 92 184 137 117 255 225 199 130 102 81 49 20 16 70 38 39 70 30 30 77 42 40 87 67 68 99 80 82 86 55 60 79 53 64 115 113 134 130 146 180 109 136 189 99 135 197 93 138 197 86 135 191 83 130 186 93 138 197 93 133 194 92 132 193 91 131 192 91 131 192 92 132 193 92 132 193 91 131 192 90 130 191 96 136 197 95 135 196 94 134 195 93 133 194 93 133 194 93 133 194 92 132 193 91 131 192 94 132 195 95 133 196 95 133 196 95 133 196 95 133 196 95 133 196 95 133 198 95 133 198 93 131 196 94 132 197 94 132 197 95 133 198 95 133 198 95 133 198 95 133 198 95 133 198 193 177 152 247 234 217 214 203 201 224 214 223 239 230 247 172 165 183 32 24 37 66 55 61 208 195 189 241 228 212 239 224 203 235 222 203 223 212 206 101 91 102 41 32 63 21 15 59 21 22 68 21 23 71 24 20 71 32 25 69 19 10 37 80 72 85 57 49 47 55 47 45 147 139 150 166 158 181 32 25 58 23 17 51 10 7 36 17 15 39 15 13 37 34 14 13 166 112 65 166 110 59 52 25 14 28 16 16 49 31 19 124 96 74 169 123 97 147 96 67 122 75 47 98 56 32 57 21 9 76 43 34 83 54 40 79 42 26 72 16 1 163 101 86 233 174 160 215 157 145 184 126 114 166 108 96 168 110 96 193 134 116 199 134 114 146 79 53 137 63 34 121 45 11 175 97 58 208 131 87 251 174 130 251 177 132 241 168 123 236 164 116 222 148 99 199 125 76 164 91 48 174 105 66 227 160 131 182 122 96 156 104 83 89 38 21 78 24 12 85 27 16 109 42 34 125 58 49 166 104 89 162 107 86 190 146 117 229 193 167 87 53 44 73 38 36 121 77 74 99 59 57 89 61 58 112 84 81 138 96 97 120 84 88 141 127 140 119 125 151 116 136 187 102 134 193 93 136 191 94 141 195 89 134 189 89 134 191 93 133 194 94 132 194 94 132 194 94 132 194 93 131 193 93 131 193 93 131 193 93 131 193 95 133 195 95 133 195 96 134 196 97 135 197 97 135 197 96 134 196 96 134 196 96 134 196 95 133 196 95 133 196 95 133 196 96 134 197 96 134 197 96 134 197 95 133 196 95 133 196 96 134 197 95 133 196 95 133 198 95 133 198 95 133 198 95 133 198 96 134 199 96 134 199 159 147 121 225 213 197 78 67 73 64 52 72 100 92 116 155 147 170 96 88 103 63 54 59 214 200 197 233 219 206 230 218 196 230 217 198 199 189 180 48 38 47 33 23 58 23 17 65 20 18 68 23 23 75 24 20 71 33 24 67 26 16 40 80 69 75 118 108 99 139 129 119 190 179 183 86 75 89 20 12 36 12 6 34 20 14 42 17 13 38 15 11 36 33 13 15 169 112 69 164 106 56 55 23 10 67 48 44 97 70 51 147 112 84 162 111 80 136 81 51 107 57 30 90 47 28 45 8 0 52 19 14 38 8 0 69 32 16 165 112 96 174 112 99 180 120 110 141 82 74 102 43 35 90 32 21 88 30 16 84 25 7 138 73 53 205 135 110 194 119 88 190 112 76 201 119 79 205 124 79 228 150 104 230 153 107 227 153 106 206 132 83 203 126 74 184 104 53 188 107 62 187 109 71 176 102 73 158 92 70 89 32 15 118 65 51 72 15 6 95 36 28 94 27 18 93 23 11 114 45 29 136 72 47 122 65 35 111 60 31 113 67 51 103 59 48 95 47 35 74 28 15 70 32 19 111 71 59 161 107 97 186 135 131 194 164 166 162 153 172 113 126 171 112 138 197 103 138 194 95 136 190 96 137 193 102 142 201 96 135 194 96 134 196 96 134 196 95 133 195 95 133 195 95 133 195 95 133 195 95 133 195 97 135 197 97 135 197 98 136 198 98 136 198 98 136 198 97 135 197 97 135 197 96 134 196 96 134 197 96 134 197 96 134 197 97 135 198 96 134 197 96 134 197 96 134 197 95 133 196 94 132 195 95 133 196 95 133 198 96 134 199 96 134 199 97 135 200 98 136 201 98 136 201 149 137 123 195 184 182 42 30 52 42 32 67 22 14 53 35 28 61 32 25 43 32 25 32 132 121 119 228 215 206 244 231 215 241 229 213 224 215 206 118 111 118 37 30 63 27 21 67 24 21 74 30 27 82 26 19 73 27 19 60 26 14 36 57 45 47 119 107 93 123 111 95 118 107 105 42 30 40 22 13 34 18 9 36 15 9 35 18 12 38 21 17 44 32 11 16 172 114 74 182 121 74 58 20 7 74 45 39 144 108 86 167 122 91 151 94 64 118 59 29 88 34 10 84 37 21 88 47 43 79 41 38 54 20 8 133 91 75 155 98 81 163 104 90 127 74 66 84 37 31 74 27 21 69 22 14 72 25 15 71 22 8 98 41 24 123 58 38 173 101 76 173 94 64 174 91 59 210 126 90 213 129 93 195 113 75 200 121 82 196 117 74 195 113 63 170 87 35 209 120 78 202 112 78 174 86 62 117 36 17 131 59 45 94 30 18 50 0 0 82 20 9 88 20 7 96 23 8 104 26 4 132 52 25 136 60 28 144 74 40 166 107 75 142 87 57 124 67 38 124 67 40 131 77 49 170 113 86 193 125 102 156 91 73 169 119 112 187 159 171 127 128 172 119 138 196 101 127 184 100 131 188 107 142 200 100 136 196 98 137 196 98 137 196 98 137 196 98 137 196 98 137 196 98 137 196 97 136 195 97 136 195 99 138 197 99 138 197 100 139 198 100 139 198 99 138 197 99 138 197 98 137 196 97 136 195 98 136 198 98 136 198 98 136 198 98 136 198 97 135 197 97 135 197 97 135 198 96 134 197 95 133 196 96 134 197 97 135 198 98 136 199 98 136 199 98 136 199 98 136 199 98 136 199 126 115 119 118 107 121 29 19 56 34 24 74 32 24 73 26 19 60 15 10 32 18 12 22 48 39 40 208 197 193 240 227 219 214 203 197 236 228 226 205 199 209 46 42 69 12 10 50 17 17 67 27 27 81 27 22 76 26 17 60 34 22 42 71 59 61 129 117 103 115 103 87 46 35 31 17 6 14 26 17 36 27 18 45 11 5 31 16 12 37 24 20 47 32 11 18 156 101 62 190 127 83 65 23 9 84 47 41 189 143 120 178 123 92 164 99 69 156 91 63 115 57 35 121 70 53 93 46 38 98 54 45 80 39 21 126 78 56 117 54 36 148 82 66 91 33 22 81 28 20 93 43 32 62 12 1 54 2 0 84 31 15 133 72 54 154 86 65 124 48 22 146 62 34 157 70 40 207 118 84 193 107 72 229 147 110 254 174 137 250 173 131 227 147 98 213 127 78 223 130 89 219 122 90 175 79 55 191 100 81 170 89 72 150 78 63 137 70 54 129 62 45 127 54 37 136 58 36 151 65 40 159 69 42 173 84 52 188 106 69 205 136 95 187 124 83 197 133 95 177 113 77 162 97 59 162 92 56 178 102 68 127 51 25 110 44 28 174 131 138 137 132 172 117 133 192 104 124 183 104 130 189 111 141 203 94 128 189 102 138 198 100 139 198 100 139 198 100 139 198 100 139 198 100 139 198 100 139 198 100 139 198 101 140 199 101 140 199 101 140 199 101 140 199 101 140 199 100 139 198 99 138 197 98 137 196 98 136 198 98 136 198 99 137 199 99 137 199 99 137 199 99 137 199 98 136 199 98 136 199 98 136 199 99 137 200 100 138 201 100 138 201 99 137 200 98 136 199 97 135 198 96 134 197 82 72 97 66 56 90 42 32 82 20 12 69 21 16 72 31 28 73 18 15 42 17 13 27 32 26 30 207 197 198 154 143 149 50 39 47 108 100 113 180 176 193 115 113 137 20 20 54 20 22 70 26 27 81 28 25 78 22 15 57 23 11 35 72 59 66 126 113 104 135 122 113 110 98 100 14 4 15 17 9 32 19 13 39 20 16 41 15 13 37 18 15 42 32 13 17 154 102 63 185 122 78 68 22 7 114 69 64 182 125 106 137 71 45 138 66 41 158 88 63 155 93 70 153 96 76 147 91 74 167 115 94 148 94 66 128 69 39 133 61 37 131 53 33 127 48 31 121 44 28 128 52 36 129 56 37 153 82 62 153 82 60 156 82 57 188 109 79 202 115 85 217 127 93 220 128 91 223 133 96 164 79 40 237 158 117 255 192 151 255 193 150 246 169 123 187 105 58 246 154 117 240 143 111 228 128 102 206 110 86 189 103 80 205 127 104 191 116 93 184 110 85 178 98 73 175 89 62 185 94 65 204 111 80 210 117 83 197 112 73 201 124 80 202 134 89 218 151 108 186 117 76 177 102 62 174 95 54 199 117 79 205 125 92 118 41 21 158 106 110 129 123 161 112 128 187 117 137 196 107 131 193 104 135 199 102 138 200 101 140 199 101 140 199 101 140 199 101 140 199 101 140 199 101 140 199 101 140 199 101 140 199 102 141 200 102 141 200 102 141 200 102 141 200 101 140 199 100 139 198 99 138 197 98 137 196 100 136 198 100 136 198 101 137 199 102 138 200 102 138 200 102 138 200 102 138 200 102 138 200 101 137 199 102 138 200 103 138 202 103 138 202 102 137 201 101 136 200 100 135 199 99 134 198 41 33 72 25 18 60 23 16 68 30 25 81 30 27 82 29 28 72 11 11 39 13 10 27 44 38 48 209 199 210 86 74 94 32 22 49 39 29 63 32 28 61 52 52 78 21 26 56 20 24 69 17 20 71 24 24 76 29 23 67 29 19 44 68 56 66 98 87 83 126 115 111 157 146 154 88 77 94 18 9 36 17 12 42 19 16 43 18 18 42 15 15 39 27 12 15 159 110 69 177 116 71 61 12 0 129 78 75 157 91 75 136 61 40 165 87 67 170 93 73 163 97 73 152 90 65 193 132 104 201 138 107 163 101 64 162 94 57 136 55 28 136 48 26 129 37 16 138 46 23 167 79 55 208 124 98 231 150 123 220 141 111 205 122 92 216 130 97 215 122 88 244 150 114 255 172 134 238 146 107 161 78 38 230 152 113 252 183 141 255 194 152 255 186 143 228 149 108 236 148 112 251 158 125 223 128 98 207 116 85 238 153 122 228 148 115 241 162 131 253 173 140 247 160 130 231 140 109 209 114 82 235 141 107 236 146 111 218 135 95 217 140 98 212 141 99 210 142 103 184 115 76 180 98 60 214 129 90 222 139 99 203 120 86 144 62 40 161 105 106 131 127 164 106 128 185 117 139 197 105 134 194 96 134 196 105 147 207 101 141 200 102 141 200 102 141 200 102 141 200 102 141 200 102 141 200 102 141 200 102 141 200 103 142 201 103 142 201 103 142 201 102 141 200 102 141 200 101 140 199 100 139 198 99 138 197 101 137 199 101 137 199 102 138 200 102 138 200 103 139 201 103 139 201 103 139 201 103 139 201 100 136 198 101 137 199 102 137 201 103 138 202 103 138 202 103 138 202 103 138 202 103 138 202 21 14 55 25 20 61 25 22 67 27 28 74 20 23 68 17 21 59 10 11 39 14 13 31 47 43 57 153 144 165 24 12 52 35 22 75 38 27 87 18 13 67 20 20 58 9 15 47 22 29 71 19 25 73 21 23 74 24 21 66 24 15 44 29 18 34 26 16 17 39 29 30 93 82 96 113 103 127 25 18 51 26 20 54 10 10 36 17 19 42 14 19 39 25 14 12 162 117 75 181 123 75 58 9 0 104 48 47 134 64 52 127 48 31 182 99 83 169 88 69 156 86 60 159 93 61 193 123 87 218 147 105 186 114 66 191 112 69 164 75 45 157 61 37 160 62 37 181 86 58 205 116 86 242 157 128 242 159 129 244 161 131 230 145 114 214 125 93 203 110 76 253 156 123 255 181 146 217 124 90 186 102 66 246 170 134 255 190 155 246 180 145 255 187 151 236 162 125 196 116 81 249 167 130 254 170 133 222 140 102 201 122 81 233 154 111 248 166 126 243 155 117 236 142 106 227 130 95 208 114 78 224 136 100 230 151 112 216 142 103 222 148 113 215 144 112 211 148 117 198 131 102 185 104 74 224 138 105 249 169 132 214 134 101 147 65 44 172 116 119 132 134 172 103 134 189 103 134 191 104 139 197 94 141 197 96 145 203 100 142 200 102 141 200 103 142 201 103 142 201 103 142 201 103 142 201 103 142 201 104 143 202 103 142 201 103 142 201 103 142 201 103 142 201 103 142 201 102 141 200 101 140 199 100 139 198 102 138 198 102 138 198 103 139 201 103 139 201 103 139 201 103 139 201 102 138 200 102 138 200 101 137 199 101 137 199 102 137 201 102 137 201 103 138 202 103 138 202 104 139 203 104 139 203 24 22 61 18 16 55 22 21 65 16 17 61 25 28 71 24 26 65 13 14 42 12 13 34 33 30 51 59 53 81 30 18 66 36 25 85 28 19 84 27 21 81 20 21 65 17 21 59 15 19 64 25 29 77 24 26 74 19 18 60 22 19 48 16 13 30 15 9 19 19 13 23 24 16 37 32 26 54 25 21 56 25 24 58 12 13 43 9 13 38 12 17 37 27 16 14 146 102 57 179 121 71 63 16 0 87 36 33 153 83 73 124 41 25 201 114 95 210 125 104 184 108 82 147 74 41 190 112 74 210 129 86 201 121 72 197 114 70 203 114 82 188 94 66 174 77 45 205 111 77 198 109 77 219 134 103 207 126 96 190 111 80 179 98 68 200 117 85 255 178 144 255 168 133 240 150 115 187 98 64 220 137 105 253 177 145 255 190 159 255 190 158 246 180 146 255 189 155 199 125 90 228 152 116 239 161 123 255 189 149 246 171 129 213 136 92 212 133 90 195 112 70 208 119 79 195 106 66 199 111 73 202 120 80 229 156 113 223 151 111 207 131 99 207 133 104 199 136 105 196 129 100 202 118 92 185 95 69 222 142 109 221 144 114 150 75 56 174 125 129 124 130 166 108 141 194 102 133 188 107 143 201 97 144 200 94 143 201 102 142 201 103 142 201 103 142 201 103 142 201 104 143 202 104 143 202 104 143 202 104 143 202 104 143 202 104 143 202 104 143 202 104 143 202 104 143 202 103 142 201 102 141 200 101 140 199 104 140 200 104 140 200 104 140 202 103 139 201 103 139 201 102 138 200 101 137 199 101 137 199 103 139 201 103 139 201 102 137 201 102 137 201 102 137 201 103 138 202 103 138 202 103 138 202 18 19 50 20 20 56 20 18 65 24 22 72 29 27 74 23 22 62 14 13 44 14 11 38 23 20 49 23 17 53 25 19 65 30 23 74 27 23 74 21 19 68 18 19 65 23 21 68 26 19 71 32 25 77 22 23 67 18 24 58 12 19 45 6 14 33 12 15 32 9 10 30 14 11 38 16 15 47 14 17 52 18 22 59 7 12 44 13 17 46 14 16 41 29 13 13 153 102 55 182 122 70 52 11 0 62 19 13 170 104 92 134 53 32 223 132 105 213 122 91 176 95 66 155 76 46 203 114 82 208 118 83 216 128 90 201 118 78 209 129 94 186 103 69 166 76 39 199 109 72 196 112 76 193 114 83 195 121 92 198 127 99 230 159 131 255 189 157 255 188 153 251 173 135 228 149 110 183 103 66 241 165 131 251 176 147 255 195 170 255 190 163 255 193 160 255 195 160 230 161 128 204 131 99 250 174 142 255 182 150 255 182 148 255 197 160 255 183 145 243 171 131 225 152 111 210 137 96 215 140 98 232 157 115 236 161 119 227 150 108 218 136 99 210 132 94 200 133 90 197 128 87 179 89 65 143 48 28 196 110 83 216 142 117 168 109 95 150 117 124 126 134 170 113 141 191 106 135 191 104 136 195 104 142 204 104 146 206 105 144 203 106 142 200 105 141 199 105 141 199 106 142 200 107 143 201 106 142 200 106 142 200 106 142 200 105 141 199 106 142 200 108 144 202 107 143 201 105 141 199 103 139 197 104 140 198 106 142 202 106 142 202 105 141 203 104 140 202 103 139 201 102 138 200 101 137 199 101 137 199 102 138 200 102 138 200 103 138 202 103 138 202 103 138 202 103 138 202 103 138 202 103 138 202 18 22 51 19 22 57 21 19 68 24 22 72 29 26 73 22 20 59 14 11 40 12 9 36 21 17 50 20 15 55 21 18 63 26 23 70 24 23 67 19 18 62 16 17 63 21 17 67 29 18 74 33 24 77 22 24 65 16 25 56 9 21 45 4 17 36 12 17 39 12 12 38 11 7 40 20 18 55 11 13 51 10 14 51 19 24 56 13 17 46 17 17 41 35 15 14 162 110 63 190 130 76 38 2 0 76 39 30 163 104 88 159 81 58 230 141 107 213 123 88 159 76 46 156 75 46 197 108 78 202 109 76 218 128 93 216 132 96 208 134 97 178 104 65 186 103 61 209 124 83 228 148 111 244 169 137 252 183 154 255 193 167 255 200 172 255 190 161 251 183 148 255 196 157 206 132 93 218 144 105 247 174 141 248 177 147 255 188 163 255 198 172 255 196 165 250 187 154 255 195 163 213 142 112 230 153 125 252 175 147 255 186 156 255 182 151 253 183 149 254 186 149 241 176 136 231 164 122 234 165 123 237 164 121 227 150 108 217 138 97 222 140 102 212 135 93 176 110 60 215 144 100 202 110 87 151 54 35 175 88 61 212 142 117 182 134 124 157 135 147 129 142 177 114 142 192 107 133 190 104 134 194 104 140 202 101 139 201 107 143 203 106 142 200 105 141 199 105 141 199 106 142 200 107 143 201 106 142 200 106 142 200 108 144 202 106 142 200 106 142 200 107 143 201 107 143 201 105 141 199 105 141 199 106 142 200 106 142 202 106 142 202 105 141 203 104 140 202 103 139 201 103 139 201 102 138 200 102 138 200 103 139 201 103 139 201 103 138 202 103 138 202 103 138 202 104 139 203 104 139 203 104 139 203 17 22 52 18 22 57 19 20 68 21 22 70 27 26 68 20 20 54 12 12 36 10 10 34 20 19 50 20 18 57 22 19 64 27 24 71 25 24 68 21 20 62 17 18 62 21 19 68 28 19 74 31 24 76 20 23 64 14 23 54 10 20 45 8 16 37 16 16 40 16 11 41 20 13 47 19 13 51 13 11 50 10 13 48 21 24 55 7 9 34 19 17 38 37 18 14 153 101 53 185 128 73 87 49 26 183 147 135 233 177 160 184 114 88 219 135 99 218 133 96 154 75 44 178 102 70 207 123 89 192 103 69 202 114 78 214 130 93 207 133 94 190 119 77 161 83 37 185 104 61 243 165 127 255 199 166 255 193 164 255 197 169 255 205 177 255 198 169 255 198 163 249 180 141 180 111 70 238 169 130 250 182 147 255 189 157 254 188 162 255 198 171 255 196 165 253 188 156 255 189 158 239 165 136 187 107 80 236 155 128 255 174 145 255 176 146 255 187 155 255 184 149 235 166 127 223 151 111 228 155 114 226 151 109 217 138 97 212 133 92 213 133 96 191 116 76 189 125 77 215 146 104 242 152 128 182 87 67 189 102 74 192 124 101 177 136 134 147 134 152 120 138 178 108 139 193 106 137 194 109 141 202 110 145 209 105 143 206 107 143 203 106 142 200 106 142 200 106 142 200 106 142 200 107 143 201 107 143 201 106 142 200 109 145 203 107 143 201 106 142 200 107 143 201 107 143 201 106 142 200 106 142 200 108 144 202 106 142 202 106 142 202 105 141 201 105 141 201 104 140 202 104 140 202 104 140 202 104 140 202 103 139 201 103 139 201 104 140 202 104 140 202 104 139 203 104 139 203 104 139 203 104 139 203 15 20 52 17 21 58 18 19 67 21 22 68 26 26 64 20 21 49 13 14 34 12 13 33 19 18 49 20 18 57 23 20 67 26 24 73 26 25 69 23 22 64 20 22 63 23 21 68 26 19 71 28 24 75 18 21 62 12 21 52 8 18 43 7 13 35 18 14 41 17 8 39 26 14 50 17 6 46 21 17 54 15 15 49 16 17 45 15 16 37 18 15 32 27 8 1 125 75 24 215 159 102 180 140 115 209 173 159 233 185 165 216 152 124 239 161 123 198 116 78 185 111 76 220 147 114 223 145 109 185 103 66 182 97 60 201 117 80 199 126 83 204 133 87 206 125 80 190 107 65 226 146 109 255 185 153 255 192 165 255 194 169 255 183 158 247 173 146 236 161 129 178 104 69 178 102 66 231 157 120 243 170 137 255 188 157 255 188 162 249 183 157 255 194 164 255 197 165 255 197 167 251 176 147 180 99 72 162 78 52 213 127 102 231 147 119 232 151 122 227 148 117 226 150 116 236 160 124 246 171 132 245 167 128 227 148 109 207 128 89 197 119 83 190 118 80 196 132 88 219 150 109 248 162 135 226 134 111 188 103 72 190 124 102 166 135 140 138 135 162 116 139 183 107 142 198 107 139 198 107 141 202 104 142 205 99 139 201 108 144 204 107 143 201 106 142 200 106 142 200 107 143 201 107 143 201 107 143 201 107 143 201 108 144 202 106 142 200 106 142 200 107 143 201 108 144 202 106 142 200 107 143 201 108 144 202 106 142 202 106 142 202 106 142 202 105 141 201 105 141 203 105 141 203 106 142 204 106 142 204 104 140 202 104 140 202 104 140 202 104 140 202 105 140 204 105 140 204 105 140 204 105 140 204 15 19 56 17 20 61 17 19 67 19 22 65 23 26 59 17 22 44 11 14 29 11 14 29 15 16 44 17 17 55 21 19 66 24 22 71 25 24 68 21 23 64 21 23 64 24 23 67 25 21 71 26 24 73 16 21 61 11 20 51 10 17 43 8 12 37 18 11 42 17 6 40 22 7 46 20 8 48 25 19 57 16 15 47 15 15 39 26 25 43 16 10 22 50 31 17 171 124 70 211 157 97 189 148 120 209 172 156 230 188 164 209 154 123 226 154 114 214 137 95 213 144 105 238 170 133 224 155 114 185 110 70 183 98 61 197 112 73 189 116 71 203 129 82 229 142 99 201 109 70 204 114 80 227 138 108 249 163 138 252 166 143 219 133 110 198 110 86 169 82 54 165 76 46 226 140 107 243 158 127 251 168 138 255 184 157 255 193 170 255 184 161 255 190 161 255 186 157 253 179 152 255 185 159 244 160 136 192 104 80 178 88 64 183 93 67 165 78 51 166 81 52 195 112 80 227 147 112 236 156 119 238 159 120 227 145 107 197 117 80 195 121 86 195 125 89 191 128 87 214 146 107 244 160 132 210 123 96 183 99 65 200 138 117 153 133 145 126 136 172 112 142 192 109 145 203 107 141 202 108 142 203 106 144 207 102 142 203 108 144 204 107 143 201 106 142 200 106 142 200 107 143 201 108 144 202 107 143 201 107 143 201 107 143 201 106 142 200 106 142 200 109 145 203 109 145 203 107 143 201 106 142 200 107 143 201 107 141 202 107 141 202 107 141 202 107 141 202 107 141 202 107 141 202 108 142 205 108 142 205 106 140 203 106 140 203 106 140 203 106 140 203 107 141 204 107 141 204 107 141 205 107 141 205 17 21 59 19 22 63 18 20 68 19 22 65 21 24 55 15 20 39 9 13 24 10 14 26 15 16 44 17 16 56 21 19 68 24 22 72 23 24 70 23 25 66 22 24 65 24 23 65 24 20 70 26 24 71 17 21 59 14 20 52 11 18 44 12 14 39 19 14 44 17 7 41 20 8 46 23 13 50 20 14 50 20 17 46 18 15 36 12 8 22 32 21 29 142 119 103 214 165 107 217 163 101 185 141 112 219 180 163 231 194 167 209 163 129 213 148 108 227 154 113 212 144 105 230 165 125 225 160 118 197 126 84 191 106 69 195 107 69 186 112 65 199 125 78 232 145 102 226 132 94 199 106 73 164 73 44 161 69 44 173 81 58 173 81 60 174 79 57 142 48 22 215 121 93 254 163 134 243 154 124 247 162 133 254 173 146 255 187 164 254 180 155 254 185 156 255 186 157 249 175 148 251 171 146 240 154 131 232 141 120 183 91 68 229 137 114 242 150 125 235 145 119 245 158 130 246 161 130 227 143 109 220 138 101 219 137 100 201 123 85 202 129 94 189 121 86 202 140 101 220 155 117 229 150 119 188 103 74 219 135 101 194 135 117 142 133 152 115 136 181 106 141 195 104 144 203 104 140 200 108 142 203 109 147 209 108 146 208 109 145 205 108 144 202 107 143 201 107 143 201 108 144 202 108 144 202 108 144 202 107 143 201 107 143 201 106 142 200 107 143 201 109 145 203 109 145 203 107 143 201 106 142 200 106 142 200 107 141 202 107 141 202 107 141 202 107 141 202 107 141 202 108 142 203 108 142 205 108 142 205 107 141 204 107 141 204 107 141 204 107 141 204 107 141 204 107 141 204 108 142 206 108 142 206 18 20 61 20 21 65 19 20 68 19 20 64 20 24 53 14 20 36 7 14 22 8 14 26 14 18 45 17 19 58 21 21 71 23 23 75 23 24 72 23 24 68 22 24 65 23 22 66 22 19 66 24 22 69 15 19 57 14 20 52 12 19 45 12 16 41 19 16 45 14 9 41 20 13 47 20 14 48 15 12 43 21 19 43 17 13 30 23 13 22 97 81 84 209 181 160 225 175 116 222 167 103 181 132 100 223 182 162 224 189 161 187 144 109 201 139 98 231 162 120 215 147 108 220 157 114 222 161 116 200 131 89 192 107 70 193 103 66 193 116 70 202 128 79 206 125 80 230 145 104 227 142 105 213 127 94 216 129 101 230 140 114 222 132 108 194 102 77 164 73 46 236 147 117 223 136 106 212 129 97 228 149 118 232 157 128 244 173 145 240 170 144 231 162 133 224 153 125 232 156 130 209 129 104 213 127 104 213 122 101 164 69 49 192 97 75 245 151 126 254 160 135 253 162 135 241 152 122 223 139 105 214 132 95 214 132 95 213 135 99 202 129 94 189 123 88 201 137 101 220 155 117 205 129 95 219 139 106 230 150 115 189 136 120 141 141 167 113 143 193 106 145 202 104 144 205 102 138 196 104 139 197 105 141 199 102 138 198 109 145 203 108 144 202 107 143 201 107 143 201 108 144 202 108 144 202 108 144 202 108 144 202 109 145 203 107 143 201 108 144 202 109 145 203 109 145 203 107 143 201 106 142 200 107 143 201 108 142 203 107 141 202 107 141 202 107 141 202 107 141 202 108 142 203 108 142 203 108 142 203 107 141 204 107 141 204 107 141 204 108 142 205 108 142 205 108 142 205 108 142 205 108 142 205 18 17 57 20 19 61 20 18 65 19 21 62 22 23 53 14 20 36 8 15 23 10 16 28 12 16 43 16 18 57 19 19 71 20 20 74 20 21 69 20 21 67 19 20 64 20 19 63 20 14 62 22 19 64 14 16 54 13 20 49 12 19 45 11 17 41 16 18 43 11 11 39 13 10 41 16 15 46 17 17 43 15 13 34 19 13 25 69 57 61 175 154 153 203 167 145 216 164 104 225 167 103 185 132 100 226 181 160 227 192 162 167 129 92 193 135 95 215 150 108 235 170 130 219 156 113 211 153 107 190 123 80 191 105 70 199 109 74 204 126 80 201 128 77 195 118 72 214 135 92 221 139 101 232 148 114 244 157 129 248 161 134 223 133 107 161 71 45 144 54 27 168 81 53 143 60 30 148 69 38 176 101 70 180 109 79 196 127 98 201 131 105 202 133 104 197 126 98 168 92 66 159 79 54 117 33 9 145 57 35 120 25 5 156 60 38 215 119 95 239 145 120 237 146 119 226 137 107 230 143 113 222 139 105 213 133 96 221 145 109 205 135 99 190 124 89 197 133 97 169 101 64 219 145 108 237 161 125 202 127 95 197 151 138 129 135 167 103 138 194 102 142 203 105 145 206 109 144 200 113 146 200 112 147 203 108 143 199 109 145 203 108 144 202 107 143 201 107 143 201 108 144 202 109 145 203 108 144 202 108 144 202 111 147 205 109 145 203 108 144 202 109 145 203 108 144 202 107 143 201 107 143 201 109 145 203 108 142 203 107 141 202 107 141 202 107 141 202 107 141 202 108 142 203 108 142 203 108 142 203 108 142 205 108 142 205 108 142 205 108 142 205 108 142 205 108 142 205 109 143 206 109 143 206 13 11 48 27 25 64 21 18 63 16 15 55 37 36 67 14 17 36 8 12 23 14 18 30 22 23 51 7 6 46 21 21 73 27 27 81 21 21 71 19 20 66 13 14 58 22 21 65 25 17 64 31 24 68 19 22 57 9 16 44 9 17 40 13 21 42 14 20 42 4 9 31 10 14 39 9 13 38 15 20 40 18 20 35 77 70 78 161 145 146 195 165 163 210 168 144 220 164 103 221 162 96 190 136 102 228 182 159 215 180 150 168 130 94 191 138 98 220 157 116 200 135 95 197 134 91 144 86 40 177 112 70 198 114 80 199 110 76 207 126 81 214 141 90 201 127 80 207 130 86 220 141 102 223 141 104 231 146 115 215 128 100 167 77 50 144 54 27 220 133 105 188 103 74 122 41 11 119 40 10 122 47 16 130 59 29 142 73 44 142 73 44 137 66 38 133 59 32 151 75 49 133 53 28 150 66 42 212 126 103 166 74 51 114 19 0 171 77 52 208 114 89 217 126 99 209 120 90 222 137 106 212 129 95 215 137 99 222 148 109 208 139 100 207 139 102 191 125 90 179 111 74 183 111 71 189 117 79 215 148 119 181 142 135 127 140 175 104 143 202 103 143 204 105 144 203 108 143 197 110 141 195 110 143 197 112 145 199 108 143 199 109 144 202 110 145 203 109 144 202 109 144 202 108 143 201 109 144 202 110 145 203 112 147 205 111 146 204 109 144 202 108 143 201 107 142 200 106 141 199 106 141 199 105 140 198 107 141 202 106 140 201 106 140 201 106 140 201 108 142 203 108 142 203 108 142 203 107 141 202 108 142 203 108 142 203 108 142 205 108 142 205 109 143 206 110 144 207 111 145 208 111 145 208 29 24 54 22 17 49 19 12 53 36 31 71 21 18 47 19 20 40 15 17 29 0 3 18 17 18 46 22 21 61 16 14 63 16 16 68 21 22 70 16 17 63 15 16 62 16 15 59 22 14 61 23 16 58 14 14 48 10 16 42 10 18 37 12 19 37 15 20 39 15 20 39 7 12 32 15 20 40 16 22 38 94 97 106 169 160 163 178 158 157 200 167 160 212 166 140 221 163 100 223 162 97 193 137 100 228 180 157 215 178 149 181 143 107 193 141 104 218 156 117 212 149 108 202 139 96 212 153 109 226 161 121 211 130 100 195 109 76 202 124 78 213 137 87 218 144 97 202 125 81 225 146 105 221 137 100 215 129 96 199 110 80 143 52 23 191 100 73 237 147 120 229 144 115 221 137 109 219 140 110 212 135 107 176 101 72 132 56 30 95 21 0 118 42 16 170 93 67 200 123 97 209 129 104 207 125 101 220 134 111 194 106 82 125 33 10 119 27 2 176 84 59 201 110 83 208 119 89 210 125 94 207 127 92 219 141 103 218 144 105 204 135 96 216 148 111 242 173 140 223 155 120 229 162 120 209 145 107 219 163 138 170 140 142 124 139 178 103 142 201 103 141 203 107 143 203 110 143 197 110 142 193 111 142 196 112 143 198 110 142 199 109 144 202 110 145 203 109 144 202 109 144 202 108 143 201 109 144 202 110 145 203 111 146 204 110 145 203 109 144 202 107 142 200 106 141 199 106 141 199 106 141 199 106 141 199 106 140 201 105 139 200 105 139 200 105 139 200 106 140 201 107 141 202 107 141 202 106 140 201 107 141 202 107 141 202 108 142 205 108 142 205 109 143 206 109 143 206 110 144 207 110 144 207 26 17 34 29 21 44 40 30 65 60 52 89 29 22 53 8 4 27 19 18 32 18 20 35 10 10 36 20 20 56 26 25 69 29 27 74 27 26 70 19 18 62 16 17 63 17 16 60 22 14 61 20 13 55 14 13 45 11 15 40 15 20 39 16 19 34 13 15 28 17 16 32 9 11 26 28 30 45 138 142 153 176 177 182 162 152 151 192 171 166 205 168 160 211 164 136 221 161 99 221 160 93 195 140 101 228 178 153 215 176 147 193 154 121 187 134 100 206 146 110 230 166 128 222 159 118 235 173 132 241 175 140 235 158 130 199 118 88 199 122 78 217 141 92 224 147 101 209 130 87 214 131 91 221 133 97 197 104 71 157 62 32 172 74 47 227 131 106 233 139 113 225 135 109 232 144 120 221 135 110 230 146 120 223 139 113 207 123 97 187 106 79 194 113 86 216 135 108 207 126 99 215 134 107 209 128 101 201 117 91 220 134 109 185 97 73 116 24 0 134 42 17 169 78 51 178 91 61 193 110 78 205 127 91 216 141 102 218 146 106 208 141 99 212 144 107 233 162 132 238 169 138 209 146 105 223 169 133 203 161 145 151 134 144 121 138 181 106 141 199 105 141 203 108 144 204 111 144 198 111 143 194 112 142 196 113 144 199 110 142 199 109 144 202 109 144 202 109 144 202 108 143 201 108 143 201 109 144 202 109 144 202 109 144 202 108 143 201 107 142 200 106 141 199 106 141 199 106 141 199 106 141 199 106 141 199 106 141 199 105 140 198 105 139 200 105 139 200 106 140 201 107 141 202 106 140 201 106 140 201 106 140 201 106 140 201 107 141 202 107 141 202 108 142 205 108 142 205 109 143 206 110 144 207 74 63 67 45 33 45 133 120 146 83 72 104 30 21 50 24 19 42 11 8 25 11 10 26 31 29 53 11 10 41 20 18 55 22 21 61 19 18 58 22 21 61 15 16 60 13 12 56 20 14 58 22 17 57 18 17 48 13 13 37 20 19 35 21 17 31 16 8 21 22 11 25 38 30 43 154 148 160 175 174 182 172 170 173 184 173 171 181 161 154 204 167 158 207 160 130 221 161 98 217 156 89 196 141 100 227 180 152 215 174 146 197 156 126 166 115 84 178 122 87 231 169 132 244 180 142 235 171 133 248 184 149 228 158 133 195 121 94 206 131 89 217 143 94 222 148 103 212 135 91 206 124 84 203 118 79 163 73 38 133 40 7 216 121 91 228 134 106 231 139 114 231 144 117 236 150 125 227 143 117 231 147 121 236 152 126 238 152 127 228 140 116 240 152 128 223 137 112 216 132 106 217 133 107 223 142 115 215 134 107 225 141 115 214 128 101 151 61 35 112 21 0 152 62 35 170 83 53 203 120 86 210 130 95 202 127 88 207 135 95 201 134 92 216 151 113 244 172 147 226 156 130 218 160 120 205 162 128 165 140 135 137 132 152 120 139 182 108 140 197 107 141 204 110 144 207 112 145 199 112 143 197 112 143 197 113 144 199 110 142 199 109 144 202 109 144 202 109 144 202 108 143 201 108 143 201 108 143 201 109 144 202 108 143 201 107 142 200 107 142 200 106 141 199 106 141 199 106 141 199 106 141 199 106 141 199 107 142 200 106 141 199 106 140 201 106 140 201 107 141 202 107 141 202 107 141 202 106 140 201 106 140 201 106 140 201 106 140 201 106 140 201 107 141 204 108 142 205 108 142 205 109 143 206 177 163 152 165 151 150 145 131 148 37 23 49 25 12 40 20 10 35 19 12 30 13 9 24 15 13 34 22 22 46 20 19 50 21 20 52 19 20 51 12 12 48 16 15 55 19 18 60 16 15 57 16 16 54 18 19 49 16 14 35 22 14 29 23 10 20 36 14 26 70 46 59 146 124 137 168 150 162 182 171 179 180 171 172 183 170 164 192 172 163 201 169 156 212 167 136 223 165 101 214 155 87 195 144 101 228 185 153 218 177 149 195 151 124 145 96 66 149 94 63 212 152 116 236 172 136 233 169 134 237 174 143 169 104 82 184 118 92 210 141 100 202 131 85 226 158 111 204 133 89 212 138 93 174 95 54 131 48 8 161 76 39 212 126 93 225 140 109 230 146 118 240 160 133 232 155 129 239 162 136 235 155 130 246 165 138 255 180 154 255 170 143 255 170 145 249 157 132 247 157 131 224 138 111 228 144 116 230 149 120 228 144 116 220 134 107 212 122 96 151 61 34 140 49 22 168 79 49 193 109 75 206 123 89 212 134 96 221 149 109 202 137 97 196 132 96 244 173 151 233 167 143 197 145 106 181 147 120 154 144 152 129 135 167 117 138 183 109 138 194 106 140 203 108 144 206 110 145 201 110 143 197 110 142 199 111 143 200 108 143 201 109 144 202 109 144 202 109 144 202 108 143 201 108 143 201 108 143 201 109 144 202 107 142 200 107 142 200 106 141 199 106 141 199 106 141 199 106 141 199 107 142 200 107 142 200 109 141 200 108 140 199 108 140 199 108 140 199 109 141 202 109 141 202 108 140 201 108 140 201 107 139 200 107 139 200 107 139 200 108 140 201 108 140 201 109 141 202 110 141 205 110 141 205 217 201 176 177 161 148 114 97 107 38 20 44 25 9 36 32 20 44 15 6 23 17 13 28 43 40 57 75 73 94 34 32 54 23 23 49 30 30 56 7 8 38 17 17 55 18 20 61 14 17 58 10 14 49 19 20 48 22 17 37 29 12 28 33 7 18 67 31 43 139 98 112 182 146 160 181 151 163 187 168 174 194 180 180 180 165 158 182 164 152 207 179 165 214 173 141 222 167 102 214 157 88 193 146 100 227 186 154 223 182 154 192 148 123 134 89 60 126 75 44 153 97 62 203 140 105 218 153 121 168 107 79 76 18 0 137 79 55 191 126 86 221 153 106 222 154 109 215 144 100 211 137 92 167 88 45 139 56 16 199 114 77 202 116 81 233 148 117 228 147 118 238 158 131 227 150 124 245 168 142 236 154 130 245 161 135 255 179 155 255 171 146 255 169 144 255 164 139 252 158 133 246 156 130 245 159 132 234 150 122 229 145 117 221 135 108 218 131 103 198 108 81 145 54 25 201 110 81 214 128 95 223 141 104 225 145 108 206 134 94 201 136 96 148 83 51 170 99 81 200 135 115 173 127 91 181 157 133 150 153 170 117 136 178 116 137 184 111 136 192 107 138 202 106 141 205 107 143 201 106 141 197 106 141 199 108 142 203 108 143 201 108 143 201 109 144 202 109 144 202 108 143 201 108 143 201 108 143 201 109 144 202 108 143 201 108 143 201 107 142 200 107 142 200 107 142 200 107 142 200 108 143 201 108 143 201 108 140 199 107 139 198 107 139 198 107 139 198 108 140 201 108 140 201 107 139 200 106 138 199 107 139 200 107 139 200 107 139 200 107 139 200 108 140 201 109 141 202 109 140 204 110 141 205 201 185 151 187 169 147 64 44 46 35 16 35 37 19 45 20 6 31 18 7 24 64 58 72 127 123 138 88 87 101 27 26 42 10 11 29 23 24 45 18 18 44 14 13 47 7 9 48 10 18 55 9 18 51 20 21 49 22 14 35 36 14 27 50 14 26 82 34 48 154 100 114 171 120 135 201 159 171 192 165 172 191 171 172 183 166 159 184 166 154 209 186 170 200 164 130 215 164 98 214 160 90 191 149 101 221 182 149 225 184 156 193 149 124 140 96 71 117 70 40 141 87 53 162 102 66 164 101 70 105 45 19 63 11 0 124 72 48 181 119 80 210 142 97 215 142 99 232 155 113 223 142 99 189 104 63 176 86 49 211 118 84 213 120 87 224 133 104 213 126 99 199 113 88 189 105 81 197 110 90 194 103 82 206 111 89 238 139 118 248 146 124 232 127 105 217 115 92 201 103 78 208 117 90 208 121 93 201 118 88 207 124 94 223 138 109 210 123 95 220 130 103 174 83 54 215 124 95 233 144 112 232 148 112 230 148 111 209 135 96 184 120 82 124 61 30 128 59 43 154 91 74 214 171 137 177 160 142 128 144 169 109 137 185 117 139 188 114 137 191 107 138 202 104 142 207 104 143 202 105 141 199 105 141 203 107 143 205 107 141 202 108 143 201 109 144 202 108 143 201 108 143 201 107 142 200 108 143 201 109 144 202 109 144 202 109 144 202 108 143 201 108 143 201 108 143 201 108 143 201 109 144 202 109 144 202 108 140 199 107 139 198 107 139 198 107 139 198 107 139 198 107 139 198 107 139 200 106 138 199 107 139 200 107 139 200 107 139 200 107 139 200 108 140 201 109 141 202 109 141 202 110 142 203 214 193 164 190 167 149 82 60 63 39 16 34 26 7 29 29 14 35 19 8 22 78 72 82 114 111 122 25 23 37 27 26 42 27 25 46 12 13 34 20 22 47 8 9 40 18 21 56 7 13 49 13 19 51 21 22 50 14 5 26 35 11 25 61 19 31 84 20 34 130 59 73 169 106 117 198 149 155 197 166 171 192 172 171 189 170 163 184 165 151 197 174 156 211 175 141 214 158 99 218 158 95 194 148 98 216 177 138 227 184 152 197 151 125 151 104 78 119 69 42 125 72 40 145 92 61 184 132 108 142 95 75 76 34 18 79 34 13 182 124 86 212 144 99 218 143 103 229 150 109 251 168 128 208 120 82 201 109 72 206 113 79 217 122 90 201 107 79 160 68 43 134 44 20 143 52 31 145 52 34 147 52 34 149 50 31 168 65 48 178 73 54 157 50 30 154 49 28 152 53 30 146 52 26 133 44 14 140 53 23 137 52 21 163 78 47 182 95 65 206 119 89 224 134 100 234 144 109 255 168 130 228 146 108 208 133 94 207 139 102 195 135 101 114 54 26 138 72 56 185 129 112 182 146 120 149 139 127 132 151 181 108 140 189 116 142 193 114 139 196 109 140 205 107 142 208 107 143 203 106 142 200 105 144 203 107 146 205 106 142 200 108 143 201 109 144 202 108 143 201 108 143 201 107 142 200 108 143 201 109 144 202 110 145 203 110 145 203 109 144 202 108 143 201 108 143 201 109 144 202 109 144 202 109 144 202 109 141 200 108 140 199 108 140 199 108 140 199 108 140 199 108 140 199 108 140 201 107 139 200 107 139 200 107 139 200 107 139 200 107 139 200 108 140 201 109 141 202 109 141 202 110 142 203 245 215 207 221 191 189 153 126 135 42 19 35 32 13 32 24 12 26 13 6 14 76 71 78 54 51 60 33 31 45 23 21 43 19 19 47 24 25 55 8 11 42 9 14 43 15 20 50 16 15 49 22 18 51 19 19 47 17 9 32 30 7 23 67 21 32 105 25 38 108 18 27 173 97 101 190 134 133 197 169 166 191 174 167 197 174 168 190 163 152 203 176 155 212 171 139 223 155 110 225 152 101 200 144 93 223 176 130 221 173 135 175 124 93 155 98 71 126 68 44 108 54 28 93 51 29 77 47 36 77 54 46 63 38 33 68 31 15 160 104 69 200 131 89 216 142 103 228 153 114 243 163 126 243 161 123 195 110 73 192 103 69 167 76 45 149 55 27 136 42 16 160 66 41 170 75 55 175 80 62 188 91 75 182 85 69 165 64 52 160 58 44 173 70 55 204 103 85 232 135 116 224 130 105 214 121 90 225 132 98 220 130 95 206 121 84 192 110 73 209 127 89 217 130 87 252 164 118 242 158 111 231 157 110 206 144 105 199 144 113 175 117 93 124 66 46 138 83 63 172 127 108 187 160 143 171 166 163 124 141 167 114 147 192 109 141 198 107 141 205 110 140 210 111 142 209 112 142 204 109 144 200 106 145 202 104 145 201 108 145 200 109 144 200 109 144 200 109 144 200 109 144 200 109 144 200 109 144 200 109 144 200 113 148 204 111 146 202 109 144 200 108 143 199 109 144 200 109 144 200 108 143 199 107 142 198 108 143 201 108 143 201 108 143 201 108 143 201 107 141 202 106 140 201 106 140 201 105 139 200 104 138 199 105 139 200 106 140 201 107 141 202 107 141 204 107 141 204 109 143 206 110 144 207 170 141 146 186 159 168 188 164 178 77 56 73 22 5 21 30 19 33 7 0 7 102 97 103 62 59 68 17 15 29 26 22 47 20 17 48 17 16 50 15 18 49 11 16 45 16 20 47 20 15 47 21 16 48 18 18 46 18 13 35 33 10 26 69 19 30 107 20 29 115 14 20 157 68 70 200 135 131 198 167 162 191 172 165 196 171 166 193 165 154 205 174 154 214 169 138 227 154 111 226 148 99 201 145 94 225 179 130 207 164 122 156 106 71 160 95 63 162 93 60 110 47 12 68 19 0 37 8 0 48 28 17 61 36 29 72 36 22 149 92 62 198 129 88 209 136 95 210 135 93 235 158 116 242 163 122 219 137 97 200 115 76 207 119 83 204 114 79 214 121 88 216 122 94 220 126 100 234 139 117 246 151 131 240 144 128 237 141 127 247 151 135 250 150 134 235 138 119 239 147 126 245 153 128 242 147 115 231 137 99 217 128 88 215 130 91 221 142 103 198 119 78 238 152 105 247 159 109 239 156 104 215 141 94 211 155 118 191 142 112 158 107 80 121 69 47 142 96 73 169 133 117 188 169 165 162 162 174 126 145 177 110 142 189 106 144 206 105 144 211 109 142 209 112 142 206 112 142 202 111 143 200 106 145 200 106 145 200 109 146 201 110 145 201 109 144 200 109 144 200 109 144 200 109 144 200 109 144 200 109 144 200 110 145 201 109 144 200 109 144 200 108 143 199 107 142 198 107 142 198 108 143 199 109 144 200 108 143 201 108 143 201 108 143 201 108 143 201 107 141 202 106 140 201 106 140 201 106 140 201 105 139 200 106 140 201 107 141 202 108 142 203 108 142 205 108 142 205 109 143 206 110 144 207 92 72 83 24 6 18 101 84 102 124 109 128 33 20 38 16 8 23 28 21 29 153 146 154 99 93 103 15 8 24 26 17 44 21 14 47 15 9 43 21 20 51 11 12 40 17 19 44 20 17 48 18 15 46 13 17 42 17 14 33 34 12 25 69 18 27 107 17 26 116 11 16 131 35 37 200 125 122 206 165 161 195 172 164 193 170 162 194 167 156 206 171 152 215 164 133 226 153 100 223 151 92 196 149 97 213 179 133 193 158 120 145 100 61 169 100 58 192 110 62 180 98 42 120 49 0 72 20 0 64 25 0 70 33 14 71 28 9 123 63 35 185 115 79 199 127 79 214 138 86 239 163 111 229 152 100 220 140 89 199 116 66 228 142 95 216 127 83 212 123 81 220 130 93 220 130 95 219 128 97 231 141 114 247 157 131 243 155 131 231 141 117 237 142 120 229 134 112 228 138 114 218 131 103 207 117 82 207 118 76 217 130 87 232 149 107 226 147 108 198 119 78 233 146 101 251 163 115 223 137 88 216 139 95 199 140 108 181 132 102 131 85 49 122 79 45 149 113 89 165 138 129 171 159 169 136 140 169 122 143 188 113 144 201 108 146 209 106 146 208 108 144 206 108 143 201 110 143 197 111 144 198 109 146 201 109 145 203 111 146 202 110 145 201 110 145 201 109 144 200 109 144 200 109 144 200 109 144 200 109 144 200 107 142 198 108 143 199 109 144 200 108 143 199 106 141 197 105 140 196 107 142 198 109 144 200 108 143 201 108 143 201 108 143 201 108 143 201 107 141 202 107 141 202 106 140 201 106 140 201 107 141 202 107 141 202 108 142 203 108 142 203 108 142 205 108 142 205 109 143 206 110 144 207 32 24 39 27 18 35 22 13 34 32 23 44 22 14 35 22 15 31 19 11 22 118 111 119 136 128 139 34 25 42 26 16 43 24 15 46 21 14 47 21 16 48 8 8 34 21 21 45 17 17 45 17 17 43 14 16 37 17 10 26 36 8 20 74 17 24 110 18 23 117 8 11 118 14 15 181 95 94 216 163 159 204 171 164 196 169 160 196 168 156 208 169 152 212 162 129 222 153 94 220 155 89 193 148 93 208 174 128 194 159 119 158 110 70 180 108 58 192 106 47 219 126 57 211 123 52 179 105 44 111 49 0 70 14 0 68 12 0 104 39 17 172 101 73 201 127 82 205 130 75 221 144 90 238 158 105 227 146 93 193 112 59 221 138 88 227 141 92 213 126 81 205 118 75 202 114 76 199 114 77 192 106 73 184 99 70 177 93 65 171 85 58 165 75 49 170 80 53 180 95 66 185 101 67 203 115 75 228 141 98 231 147 103 222 141 98 215 137 98 200 121 82 227 140 95 224 136 90 226 137 93 202 123 84 191 130 102 165 115 88 120 81 42 128 95 60 142 117 97 151 135 136 146 142 165 119 128 169 120 143 197 114 146 207 111 147 209 107 147 206 108 147 204 109 146 199 110 145 199 110 145 199 110 145 203 110 145 203 112 147 203 111 146 202 111 146 202 110 145 201 110 145 201 109 144 200 109 144 200 109 144 200 108 143 199 108 143 199 108 143 199 108 143 199 106 141 197 106 141 197 107 142 198 108 143 199 108 143 201 108 143 201 108 143 201 108 143 201 108 142 203 107 141 202 107 141 202 106 140 201 107 141 202 107 141 202 108 142 203 108 142 203 107 141 204 107 141 204 108 142 205 109 143 206 17 18 38 24 25 46 17 15 39 23 21 45 18 15 36 16 12 29 13 7 17 28 21 29 115 105 116 48 39 56 29 19 46 24 15 46 29 22 55 19 14 44 13 11 35 23 24 45 14 19 41 17 19 40 18 17 35 23 7 20 46 4 14 85 19 23 115 21 22 117 9 9 121 11 12 151 55 56 216 145 143 214 165 160 200 167 158 197 168 154 210 168 152 208 159 126 215 156 96 212 155 84 192 143 84 215 169 119 206 159 115 169 115 69 180 110 59 186 102 42 213 119 49 209 109 33 224 127 48 220 129 58 180 98 42 111 36 0 80 7 0 150 77 62 188 113 74 204 126 77 204 124 75 228 148 99 210 128 80 204 122 74 218 134 87 220 136 90 230 146 102 224 139 98 216 132 95 200 118 81 178 95 63 162 81 51 160 81 51 163 82 53 170 83 55 160 73 43 165 82 50 176 96 59 197 114 70 214 132 85 210 129 82 201 122 79 216 141 101 214 136 97 216 131 90 217 128 86 214 125 85 196 116 81 191 126 104 142 94 72 131 96 64 136 115 86 124 111 102 135 133 146 130 138 174 124 142 192 120 146 205 110 142 203 111 147 205 108 149 203 108 149 201 108 149 201 111 148 201 111 146 202 114 144 206 113 143 205 114 146 205 112 147 203 111 146 202 111 146 202 110 145 201 110 145 201 110 145 201 110 145 201 112 147 203 110 145 201 108 143 199 108 143 199 108 143 199 108 143 199 108 143 199 107 142 198 108 143 201 108 143 201 108 143 201 108 143 201 108 142 203 107 141 202 107 141 202 106 140 201 106 140 201 106 140 201 107 141 202 107 141 202 106 140 203 106 140 203 106 140 203 107 141 204 15 23 46 10 16 42 17 21 50 15 15 43 10 8 30 19 15 32 22 16 28 23 15 26 45 37 50 38 29 48 28 19 46 18 11 44 25 20 52 16 13 42 17 18 39 17 20 39 12 19 38 17 18 36 27 14 31 39 7 20 66 6 16 100 19 25 118 23 21 115 10 7 127 15 14 131 25 25 196 106 108 218 150 147 204 160 151 200 164 150 209 168 150 203 157 123 214 161 95 210 154 80 193 139 79 221 168 118 211 157 113 168 107 62 178 105 54 190 106 46 218 120 49 214 108 30 219 110 25 217 112 29 206 109 41 169 78 34 118 32 19 145 65 54 180 101 68 209 130 87 198 119 76 204 123 78 189 108 63 221 138 94 225 141 97 208 124 80 202 117 76 211 126 87 209 125 88 203 120 86 211 131 98 229 148 118 233 154 124 226 147 117 225 140 109 215 131 97 213 133 98 203 125 86 195 114 69 197 117 68 204 126 78 215 141 96 217 145 105 217 143 104 210 127 87 230 142 102 199 111 75 198 119 89 189 123 107 118 70 56 131 103 82 141 127 114 121 120 126 129 139 164 119 138 181 121 148 201 114 145 202 109 144 200 111 148 200 108 150 200 108 152 201 109 152 203 111 150 207 113 147 208 116 144 209 117 142 206 115 147 206 112 147 203 112 147 203 111 146 202 111 146 202 111 146 202 111 146 202 111 146 202 114 149 205 111 146 202 109 144 200 108 143 199 110 145 201 110 145 201 109 144 200 107 142 198 109 144 202 109 144 202 109 144 202 108 143 201 108 142 203 107 141 202 106 140 201 106 140 201 105 139 200 106 140 201 106 140 201 106 140 201 105 139 202 105 139 202 106 140 203 107 141 204 17 27 54 4 11 39 14 19 49 26 27 55 22 20 42 12 8 25 4 0 10 17 11 21 10 4 16 20 16 33 23 19 46 13 9 42 17 16 48 12 13 43 14 19 41 10 15 34 12 18 34 18 12 26 41 10 26 69 12 27 98 12 25 117 18 23 119 19 17 116 13 8 127 15 13 129 16 18 163 54 59 219 129 131 208 149 143 203 159 146 207 166 148 201 157 120 216 159 88 217 155 78 201 140 85 225 164 120 212 150 111 171 104 62 186 106 55 197 107 47 207 106 38 209 103 27 213 107 23 212 107 26 201 98 31 172 71 25 130 29 11 153 59 47 199 115 87 189 111 73 188 110 72 204 125 86 213 131 91 218 136 96 224 141 99 231 148 108 231 146 107 223 138 101 218 134 98 233 150 116 254 174 141 255 174 144 247 168 137 251 170 140 254 170 136 248 164 128 249 171 133 246 168 129 242 164 118 243 165 116 239 163 114 236 163 118 211 142 103 199 127 89 212 133 94 201 117 80 207 123 89 185 109 83 192 130 117 119 75 72 124 102 104 138 132 142 129 136 162 125 145 182 110 141 188 111 147 199 108 144 194 113 151 198 110 151 195 109 152 197 106 152 202 106 151 206 109 149 210 113 147 211 119 144 210 120 144 208 115 145 205 112 147 203 112 147 203 112 147 203 111 146 202 112 147 203 112 147 203 112 147 203 113 148 204 112 147 203 110 145 201 110 145 201 110 145 201 110 145 201 110 145 201 110 145 201 110 145 203 110 145 203 109 144 202 109 144 202 108 142 203 107 141 202 106 140 201 105 139 200 105 139 200 106 140 201 107 141 202 107 141 202 106 140 203 106 140 203 107 141 204 108 142 205 8 17 46 18 25 54 14 17 48 19 19 47 9 5 28 13 9 26 18 15 24 16 13 22 20 18 31 15 14 32 20 20 48 11 14 47 14 17 50 10 15 44 13 20 39 11 17 31 18 16 30 24 4 16 55 4 19 95 17 31 124 18 32 130 15 22 120 16 13 118 15 8 125 11 10 136 17 19 139 15 23 219 113 117 213 140 134 206 157 142 206 165 145 201 156 115 214 155 77 227 161 77 212 146 88 226 159 117 214 144 108 182 109 68 194 111 59 194 102 39 203 102 32 215 109 33 215 111 26 211 109 27 211 108 39 197 95 46 145 43 21 139 43 29 205 124 95 179 105 68 191 115 79 196 118 80 219 140 101 207 125 85 213 132 89 221 138 96 210 125 84 226 141 102 225 142 102 228 144 107 248 166 129 255 177 140 255 176 141 255 180 145 255 174 140 243 159 125 243 165 129 249 174 134 252 174 128 245 169 120 233 159 112 232 161 117 203 135 98 196 128 93 192 116 80 174 94 59 196 117 86 189 117 93 184 130 118 150 114 116 127 110 126 134 133 164 130 143 185 116 142 191 112 147 201 111 150 205 110 148 197 114 152 197 111 153 195 109 152 195 106 152 202 105 152 208 108 149 211 113 148 212 121 146 212 122 146 210 115 145 205 112 147 203 112 147 203 112 147 203 112 147 203 112 147 203 112 147 203 113 148 204 110 145 201 111 146 202 112 147 203 111 146 202 109 144 200 109 144 200 110 145 201 112 147 203 111 146 204 110 145 203 110 145 203 109 144 202 108 142 203 106 140 201 105 139 200 105 139 200 106 140 201 107 141 202 107 141 202 108 142 203 107 141 204 107 141 204 108 142 205 109 143 206 12 17 46 15 19 48 18 19 49 19 16 43 17 12 34 14 10 25 13 10 19 13 12 20 18 20 32 13 18 37 11 16 45 11 17 49 11 17 49 8 15 43 9 16 35 18 17 31 24 8 21 53 12 26 91 19 33 118 20 35 135 13 26 135 8 15 123 11 9 121 12 7 127 11 11 138 13 17 148 17 25 174 57 63 216 136 129 203 145 131 199 154 133 187 140 98 209 147 64 247 178 87 206 132 71 212 138 93 208 133 94 182 103 60 206 121 66 196 104 39 205 104 32 212 110 28 210 109 21 216 116 31 203 106 35 205 111 59 128 36 13 102 14 0 191 116 85 199 130 91 176 104 64 187 114 73 197 122 80 205 128 84 195 114 69 213 132 85 220 138 91 224 142 95 221 139 92 217 135 88 231 150 105 232 151 106 247 166 123 250 168 128 235 151 117 236 153 121 243 164 131 252 176 140 253 174 133 237 160 116 220 146 101 214 143 101 184 118 83 171 105 71 164 91 56 183 109 74 181 108 76 169 104 82 172 125 115 148 119 124 137 124 150 131 134 175 125 141 193 121 147 206 113 149 211 109 149 210 113 152 207 111 149 198 115 153 198 108 151 194 103 149 199 105 150 205 107 149 209 111 147 209 119 144 208 123 147 209 116 147 204 113 148 204 113 148 204 113 148 204 113 148 204 113 148 204 113 148 204 113 148 204 111 146 202 111 146 202 111 146 202 111 146 202 111 146 202 111 146 202 111 146 202 111 146 202 112 147 205 111 146 204 111 146 204 110 145 203 110 144 205 109 143 204 109 143 204 108 142 203 108 142 203 108 142 203 108 142 203 108 142 203 108 142 205 108 142 205 108 142 205 108 142 205 15 16 46 18 18 46 21 18 47 20 16 41 17 12 32 14 10 24 13 12 18 13 14 19 10 14 26 18 25 43 17 24 52 8 14 46 7 12 42 13 15 40 16 18 33 27 15 27 49 13 27 77 15 30 109 18 33 130 18 30 143 11 22 141 7 14 128 10 10 123 9 8 129 9 11 139 12 19 151 17 26 146 25 30 196 109 100 208 142 126 209 157 136 179 126 82 207 143 55 255 186 88 210 133 63 199 119 68 193 112 67 181 99 52 206 120 63 193 101 34 217 121 44 194 94 8 209 108 16 221 125 38 201 113 41 191 111 60 117 45 23 127 59 46 218 151 122 195 127 90 163 94 55 169 97 57 185 112 71 196 121 79 181 104 60 199 121 75 218 137 90 217 136 89 209 128 81 204 126 78 219 141 95 218 140 94 226 147 104 221 142 103 211 130 100 222 142 115 226 149 121 222 147 115 224 146 108 223 146 104 208 135 94 188 119 80 168 103 71 133 68 38 147 78 45 132 62 28 145 79 47 188 132 109 163 125 116 65 42 48 145 139 165 122 127 169 117 134 188 114 139 203 113 148 216 114 151 221 107 142 206 111 148 203 109 147 196 106 147 193 105 149 198 107 150 201 109 150 206 111 147 205 118 147 207 123 149 208 115 147 204 113 148 204 113 148 204 113 148 204 113 148 204 113 148 204 113 148 204 113 148 204 112 147 203 112 147 203 111 146 202 111 146 202 111 146 202 111 146 202 111 146 202 112 147 203 112 147 205 111 146 204 111 146 204 110 145 203 109 143 204 109 143 204 108 142 203 108 142 203 108 142 203 108 142 203 108 142 203 108 142 203 108 142 205 108 142 205 108 142 205 108 142 205 18 15 44 21 17 44 23 17 45 21 16 39 19 11 32 15 11 25 13 14 19 12 15 20 12 18 30 22 29 47 17 22 51 11 12 43 20 15 45 27 17 41 29 15 30 43 11 22 82 20 35 104 17 33 124 20 31 138 18 27 144 13 19 142 9 12 132 8 10 129 6 9 133 8 16 142 15 24 149 19 27 126 10 11 161 71 62 211 138 121 214 151 133 187 126 82 203 138 46 255 188 84 208 131 51 189 108 45 181 98 46 175 92 40 201 117 57 191 104 33 208 116 33 210 112 21 210 112 15 230 136 46 184 106 34 140 78 31 60 17 1 206 163 156 216 158 136 203 137 105 174 108 76 141 72 39 133 63 29 149 76 41 150 76 39 167 92 53 199 121 82 207 129 90 204 129 90 201 126 87 204 128 92 191 117 80 190 116 81 183 108 77 188 112 88 201 126 105 203 131 107 195 124 96 195 120 88 197 121 85 181 107 70 156 86 52 128 63 33 114 50 22 103 37 5 118 52 18 175 112 81 203 153 128 149 117 106 28 12 13 57 55 69 114 120 152 126 140 187 119 141 201 116 144 217 114 146 223 119 153 227 109 144 212 109 145 203 108 148 199 109 149 198 109 151 199 109 149 200 111 146 200 115 146 201 118 149 206 115 147 204 113 148 204 113 148 204 113 148 204 113 148 204 113 148 204 113 148 204 113 148 204 114 149 205 113 148 204 112 147 203 111 146 202 111 146 202 111 146 202 112 147 203 112 147 203 112 147 205 112 147 205 111 146 204 110 145 203 109 143 204 108 142 203 108 142 203 107 141 202 108 142 203 108 142 203 108 142 203 108 142 203 108 142 205 108 142 205 108 142 205 108 142 205 20 15 45 21 17 44 23 17 45 23 15 39 19 11 32 15 11 25 14 14 22 12 17 23 11 17 29 17 20 39 14 14 42 18 9 40 30 11 39 35 7 29 42 9 20 70 17 25 106 22 38 123 18 33 134 18 27 141 18 21 144 14 16 141 11 13 134 8 12 130 5 13 134 8 19 142 16 27 140 17 22 131 17 16 134 38 26 206 123 107 210 137 118 203 135 90 206 137 42 253 182 74 206 130 44 188 109 42 180 99 44 168 87 34 193 111 51 197 113 41 215 126 42 192 103 13 208 120 31 206 127 50 159 98 41 55 12 0 53 29 17 250 223 216 186 134 113 208 142 116 194 127 100 146 79 52 105 36 7 93 22 0 107 36 6 124 51 19 154 81 49 174 100 71 178 107 77 170 101 72 156 89 62 140 73 47 143 77 51 148 82 60 158 91 74 157 90 74 152 87 69 150 84 62 153 82 54 153 78 46 143 70 37 131 60 30 105 39 13 87 23 0 103 36 7 171 105 73 194 133 102 197 149 126 146 118 106 19 8 6 22 21 29 29 36 55 94 107 142 125 142 194 114 137 205 115 143 217 112 144 219 117 151 223 112 147 211 112 149 204 112 150 199 113 151 198 113 151 198 113 149 199 113 148 202 115 147 204 113 148 204 113 148 204 113 148 204 113 148 204 113 148 204 113 148 204 113 148 204 113 148 204 115 150 206 114 149 205 112 147 203 111 146 202 110 145 201 111 146 202 111 146 202 112 147 203 112 147 205 112 147 205 111 146 204 110 145 203 109 143 204 108 142 203 108 142 203 107 141 202 109 143 204 109 143 204 109 143 204 109 143 204 109 143 206 109 143 206 109 143 206 109 143 206 19 16 47 22 17 47 23 16 47 20 14 40 17 12 32 14 12 25 12 15 22 13 18 24 12 16 28 21 20 38 24 14 41 30 7 36 42 3 30 55 4 23 77 16 23 111 32 37 122 20 34 133 15 29 140 17 22 142 16 17 141 16 12 138 13 11 133 8 14 128 6 17 127 7 19 132 15 24 131 15 18 130 18 14 126 19 9 196 99 83 212 129 111 201 127 80 217 141 43 255 182 69 209 139 53 180 111 44 173 101 51 161 84 38 197 112 55 199 112 43 208 121 41 182 101 22 196 127 60 167 114 62 50 11 0 29 3 0 128 114 103 214 192 181 158 105 87 202 136 112 194 126 103 180 112 89 138 70 47 85 17 0 90 22 0 96 28 5 110 44 22 121 56 36 112 49 32 102 40 25 88 28 17 83 25 14 93 36 27 104 47 38 107 50 41 96 38 27 84 26 14 86 24 9 99 33 9 113 42 14 124 50 21 127 56 28 111 45 21 107 41 17 167 98 69 197 131 99 198 137 106 197 151 127 132 106 93 21 10 6 19 19 21 8 15 25 12 20 43 79 92 127 131 148 200 124 148 210 117 146 214 105 138 207 114 149 213 113 149 209 115 148 201 116 150 198 118 152 200 117 153 203 115 152 205 113 150 205 114 149 205 114 149 205 114 149 205 114 149 205 114 149 205 114 149 205 114 149 205 114 149 205 115 150 206 114 149 205 113 148 204 111 146 202 111 146 202 111 146 202 111 146 202 111 146 202 112 147 205 112 147 205 111 146 204 111 146 204 110 144 205 109 143 204 108 142 203 108 142 203 109 143 204 109 143 204 109 143 204 109 143 204 109 143 206 109 143 206 109 143 206 109 143 206 18 17 49 21 18 49 22 17 49 19 15 42 14 12 33 12 11 25 11 14 23 13 18 24 19 18 32 25 17 38 31 11 38 38 2 30 58 2 27 89 16 33 114 29 34 127 27 29 130 14 25 138 12 24 143 14 18 144 16 13 140 15 9 135 13 10 130 9 16 124 6 18 121 9 23 124 14 23 129 19 20 123 10 6 137 18 10 181 71 58 221 132 114 184 106 60 226 147 52 255 189 78 222 152 64 169 100 33 162 89 44 158 80 41 198 116 68 179 97 41 190 112 46 177 111 51 141 92 49 54 18 0 31 6 0 21 5 5 171 160 154 193 170 156 128 76 54 207 140 114 193 126 100 192 125 99 170 103 77 120 54 30 116 51 29 109 47 26 105 44 26 100 41 27 73 16 5 63 10 2 56 7 2 57 9 7 56 10 10 56 11 8 59 11 7 58 9 4 54 4 0 61 7 0 90 27 9 122 55 29 140 68 43 140 68 44 125 57 36 156 90 68 199 127 102 191 122 93 205 144 115 195 149 125 137 111 98 26 15 11 11 9 10 13 16 23 11 15 27 12 18 40 71 84 118 125 146 191 122 149 204 127 159 220 116 150 213 117 151 212 120 151 208 119 150 204 119 150 204 116 152 204 111 150 207 107 148 204 113 150 205 114 149 205 114 149 205 114 149 205 114 149 205 114 149 205 114 149 205 114 149 205 115 150 206 115 150 206 114 149 205 113 148 204 112 147 203 111 146 202 111 146 202 111 146 202 112 147 205 112 147 205 112 147 205 111 146 204 111 145 206 110 144 205 110 144 205 109 143 204 110 144 205 110 144 205 110 144 205 110 144 205 110 144 207 110 144 207 110 144 207 110 144 207 17 17 51 18 18 52 18 17 49 15 15 43 11 12 33 9 11 26 10 14 23 13 16 25 18 16 29 24 11 31 43 14 42 63 14 43 82 10 34 112 20 35 132 23 26 127 9 9 131 10 19 139 12 21 148 14 15 148 16 12 141 17 9 131 13 9 124 9 16 118 8 19 112 12 24 119 19 27 128 19 22 130 12 10 148 16 12 164 41 33 225 132 114 170 93 49 227 153 64 255 189 86 234 160 73 165 88 20 159 78 31 153 73 36 191 112 79 146 76 40 165 107 67 85 40 7 36 4 0 34 13 12 25 7 21 22 8 21 200 186 185 200 174 159 104 51 20 215 149 114 200 134 99 185 119 85 174 110 75 150 87 56 139 78 49 136 79 52 127 72 51 117 66 49 84 36 22 71 28 19 58 19 14 54 16 15 43 9 8 39 3 3 42 4 3 54 13 9 65 22 16 81 33 23 115 56 40 150 84 62 159 88 66 149 77 55 144 76 57 168 100 79 188 114 89 200 129 101 193 129 101 202 154 131 160 132 120 16 5 1 24 19 25 10 8 19 14 13 19 13 16 23 0 7 19 37 55 77 107 135 172 117 151 199 110 145 203 116 150 213 124 152 215 124 150 211 120 149 207 116 148 207 108 148 207 105 147 205 113 149 207 114 149 205 114 149 205 114 149 205 114 149 205 114 149 205 114 149 205 114 149 205 116 151 207 116 151 207 116 151 207 115 150 206 114 149 205 113 148 204 111 146 202 111 146 202 112 147 205 112 147 205 112 147 205 112 147 205 111 145 206 111 145 206 111 145 206 111 145 206 110 144 205 110 144 205 110 144 205 110 144 205 110 144 207 110 144 207 110 144 207 110 144 207 18 19 50 18 19 50 18 17 49 14 14 42 10 10 34 8 9 27 10 13 22 15 16 21 23 17 27 38 22 35 81 46 68 112 54 78 114 29 50 124 18 30 145 21 23 145 15 15 134 9 15 139 12 19 153 14 17 152 17 14 143 16 10 128 13 8 116 10 12 109 10 15 104 12 17 119 23 27 122 12 13 145 21 21 144 8 8 145 21 13 216 123 106 165 92 51 223 154 77 250 183 92 231 162 84 157 88 23 150 79 35 135 68 39 171 110 89 113 64 47 46 9 0 35 8 0 23 7 8 16 6 17 23 12 29 13 2 16 209 199 198 227 204 188 92 43 11 210 146 108 200 136 98 187 122 84 185 117 82 164 98 64 139 74 42 153 92 64 145 89 64 142 90 69 110 61 46 92 44 32 66 21 15 55 16 11 47 16 13 52 21 16 54 15 10 66 23 16 80 33 25 101 47 35 133 70 52 156 88 67 157 86 64 145 73 51 158 87 65 193 122 100 189 117 92 197 130 103 192 135 108 240 199 179 139 119 110 20 11 12 14 11 20 19 16 27 17 15 18 13 13 13 12 17 20 2 15 23 9 31 52 49 75 108 103 132 176 114 142 192 127 150 204 128 150 207 122 148 205 118 149 206 113 152 209 113 153 212 113 149 207 114 149 205 114 149 205 114 149 205 114 149 207 113 149 207 114 149 207 113 149 207 117 152 210 117 152 208 117 152 208 117 152 208 116 151 207 114 149 205 112 147 203 111 146 202 112 147 203 112 147 203 112 147 205 112 147 205 112 146 207 112 146 207 112 146 207 112 146 207 110 145 203 110 145 203 110 145 203 110 145 203 110 144 205 110 144 207 110 144 207 110 144 208 18 19 40 23 21 45 26 21 51 11 4 35 18 12 38 12 9 28 13 12 20 14 12 15 35 27 25 124 104 105 197 157 165 183 120 129 117 25 36 139 26 32 150 17 18 144 9 6 139 13 16 144 15 20 155 14 22 155 14 20 145 12 15 128 13 10 109 14 8 102 15 8 106 19 12 118 20 17 129 9 10 138 8 10 150 16 17 140 21 17 202 114 100 164 95 62 210 148 87 238 181 110 208 157 94 141 93 44 100 56 21 68 32 10 38 10 0 29 10 6 21 9 9 18 12 14 12 11 17 9 9 17 13 14 19 16 16 16 210 210 202 233 220 201 98 59 28 183 127 90 200 140 104 188 122 88 189 113 81 171 92 61 161 84 54 160 86 59 156 92 65 149 87 64 133 68 50 115 49 35 105 41 31 91 37 27 68 32 18 60 27 12 72 24 12 84 28 13 100 39 20 118 52 30 154 82 58 167 93 68 147 75 50 149 78 50 171 100 72 197 126 96 182 113 82 199 139 111 215 170 149 245 221 209 121 117 118 11 15 24 13 11 22 15 12 21 16 14 19 16 14 17 14 15 17 10 15 19 8 17 26 5 17 33 0 12 33 25 42 70 77 96 129 122 144 185 134 158 204 120 148 196 114 145 199 122 155 209 116 151 207 113 148 206 110 146 204 114 150 208 116 152 212 113 152 211 112 148 208 108 147 206 114 150 210 116 152 210 116 152 210 114 151 206 113 148 204 113 148 202 115 148 202 115 148 202 114 149 203 113 148 202 113 148 204 113 147 208 113 147 210 113 147 210 113 147 208 113 148 206 110 145 201 109 144 198 110 143 196 110 143 197 111 143 202 112 144 205 112 143 208 112 143 210 21 18 37 18 15 36 23 17 45 17 11 39 23 17 43 15 11 28 11 8 15 43 38 35 143 132 126 192 172 165 197 157 155 179 117 118 152 63 67 136 23 27 150 17 18 146 10 10 128 5 7 133 10 13 149 12 22 155 14 23 149 15 22 133 14 16 113 16 10 102 15 6 101 10 5 123 19 18 139 14 18 139 8 14 132 7 11 125 17 17 187 109 99 170 112 88 162 113 70 151 109 59 100 63 21 46 13 0 29 4 0 25 7 0 15 5 3 15 10 16 11 11 19 10 13 22 10 13 22 10 13 18 17 19 18 16 17 12 210 210 200 236 223 206 95 60 32 160 107 75 209 148 117 187 118 87 183 102 72 184 99 70 174 93 63 155 80 49 160 91 62 150 83 57 136 63 44 129 56 41 119 47 35 104 42 29 84 41 24 89 48 28 109 54 34 115 50 30 128 57 35 147 73 48 166 90 64 160 85 56 145 71 42 162 89 57 187 114 81 183 114 81 196 133 102 193 143 116 255 222 205 242 227 220 97 98 103 7 15 26 11 11 21 13 12 20 15 12 19 14 13 18 13 12 17 13 12 17 13 14 18 11 14 21 10 17 27 13 22 37 7 19 41 14 29 58 60 80 115 118 139 182 134 158 204 115 141 190 119 149 203 117 150 204 117 149 206 114 149 205 113 148 204 113 149 207 117 153 211 122 158 218 115 154 213 115 154 211 117 153 211 116 152 210 114 151 206 113 148 202 113 148 202 113 148 202 113 149 201 113 148 202 114 149 207 115 149 210 114 148 212 114 148 212 113 147 210 113 148 206 112 147 201 111 147 199 112 145 198 111 144 197 112 144 201 112 144 205 113 142 210 113 142 212 21 16 38 16 12 35 22 20 44 16 16 40 14 12 33 10 8 21 25 19 21 118 107 103 183 169 160 207 185 174 188 153 147 176 125 121 196 120 120 146 48 49 141 19 18 142 17 15 126 16 15 129 19 20 143 16 25 148 14 25 144 10 21 132 7 15 115 6 11 107 4 7 118 13 18 126 12 20 131 5 19 137 11 25 135 24 33 120 29 34 119 58 53 80 38 24 39 7 0 37 11 0 27 4 0 21 2 0 25 10 3 26 16 15 18 13 17 14 13 19 13 13 23 13 13 23 13 12 20 15 13 18 22 16 18 17 9 7 212 201 197 244 224 213 72 34 11 137 83 55 199 134 106 212 137 108 194 113 83 178 98 63 162 86 50 169 99 63 161 92 59 147 80 53 133 61 39 136 65 47 123 54 39 111 49 34 102 49 31 121 69 48 133 68 48 131 60 38 138 66 42 156 82 57 169 93 67 159 84 53 153 77 45 174 100 65 179 109 75 189 124 92 192 138 110 223 182 162 255 232 221 239 225 222 84 83 88 9 12 21 10 10 18 11 11 19 13 12 18 13 12 17 12 11 16 12 11 16 13 12 17 12 13 18 13 16 25 3 9 21 9 16 34 18 29 51 8 19 49 1 15 50 46 61 100 106 125 167 128 150 197 121 148 195 120 146 195 124 152 202 124 156 207 121 154 207 116 149 203 112 147 203 114 151 206 112 148 206 110 149 206 113 152 209 113 152 209 113 150 205 114 151 206 117 154 209 112 147 201 113 148 202 114 150 208 115 151 211 115 150 214 114 149 213 112 148 210 111 147 207 114 149 205 113 149 201 111 147 199 110 145 199 112 144 201 113 145 206 113 142 208 112 141 209 26 18 42 18 13 36 17 19 42 12 17 37 6 9 24 13 12 20 39 29 28 178 161 153 191 172 158 195 172 158 185 155 144 169 129 121 213 152 147 178 94 92 147 39 37 144 25 27 124 14 17 127 16 22 137 16 25 142 16 28 137 15 26 127 13 23 110 13 20 102 11 18 95 5 14 112 19 30 124 23 37 111 15 29 82 1 10 57 0 0 43 8 6 30 9 4 27 9 5 24 10 9 23 11 11 18 9 10 11 6 10 10 8 13 14 13 19 15 15 23 15 15 23 13 12 20 15 10 17 18 11 18 24 13 17 20 5 8 204 188 189 254 229 224 91 54 36 113 61 39 205 138 112 211 134 106 190 110 77 190 112 74 173 102 60 171 104 62 165 97 62 154 85 56 137 66 44 141 70 52 127 60 43 121 56 38 120 57 39 147 81 59 153 78 55 151 73 50 149 74 51 161 87 62 173 98 69 172 96 64 172 94 58 182 108 71 183 115 80 183 124 94 199 154 133 255 226 213 237 218 212 235 225 224 60 55 59 14 13 19 10 10 18 9 12 19 11 12 17 11 12 17 11 10 15 11 10 15 12 11 16 13 12 18 17 16 24 19 19 29 17 19 34 13 16 35 11 16 38 13 19 45 15 22 51 12 23 55 32 48 84 84 103 143 126 145 187 123 146 188 115 139 185 121 149 196 125 156 203 118 150 201 124 157 210 118 153 207 116 153 208 118 155 210 114 153 208 108 147 204 107 146 203 110 149 206 113 148 204 113 148 204 113 149 209 114 150 212 114 149 213 113 148 214 112 147 211 111 147 207 114 149 205 112 147 201 111 146 200 110 145 199 112 144 201 112 144 205 113 142 208 113 142 208 29 19 46 17 13 38 10 15 37 8 17 34 10 16 28 23 21 26 41 26 21 183 161 148 202 176 159 203 178 158 186 163 147 173 144 130 213 166 158 207 142 138 171 81 80 127 18 21 134 12 23 139 13 27 141 16 32 135 19 32 120 18 29 99 17 23 75 13 14 61 11 12 60 16 17 55 13 15 50 5 10 44 1 8 36 4 9 32 12 13 14 10 7 6 8 7 9 8 16 10 8 21 15 13 26 16 16 26 12 15 24 15 18 25 15 18 25 8 11 16 12 13 17 11 10 15 15 10 14 22 13 18 26 13 20 22 7 12 181 164 172 252 227 230 211 177 167 84 35 18 177 109 90 214 134 109 199 119 84 180 105 63 174 108 60 177 113 69 173 105 68 164 95 64 148 78 53 144 76 55 135 68 49 134 66 45 144 69 50 168 87 66 170 86 62 164 80 54 161 81 58 169 93 67 175 98 68 169 93 59 175 97 59 184 110 73 192 128 93 163 112 85 230 197 182 252 232 225 239 229 230 228 222 226 25 16 19 21 16 20 11 12 17 9 14 20 10 13 18 11 12 17 11 10 16 11 10 16 13 11 16 16 11 17 19 14 21 13 10 19 15 12 23 22 20 33 26 23 40 20 19 37 16 14 35 14 16 39 16 23 51 0 9 39 16 29 61 79 94 127 128 146 182 135 157 196 127 150 192 123 150 195 119 147 195 116 148 197 118 151 202 120 156 208 118 155 208 113 150 205 110 149 206 113 152 209 114 149 205 114 149 205 114 148 209 113 147 210 112 147 211 112 147 213 112 147 211 112 148 210 111 147 205 110 147 202 109 144 200 109 144 200 112 144 203 112 144 205 113 143 207 113 143 207 23 14 43 19 15 40 12 18 40 8 17 34 10 17 27 23 19 20 43 26 18 184 158 143 191 164 143 209 184 162 185 166 149 186 164 150 208 175 166 224 175 170 221 150 148 136 47 51 124 22 33 122 17 31 110 12 25 92 9 17 73 7 11 54 6 6 35 5 3 23 5 1 19 8 6 17 7 6 20 8 10 18 7 11 15 9 13 15 15 17 6 14 16 5 16 20 11 15 27 11 14 29 12 14 27 7 11 22 5 9 18 10 15 19 13 17 20 6 10 11 10 11 13 13 11 12 16 12 13 21 15 17 22 13 18 16 5 13 136 123 132 230 211 217 255 232 228 157 113 102 135 69 53 197 121 97 194 116 80 182 108 63 182 116 66 169 105 57 179 110 71 169 98 66 155 85 60 144 76 53 143 77 55 144 73 51 159 79 56 177 91 68 170 80 54 164 74 48 164 80 54 181 101 74 177 101 69 160 84 48 171 96 57 186 118 81 161 104 74 193 151 129 249 225 215 236 224 224 242 235 242 191 186 192 16 5 9 23 14 17 12 13 17 9 14 18 10 13 18 9 12 17 11 10 16 11 10 16 15 10 16 16 11 17 18 11 19 21 14 22 24 18 28 24 18 28 21 15 27 20 14 26 23 17 31 25 23 37 17 18 38 18 23 45 16 22 46 10 17 43 16 27 55 47 62 93 92 108 142 122 142 179 129 152 194 127 154 197 127 155 202 122 153 200 117 149 198 114 147 198 115 151 203 118 153 207 117 150 204 117 149 206 116 148 209 113 147 210 112 146 210 111 146 210 111 146 210 111 147 209 110 146 206 109 145 203 109 144 202 108 143 201 111 143 202 112 144 205 112 142 204 112 142 206 17 12 42 18 18 44 14 20 44 8 15 33 10 12 24 16 10 10 50 32 22 192 166 149 197 171 148 207 185 162 189 173 157 187 171 158 202 179 173 227 192 190 241 190 189 141 82 84 68 16 18 58 14 13 46 8 7 34 6 3 25 7 3 19 12 6 14 15 9 12 17 11 15 20 16 9 13 12 12 12 14 10 8 13 7 4 11 7 6 12 13 13 21 11 14 21 3 3 11 8 11 18 16 17 22 16 17 21 15 16 18 14 16 15 14 16 13 14 16 13 13 13 11 14 14 12 13 11 12 17 15 18 12 10 15 11 8 15 97 94 103 214 204 212 252 232 233 238 201 195 112 56 43 171 103 80 200 126 91 178 104 59 173 101 53 182 111 65 183 111 73 168 95 63 155 85 59 141 74 48 151 83 60 146 74 49 164 83 56 179 92 65 172 81 54 170 79 52 171 85 58 189 108 78 182 103 70 162 88 51 170 100 64 176 116 82 159 113 87 242 211 193 243 228 223 234 228 232 232 229 236 123 118 125 24 13 17 20 11 14 12 13 17 9 14 17 10 14 17 9 13 16 11 10 16 11 10 16 15 10 17 16 11 18 17 10 18 18 11 19 22 15 23 27 20 28 27 20 28 24 17 25 23 16 24 24 18 28 18 16 29 20 19 35 19 20 38 16 19 38 12 17 39 11 19 42 14 24 51 15 29 58 34 50 84 59 79 114 93 115 154 116 139 181 127 151 195 128 155 200 124 152 199 118 146 194 120 150 202 118 149 204 118 149 206 116 148 207 113 147 208 112 146 209 110 146 208 109 145 207 111 147 209 110 146 206 109 143 204 108 142 203 111 143 204 111 143 204 111 141 203 111 141 203 16 16 44 15 16 44 11 15 42 10 12 33 17 15 28 18 9 10 51 33 21 181 158 140 204 182 159 195 174 153 198 185 169 189 176 167 206 191 186 219 195 195 192 158 159 75 41 42 30 9 8 23 12 8 18 13 7 15 16 10 12 19 12 9 19 11 6 17 11 3 14 10 10 19 18 5 11 11 11 15 18 14 17 22 17 18 23 11 12 17 20 19 27 9 8 14 16 16 18 15 15 15 11 11 9 11 11 9 16 15 11 13 12 8 11 10 6 15 14 10 14 14 12 14 14 12 11 11 11 13 14 16 11 12 16 19 20 25 87 90 97 220 218 223 243 228 231 255 237 234 153 109 98 113 56 36 187 118 85 198 125 84 183 106 62 179 102 58 189 113 77 166 93 60 155 86 57 141 74 48 159 89 64 148 74 47 165 86 56 179 94 65 176 86 60 178 88 62 175 88 60 183 100 68 174 96 60 157 84 49 154 91 58 138 89 59 211 176 156 248 229 215 241 231 230 233 231 236 235 232 239 70 65 72 19 8 14 20 11 16 13 13 15 10 14 17 9 13 16 10 11 15 10 9 15 10 9 15 12 9 16 15 10 17 17 12 19 24 18 28 25 19 29 21 15 25 22 17 24 28 23 30 27 22 29 20 15 22 25 22 31 23 20 31 19 17 30 18 17 31 20 22 37 22 25 44 18 23 43 12 18 42 13 23 50 16 27 55 13 26 58 10 25 58 26 42 78 65 83 119 108 128 165 132 153 196 124 148 196 122 149 202 122 149 204 120 149 205 118 149 206 115 147 206 111 145 206 109 143 204 112 148 210 111 147 209 110 144 207 109 143 204 111 143 204 110 142 203 111 141 203 110 140 202 18 19 49 13 14 42 16 16 44 14 12 34 19 13 27 19 8 12 49 32 22 173 156 138 198 182 159 198 183 162 196 184 172 187 176 170 173 161 161 142 127 130 44 25 29 29 13 14 21 11 12 17 13 12 14 14 12 13 15 14 11 15 14 12 14 13 13 13 13 14 12 15 17 12 16 15 13 16 14 14 16 11 15 18 10 15 18 7 15 17 9 14 17 10 14 13 16 17 12 15 15 7 14 14 6 14 13 8 16 12 9 16 12 9 15 11 10 13 9 10 19 17 20 13 13 15 14 13 18 10 11 15 14 15 20 9 12 17 74 79 83 222 223 227 229 220 225 255 243 241 244 216 205 98 55 36 152 93 63 189 116 81 198 116 78 181 97 60 185 107 71 182 107 75 155 84 56 148 78 52 148 76 51 127 53 24 169 94 62 162 86 54 164 82 58 159 75 49 176 91 60 185 102 68 164 86 50 150 84 50 116 62 34 179 143 121 244 222 209 252 241 235 238 232 232 232 230 233 228 223 229 27 20 28 21 11 20 15 8 15 11 11 13 11 12 14 12 13 15 12 12 14 11 10 15 11 10 16 12 11 17 15 12 21 14 11 20 15 12 23 18 15 26 21 18 29 22 20 31 23 21 32 23 22 30 23 22 30 24 21 30 26 20 30 25 22 31 24 21 32 22 20 33 21 19 33 21 20 36 19 20 38 20 23 42 19 24 44 21 26 48 18 24 48 12 17 46 8 15 43 17 26 55 28 41 75 86 105 147 127 149 196 125 149 197 119 145 194 119 146 199 121 151 205 111 142 197 116 148 207 111 145 206 111 145 208 111 145 208 111 145 208 111 142 206 111 143 204 112 142 204 112 142 202 16 17 45 16 17 45 18 15 44 20 15 38 24 13 30 18 5 12 38 23 16 173 160 143 187 174 155 205 192 176 172 159 151 38 26 26 26 15 21 26 16 24 7 0 3 25 19 23 18 12 16 17 12 18 16 14 19 15 15 17 13 14 18 14 14 16 17 12 16 17 11 15 18 12 16 17 12 16 14 14 16 13 15 14 11 15 14 10 14 13 12 14 13 13 13 11 14 13 8 13 13 5 13 12 7 13 12 8 17 13 14 17 12 16 18 11 18 16 11 17 14 9 16 12 9 16 16 15 23 17 16 22 15 16 21 14 15 19 53 57 60 229 230 232 229 224 228 246 236 235 255 243 234 232 205 186 129 79 54 175 108 79 188 107 77 195 111 77 186 107 74 188 115 82 159 88 60 143 72 44 155 81 54 124 50 21 138 67 35 143 72 40 141 67 42 153 75 52 158 77 47 188 109 76 161 88 55 114 55 25 152 110 88 248 222 209 250 237 231 243 237 237 249 245 246 234 229 233 152 146 150 20 13 20 14 6 17 16 11 18 11 11 13 12 12 12 13 13 15 12 12 14 11 10 15 11 10 15 13 12 20 14 13 21 16 14 25 16 16 26 17 17 29 19 19 31 20 19 33 19 21 34 19 21 33 21 21 31 24 21 30 27 22 29 27 22 29 27 21 31 24 21 30 24 21 32 24 21 32 25 21 35 27 25 38 23 21 35 22 19 36 24 23 41 24 22 43 19 20 40 19 20 41 19 23 50 11 24 59 20 35 76 88 105 148 130 151 194 122 146 192 118 144 193 118 148 200 111 142 197 113 145 204 111 145 208 111 145 209 110 144 208 111 142 207 110 141 205 111 141 203 112 142 202 15 15 39 20 18 42 16 10 36 23 13 38 27 14 32 21 8 17 31 18 12 177 165 153 206 194 180 184 171 162 38 27 25 29 18 22 17 7 16 18 11 19 20 18 23 17 12 18 23 10 20 25 9 20 22 12 21 19 14 20 16 13 20 15 14 19 15 15 17 14 14 16 13 13 13 14 14 14 14 14 14 14 14 12 17 13 12 19 10 11 21 9 9 19 9 8 13 11 12 11 13 12 12 12 14 12 11 16 15 12 21 15 12 23 17 11 25 17 11 25 18 12 26 18 12 26 19 13 25 21 15 25 14 9 16 20 15 21 35 33 34 226 222 223 239 234 240 242 236 238 255 255 246 254 238 222 249 211 192 148 90 68 191 115 91 201 120 93 192 117 86 185 114 82 164 95 64 150 81 50 156 82 53 123 49 20 135 69 37 148 83 53 135 67 44 128 57 35 173 99 72 170 99 69 137 76 47 137 89 66 251 222 208 248 233 226 255 250 252 240 239 244 245 243 246 231 227 228 67 58 61 15 6 11 13 7 17 15 12 21 14 12 15 13 13 13 13 13 13 13 13 15 12 11 16 12 11 16 13 13 21 14 14 22 16 16 26 15 18 27 15 17 29 15 19 31 16 19 34 17 20 35 16 22 36 18 22 34 22 20 31 24 21 30 25 22 31 25 22 31 26 20 30 26 20 30 26 20 30 27 21 31 32 24 35 26 18 31 25 17 30 28 22 36 30 24 38 25 21 35 21 17 32 17 15 36 13 17 46 12 21 54 20 31 63 50 64 99 117 135 173 127 150 192 119 146 193 118 145 198 113 144 201 113 145 206 111 145 209 110 144 208 108 142 206 107 141 204 110 142 203 110 142 201 20 17 36 23 18 38 15 7 30 20 11 32 23 12 28 23 12 20 25 14 12 173 163 154 193 183 173 87 76 70 20 10 11 30 20 28 12 4 15 15 12 21 13 12 17 13 11 16 23 11 21 24 11 21 23 11 21 20 13 21 17 14 21 16 15 20 14 15 17 12 16 17 10 14 13 10 14 13 14 14 12 15 14 12 17 11 11 20 10 9 23 9 9 20 8 10 14 12 17 10 13 18 12 12 20 12 12 22 13 11 24 13 11 25 14 10 25 16 9 25 19 12 28 20 11 28 20 12 25 20 12 23 14 7 15 20 14 18 24 18 20 171 165 167 229 222 229 252 247 251 246 247 241 255 255 243 255 249 234 232 186 170 147 82 64 200 125 102 215 144 116 191 122 91 174 108 76 164 98 66 148 77 47 118 49 18 149 84 54 141 77 50 130 68 45 136 74 53 177 113 88 160 103 76 121 73 51 253 217 201 255 237 230 255 249 250 253 252 255 247 248 253 238 236 239 202 198 199 25 16 19 14 8 12 14 11 18 13 12 18 15 13 18 15 13 16 14 14 16 13 13 15 13 13 15 14 13 18 14 15 20 15 16 21 12 15 22 13 16 25 14 18 29 15 19 31 17 20 35 15 21 37 16 22 38 17 20 35 20 20 32 23 20 31 23 20 31 23 20 31 25 19 29 25 19 29 27 19 30 27 20 28 32 22 31 30 20 29 30 20 31 28 20 31 27 19 30 25 17 28 26 18 31 27 23 38 27 25 46 16 18 43 13 17 42 13 23 50 8 22 51 33 51 87 105 128 170 121 147 196 114 143 199 113 143 205 112 143 208 109 142 209 108 142 206 107 141 204 109 141 202 109 141 200 29 18 32 24 16 31 19 12 28 20 13 29 16 10 24 19 12 20 23 11 11 168 155 149 182 169 163 23 12 10 36 27 30 8 3 10 10 7 16 25 24 30 15 15 17 16 16 18 16 15 21 17 14 21 20 15 22 20 15 21 19 14 20 17 15 20 15 16 18 14 15 17 10 14 13 10 14 13 13 13 13 13 13 11 16 12 11 16 12 11 16 10 10 15 11 12 12 13 18 10 13 22 10 13 22 10 12 24 12 12 24 12 12 24 13 11 24 13 11 24 11 7 21 15 9 21 19 13 25 21 13 24 21 14 22 21 11 19 20 11 16 85 76 81 225 216 221 231 225 229 255 255 253 254 253 248 255 249 241 255 247 236 203 155 141 151 93 73 201 137 110 199 134 104 193 129 94 181 117 82 158 93 61 136 71 41 172 106 80 140 76 51 130 69 48 179 122 102 161 110 89 116 73 56 252 220 207 255 249 243 255 245 245 255 253 255 249 250 255 254 255 255 244 243 248 130 125 129 20 14 18 19 13 17 16 14 19 16 13 20 16 15 20 15 14 19 14 13 18 14 14 16 13 14 16 14 15 17 15 16 20 15 16 20 12 15 20 14 17 22 16 19 28 18 20 32 19 21 34 18 21 36 17 20 37 16 19 34 20 19 33 21 19 32 22 20 33 22 20 33 23 20 31 23 20 29 25 19 29 26 20 30 27 19 30 28 20 31 28 20 31 26 20 30 24 18 30 23 17 29 26 20 32 30 24 36 28 22 36 32 28 43 18 17 33 15 18 37 11 19 42 8 22 51 43 61 99 127 149 196 117 142 198 114 143 203 112 143 208 111 142 209 108 142 206 106 140 203 106 140 201 107 142 200 53 36 46 25 12 22 21 15 27 21 19 32 12 10 21 17 12 19 26 14 16 163 148 145 104 89 86 22 10 10 10 4 8 25 24 30 16 16 24 9 10 15 17 15 16 21 19 20 13 17 18 13 17 20 16 15 20 19 14 20 20 13 20 19 14 20 16 14 17 14 15 17 12 14 13 12 14 13 13 13 13 14 12 13 14 12 13 13 13 13 12 12 12 11 12 14 10 11 16 11 11 19 12 12 20 12 12 20 12 13 18 12 13 18 13 14 19 14 15 19 14 13 19 15 14 20 17 14 21 16 13 20 22 17 24 19 12 20 24 17 25 31 21 30 194 183 189 229 220 225 242 240 243 251 251 251 255 255 251 255 251 243 255 247 236 183 137 121 166 110 85 202 141 110 215 155 119 198 138 102 183 123 89 166 102 74 191 123 102 167 99 80 172 112 88 171 121 98 135 93 77 253 223 213 255 250 246 253 243 244 255 253 255 254 253 255 251 252 255 253 254 255 236 235 240 49 47 50 23 18 24 21 16 20 18 13 17 18 16 21 17 16 22 15 16 21 14 13 19 13 14 18 14 15 17 15 16 18 16 17 19 16 17 19 17 21 22 18 22 25 19 22 27 22 22 32 22 22 34 21 20 34 18 20 35 17 19 34 21 20 36 22 21 35 23 21 35 23 21 35 23 19 33 23 20 31 24 21 32 27 21 31 26 20 32 24 18 30 24 18 30 23 20 31 24 20 34 23 19 33 20 18 31 20 17 28 22 12 21 21 11 20 34 28 38 22 22 34 11 17 33 8 19 41 2 18 51 102 123 166 119 142 196 115 144 204 114 143 209 111 142 209 108 141 208 106 140 203 106 140 201 107 142 200 120 95 101 42 23 29 20 15 22 17 20 29 11 14 21 14 13 19 26 14 16 148 130 130 57 37 38 28 14 14 19 14 18 6 9 14 14 19 23 17 18 22 23 17 17 13 8 5 11 17 15 11 17 17 16 14 17 19 13 17 22 11 17 22 11 17 17 12 16 15 13 16 13 13 15 13 13 15 14 12 15 14 12 15 14 12 15 12 12 14 9 13 14 9 13 14 13 11 14 16 12 13 17 13 14 14 13 11 13 14 9 11 14 7 12 15 8 11 16 9 15 20 14 14 19 15 12 16 15 10 11 15 15 16 21 14 13 21 24 22 33 21 15 25 130 117 126 234 223 229 243 238 244 254 255 255 247 253 253 254 255 250 255 248 239 255 241 226 181 131 108 177 118 88 200 142 105 194 136 98 190 134 99 180 119 91 191 118 101 183 110 93 178 118 94 184 138 114 253 221 208 255 245 241 251 245 249 255 254 255 255 254 255 250 249 255 255 254 255 250 248 253 170 169 174 15 14 19 20 19 25 18 16 21 21 17 18 21 16 20 19 18 26 14 17 26 14 14 22 13 14 19 15 16 20 16 17 19 17 19 18 17 19 18 21 23 22 21 23 22 20 21 23 21 20 26 21 20 28 21 19 30 21 19 32 20 18 32 21 20 36 22 21 37 22 21 37 22 21 35 22 20 34 22 20 33 22 20 33 24 21 32 22 18 32 20 18 31 20 18 31 20 18 32 21 20 36 20 19 35 15 17 32 15 13 26 35 24 30 26 11 14 35 26 29 17 15 20 11 13 25 16 24 43 6 21 50 52 71 111 120 143 193 117 143 202 114 143 209 112 143 210 108 141 208 106 141 205 105 141 203 106 142 202 190 162 161 66 44 46 23 12 16 18 16 21 15 14 19 14 12 17 20 8 12 123 107 110 27 8 10 16 4 6 20 18 21 16 20 23 12 16 19 14 15 17 23 14 15 22 17 14 12 16 15 11 17 15 16 14 15 19 13 15 22 11 17 20 11 16 17 12 16 15 13 16 13 13 15 13 13 15 14 12 17 14 12 17 14 12 17 12 11 16 9 13 16 9 13 14 17 13 14 19 13 13 18 14 11 17 13 10 13 12 7 11 12 6 10 13 4 9 15 5 6 11 4 10 15 9 10 14 13 10 14 15 13 16 21 13 13 21 16 16 26 24 21 32 88 77 85 238 225 232 233 226 234 255 254 255 251 255 255 247 251 250 255 254 248 255 250 240 255 230 214 174 129 108 194 150 123 219 175 148 236 194 169 244 197 177 245 187 176 237 179 168 223 176 158 182 147 128 255 232 224 255 252 251 255 253 255 249 252 255 251 251 255 255 254 255 255 254 255 248 246 251 91 90 95 19 20 25 18 19 24 15 14 19 26 22 23 20 15 19 19 18 26 14 17 26 14 14 22 14 15 20 15 16 20 17 18 20 17 18 20 17 19 18 20 21 23 19 20 22 18 17 22 18 17 23 19 16 25 20 17 28 22 19 30 22 20 33 22 19 36 21 20 36 22 21 37 21 20 34 21 19 33 20 18 32 21 19 32 21 19 32 19 17 30 21 19 32 20 19 33 19 18 32 16 15 31 15 17 32 16 18 33 21 19 32 24 15 20 29 19 20 24 15 18 21 16 22 30 29 37 7 13 25 11 22 44 15 31 65 122 142 192 118 142 202 114 143 209 110 143 210 108 142 206 106 142 204 105 141 201 106 142 202 208 179 171 160 135 128 35 14 11 29 13 13 25 10 13 22 10 14 29 17 21 61 49 53 22 11 15 19 13 15 18 16 19 17 17 19 17 17 19 18 16 19 18 13 17 18 13 17 17 17 19 15 16 18 15 15 17 16 14 17 17 12 16 15 13 16 15 13 16 14 14 16 12 12 14 12 12 14 13 13 15 13 13 15 13 13 15 13 13 15 11 12 14 10 11 13 14 12 15 15 13 14 16 14 15 16 14 15 14 14 14 13 13 13 12 12 12 11 13 12 11 13 12 12 13 15 12 13 15 13 14 18 13 14 19 16 15 21 18 17 25 21 18 25 44 34 42 221 210 218 239 229 237 248 243 249 252 250 255 252 252 254 255 254 255 255 251 252 255 247 246 255 240 236 251 234 227 242 225 218 246 227 223 236 217 213 203 177 178 177 153 153 65 44 41 35 20 15 68 58 57 228 222 224 253 251 255 255 254 255 255 254 255 252 251 255 247 246 251 214 213 218 24 23 28 23 22 27 24 23 28 21 19 22 15 13 14 22 20 23 18 17 22 17 18 23 19 18 24 19 18 24 19 18 24 19 18 24 20 19 25 20 19 25 22 21 29 22 21 29 22 21 29 21 20 28 21 19 30 20 18 29 20 18 29 20 18 29 21 19 32 21 19 33 20 18 32 21 19 32 22 20 33 22 20 33 20 18 31 18 16 29 19 17 30 18 16 29 18 16 29 18 16 29 19 17 31 18 17 31 18 17 31 18 16 27 16 15 21 19 17 22 23 17 27 21 18 27 19 17 22 15 16 21 8 14 26 4 14 41 111 128 174 116 138 198 112 140 205 113 147 211 106 141 199 110 147 202 104 140 198 109 145 203 206 176 165 188 160 148 91 64 57 29 5 1 30 10 9 25 9 10 27 15 19 36 27 32 18 12 16 18 13 17 17 15 16 18 16 17 18 16 19 17 15 18 18 13 20 15 13 18 16 15 20 15 15 17 15 15 17 14 14 16 14 14 16 14 14 16 14 14 16 14 14 16 13 13 15 13 13 15 13 13 15 13 13 15 13 13 15 13 13 15 12 12 14 12 12 14 12 12 14 13 13 15 14 14 16 15 15 17 14 13 18 13 12 17 13 12 17 13 12 17 14 13 18 14 13 18 15 14 19 14 13 18 14 13 19 14 13 19 16 15 21 18 15 22 30 23 30 177 167 175 244 234 242 247 240 247 248 243 249 255 253 255 250 248 253 255 253 255 250 248 253 137 135 140 82 77 83 76 71 77 55 48 55 39 32 39 36 26 34 30 21 26 28 22 26 29 23 25 19 14 18 42 37 41 206 204 209 250 248 253 255 254 255 252 251 255 255 254 255 117 116 121 23 22 27 16 15 20 19 17 20 21 19 22 19 17 18 22 20 21 20 19 24 20 19 24 20 19 24 20 19 24 20 19 27 21 20 28 21 19 30 21 19 30 21 19 30 21 19 30 21 19 30 22 20 31 22 20 31 22 20 31 22 21 29 22 21 29 22 20 33 21 19 32 21 19 32 22 20 33 22 20 33 22 20 33 20 18 31 19 17 30 23 21 34 22 20 33 21 19 32 20 18 31 19 17 30 18 16 29 17 15 28 15 15 25 15 15 23 16 16 24 19 17 30 20 17 28 21 16 22 17 15 18 12 15 22 8 14 36 70 83 128 115 135 194 119 147 210 107 139 200 110 145 199 108 145 197 105 142 197 112 148 206 201 169 156 200 171 157 153 126 117 66 41 36 29 8 7 32 13 15 32 20 24 22 13 16 18 12 16 17 12 16 16 14 15 17 15 16 17 15 18 16 14 17 15 12 19 15 13 18 15 14 19 15 15 17 14 14 16 14 14 16 14 14 16 13 13 15 13 13 15 13 13 15 15 15 17 15 15 17 14 14 16 13 13 15 13 13 15 13 13 15 12 12 14 12 12 14 11 11 13 12 12 14 14 14 16 14 14 16 14 13 18 13 12 17 14 13 18 14 13 18 13 12 17 14 13 18 15 14 19 14 13 18 14 13 19 14 13 19 14 13 19 16 13 20 27 20 27 123 116 123 230 223 230 248 241 248 255 253 255 250 245 251 255 254 255 250 248 253 112 110 115 24 22 27 8 6 11 30 28 33 21 16 22 20 15 21 29 22 29 22 16 20 20 14 18 24 18 20 35 30 34 22 17 21 44 42 47 217 215 220 254 253 255 245 244 249 226 225 230 40 39 44 25 24 29 19 18 23 19 17 20 24 22 25 22 20 21 22 20 21 21 20 25 21 20 25 21 20 25 21 20 25 21 20 28 21 20 28 21 19 30 21 19 30 22 20 31 22 20 31 23 21 32 23 21 32 23 21 32 23 21 32 23 22 30 22 21 29 24 22 35 23 21 34 23 21 34 24 22 35 24 22 35 24 22 35 23 21 34 22 20 33 25 23 36 24 22 35 22 20 33 20 18 31 19 17 30 17 15 28 15 13 26 13 13 25 14 14 24 16 16 26 18 16 29 19 16 27 20 15 21 17 15 18 13 16 23 9 16 35 29 42 84 127 146 202 121 147 208 109 139 199 111 146 200 105 142 195 110 146 204 109 145 205 201 168 151 193 161 146 195 165 154 139 112 105 36 12 10 35 17 17 33 18 21 22 13 16 18 12 16 17 12 16 16 14 15 17 15 16 17 15 18 16 14 19 15 12 19 15 13 18 15 14 19 15 15 17 15 15 17 15 15 17 14 14 16 14 14 16 14 14 16 13 13 15 16 16 18 16 16 18 15 15 17 13 13 15 13 13 15 13 13 15 13 13 15 13 13 15 12 12 14 13 13 15 14 14 16 14 14 16 13 12 17 13 12 17 13 12 17 14 13 18 11 10 15 12 11 16 14 13 18 14 13 18 14 13 19 14 13 19 15 14 20 16 13 20 21 16 22 87 80 87 229 222 229 254 249 255 255 252 255 251 249 254 249 247 252 105 103 108 20 18 23 16 14 19 27 25 30 28 26 31 26 21 27 30 25 31 25 18 25 19 13 17 31 25 29 18 12 14 14 9 13 15 10 14 31 29 34 49 47 52 228 227 232 250 249 254 137 136 141 25 24 29 22 21 26 25 24 29 22 20 23 23 21 24 22 20 21 21 19 22 21 20 25 21 20 25 21 20 25 21 20 25 21 20 28 20 19 27 20 18 29 20 18 29 25 23 34 25 23 34 25 23 34 24 22 33 23 21 32 22 20 31 21 20 28 21 20 28 23 21 34 23 21 34 23 21 34 24 22 35 24 22 35 24 22 35 23 21 34 22 20 33 22 20 33 21 19 32 20 18 31 19 17 30 18 16 29 17 15 28 16 14 27 14 14 26 15 15 27 16 16 28 17 15 29 18 15 26 19 14 20 16 14 17 13 16 21 9 16 34 10 22 60 122 140 190 112 137 193 115 146 203 110 143 197 105 142 197 115 151 211 104 140 202 208 173 153 189 156 137 208 176 163 180 152 141 63 38 34 35 15 14 24 10 10 24 14 15 19 13 15 18 14 15 16 14 15 17 15 16 17 15 18 16 14 19 16 13 20 16 14 19 15 14 19 15 15 17 15 15 17 15 15 17 15 15 17 15 15 17 14 14 16 14 14 16 17 17 19 16 16 18 15 15 17 14 14 16 13 13 15 13 13 15 13 13 15 13 13 15 13 13 15 13 13 15 14 14 16 14 14 16 13 12 17 12 11 16 12 11 16 13 12 17 11 10 15 12 11 16 13 12 17 14 13 18 14 13 19 14 13 19 15 14 20 16 13 20 23 18 25 39 34 41 204 199 206 239 236 243 249 247 252 255 254 255 100 98 103 14 13 18 17 16 21 24 23 28 22 20 25 12 10 15 18 16 19 23 21 24 20 15 19 31 26 30 24 18 22 24 18 22 27 22 26 25 20 24 20 18 23 16 14 19 84 83 89 230 229 235 49 48 54 22 21 27 19 18 23 22 21 26 21 19 22 19 17 20 23 21 24 19 17 20 22 21 26 22 21 26 22 21 26 21 20 25 21 20 28 21 20 28 21 19 30 21 19 30 26 24 35 26 24 35 25 23 34 25 23 34 24 22 33 22 20 31 21 20 28 20 19 27 20 18 31 21 19 32 21 19 32 21 19 32 21 19 32 21 19 32 21 19 32 21 19 32 21 19 32 20 18 31 19 17 30 18 16 29 19 17 30 18 16 29 17 15 28 15 15 27 16 16 28 16 15 29 17 15 29 17 14 25 18 13 19 15 13 16 13 14 19 9 15 29 3 14 44 74 91 134 117 140 190 116 146 200 109 140 194 110 145 201 113 149 211 108 143 207 209 171 148 195 158 139 200 167 150 173 143 132 106 78 74 38 17 16 20 4 5 23 13 14 19 13 15 18 14 15 16 14 15 16 14 17 16 14 19 16 14 19 16 13 20 16 14 19 14 13 18 15 15 17 15 15 17 16 16 18 16 16 18 15 15 17 15 15 17 14 14 16 16 16 18 15 15 17 15 15 17 14 14 16 13 13 15 13 13 15 13 13 15 13 13 15 13 13 15 14 14 16 14 14 16 13 13 15 12 11 16 11 10 15 12 11 16 12 11 16 12 11 16 13 12 17 14 13 18 14 13 18 13 12 18 13 12 18 14 13 19 14 13 19 16 13 20 29 26 33 127 124 131 242 239 246 255 254 255 98 96 101 21 20 25 23 22 27 20 19 24 12 11 16 16 14 19 16 14 19 16 14 17 19 17 20 19 14 18 23 18 22 20 14 18 21 15 19 18 13 17 20 15 19 11 9 14 34 32 37 14 13 19 68 67 73 24 23 29 14 13 19 28 27 32 20 19 24 22 20 23 20 18 21 25 23 26 20 18 21 23 22 27 23 22 27 22 21 26 22 21 26 22 21 29 22 21 29 22 20 31 22 20 31 24 22 33 24 22 33 24 22 33 25 23 34 24 22 33 23 21 32 21 20 28 21 20 28 20 18 31 21 19 32 22 20 33 22 20 33 21 19 32 21 19 32 22 20 33 22 20 33 22 20 33 21 19 32 20 18 31 20 18 31 19 17 30 19 17 30 18 16 29 17 15 28 16 15 29 16 15 29 17 15 29 17 14 25 18 13 19 15 13 16 13 14 18 10 14 26 6 16 41 22 36 73 127 148 193 117 143 194 114 145 199 115 147 206 106 140 204 116 151 217 207 167 142 197 159 136 194 159 140 170 138 125 135 108 101 38 17 14 27 11 11 20 8 10 19 13 15 18 14 15 16 14 17 15 15 17 15 14 19 15 14 19 16 13 20 16 14 19 14 13 18 14 14 16 15 15 17 15 15 17 16 16 18 15 15 17 14 14 16 14 14 16 14 14 16 14 14 16 14 14 16 14 14 16 14 14 16 13 13 15 13 13 15 13 13 15 12 12 14 13 13 15 13 13 15 13 13 15 12 11 16 12 11 16 12 11 16 13 12 17 13 12 17 13 12 17 14 13 18 13 12 17 13 12 18 13 12 18 15 14 20 16 15 21 12 11 17 29 28 34 68 67 73 239 238 244 118 117 123 19 18 24 12 11 16 12 11 16 16 15 20 11 10 15 21 21 23 19 19 21 11 9 12 17 15 18 19 17 20 15 13 16 22 17 21 24 19 23 26 21 25 20 15 19 31 29 34 10 8 13 21 20 26 25 24 30 28 27 33 16 15 21 32 31 36 23 22 27 20 20 22 22 22 24 23 21 24 21 19 22 22 21 26 22 21 26 22 21 26 22 21 26 22 21 29 22 21 29 22 20 31 22 20 31 23 21 32 24 22 33 24 22 33 24 22 33 23 21 32 22 20 31 20 19 27 19 18 26 21 19 32 22 20 33 23 21 34 22 20 33 21 19 32 21 19 32 23 21 34 24 22 35 22 20 33 21 19 32 20 18 31 19 17 30 20 18 31 19 17 30 18 16 29 17 15 28 14 13 27 15 14 28 17 15 29 16 14 25 16 14 19 15 13 16 13 14 18 10 14 23 10 17 36 2 16 45 102 121 161 120 144 190 117 147 199 113 145 204 107 140 207 116 151 219 213 169 144 198 158 133 195 157 138 186 153 138 143 114 106 37 13 9 35 17 17 15 3 3 18 12 14 15 13 14 15 13 16 14 14 16 14 13 18 14 13 18 15 12 19 15 13 18 13 12 17 13 13 15 14 14 16 15 15 17 15 15 17 15 15 17 14 14 16 13 13 15 13 13 15 13 13 15 14 14 16 14 14 16 14 14 16 14 14 16 13 13 15 12 12 14 11 11 13 12 12 14 13 13 15 12 12 14 12 11 16 12 11 16 13 12 17 14 13 18 11 10 15 12 11 16 12 11 16 13 12 17 13 12 18 15 14 20 17 16 22 19 18 24 22 21 27 11 10 16 33 32 38 106 105 111 16 15 21 20 19 25 19 18 23 11 10 15 15 14 19 13 12 17 17 17 19 14 14 16 13 11 14 15 13 16 14 12 15 21 19 22 20 15 19 20 15 19 25 20 24 21 19 22 13 11 16 31 29 34 21 20 26 24 23 29 19 18 24 23 22 28 21 20 25 22 21 26 15 14 19 23 23 25 18 16 19 22 20 23 20 19 24 20 19 24 21 20 25 21 20 25 21 20 28 21 20 28 22 20 31 22 20 31 25 23 34 25 23 34 25 23 34 24 22 33 23 21 32 20 18 29 18 17 25 17 16 24 18 16 27 19 17 30 20 18 31 20 18 31 18 16 29 18 16 29 20 18 31 21 19 32 19 17 30 18 16 29 18 16 29 18 16 29 19 17 30 19 17 30 19 17 30 18 16 29 13 12 26 14 13 27 16 14 27 16 15 23 16 14 19 15 13 16 14 13 18 11 14 21 5 11 25 7 18 40 57 73 106 123 146 188 116 144 194 111 141 201 116 149 216 112 146 217 216 170 144 197 155 130 201 160 140 182 146 130 147 119 108 48 24 20 25 7 7 24 12 12 17 11 13 14 12 13 13 11 14 13 13 15 13 12 17 15 14 19 17 14 21 18 16 21 14 13 18 13 13 15 13 13 15 13 13 15 13 13 15 14 14 16 14 14 16 15 15 17 15 15 17 14 14 16 13 13 15 13 13 15 14 14 16 14 14 16 13 13 15 12 12 14 13 13 15 13 13 15 12 12 14 12 12 14 12 11 16 12 11 16 13 12 17 13 12 17 15 14 19 15 14 19 14 13 18 13 12 17 13 12 18 14 13 19 15 14 20 17 16 22 16 16 24 16 16 24 27 28 33 14 15 20 15 16 21 24 25 30 11 10 15 17 16 21 24 23 28 19 18 23 22 20 23 18 16 19 25 23 26 13 11 14 23 21 22 41 39 40 33 28 32 38 33 37 45 43 48 22 20 25 21 18 25 19 18 24 20 19 25 24 23 29 20 21 26 19 20 25 20 19 25 21 20 25 23 22 27 23 22 27 23 21 24 22 20 23 22 21 26 22 21 26 22 21 26 22 21 26 22 21 29 23 22 30 23 21 32 24 22 33 24 22 33 23 21 32 22 20 31 23 21 32 24 22 33 24 22 33 23 22 30 22 21 29 20 18 29 20 18 29 19 17 28 17 15 26 17 15 26 18 16 27 21 19 30 23 21 32 22 20 31 20 18 29 19 17 28 18 16 27 18 16 27 19 17 28 19 17 28 18 16 29 14 13 27 14 13 27 15 13 24 15 14 22 16 14 19 16 14 19 16 15 20 15 16 21 13 17 28 7 16 33 22 36 63 126 146 183 120 146 195 113 143 203 116 149 216 109 146 216 217 172 143 202 158 131 200 159 139 182 146 130 150 120 110 43 20 14 23 5 5 21 11 12 17 11 13 14 12 13 13 13 15 14 14 16 14 13 18 14 13 18 15 12 19 15 13 18 14 13 18 14 14 16 14 14 16 13 13 15 14 14 16 14 14 16 15 15 17 15 15 17 14 14 16 13 13 15 13 13 15 13 13 15 14 14 16 14 14 16 13 13 15 12 12 14 13 13 15 13 13 15 13 13 15 12 12 14 12 11 16 13 12 17 13 12 17 13 12 17 14 13 18 14 13 18 14 13 18 13 12 17 12 11 17 12 11 17 14 13 19 16 15 21 17 17 25 16 16 24 24 25 30 16 17 22 10 11 16 12 13 18 9 8 13 19 18 23 13 12 17 16 15 20 15 13 16 129 127 130 171 169 172 200 198 201 216 214 215 206 204 205 214 209 213 204 199 203 185 183 188 158 156 161 29 28 34 17 16 22 20 19 25 17 16 22 18 19 24 18 19 24 19 18 24 20 19 25 22 21 26 23 22 27 23 21 24 22 20 23 21 20 25 20 19 24 19 18 23 18 17 22 18 17 25 18 17 25 20 18 29 20 18 29 21 19 30 21 19 30 21 19 30 23 21 32 25 23 34 25 23 34 24 23 31 22 21 29 20 18 29 20 18 29 19 17 28 17 15 26 17 15 26 18 16 27 21 19 30 23 21 32 23 21 32 21 19 30 19 17 28 18 16 27 18 16 27 18 16 27 17 15 26 17 15 28 15 14 28 15 15 27 15 13 24 15 14 20 15 12 19 15 12 19 14 13 19 14 13 19 10 13 20 6 12 24 12 23 45 107 123 157 119 141 191 116 145 205 114 147 214 112 149 219 215 170 141 204 160 133 196 155 135 181 145 129 150 122 111 35 15 8 22 6 6 20 10 11 17 11 13 15 13 16 14 14 16 14 15 17 14 13 18 13 12 17 13 10 17 12 10 15 14 13 18 14 14 16 14 14 16 14 14 16 14 14 16 15 15 17 15 15 17 15 15 17 14 14 16 13 13 15 12 12 14 12 12 14 13 13 15 14 14 16 14 14 16 13 13 15 14 14 16 14 14 16 13 13 15 13 13 15 13 12 17 13 12 17 14 13 18 14 13 18 14 13 18 14 13 18 14 13 18 13 12 17 12 11 17 12 11 17 14 13 19 15 14 20 12 12 20 15 18 25 21 21 29 17 17 25 16 15 21 24 23 29 26 24 29 17 15 20 26 24 29 22 20 25 105 103 106 205 203 206 241 239 240 253 251 252 255 254 255 255 254 255 255 250 254 255 250 254 242 240 245 223 221 226 91 90 96 26 25 31 16 17 22 14 15 20 16 17 22 16 17 22 17 18 23 18 19 24 21 20 25 22 21 26 22 22 24 21 21 23 21 20 25 20 19 24 18 17 22 16 15 20 15 14 22 16 15 23 17 15 26 18 16 27 19 17 28 20 18 29 21 19 30 24 22 33 25 23 34 25 23 34 23 22 30 21 20 28 20 18 29 20 18 29 19 17 28 17 15 26 17 15 26 18 16 27 20 18 29 22 20 31 23 21 32 21 19 30 19 17 28 18 16 27 17 15 26 17 15 26 16 14 25 15 13 24 16 16 28 16 16 26 15 15 23 14 15 20 14 13 21 13 12 20 14 11 20 13 12 18 11 10 15 7 12 18 2 11 28 74 89 120 119 139 189 120 146 205 111 145 209 114 151 219 212 167 138 205 161 136 193 152 132 181 145 129 147 119 108 31 10 5 25 9 9 20 11 12 16 12 13 14 14 16 13 14 16 13 14 16 13 12 17 13 12 17 13 10 17 12 10 15 14 14 16 14 14 16 14 14 16 14 14 16 14 14 16 15 15 17 15 15 17 15 15 17 13 13 15 12 12 14 12 12 14 12 12 14 13 13 15 14 14 16 14 14 16 13 13 15 14 14 16 14 14 16 14 14 16 14 14 16 14 13 18 14 13 18 14 13 18 14 13 18 15 14 19 16 15 20 15 14 19 14 13 18 13 12 18 13 12 18 14 13 19 14 15 20 15 15 23 16 19 26 16 16 24 17 17 25 17 16 22 21 20 26 27 25 30 14 12 17 18 16 21 29 27 32 176 174 177 222 220 223 255 254 255 255 254 255 249 247 248 255 254 255 253 248 252 252 247 251 237 235 240 222 220 225 116 115 121 25 24 30 14 15 20 20 21 26 16 17 22 16 17 22 17 18 23 18 19 24 21 20 25 21 20 25 22 22 24 22 22 24 22 21 26 21 20 25 19 18 23 17 16 21 17 16 24 17 16 24 18 16 27 18 16 27 19 17 28 21 19 30 22 20 31 24 22 33 24 22 33 23 21 32 20 19 27 18 17 25 19 17 28 19 17 28 18 16 27 17 15 26 16 14 25 16 14 25 18 16 27 20 18 29 21 19 30 20 18 29 18 16 27 17 15 26 17 15 26 17 15 26 16 14 25 15 13 24 16 16 26 15 15 23 14 15 19 13 14 18 13 12 20 13 11 22 13 9 23 13 10 19 15 13 16 11 12 16 2 10 23 43 57 84 125 143 191 121 147 206 111 145 209 113 151 216 212 167 138 206 162 137 191 153 132 180 147 132 134 107 98 27 8 2 25 11 10 21 12 13 17 13 14 14 14 16 12 13 15 11 12 14 12 11 16 13 12 17 14 12 17 14 12 17 13 13 15 13 13 15 13 13 15 14 14 16 14 14 16 14 14 16 14 14 16 14 14 16 14 14 16 13 13 15 12 12 14 12 12 14 13 13 15 13 13 15 14 14 16 13 13 15 14 14 16 14 14 16 14 14 16 14 14 16 14 13 18 14 13 18 14 13 18 14 13 18 15 14 19 15 14 19 15 14 19 15 14 19 13 12 18 13 12 18 14 13 19 14 15 20 17 17 25 12 15 22 13 13 21 23 23 31 23 22 28 17 16 22 25 23 28 31 26 32 24 19 25 105 100 106 205 200 204 252 247 251 255 254 255 255 253 254 255 254 255 253 251 252 255 253 255 255 250 255 239 237 242 226 224 229 64 63 69 18 17 23 18 18 26 16 16 24 17 17 25 18 18 26 19 20 25 20 21 26 21 20 25 22 21 26 22 21 26 23 22 27 21 20 25 21 20 25 21 20 25 21 20 25 21 20 28 20 19 27 20 18 29 20 18 29 22 20 31 23 21 32 24 22 33 24 22 33 22 20 31 19 17 28 16 15 23 15 14 22 19 17 28 19 17 28 18 16 27 16 14 25 15 13 24 15 13 24 17 15 26 18 16 27 20 18 29 18 16 27 17 15 26 17 15 26 17 15 26 17 15 26 16 14 25 15 13 24 14 14 22 14 15 20 13 14 16 13 14 16 13 11 22 13 11 24 14 10 25 16 10 20 18 14 15 12 12 14 10 14 25 21 31 58 127 145 191 121 146 203 113 147 210 111 149 212 213 168 139 203 161 136 193 155 134 181 148 133 115 88 79 26 7 3 24 10 10 19 10 13 15 13 16 14 14 16 12 13 15 11 12 14 12 11 16 13 12 17 15 13 18 16 14 19 13 13 15 13 13 15 13 13 15 14 14 16 14 14 16 14 14 16 14 14 16 14 14 16 17 17 19 15 15 17 13 13 15 12 12 14 13 13 15 13 13 15 13 13 15 13 13 15 14 14 16 14 14 16 14 14 16 14 14 16 14 13 18 14 13 18 14 13 18 14 13 18 13 12 17 14 13 18 14 13 18 14 13 18 13 12 18 13 12 18 13 12 18 13 14 19 12 12 20 14 17 24 17 17 25 18 18 26 27 26 32 27 24 31 22 17 23 22 17 23 55 50 56 166 161 167 225 220 224 255 253 255 255 251 252 252 248 249 255 254 255 255 253 254 252 247 251 242 237 243 231 229 234 172 170 175 22 21 27 19 18 24 26 26 34 13 13 21 19 19 27 20 20 28 21 22 27 21 22 27 22 21 26 22 21 26 22 21 26 23 22 27 21 20 25 21 20 25 22 21 26 23 22 27 23 22 30 22 21 29 22 20 31 21 19 30 25 23 34 25 23 34 24 22 33 22 20 31 19 17 28 16 14 25 14 13 21 14 13 21 19 17 28 19 17 28 18 16 27 17 15 26 15 13 24 15 13 24 17 15 26 18 16 27 18 16 27 17 15 26 17 15 26 17 15 26 17 15 26 17 15 26 16 14 25 15 14 22 13 14 19 13 14 18 12 14 13 12 13 15 13 11 22 13 11 25 16 9 25 16 10 22 17 13 12 11 10 8 12 15 24 7 17 42 118 135 179 122 147 204 115 149 210 111 149 211 211 167 140 200 158 133 194 156 135 180 148 133 96 69 62 27 8 4 21 9 9 17 11 13 15 13 16 14 13 18 13 14 18 13 14 18 13 12 17 14 13 18 15 13 18 15 13 18 13 13 15 13 13 15 14 14 16 14 14 16 15 15 17 14 14 16 14 14 16 14 14 16 19 19 21 17 17 19 15 15 17 13 13 15 13 13 15 13 13 15 12 12 14 12 12 14 13 13 15 13 13 15 14 14 16 14 14 16 14 13 18 14 13 18 13 12 17 13 12 17 13 12 17 13 12 17 14 13 18 14 13 18 13 12 18 13 12 18 14 13 19 13 14 19 14 14 22 17 20 29 21 21 29 13 13 21 20 19 25 21 18 25 19 14 21 45 40 46 152 145 152 210 204 208 252 246 250 255 250 254 255 254 255 255 254 255 255 254 253 253 249 250 255 254 255 236 234 239 210 208 213 76 74 79 21 20 26 22 21 27 23 23 31 21 21 29 21 21 29 22 22 30 23 24 29 22 23 28 22 21 26 21 20 25 21 20 25 22 21 26 23 22 27 23 22 27 23 22 27 23 22 27 23 22 30 23 22 30 23 21 32 23 21 32 25 23 34 25 23 34 23 21 32 20 18 29 16 14 25 15 13 24 15 14 22 16 15 23 21 19 30 20 18 29 20 18 29 18 16 27 17 15 26 16 14 25 17 15 26 18 16 27 18 16 27 18 16 27 17 15 26 16 14 25 16 14 25 16 14 25 15 13 24 13 12 20 14 13 19 14 14 16 12 14 11 12 13 15 13 11 22 13 10 27 16 9 27 16 10 22 18 12 12 13 12 10 10 13 22 6 14 37 100 114 159 123 148 204 114 149 207 113 152 211 210 165 136 196 154 129 194 156 135 180 148 133 82 57 50 27 9 7 21 11 12 19 13 17 15 13 18 14 13 18 15 14 19 15 14 19 15 14 19 14 13 18 13 12 17 12 11 16 13 13 15 14 14 14 14 14 16 15 15 15 15 15 17 15 15 15 15 15 17 14 14 14 20 20 22 18 18 20 15 15 17 14 14 16 13 13 15 12 12 14 12 12 14 11 11 13 13 13 15 13 13 15 14 14 16 14 14 16 14 13 18 14 13 18 13 12 17 13 12 17 13 12 17 14 13 18 15 14 19 15 14 19 14 13 19 14 13 19 15 14 20 16 15 21 17 17 25 9 9 17 20 20 28 26 26 34 27 26 34 16 15 21 48 45 52 149 144 151 208 203 209 237 231 235 255 251 255 255 253 255 251 247 248 255 252 253 255 251 252 255 254 255 246 239 246 211 201 209 179 172 179 24 19 25 27 24 31 29 28 34 16 16 24 23 26 33 20 23 30 23 24 29 24 25 30 23 24 28 22 21 26 21 21 23 21 21 23 22 22 24 26 25 30 26 25 30 25 24 29 24 23 28 23 24 29 23 23 31 24 22 33 25 23 34 24 22 33 24 22 33 24 18 30 19 16 25 15 13 24 13 13 21 17 14 23 19 16 25 23 20 29 23 20 31 21 19 30 19 17 28 17 17 27 16 16 26 17 17 27 18 18 28 18 18 28 17 17 27 16 16 26 16 14 25 17 14 23 16 13 22 13 12 20 12 11 19 14 13 19 14 13 18 13 13 13 13 13 15 12 10 21 12 10 24 13 9 26 13 9 23 19 14 18 17 15 18 10 10 20 10 16 40 85 98 142 126 149 203 114 146 205 114 152 214 206 157 127 203 157 131 194 156 135 181 152 138 68 45 39 22 8 8 22 13 16 13 8 14 17 15 20 16 14 19 15 13 16 14 12 15 14 12 15 13 12 17 13 12 17 14 13 18 11 11 11 10 10 8 19 19 19 5 5 3 24 24 24 16 16 14 10 10 10 17 17 15 16 16 16 12 12 12 13 11 12 20 18 19 9 7 8 15 13 14 15 13 14 11 9 10 9 7 10 23 21 24 16 14 17 13 11 14 14 12 17 15 13 18 19 17 22 13 11 16 17 16 21 17 16 21 16 15 20 15 14 19 14 13 19 14 13 19 15 14 20 16 15 21 20 19 25 12 11 17 25 25 33 23 23 31 24 24 32 21 21 29 153 152 160 193 192 198 231 226 232 255 250 254 253 247 249 255 253 255 255 253 255 255 252 255 252 250 255 253 246 253 222 205 215 202 182 193 72 56 66 33 23 31 29 26 33 22 23 28 16 19 24 22 27 31 24 29 33 22 26 29 22 23 27 23 23 25 22 20 23 19 17 18 19 17 18 22 20 21 24 24 26 15 16 18 25 24 29 20 21 25 17 20 25 22 25 30 20 19 27 19 18 26 22 22 30 17 16 24 27 15 27 17 7 16 22 22 30 11 14 19 22 17 24 23 16 24 24 17 25 17 11 21 21 18 29 15 13 24 16 21 27 18 23 29 15 20 26 16 19 26 17 20 27 14 17 24 14 17 26 16 15 23 19 14 21 19 14 20 13 10 17 14 13 19 15 15 23 14 15 20 16 14 19 15 13 18 15 12 19 14 12 23 13 12 26 11 13 25 16 16 26 11 11 21 13 12 26 14 16 39 70 79 118 128 148 199 123 154 218 113 150 218 210 159 128 202 157 128 192 154 133 181 152 138 55 34 29 19 7 7 20 14 18 15 10 16 16 14 19 16 14 17 17 12 16 16 12 13 14 12 15 13 13 15 13 12 18 13 14 18 22 22 22 10 9 7 13 12 10 27 26 24 12 11 9 12 11 9 32 31 29 13 12 10 18 17 15 17 16 14 19 15 16 25 21 22 13 9 10 18 14 15 19 15 16 19 15 16 14 9 13 9 7 10 13 11 14 15 13 16 23 21 26 24 22 27 5 3 8 25 23 28 16 15 20 16 15 20 16 15 20 14 13 18 13 12 18 13 12 18 14 13 19 15 14 20 15 13 18 20 19 25 18 17 25 17 17 25 21 24 33 30 33 42 127 127 135 196 195 201 225 220 226 255 253 255 255 253 255 255 251 253 255 252 255 255 253 255 244 242 247 219 212 220 168 152 163 125 107 119 43 30 40 24 17 25 24 21 28 25 26 31 16 21 27 20 25 29 23 26 31 23 24 28 22 21 26 24 22 25 23 21 24 20 18 19 20 18 19 22 20 21 24 22 27 19 16 23 24 21 28 30 27 34 25 26 31 22 23 28 28 25 32 29 24 31 26 18 29 28 15 25 29 9 21 27 7 19 14 4 15 21 11 22 29 13 24 31 13 25 37 19 31 23 9 22 30 27 36 8 11 18 8 17 22 7 16 21 15 18 25 19 18 26 17 17 25 13 13 21 13 13 21 15 14 22 21 14 22 22 15 22 16 14 19 15 16 21 12 15 22 14 15 20 15 13 18 17 12 18 14 11 18 14 13 21 11 13 25 10 14 26 13 15 27 11 13 25 15 13 27 13 14 35 58 66 102 131 149 197 123 153 217 115 152 222 212 163 133 199 153 127 189 151 132 179 151 139 39 20 14 18 6 6 20 14 18 16 14 19 15 13 18 14 12 15 16 11 15 16 12 13 14 12 15 13 13 15 13 12 18 13 12 17 9 7 8 17 13 12 134 130 129 141 137 136 143 139 140 125 121 122 114 110 111 95 91 92 87 83 84 75 71 72 57 51 53 45 39 41 24 18 22 22 16 20 17 11 15 14 8 12 23 17 21 19 14 18 22 17 21 18 13 17 11 6 12 24 19 25 10 8 13 15 13 18 16 14 19 16 14 19 15 14 19 14 13 18 13 12 18 12 11 17 13 12 18 14 13 19 13 10 17 25 24 30 11 10 18 16 16 24 17 20 29 27 30 39 76 76 84 184 183 189 220 215 221 246 240 244 255 246 249 255 252 255 255 252 255 251 245 249 214 207 214 153 148 155 116 111 118 51 45 55 23 20 29 23 22 30 22 22 30 27 30 37 19 22 31 22 22 30 24 23 31 24 21 30 26 20 30 26 21 28 26 21 27 22 20 23 21 19 22 22 20 25 23 17 27 31 23 36 26 15 29 27 16 30 23 17 27 23 18 25 23 18 25 23 10 19 35 7 22 46 9 27 44 10 27 46 13 32 34 0 21 44 9 31 43 8 32 41 6 28 35 0 20 38 11 28 20 17 24 12 22 24 13 28 33 15 24 31 17 11 21 22 10 22 25 14 28 21 13 24 17 14 21 18 15 22 20 12 23 16 8 19 11 10 15 9 13 16 10 13 18 12 13 18 14 12 17 16 11 17 14 11 18 14 13 21 11 13 25 10 14 26 13 15 27 14 16 28 16 14 28 13 14 35 41 49 85 134 152 200 122 152 214 120 155 223 211 162 132 196 152 125 188 151 132 174 146 134 29 10 4 19 9 8 19 14 18 16 14 19 13 11 16 14 12 15 16 12 13 17 13 14 15 13 16 13 13 15 13 12 18 13 12 17 32 28 29 13 7 7 140 131 132 193 184 185 255 252 255 255 252 255 251 242 245 150 141 144 153 144 147 160 151 154 167 158 161 186 175 179 190 179 185 193 182 188 177 166 172 163 154 159 140 131 136 132 126 130 104 98 102 81 76 80 18 13 19 6 1 7 28 23 29 18 16 21 16 14 19 16 14 19 15 14 19 14 13 18 13 12 18 13 12 18 13 12 18 14 13 19 15 12 19 20 19 25 11 11 19 22 22 30 17 20 29 19 19 29 31 30 38 153 150 157 198 191 198 241 232 237 255 251 255 255 247 251 248 237 243 224 215 220 163 153 161 105 100 106 84 84 92 22 25 34 22 25 34 24 27 36 22 24 36 24 24 34 23 21 34 22 20 31 23 19 33 25 19 31 26 20 32 27 21 31 25 22 29 22 21 27 20 21 26 21 20 28 18 11 27 32 23 42 31 18 36 30 17 34 29 21 32 26 19 27 30 24 28 91 68 76 97 38 58 115 39 65 117 43 68 116 43 70 117 42 72 116 41 71 115 40 71 117 42 75 120 46 81 147 94 120 26 17 22 7 22 19 5 19 22 13 20 28 16 6 15 30 14 25 21 8 18 20 10 19 16 11 15 19 17 22 18 15 26 12 10 21 14 15 17 14 18 19 9 12 17 11 12 17 14 12 17 16 11 15 14 12 17 14 13 21 11 13 25 11 15 26 12 14 26 15 17 29 16 14 27 11 12 32 25 34 67 133 150 196 121 150 210 119 154 220 206 159 131 195 151 126 190 153 135 159 133 120 26 9 2 21 11 10 17 12 16 14 12 17 13 11 14 14 12 15 16 12 13 17 13 14 15 13 16 13 13 15 13 12 18 13 12 17 14 8 12 38 29 32 150 139 143 204 193 197 255 246 252 255 249 255 217 206 212 130 119 125 154 143 149 159 148 154 158 147 153 169 156 163 175 162 171 190 177 186 191 178 187 194 181 190 186 175 181 185 176 181 175 166 171 175 169 173 101 94 101 21 16 22 9 4 10 12 7 13 16 14 19 16 14 19 16 15 20 15 14 19 14 13 19 14 13 19 14 15 20 14 15 20 16 15 23 13 12 20 17 17 27 24 24 34 23 23 35 22 22 32 19 18 26 103 98 105 187 177 185 226 215 221 255 240 245 246 231 234 223 208 213 162 150 154 115 102 109 126 119 126 46 49 58 14 21 31 18 25 35 19 23 34 24 23 37 16 14 27 23 19 34 25 19 33 24 17 33 25 19 33 26 20 34 25 22 33 24 22 33 20 23 30 18 23 29 17 21 30 30 29 45 20 15 35 19 10 29 24 16 31 34 26 37 20 14 18 24 15 16 166 129 136 150 52 75 177 46 78 176 38 72 174 39 72 174 49 81 173 48 80 180 41 80 190 53 97 172 45 98 233 147 186 44 24 26 10 23 13 14 21 27 19 19 29 24 15 20 26 14 18 26 17 20 23 17 17 17 9 6 18 17 15 10 17 27 0 10 20 8 14 14 8 12 11 9 13 16 12 13 18 14 12 17 17 12 16 15 13 18 15 14 20 12 15 24 11 15 26 10 13 22 13 16 25 14 12 23 9 10 28 16 23 52 120 137 180 119 148 206 116 150 214 204 157 131 194 152 128 191 156 137 135 108 97 28 10 6 22 14 12 16 11 15 14 12 17 15 13 16 14 12 15 16 12 13 16 12 13 14 12 15 13 13 15 13 12 17 13 12 17 13 4 9 50 39 43 176 165 169 194 183 187 222 209 216 235 222 229 211 198 205 170 157 164 152 139 146 174 161 168 184 171 178 192 179 186 190 174 184 193 177 187 189 173 183 194 178 188 191 178 185 181 170 176 177 166 172 181 172 177 160 150 158 83 76 83 23 16 23 17 12 18 17 12 18 16 14 19 17 15 20 16 15 20 15 14 20 15 14 20 15 16 21 16 17 22 19 18 26 13 12 20 19 19 29 17 17 27 21 21 33 28 26 37 23 20 31 46 41 48 185 175 183 202 190 194 235 218 224 230 214 217 180 163 169 140 125 130 150 135 142 143 133 141 19 22 31 15 21 33 19 23 35 20 19 33 26 24 38 17 13 28 26 17 34 26 17 34 25 18 34 26 20 34 25 21 35 22 22 32 20 23 32 19 24 30 16 23 29 15 22 30 14 18 29 21 20 34 37 26 40 25 13 25 24 17 25 28 19 24 32 16 19 184 138 149 150 54 81 170 46 80 178 44 81 178 44 81 176 48 83 177 47 85 185 44 86 184 47 93 177 56 110 229 147 185 57 36 33 20 22 9 25 14 22 26 2 16 50 9 17 44 0 4 34 0 1 33 9 5 25 11 0 27 24 15 16 20 31 2 8 22 14 15 17 15 15 13 12 13 17 12 13 17 15 13 16 18 13 17 16 14 19 16 15 21 13 16 25 12 16 25 13 16 25 16 16 24 18 17 25 12 14 29 13 20 48 104 119 160 123 149 206 118 152 215 202 156 132 197 155 133 191 155 139 105 81 69 28 13 8 22 13 14 15 10 14 14 13 18 16 14 17 16 14 17 17 13 14 16 12 11 14 12 13 13 13 15 13 12 17 14 13 18 28 19 24 66 55 61 179 168 174 219 208 214 214 203 209 201 190 196 231 218 227 158 145 154 148 135 144 179 166 175 197 184 193 209 196 205 208 195 204 209 196 205 203 187 198 209 193 203 206 193 200 199 188 194 195 184 190 187 178 183 197 187 195 156 149 156 86 79 86 11 6 12 16 11 17 16 14 19 17 15 20 17 15 20 16 15 21 16 15 21 16 17 22 17 18 23 19 18 26 17 16 24 17 17 27 17 17 27 19 19 31 23 21 32 26 23 34 21 14 22 141 130 138 207 195 199 229 212 218 197 178 182 148 129 135 167 150 156 205 188 196 86 75 83 20 17 28 21 19 32 25 21 35 23 17 31 28 19 36 26 17 34 26 17 34 25 18 34 24 20 34 25 23 36 24 24 36 20 23 32 18 22 31 17 24 30 16 23 29 14 22 24 18 28 27 19 19 19 31 12 18 43 20 28 100 89 95 167 151 161 127 93 109 142 94 116 133 79 105 119 63 92 158 99 131 140 75 109 152 74 114 143 57 102 172 85 130 130 53 97 162 107 147 162 128 152 106 87 80 142 122 111 132 90 102 138 69 90 148 45 62 145 35 46 137 46 55 145 81 79 129 104 82 112 105 89 71 58 75 23 9 32 21 8 15 22 12 13 14 13 18 14 15 19 17 15 18 19 14 18 18 16 21 16 17 22 12 16 25 12 16 25 15 18 27 16 16 24 23 22 30 16 15 29 12 19 45 82 97 136 125 152 205 124 156 217 202 156 133 198 157 135 189 156 139 86 62 52 28 13 10 19 13 13 13 11 14 16 15 20 17 15 18 16 14 15 17 13 14 16 12 11 14 12 13 14 12 15 13 12 17 14 13 18 19 13 17 82 73 78 135 126 131 180 171 176 232 221 227 225 214 220 156 145 153 129 118 126 142 131 139 161 150 158 163 152 160 168 157 165 171 160 168 181 168 177 179 166 176 188 175 184 187 174 183 183 170 177 179 168 174 175 164 170 178 168 176 171 161 169 157 150 157 38 31 38 16 11 17 17 12 18 16 14 19 17 15 20 16 15 21 16 15 21 16 17 22 17 18 23 15 14 22 18 17 25 13 13 23 25 25 35 22 22 34 16 14 25 29 26 37 29 22 30 80 69 77 206 191 196 199 182 188 168 149 153 151 132 138 152 133 139 167 150 158 25 12 21 26 18 29 17 11 23 24 16 31 20 12 27 21 13 28 29 21 36 23 17 31 21 17 31 24 22 35 25 25 35 22 25 34 20 23 30 19 22 29 22 23 28 22 21 27 21 19 20 21 17 8 39 20 14 72 30 34 114 67 77 190 154 166 255 235 253 214 155 183 144 87 119 163 124 153 117 91 118 184 164 191 130 104 133 160 114 151 134 79 118 198 146 184 123 83 118 192 177 208 114 106 121 125 102 94 225 182 173 184 113 127 199 95 118 186 43 61 204 50 62 175 39 49 207 105 103 204 154 129 177 151 134 103 76 93 27 3 29 19 2 10 25 15 16 16 14 19 15 16 20 17 15 18 18 16 19 18 16 21 16 17 22 13 17 26 10 17 25 13 16 23 14 15 20 23 22 28 14 14 26 8 16 39 63 79 115 124 150 201 125 157 214 206 161 140 191 153 132 190 158 143 56 34 23 25 11 8 22 16 16 14 12 15 11 12 16 12 12 14 14 12 13 16 12 13 16 12 11 15 11 12 13 11 14 13 12 17 14 13 18 20 15 19 57 51 55 98 91 98 117 110 117 117 107 115 117 107 115 131 121 129 148 138 146 149 139 147 154 144 152 160 150 159 165 155 164 169 159 168 174 162 172 180 168 178 184 173 181 175 162 171 178 165 172 171 160 166 171 160 166 169 159 167 164 154 162 159 152 159 37 30 37 15 10 16 16 11 17 15 13 18 16 14 19 15 14 20 14 13 19 12 13 18 11 12 17 17 16 22 17 16 24 16 16 26 16 16 26 17 17 29 19 17 28 20 17 28 24 17 25 23 12 20 190 175 180 186 169 175 127 108 112 91 72 78 156 137 143 53 36 44 23 10 19 26 16 27 22 14 25 19 11 24 20 12 25 21 15 27 22 19 30 22 20 31 21 21 31 25 28 37 18 23 29 16 19 26 22 21 27 27 22 29 28 17 25 31 15 25 41 16 20 52 11 7 89 30 26 135 52 62 164 75 95 239 160 182 255 227 255 231 146 185 153 78 117 168 118 153 120 87 116 196 173 199 138 116 139 160 132 155 139 106 127 193 154 175 125 97 119 182 182 206 120 118 132 129 96 89 223 158 152 206 112 126 214 87 108 209 44 58 216 39 47 205 35 44 216 76 75 212 125 105 199 148 131 110 71 89 32 8 32 21 10 18 18 16 17 16 15 20 17 16 21 19 17 20 18 16 19 18 16 21 15 15 23 13 17 26 12 19 27 15 18 25 17 18 23 21 20 26 13 13 25 14 20 42 45 61 95 123 150 197 126 157 212 202 160 138 192 154 135 180 148 135 48 25 17 20 9 5 21 15 15 12 12 14 13 14 18 14 14 16 14 14 14 18 14 13 17 13 12 17 13 14 15 13 14 16 14 19 16 15 20 11 9 12 17 12 16 19 14 20 15 10 16 10 5 11 13 8 14 24 19 25 35 30 36 42 37 43 50 45 51 62 55 63 75 68 76 89 82 90 102 95 103 115 108 116 125 115 123 141 130 136 151 140 146 154 143 149 156 147 152 159 149 157 158 151 158 144 137 144 33 28 34 15 10 16 14 12 17 14 12 17 14 12 17 14 13 19 14 13 19 13 14 19 13 14 19 17 16 22 17 16 22 16 16 24 16 16 26 16 16 28 17 15 26 19 16 25 22 15 23 30 19 27 99 86 93 154 137 143 81 65 68 70 53 59 106 89 97 23 7 17 27 15 25 23 18 25 20 17 24 19 16 25 18 15 24 18 17 25 19 19 27 17 20 27 17 22 28 14 21 27 15 20 24 20 21 26 27 22 28 31 15 25 32 9 19 39 9 21 56 11 16 99 33 21 146 64 50 167 76 73 170 66 75 168 52 75 197 79 113 159 50 91 148 63 102 149 100 130 171 143 168 149 132 151 128 114 131 135 119 132 164 142 155 129 95 109 130 103 118 135 133 155 113 105 120 121 73 71 160 74 73 170 47 68 201 47 71 207 31 42 205 19 24 208 24 32 192 34 33 166 55 38 171 101 89 65 24 40 19 2 21 14 9 15 16 18 17 14 15 19 17 16 22 19 17 22 20 18 23 19 16 23 16 16 24 12 16 27 10 17 25 14 19 25 16 17 22 20 19 24 17 17 27 13 19 41 32 47 80 128 152 200 127 158 212 198 157 137 192 155 137 161 131 120 36 16 9 18 7 5 19 15 16 11 11 13 14 15 19 12 12 14 13 13 13 16 12 11 16 12 11 15 11 12 14 12 13 15 13 18 15 14 19 15 15 17 15 13 16 14 12 15 15 13 16 16 14 17 16 14 17 15 13 18 14 12 17 17 15 20 17 15 20 18 13 19 16 11 17 14 9 15 13 8 14 15 10 16 18 11 18 22 13 18 22 11 17 27 16 22 38 29 34 42 32 40 49 42 49 58 51 58 18 13 19 15 10 16 13 11 16 13 11 16 12 11 16 13 12 18 14 13 19 15 16 21 16 17 22 17 16 21 17 16 22 15 15 23 15 15 23 15 15 25 16 14 25 17 14 23 19 14 21 25 15 23 34 23 29 90 78 82 69 54 59 72 57 64 42 29 36 19 8 16 21 14 22 15 16 21 12 17 20 14 17 22 15 18 23 16 19 24 16 21 25 16 24 27 17 25 28 16 26 28 18 23 26 22 21 26 28 17 23 34 11 21 48 12 24 72 25 41 100 40 42 182 105 77 237 155 115 232 159 124 223 130 112 179 33 46 186 27 57 168 39 70 196 107 135 187 150 167 195 185 196 79 71 84 151 143 154 188 176 186 243 221 233 92 54 67 115 78 96 182 164 188 169 141 163 161 94 103 164 54 65 194 36 69 209 28 61 208 26 41 207 26 33 207 30 38 189 30 34 170 46 38 192 112 105 52 22 30 17 15 28 14 12 17 20 20 22 14 15 20 16 15 21 19 17 22 20 18 23 19 16 23 16 16 24 10 17 27 10 17 27 14 18 27 15 16 21 18 17 22 22 22 32 12 17 37 15 30 61 132 156 202 127 157 209 195 154 136 191 156 137 141 113 101 27 8 1 17 7 5 19 15 16 10 10 12 15 16 18 11 11 13 12 12 10 15 11 10 15 11 8 15 11 12 13 11 12 14 12 17 14 13 18 11 11 13 12 12 14 13 13 15 15 15 17 15 15 17 15 15 17 15 14 19 15 14 19 15 13 18 16 14 19 16 14 19 15 13 18 14 12 17 13 11 16 15 13 18 18 13 19 20 11 16 14 5 10 19 10 15 21 15 19 12 5 12 11 6 12 15 10 16 15 10 16 13 11 16 13 11 16 12 11 16 12 11 16 13 12 18 14 13 19 14 15 20 16 15 21 18 16 21 17 15 20 16 15 21 14 14 22 14 14 24 14 14 24 15 14 22 16 13 20 17 10 17 21 12 17 38 27 31 71 60 64 64 53 59 15 5 13 28 18 27 16 13 20 8 16 19 8 18 19 11 19 21 13 18 21 14 17 22 15 20 24 16 24 27 19 27 30 21 29 31 20 25 28 22 20 25 27 14 21 39 13 24 60 22 35 89 41 57 118 59 61 191 124 95 232 168 124 209 160 119 205 133 109 173 42 50 177 33 58 157 45 69 165 92 112 180 152 164 194 191 198 98 98 106 149 148 154 160 153 161 221 204 212 112 80 91 132 100 115 178 159 181 161 129 150 151 88 99 157 59 72 188 51 81 185 35 64 175 34 43 178 42 44 185 46 51 175 47 48 169 61 58 181 110 106 53 32 37 14 17 24 7 8 12 10 11 15 14 15 20 17 16 24 19 16 23 19 16 23 19 16 25 16 16 26 10 16 28 10 17 27 14 18 27 17 18 23 18 17 23 22 22 32 11 16 36 6 19 51 136 158 205 125 155 207 191 152 135 188 152 136 122 94 83 25 6 0 20 10 8 19 15 16 9 10 12 14 15 17 13 13 15 14 14 12 17 13 12 17 13 10 16 12 13 14 12 13 15 13 18 15 14 19 15 16 18 16 18 17 16 16 18 13 13 15 10 10 12 9 9 11 11 11 13 13 13 15 12 12 14 13 13 15 15 13 18 15 13 18 14 12 17 13 11 16 13 11 16 15 10 16 16 10 14 18 12 16 24 18 22 15 10 14 15 10 16 28 23 29 19 14 20 10 8 13 13 11 16 13 11 16 13 12 17 13 12 17 13 12 18 13 12 18 13 12 18 13 12 18 17 15 20 17 15 20 15 14 20 14 15 20 13 13 21 13 13 21 13 13 21 14 13 19 20 15 21 17 11 15 22 16 20 44 35 40 30 23 30 21 16 22 15 10 17 19 20 25 10 20 22 9 20 22 11 19 22 13 16 21 15 16 21 16 17 22 17 20 25 20 23 28 18 23 27 17 21 24 19 18 23 26 17 22 38 17 26 52 20 31 68 26 40 76 34 38 95 59 45 108 77 56 88 61 42 90 52 43 87 16 24 93 16 34 80 21 39 71 32 50 71 51 63 65 62 71 24 29 35 33 41 44 38 35 42 75 66 71 43 31 35 47 34 41 56 45 59 50 32 46 54 14 23 61 8 16 63 6 15 66 9 15 59 8 4 57 4 0 70 7 2 64 0 0 63 5 3 66 25 23 24 9 12 8 9 13 14 18 21 17 22 25 14 17 24 17 16 24 19 16 23 18 15 22 17 14 23 15 15 25 11 17 29 12 18 30 13 17 26 16 19 24 20 17 24 20 18 29 13 16 35 5 19 48 128 151 195 125 153 203 192 153 136 183 147 133 103 76 65 27 8 2 22 12 10 19 15 16 10 11 13 11 15 16 12 12 14 13 13 11 16 12 11 16 12 9 15 11 10 15 11 12 14 12 15 14 14 16 14 14 16 13 15 14 15 15 17 17 17 19 19 19 21 18 18 20 16 16 18 14 14 16 15 15 17 16 16 18 18 16 21 19 17 22 19 17 22 19 17 22 18 16 21 17 15 20 22 17 21 19 14 18 25 20 24 12 7 11 12 7 13 18 13 19 12 10 15 13 11 16 14 12 17 14 12 17 13 12 17 13 12 17 13 12 18 13 12 18 12 11 17 12 11 16 18 13 17 16 14 17 16 14 19 14 15 20 14 14 22 12 15 22 14 14 22 15 16 21 18 16 21 19 14 18 23 18 22 19 14 18 13 8 14 24 21 28 11 10 18 13 16 23 8 17 22 8 18 20 12 17 21 15 16 21 19 16 23 23 18 25 24 21 30 25 24 30 18 21 26 15 20 24 15 18 23 19 18 23 29 18 26 36 16 25 39 11 23 38 11 18 33 17 17 31 20 16 29 15 14 39 20 22 42 15 24 43 13 25 43 17 30 42 20 33 35 21 34 26 20 30 21 26 32 26 31 37 32 27 34 28 21 29 23 22 27 27 26 31 26 23 30 30 18 28 39 16 24 49 24 30 27 12 15 28 20 18 26 21 15 26 17 12 40 19 18 38 9 11 37 8 10 22 0 3 24 13 17 11 9 12 15 19 20 12 17 20 15 18 25 18 16 27 18 15 24 17 14 23 16 13 24 15 15 27 11 17 29 12 20 31 13 17 26 16 19 26 21 18 25 16 14 25 14 17 36 7 18 48 106 129 173 126 154 204 194 157 139 176 143 128 89 62 51 29 10 4 23 13 12 15 13 14 12 13 15 11 15 16 11 11 13 11 11 9 15 11 10 14 10 7 14 10 9 14 10 11 13 11 14 13 13 15 19 19 21 14 14 14 9 9 9 7 7 7 10 10 10 13 13 13 16 14 17 16 14 17 15 13 16 15 13 16 15 13 16 15 13 16 15 13 16 14 12 15 13 11 14 12 10 13 11 9 12 4 2 5 20 18 21 18 16 19 21 19 24 8 6 11 12 10 15 19 17 22 14 13 18 13 12 17 13 12 17 12 11 16 12 11 17 12 11 17 12 11 17 13 12 17 17 12 16 15 13 16 15 13 18 14 15 20 15 15 23 13 16 23 14 17 22 17 18 23 15 14 19 21 19 24 17 15 18 19 17 20 14 13 18 15 16 21 18 21 28 8 13 19 9 16 22 11 16 22 17 16 24 23 15 26 29 17 29 30 18 30 30 19 33 27 21 31 20 20 28 14 22 25 12 22 24 12 20 22 19 20 24 22 20 23 28 19 24 28 19 24 28 25 32 24 21 30 34 24 35 38 28 37 27 26 32 24 21 28 30 14 24 35 18 28 33 23 32 34 31 38 27 26 34 21 18 27 30 19 33 38 30 43 28 31 40 12 17 23 21 22 26 24 19 25 30 17 27 38 25 35 25 18 26 20 17 24 20 19 27 27 24 33 24 12 24 29 15 28 34 20 33 24 11 21 26 15 21 16 10 12 13 13 13 11 12 16 16 16 24 17 15 26 18 15 24 18 15 24 17 14 25 15 15 27 11 17 29 11 19 30 14 18 29 13 16 23 21 18 25 14 12 23 15 18 37 5 16 46 78 99 142 131 157 206 197 160 142 173 140 125 81 54 43 29 12 5 23 13 12 13 11 12 13 14 18 11 15 16 12 12 14 13 13 11 16 12 11 16 12 9 15 11 10 16 12 13 15 13 16 16 14 17 13 13 15 14 14 14 15 15 15 15 15 15 15 15 15 16 16 16 17 15 18 17 15 18 17 15 18 16 14 17 16 14 17 16 14 17 16 14 17 16 14 17 16 14 17 16 14 17 19 17 20 10 8 11 19 17 20 8 6 9 19 17 22 14 12 17 22 20 25 13 11 16 15 14 19 13 12 17 12 11 16 11 10 15 11 10 16 12 11 17 13 12 18 14 13 18 17 12 16 15 13 14 15 13 18 14 15 19 15 15 23 14 17 24 15 18 23 16 19 24 22 21 26 11 10 15 14 12 15 21 21 23 9 8 14 13 16 21 16 19 28 15 19 28 17 22 28 18 21 28 24 18 28 29 19 30 34 17 33 32 15 31 27 13 28 20 12 25 14 14 22 12 19 25 13 24 26 13 24 26 13 23 24 17 22 25 23 23 25 25 24 30 21 23 35 20 19 35 28 19 36 25 21 35 8 21 27 21 32 34 31 19 29 35 18 28 25 18 25 24 22 27 26 20 30 39 31 42 34 20 37 26 15 31 16 20 32 19 28 35 21 22 26 29 27 30 29 19 30 20 9 23 22 19 30 23 21 34 18 17 33 17 14 33 20 16 33 22 15 33 24 15 32 23 15 28 16 5 13 23 14 17 17 15 16 20 20 22 15 15 25 17 15 26 19 16 27 19 16 25 18 14 28 16 16 28 11 17 31 10 18 29 15 19 30 9 12 19 21 18 25 14 12 23 16 19 38 2 13 43 56 77 120 134 160 209 \ No newline at end of file diff --git a/Tests/images/hopper_bigtiff.tif b/Tests/images/hopper_bigtiff.tif new file mode 100644 index 00000000000..9588a37d80b Binary files /dev/null and b/Tests/images/hopper_bigtiff.tif differ diff --git a/Tests/images/hopper_draw.ico b/Tests/images/hopper_draw.ico new file mode 100644 index 00000000000..01471189693 Binary files /dev/null and b/Tests/images/hopper_draw.ico differ diff --git a/Tests/images/hopper_float_dpi_2.tif b/Tests/images/hopper_float_dpi_2.tif new file mode 100644 index 00000000000..e38541c5d84 Binary files /dev/null and b/Tests/images/hopper_float_dpi_2.tif differ diff --git a/Tests/images/hopper_float_dpi_3.tif b/Tests/images/hopper_float_dpi_3.tif new file mode 100644 index 00000000000..af6c96bd4e0 Binary files /dev/null and b/Tests/images/hopper_float_dpi_3.tif differ diff --git a/Tests/images/hopper_float_dpi_None.tif b/Tests/images/hopper_float_dpi_None.tif new file mode 100644 index 00000000000..b9863510829 Binary files /dev/null and b/Tests/images/hopper_float_dpi_None.tif differ diff --git a/Tests/images/hopper_idat_after_image_end.png b/Tests/images/hopper_idat_after_image_end.png new file mode 100644 index 00000000000..70b4a64002e Binary files /dev/null and b/Tests/images/hopper_idat_after_image_end.png differ diff --git a/Tests/images/hopper_long_name.im b/Tests/images/hopper_long_name.im new file mode 100644 index 00000000000..ff45b7c7539 Binary files /dev/null and b/Tests/images/hopper_long_name.im differ diff --git a/Tests/images/hopper_lzma.tif b/Tests/images/hopper_lzma.tif new file mode 100644 index 00000000000..d7ca089fc40 Binary files /dev/null and b/Tests/images/hopper_lzma.tif differ diff --git a/Tests/images/hopper_mask.ico b/Tests/images/hopper_mask.ico new file mode 100644 index 00000000000..e8d66c689fd Binary files /dev/null and b/Tests/images/hopper_mask.ico differ diff --git a/Tests/images/hopper_mask.png b/Tests/images/hopper_mask.png new file mode 100644 index 00000000000..c7bd2f70842 Binary files /dev/null and b/Tests/images/hopper_mask.png differ diff --git a/Tests/images/hopper_naxis_zero.fits b/Tests/images/hopper_naxis_zero.fits new file mode 100644 index 00000000000..580cf3a2c00 Binary files /dev/null and b/Tests/images/hopper_naxis_zero.fits differ diff --git a/Tests/images/hopper_orientation_2.jpg b/Tests/images/hopper_orientation_2.jpg new file mode 100644 index 00000000000..02b4f392e6f Binary files /dev/null and b/Tests/images/hopper_orientation_2.jpg differ diff --git a/Tests/images/hopper_orientation_2.webp b/Tests/images/hopper_orientation_2.webp new file mode 100644 index 00000000000..43381d2ba47 Binary files /dev/null and b/Tests/images/hopper_orientation_2.webp differ diff --git a/Tests/images/hopper_orientation_3.jpg b/Tests/images/hopper_orientation_3.jpg new file mode 100644 index 00000000000..01717d980da Binary files /dev/null and b/Tests/images/hopper_orientation_3.jpg differ diff --git a/Tests/images/hopper_orientation_3.webp b/Tests/images/hopper_orientation_3.webp new file mode 100644 index 00000000000..9537ff68e04 Binary files /dev/null and b/Tests/images/hopper_orientation_3.webp differ diff --git a/Tests/images/hopper_orientation_4.jpg b/Tests/images/hopper_orientation_4.jpg new file mode 100644 index 00000000000..3e0bb4e1a7e Binary files /dev/null and b/Tests/images/hopper_orientation_4.jpg differ diff --git a/Tests/images/hopper_orientation_4.webp b/Tests/images/hopper_orientation_4.webp new file mode 100644 index 00000000000..ca7b8cd302c Binary files /dev/null and b/Tests/images/hopper_orientation_4.webp differ diff --git a/Tests/images/hopper_orientation_5.jpg b/Tests/images/hopper_orientation_5.jpg new file mode 100644 index 00000000000..fd32afc27fa Binary files /dev/null and b/Tests/images/hopper_orientation_5.jpg differ diff --git a/Tests/images/hopper_orientation_5.webp b/Tests/images/hopper_orientation_5.webp new file mode 100644 index 00000000000..a3164a90d2d Binary files /dev/null and b/Tests/images/hopper_orientation_5.webp differ diff --git a/Tests/images/hopper_orientation_6.jpg b/Tests/images/hopper_orientation_6.jpg new file mode 100644 index 00000000000..22a09619827 Binary files /dev/null and b/Tests/images/hopper_orientation_6.jpg differ diff --git a/Tests/images/hopper_orientation_6.webp b/Tests/images/hopper_orientation_6.webp new file mode 100644 index 00000000000..3e24c5bcb5a Binary files /dev/null and b/Tests/images/hopper_orientation_6.webp differ diff --git a/Tests/images/hopper_orientation_7.jpg b/Tests/images/hopper_orientation_7.jpg new file mode 100644 index 00000000000..a7c45146a81 Binary files /dev/null and b/Tests/images/hopper_orientation_7.jpg differ diff --git a/Tests/images/hopper_orientation_7.webp b/Tests/images/hopper_orientation_7.webp new file mode 100644 index 00000000000..f78163aedc2 Binary files /dev/null and b/Tests/images/hopper_orientation_7.webp differ diff --git a/Tests/images/hopper_orientation_8.jpg b/Tests/images/hopper_orientation_8.jpg new file mode 100644 index 00000000000..e6b8c2c1c07 Binary files /dev/null and b/Tests/images/hopper_orientation_8.jpg differ diff --git a/Tests/images/hopper_orientation_8.webp b/Tests/images/hopper_orientation_8.webp new file mode 100644 index 00000000000..3cce80a47ec Binary files /dev/null and b/Tests/images/hopper_orientation_8.webp differ diff --git a/Tests/images/hopper_palette_chunk_second.fli b/Tests/images/hopper_palette_chunk_second.fli new file mode 100644 index 00000000000..54447de0af7 Binary files /dev/null and b/Tests/images/hopper_palette_chunk_second.fli differ diff --git a/Tests/images/hopper_resized.gif b/Tests/images/hopper_resized.gif new file mode 100644 index 00000000000..f7be6c26298 Binary files /dev/null and b/Tests/images/hopper_resized.gif differ diff --git a/Tests/images/hopper_rle8.bmp b/Tests/images/hopper_rle8.bmp new file mode 100644 index 00000000000..0fff4a0d43d Binary files /dev/null and b/Tests/images/hopper_rle8.bmp differ diff --git a/Tests/images/hopper_rle8_greyscale.bmp b/Tests/images/hopper_rle8_greyscale.bmp new file mode 100644 index 00000000000..ead32ff95a4 Binary files /dev/null and b/Tests/images/hopper_rle8_greyscale.bmp differ diff --git a/Tests/images/hopper_rle8_row_overflow.bmp b/Tests/images/hopper_rle8_row_overflow.bmp new file mode 100644 index 00000000000..d606dc3e41a Binary files /dev/null and b/Tests/images/hopper_rle8_row_overflow.bmp differ diff --git a/Tests/images/hopper_unexpected.ico b/Tests/images/hopper_unexpected.ico new file mode 100644 index 00000000000..639828ae045 Binary files /dev/null and b/Tests/images/hopper_unexpected.ico differ diff --git a/Tests/images/hopper_unknown_pixel_mode.tif b/Tests/images/hopper_unknown_pixel_mode.tif new file mode 100644 index 00000000000..89a8c5e1717 Binary files /dev/null and b/Tests/images/hopper_unknown_pixel_mode.tif differ diff --git a/Tests/images/hopper_wal.png b/Tests/images/hopper_wal.png new file mode 100644 index 00000000000..b6067c219c4 Binary files /dev/null and b/Tests/images/hopper_wal.png differ diff --git a/Tests/images/hopper_webp.png b/Tests/images/hopper_webp.png new file mode 100644 index 00000000000..94b927ac24a 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 00000000000..5e398606c34 Binary files /dev/null and b/Tests/images/hopper_webp.tif differ diff --git a/Tests/images/hopper_webp_bits.ppm b/Tests/images/hopper_webp_bits.ppm index 6dce2da2eb9..f431bc7b1fc 100644 Binary files a/Tests/images/hopper_webp_bits.ppm and b/Tests/images/hopper_webp_bits.ppm differ diff --git a/Tests/images/hopper_zero_comment_subblocks.gif b/Tests/images/hopper_zero_comment_subblocks.gif new file mode 100644 index 00000000000..5f482c042d3 Binary files /dev/null and b/Tests/images/hopper_zero_comment_subblocks.gif differ diff --git a/Tests/images/i_trns.png b/Tests/images/i_trns.png new file mode 100644 index 00000000000..ef63d33b0d2 Binary files /dev/null and b/Tests/images/i_trns.png differ diff --git a/Tests/images/icc-after-SOF.jpg b/Tests/images/icc-after-SOF.jpg new file mode 100644 index 00000000000..a284a2298dc Binary files /dev/null and b/Tests/images/icc-after-SOF.jpg differ diff --git a/Tests/images/ifd_tag_type.tiff b/Tests/images/ifd_tag_type.tiff new file mode 100644 index 00000000000..316d2089e35 Binary files /dev/null and b/Tests/images/ifd_tag_type.tiff differ diff --git a/Tests/images/ignore_frame_size.mpo b/Tests/images/ignore_frame_size.mpo new file mode 100644 index 00000000000..c4d60707a47 Binary files /dev/null and b/Tests/images/ignore_frame_size.mpo differ diff --git a/Tests/images/imagedraw/continuous_horizontal_edges_polygon.png b/Tests/images/imagedraw/continuous_horizontal_edges_polygon.png new file mode 100644 index 00000000000..beffed5b918 Binary files /dev/null and b/Tests/images/imagedraw/continuous_horizontal_edges_polygon.png differ diff --git a/Tests/images/imagedraw/discontiguous_corners_polygon.png b/Tests/images/imagedraw/discontiguous_corners_polygon.png new file mode 100644 index 00000000000..509c42b26e0 Binary files /dev/null and b/Tests/images/imagedraw/discontiguous_corners_polygon.png differ diff --git a/Tests/images/imagedraw/triangle_right_width.png b/Tests/images/imagedraw/triangle_right_width.png new file mode 100644 index 00000000000..57b73553a6d Binary files /dev/null and b/Tests/images/imagedraw/triangle_right_width.png differ diff --git a/Tests/images/imagedraw/triangle_right_width_no_fill.png b/Tests/images/imagedraw/triangle_right_width_no_fill.png new file mode 100644 index 00000000000..dd65be6be7b Binary files /dev/null and b/Tests/images/imagedraw/triangle_right_width_no_fill.png differ diff --git a/Tests/images/imagedraw2_text.png b/Tests/images/imagedraw2_text.png new file mode 100644 index 00000000000..b22e6545b9c Binary files /dev/null and b/Tests/images/imagedraw2_text.png differ diff --git a/Tests/images/imagedraw_arc.png b/Tests/images/imagedraw_arc.png index b097743890c..967e214d9bd 100644 Binary files a/Tests/images/imagedraw_arc.png and b/Tests/images/imagedraw_arc.png differ diff --git a/Tests/images/imagedraw_arc_end_le_start.png b/Tests/images/imagedraw_arc_end_le_start.png index aee48e1c6bb..191cc0b3a67 100644 Binary files a/Tests/images/imagedraw_arc_end_le_start.png and b/Tests/images/imagedraw_arc_end_le_start.png differ diff --git a/Tests/images/imagedraw_arc_high.png b/Tests/images/imagedraw_arc_high.png new file mode 100644 index 00000000000..e3fb66cd0c0 Binary files /dev/null and b/Tests/images/imagedraw_arc_high.png differ diff --git a/Tests/images/imagedraw_arc_no_loops.png b/Tests/images/imagedraw_arc_no_loops.png index e45ad57a5c3..03bbd4b4336 100644 Binary files a/Tests/images/imagedraw_arc_no_loops.png and b/Tests/images/imagedraw_arc_no_loops.png differ diff --git a/Tests/images/imagedraw_arc_width.png b/Tests/images/imagedraw_arc_width.png new file mode 100644 index 00000000000..70dae7d5ff5 Binary files /dev/null and b/Tests/images/imagedraw_arc_width.png differ diff --git a/Tests/images/imagedraw_arc_width_fill.png b/Tests/images/imagedraw_arc_width_fill.png new file mode 100644 index 00000000000..6c135ab7679 Binary files /dev/null and b/Tests/images/imagedraw_arc_width_fill.png differ diff --git a/Tests/images/imagedraw_arc_width_non_whole_angle.png b/Tests/images/imagedraw_arc_width_non_whole_angle.png new file mode 100644 index 00000000000..f54eb1c2932 Binary files /dev/null and b/Tests/images/imagedraw_arc_width_non_whole_angle.png differ diff --git a/Tests/images/imagedraw_arc_width_pieslice.png b/Tests/images/imagedraw_arc_width_pieslice.png new file mode 100644 index 00000000000..e1aa95e88e5 Binary files /dev/null and b/Tests/images/imagedraw_arc_width_pieslice.png differ diff --git a/Tests/images/imagedraw_chord_L.png b/Tests/images/imagedraw_chord_L.png index a5a0078d042..6c89da9eaf8 100644 Binary files a/Tests/images/imagedraw_chord_L.png and b/Tests/images/imagedraw_chord_L.png differ diff --git a/Tests/images/imagedraw_chord_RGB.png b/Tests/images/imagedraw_chord_RGB.png index af6fc766007..d4592cda109 100644 Binary files a/Tests/images/imagedraw_chord_RGB.png and b/Tests/images/imagedraw_chord_RGB.png differ diff --git a/Tests/images/imagedraw_chord_too_fat.png b/Tests/images/imagedraw_chord_too_fat.png new file mode 100644 index 00000000000..2021202fe79 Binary files /dev/null and b/Tests/images/imagedraw_chord_too_fat.png differ diff --git a/Tests/images/imagedraw_chord_width.png b/Tests/images/imagedraw_chord_width.png new file mode 100644 index 00000000000..04d3dadf8d1 Binary files /dev/null and b/Tests/images/imagedraw_chord_width.png differ diff --git a/Tests/images/imagedraw_chord_width_fill.png b/Tests/images/imagedraw_chord_width_fill.png new file mode 100644 index 00000000000..77475d266c6 Binary files /dev/null and b/Tests/images/imagedraw_chord_width_fill.png differ diff --git a/Tests/images/imagedraw_chord_zero_width.png b/Tests/images/imagedraw_chord_zero_width.png new file mode 100644 index 00000000000..c8ebe39175c Binary files /dev/null and b/Tests/images/imagedraw_chord_zero_width.png differ diff --git a/Tests/images/imagedraw_ellipse_L.png b/Tests/images/imagedraw_ellipse_L.png index e47e6e441c1..d5959cc0820 100644 Binary files a/Tests/images/imagedraw_ellipse_L.png and b/Tests/images/imagedraw_ellipse_L.png differ diff --git a/Tests/images/imagedraw_ellipse_RGB.png b/Tests/images/imagedraw_ellipse_RGB.png index b52b128023c..1d310ce115a 100644 Binary files a/Tests/images/imagedraw_ellipse_RGB.png and b/Tests/images/imagedraw_ellipse_RGB.png differ diff --git a/Tests/images/imagedraw_ellipse_edge.png b/Tests/images/imagedraw_ellipse_edge.png index 25a95a6018a..a2235115af3 100644 Binary files a/Tests/images/imagedraw_ellipse_edge.png and b/Tests/images/imagedraw_ellipse_edge.png differ diff --git a/Tests/images/imagedraw_ellipse_translucent.png b/Tests/images/imagedraw_ellipse_translucent.png new file mode 100644 index 00000000000..964dce67889 Binary files /dev/null and b/Tests/images/imagedraw_ellipse_translucent.png differ diff --git a/Tests/images/imagedraw_ellipse_various_sizes.png b/Tests/images/imagedraw_ellipse_various_sizes.png new file mode 100644 index 00000000000..11a1be6faeb Binary files /dev/null and b/Tests/images/imagedraw_ellipse_various_sizes.png differ diff --git a/Tests/images/imagedraw_ellipse_various_sizes_filled.png b/Tests/images/imagedraw_ellipse_various_sizes_filled.png new file mode 100644 index 00000000000..d71e175b8fd Binary files /dev/null and b/Tests/images/imagedraw_ellipse_various_sizes_filled.png differ diff --git a/Tests/images/imagedraw_ellipse_width.png b/Tests/images/imagedraw_ellipse_width.png new file mode 100644 index 00000000000..54cfc291ca2 Binary files /dev/null and b/Tests/images/imagedraw_ellipse_width.png differ diff --git a/Tests/images/imagedraw_ellipse_width_fill.png b/Tests/images/imagedraw_ellipse_width_fill.png new file mode 100644 index 00000000000..4a1edc3797f Binary files /dev/null and b/Tests/images/imagedraw_ellipse_width_fill.png differ diff --git a/Tests/images/imagedraw_ellipse_width_large.png b/Tests/images/imagedraw_ellipse_width_large.png new file mode 100644 index 00000000000..e9518601980 Binary files /dev/null and b/Tests/images/imagedraw_ellipse_width_large.png differ diff --git a/Tests/images/imagedraw_ellipse_zero_width.png b/Tests/images/imagedraw_ellipse_zero_width.png new file mode 100644 index 00000000000..6661b7d999e Binary files /dev/null and b/Tests/images/imagedraw_ellipse_zero_width.png differ diff --git a/Tests/images/imagedraw_floodfill_L.png b/Tests/images/imagedraw_floodfill_L.png new file mode 100644 index 00000000000..daaf9422d30 Binary files /dev/null and b/Tests/images/imagedraw_floodfill_L.png differ diff --git a/Tests/images/imagedraw_floodfill.png b/Tests/images/imagedraw_floodfill_RGB.png similarity index 100% rename from Tests/images/imagedraw_floodfill.png rename to Tests/images/imagedraw_floodfill_RGB.png diff --git a/Tests/images/imagedraw_floodfill_RGBA.png b/Tests/images/imagedraw_floodfill_RGBA.png new file mode 100644 index 00000000000..5e02064d418 Binary files /dev/null and b/Tests/images/imagedraw_floodfill_RGBA.png differ diff --git a/Tests/images/imagedraw_floodfill_not_negative.png b/Tests/images/imagedraw_floodfill_not_negative.png new file mode 100644 index 00000000000..c3f34a174c0 Binary files /dev/null and b/Tests/images/imagedraw_floodfill_not_negative.png differ diff --git a/Tests/images/imagedraw_line_joint_curve.png b/Tests/images/imagedraw_line_joint_curve.png new file mode 100644 index 00000000000..ad729f52858 Binary files /dev/null and b/Tests/images/imagedraw_line_joint_curve.png differ diff --git a/Tests/images/imagedraw_outline_chord_L.png b/Tests/images/imagedraw_outline_chord_L.png new file mode 100644 index 00000000000..9c20ad21714 Binary files /dev/null and b/Tests/images/imagedraw_outline_chord_L.png differ diff --git a/Tests/images/imagedraw_outline_chord_RGB.png b/Tests/images/imagedraw_outline_chord_RGB.png new file mode 100644 index 00000000000..3c71312c75d Binary files /dev/null and b/Tests/images/imagedraw_outline_chord_RGB.png differ diff --git a/Tests/images/imagedraw_outline_ellipse_L.png b/Tests/images/imagedraw_outline_ellipse_L.png new file mode 100644 index 00000000000..53b76b62b42 Binary files /dev/null and b/Tests/images/imagedraw_outline_ellipse_L.png differ diff --git a/Tests/images/imagedraw_outline_ellipse_RGB.png b/Tests/images/imagedraw_outline_ellipse_RGB.png new file mode 100644 index 00000000000..37a5193273b Binary files /dev/null and b/Tests/images/imagedraw_outline_ellipse_RGB.png differ diff --git a/Tests/images/imagedraw_outline_pieslice_L.png b/Tests/images/imagedraw_outline_pieslice_L.png new file mode 100644 index 00000000000..92972d54cc9 Binary files /dev/null and b/Tests/images/imagedraw_outline_pieslice_L.png differ diff --git a/Tests/images/imagedraw_outline_pieslice_RGB.png b/Tests/images/imagedraw_outline_pieslice_RGB.png new file mode 100644 index 00000000000..4be4fc4afbb Binary files /dev/null and b/Tests/images/imagedraw_outline_pieslice_RGB.png differ diff --git a/Tests/images/imagedraw_outline_polygon_L.png b/Tests/images/imagedraw_outline_polygon_L.png new file mode 100644 index 00000000000..57ed9d43b14 Binary files /dev/null and b/Tests/images/imagedraw_outline_polygon_L.png differ diff --git a/Tests/images/imagedraw_outline_polygon_RGB.png b/Tests/images/imagedraw_outline_polygon_RGB.png new file mode 100644 index 00000000000..286b71c2302 Binary files /dev/null and b/Tests/images/imagedraw_outline_polygon_RGB.png differ diff --git a/Tests/images/imagedraw_outline_rectangle_L.png b/Tests/images/imagedraw_outline_rectangle_L.png new file mode 100644 index 00000000000..b9c47018fb4 Binary files /dev/null and b/Tests/images/imagedraw_outline_rectangle_L.png differ diff --git a/Tests/images/imagedraw_outline_rectangle_RGB.png b/Tests/images/imagedraw_outline_rectangle_RGB.png new file mode 100644 index 00000000000..41b92fb75a0 Binary files /dev/null and b/Tests/images/imagedraw_outline_rectangle_RGB.png differ diff --git a/Tests/images/imagedraw_outline_shape_L.png b/Tests/images/imagedraw_outline_shape_L.png new file mode 100644 index 00000000000..20ebef1578c Binary files /dev/null and b/Tests/images/imagedraw_outline_shape_L.png differ diff --git a/Tests/images/imagedraw_outline_shape_RGB.png b/Tests/images/imagedraw_outline_shape_RGB.png new file mode 100644 index 00000000000..6fb6f623e4b Binary files /dev/null and b/Tests/images/imagedraw_outline_shape_RGB.png differ diff --git a/Tests/images/imagedraw_pieslice.png b/Tests/images/imagedraw_pieslice.png index 2f8c091915f..41c786e7712 100644 Binary files a/Tests/images/imagedraw_pieslice.png and b/Tests/images/imagedraw_pieslice.png differ diff --git a/Tests/images/imagedraw_pieslice_wide.png b/Tests/images/imagedraw_pieslice_wide.png new file mode 100644 index 00000000000..44687478836 Binary files /dev/null and b/Tests/images/imagedraw_pieslice_wide.png differ diff --git a/Tests/images/imagedraw_pieslice_width.png b/Tests/images/imagedraw_pieslice_width.png new file mode 100644 index 00000000000..422d92f3bba Binary files /dev/null and b/Tests/images/imagedraw_pieslice_width.png differ diff --git a/Tests/images/imagedraw_pieslice_width_fill.png b/Tests/images/imagedraw_pieslice_width_fill.png new file mode 100644 index 00000000000..bee6aac3b99 Binary files /dev/null and b/Tests/images/imagedraw_pieslice_width_fill.png differ diff --git a/Tests/images/imagedraw_pieslice_zero_width.png b/Tests/images/imagedraw_pieslice_zero_width.png new file mode 100644 index 00000000000..d6ceb0b5a04 Binary files /dev/null and b/Tests/images/imagedraw_pieslice_zero_width.png differ diff --git a/Tests/images/imagedraw_polygon_1px_high.png b/Tests/images/imagedraw_polygon_1px_high.png new file mode 100644 index 00000000000..e06508a0af0 Binary files /dev/null and b/Tests/images/imagedraw_polygon_1px_high.png differ diff --git a/Tests/images/imagedraw_polygon_1px_high_translucent.png b/Tests/images/imagedraw_polygon_1px_high_translucent.png new file mode 100644 index 00000000000..8bbf9397c72 Binary files /dev/null and b/Tests/images/imagedraw_polygon_1px_high_translucent.png differ diff --git a/Tests/images/imagedraw_polygon_kite_L.png b/Tests/images/imagedraw_polygon_kite_L.png index 241d86bf40d..0d9a1c8f816 100644 Binary files a/Tests/images/imagedraw_polygon_kite_L.png and b/Tests/images/imagedraw_polygon_kite_L.png differ diff --git a/Tests/images/imagedraw_polygon_translucent.png b/Tests/images/imagedraw_polygon_translucent.png new file mode 100644 index 00000000000..da8d790a36f Binary files /dev/null and b/Tests/images/imagedraw_polygon_translucent.png differ diff --git a/Tests/images/imagedraw_rectangle_I.png b/Tests/images/imagedraw_rectangle_I.png new file mode 100644 index 00000000000..4e94f6943dd Binary files /dev/null and b/Tests/images/imagedraw_rectangle_I.png differ diff --git a/Tests/images/imagedraw_rectangle_translucent_outline.png b/Tests/images/imagedraw_rectangle_translucent_outline.png new file mode 100644 index 00000000000..845648762cc Binary files /dev/null and b/Tests/images/imagedraw_rectangle_translucent_outline.png differ diff --git a/Tests/images/imagedraw_rectangle_width.png b/Tests/images/imagedraw_rectangle_width.png new file mode 100644 index 00000000000..e39659921fc Binary files /dev/null and b/Tests/images/imagedraw_rectangle_width.png differ diff --git a/Tests/images/imagedraw_rectangle_width_fill.png b/Tests/images/imagedraw_rectangle_width_fill.png new file mode 100644 index 00000000000..d5243c608b6 Binary files /dev/null and b/Tests/images/imagedraw_rectangle_width_fill.png differ diff --git a/Tests/images/imagedraw_rectangle_zero_width.png b/Tests/images/imagedraw_rectangle_zero_width.png new file mode 100644 index 00000000000..989c9576196 Binary files /dev/null and b/Tests/images/imagedraw_rectangle_zero_width.png differ diff --git a/Tests/images/imagedraw_regular_octagon.png b/Tests/images/imagedraw_regular_octagon.png new file mode 100644 index 00000000000..7f215dc08bf Binary files /dev/null and b/Tests/images/imagedraw_regular_octagon.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle.png b/Tests/images/imagedraw_rounded_rectangle.png new file mode 100644 index 00000000000..2e815f4ada2 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_both.png b/Tests/images/imagedraw_rounded_rectangle_both.png new file mode 100644 index 00000000000..24f600e3913 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_both.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_given.png b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_given.png new file mode 100644 index 00000000000..59e55b2a1e9 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_given.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_height.png b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_height.png new file mode 100644 index 00000000000..c4e54896ba0 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_height.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_width.png b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_width.png new file mode 100644 index 00000000000..6b0f11fa627 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_width.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_x.png b/Tests/images/imagedraw_rounded_rectangle_x.png new file mode 100644 index 00000000000..4bf5211a334 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_x.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_y.png b/Tests/images/imagedraw_rounded_rectangle_y.png new file mode 100644 index 00000000000..9b391b95e28 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_y.png differ diff --git a/Tests/images/imagedraw_square.png b/Tests/images/imagedraw_square.png new file mode 100644 index 00000000000..fd75f2f3b0c Binary files /dev/null and b/Tests/images/imagedraw_square.png differ diff --git a/Tests/images/imagedraw_square_rotate_45.png b/Tests/images/imagedraw_square_rotate_45.png new file mode 100644 index 00000000000..8ab0e3c1839 Binary files /dev/null and b/Tests/images/imagedraw_square_rotate_45.png differ diff --git a/Tests/images/imagedraw_stroke_descender.png b/Tests/images/imagedraw_stroke_descender.png new file mode 100644 index 00000000000..93462334ae4 Binary files /dev/null and b/Tests/images/imagedraw_stroke_descender.png differ diff --git a/Tests/images/imagedraw_stroke_different.png b/Tests/images/imagedraw_stroke_different.png new file mode 100644 index 00000000000..e58cbdc4e23 Binary files /dev/null and b/Tests/images/imagedraw_stroke_different.png differ diff --git a/Tests/images/imagedraw_stroke_multiline.png b/Tests/images/imagedraw_stroke_multiline.png new file mode 100644 index 00000000000..fc5e07c8679 Binary files /dev/null and b/Tests/images/imagedraw_stroke_multiline.png differ diff --git a/Tests/images/imagedraw_stroke_same.png b/Tests/images/imagedraw_stroke_same.png new file mode 100644 index 00000000000..8f2f3abe1a6 Binary files /dev/null and b/Tests/images/imagedraw_stroke_same.png differ diff --git a/Tests/images/imagedraw_wide_line_larger_than_int.png b/Tests/images/imagedraw_wide_line_larger_than_int.png new file mode 100644 index 00000000000..68073ce4827 Binary files /dev/null and b/Tests/images/imagedraw_wide_line_larger_than_int.png differ diff --git a/Tests/images/imageops_pad_h_0.jpg b/Tests/images/imageops_pad_h_0.jpg new file mode 100644 index 00000000000..7afbbb96a6e Binary files /dev/null and b/Tests/images/imageops_pad_h_0.jpg differ diff --git a/Tests/images/imageops_pad_h_1.jpg b/Tests/images/imageops_pad_h_1.jpg new file mode 100644 index 00000000000..b9bf8a49a8d Binary files /dev/null and b/Tests/images/imageops_pad_h_1.jpg differ diff --git a/Tests/images/imageops_pad_h_2.jpg b/Tests/images/imageops_pad_h_2.jpg new file mode 100644 index 00000000000..7e0eb95994a Binary files /dev/null and b/Tests/images/imageops_pad_h_2.jpg differ diff --git a/Tests/images/imageops_pad_v_0.jpg b/Tests/images/imageops_pad_v_0.jpg new file mode 100644 index 00000000000..73a96c86cac Binary files /dev/null and b/Tests/images/imageops_pad_v_0.jpg differ diff --git a/Tests/images/imageops_pad_v_1.jpg b/Tests/images/imageops_pad_v_1.jpg new file mode 100644 index 00000000000..04545f81742 Binary files /dev/null and b/Tests/images/imageops_pad_v_1.jpg differ diff --git a/Tests/images/imageops_pad_v_2.jpg b/Tests/images/imageops_pad_v_2.jpg new file mode 100644 index 00000000000..f3e399d7b18 Binary files /dev/null and b/Tests/images/imageops_pad_v_2.jpg differ diff --git a/Tests/images/input_bw_five_bands.fpx b/Tests/images/input_bw_five_bands.fpx new file mode 100644 index 00000000000..5fcb144aef1 Binary files /dev/null and b/Tests/images/input_bw_five_bands.fpx differ diff --git a/Tests/images/input_bw_one_band.fpx b/Tests/images/input_bw_one_band.fpx new file mode 100644 index 00000000000..9bdc53763fe Binary files /dev/null and b/Tests/images/input_bw_one_band.fpx differ diff --git a/Tests/images/input_bw_one_band.png b/Tests/images/input_bw_one_band.png new file mode 100644 index 00000000000..6b4c1f37696 Binary files /dev/null and b/Tests/images/input_bw_one_band.png differ diff --git a/Tests/images/invalid-exif-without-x-resolution.jpg b/Tests/images/invalid-exif-without-x-resolution.jpg new file mode 100644 index 00000000000..00f6bd2f305 Binary files /dev/null and b/Tests/images/invalid-exif-without-x-resolution.jpg differ diff --git a/Tests/images/invalid_header_length.jp2 b/Tests/images/invalid_header_length.jp2 new file mode 100644 index 00000000000..c0c14f42160 Binary files /dev/null and b/Tests/images/invalid_header_length.jp2 differ diff --git a/Tests/images/iptc_roundUp.jpg b/Tests/images/iptc_roundUp.jpg new file mode 100644 index 00000000000..68ac20b71f4 Binary files /dev/null and b/Tests/images/iptc_roundUp.jpg differ diff --git a/Tests/images/iss634.apng b/Tests/images/iss634.apng new file mode 100644 index 00000000000..89e02590664 Binary files /dev/null and b/Tests/images/iss634.apng differ diff --git a/Tests/images/issue_6194.j2k b/Tests/images/issue_6194.j2k new file mode 100644 index 00000000000..b1b8851670b Binary files /dev/null and b/Tests/images/issue_6194.j2k differ diff --git a/Tests/images/itxt_chunks.png b/Tests/images/itxt_chunks.png new file mode 100644 index 00000000000..ca098440c15 Binary files /dev/null and b/Tests/images/itxt_chunks.png differ diff --git a/Tests/images/la.tga b/Tests/images/la.tga new file mode 100644 index 00000000000..8c83104ed7e Binary files /dev/null and b/Tests/images/la.tga differ diff --git a/Tests/images/missing_background.gif b/Tests/images/missing_background.gif new file mode 100644 index 00000000000..550d68d8101 Binary files /dev/null and b/Tests/images/missing_background.gif differ diff --git a/Tests/images/missing_background_first_frame.png b/Tests/images/missing_background_first_frame.png new file mode 100644 index 00000000000..25237ba5d20 Binary files /dev/null and b/Tests/images/missing_background_first_frame.png differ diff --git a/Tests/images/mmap_error.bmp b/Tests/images/mmap_error.bmp new file mode 100644 index 00000000000..04df163d7fe Binary files /dev/null and b/Tests/images/mmap_error.bmp differ diff --git a/Tests/images/multiline_text.png b/Tests/images/multiline_text.png index ff1308c5ef2..e39c6586ca5 100644 Binary files a/Tests/images/multiline_text.png and b/Tests/images/multiline_text.png differ diff --git a/Tests/images/multiline_text_center.png b/Tests/images/multiline_text_center.png index f44d0783a09..837c6382a97 100644 Binary files a/Tests/images/multiline_text_center.png and b/Tests/images/multiline_text_center.png differ diff --git a/Tests/images/multiline_text_right.png b/Tests/images/multiline_text_right.png index 1b32d916754..58b3bdddd87 100644 Binary files a/Tests/images/multiline_text_right.png and b/Tests/images/multiline_text_right.png differ diff --git a/Tests/images/multiline_text_spacing.png b/Tests/images/multiline_text_spacing.png index 3c3bc0f267d..3b367c7ddee 100644 Binary files a/Tests/images/multiline_text_spacing.png and b/Tests/images/multiline_text_spacing.png differ diff --git a/Tests/images/multipage_multiple_frame_loop.tiff b/Tests/images/multipage_multiple_frame_loop.tiff new file mode 100644 index 00000000000..b6759b08023 Binary files /dev/null and b/Tests/images/multipage_multiple_frame_loop.tiff differ diff --git a/Tests/images/multipage_out_of_order.tiff b/Tests/images/multipage_out_of_order.tiff new file mode 100644 index 00000000000..1576a549b58 Binary files /dev/null and b/Tests/images/multipage_out_of_order.tiff differ diff --git a/Tests/images/multipage_single_frame_loop.tiff b/Tests/images/multipage_single_frame_loop.tiff new file mode 100644 index 00000000000..26f27c421cd Binary files /dev/null and b/Tests/images/multipage_single_frame_loop.tiff differ diff --git a/Tests/images/multiple_comments.gif b/Tests/images/multiple_comments.gif new file mode 100644 index 00000000000..88b2af800e8 Binary files /dev/null and b/Tests/images/multiple_comments.gif differ diff --git a/Tests/images/negative_layer_count.psd b/Tests/images/negative_layer_count.psd new file mode 100644 index 00000000000..b111c2d5675 Binary files /dev/null and b/Tests/images/negative_layer_count.psd differ diff --git a/Tests/images/no_palette.gif b/Tests/images/no_palette.gif new file mode 100644 index 00000000000..0432ebcb61c Binary files /dev/null and b/Tests/images/no_palette.gif differ diff --git a/Tests/images/no_palette_after_rgb.gif b/Tests/images/no_palette_after_rgb.gif new file mode 100644 index 00000000000..8704c464cc4 Binary files /dev/null and b/Tests/images/no_palette_after_rgb.gif differ diff --git a/Tests/images/no_palette_with_background.gif b/Tests/images/no_palette_with_background.gif new file mode 100644 index 00000000000..e49e5d461aa Binary files /dev/null and b/Tests/images/no_palette_with_background.gif differ diff --git a/Tests/images/no_palette_with_transparency.gif b/Tests/images/no_palette_with_transparency.gif new file mode 100644 index 00000000000..031bdcfce10 Binary files /dev/null and b/Tests/images/no_palette_with_transparency.gif differ diff --git a/Tests/images/no_rows_per_strip.tif b/Tests/images/no_rows_per_strip.tif new file mode 100644 index 00000000000..67942aec40c Binary files /dev/null and b/Tests/images/no_rows_per_strip.tif differ diff --git a/Tests/images/not_enough_data.jp2 b/Tests/images/not_enough_data.jp2 new file mode 100644 index 00000000000..2d28bb5e96b Binary files /dev/null and b/Tests/images/not_enough_data.jp2 differ diff --git a/Tests/images/odd_stride.pcx b/Tests/images/odd_stride.pcx new file mode 100644 index 00000000000..ee0c2eecaeb Binary files /dev/null and b/Tests/images/odd_stride.pcx differ diff --git a/Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif b/Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif new file mode 100644 index 00000000000..d43ba919220 Binary files /dev/null and b/Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif differ diff --git a/Tests/images/old-style-jpeg-compression.png b/Tests/images/old-style-jpeg-compression.png new file mode 100644 index 00000000000..c035542ea7b Binary files /dev/null and b/Tests/images/old-style-jpeg-compression.png differ diff --git a/Tests/images/old-style-jpeg-compression.tif b/Tests/images/old-style-jpeg-compression.tif new file mode 100644 index 00000000000..8d726c40492 Binary files /dev/null and b/Tests/images/old-style-jpeg-compression.tif differ diff --git a/Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif b/Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif new file mode 100644 index 00000000000..01dca594f53 Binary files /dev/null and b/Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif differ diff --git a/Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns b/Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns new file mode 100644 index 00000000000..0521f5cf176 Binary files /dev/null and b/Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns differ diff --git a/Tests/images/ossfuzz-4836216264589312.pcx b/Tests/images/ossfuzz-4836216264589312.pcx new file mode 100644 index 00000000000..fdde9716a0c Binary files /dev/null and b/Tests/images/ossfuzz-4836216264589312.pcx differ diff --git a/Tests/images/ossfuzz-5730089102868480.sgi b/Tests/images/ossfuzz-5730089102868480.sgi new file mode 100644 index 00000000000..a92c1ed019b Binary files /dev/null and b/Tests/images/ossfuzz-5730089102868480.sgi differ diff --git a/Tests/images/p_16.png b/Tests/images/p_16.png new file mode 100644 index 00000000000..e3588641277 Binary files /dev/null and b/Tests/images/p_16.png differ diff --git a/Tests/images/p_16.tga b/Tests/images/p_16.tga new file mode 100644 index 00000000000..2b2ca4c703c Binary files /dev/null and b/Tests/images/p_16.tga differ diff --git a/Tests/images/padded_idat.png b/Tests/images/padded_idat.png new file mode 100644 index 00000000000..18c5a4990cd Binary files /dev/null and b/Tests/images/padded_idat.png differ diff --git a/Tests/images/pal8_offset.bmp b/Tests/images/pal8_offset.bmp new file mode 100644 index 00000000000..24be65f22c3 Binary files /dev/null and b/Tests/images/pal8_offset.bmp differ diff --git a/Tests/images/palette_negative.png b/Tests/images/palette_negative.png new file mode 100644 index 00000000000..7fcfd29a0db Binary files /dev/null and b/Tests/images/palette_negative.png differ diff --git a/Tests/images/palette_not_needed_for_second_frame.gif b/Tests/images/palette_not_needed_for_second_frame.gif new file mode 100644 index 00000000000..0617291d152 Binary files /dev/null and b/Tests/images/palette_not_needed_for_second_frame.gif differ diff --git a/Tests/images/palette_sepia.png b/Tests/images/palette_sepia.png new file mode 100644 index 00000000000..9e7d6b0345b Binary files /dev/null and b/Tests/images/palette_sepia.png differ diff --git a/Tests/images/palette_wedge.png b/Tests/images/palette_wedge.png new file mode 100644 index 00000000000..4b3d9ff3a21 Binary files /dev/null and b/Tests/images/palette_wedge.png differ diff --git a/Tests/images/pcx_overrun.bin b/Tests/images/pcx_overrun.bin new file mode 100644 index 00000000000..ea46d2c1194 Binary files /dev/null and b/Tests/images/pcx_overrun.bin differ diff --git a/Tests/images/pcx_overrun2.bin b/Tests/images/pcx_overrun2.bin new file mode 100644 index 00000000000..5f00b50595a Binary files /dev/null and b/Tests/images/pcx_overrun2.bin differ diff --git a/Tests/images/photoshop-200dpi-broken.jpg b/Tests/images/photoshop-200dpi-broken.jpg new file mode 100644 index 00000000000..a574872f267 Binary files /dev/null and b/Tests/images/photoshop-200dpi-broken.jpg differ diff --git a/Tests/images/pillow3.icns b/Tests/images/pillow3.icns index ef9b8917872..49b691d90f2 100644 Binary files a/Tests/images/pillow3.icns and b/Tests/images/pillow3.icns differ diff --git a/Tests/images/radial_gradients.png b/Tests/images/radial_gradients.png new file mode 100644 index 00000000000..39a02fbbfdf Binary files /dev/null and b/Tests/images/radial_gradients.png differ diff --git a/Tests/images/raw_negative_stride.bin b/Tests/images/raw_negative_stride.bin new file mode 100644 index 00000000000..312e82a5fe2 Binary files /dev/null and b/Tests/images/raw_negative_stride.bin differ diff --git a/Tests/images/rectangle_surrounding_text.png b/Tests/images/rectangle_surrounding_text.png index 2b75a5e9c7a..ca77cea7323 100644 Binary files a/Tests/images/rectangle_surrounding_text.png and b/Tests/images/rectangle_surrounding_text.png differ diff --git a/Tests/images/reqd_showpage_transparency.png b/Tests/images/reqd_showpage_transparency.png new file mode 100644 index 00000000000..3ce159d0fc0 Binary files /dev/null and b/Tests/images/reqd_showpage_transparency.png differ diff --git a/Tests/images/rgb32bf-abgr.bmp b/Tests/images/rgb32bf-abgr.bmp new file mode 100644 index 00000000000..2443714cadc Binary files /dev/null and b/Tests/images/rgb32bf-abgr.bmp differ diff --git a/Tests/images/rgb32bf-rgba.bmp b/Tests/images/rgb32bf-rgba.bmp new file mode 100644 index 00000000000..467c2570b1b Binary files /dev/null and b/Tests/images/rgb32bf-rgba.bmp differ diff --git a/Tests/images/rgb32rle_bottom_right.tga b/Tests/images/rgb32rle_bottom_right.tga new file mode 100644 index 00000000000..bd4609e9c1c Binary files /dev/null and b/Tests/images/rgb32rle_bottom_right.tga differ diff --git a/Tests/images/rgb32rle_top_right.tga b/Tests/images/rgb32rle_top_right.tga new file mode 100644 index 00000000000..78f9dc5dfb0 Binary files /dev/null and b/Tests/images/rgb32rle_top_right.tga differ diff --git a/Tests/images/rgba.psd b/Tests/images/rgba.psd new file mode 100644 index 00000000000..45fb7c3cca0 Binary files /dev/null and b/Tests/images/rgba.psd differ diff --git a/Tests/images/rotate_45_no_fill.png b/Tests/images/rotate_45_no_fill.png new file mode 100644 index 00000000000..3c9d03e6ca9 Binary files /dev/null and b/Tests/images/rotate_45_no_fill.png differ diff --git a/Tests/images/rotate_45_with_fill.png b/Tests/images/rotate_45_with_fill.png new file mode 100644 index 00000000000..05b2d34d54c Binary files /dev/null and b/Tests/images/rotate_45_with_fill.png differ diff --git a/Tests/images/second_frame_comment.gif b/Tests/images/second_frame_comment.gif new file mode 100644 index 00000000000..c8fc957911e Binary files /dev/null and b/Tests/images/second_frame_comment.gif differ diff --git a/Tests/images/sgi_crash.bin b/Tests/images/sgi_crash.bin new file mode 100644 index 00000000000..9b138f6fe0a Binary files /dev/null and b/Tests/images/sgi_crash.bin differ diff --git a/Tests/images/sgi_overrun.bin b/Tests/images/sgi_overrun.bin new file mode 100644 index 00000000000..9a45d065ab8 Binary files /dev/null and b/Tests/images/sgi_overrun.bin differ diff --git a/Tests/images/sgi_overrun_expandrow.bin b/Tests/images/sgi_overrun_expandrow.bin new file mode 100644 index 00000000000..316d618818e Binary files /dev/null and b/Tests/images/sgi_overrun_expandrow.bin differ diff --git a/Tests/images/sgi_overrun_expandrow2.bin b/Tests/images/sgi_overrun_expandrow2.bin new file mode 100644 index 00000000000..f70e03a3960 Binary files /dev/null and b/Tests/images/sgi_overrun_expandrow2.bin differ diff --git a/Tests/images/sgi_overrun_expandrowF04.bin b/Tests/images/sgi_overrun_expandrowF04.bin new file mode 100644 index 00000000000..1907d5d3d47 Binary files /dev/null and b/Tests/images/sgi_overrun_expandrowF04.bin differ diff --git a/Tests/images/standard_embedded.png b/Tests/images/standard_embedded.png new file mode 100644 index 00000000000..8905325317f Binary files /dev/null and b/Tests/images/standard_embedded.png differ diff --git a/Tests/images/sugarshack_frame_size.mpo b/Tests/images/sugarshack_frame_size.mpo new file mode 100644 index 00000000000..009280a79a6 Binary files /dev/null and b/Tests/images/sugarshack_frame_size.mpo differ diff --git a/Tests/images/sugarshack_ifd_offset.mpo b/Tests/images/sugarshack_ifd_offset.mpo new file mode 100644 index 00000000000..2dcac876f37 Binary files /dev/null and b/Tests/images/sugarshack_ifd_offset.mpo differ diff --git a/Tests/images/sugarshack_no_data.mpo b/Tests/images/sugarshack_no_data.mpo new file mode 100644 index 00000000000..d94bad53b1f Binary files /dev/null and b/Tests/images/sugarshack_no_data.mpo differ diff --git a/Tests/images/test_anchor_multiline_lm_center.png b/Tests/images/test_anchor_multiline_lm_center.png new file mode 100644 index 00000000000..6fff287e47c Binary files /dev/null and b/Tests/images/test_anchor_multiline_lm_center.png differ diff --git a/Tests/images/test_anchor_multiline_lm_left.png b/Tests/images/test_anchor_multiline_lm_left.png new file mode 100644 index 00000000000..b76a81b8127 Binary files /dev/null and b/Tests/images/test_anchor_multiline_lm_left.png differ diff --git a/Tests/images/test_anchor_multiline_lm_right.png b/Tests/images/test_anchor_multiline_lm_right.png new file mode 100644 index 00000000000..c12a8d63e9b Binary files /dev/null and b/Tests/images/test_anchor_multiline_lm_right.png differ diff --git a/Tests/images/test_anchor_multiline_ma_center.png b/Tests/images/test_anchor_multiline_ma_center.png new file mode 100644 index 00000000000..4f35d781f62 Binary files /dev/null and b/Tests/images/test_anchor_multiline_ma_center.png differ diff --git a/Tests/images/test_anchor_multiline_md_center.png b/Tests/images/test_anchor_multiline_md_center.png new file mode 100644 index 00000000000..8290d045cfa Binary files /dev/null and b/Tests/images/test_anchor_multiline_md_center.png differ diff --git a/Tests/images/test_anchor_multiline_mm_center.png b/Tests/images/test_anchor_multiline_mm_center.png new file mode 100644 index 00000000000..773cf2a4a06 Binary files /dev/null and b/Tests/images/test_anchor_multiline_mm_center.png differ diff --git a/Tests/images/test_anchor_multiline_mm_left.png b/Tests/images/test_anchor_multiline_mm_left.png new file mode 100644 index 00000000000..87d56636a13 Binary files /dev/null and b/Tests/images/test_anchor_multiline_mm_left.png differ diff --git a/Tests/images/test_anchor_multiline_mm_right.png b/Tests/images/test_anchor_multiline_mm_right.png new file mode 100644 index 00000000000..cf002b12cd0 Binary files /dev/null and b/Tests/images/test_anchor_multiline_mm_right.png differ diff --git a/Tests/images/test_anchor_multiline_rm_center.png b/Tests/images/test_anchor_multiline_rm_center.png new file mode 100644 index 00000000000..98073144bc2 Binary files /dev/null and b/Tests/images/test_anchor_multiline_rm_center.png differ diff --git a/Tests/images/test_anchor_multiline_rm_left.png b/Tests/images/test_anchor_multiline_rm_left.png new file mode 100644 index 00000000000..838fd7858a4 Binary files /dev/null and b/Tests/images/test_anchor_multiline_rm_left.png differ diff --git a/Tests/images/test_anchor_multiline_rm_right.png b/Tests/images/test_anchor_multiline_rm_right.png new file mode 100644 index 00000000000..290f5841794 Binary files /dev/null and b/Tests/images/test_anchor_multiline_rm_right.png differ diff --git a/Tests/images/test_anchor_quick_ls.png b/Tests/images/test_anchor_quick_ls.png new file mode 100644 index 00000000000..524c417c394 Binary files /dev/null and b/Tests/images/test_anchor_quick_ls.png differ diff --git a/Tests/images/test_anchor_quick_ma.png b/Tests/images/test_anchor_quick_ma.png new file mode 100644 index 00000000000..cfff27f7dda Binary files /dev/null and b/Tests/images/test_anchor_quick_ma.png differ diff --git a/Tests/images/test_anchor_quick_mb.png b/Tests/images/test_anchor_quick_mb.png new file mode 100644 index 00000000000..ff11f478e7d Binary files /dev/null and b/Tests/images/test_anchor_quick_mb.png differ diff --git a/Tests/images/test_anchor_quick_md.png b/Tests/images/test_anchor_quick_md.png new file mode 100644 index 00000000000..5cbccb170bd Binary files /dev/null and b/Tests/images/test_anchor_quick_md.png differ diff --git a/Tests/images/test_anchor_quick_mm.png b/Tests/images/test_anchor_quick_mm.png new file mode 100644 index 00000000000..500294c3b8c Binary files /dev/null and b/Tests/images/test_anchor_quick_mm.png differ diff --git a/Tests/images/test_anchor_quick_ms.png b/Tests/images/test_anchor_quick_ms.png new file mode 100644 index 00000000000..b1012463eb7 Binary files /dev/null and b/Tests/images/test_anchor_quick_ms.png differ diff --git a/Tests/images/test_anchor_quick_mt.png b/Tests/images/test_anchor_quick_mt.png new file mode 100644 index 00000000000..19423e51afe Binary files /dev/null and b/Tests/images/test_anchor_quick_mt.png differ diff --git a/Tests/images/test_anchor_quick_rs.png b/Tests/images/test_anchor_quick_rs.png new file mode 100644 index 00000000000..20a5e6c6e73 Binary files /dev/null and b/Tests/images/test_anchor_quick_rs.png differ diff --git a/Tests/images/test_anchor_ttb_f_lt.png b/Tests/images/test_anchor_ttb_f_lt.png new file mode 100644 index 00000000000..5f70a65c425 Binary files /dev/null and b/Tests/images/test_anchor_ttb_f_lt.png differ diff --git a/Tests/images/test_anchor_ttb_f_mm.png b/Tests/images/test_anchor_ttb_f_mm.png new file mode 100644 index 00000000000..e7be557d2a5 Binary files /dev/null and b/Tests/images/test_anchor_ttb_f_mm.png differ diff --git a/Tests/images/test_anchor_ttb_f_rb.png b/Tests/images/test_anchor_ttb_f_rb.png new file mode 100644 index 00000000000..b78e2f954fa Binary files /dev/null and b/Tests/images/test_anchor_ttb_f_rb.png differ diff --git a/Tests/images/test_anchor_ttb_f_sm.png b/Tests/images/test_anchor_ttb_f_sm.png new file mode 100644 index 00000000000..f6dc7c70f0e Binary files /dev/null and b/Tests/images/test_anchor_ttb_f_sm.png differ diff --git a/Tests/images/test_arabictext_features.png b/Tests/images/test_arabictext_features.png index 9bfa5a931cf..a03845acef3 100644 Binary files a/Tests/images/test_arabictext_features.png and b/Tests/images/test_arabictext_features.png differ diff --git a/Tests/images/test_combine_caron.png b/Tests/images/test_combine_caron.png new file mode 100644 index 00000000000..1097f4be59e Binary files /dev/null and b/Tests/images/test_combine_caron.png differ diff --git a/Tests/images/test_combine_caron_below.png b/Tests/images/test_combine_caron_below.png new file mode 100644 index 00000000000..6e7d88a92c7 Binary files /dev/null and b/Tests/images/test_combine_caron_below.png differ diff --git a/Tests/images/test_combine_caron_below_lb.png b/Tests/images/test_combine_caron_below_lb.png new file mode 100644 index 00000000000..f59e722b2da Binary files /dev/null and b/Tests/images/test_combine_caron_below_lb.png differ diff --git a/Tests/images/test_combine_caron_below_ld.png b/Tests/images/test_combine_caron_below_ld.png new file mode 100644 index 00000000000..540ab7d4264 Binary files /dev/null and b/Tests/images/test_combine_caron_below_ld.png differ diff --git a/Tests/images/test_combine_caron_below_ls.png b/Tests/images/test_combine_caron_below_ls.png new file mode 100644 index 00000000000..1109b4ee670 Binary files /dev/null and b/Tests/images/test_combine_caron_below_ls.png differ diff --git a/Tests/images/test_combine_caron_below_ttb.png b/Tests/images/test_combine_caron_below_ttb.png new file mode 100644 index 00000000000..5c7576de01b Binary files /dev/null and b/Tests/images/test_combine_caron_below_ttb.png differ diff --git a/Tests/images/test_combine_caron_below_ttb_lb.png b/Tests/images/test_combine_caron_below_ttb_lb.png new file mode 100644 index 00000000000..bacd6a141f1 Binary files /dev/null and b/Tests/images/test_combine_caron_below_ttb_lb.png differ diff --git a/Tests/images/test_combine_caron_la.png b/Tests/images/test_combine_caron_la.png new file mode 100644 index 00000000000..1097f4be59e Binary files /dev/null and b/Tests/images/test_combine_caron_la.png differ diff --git a/Tests/images/test_combine_caron_ls.png b/Tests/images/test_combine_caron_ls.png new file mode 100644 index 00000000000..1a721873cad Binary files /dev/null and b/Tests/images/test_combine_caron_ls.png differ diff --git a/Tests/images/test_combine_caron_lt.png b/Tests/images/test_combine_caron_lt.png new file mode 100644 index 00000000000..91e50d45f1f Binary files /dev/null and b/Tests/images/test_combine_caron_lt.png differ diff --git a/Tests/images/test_combine_caron_ttb.png b/Tests/images/test_combine_caron_ttb.png new file mode 100644 index 00000000000..a94be2f0af6 Binary files /dev/null and b/Tests/images/test_combine_caron_ttb.png differ diff --git a/Tests/images/test_combine_caron_ttb_lt.png b/Tests/images/test_combine_caron_ttb_lt.png new file mode 100644 index 00000000000..a94be2f0af6 Binary files /dev/null and b/Tests/images/test_combine_caron_ttb_lt.png differ diff --git a/Tests/images/test_combine_double_breve_below.png b/Tests/images/test_combine_double_breve_below.png new file mode 100644 index 00000000000..30252107faa Binary files /dev/null and b/Tests/images/test_combine_double_breve_below.png differ diff --git a/Tests/images/test_combine_double_breve_below_ma.png b/Tests/images/test_combine_double_breve_below_ma.png new file mode 100644 index 00000000000..aea09538f7e Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ma.png differ diff --git a/Tests/images/test_combine_double_breve_below_ra.png b/Tests/images/test_combine_double_breve_below_ra.png new file mode 100644 index 00000000000..febd3ab670c Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ra.png differ diff --git a/Tests/images/test_combine_double_breve_below_ttb.png b/Tests/images/test_combine_double_breve_below_ttb.png new file mode 100644 index 00000000000..8e42c0d16da Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ttb.png differ diff --git a/Tests/images/test_combine_double_breve_below_ttb_mt.png b/Tests/images/test_combine_double_breve_below_ttb_mt.png new file mode 100644 index 00000000000..9a755e8f8d8 Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ttb_mt.png differ diff --git a/Tests/images/test_combine_double_breve_below_ttb_rt.png b/Tests/images/test_combine_double_breve_below_ttb_rt.png new file mode 100644 index 00000000000..b954a541522 Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ttb_rt.png differ diff --git a/Tests/images/test_combine_double_breve_below_ttb_st.png b/Tests/images/test_combine_double_breve_below_ttb_st.png new file mode 100644 index 00000000000..b6b08145e8f Binary files /dev/null and b/Tests/images/test_combine_double_breve_below_ttb_st.png differ diff --git a/Tests/images/test_combine_multiline_lm_center.png b/Tests/images/test_combine_multiline_lm_center.png new file mode 100644 index 00000000000..7b1e9c4e42f Binary files /dev/null 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 new file mode 100644 index 00000000000..a26996c2dbe Binary files /dev/null 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 new file mode 100644 index 00000000000..7caf5cb742a Binary files /dev/null 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 new file mode 100644 index 00000000000..a859e9570c8 Binary files /dev/null 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 new file mode 100644 index 00000000000..aadb5191f0e Binary files /dev/null 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 new file mode 100644 index 00000000000..8238d4ec8ca Binary files /dev/null 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 new file mode 100644 index 00000000000..7568dd63a33 Binary files /dev/null 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 new file mode 100644 index 00000000000..b8c3b5b143d Binary files /dev/null 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 new file mode 100644 index 00000000000..14c478a72d0 Binary files /dev/null and b/Tests/images/test_combine_multiline_rm_right.png differ diff --git a/Tests/images/test_combine_overline.png b/Tests/images/test_combine_overline.png new file mode 100644 index 00000000000..dc5e8636163 Binary files /dev/null and b/Tests/images/test_combine_overline.png differ diff --git a/Tests/images/test_combine_overline_la.png b/Tests/images/test_combine_overline_la.png new file mode 100644 index 00000000000..dc5e8636163 Binary files /dev/null and b/Tests/images/test_combine_overline_la.png differ diff --git a/Tests/images/test_combine_overline_ra.png b/Tests/images/test_combine_overline_ra.png new file mode 100644 index 00000000000..cbb2d472dc7 Binary files /dev/null and b/Tests/images/test_combine_overline_ra.png differ diff --git a/Tests/images/test_combine_overline_ttb.png b/Tests/images/test_combine_overline_ttb.png new file mode 100644 index 00000000000..f74538d9c3a Binary files /dev/null and b/Tests/images/test_combine_overline_ttb.png differ diff --git a/Tests/images/test_combine_overline_ttb_mt.png b/Tests/images/test_combine_overline_ttb_mt.png new file mode 100644 index 00000000000..e915543d66c Binary files /dev/null and b/Tests/images/test_combine_overline_ttb_mt.png differ diff --git a/Tests/images/test_combine_overline_ttb_rt.png b/Tests/images/test_combine_overline_ttb_rt.png new file mode 100644 index 00000000000..186d6ee843b Binary files /dev/null and b/Tests/images/test_combine_overline_ttb_rt.png differ diff --git a/Tests/images/test_combine_overline_ttb_st.png b/Tests/images/test_combine_overline_ttb_st.png new file mode 100644 index 00000000000..e915543d66c Binary files /dev/null and b/Tests/images/test_combine_overline_ttb_st.png differ diff --git a/Tests/images/test_complex_unicode_text.png b/Tests/images/test_complex_unicode_text.png index f1a6f7ec61d..61174d75f68 100644 Binary files a/Tests/images/test_complex_unicode_text.png and b/Tests/images/test_complex_unicode_text.png differ diff --git a/Tests/images/test_complex_unicode_text2.png b/Tests/images/test_complex_unicode_text2.png new file mode 100644 index 00000000000..0526233c0f5 Binary files /dev/null and b/Tests/images/test_complex_unicode_text2.png differ diff --git a/Tests/images/test_direction_ltr.png b/Tests/images/test_direction_ltr.png index 42239334d12..b30fcd5d819 100644 Binary files a/Tests/images/test_direction_ltr.png and b/Tests/images/test_direction_ltr.png differ diff --git a/Tests/images/test_direction_rtl.png b/Tests/images/test_direction_rtl.png index 966b67d6b64..282eed88393 100644 Binary files a/Tests/images/test_direction_rtl.png and b/Tests/images/test_direction_rtl.png differ diff --git a/Tests/images/test_direction_ttb.png b/Tests/images/test_direction_ttb.png new file mode 100644 index 00000000000..52dbf572340 Binary files /dev/null and b/Tests/images/test_direction_ttb.png differ diff --git a/Tests/images/test_direction_ttb_stroke.png b/Tests/images/test_direction_ttb_stroke.png new file mode 100644 index 00000000000..4b689c38ec7 Binary files /dev/null and b/Tests/images/test_direction_ttb_stroke.png differ diff --git a/Tests/images/test_draw_pbm_ter_en_target.png b/Tests/images/test_draw_pbm_ter_en_target.png new file mode 100644 index 00000000000..f1fa25b5539 Binary files /dev/null and b/Tests/images/test_draw_pbm_ter_en_target.png differ diff --git a/Tests/images/test_draw_pbm_ter_pl_target.png b/Tests/images/test_draw_pbm_ter_pl_target.png new file mode 100644 index 00000000000..503337d2bfa Binary files /dev/null and b/Tests/images/test_draw_pbm_ter_pl_target.png differ diff --git a/Tests/images/test_extents.gif b/Tests/images/test_extents.gif new file mode 100644 index 00000000000..03c436435d6 Binary files /dev/null and b/Tests/images/test_extents.gif differ diff --git a/Tests/images/test_kerning_features.png b/Tests/images/test_kerning_features.png index ca895735c4d..78bcd951bba 100644 Binary files a/Tests/images/test_kerning_features.png and b/Tests/images/test_kerning_features.png differ diff --git a/Tests/images/test_language.png b/Tests/images/test_language.png new file mode 100644 index 00000000000..c7721531892 Binary files /dev/null and b/Tests/images/test_language.png differ diff --git a/Tests/images/test_ligature_features.png b/Tests/images/test_ligature_features.png index 664e9929d05..89ea648bfc1 100644 Binary files a/Tests/images/test_ligature_features.png and b/Tests/images/test_ligature_features.png differ diff --git a/Tests/images/test_text.png b/Tests/images/test_text.png index c156399cd1b..5888e52e53c 100644 Binary files a/Tests/images/test_text.png and b/Tests/images/test_text.png differ diff --git a/Tests/images/test_woff2.png b/Tests/images/test_woff2.png new file mode 100644 index 00000000000..4eb3be4c73c Binary files /dev/null and b/Tests/images/test_woff2.png differ diff --git a/Tests/images/test_x_max_and_y_offset.png b/Tests/images/test_x_max_and_y_offset.png new file mode 100644 index 00000000000..21401813f46 Binary files /dev/null and b/Tests/images/test_x_max_and_y_offset.png differ diff --git a/Tests/images/test_y_offset.png b/Tests/images/test_y_offset.png index 5a166be8c2e..bda2490df11 100644 Binary files a/Tests/images/test_y_offset.png and b/Tests/images/test_y_offset.png differ diff --git a/Tests/images/text_float_coord.png b/Tests/images/text_float_coord.png new file mode 100644 index 00000000000..49468698cd4 Binary files /dev/null and b/Tests/images/text_float_coord.png differ diff --git a/Tests/images/text_float_coord_1_alt.png b/Tests/images/text_float_coord_1_alt.png new file mode 100644 index 00000000000..50bdac3d8f3 Binary files /dev/null and b/Tests/images/text_float_coord_1_alt.png differ diff --git a/Tests/images/text_mono.gif b/Tests/images/text_mono.gif new file mode 100644 index 00000000000..b350c10e64a Binary files /dev/null and b/Tests/images/text_mono.gif differ diff --git a/Tests/images/tga/common/1x1_l.png b/Tests/images/tga/common/1x1_l.png new file mode 100644 index 00000000000..d1a2cb81328 Binary files /dev/null and b/Tests/images/tga/common/1x1_l.png differ diff --git a/Tests/images/tga/common/1x1_l_bl_raw.tga b/Tests/images/tga/common/1x1_l_bl_raw.tga new file mode 100644 index 00000000000..c79e125eaaa Binary files /dev/null and b/Tests/images/tga/common/1x1_l_bl_raw.tga differ diff --git a/Tests/images/tga/common/1x1_l_bl_rle.tga b/Tests/images/tga/common/1x1_l_bl_rle.tga new file mode 100644 index 00000000000..ee1a7d2d8b8 Binary files /dev/null and b/Tests/images/tga/common/1x1_l_bl_rle.tga differ diff --git a/Tests/images/tga/common/1x1_l_tl_raw.tga b/Tests/images/tga/common/1x1_l_tl_raw.tga new file mode 100644 index 00000000000..6c99687582a Binary files /dev/null and b/Tests/images/tga/common/1x1_l_tl_raw.tga differ diff --git a/Tests/images/tga/common/1x1_l_tl_rle.tga b/Tests/images/tga/common/1x1_l_tl_rle.tga new file mode 100644 index 00000000000..efd4e3af40a Binary files /dev/null and b/Tests/images/tga/common/1x1_l_tl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_l.png b/Tests/images/tga/common/200x32_l.png new file mode 100644 index 00000000000..ff37cbe30a9 Binary files /dev/null and b/Tests/images/tga/common/200x32_l.png differ diff --git a/Tests/images/tga/common/200x32_l_bl_raw.tga b/Tests/images/tga/common/200x32_l_bl_raw.tga new file mode 100644 index 00000000000..e629910a080 Binary files /dev/null and b/Tests/images/tga/common/200x32_l_bl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_l_bl_rle.tga b/Tests/images/tga/common/200x32_l_bl_rle.tga new file mode 100644 index 00000000000..2e6f9377b75 Binary files /dev/null and b/Tests/images/tga/common/200x32_l_bl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_l_tl_raw.tga b/Tests/images/tga/common/200x32_l_tl_raw.tga new file mode 100644 index 00000000000..f9ed8b9c224 Binary files /dev/null and b/Tests/images/tga/common/200x32_l_tl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_l_tl_rle.tga b/Tests/images/tga/common/200x32_l_tl_rle.tga new file mode 100644 index 00000000000..03c797e537c Binary files /dev/null and b/Tests/images/tga/common/200x32_l_tl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_la.png b/Tests/images/tga/common/200x32_la.png new file mode 100644 index 00000000000..a8c4f274f85 Binary files /dev/null and b/Tests/images/tga/common/200x32_la.png differ diff --git a/Tests/images/tga/common/200x32_la_bl_raw.tga b/Tests/images/tga/common/200x32_la_bl_raw.tga new file mode 100644 index 00000000000..afdc9715113 Binary files /dev/null and b/Tests/images/tga/common/200x32_la_bl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_la_bl_rle.tga b/Tests/images/tga/common/200x32_la_bl_rle.tga new file mode 100644 index 00000000000..9fb8b06ab01 Binary files /dev/null and b/Tests/images/tga/common/200x32_la_bl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_la_tl_raw.tga b/Tests/images/tga/common/200x32_la_tl_raw.tga new file mode 100644 index 00000000000..6af1fa053b0 Binary files /dev/null and b/Tests/images/tga/common/200x32_la_tl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_la_tl_rle.tga b/Tests/images/tga/common/200x32_la_tl_rle.tga new file mode 100644 index 00000000000..fce83e3cf01 Binary files /dev/null and b/Tests/images/tga/common/200x32_la_tl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_p.png b/Tests/images/tga/common/200x32_p.png new file mode 100644 index 00000000000..a57a8a22af2 Binary files /dev/null and b/Tests/images/tga/common/200x32_p.png differ diff --git a/Tests/images/tga/common/200x32_p_bl_raw.tga b/Tests/images/tga/common/200x32_p_bl_raw.tga new file mode 100644 index 00000000000..89145aa8141 Binary files /dev/null and b/Tests/images/tga/common/200x32_p_bl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_p_bl_rle.tga b/Tests/images/tga/common/200x32_p_bl_rle.tga new file mode 100644 index 00000000000..bc53f2f9346 Binary files /dev/null and b/Tests/images/tga/common/200x32_p_bl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_p_tl_raw.tga b/Tests/images/tga/common/200x32_p_tl_raw.tga new file mode 100644 index 00000000000..247db20a232 Binary files /dev/null and b/Tests/images/tga/common/200x32_p_tl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_p_tl_rle.tga b/Tests/images/tga/common/200x32_p_tl_rle.tga new file mode 100644 index 00000000000..3092ff9236e Binary files /dev/null and b/Tests/images/tga/common/200x32_p_tl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_rgb.png b/Tests/images/tga/common/200x32_rgb.png new file mode 100644 index 00000000000..6614141a5c0 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgb.png differ diff --git a/Tests/images/tga/common/200x32_rgb_bl_raw.tga b/Tests/images/tga/common/200x32_rgb_bl_raw.tga new file mode 100644 index 00000000000..ebcea6b03e9 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgb_bl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_rgb_bl_rle.tga b/Tests/images/tga/common/200x32_rgb_bl_rle.tga new file mode 100644 index 00000000000..87eb71c75da Binary files /dev/null and b/Tests/images/tga/common/200x32_rgb_bl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_rgb_tl_raw.tga b/Tests/images/tga/common/200x32_rgb_tl_raw.tga new file mode 100644 index 00000000000..2122ffa1038 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgb_tl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_rgb_tl_rle.tga b/Tests/images/tga/common/200x32_rgb_tl_rle.tga new file mode 100644 index 00000000000..2122ffa1038 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgb_tl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_rgba.png b/Tests/images/tga/common/200x32_rgba.png new file mode 100644 index 00000000000..74def0b7c2d Binary files /dev/null and b/Tests/images/tga/common/200x32_rgba.png differ diff --git a/Tests/images/tga/common/200x32_rgba_bl_raw.tga b/Tests/images/tga/common/200x32_rgba_bl_raw.tga new file mode 100644 index 00000000000..148cc206a5d Binary files /dev/null and b/Tests/images/tga/common/200x32_rgba_bl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_rgba_bl_rle.tga b/Tests/images/tga/common/200x32_rgba_bl_rle.tga new file mode 100644 index 00000000000..1727fe338fc Binary files /dev/null and b/Tests/images/tga/common/200x32_rgba_bl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_rgba_tl_raw.tga b/Tests/images/tga/common/200x32_rgba_tl_raw.tga new file mode 100644 index 00000000000..92ab8940d4f Binary files /dev/null and b/Tests/images/tga/common/200x32_rgba_tl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_rgba_tl_rle.tga b/Tests/images/tga/common/200x32_rgba_tl_rle.tga new file mode 100644 index 00000000000..2b593aee2db Binary files /dev/null and b/Tests/images/tga/common/200x32_rgba_tl_rle.tga differ diff --git a/Tests/images/tga/common/readme.txt b/Tests/images/tga/common/readme.txt new file mode 100644 index 00000000000..4535d7fe617 --- /dev/null +++ b/Tests/images/tga/common/readme.txt @@ -0,0 +1,12 @@ +Images in this directory were created with GIMP. + +TGAs have names in the following format: + + {width}x{height}_{mode}_{origin}_{compression}.tga + +Where: + mode is PIL mode in lower case (L, P, RGB, etc.) + origin: + "bl" - bottom left + "tl" - top left + compression is either "raw" or "rle" diff --git a/Tests/images/tiff_16bit_RGB.tiff b/Tests/images/tiff_16bit_RGB.tiff new file mode 100644 index 00000000000..5eb7c73c245 Binary files /dev/null and b/Tests/images/tiff_16bit_RGB.tiff differ diff --git a/Tests/images/tiff_16bit_RGB_target.png b/Tests/images/tiff_16bit_RGB_target.png new file mode 100644 index 00000000000..9235800043b Binary files /dev/null and b/Tests/images/tiff_16bit_RGB_target.png differ diff --git a/Tests/images/tiff_overflow_rows_per_strip.tif b/Tests/images/tiff_overflow_rows_per_strip.tif new file mode 100644 index 00000000000..979c7f17696 Binary files /dev/null and b/Tests/images/tiff_overflow_rows_per_strip.tif differ diff --git a/Tests/images/tiff_strip_cmyk_16l_jpeg.tif b/Tests/images/tiff_strip_cmyk_16l_jpeg.tif new file mode 100644 index 00000000000..8bfd8bd6a89 Binary files /dev/null and b/Tests/images/tiff_strip_cmyk_16l_jpeg.tif differ diff --git a/Tests/images/tiff_strip_cmyk_jpeg.tif b/Tests/images/tiff_strip_cmyk_jpeg.tif new file mode 100644 index 00000000000..0207d27c74b Binary files /dev/null and b/Tests/images/tiff_strip_cmyk_jpeg.tif differ diff --git a/Tests/images/tiff_strip_planar_16bit_RGB.tiff b/Tests/images/tiff_strip_planar_16bit_RGB.tiff new file mode 100644 index 00000000000..360b4c16533 Binary files /dev/null and b/Tests/images/tiff_strip_planar_16bit_RGB.tiff differ diff --git a/Tests/images/tiff_strip_planar_16bit_RGBa.tiff b/Tests/images/tiff_strip_planar_16bit_RGBa.tiff new file mode 100644 index 00000000000..b8c3dcf6438 Binary files /dev/null and b/Tests/images/tiff_strip_planar_16bit_RGBa.tiff differ diff --git a/Tests/images/tiff_strip_planar_lzw.tiff b/Tests/images/tiff_strip_planar_lzw.tiff new file mode 100644 index 00000000000..8145703f430 Binary files /dev/null and b/Tests/images/tiff_strip_planar_lzw.tiff differ diff --git a/Tests/images/tiff_strip_planar_raw.tif b/Tests/images/tiff_strip_planar_raw.tif new file mode 100644 index 00000000000..ab8b3c3f329 Binary files /dev/null and b/Tests/images/tiff_strip_planar_raw.tif differ diff --git a/Tests/images/tiff_strip_planar_raw_with_overviews.tif b/Tests/images/tiff_strip_planar_raw_with_overviews.tif new file mode 100644 index 00000000000..e032c5c36f9 Binary files /dev/null and b/Tests/images/tiff_strip_planar_raw_with_overviews.tif differ diff --git a/Tests/images/tiff_strip_raw.tif b/Tests/images/tiff_strip_raw.tif new file mode 100644 index 00000000000..81bb42ce7dc Binary files /dev/null and b/Tests/images/tiff_strip_raw.tif differ diff --git a/Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif b/Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif new file mode 100644 index 00000000000..ca8b634bb32 Binary files /dev/null and b/Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif differ diff --git a/Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif b/Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif new file mode 100644 index 00000000000..c3207e451e6 Binary files /dev/null and b/Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif differ diff --git a/Tests/images/tiff_tiled_cmyk_jpeg.tif b/Tests/images/tiff_tiled_cmyk_jpeg.tif new file mode 100644 index 00000000000..0cc27b69cd9 Binary files /dev/null and b/Tests/images/tiff_tiled_cmyk_jpeg.tif differ diff --git a/Tests/images/tiff_tiled_planar_16bit_RGB.tiff b/Tests/images/tiff_tiled_planar_16bit_RGB.tiff new file mode 100644 index 00000000000..0376e90a7be Binary files /dev/null and b/Tests/images/tiff_tiled_planar_16bit_RGB.tiff differ diff --git a/Tests/images/tiff_tiled_planar_16bit_RGBa.tiff b/Tests/images/tiff_tiled_planar_16bit_RGBa.tiff new file mode 100644 index 00000000000..ae777386704 Binary files /dev/null and b/Tests/images/tiff_tiled_planar_16bit_RGBa.tiff differ diff --git a/Tests/images/tiff_tiled_planar_lzw.tiff b/Tests/images/tiff_tiled_planar_lzw.tiff new file mode 100644 index 00000000000..57cd6094a28 Binary files /dev/null and b/Tests/images/tiff_tiled_planar_lzw.tiff differ diff --git a/Tests/images/tiff_tiled_planar_raw.tif b/Tests/images/tiff_tiled_planar_raw.tif new file mode 100644 index 00000000000..2e3ecc81181 Binary files /dev/null and b/Tests/images/tiff_tiled_planar_raw.tif differ diff --git a/Tests/images/tiff_tiled_raw.tif b/Tests/images/tiff_tiled_raw.tif new file mode 100644 index 00000000000..25803c39576 Binary files /dev/null and b/Tests/images/tiff_tiled_raw.tif differ diff --git a/Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif b/Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif new file mode 100644 index 00000000000..75ce833a19a Binary files /dev/null and b/Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif differ diff --git a/Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif b/Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif new file mode 100644 index 00000000000..ff8b4a409ca Binary files /dev/null and b/Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif differ diff --git a/Tests/images/tiff_wrong_bits_per_sample_2.tiff b/Tests/images/tiff_wrong_bits_per_sample_2.tiff new file mode 100644 index 00000000000..d44176ce76c Binary files /dev/null and b/Tests/images/tiff_wrong_bits_per_sample_2.tiff differ diff --git a/Tests/images/tiff_wrong_bits_per_sample_3.tiff b/Tests/images/tiff_wrong_bits_per_sample_3.tiff new file mode 100644 index 00000000000..43526b157a5 Binary files /dev/null and b/Tests/images/tiff_wrong_bits_per_sample_3.tiff differ diff --git a/Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp b/Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp new file mode 100644 index 00000000000..97def320f3a Binary files /dev/null and b/Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp differ diff --git a/Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd b/Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd new file mode 100644 index 00000000000..63319e545a2 Binary files /dev/null and b/Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd differ diff --git a/Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp b/Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp new file mode 100644 index 00000000000..73022abfc4e Binary files /dev/null and b/Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp differ diff --git a/Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd b/Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd new file mode 100644 index 00000000000..c259a15e7f8 Binary files /dev/null and b/Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd differ diff --git a/Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp b/Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp new file mode 100644 index 00000000000..79e97dce357 Binary files /dev/null and b/Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp differ diff --git a/Tests/images/timeout-6646305047838720 b/Tests/images/timeout-6646305047838720 new file mode 100644 index 00000000000..eae1f333a03 Binary files /dev/null and b/Tests/images/timeout-6646305047838720 differ diff --git a/Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp b/Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp new file mode 100644 index 00000000000..9b9ecbcb077 Binary files /dev/null and b/Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp differ diff --git a/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli b/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli new file mode 100644 index 00000000000..ce4607d2dd0 Binary files /dev/null and b/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli differ diff --git a/Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp b/Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp new file mode 100644 index 00000000000..cb9a4e8b37f Binary files /dev/null and b/Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp differ diff --git a/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli b/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli new file mode 100644 index 00000000000..77a94b87a3a Binary files /dev/null and b/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli differ diff --git a/Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd b/Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd new file mode 100644 index 00000000000..955fc332522 Binary files /dev/null and b/Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd differ diff --git a/Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps b/Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps new file mode 100644 index 00000000000..5000ca9aa55 Binary files /dev/null and b/Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps differ diff --git a/Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp b/Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp new file mode 100644 index 00000000000..5044fbde107 Binary files /dev/null and b/Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp differ diff --git a/Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd b/Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd new file mode 100644 index 00000000000..c658ea45c4b Binary files /dev/null and b/Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd differ diff --git a/Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp b/Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp new file mode 100644 index 00000000000..7ef78eeec77 Binary files /dev/null and b/Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp differ diff --git a/Tests/images/tiny.png b/Tests/images/tiny.png new file mode 100644 index 00000000000..3d9ff56e7ef Binary files /dev/null and b/Tests/images/tiny.png differ diff --git a/Tests/images/transparent_background_text.png b/Tests/images/transparent_background_text.png new file mode 100644 index 00000000000..8ddd65cc68b Binary files /dev/null and b/Tests/images/transparent_background_text.png differ diff --git a/Tests/images/transparent_background_text_L.png b/Tests/images/transparent_background_text_L.png new file mode 100644 index 00000000000..d37de20a734 Binary files /dev/null and b/Tests/images/transparent_background_text_L.png differ diff --git a/Tests/images/transparent_dispose.gif b/Tests/images/transparent_dispose.gif new file mode 100644 index 00000000000..92b615543de Binary files /dev/null and b/Tests/images/transparent_dispose.gif differ diff --git a/Tests/images/truncated_app14.jpg b/Tests/images/truncated_app14.jpg new file mode 100644 index 00000000000..232a4c35f8c Binary files /dev/null and b/Tests/images/truncated_app14.jpg differ diff --git a/Tests/images/truncated_jpeg.jpg b/Tests/images/truncated_jpeg.jpg new file mode 100644 index 00000000000..f4fec450df9 Binary files /dev/null and b/Tests/images/truncated_jpeg.jpg differ diff --git a/Tests/images/uncompressed_rgb.dds b/Tests/images/uncompressed_rgb.dds new file mode 100755 index 00000000000..cd5189532f5 Binary files /dev/null and b/Tests/images/uncompressed_rgb.dds differ diff --git a/Tests/images/uncompressed_rgb.png b/Tests/images/uncompressed_rgb.png new file mode 100644 index 00000000000..f02b50f6f6f Binary files /dev/null and b/Tests/images/uncompressed_rgb.png differ diff --git a/Tests/images/unicode_extended.png b/Tests/images/unicode_extended.png new file mode 100644 index 00000000000..c0ffad3c69e Binary files /dev/null and b/Tests/images/unicode_extended.png differ diff --git a/Tests/images/unimplemented_dxgi_format.dds b/Tests/images/unimplemented_dxgi_format.dds new file mode 100644 index 00000000000..70860f2fcc4 Binary files /dev/null and b/Tests/images/unimplemented_dxgi_format.dds differ diff --git a/Tests/images/unimplemented_pixel_format.dds b/Tests/images/unimplemented_pixel_format.dds new file mode 100755 index 00000000000..41a34388615 Binary files /dev/null and b/Tests/images/unimplemented_pixel_format.dds differ diff --git a/Tests/images/variation_adobe.png b/Tests/images/variation_adobe.png new file mode 100644 index 00000000000..e9cfafb48b5 Binary files /dev/null and b/Tests/images/variation_adobe.png differ diff --git a/Tests/images/variation_adobe_axes.png b/Tests/images/variation_adobe_axes.png new file mode 100644 index 00000000000..ad3a3a96088 Binary files /dev/null and b/Tests/images/variation_adobe_axes.png differ diff --git a/Tests/images/variation_adobe_name.png b/Tests/images/variation_adobe_name.png new file mode 100644 index 00000000000..5168e04b99b Binary files /dev/null and b/Tests/images/variation_adobe_name.png differ diff --git a/Tests/images/variation_adobe_older_harfbuzz.png b/Tests/images/variation_adobe_older_harfbuzz.png new file mode 100644 index 00000000000..5abc907caab Binary files /dev/null and b/Tests/images/variation_adobe_older_harfbuzz.png differ diff --git a/Tests/images/variation_adobe_older_harfbuzz_axes.png b/Tests/images/variation_adobe_older_harfbuzz_axes.png new file mode 100644 index 00000000000..b39d460f977 Binary files /dev/null and b/Tests/images/variation_adobe_older_harfbuzz_axes.png differ diff --git a/Tests/images/variation_adobe_older_harfbuzz_name.png b/Tests/images/variation_adobe_older_harfbuzz_name.png new file mode 100644 index 00000000000..fa0e307b4f6 Binary files /dev/null and b/Tests/images/variation_adobe_older_harfbuzz_name.png differ diff --git a/Tests/images/variation_tiny.png b/Tests/images/variation_tiny.png new file mode 100644 index 00000000000..a0ff3f5946e Binary files /dev/null and b/Tests/images/variation_tiny.png differ diff --git a/Tests/images/variation_tiny_axes.png b/Tests/images/variation_tiny_axes.png new file mode 100644 index 00000000000..8cb6d1f62a4 Binary files /dev/null and b/Tests/images/variation_tiny_axes.png differ diff --git a/Tests/images/variation_tiny_name.png b/Tests/images/variation_tiny_name.png new file mode 100644 index 00000000000..69f1550dbfc Binary files /dev/null and b/Tests/images/variation_tiny_name.png differ diff --git a/Tests/images/xmp_tags_orientation.png b/Tests/images/xmp_tags_orientation.png new file mode 100644 index 00000000000..c1be1665fa7 Binary files /dev/null and b/Tests/images/xmp_tags_orientation.png differ diff --git a/Tests/images/xmp_tags_orientation_exiftool.png b/Tests/images/xmp_tags_orientation_exiftool.png new file mode 100644 index 00000000000..10f0f44009a Binary files /dev/null and b/Tests/images/xmp_tags_orientation_exiftool.png differ diff --git a/Tests/images/xmp_test.jpg b/Tests/images/xmp_test.jpg new file mode 100644 index 00000000000..4b9354f3a17 Binary files /dev/null and b/Tests/images/xmp_test.jpg differ diff --git a/Tests/images/zero_dpi.jp2 b/Tests/images/zero_dpi.jp2 new file mode 100644 index 00000000000..079271fc6d8 Binary files /dev/null and b/Tests/images/zero_dpi.jp2 differ diff --git a/Tests/images/zero_height.j2k b/Tests/images/zero_height.j2k new file mode 100644 index 00000000000..21bdd8f7667 Binary files /dev/null and b/Tests/images/zero_height.j2k differ diff --git a/Tests/import_all.py b/Tests/import_all.py deleted file mode 100644 index 11682237b28..00000000000 --- a/Tests/import_all.py +++ /dev/null @@ -1,16 +0,0 @@ -from __future__ import print_function - -import glob -import os -import traceback - -import sys -sys.path.insert(0, ".") - -for file in glob.glob("src/PIL/*.py"): - module = os.path.basename(file)[:-3] - try: - exec("from PIL import " + module) - except (ImportError, SyntaxError): - print("===", "failed to import", module) - traceback.print_exc() diff --git a/Tests/make_hash.py b/Tests/make_hash.py deleted file mode 100644 index 4412f65be53..00000000000 --- a/Tests/make_hash.py +++ /dev/null @@ -1,60 +0,0 @@ -# brute-force search for access descriptor hash table - -from __future__ import print_function - -modes = [ - "1", - "L", "LA", "La", - "I", "I;16", "I;16L", "I;16B", "I;32L", "I;32B", - "F", - "P", "PA", - "RGB", "RGBA", "RGBa", "RGBX", - "CMYK", - "YCbCr", - "LAB", "HSV", - ] - - -def hash(s, i): - # djb2 hash: multiply by 33 and xor character - for c in s: - i = (((i << 5) + i) ^ ord(c)) & 0xffffffff - return i - - -def check(size, i0): - h = [None] * size - for m in modes: - i = hash(m, i0) - i = i % size - if h[i]: - return 0 - h[i] = m - return h - -min_start = 0 - -# 1) find the smallest table size with no collisions -for min_size in range(len(modes), 16384): - if check(min_size, 0): - print(len(modes), "modes fit in", min_size, "slots") - break - -# 2) see if we can do better with a different initial value -for i0 in range(65556): - for size in range(1, min_size): - if check(size, i0): - if size < min_size: - print(len(modes), "modes fit in", size, "slots with start", i0) - min_size = size - min_start = i0 - -print() - -# print(check(min_size, min_start)) - -print("#define ACCESS_TABLE_SIZE", min_size) -print("#define ACCESS_TABLE_HASH", min_start) - -# for m in modes: -# print(m, "=>", hash(m, min_start) % min_size) diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh new file mode 100755 index 00000000000..09cc7bc1696 --- /dev/null +++ b/Tests/oss-fuzz/build.sh @@ -0,0 +1,48 @@ +#!/bin/bash -eu +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +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 \ + --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/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 +done + +find Tests/images Tests/icc -print | zip -q $OUT/fuzz_pillow_seed_corpus.zip -@ +find Tests/fonts -print | zip -q $OUT/fuzz_font_seed_corpus.zip -@ diff --git a/Tests/oss-fuzz/build_dictionaries.sh b/Tests/oss-fuzz/build_dictionaries.sh new file mode 100755 index 00000000000..9aae56ca8d1 --- /dev/null +++ b/Tests/oss-fuzz/build_dictionaries.sh @@ -0,0 +1,33 @@ +#!/bin/bash -eu +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +# Generate image dictionaries here for each of the fuzzers and put them in the +# $OUT directory, named for the fuzzer + +git clone --depth 1 https://github.com/google/fuzzing +cat fuzzing/dictionaries/bmp.dict \ + fuzzing/dictionaries/dds.dict \ + fuzzing/dictionaries/gif.dict \ + fuzzing/dictionaries/icns.dict \ + fuzzing/dictionaries/jpeg.dict \ + fuzzing/dictionaries/jpeg2000.dict \ + fuzzing/dictionaries/pbm.dict \ + fuzzing/dictionaries/png.dict \ + fuzzing/dictionaries/psd.dict \ + fuzzing/dictionaries/tiff.dict \ + fuzzing/dictionaries/webp.dict \ + > $OUT/fuzz_pillow.dict diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py new file mode 100755 index 00000000000..bc2ba9a7e27 --- /dev/null +++ b/Tests/oss-fuzz/fuzz_font.py @@ -0,0 +1,43 @@ +#!/usr/bin/python3 + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import atheris + +with atheris.instrument_imports(): + import sys + + import fuzzers + + +def TestOneInput(data): + try: + fuzzers.fuzz_font(data) + except Exception: + # We're catching all exceptions because Pillow's exceptions are + # directly inheriting from Exception. + pass + + +def main(): + fuzzers.enable_decompressionbomb_error() + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz() + fuzzers.disable_decompressionbomb_error() + + +if __name__ == "__main__": + main() diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py new file mode 100644 index 00000000000..545daccb680 --- /dev/null +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -0,0 +1,43 @@ +#!/usr/bin/python3 + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import atheris + +with atheris.instrument_imports(): + import sys + + import fuzzers + + +def TestOneInput(data): + try: + fuzzers.fuzz_image(data) + except Exception: + # We're catching all exceptions because Pillow's exceptions are + # directly inheriting from Exception. + pass + + +def main(): + fuzzers.enable_decompressionbomb_error() + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz() + fuzzers.disable_decompressionbomb_error() + + +if __name__ == "__main__": + main() diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py new file mode 100644 index 00000000000..10a172b4675 --- /dev/null +++ b/Tests/oss-fuzz/fuzzers.py @@ -0,0 +1,41 @@ +import io +import warnings + +from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont + + +def enable_decompressionbomb_error(): + ImageFile.LOAD_TRUNCATED_IMAGES = True + warnings.filterwarnings("ignore") + warnings.simplefilter("error", Image.DecompressionBombWarning) + + +def disable_decompressionbomb_error(): + ImageFile.LOAD_TRUNCATED_IMAGES = False + warnings.resetwarnings() + + +def fuzz_image(data): + # This will fail on some images in the corpus, as we have many + # invalid images in the test suite. + with Image.open(io.BytesIO(data)) as im: + im.rotate(45) + im.filter(ImageFilter.DETAIL) + im.save(io.BytesIO(), "BMP") + + +def fuzz_font(data): + wrapper = io.BytesIO(data) + try: + font = ImageFont.truetype(wrapper) + except OSError: + # Catch pcf/pilfonts/random garbage here. They return + # different font objects. + return + + font.getbbox("ABC") + font.getmask("test text") + with Image.new(mode="RGBA", size=(200, 200)) as im: + draw = ImageDraw.Draw(im) + draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2) + draw.text((10, 10), "Test Text", font=font, fill="#000") diff --git a/Tests/oss-fuzz/python.supp b/Tests/oss-fuzz/python.supp new file mode 100644 index 00000000000..94cc87db97d --- /dev/null +++ b/Tests/oss-fuzz/python.supp @@ -0,0 +1,16 @@ +{ + + Memcheck:Cond + ... + fun:encode_current_locale +} + + +{ + + Memcheck:Cond + fun:inflate + fun:ZIPDecode + fun:_TIFFReadEncodedTileAndAllocBuffer + ... +} diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py new file mode 100644 index 00000000000..629e9ac00d4 --- /dev/null +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -0,0 +1,62 @@ +import subprocess +import sys + +import fuzzers +import packaging +import pytest + +from PIL import Image, features + +if sys.platform.startswith("win32"): + pytest.skip("Fuzzer is linux only", allow_module_level=True) +if features.check("libjpeg_turbo"): + version = packaging.version.parse(features.version("libjpeg_turbo")) + if version.major == 2 and version.minor == 0: + pytestmark = pytest.mark.valgrind_known_error( + reason="Known failing with libjpeg_turbo 2.0" + ) + + +@pytest.mark.parametrize( + "path", + subprocess.check_output("find Tests/images -type f", shell=True).split(b"\n"), +) +def test_fuzz_images(path): + fuzzers.enable_decompressionbomb_error() + try: + with open(path, "rb") as f: + fuzzers.fuzz_image(f.read()) + assert True + except ( + OSError, + SyntaxError, + MemoryError, + ValueError, + NotImplementedError, + OverflowError, + ): + # Known exceptions that are through from Pillow + assert True + except ( + Image.DecompressionBombError, + Image.DecompressionBombWarning, + Image.UnidentifiedImageError, + ): + # Known Image.* exceptions + assert True + finally: + fuzzers.disable_decompressionbomb_error() + + +@pytest.mark.parametrize( + "path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n") +) +def test_fuzz_fonts(path): + if not path: + return + with open(path, "rb") as f: + try: + fuzzers.fuzz_font(f.read()) + except (Image.DecompressionBombError, Image.DecompressionBombWarning): + pass + assert True diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py index 67aff8ecc87..3fd982474d4 100644 --- a/Tests/test_000_sanity.py +++ b/Tests/test_000_sanity.py @@ -1,30 +1,18 @@ -from helper import unittest, PillowTestCase +from PIL import Image -import PIL -import PIL.Image +def test_sanity(): + # Make sure we have the binary extension + Image.core.new("L", (100, 100)) -class TestSanity(PillowTestCase): + # Create an image and do stuff with it. + im = Image.new("1", (100, 100)) + assert (im.mode, im.size) == ("1", (100, 100)) + assert len(im.tobytes()) == 1300 - def test_sanity(self): - - # Make sure we have the binary extension - im = PIL.Image.core.new("L", (100, 100)) - - self.assertEqual(PIL.Image.VERSION[:3], '1.1') - - # Create an image and do stuff with it. - im = PIL.Image.new("1", (100, 100)) - self.assertEqual((im.mode, im.size), ('1', (100, 100))) - self.assertEqual(len(im.tobytes()), 1300) - - # Create images in all remaining major modes. - im = PIL.Image.new("L", (100, 100)) - im = PIL.Image.new("P", (100, 100)) - im = PIL.Image.new("RGB", (100, 100)) - im = PIL.Image.new("I", (100, 100)) - im = PIL.Image.new("F", (100, 100)) - - -if __name__ == '__main__': - unittest.main() + # Create images in all remaining major modes. + Image.new("L", (100, 100)) + Image.new("P", (100, 100)) + Image.new("RGB", (100, 100)) + Image.new("I", (100, 100)) + Image.new("F", (100, 100)) diff --git a/Tests/test_binary.py b/Tests/test_binary.py index 2fac9b3d591..4882e65e655 100644 --- a/Tests/test_binary.py +++ b/Tests/test_binary.py @@ -1,27 +1,22 @@ -from helper import unittest, PillowTestCase - from PIL import _binary -class TestBinary(PillowTestCase): +def test_standard(): + assert _binary.i8(b"*") == 42 + assert _binary.o8(42) == b"*" - def test_standard(self): - self.assertEqual(_binary.i8(b'*'), 42) - self.assertEqual(_binary.o8(42), b'*') - def test_little_endian(self): - self.assertEqual(_binary.i16le(b'\xff\xff\x00\x00'), 65535) - self.assertEqual(_binary.i32le(b'\xff\xff\x00\x00'), 65535) +def test_little_endian(): + assert _binary.i16le(b"\xff\xff\x00\x00") == 65535 + assert _binary.i32le(b"\xff\xff\x00\x00") == 65535 - self.assertEqual(_binary.o16le(65535), b'\xff\xff') - self.assertEqual(_binary.o32le(65535), b'\xff\xff\x00\x00') + assert _binary.o16le(65535) == b"\xff\xff" + assert _binary.o32le(65535) == b"\xff\xff\x00\x00" - def test_big_endian(self): - self.assertEqual(_binary.i16be(b'\x00\x00\xff\xff'), 0) - self.assertEqual(_binary.i32be(b'\x00\x00\xff\xff'), 65535) - self.assertEqual(_binary.o16be(65535), b'\xff\xff') - self.assertEqual(_binary.o32be(65535), b'\x00\x00\xff\xff') +def test_big_endian(): + assert _binary.i16be(b"\x00\x00\xff\xff") == 0 + assert _binary.i32be(b"\x00\x00\xff\xff") == 65535 -if __name__ == '__main__': - unittest.main() + assert _binary.o16be(65535) == b"\xff\xff" + assert _binary.o32be(65535) == b"\x00\x00\xff\xff" diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 8e84cc8f19d..b17aad2ea50 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -1,106 +1,109 @@ -from __future__ import print_function -from helper import unittest, PillowTestCase +import os +import warnings from PIL import Image -import os -base = os.path.join('Tests', 'images', 'bmp') +from .helper import assert_image_similar + +base = os.path.join("Tests", "images", "bmp") + +def get_files(d, ext=".bmp"): + return [ + os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f + ] -class TestBmpReference(PillowTestCase): - def get_files(self, d, ext='.bmp'): - return [os.path.join(base, d, f) for f - in os.listdir(os.path.join(base, d)) if ext in f] +def test_bad(): + """These shouldn't crash/dos, but they shouldn't return anything + either""" + for f in get_files("b"): - def test_bad(self): - """ These shouldn't crash/dos, but they shouldn't return anything - either """ - for f in self.get_files('b'): + # Assert that there is no unclosed file warning + with warnings.catch_warnings(): try: - im = Image.open(f) - im.load() + with Image.open(f) as im: + im.load() except Exception: # as msg: pass - # print("Bad Image %s: %s" %(f,msg)) - - def test_questionable(self): - """ These shouldn't crash/dos, but it's not well defined that these - are in spec """ - supported = [ - "pal8os2v2.bmp", - "rgb24prof.bmp", - "pal1p1.bmp", - "pal8offs.bmp", - "rgb24lprof.bmp", - "rgb32fakealpha.bmp", - "rgb24largepal.bmp", - "pal8os2sp.bmp", - "rgb32bf-xbgr.bmp", - ] - for f in self.get_files('q'): - try: - im = Image.open(f) + + +def test_questionable(): + """These shouldn't crash/dos, but it's not well defined that these + are in spec""" + supported = [ + "pal8os2v2.bmp", + "rgb24prof.bmp", + "pal1p1.bmp", + "pal8offs.bmp", + "rgb24lprof.bmp", + "rgb32fakealpha.bmp", + "rgb24largepal.bmp", + "pal8os2sp.bmp", + "pal8rletrns.bmp", + "rgb32bf-xbgr.bmp", + ] + for f in get_files("q"): + try: + with Image.open(f) as im: im.load() - if os.path.basename(f) not in supported: - print("Please add %s to the partially supported bmp specs." % f) - except Exception: # as msg: - if os.path.basename(f) in supported: - raise - # print("Bad Image %s: %s" %(f,msg)) - - def test_good(self): - """ These should all work. There's a set of target files in the - html directory that we can compare against. """ - - # Target files, if they're not just replacing the extension - file_map = {'pal1wb.bmp': 'pal1.png', - 'pal4rle.bmp': 'pal4.png', - 'pal8-0.bmp': 'pal8.png', - 'pal8rle.bmp': 'pal8.png', - 'pal8topdown.bmp': 'pal8.png', - 'pal8nonsquare.bmp': 'pal8nonsquare-v.png', - 'pal8os2.bmp': 'pal8.png', - 'pal8os2sp.bmp': 'pal8.png', - 'pal8os2v2.bmp': 'pal8.png', - 'pal8os2v2-16.bmp': 'pal8.png', - 'pal8v4.bmp': 'pal8.png', - 'pal8v5.bmp': 'pal8.png', - 'rgb16-565pal.bmp': 'rgb16-565.png', - 'rgb24pal.bmp': 'rgb24.png', - 'rgb32.bmp': 'rgb24.png', - 'rgb32bf.bmp': 'rgb24.png' - } - - def get_compare(f): - name = os.path.split(f)[1] - if name in file_map: - return os.path.join(base, 'html', file_map[name]) - name = os.path.splitext(name)[0] - return os.path.join(base, 'html', "%s.png" % name) - - for f in self.get_files('g'): - try: - im = Image.open(f) + if os.path.basename(f) not in supported: + print(f"Please add {f} to the partially supported bmp specs.") + except Exception: # as msg: + if os.path.basename(f) in supported: + raise + + +def test_good(): + """These should all work. There's a set of target files in the + html directory that we can compare against.""" + + # Target files, if they're not just replacing the extension + file_map = { + "pal1wb.bmp": "pal1.png", + "pal4rle.bmp": "pal4.png", + "pal8-0.bmp": "pal8.png", + "pal8rle.bmp": "pal8.png", + "pal8topdown.bmp": "pal8.png", + "pal8nonsquare.bmp": "pal8nonsquare-v.png", + "pal8os2.bmp": "pal8.png", + "pal8os2sp.bmp": "pal8.png", + "pal8os2v2.bmp": "pal8.png", + "pal8os2v2-16.bmp": "pal8.png", + "pal8v4.bmp": "pal8.png", + "pal8v5.bmp": "pal8.png", + "rgb16-565pal.bmp": "rgb16-565.png", + "rgb24pal.bmp": "rgb24.png", + "rgb32.bmp": "rgb24.png", + "rgb32bf.bmp": "rgb24.png", + } + + def get_compare(f): + name = os.path.split(f)[1] + if name in file_map: + return os.path.join(base, "html", file_map[name]) + name = os.path.splitext(name)[0] + return os.path.join(base, "html", f"{name}.png") + + for f in get_files("g"): + try: + with Image.open(f) as im: im.load() - compare = Image.open(get_compare(f)) - compare.load() - if im.mode == 'P': - # assert image similar doesn't really work - # with paletized image, since the palette might - # be differently ordered for an equivalent image. - im = im.convert('RGBA') - compare = im.convert('RGBA') - self.assert_image_similar(im, compare, 5) - - except Exception as msg: - # there are three here that are unsupported: - unsupported = (os.path.join(base, 'g', 'rgb32bf.bmp'), - os.path.join(base, 'g', 'pal8rle.bmp'), - os.path.join(base, 'g', 'pal4rle.bmp')) - if f not in unsupported: - self.fail("Unsupported Image %s: %s" % (f, msg)) - - -if __name__ == '__main__': - unittest.main() + with Image.open(get_compare(f)) as compare: + compare.load() + if im.mode == "P": + # assert image similar doesn't really work + # with paletized image, since the palette might + # be differently ordered for an equivalent image. + im = im.convert("RGBA") + compare = im.convert("RGBA") + assert_image_similar(im, compare, 5) + + except Exception as msg: + # there are three here that are unsupported: + unsupported = ( + os.path.join(base, "g", "rgb32bf.bmp"), + os.path.join(base, "g", "pal8rle.bmp"), + os.path.join(base, "g", "pal4rle.bmp"), + ) + assert f in unsupported, f"Unsupported Image {f}: {msg}" diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index 622b842d004..3bdd5177d3f 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -1,9 +1,9 @@ -from helper import unittest, PillowTestCase - -from PIL import Image, ImageOps +import pytest +from PIL import Image, ImageFilter sample = Image.new("L", (7, 5)) +# fmt: off sample.putdata(sum([ [210, 50, 20, 10, 220, 230, 80], [190, 210, 20, 180, 170, 40, 110], @@ -11,211 +11,254 @@ [220, 40, 230, 80, 130, 250, 40], [250, 0, 80, 30, 60, 20, 110], ], [])) +# fmt: on + + +def test_imageops_box_blur(): + i = sample.filter(ImageFilter.BoxBlur(1)) + assert i.mode == sample.mode + assert i.size == sample.size + assert isinstance(i, Image.Image) + + +def box_blur(image, radius=1, n=1): + return image._new(image.im.box_blur(radius, n)) + + +def assert_image(im, data, delta=0): + it = iter(im.getdata()) + for data_row in data: + im_row = [next(it) for _ in range(im.size[0])] + if any(abs(data_v - im_v) > delta for data_v, im_v in zip(data_row, im_row)): + assert im_row == data_row + with pytest.raises(StopIteration): + next(it) + + +def assert_blur(im, radius, data, passes=1, delta=0): + # check grayscale image + assert_image(box_blur(im, radius, passes), data, delta) + rgba = Image.merge("RGBA", (im, im, im, im)) + for band in box_blur(rgba, radius, passes).split(): + assert_image(band, data, delta) + + +def test_color_modes(): + with pytest.raises(ValueError): + box_blur(sample.convert("1")) + with pytest.raises(ValueError): + box_blur(sample.convert("P")) + box_blur(sample.convert("L")) + box_blur(sample.convert("LA")) + box_blur(sample.convert("LA").convert("La")) + with pytest.raises(ValueError): + box_blur(sample.convert("I")) + with pytest.raises(ValueError): + box_blur(sample.convert("F")) + box_blur(sample.convert("RGB")) + box_blur(sample.convert("RGBA")) + box_blur(sample.convert("RGBA").convert("RGBa")) + box_blur(sample.convert("CMYK")) + with pytest.raises(ValueError): + box_blur(sample.convert("YCbCr")) + + +def test_radius_0(): + assert_blur( + sample, + 0, + [ + # fmt: off + [210, 50, 20, 10, 220, 230, 80], + [190, 210, 20, 180, 170, 40, 110], + [120, 210, 250, 60, 220, 0, 220], + [220, 40, 230, 80, 130, 250, 40], + [250, 0, 80, 30, 60, 20, 110], + # fmt: on + ], + ) + + +def test_radius_0_02(): + assert_blur( + sample, + 0.02, + [ + # fmt: off + [206, 55, 20, 17, 215, 223, 83], + [189, 203, 31, 171, 169, 46, 110], + [125, 206, 241, 69, 210, 13, 210], + [215, 49, 221, 82, 131, 235, 48], + [244, 7, 80, 32, 60, 27, 107], + # fmt: on + ], + delta=2, + ) + + +def test_radius_0_05(): + assert_blur( + sample, + 0.05, + [ + # fmt: off + [202, 62, 22, 27, 209, 215, 88], + [188, 194, 44, 161, 168, 56, 111], + [131, 201, 229, 81, 198, 31, 198], + [209, 62, 209, 86, 133, 216, 59], + [237, 17, 80, 36, 60, 35, 103], + # fmt: on + ], + delta=2, + ) + + +def test_radius_0_1(): + assert_blur( + sample, + 0.1, + [ + # fmt: off + [196, 72, 24, 40, 200, 203, 93], + [187, 183, 62, 148, 166, 68, 111], + [139, 193, 213, 96, 182, 54, 182], + [201, 78, 193, 91, 133, 191, 73], + [227, 31, 80, 42, 61, 47, 99], + # fmt: on + ], + delta=1, + ) + + +def test_radius_0_5(): + assert_blur( + sample, + 0.5, + [ + # fmt: off + [176, 101, 46, 83, 163, 165, 111], + [176, 149, 108, 122, 144, 120, 117], + [164, 171, 159, 141, 134, 119, 129], + [170, 136, 133, 114, 116, 124, 109], + [184, 95, 72, 70, 69, 81, 89], + # fmt: on + ], + delta=1, + ) + + +def test_radius_1(): + assert_blur( + sample, + 1, + [ + # fmt: off + [170, 109, 63, 97, 146, 153, 116], + [168, 142, 112, 128, 126, 143, 121], + [169, 166, 142, 149, 126, 131, 114], + [159, 156, 109, 127, 94, 117, 112], + [164, 128, 63, 87, 76, 89, 90], + # fmt: on + ], + delta=1, + ) + + +def test_radius_1_5(): + assert_blur( + sample, + 1.5, + [ + # fmt: off + [155, 120, 105, 112, 124, 137, 130], + [160, 136, 124, 125, 127, 134, 130], + [166, 147, 130, 125, 120, 121, 119], + [168, 145, 119, 109, 103, 105, 110], + [168, 134, 96, 85, 85, 89, 97], + # fmt: on + ], + delta=1, + ) + + +def test_radius_bigger_then_half(): + assert_blur( + sample, + 3, + [ + # fmt: off + [144, 145, 142, 128, 114, 115, 117], + [148, 145, 137, 122, 109, 111, 112], + [152, 145, 131, 117, 103, 107, 108], + [156, 144, 126, 111, 97, 102, 103], + [160, 144, 121, 106, 92, 98, 99], + # fmt: on + ], + delta=1, + ) + + +def test_radius_bigger_then_width(): + assert_blur( + sample, + 10, + [ + [158, 153, 147, 141, 135, 129, 123], + [159, 153, 147, 141, 136, 130, 124], + [159, 154, 148, 142, 136, 130, 124], + [160, 154, 148, 142, 137, 131, 125], + [160, 155, 149, 143, 137, 131, 125], + ], + delta=0, + ) + + +def test_extreme_large_radius(): + assert_blur( + sample, + 600, + [ + [162, 162, 162, 162, 162, 162, 162], + [162, 162, 162, 162, 162, 162, 162], + [162, 162, 162, 162, 162, 162, 162], + [162, 162, 162, 162, 162, 162, 162], + [162, 162, 162, 162, 162, 162, 162], + ], + delta=1, + ) + + +def test_two_passes(): + assert_blur( + sample, + 1, + [ + # fmt: off + [153, 123, 102, 109, 132, 135, 129], + [159, 138, 123, 121, 133, 131, 126], + [162, 147, 136, 124, 127, 121, 121], + [159, 140, 125, 108, 111, 106, 108], + [154, 126, 105, 87, 94, 93, 97], + # fmt: on + ], + passes=2, + delta=1, + ) -class TestBoxBlurApi(PillowTestCase): - - def test_imageops_box_blur(self): - i = ImageOps.box_blur(sample, 1) - self.assertEqual(i.mode, sample.mode) - self.assertEqual(i.size, sample.size) - self.assertIsInstance(i, Image.Image) - - -class TestBoxBlur(PillowTestCase): - - def box_blur(self, image, radius=1, n=1): - return image._new(image.im.box_blur(radius, n)) - - def assertImage(self, im, data, delta=0): - it = iter(im.getdata()) - for data_row in data: - im_row = [next(it) for _ in range(im.size[0])] - if any( - abs(data_v - im_v) > delta - for data_v, im_v in zip(data_row, im_row) - ): - self.assertEqual(im_row, data_row) - self.assertRaises(StopIteration, next, it) - - def assertBlur(self, im, radius, data, passes=1, delta=0): - # check grayscale image - self.assertImage(self.box_blur(im, radius, passes), data, delta) - rgba = Image.merge('RGBA', (im, im, im, im)) - for band in self.box_blur(rgba, radius, passes).split(): - self.assertImage(band, data, delta) - - def test_color_modes(self): - self.assertRaises(ValueError, self.box_blur, sample.convert("1")) - self.assertRaises(ValueError, self.box_blur, sample.convert("P")) - self.box_blur(sample.convert("L")) - self.box_blur(sample.convert("LA")) - self.box_blur(sample.convert("LA").convert("La")) - self.assertRaises(ValueError, self.box_blur, sample.convert("I")) - self.assertRaises(ValueError, self.box_blur, sample.convert("F")) - self.box_blur(sample.convert("RGB")) - self.box_blur(sample.convert("RGBA")) - self.box_blur(sample.convert("RGBA").convert("RGBa")) - self.box_blur(sample.convert("CMYK")) - self.assertRaises(ValueError, self.box_blur, sample.convert("YCbCr")) - - def test_radius_0(self): - self.assertBlur( - sample, 0, - [ - [210, 50, 20, 10, 220, 230, 80], - [190, 210, 20, 180, 170, 40, 110], - [120, 210, 250, 60, 220, 0, 220], - [220, 40, 230, 80, 130, 250, 40], - [250, 0, 80, 30, 60, 20, 110], - ] - ) - - def test_radius_0_02(self): - self.assertBlur( - sample, 0.02, - [ - [206, 55, 20, 17, 215, 223, 83], - [189, 203, 31, 171, 169, 46, 110], - [125, 206, 241, 69, 210, 13, 210], - [215, 49, 221, 82, 131, 235, 48], - [244, 7, 80, 32, 60, 27, 107], - ], - delta=2, - ) - - def test_radius_0_05(self): - self.assertBlur( - sample, 0.05, - [ - [202, 62, 22, 27, 209, 215, 88], - [188, 194, 44, 161, 168, 56, 111], - [131, 201, 229, 81, 198, 31, 198], - [209, 62, 209, 86, 133, 216, 59], - [237, 17, 80, 36, 60, 35, 103], - ], - delta=2, - ) - - def test_radius_0_1(self): - self.assertBlur( - sample, 0.1, - [ - [196, 72, 24, 40, 200, 203, 93], - [187, 183, 62, 148, 166, 68, 111], - [139, 193, 213, 96, 182, 54, 182], - [201, 78, 193, 91, 133, 191, 73], - [227, 31, 80, 42, 61, 47, 99], - ], - delta=1, - ) - - def test_radius_0_5(self): - self.assertBlur( - sample, 0.5, - [ - [176, 101, 46, 83, 163, 165, 111], - [176, 149, 108, 122, 144, 120, 117], - [164, 171, 159, 141, 134, 119, 129], - [170, 136, 133, 114, 116, 124, 109], - [184, 95, 72, 70, 69, 81, 89], - ], - delta=1, - ) - - def test_radius_1(self): - self.assertBlur( - sample, 1, - [ - [170, 109, 63, 97, 146, 153, 116], - [168, 142, 112, 128, 126, 143, 121], - [169, 166, 142, 149, 126, 131, 114], - [159, 156, 109, 127, 94, 117, 112], - [164, 128, 63, 87, 76, 89, 90], - ], - delta=1, - ) - - def test_radius_1_5(self): - self.assertBlur( - sample, 1.5, - [ - [155, 120, 105, 112, 124, 137, 130], - [160, 136, 124, 125, 127, 134, 130], - [166, 147, 130, 125, 120, 121, 119], - [168, 145, 119, 109, 103, 105, 110], - [168, 134, 96, 85, 85, 89, 97], - ], - delta=1, - ) - - def test_radius_bigger_then_half(self): - self.assertBlur( - sample, 3, - [ - [144, 145, 142, 128, 114, 115, 117], - [148, 145, 137, 122, 109, 111, 112], - [152, 145, 131, 117, 103, 107, 108], - [156, 144, 126, 111, 97, 102, 103], - [160, 144, 121, 106, 92, 98, 99], - ], - delta=1, - ) - - def test_radius_bigger_then_width(self): - self.assertBlur( - sample, 10, - [ - [158, 153, 147, 141, 135, 129, 123], - [159, 153, 147, 141, 136, 130, 124], - [159, 154, 148, 142, 136, 130, 124], - [160, 154, 148, 142, 137, 131, 125], - [160, 155, 149, 143, 137, 131, 125], - ], - delta=0, - ) - - def test_exteme_large_radius(self): - self.assertBlur( - sample, 600, - [ - [162, 162, 162, 162, 162, 162, 162], - [162, 162, 162, 162, 162, 162, 162], - [162, 162, 162, 162, 162, 162, 162], - [162, 162, 162, 162, 162, 162, 162], - [162, 162, 162, 162, 162, 162, 162], - ], - delta=1, - ) - - def test_two_passes(self): - self.assertBlur( - sample, 1, - [ - [153, 123, 102, 109, 132, 135, 129], - [159, 138, 123, 121, 133, 131, 126], - [162, 147, 136, 124, 127, 121, 121], - [159, 140, 125, 108, 111, 106, 108], - [154, 126, 105, 87, 94, 93, 97], - ], - passes=2, - delta=1, - ) - - def test_three_passes(self): - self.assertBlur( - sample, 1, - [ - [146, 131, 116, 118, 126, 131, 130], - [151, 138, 125, 123, 126, 128, 127], - [154, 143, 129, 123, 120, 120, 119], - [152, 139, 122, 113, 108, 108, 108], - [148, 132, 112, 102, 97, 99, 100], - ], - passes=3, - delta=1, - ) - - -if __name__ == '__main__': - unittest.main() +def test_three_passes(): + assert_blur( + sample, + 1, + [ + # fmt: off + [146, 131, 116, 118, 126, 131, 130], + [151, 138, 125, 123, 126, 128, 127], + [154, 143, 129, 123, 120, 120, 119], + [152, 139, 122, 113, 108, 108, 108], + [148, 132, 112, 102, 97, 99, 100], + # fmt: on + ], + passes=3, + delta=1, + ) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py new file mode 100644 index 00000000000..6d9a6057029 --- /dev/null +++ b/Tests/test_color_lut.py @@ -0,0 +1,645 @@ +from array import array + +import pytest + +from PIL import Image, ImageFilter + +from .helper import assert_image_equal + +try: + import numpy +except ImportError: + numpy = None + + +class TestColorLut3DCoreAPI: + def generate_identity_table(self, channels, size): + if isinstance(size, tuple): + size_1d, size_2d, size_3d = size + else: + size_1d, size_2d, size_3d = (size, size, size) + + table = [ + [ + r / (size_1d - 1) if size_1d != 1 else 0, + g / (size_2d - 1) if size_2d != 1 else 0, + b / (size_3d - 1) if size_3d != 1 else 0, + r / (size_1d - 1) if size_1d != 1 else 0, + g / (size_2d - 1) if size_2d != 1 else 0, + ][:channels] + for b in range(size_3d) + for g in range(size_2d) + for r in range(size_1d) + ] + return ( + channels, + size_1d, + size_2d, + size_3d, + [item for sublist in table for item in sublist], + ) + + def test_wrong_args(self): + im = Image.new("RGB", (10, 10), 0) + + with pytest.raises(ValueError, match="filter"): + im.im.color_lut_3d( + "RGB", Image.Resampling.BICUBIC, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="image mode"): + im.im.color_lut_3d( + "wrong", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="table_channels"): + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(5, 3) + ) + + with pytest.raises(ValueError, match="table_channels"): + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(1, 3) + ) + + with pytest.raises(ValueError, match="table_channels"): + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(2, 3) + ) + + with pytest.raises(ValueError, match="Table size"): + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (1, 3, 3)), + ) + + with pytest.raises(ValueError, match="Table size"): + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (66, 3, 3)), + ) + + with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 7 + ) + + with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 9 + ) + + with pytest.raises(TypeError): + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8 + ) + + with pytest.raises(TypeError): + im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) + + def test_correct_args(self): + im = Image.new("RGB", (10, 10), 0) + + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + im.im.color_lut_3d( + "CMYK", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) + ) + + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (2, 3, 3)), + ) + + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (65, 3, 3)), + ) + + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (3, 65, 3)), + ) + + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (3, 3, 65)), + ) + + def test_wrong_mode(self): + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new("L", (10, 10), 0) + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d( + "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new("L", (10, 10), 0) + im.im.color_lut_3d( + "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d( + "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) + ) + + def test_correct_mode(self): + im = Image.new("RGBA", (10, 10), 0) + im.im.color_lut_3d( + "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + im = Image.new("RGBA", (10, 10), 0) + im.im.color_lut_3d( + "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) + ) + + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d( + "HSV", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d( + "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) + ) + + def test_identities(self): + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], + ) + + # Fast test with small cubes + for size in [2, 3, 5, 7, 11, 16, 17]: + assert_image_equal( + im, + im._new( + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, size), + ) + ), + ) + + # Not so fast + assert_image_equal( + im, + im._new( + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (2, 2, 65)), + ) + ), + ) + + def test_identities_4_channels(self): + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], + ) + + # Red channel copied to alpha + assert_image_equal( + Image.merge("RGBA", (im.split() * 2)[:4]), + im._new( + im.im.color_lut_3d( + "RGBA", + Image.Resampling.BILINEAR, + *self.generate_identity_table(4, 17), + ) + ), + ) + + def test_copy_alpha_channel(self): + g = Image.linear_gradient("L") + im = Image.merge( + "RGBA", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + g.transpose(Image.Transpose.ROTATE_270), + ], + ) + + assert_image_equal( + im, + im._new( + im.im.color_lut_3d( + "RGBA", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, 17), + ) + ), + ) + + def test_channels_order(self): + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], + ) + + # Reverse channels by splitting and using table + # fmt: off + assert_image_equal( + Image.merge('RGB', im.split()[::-1]), + im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, + 3, 2, 2, 2, [ + 0, 0, 0, 0, 0, 1, + 0, 1, 0, 0, 1, 1, + + 1, 0, 0, 1, 0, 1, + 1, 1, 0, 1, 1, 1, + ]))) + # fmt: on + + def test_overflow(self): + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], + ) + + # fmt: off + transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, + 3, 2, 2, 2, + [ + -1, -1, -1, 2, -1, -1, + -1, 2, -1, 2, 2, -1, + + -1, -1, 2, 2, -1, 2, + -1, 2, 2, 2, 2, 2, + ])).load() + # fmt: on + assert transformed[0, 0] == (0, 0, 255) + assert transformed[50, 50] == (0, 0, 255) + assert transformed[255, 0] == (0, 255, 255) + assert transformed[205, 50] == (0, 255, 255) + assert transformed[0, 255] == (255, 0, 0) + assert transformed[50, 205] == (255, 0, 0) + assert transformed[255, 255] == (255, 255, 0) + assert transformed[205, 205] == (255, 255, 0) + + # fmt: off + transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, + 3, 2, 2, 2, + [ + -3, -3, -3, 5, -3, -3, + -3, 5, -3, 5, 5, -3, + + -3, -3, 5, 5, -3, 5, + -3, 5, 5, 5, 5, 5, + ])).load() + # fmt: on + assert transformed[0, 0] == (0, 0, 255) + assert transformed[50, 50] == (0, 0, 255) + assert transformed[255, 0] == (0, 255, 255) + assert transformed[205, 50] == (0, 255, 255) + assert transformed[0, 255] == (255, 0, 0) + assert transformed[50, 205] == (255, 0, 0) + assert transformed[255, 255] == (255, 255, 0) + assert transformed[205, 205] == (255, 255, 0) + + +class TestColorLut3DFilter: + def test_wrong_args(self): + with pytest.raises(ValueError, match="should be either an integer"): + ImageFilter.Color3DLUT("small", [1]) + + with pytest.raises(ValueError, match="should be either an integer"): + ImageFilter.Color3DLUT((11, 11), [1]) + + with pytest.raises(ValueError, match=r"in \[2, 65\] range"): + ImageFilter.Color3DLUT((11, 11, 1), [1]) + + with pytest.raises(ValueError, match=r"in \[2, 65\] range"): + ImageFilter.Color3DLUT((11, 11, 66), [1]) + + with pytest.raises(ValueError, match="table should have .+ items"): + ImageFilter.Color3DLUT((3, 3, 3), [1, 1, 1]) + + with pytest.raises(ValueError, match="table should have .+ items"): + ImageFilter.Color3DLUT((3, 3, 3), [[1, 1, 1]] * 2) + + with pytest.raises(ValueError, match="should have a length of 4"): + ImageFilter.Color3DLUT((3, 3, 3), [[1, 1, 1]] * 27, channels=4) + + with pytest.raises(ValueError, match="should have a length of 3"): + ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8) + + with pytest.raises(ValueError, match="Only 3 or 4 output"): + ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8, channels=2) + + def test_convert_table(self): + lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) + assert tuple(lut.size) == (2, 2, 2) + assert lut.name == "Color 3D LUT" + + # fmt: off + lut = ImageFilter.Color3DLUT((2, 2, 2), [ + (0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10, 11), + (12, 13, 14), (15, 16, 17), (18, 19, 20), (21, 22, 23)]) + # fmt: on + assert tuple(lut.size) == (2, 2, 2) + assert lut.table == list(range(24)) + + lut = ImageFilter.Color3DLUT((2, 2, 2), [(0, 1, 2, 3)] * 8, channels=4) + assert tuple(lut.size) == (2, 2, 2) + assert lut.table == list(range(4)) * 8 + + @pytest.mark.skipif(numpy is None, reason="NumPy not installed") + def test_numpy_sources(self): + table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16) + with pytest.raises(ValueError, match="should have either channels"): + lut = ImageFilter.Color3DLUT((5, 6, 7), table) + + table = numpy.ones((7, 6, 5, 3), dtype=numpy.float16) + lut = ImageFilter.Color3DLUT((5, 6, 7), table) + assert isinstance(lut.table, numpy.ndarray) + assert lut.table.dtype == table.dtype + assert lut.table.shape == (table.size,) + + table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16) + lut = ImageFilter.Color3DLUT((5, 6, 7), table) + assert lut.table.shape == (table.size,) + + table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) + lut = ImageFilter.Color3DLUT((5, 6, 7), table) + assert lut.table.shape == (table.size,) + + # Check application + Image.new("RGB", (10, 10), 0).filter(lut) + + # Check copy + table[0] = 33 + assert lut.table[0] == 1 + + # Check not copy + table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) + lut = ImageFilter.Color3DLUT((5, 6, 7), table, _copy_table=False) + table[0] = 33 + assert lut.table[0] == 33 + + @pytest.mark.skipif(numpy is None, reason="NumPy not installed") + def test_numpy_formats(self): + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], + ) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.float32)[:-1] + with pytest.raises(ValueError, match="should have table_channels"): + im.filter(lut) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.float32).reshape((7 * 9 * 11), 3) + with pytest.raises(ValueError, match="should have table_channels"): + im.filter(lut) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.float16) + assert_image_equal(im, im.filter(lut)) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.float32) + assert_image_equal(im, im.filter(lut)) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.float64) + assert_image_equal(im, im.filter(lut)) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.int32) + im.filter(lut) + lut.table = numpy.array(lut.table, dtype=numpy.int8) + im.filter(lut) + + def test_repr(self): + lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) + assert repr(lut) == "" + + lut = ImageFilter.Color3DLUT( + (3, 4, 5), + array("f", [0, 0, 0, 0] * (3 * 4 * 5)), + channels=4, + target_mode="YCbCr", + _copy_table=False, + ) + assert ( + repr(lut) + == "" + ) + + +class TestGenerateColorLut3D: + def test_wrong_channels_count(self): + with pytest.raises(ValueError, match="3 or 4 output channels"): + ImageFilter.Color3DLUT.generate( + 5, channels=2, callback=lambda r, g, b: (r, g, b) + ) + + with pytest.raises(ValueError, match="should have either channels"): + ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b, r)) + + with pytest.raises(ValueError, match="should have either channels"): + ImageFilter.Color3DLUT.generate( + 5, channels=4, callback=lambda r, g, b: (r, g, b) + ) + + def test_3_channels(self): + lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) + assert tuple(lut.size) == (5, 5, 5) + assert lut.name == "Color 3D LUT" + # fmt: off + assert lut.table[:24] == [ + 0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 0.5, 0.0, 0.0, 0.75, 0.0, 0.0, + 1.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.25, 0.25, 0.0, 0.5, 0.25, 0.0] + # fmt: on + + def test_4_channels(self): + lut = ImageFilter.Color3DLUT.generate( + 5, channels=4, callback=lambda r, g, b: (b, r, g, (r + g + b) / 2) + ) + assert tuple(lut.size) == (5, 5, 5) + assert lut.name == "Color 3D LUT" + # fmt: off + assert lut.table[:24] == [ + 0.0, 0.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.125, 0.0, 0.5, 0.0, 0.25, + 0.0, 0.75, 0.0, 0.375, 0.0, 1.0, 0.0, 0.5, 0.0, 0.0, 0.25, 0.125 + ] + # fmt: on + + def test_apply(self): + lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) + + g = Image.linear_gradient("L") + im = Image.merge( + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], + ) + assert im == im.filter(lut) + + +class TestTransformColorLut3D: + def test_wrong_args(self): + source = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) + + with pytest.raises(ValueError, match="Only 3 or 4 output"): + source.transform(lambda r, g, b: (r, g, b), channels=8) + + with pytest.raises(ValueError, match="should have either channels"): + source.transform(lambda r, g, b: (r, g, b), channels=4) + + with pytest.raises(ValueError, match="should have either channels"): + source.transform(lambda r, g, b: (r, g, b, 1)) + + with pytest.raises(TypeError): + source.transform(lambda r, g, b, a: (r, g, b)) + + def test_target_mode(self): + source = ImageFilter.Color3DLUT.generate( + 2, lambda r, g, b: (r, g, b), target_mode="HSV" + ) + + lut = source.transform(lambda r, g, b: (r, g, b)) + assert lut.mode == "HSV" + + lut = source.transform(lambda r, g, b: (r, g, b), target_mode="RGB") + assert lut.mode == "RGB" + + def test_3_to_3_channels(self): + source = ImageFilter.Color3DLUT.generate((3, 4, 5), lambda r, g, b: (r, g, b)) + lut = source.transform(lambda r, g, b: (r * r, g * g, b * b)) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) == len(source.table) + assert lut.table != source.table + assert lut.table[:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0] + + def test_3_to_4_channels(self): + source = ImageFilter.Color3DLUT.generate((6, 5, 4), lambda r, g, b: (r, g, b)) + lut = source.transform(lambda r, g, b: (r * r, g * g, b * b, 1), channels=4) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) != len(source.table) + assert lut.table != source.table + # fmt: off + assert lut.table[:16] == [ + 0.0, 0.0, 0.0, 1, 0.2**2, 0.0, 0.0, 1, + 0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1] + # fmt: on + + def test_4_to_3_channels(self): + source = ImageFilter.Color3DLUT.generate( + (3, 6, 5), lambda r, g, b: (r, g, b, 1), channels=4 + ) + lut = source.transform( + lambda r, g, b, a: (a - r * r, a - g * g, a - b * b), channels=3 + ) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) != len(source.table) + assert lut.table != source.table + # fmt: off + assert lut.table[:18] == [ + 1.0, 1.0, 1.0, 0.75, 1.0, 1.0, 0.0, 1.0, 1.0, + 1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0] + # fmt: on + + def test_4_to_4_channels(self): + source = ImageFilter.Color3DLUT.generate( + (6, 5, 4), lambda r, g, b: (r, g, b, 1), channels=4 + ) + lut = source.transform(lambda r, g, b, a: (r * r, g * g, b * b, a - 0.5)) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) == len(source.table) + assert lut.table != source.table + # fmt: off + assert lut.table[:16] == [ + 0.0, 0.0, 0.0, 0.5, 0.2**2, 0.0, 0.0, 0.5, + 0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5] + # fmt: on + + def test_with_normals_3_channels(self): + source = ImageFilter.Color3DLUT.generate( + (6, 5, 4), lambda r, g, b: (r * r, g * g, b * b) + ) + lut = source.transform( + lambda nr, ng, nb, r, g, b: (nr - r, ng - g, nb - b), with_normals=True + ) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) == len(source.table) + assert lut.table != source.table + # fmt: off + assert lut.table[:18] == [ + 0.0, 0.0, 0.0, 0.16, 0.0, 0.0, 0.24, 0.0, 0.0, + 0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0] + # fmt: on + + def test_with_normals_4_channels(self): + source = ImageFilter.Color3DLUT.generate( + (3, 6, 5), lambda r, g, b: (r * r, g * g, b * b, 1), channels=4 + ) + lut = source.transform( + lambda nr, ng, nb, r, g, b, a: (nr - r, ng - g, nb - b, a - 0.5), + with_normals=True, + ) + assert tuple(lut.size) == tuple(source.size) + assert len(lut.table) == len(source.table) + assert lut.table != source.table + # fmt: off + assert lut.table[:16] == [ + 0.0, 0.0, 0.0, 0.5, 0.25, 0.0, 0.0, 0.5, + 0.0, 0.0, 0.0, 0.5, 0.0, 0.16, 0.0, 0.5] + # fmt: on diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 11f26d38e80..385192a3cdc 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -1,181 +1,189 @@ -from __future__ import division, print_function - import sys -from helper import unittest, PillowTestCase +import pytest + from PIL import Image +from .helper import is_pypy -is_pypy = hasattr(sys, 'pypy_version_info') +def test_get_stats(): + # Create at least one image + Image.new("RGB", (10, 10)) -class TestCoreStats(PillowTestCase): - def test_get_stats(self): - # Create at least one image - Image.new('RGB', (10, 10)) + stats = Image.core.get_stats() + assert "new_count" in stats + assert "reused_blocks" in stats + assert "freed_blocks" in stats + assert "allocated_blocks" in stats + assert "reallocated_blocks" in stats + assert "blocks_cached" in stats - stats = Image.core.get_stats() - self.assertIn('new_count', stats) - self.assertIn('reused_blocks', stats) - self.assertIn('freed_blocks', stats) - self.assertIn('allocated_blocks', stats) - self.assertIn('reallocated_blocks', stats) - self.assertIn('blocks_cached', stats) - - def test_reset_stats(self): - Image.core.reset_stats() - stats = Image.core.get_stats() - self.assertEqual(stats['new_count'], 0) - self.assertEqual(stats['reused_blocks'], 0) - self.assertEqual(stats['freed_blocks'], 0) - self.assertEqual(stats['allocated_blocks'], 0) - self.assertEqual(stats['reallocated_blocks'], 0) - self.assertEqual(stats['blocks_cached'], 0) +def test_reset_stats(): + Image.core.reset_stats() + + stats = Image.core.get_stats() + assert stats["new_count"] == 0 + assert stats["reused_blocks"] == 0 + assert stats["freed_blocks"] == 0 + assert stats["allocated_blocks"] == 0 + assert stats["reallocated_blocks"] == 0 + assert stats["blocks_cached"] == 0 -class TestCoreMemory(PillowTestCase): - def tearDown(self): +class TestCoreMemory: + def teardown_method(self): # Restore default values Image.core.set_alignment(1) - Image.core.set_block_size(1024*1024) + Image.core.set_block_size(1024 * 1024) Image.core.set_blocks_max(0) Image.core.clear_cache() def test_get_alignment(self): alignment = Image.core.get_alignment() - self.assertGreater(alignment, 0) + assert alignment > 0 def test_set_alignment(self): for i in [1, 2, 4, 8, 16, 32]: Image.core.set_alignment(i) alignment = Image.core.get_alignment() - self.assertEqual(alignment, i) + assert alignment == i # Try to construct new image - Image.new('RGB', (10, 10)) + Image.new("RGB", (10, 10)) - self.assertRaises(ValueError, Image.core.set_alignment, 0) - self.assertRaises(ValueError, Image.core.set_alignment, -1) - self.assertRaises(ValueError, Image.core.set_alignment, 3) + with pytest.raises(ValueError): + Image.core.set_alignment(0) + with pytest.raises(ValueError): + Image.core.set_alignment(-1) + with pytest.raises(ValueError): + Image.core.set_alignment(3) def test_get_block_size(self): block_size = Image.core.get_block_size() - self.assertGreaterEqual(block_size, 4096) + assert block_size >= 4096 def test_set_block_size(self): - for i in [4096, 2*4096, 3*4096]: + for i in [4096, 2 * 4096, 3 * 4096]: Image.core.set_block_size(i) block_size = Image.core.get_block_size() - self.assertEqual(block_size, i) + assert block_size == i # Try to construct new image - Image.new('RGB', (10, 10)) + Image.new("RGB", (10, 10)) - self.assertRaises(ValueError, Image.core.set_block_size, 0) - self.assertRaises(ValueError, Image.core.set_block_size, -1) - self.assertRaises(ValueError, Image.core.set_block_size, 4000) + with pytest.raises(ValueError): + Image.core.set_block_size(0) + with pytest.raises(ValueError): + Image.core.set_block_size(-1) + with pytest.raises(ValueError): + Image.core.set_block_size(4000) def test_set_block_size_stats(self): Image.core.reset_stats() Image.core.set_blocks_max(0) Image.core.set_block_size(4096) - Image.new('RGB', (256, 256)) + Image.new("RGB", (256, 256)) stats = Image.core.get_stats() - self.assertGreaterEqual(stats['new_count'], 1) - self.assertGreaterEqual(stats['allocated_blocks'], 64) - if not is_pypy: - self.assertGreaterEqual(stats['freed_blocks'], 64) + assert stats["new_count"] >= 1 + assert stats["allocated_blocks"] >= 64 + if not is_pypy(): + assert stats["freed_blocks"] >= 64 def test_get_blocks_max(self): blocks_max = Image.core.get_blocks_max() - self.assertGreaterEqual(blocks_max, 0) + assert blocks_max >= 0 def test_set_blocks_max(self): for i in [0, 1, 10]: Image.core.set_blocks_max(i) blocks_max = Image.core.get_blocks_max() - self.assertEqual(blocks_max, i) + assert blocks_max == i # Try to construct new image - Image.new('RGB', (10, 10)) + Image.new("RGB", (10, 10)) - self.assertRaises(ValueError, Image.core.set_blocks_max, -1) + with pytest.raises(ValueError): + Image.core.set_blocks_max(-1) + if sys.maxsize < 2**32: + with pytest.raises(ValueError): + Image.core.set_blocks_max(2**29) - @unittest.skipIf(is_pypy, "images are not collected") + @pytest.mark.skipif(is_pypy(), reason="Images not collected") def test_set_blocks_max_stats(self): Image.core.reset_stats() Image.core.set_blocks_max(128) Image.core.set_block_size(4096) - Image.new('RGB', (256, 256)) - Image.new('RGB', (256, 256)) + Image.new("RGB", (256, 256)) + Image.new("RGB", (256, 256)) stats = Image.core.get_stats() - self.assertGreaterEqual(stats['new_count'], 2) - self.assertGreaterEqual(stats['allocated_blocks'], 64) - self.assertGreaterEqual(stats['reused_blocks'], 64) - self.assertEqual(stats['freed_blocks'], 0) - self.assertEqual(stats['blocks_cached'], 64) + assert stats["new_count"] >= 2 + assert stats["allocated_blocks"] >= 64 + assert stats["reused_blocks"] >= 64 + assert stats["freed_blocks"] == 0 + assert stats["blocks_cached"] == 64 - @unittest.skipIf(is_pypy, "images are not collected") + @pytest.mark.skipif(is_pypy(), reason="Images not collected") def test_clear_cache_stats(self): Image.core.reset_stats() Image.core.clear_cache() Image.core.set_blocks_max(128) Image.core.set_block_size(4096) - Image.new('RGB', (256, 256)) - Image.new('RGB', (256, 256)) + Image.new("RGB", (256, 256)) + Image.new("RGB", (256, 256)) # Keep 16 blocks in cache Image.core.clear_cache(16) stats = Image.core.get_stats() - self.assertGreaterEqual(stats['new_count'], 2) - self.assertGreaterEqual(stats['allocated_blocks'], 64) - self.assertGreaterEqual(stats['reused_blocks'], 64) - self.assertGreaterEqual(stats['freed_blocks'], 48) - self.assertEqual(stats['blocks_cached'], 16) + assert stats["new_count"] >= 2 + assert stats["allocated_blocks"] >= 64 + assert stats["reused_blocks"] >= 64 + assert stats["freed_blocks"] >= 48 + assert stats["blocks_cached"] == 16 def test_large_images(self): Image.core.reset_stats() Image.core.set_blocks_max(0) Image.core.set_block_size(4096) - Image.new('RGB', (2048, 16)) + Image.new("RGB", (2048, 16)) Image.core.clear_cache() stats = Image.core.get_stats() - self.assertGreaterEqual(stats['new_count'], 1) - self.assertGreaterEqual(stats['allocated_blocks'], 16) - self.assertGreaterEqual(stats['reused_blocks'], 0) - self.assertEqual(stats['blocks_cached'], 0) - if not is_pypy: - self.assertGreaterEqual(stats['freed_blocks'], 16) + assert stats["new_count"] >= 1 + assert stats["allocated_blocks"] >= 16 + assert stats["reused_blocks"] >= 0 + assert stats["blocks_cached"] == 0 + if not is_pypy(): + assert stats["freed_blocks"] >= 16 -class TestEnvVars(PillowTestCase): - def tearDown(self): +class TestEnvVars: + def teardown_method(self): # Restore default values Image.core.set_alignment(1) - Image.core.set_block_size(1024*1024) + Image.core.set_block_size(1024 * 1024) Image.core.set_blocks_max(0) Image.core.clear_cache() def test_units(self): - Image._apply_env_variables({'PILLOW_BLOCKS_MAX': '2K'}) - self.assertEqual(Image.core.get_blocks_max(), 2*1024) - Image._apply_env_variables({'PILLOW_BLOCK_SIZE': '2m'}) - self.assertEqual(Image.core.get_block_size(), 2*1024*1024) + Image._apply_env_variables({"PILLOW_BLOCKS_MAX": "2K"}) + assert Image.core.get_blocks_max() == 2 * 1024 + Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"}) + assert Image.core.get_block_size() == 2 * 1024 * 1024 def test_warnings(self): - self.assert_warning( - UserWarning, Image._apply_env_variables, - {'PILLOW_ALIGNMENT': '15'}) - self.assert_warning( - UserWarning, Image._apply_env_variables, - {'PILLOW_BLOCK_SIZE': '1024'}) - self.assert_warning( - UserWarning, Image._apply_env_variables, - {'PILLOW_BLOCKS_MAX': 'wat'}) + pytest.warns( + UserWarning, Image._apply_env_variables, {"PILLOW_ALIGNMENT": "15"} + ) + pytest.warns( + UserWarning, Image._apply_env_variables, {"PILLOW_BLOCK_SIZE": "1024"} + ) + pytest.warns( + UserWarning, Image._apply_env_variables, {"PILLOW_BLOCKS_MAX": "wat"} + ) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 4da8760cdc1..63071b78c9c 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -1,65 +1,101 @@ -from helper import unittest, PillowTestCase, hopper +import pytest from PIL import Image +from .helper import hopper + TEST_FILE = "Tests/images/hopper.ppm" ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS -class TestDecompressionBomb(PillowTestCase): - - def tearDown(self): +class TestDecompressionBomb: + def teardown_method(self, method): Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT def test_no_warning_small_file(self): # Implicit assert: no warning. # A warning would cause a failure. - Image.open(TEST_FILE) + with Image.open(TEST_FILE): + pass def test_no_warning_no_limit(self): # Arrange # Turn limit off Image.MAX_IMAGE_PIXELS = None - self.assertIsNone(Image.MAX_IMAGE_PIXELS) + assert Image.MAX_IMAGE_PIXELS is None # Act / Assert # Implicit assert: no warning. # A warning would cause a failure. - Image.open(TEST_FILE) + with Image.open(TEST_FILE): + pass def test_warning(self): # Set limit to trigger warning on the test file - Image.MAX_IMAGE_PIXELS = 128 * 128 -1 - self.assertEqual(Image.MAX_IMAGE_PIXELS, 128 * 128 - 1) + Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 + assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1 + + def open(): + with Image.open(TEST_FILE): + pass - self.assert_warning(Image.DecompressionBombWarning, - Image.open, TEST_FILE) + pytest.warns(Image.DecompressionBombWarning, open) def test_exception(self): # Set limit to trigger exception on the test file - Image.MAX_IMAGE_PIXELS = 64 * 128 -1 - self.assertEqual(Image.MAX_IMAGE_PIXELS, 64 * 128 - 1) - - self.assertRaises(Image.DecompressionBombError, - lambda: Image.open(TEST_FILE)) - -class TestDecompressionCrop(PillowTestCase): - - def setUp(self): - self.src = hopper() - Image.MAX_IMAGE_PIXELS = self.src.height * self.src.width * 4 - 1 - - def tearDown(self): + Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 + assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1 + + with pytest.raises(Image.DecompressionBombError): + with Image.open(TEST_FILE): + pass + + def test_exception_ico(self): + with pytest.raises(Image.DecompressionBombError): + with Image.open("Tests/images/decompression_bomb.ico"): + pass + + def test_exception_gif(self): + with pytest.raises(Image.DecompressionBombError): + with Image.open("Tests/images/decompression_bomb.gif"): + pass + + def test_exception_gif_extents(self): + with Image.open("Tests/images/decompression_bomb_extents.gif") as im: + with pytest.raises(Image.DecompressionBombError): + im.seek(1) + + def test_exception_bmp(self): + with pytest.raises(Image.DecompressionBombError): + with Image.open("Tests/images/bmp/b/reallybig.bmp"): + pass + + +class TestDecompressionCrop: + @classmethod + def setup_class(cls): + width, height = 128, 128 + Image.MAX_IMAGE_PIXELS = height * width * 4 - 1 + + @classmethod + def teardown_class(cls): Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT - def testEnlargeCrop(self): + def test_enlarge_crop(self): # Crops can extend the extents, therefore we should have the # same decompression bomb warnings on them. - box = (0, 0, self.src.width * 2, self.src.height * 2) - self.assert_warning(Image.DecompressionBombWarning, - self.src.crop, box) + with hopper() as src: + box = (0, 0, src.width * 2, src.height * 2) + pytest.warns(Image.DecompressionBombWarning, src.crop, box) + + def test_crop_decompression_checks(self): + im = Image.new("RGB", (100, 100)) + + for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)): + assert im.crop(value).size == (9, 9) + pytest.warns(Image.DecompressionBombWarning, im.crop, (-160, -160, 99, 99)) -if __name__ == '__main__': - unittest.main() + with pytest.raises(Image.DecompressionBombError): + im.crop((-99909, -99990, 99999, 99999)) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py new file mode 100644 index 00000000000..30ed4a8081d --- /dev/null +++ b/Tests/test_deprecate.py @@ -0,0 +1,91 @@ +import pytest + +from PIL import _deprecate + + +@pytest.mark.parametrize( + "version, expected", + [ + ( + 10, + "Old thing is deprecated and will be removed in Pillow 10 " + r"\(2023-07-01\)\. Use new thing instead\.", + ), + ( + None, + r"Old thing is deprecated and will be removed in a future version\. " + r"Use new thing instead\.", + ), + ], +) +def test_version(version, expected): + with pytest.warns(DeprecationWarning, match=expected): + _deprecate.deprecate("Old thing", version, "new thing") + + +def test_unknown_version(): + expected = r"Unknown removal version, update PIL\._deprecate\?" + with pytest.raises(ValueError, match=expected): + _deprecate.deprecate("Old thing", 12345, "new thing") + + +@pytest.mark.parametrize( + "deprecated, plural, expected", + [ + ( + "Old thing", + False, + r"Old thing is deprecated and should be removed\.", + ), + ( + "Old things", + True, + r"Old things are deprecated and should be removed\.", + ), + ], +) +def test_old_version(deprecated, plural, expected): + expected = r"" + with pytest.raises(RuntimeError, match=expected): + _deprecate.deprecate(deprecated, 1, plural=plural) + + +def test_plural(): + expected = ( + r"Old things are deprecated and will be removed in Pillow 10 \(2023-07-01\)\. " + r"Use new thing instead\." + ) + with pytest.warns(DeprecationWarning, match=expected): + _deprecate.deprecate("Old things", 10, "new thing", plural=True) + + +def test_replacement_and_action(): + expected = "Use only one of 'replacement' and 'action'" + with pytest.raises(ValueError, match=expected): + _deprecate.deprecate( + "Old thing", 10, replacement="new thing", action="Upgrade to new thing" + ) + + +@pytest.mark.parametrize( + "action", + [ + "Upgrade to new thing", + "Upgrade to new thing.", + ], +) +def test_action(action): + expected = ( + r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)\. " + r"Upgrade to new thing\." + ) + with pytest.warns(DeprecationWarning, match=expected): + _deprecate.deprecate("Old thing", 10, action=action) + + +def test_no_replacement_or_action(): + expected = ( + r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)" + ) + with pytest.warns(DeprecationWarning, match=expected): + _deprecate.deprecate("Old thing", 10) diff --git a/Tests/test_deprecated_imageqt.py b/Tests/test_deprecated_imageqt.py new file mode 100644 index 00000000000..2528ff3f7d4 --- /dev/null +++ b/Tests/test_deprecated_imageqt.py @@ -0,0 +1,18 @@ +import warnings + +with warnings.catch_warnings(record=True) as w: + # Arrange: cause all warnings to always be triggered + warnings.simplefilter("always") + + # Act: trigger a warning with Qt5 + from PIL import ImageQt + + +def test_deprecated(): + # Assert + if ImageQt.qt_version in ("5", "side2"): + assert len(w) == 1 + assert issubclass(w[0].category, DeprecationWarning) + assert "deprecated" in str(w[0].message) + else: + assert len(w) == 0 diff --git a/Tests/test_features.py b/Tests/test_features.py index 54d668d2f09..c4e9cd36811 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -1,69 +1,142 @@ -from helper import unittest, PillowTestCase +import io +import re + +import pytest from PIL import features +from .helper import skip_unless_feature + try: from PIL import _webp - HAVE_WEBP = True -except: - HAVE_WEBP = False - - -class TestFeatures(PillowTestCase): - - def test_check(self): - # Check the correctness of the convenience function - for module in features.modules: - self.assertEqual(features.check_module(module), - features.check(module)) - for codec in features.codecs: - self.assertEqual(features.check_codec(codec), - features.check(codec)) - for feature in features.features: - self.assertEqual(features.check_feature(feature), - features.check(feature)) - - @unittest.skipUnless(HAVE_WEBP, True) - def check_webp_transparency(self): - self.assertEqual(features.check('transp_webp'), - not _webp.WebPDecoderBuggyAlpha()) - self.assertEqual(features.check('transp_webp'), - _webp.HAVE_TRANSPARENCY) - - @unittest.skipUnless(HAVE_WEBP, True) - def check_webp_mux(self): - self.assertEqual(features.check('webp_mux'), - _webp.HAVE_WEBPMUX) - - @unittest.skipUnless(HAVE_WEBP, True) - def check_webp_anim(self): - self.assertEqual(features.check('webp_anim'), - _webp.HAVE_WEBPANIM) - - def test_check_modules(self): - for feature in features.modules: - self.assertIn(features.check_module(feature), [True, False]) - for feature in features.codecs: - self.assertIn(features.check_codec(feature), [True, False]) - - def test_supported_modules(self): - self.assertIsInstance(features.get_supported_modules(), list) - self.assertIsInstance(features.get_supported_codecs(), list) - self.assertIsInstance(features.get_supported_features(), list) - self.assertIsInstance(features.get_supported(), list) - - def test_unsupported_codec(self): - # Arrange - codec = "unsupported_codec" - # Act / Assert - self.assertRaises(ValueError, features.check_codec, codec) - - def test_unsupported_module(self): - # Arrange - module = "unsupported_module" - # Act / Assert - self.assertRaises(ValueError, features.check_module, module) - - -if __name__ == '__main__': - unittest.main() +except ImportError: + pass + + +def test_check(): + # Check the correctness of the convenience function + for module in features.modules: + assert features.check_module(module) == features.check(module) + for codec in features.codecs: + assert features.check_codec(codec) == features.check(codec) + for feature in features.features: + assert features.check_feature(feature) == features.check(feature) + + +def test_version(): + # Check the correctness of the convenience function + # and the format of version numbers + + def test(name, function): + version = features.version(name) + if not features.check(name): + assert version is None + else: + assert function(name) == version + if name != "PIL": + assert version is None or re.search(r"\d+(\.\d+)*$", version) + + for module in features.modules: + test(module, features.version_module) + for codec in features.codecs: + test(codec, features.version_codec) + for feature in features.features: + test(feature, features.version_feature) + + +@skip_unless_feature("webp") +def test_webp_transparency(): + assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha() + assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY + + +@skip_unless_feature("webp") +def test_webp_mux(): + assert features.check("webp_mux") == _webp.HAVE_WEBPMUX + + +@skip_unless_feature("webp") +def test_webp_anim(): + assert features.check("webp_anim") == _webp.HAVE_WEBPANIM + + +@skip_unless_feature("libjpeg_turbo") +def test_libjpeg_turbo_version(): + assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo")) + + +@skip_unless_feature("libimagequant") +def test_libimagequant_version(): + assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant")) + + +@pytest.mark.parametrize("feature", features.modules) +def test_check_modules(feature): + assert features.check_module(feature) in [True, False] + + +@pytest.mark.parametrize("feature", features.codecs) +def test_check_codecs(feature): + assert features.check_codec(feature) in [True, False] + + +def test_check_warns_on_nonexistent(): + with pytest.warns(UserWarning) as cm: + has_feature = features.check("typo") + assert has_feature is False + assert str(cm[-1].message) == "Unknown feature 'typo'." + + +def test_supported_modules(): + assert isinstance(features.get_supported_modules(), list) + assert isinstance(features.get_supported_codecs(), list) + assert isinstance(features.get_supported_features(), list) + assert isinstance(features.get_supported(), list) + + +def test_unsupported_codec(): + # Arrange + codec = "unsupported_codec" + # Act / Assert + with pytest.raises(ValueError): + features.check_codec(codec) + with pytest.raises(ValueError): + features.version_codec(codec) + + +def test_unsupported_module(): + # Arrange + module = "unsupported_module" + # Act / Assert + with pytest.raises(ValueError): + features.check_module(module) + with pytest.raises(ValueError): + features.version_module(module) + + +def test_pilinfo(): + buf = io.StringIO() + features.pilinfo(buf) + out = buf.getvalue() + lines = out.splitlines() + assert lines[0] == "-" * 68 + assert lines[1].startswith("Pillow ") + assert lines[2].startswith("Python ") + lines = lines[3:] + while lines[0].startswith(" "): + lines = lines[1:] + assert lines[0] == "-" * 68 + assert lines[1].startswith("Python modules loaded from ") + assert lines[2].startswith("Binary modules loaded from ") + assert lines[3] == "-" * 68 + jpeg = ( + "\n" + + "-" * 68 + + "\n" + + "JPEG image/jpeg\n" + + "Extensions: .jfif, .jpe, .jpeg, .jpg\n" + + "Features: open, save\n" + + "-" * 68 + + "\n" + ) + assert jpeg in out diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py new file mode 100644 index 00000000000..51637c78645 --- /dev/null +++ b/Tests/test_file_apng.py @@ -0,0 +1,669 @@ +import pytest + +from PIL import Image, ImageSequence, PngImagePlugin + + +# APNG browser support tests and fixtures via: +# https://philip.html5.org/tests/apng/tests.html +# (referenced from https://wiki.mozilla.org/APNG_Specification) +def test_apng_basic(): + with Image.open("Tests/images/apng/single_frame.png") as im: + assert not im.is_animated + assert im.n_frames == 1 + assert im.get_format_mimetype() == "image/apng" + assert im.info.get("default_image") is None + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/single_frame_default.png") as im: + assert im.is_animated + assert im.n_frames == 2 + assert im.get_format_mimetype() == "image/apng" + assert im.info.get("default_image") + assert im.getpixel((0, 0)) == (255, 0, 0, 255) + assert im.getpixel((64, 32)) == (255, 0, 0, 255) + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test out of bounds seek + with pytest.raises(EOFError): + im.seek(2) + + # test rewind support + im.seek(0) + assert im.getpixel((0, 0)) == (255, 0, 0, 255) + assert im.getpixel((64, 32)) == (255, 0, 0, 255) + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +@pytest.mark.parametrize( + "filename", + ("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"), +) +def test_apng_fdat(filename): + with Image.open(filename) as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +def test_apng_dispose(): + with Image.open("Tests/images/apng/dispose_op_none.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/dispose_op_background.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + with Image.open("Tests/images/apng/dispose_op_background_final.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/dispose_op_previous.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + +def test_apng_dispose_region(): + with Image.open("Tests/images/apng/dispose_op_none_region.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/dispose_op_background_before_region.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + with Image.open("Tests/images/apng/dispose_op_background_region.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 255, 255) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +def test_apng_dispose_op_previous_frame(): + # Test that the dispose settings being used are from the previous frame + # + # Image created with: + # red = Image.new("RGBA", (128, 64), (255, 0, 0, 255)) + # green = red.copy() + # green.paste(Image.new("RGBA", (64, 32), (0, 255, 0, 255))) + # blue = red.copy() + # blue.paste(Image.new("RGBA", (64, 32), (0, 255, 0, 255)), (64, 32)) + # + # red.save( + # "Tests/images/apng/dispose_op_previous_frame.png", + # save_all=True, + # append_images=[green, blue], + # disposal=[ + # PngImagePlugin.Disposal.OP_NONE, + # PngImagePlugin.Disposal.OP_PREVIOUS, + # PngImagePlugin.Disposal.OP_PREVIOUS + # ], + # ) + with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (255, 0, 0, 255) + + +def test_apng_dispose_op_background_p_mode(): + with Image.open("Tests/images/apng/dispose_op_background_p_mode.png") as im: + im.seek(1) + im.load() + assert im.size == (128, 64) + + +def test_apng_blend(): + with Image.open("Tests/images/apng/blend_op_source_solid.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + with Image.open("Tests/images/apng/blend_op_source_near_transparent.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 2) + assert im.getpixel((64, 32)) == (0, 255, 0, 2) + + with Image.open("Tests/images/apng/blend_op_over.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/blend_op_over_near_transparent.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 97) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +def test_apng_chunk_order(): + with Image.open("Tests/images/apng/fctl_actl.png") as im: + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +def test_apng_delay(): + with Image.open("Tests/images/apng/delay.png") as im: + im.seek(1) + assert im.info.get("duration") == 500.0 + im.seek(2) + assert im.info.get("duration") == 1000.0 + im.seek(3) + assert im.info.get("duration") == 500.0 + im.seek(4) + assert im.info.get("duration") == 1000.0 + + with Image.open("Tests/images/apng/delay_round.png") as im: + im.seek(1) + assert im.info.get("duration") == 500.0 + im.seek(2) + assert im.info.get("duration") == 1000.0 + + with Image.open("Tests/images/apng/delay_short_max.png") as im: + im.seek(1) + assert im.info.get("duration") == 500.0 + im.seek(2) + assert im.info.get("duration") == 1000.0 + + with Image.open("Tests/images/apng/delay_zero_denom.png") as im: + im.seek(1) + assert im.info.get("duration") == 500.0 + im.seek(2) + assert im.info.get("duration") == 1000.0 + + with Image.open("Tests/images/apng/delay_zero_numer.png") as im: + im.seek(1) + assert im.info.get("duration") == 0.0 + im.seek(2) + assert im.info.get("duration") == 0.0 + im.seek(3) + assert im.info.get("duration") == 500.0 + im.seek(4) + assert im.info.get("duration") == 1000.0 + + +def test_apng_num_plays(): + with Image.open("Tests/images/apng/num_plays.png") as im: + assert im.info.get("loop") == 0 + + with Image.open("Tests/images/apng/num_plays_1.png") as im: + assert im.info.get("loop") == 1 + + +def test_apng_mode(): + with Image.open("Tests/images/apng/mode_16bit.png") as im: + assert im.mode == "RGBA" + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (0, 0, 128, 191) + assert im.getpixel((64, 32)) == (0, 0, 128, 191) + + with Image.open("Tests/images/apng/mode_greyscale.png") as im: + assert im.mode == "L" + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == 128 + assert im.getpixel((64, 32)) == 255 + + with Image.open("Tests/images/apng/mode_greyscale_alpha.png") as im: + assert im.mode == "LA" + im.seek(im.n_frames - 1) + assert im.getpixel((0, 0)) == (128, 191) + assert im.getpixel((64, 32)) == (128, 191) + + with Image.open("Tests/images/apng/mode_palette.png") as im: + assert im.mode == "P" + im.seek(im.n_frames - 1) + im = im.convert("RGB") + assert im.getpixel((0, 0)) == (0, 255, 0) + assert im.getpixel((64, 32)) == (0, 255, 0) + + with Image.open("Tests/images/apng/mode_palette_alpha.png") as im: + assert im.mode == "P" + im.seek(im.n_frames - 1) + im = im.convert("RGBA") + assert im.getpixel((0, 0)) == (255, 0, 0, 0) + assert im.getpixel((64, 32)) == (255, 0, 0, 0) + + with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: + assert im.mode == "P" + im.seek(im.n_frames - 1) + im = im.convert("RGBA") + assert im.getpixel((0, 0)) == (0, 0, 255, 128) + assert im.getpixel((64, 32)) == (0, 0, 255, 128) + + +def test_apng_chunk_errors(): + with Image.open("Tests/images/apng/chunk_no_actl.png") as im: + assert not im.is_animated + + def open(): + with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: + im.load() + assert not im.is_animated + + pytest.warns(UserWarning, open) + + with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: + assert not im.is_animated + + with Image.open("Tests/images/apng/chunk_no_fctl.png") as im: + with pytest.raises(SyntaxError): + im.seek(im.n_frames - 1) + + with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im: + with pytest.raises(SyntaxError): + im.seek(im.n_frames - 1) + + with Image.open("Tests/images/apng/chunk_no_fdat.png") as im: + with pytest.raises(SyntaxError): + im.seek(im.n_frames - 1) + + +def test_apng_syntax_errors(): + def open_frames_zero(): + with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: + assert not im.is_animated + with pytest.raises(OSError): + im.load() + + pytest.warns(UserWarning, open_frames_zero) + + def open_frames_zero_default(): + with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: + assert not im.is_animated + im.load() + + pytest.warns(UserWarning, open_frames_zero_default) + + # we can handle this case gracefully + exception = None + with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: + try: + im.seek(im.n_frames - 1) + except Exception as e: + exception = e + assert exception is None + + with pytest.raises(OSError): + with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: + im.seek(im.n_frames - 1) + im.load() + + def open(): + with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: + assert not im.is_animated + im.load() + + pytest.warns(UserWarning, open) + + +@pytest.mark.parametrize( + "test_file", + ( + "sequence_start.png", + "sequence_gap.png", + "sequence_repeat.png", + "sequence_repeat_chunk.png", + "sequence_reorder.png", + "sequence_reorder_chunk.png", + "sequence_fdat_fctl.png", + ), +) +def test_apng_sequence_errors(test_file): + with pytest.raises(SyntaxError): + with Image.open(f"Tests/images/apng/{test_file}") as im: + im.seek(im.n_frames - 1) + im.load() + + +def test_apng_save(tmp_path): + with Image.open("Tests/images/apng/single_frame.png") as im: + test_file = str(tmp_path / "temp.png") + im.save(test_file, save_all=True) + + with Image.open(test_file) as im: + im.load() + assert not im.is_animated + assert im.n_frames == 1 + assert im.get_format_mimetype() == "image/apng" + assert im.info.get("default_image") is None + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + with Image.open("Tests/images/apng/single_frame_default.png") as im: + frames = [] + for frame_im in ImageSequence.Iterator(im): + frames.append(frame_im.copy()) + frames[0].save( + test_file, save_all=True, default_image=True, append_images=frames[1:] + ) + + with Image.open(test_file) as im: + im.load() + assert im.is_animated + assert im.n_frames == 2 + assert im.get_format_mimetype() == "image/apng" + assert im.info.get("default_image") + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + +def test_apng_save_split_fdat(tmp_path): + # test to make sure we do not generate sequence errors when writing + # frames with image data spanning multiple fdAT chunks (in this case + # both the default image and first animation frame will span multiple + # data chunks) + test_file = str(tmp_path / "temp.png") + with Image.open("Tests/images/old-style-jpeg-compression.png") as im: + frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))] + im.save( + test_file, + save_all=True, + default_image=True, + append_images=frames, + ) + with Image.open(test_file) as im: + exception = None + try: + im.seek(im.n_frames - 1) + im.load() + except Exception as e: + exception = e + assert exception is None + + +def test_apng_save_duration_loop(tmp_path): + test_file = str(tmp_path / "temp.png") + with Image.open("Tests/images/apng/delay.png") as im: + frames = [] + durations = [] + loop = im.info.get("loop") + default_image = im.info.get("default_image") + for i, frame_im in enumerate(ImageSequence.Iterator(im)): + frames.append(frame_im.copy()) + if i != 0 or not default_image: + durations.append(frame_im.info.get("duration", 0)) + frames[0].save( + test_file, + save_all=True, + default_image=default_image, + append_images=frames[1:], + duration=durations, + loop=loop, + ) + + with Image.open(test_file) as im: + im.load() + assert im.info.get("loop") == loop + im.seek(1) + assert im.info.get("duration") == 500.0 + im.seek(2) + assert im.info.get("duration") == 1000.0 + im.seek(3) + assert im.info.get("duration") == 500.0 + im.seek(4) + assert im.info.get("duration") == 1000.0 + + # test removal of duplicated frames + frame = Image.new("RGBA", (128, 64), (255, 0, 0, 255)) + frame.save( + test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150] + ) + with Image.open(test_file) as im: + im.load() + assert im.n_frames == 1 + assert im.info.get("duration") == 750 + + # test info duration + frame.info["duration"] = 750 + frame.save(test_file, save_all=True) + with Image.open(test_file) as im: + assert im.info.get("duration") == 750 + + +def test_apng_save_disposal(tmp_path): + test_file = str(tmp_path / "temp.png") + size = (128, 64) + red = Image.new("RGBA", size, (255, 0, 0, 255)) + green = Image.new("RGBA", size, (0, 255, 0, 255)) + transparent = Image.new("RGBA", size, (0, 0, 0, 0)) + + # test OP_NONE + red.save( + test_file, + save_all=True, + append_images=[green, transparent], + disposal=PngImagePlugin.Disposal.OP_NONE, + blend=PngImagePlugin.Blend.OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(2) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test OP_BACKGROUND + disposal = [ + PngImagePlugin.Disposal.OP_NONE, + PngImagePlugin.Disposal.OP_BACKGROUND, + PngImagePlugin.Disposal.OP_NONE, + ] + red.save( + test_file, + save_all=True, + append_images=[red, transparent], + disposal=disposal, + blend=PngImagePlugin.Blend.OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(2) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + disposal = [ + PngImagePlugin.Disposal.OP_NONE, + PngImagePlugin.Disposal.OP_BACKGROUND, + ] + red.save( + test_file, + save_all=True, + append_images=[green], + disposal=disposal, + blend=PngImagePlugin.Blend.OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test OP_PREVIOUS + disposal = [ + PngImagePlugin.Disposal.OP_NONE, + PngImagePlugin.Disposal.OP_PREVIOUS, + PngImagePlugin.Disposal.OP_NONE, + ] + red.save( + test_file, + save_all=True, + append_images=[green, red, transparent], + default_image=True, + disposal=disposal, + blend=PngImagePlugin.Blend.OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(3) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + disposal = [ + PngImagePlugin.Disposal.OP_NONE, + PngImagePlugin.Disposal.OP_PREVIOUS, + ] + red.save( + test_file, + save_all=True, + append_images=[green], + disposal=disposal, + blend=PngImagePlugin.Blend.OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test info disposal + red.info["disposal"] = PngImagePlugin.Disposal.OP_BACKGROUND + red.save( + test_file, + save_all=True, + append_images=[Image.new("RGBA", (10, 10), (0, 255, 0, 255))], + ) + with Image.open(test_file) as im: + im.seek(1) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + +def test_apng_save_disposal_previous(tmp_path): + test_file = str(tmp_path / "temp.png") + size = (128, 64) + 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 + 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) + + +def test_apng_save_blend(tmp_path): + test_file = str(tmp_path / "temp.png") + size = (128, 64) + red = Image.new("RGBA", size, (255, 0, 0, 255)) + green = Image.new("RGBA", size, (0, 255, 0, 255)) + transparent = Image.new("RGBA", size, (0, 0, 0, 0)) + + # test OP_SOURCE on solid color + blend = [ + PngImagePlugin.Blend.OP_OVER, + PngImagePlugin.Blend.OP_SOURCE, + ] + red.save( + test_file, + save_all=True, + append_images=[red, green], + default_image=True, + disposal=PngImagePlugin.Disposal.OP_NONE, + blend=blend, + ) + with Image.open(test_file) as im: + im.seek(2) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test OP_SOURCE on transparent color + blend = [ + PngImagePlugin.Blend.OP_OVER, + PngImagePlugin.Blend.OP_SOURCE, + ] + red.save( + test_file, + save_all=True, + append_images=[red, transparent], + default_image=True, + disposal=PngImagePlugin.Disposal.OP_NONE, + blend=blend, + ) + with Image.open(test_file) as im: + im.seek(2) + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + + # test OP_OVER + red.save( + test_file, + save_all=True, + append_images=[green, transparent], + default_image=True, + disposal=PngImagePlugin.Disposal.OP_NONE, + blend=PngImagePlugin.Blend.OP_OVER, + ) + with Image.open(test_file) as im: + im.seek(1) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + im.seek(2) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test info blend + red.info["blend"] = PngImagePlugin.Blend.OP_OVER + red.save(test_file, save_all=True, append_images=[green, transparent]) + with Image.open(test_file) as im: + im.seek(2) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + + +def test_seek_after_close(): + im = Image.open("Tests/images/apng/delay.png") + im.seek(1) + im.close() + + with pytest.raises(ValueError): + im.seek(0) + + +@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P")) +def test_different_modes_in_later_frames(mode, tmp_path): + test_file = str(tmp_path / "temp.png") + + im = Image.new("L", (1, 1)) + im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))]) + with Image.open(test_file) as reloaded: + assert reloaded.mode == mode + + +def test_constants_deprecation(): + for enum, prefix in { + PngImagePlugin.Disposal: "APNG_DISPOSE_", + PngImagePlugin.Blend: "APNG_BLEND_", + }.items(): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(PngImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py new file mode 100644 index 00000000000..ba2781820e0 --- /dev/null +++ b/Tests/test_file_blp.py @@ -0,0 +1,85 @@ +import pytest + +from PIL import BlpImagePlugin, Image + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + hopper, +) + + +def test_load_blp1(): + with Image.open("Tests/images/blp/blp1_jpeg.blp") as im: + assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png") + + with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im: + im.load() + + +def test_load_blp2_raw(): + with Image.open("Tests/images/blp/blp2_raw.blp") as im: + assert_image_equal_tofile(im, "Tests/images/blp/blp2_raw.png") + + +def test_load_blp2_dxt1(): + with Image.open("Tests/images/blp/blp2_dxt1.blp") as im: + assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1.png") + + +def test_load_blp2_dxt1a(): + with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im: + assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") + + +def test_save(tmp_path): + f = str(tmp_path / "temp.blp") + + for version in ("BLP1", "BLP2"): + im = hopper("P") + im.save(f, blp_version=version) + + with Image.open(f) as reloaded: + assert_image_equal(im.convert("RGB"), reloaded) + + with Image.open("Tests/images/transparent.png") as im: + f = str(tmp_path / "temp.blp") + im.convert("P").save(f, blp_version=version) + + with Image.open(f) as reloaded: + assert_image_similar(im, reloaded, 8) + + im = hopper() + with pytest.raises(ValueError): + im.save(f) + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp", + "Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp", + "Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp", + "Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp", + "Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp", + "Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp", + "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp", + ], +) +def test_crashes(test_file): + with open(test_file, "rb") as f: + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() + + +def test_constants_deprecation(): + for enum, prefix in { + BlpImagePlugin.Format: "BLP_FORMAT_", + BlpImagePlugin.Encoding: "BLP_ENCODING_", + BlpImagePlugin.AlphaEncoding: "BLP_ALPHA_ENCODING_", + }.items(): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(BlpImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index bfd97016fe5..5f6d523558a 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,81 +1,207 @@ -from helper import unittest, PillowTestCase, hopper - -from PIL import Image, BmpImagePlugin import io +import pytest -class TestFileBmp(PillowTestCase): +from PIL import BmpImagePlugin, Image - def roundtrip(self, im): - outfile = self.tempfile("temp.bmp") +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar_tofile, + hopper, +) - im.save(outfile, 'BMP') - reloaded = Image.open(outfile) - reloaded.load() - self.assertEqual(im.mode, reloaded.mode) - self.assertEqual(im.size, reloaded.size) - self.assertEqual(reloaded.format, "BMP") +def test_sanity(tmp_path): + def roundtrip(im): + outfile = str(tmp_path / "temp.bmp") - def test_sanity(self): - self.roundtrip(hopper()) + im.save(outfile, "BMP") - self.roundtrip(hopper("1")) - self.roundtrip(hopper("L")) - self.roundtrip(hopper("P")) - self.roundtrip(hopper("RGB")) + with Image.open(outfile) as reloaded: + reloaded.load() + assert im.mode == reloaded.mode + assert im.size == reloaded.size + assert reloaded.format == "BMP" + assert reloaded.get_format_mimetype() == "image/bmp" - def test_invalid_file(self): - with open("Tests/images/flower.jpg", "rb") as fp: - self.assertRaises(SyntaxError, - BmpImagePlugin.BmpImageFile, fp) + roundtrip(hopper()) - def test_save_to_bytes(self): - output = io.BytesIO() - im = hopper() - im.save(output, "BMP") + roundtrip(hopper("1")) + roundtrip(hopper("L")) + roundtrip(hopper("P")) + roundtrip(hopper("RGB")) - output.seek(0) - reloaded = Image.open(output) - self.assertEqual(im.mode, reloaded.mode) - self.assertEqual(im.size, reloaded.size) - self.assertEqual(reloaded.format, "BMP") +def test_invalid_file(): + with open("Tests/images/flower.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + BmpImagePlugin.BmpImageFile(fp) - def test_dpi(self): - dpi = (72, 72) - output = io.BytesIO() - im = hopper() - im.save(output, "BMP", dpi=dpi) +def test_fallback_if_mmap_errors(): + # This image has been truncated, + # so that the buffer is not large enough when using mmap + with Image.open("Tests/images/mmap_error.bmp") as im: + assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp") - output.seek(0) - reloaded = Image.open(output) - self.assertEqual(reloaded.info["dpi"], dpi) +def test_save_to_bytes(): + output = io.BytesIO() + im = hopper() + im.save(output, "BMP") - def test_save_bmp_with_dpi(self): - # Test for #1301 - # Arrange - outfile = self.tempfile("temp.jpg") - im = Image.open("Tests/images/hopper.bmp") + output.seek(0) + with Image.open(output) as reloaded: + assert im.mode == reloaded.mode + assert im.size == reloaded.size + assert reloaded.format == "BMP" - # Act - im.save(outfile, 'JPEG', dpi=im.info['dpi']) - # Assert - reloaded = Image.open(outfile) - reloaded.load() - self.assertEqual(im.info['dpi'], reloaded.info['dpi']) - self.assertEqual(im.size, reloaded.size) - self.assertEqual(reloaded.format, "JPEG") +def test_small_palette(tmp_path): + im = Image.new("P", (1, 1)) + colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] + im.putpalette(colors) + + out = str(tmp_path / "temp.bmp") + im.save(out) + + with Image.open(out) as reloaded: + assert reloaded.getpalette() == colors + + +def test_save_too_large(tmp_path): + outfile = str(tmp_path / "temp.bmp") + with Image.new("RGB", (1, 1)) as im: + im._size = (37838, 37838) + with pytest.raises(ValueError): + im.save(outfile) - def test_load_dib(self): - # test for #1293, Imagegrab returning Unsupported Bitfields Format - im = BmpImagePlugin.DibImageFile('Tests/images/clipboard.dib') - target = Image.open('Tests/images/clipboard_target.png') - self.assert_image_equal(im, target) +def test_dpi(): + dpi = (72, 72) -if __name__ == '__main__': - unittest.main() + output = io.BytesIO() + with hopper() as im: + im.save(output, "BMP", dpi=dpi) + + output.seek(0) + with Image.open(output) as reloaded: + assert reloaded.info["dpi"] == (72.008961115161, 72.008961115161) + + +def test_save_bmp_with_dpi(tmp_path): + # Test for #1301 + # Arrange + outfile = str(tmp_path / "temp.jpg") + with Image.open("Tests/images/hopper.bmp") as im: + assert im.info["dpi"] == (95.98654816726399, 95.98654816726399) + + # Act + im.save(outfile, "JPEG", dpi=im.info["dpi"]) + + # Assert + with Image.open(outfile) as reloaded: + reloaded.load() + assert reloaded.info["dpi"] == (96, 96) + assert reloaded.size == im.size + assert reloaded.format == "JPEG" + + +def test_save_float_dpi(tmp_path): + outfile = str(tmp_path / "temp.bmp") + with Image.open("Tests/images/hopper.bmp") as im: + im.save(outfile, dpi=(72.21216100543306, 72.21216100543306)) + with Image.open(outfile) as reloaded: + assert reloaded.info["dpi"] == (72.21216100543306, 72.21216100543306) + + +def test_load_dib(): + # test for #1293, Imagegrab returning Unsupported Bitfields Format + with Image.open("Tests/images/clipboard.dib") as im: + assert im.format == "DIB" + assert im.get_format_mimetype() == "image/bmp" + + assert_image_equal_tofile(im, "Tests/images/clipboard_target.png") + + +def test_save_dib(tmp_path): + outfile = str(tmp_path / "temp.dib") + + with Image.open("Tests/images/clipboard.dib") as im: + im.save(outfile) + + with Image.open(outfile) as reloaded: + assert reloaded.format == "DIB" + assert reloaded.get_format_mimetype() == "image/bmp" + assert_image_equal(im, reloaded) + + +def test_rgba_bitfields(): + # This test image has been manually hexedited + # to change the bitfield compression in the header from XBGR to RGBA + with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: + + # So before the comparing the image, swap the channels + b, g, r = im.split()[1:] + im = Image.merge("RGB", (r, g, b)) + + assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp") + + # This test image has been manually hexedited + # to change the bitfield compression in the header from XBGR to ABGR + with Image.open("Tests/images/rgb32bf-abgr.bmp") as im: + assert_image_equal_tofile( + im.convert("RGB"), "Tests/images/bmp/q/rgb32bf-xbgr.bmp" + ) + + +def test_rle8(): + with Image.open("Tests/images/hopper_rle8.bmp") as im: + assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12) + + with Image.open("Tests/images/hopper_rle8_greyscale.bmp") as im: + assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") + + # This test image has been manually hexedited + # to have rows with too much data + with Image.open("Tests/images/hopper_rle8_row_overflow.bmp") as im: + assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12) + + # Signal end of bitmap before the image is finished + with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp: + data = fp.read(1063) + b"\x01" + with Image.open(io.BytesIO(data)) as im: + with pytest.raises(ValueError): + im.load() + + +def test_rle4(): + with Image.open("Tests/images/bmp/g/pal4rle.bmp") as im: + assert_image_similar_tofile(im, "Tests/images/bmp/g/pal4.bmp", 12) + + +@pytest.mark.parametrize( + "file_name,length", + ( + # EOF immediately after the header + ("Tests/images/hopper_rle8.bmp", 1078), + # EOF during delta + ("Tests/images/bmp/q/pal8rletrns.bmp", 3670), + # EOF when reading data in absolute mode + ("Tests/images/bmp/g/pal8rle.bmp", 1064), + ), +) +def test_rle8_eof(file_name, length): + with open(file_name, "rb") as fp: + data = fp.read(length) + with Image.open(io.BytesIO(data)) as im: + with pytest.raises(ValueError): + im.load() + + +def test_offset(): + # This image has been hexedited + # to exclude the palette size from the pixel data offset + with Image.open("Tests/images/pal8_offset.bmp") as im: + assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp") diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 08980a996d0..e330404d64e 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -1,46 +1,79 @@ -from helper import unittest, PillowTestCase, hopper +import pytest from PIL import BufrStubImagePlugin, Image -TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d" +from .helper import hopper +TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d" -class TestFileBufrStub(PillowTestCase): - def test_open(self): - # Act - im = Image.open(TEST_FILE) +def test_open(): + # Act + with Image.open(TEST_FILE) as im: # Assert - self.assertEqual(im.format, "BUFR") + assert im.format == "BUFR" # Dummy data from the stub - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (1, 1)) + assert im.mode == "F" + assert im.size == (1, 1) + - def test_invalid_file(self): - # Arrange - invalid_file = "Tests/images/flower.jpg" +def test_invalid_file(): + # Arrange + invalid_file = "Tests/images/flower.jpg" - # Act / Assert - self.assertRaises(SyntaxError, - BufrStubImagePlugin.BufrStubImageFile, invalid_file) + # Act / Assert + with pytest.raises(SyntaxError): + BufrStubImagePlugin.BufrStubImageFile(invalid_file) - def test_load(self): - # Arrange - im = Image.open(TEST_FILE) + +def test_load(): + # Arrange + with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler - self.assertRaises(IOError, im.load) + with pytest.raises(OSError): + im.load() + + +def test_save(tmp_path): + # Arrange + im = hopper() + tmpfile = str(tmp_path / "temp.bufr") + + # Act / Assert: stub cannot save without an implemented handler + with pytest.raises(OSError): + im.save(tmpfile) + + +def test_handler(tmp_path): + class TestHandler: + opened = False + loaded = False + saved = False + + def open(self, im): + self.opened = True + + def load(self, im): + self.loaded = True + return Image.new("RGB", (1, 1)) + + def save(self, im, fp, filename): + self.saved = True - def test_save(self): - # Arrange - im = hopper() - tmpfile = self.tempfile("temp.bufr") + handler = TestHandler() + BufrStubImagePlugin.register_handler(handler) + with Image.open(TEST_FILE) as im: + assert handler.opened + assert not handler.loaded - # Act / Assert: stub cannot save without an implemented handler - self.assertRaises(IOError, im.save, tmpfile) + im.load() + assert handler.loaded + temp_file = str(tmp_path / "temp.bufr") + im.save(temp_file) + assert handler.saved -if __name__ == '__main__': - unittest.main() + BufrStubImagePlugin._handler = None diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 55228be0cd0..65cf6a75ea3 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -1,129 +1,149 @@ -from helper import unittest, PillowTestCase, hopper +import pytest -from PIL import Image -from PIL import ContainerIO +from PIL import ContainerIO, Image + +from .helper import hopper TEST_FILE = "Tests/images/dummy.container" -class TestFileContainer(PillowTestCase): +def test_sanity(): + dir(Image) + dir(ContainerIO) - def test_sanity(self): - dir(Image) - dir(ContainerIO) - def test_isatty(self): - im = hopper() +def test_isatty(): + with hopper() as im: container = ContainerIO.ContainerIO(im, 0, 0) - self.assertEqual(container.isatty(), 0) - - def test_seek_mode_0(self): - # Arrange - mode = 0 - with open(TEST_FILE) as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) + assert container.isatty() is False + + +def test_seek_mode_0(): + # Arrange + mode = 0 + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) + + # Act + container.seek(33, mode) + container.seek(33, mode) - # Act - container.seek(33, mode) - container.seek(33, mode) + # Assert + assert container.tell() == 33 - # Assert - self.assertEqual(container.tell(), 33) - def test_seek_mode_1(self): - # Arrange - mode = 1 - with open(TEST_FILE) as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) +def test_seek_mode_1(): + # Arrange + mode = 1 + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) - # Act - container.seek(33, mode) - container.seek(33, mode) + # Act + container.seek(33, mode) + container.seek(33, mode) - # Assert - self.assertEqual(container.tell(), 66) + # Assert + assert container.tell() == 66 - def test_seek_mode_2(self): - # Arrange - mode = 2 - with open(TEST_FILE) as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) - # Act - container.seek(33, mode) - container.seek(33, mode) +def test_seek_mode_2(): + # Arrange + mode = 2 + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) - # Assert - self.assertEqual(container.tell(), 100) + # Act + container.seek(33, mode) + container.seek(33, mode) - def test_read_n0(self): - # Arrange - with open(TEST_FILE) as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) + # Assert + assert container.tell() == 100 - # Act - container.seek(81) - data = container.read() - # Assert - self.assertEqual(data, "7\nThis is line 8\n") +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_read_n0(bytesmode): + # Arrange + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) - def test_read_n(self): - # Arrange - with open(TEST_FILE) as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) + # Act + container.seek(81) + data = container.read() - # Act - container.seek(81) - data = container.read(3) + # Assert + if bytesmode: + data = data.decode() + assert data == "7\nThis is line 8\n" - # Assert - self.assertEqual(data, "7\nT") - def test_read_eof(self): - # Arrange - with open(TEST_FILE) as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_read_n(bytesmode): + # Arrange + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) - # Act - container.seek(100) - data = container.read() + # Act + container.seek(81) + data = container.read(3) - # Assert - self.assertEqual(data, "") + # Assert + if bytesmode: + data = data.decode() + assert data == "7\nT" - def test_readline(self): - # Arrange - with open(TEST_FILE) as fh: - container = ContainerIO.ContainerIO(fh, 0, 120) - # Act - data = container.readline() +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_read_eof(bytesmode): + # Arrange + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) - # Assert - self.assertEqual(data, "This is line 1\n") + # Act + container.seek(100) + data = container.read() - def test_readlines(self): - # Arrange - expected = ["This is line 1\n", - "This is line 2\n", - "This is line 3\n", - "This is line 4\n", - "This is line 5\n", - "This is line 6\n", - "This is line 7\n", - "This is line 8\n"] - with open(TEST_FILE) as fh: - container = ContainerIO.ContainerIO(fh, 0, 120) + # Assert + if bytesmode: + data = data.decode() + assert data == "" - # Act - data = container.readlines() - - # Assert - self.assertEqual(data, expected) +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_readline(bytesmode): + # Arrange + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 0, 120) + # Act + data = container.readline() -if __name__ == '__main__': - unittest.main() + # Assert + if bytesmode: + data = data.decode() + assert data == "This is line 1\n" + + +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_readlines(bytesmode): + # Arrange + expected = [ + "This is line 1\n", + "This is line 2\n", + "This is line 3\n", + "This is line 4\n", + "This is line 5\n", + "This is line 6\n", + "This is line 7\n", + "This is line 8\n", + ] + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 0, 120) + + # Act + data = container.readlines() + + # Assert + if bytesmode: + data = [line.decode() for line in data] + assert data == expected diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index 23055a0ad48..f04a20a220a 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -1,34 +1,30 @@ -from helper import unittest, PillowTestCase +import pytest -from PIL import Image, CurImagePlugin +from PIL import CurImagePlugin, Image TEST_FILE = "Tests/images/deerstalker.cur" -class TestFileCur(PillowTestCase): - - def test_sanity(self): - im = Image.open(TEST_FILE) - - self.assertEqual(im.size, (32, 32)) - self.assertIsInstance(im, CurImagePlugin.CurImageFile) +def test_sanity(): + with Image.open(TEST_FILE) as im: + assert im.size == (32, 32) + assert isinstance(im, CurImagePlugin.CurImageFile) # Check some pixel colors to ensure image is loaded properly - self.assertEqual(im.getpixel((10, 1)), (0, 0, 0, 0)) - self.assertEqual(im.getpixel((11, 1)), (253, 254, 254, 1)) - self.assertEqual(im.getpixel((16, 16)), (84, 87, 86, 255)) - - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" + assert im.getpixel((10, 1)) == (0, 0, 0, 0) + assert im.getpixel((11, 1)) == (253, 254, 254, 1) + assert im.getpixel((16, 16)) == (84, 87, 86, 255) - self.assertRaises(SyntaxError, - CurImagePlugin.CurImageFile, invalid_file) - no_cursors_file = "Tests/images/no_cursors.cur" +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - cur = CurImagePlugin.CurImageFile(TEST_FILE) - with open(no_cursors_file, "rb") as cur.fp: - self.assertRaises(TypeError, cur._open) + with pytest.raises(SyntaxError): + CurImagePlugin.CurImageFile(invalid_file) + no_cursors_file = "Tests/images/no_cursors.cur" -if __name__ == '__main__': - unittest.main() + cur = CurImagePlugin.CurImageFile(TEST_FILE) + cur.fp.close() + with open(no_cursors_file, "rb") as cur.fp: + with pytest.raises(TypeError): + cur._open() diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 28ebb91dc9e..0f09c4b9915 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -1,64 +1,91 @@ -from helper import unittest, PillowTestCase, hopper +import warnings -from PIL import Image, DcxImagePlugin +import pytest + +from PIL import DcxImagePlugin, Image + +from .helper import assert_image_equal, hopper, is_pypy # Created with ImageMagick: convert hopper.ppm hopper.dcx TEST_FILE = "Tests/images/hopper.dcx" -class TestFileDcx(PillowTestCase): - - def test_sanity(self): - # Arrange +def test_sanity(): + # Arrange - # Act - im = Image.open(TEST_FILE) + # Act + with Image.open(TEST_FILE) as im: # Assert - self.assertEqual(im.size, (128, 128)) - self.assertIsInstance(im, DcxImagePlugin.DcxImageFile) + assert im.size == (128, 128) + assert isinstance(im, DcxImagePlugin.DcxImageFile) orig = hopper() - self.assert_image_equal(im, orig) + assert_image_equal(im, orig) + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): + im = Image.open(TEST_FILE) + im.load() + + pytest.warns(ResourceWarning, open) - def test_invalid_file(self): - with open("Tests/images/flower.jpg", "rb") as fp: - self.assertRaises(SyntaxError, - DcxImagePlugin.DcxImageFile, fp) - def test_tell(self): - # Arrange +def test_closed_file(): + with warnings.catch_warnings(): im = Image.open(TEST_FILE) + im.load() + im.close() + + +def test_context_manager(): + with warnings.catch_warnings(): + with Image.open(TEST_FILE) as im: + im.load() + + +def test_invalid_file(): + with open("Tests/images/flower.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + DcxImagePlugin.DcxImageFile(fp) + + +def test_tell(): + # Arrange + with Image.open(TEST_FILE) as im: # Act frame = im.tell() # Assert - self.assertEqual(frame, 0) + assert frame == 0 - def test_n_frames(self): - im = Image.open(TEST_FILE) - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) - def test_eoferror(self): - im = Image.open(TEST_FILE) +def test_n_frames(): + with Image.open(TEST_FILE) as im: + assert im.n_frames == 1 + assert not im.is_animated + + +def test_eoferror(): + with Image.open(TEST_FILE) as im: n_frames = im.n_frames # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames # Test that seeking to the last frame does not raise an error - im.seek(n_frames-1) + im.seek(n_frames - 1) - def test_seek_too_far(self): - # Arrange - im = Image.open(TEST_FILE) - frame = 999 # too big on purpose - - # Act / Assert - self.assertRaises(EOFError, im.seek, frame) +def test_seek_too_far(): + # Arrange + with Image.open(TEST_FILE) as im: + frame = 999 # too big on purpose -if __name__ == '__main__': - unittest.main() + # Act / Assert + with pytest.raises(EOFError): + im.seek(frame) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 89d265ec27f..4b9f8949ef5 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -1,115 +1,319 @@ +"""Test DdsImagePlugin""" from io import BytesIO -from helper import unittest, PillowTestCase -from PIL import Image, DdsImagePlugin +import pytest + +from PIL import DdsImagePlugin, Image + +from .helper import assert_image_equal, assert_image_equal_tofile, hopper TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds" +TEST_FILE_ATI1 = "Tests/images/ati1.dds" +TEST_FILE_ATI2 = "Tests/images/ati2.dds" +TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds" +TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds" +TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds" +TEST_FILE_BC5S = "Tests/images/bc5s.dds" +TEST_FILE_BC6H = "Tests/images/bc6h.dds" +TEST_FILE_BC6HS = "Tests/images/bc6h_sf.dds" TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds" +TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds" +TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds" +TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds" +TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds" +TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" + + +def test_sanity_dxt1(): + """Check DXT1 images can be opened""" + with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target: + target = target.convert("RGBA") + with Image.open(TEST_FILE_DXT1) as im: + im.load() + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) -class TestFileDds(PillowTestCase): - """Test DdsImagePlugin""" + assert_image_equal(im, target) - def test_sanity_dxt1(self): - """Check DXT1 images can be opened""" - target = Image.open(TEST_FILE_DXT1.replace('.dds', '.png')) - im = Image.open(TEST_FILE_DXT1) +def test_sanity_dxt3(): + """Check DXT3 images can be opened""" + + with Image.open(TEST_FILE_DXT3) as im: im.load() - self.assertEqual(im.format, "DDS") - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (256, 256)) + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) - self.assert_image_equal(target.convert('RGBA'), im) + assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png")) - def test_sanity_dxt5(self): - """Check DXT5 images can be opened""" - target = Image.open(TEST_FILE_DXT5.replace('.dds', '.png')) +def test_sanity_dxt5(): + """Check DXT5 images can be opened""" - im = Image.open(TEST_FILE_DXT5) + with Image.open(TEST_FILE_DXT5) as im: im.load() - self.assertEqual(im.format, "DDS") - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (256, 256)) + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) - self.assert_image_equal(target, im) + assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png")) - def test_sanity_dxt3(self): - """Check DXT3 images can be opened""" - target = Image.open(TEST_FILE_DXT3.replace('.dds', '.png')) +def test_sanity_ati1(): + """Check ATI1 images can be opened""" - im = Image.open(TEST_FILE_DXT3) + with Image.open(TEST_FILE_ATI1) as im: im.load() - self.assertEqual(im.format, "DDS") - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (256, 256)) + assert im.format == "DDS" + assert im.mode == "L" + assert im.size == (64, 64) - self.assert_image_equal(target, im) + assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) - def test_dx10_bc7(self): - """Check DX10 images can be opened""" - target = Image.open(TEST_FILE_DX10_BC7.replace('.dds', '.png')) +def test_sanity_ati2(): + """Check ATI2 images can be opened""" - im = Image.open(TEST_FILE_DX10_BC7) + with Image.open(TEST_FILE_ATI2) as im: im.load() - self.assertEqual(im.format, "DDS") - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (256, 256)) + assert im.format == "DDS" + assert im.mode == "RGB" + assert im.size == (256, 256) + + assert_image_equal_tofile(im, TEST_FILE_DX10_BC5_UNORM.replace(".dds", ".png")) + + +@pytest.mark.parametrize( + ("image_path", "expected_path"), + ( + # hexeditted to be typeless + (TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM), + (TEST_FILE_DX10_BC5_UNORM, TEST_FILE_DX10_BC5_UNORM), + # hexeditted to use DX10 FourCC + (TEST_FILE_DX10_BC5_SNORM, TEST_FILE_BC5S), + (TEST_FILE_BC5S, TEST_FILE_BC5S), + ), +) +def test_dx10_bc5(image_path, expected_path): + """Check DX10 BC5 images can be opened""" + + with Image.open(image_path) as im: + im.load() - self.assert_image_equal(target, im) + assert im.format == "DDS" + assert im.mode == "RGB" + assert im.size == (256, 256) - def test__validate_true(self): - """Check valid prefix""" - # Arrange - prefix = b"DDS etc" + assert_image_equal_tofile(im, expected_path.replace(".dds", ".png")) - # Act - output = DdsImagePlugin._validate(prefix) - # Assert - self.assertTrue(output) +@pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS)) +def test_dx10_bc6h(image_path): + """Check DX10 BC6H/BC6HS images can be opened""" + + with Image.open(image_path) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGB" + assert im.size == (128, 128) + + assert_image_equal_tofile(im, image_path.replace(".dds", ".png")) + + +def test_dx10_bc7(): + """Check DX10 images can be opened""" + + with Image.open(TEST_FILE_DX10_BC7) as im: + im.load() - def test__validate_false(self): - """Check invalid prefix""" - # Arrange - prefix = b"something invalid" + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) - # Act - output = DdsImagePlugin._validate(prefix) + assert_image_equal_tofile(im, TEST_FILE_DX10_BC7.replace(".dds", ".png")) + + +def test_dx10_bc7_unorm_srgb(): + """Check DX10 unsigned normalized integer images can be opened""" + + with Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (16, 16) + assert im.info["gamma"] == 1 / 2.2 + + assert_image_equal_tofile( + im, TEST_FILE_DX10_BC7_UNORM_SRGB.replace(".dds", ".png") + ) + + +def test_dx10_r8g8b8a8(): + """Check DX10 images can be opened""" + + with Image.open(TEST_FILE_DX10_R8G8B8A8) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) + + assert_image_equal_tofile(im, TEST_FILE_DX10_R8G8B8A8.replace(".dds", ".png")) + + +def test_dx10_r8g8b8a8_unorm_srgb(): + """Check DX10 unsigned normalized integer images can be opened""" + + with Image.open(TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB) as im: + im.load() - # Assert - self.assertFalse(output) + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (16, 16) + assert im.info["gamma"] == 1 / 2.2 - def test_short_header(self): - """ Check a short header""" - with open(TEST_FILE_DXT5, 'rb') as f: - img_file = f.read() + assert_image_equal_tofile( + im, TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB.replace(".dds", ".png") + ) - def short_header(): - Image.open(BytesIO(img_file[:119])) - self.assertRaises(IOError, short_header) +def test_unimplemented_dxgi_format(): + with pytest.raises(NotImplementedError): + with Image.open("Tests/images/unimplemented_dxgi_format.dds"): + pass - def test_short_file(self): - """ Check that the appropriate error is thrown for a short file""" - with open(TEST_FILE_DXT5, 'rb') as f: - img_file = f.read() +def test_uncompressed_rgb(): + """Check uncompressed RGB images can be opened""" - def short_file(): - im = Image.open(BytesIO(img_file[:-100])) + # convert -format dds -define dds:compression=none hopper.jpg hopper.dds + with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im: + assert im.format == "DDS" + assert im.mode == "RGB" + assert im.size == (128, 128) + + assert_image_equal_tofile(im, "Tests/images/hopper.png") + + # Test image with alpha + with Image.open(TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA) as im: + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (800, 600) + + assert_image_equal_tofile( + im, TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA.replace(".dds", ".png") + ) + + +def test__accept_true(): + """Check valid prefix""" + # Arrange + prefix = b"DDS etc" + + # Act + output = DdsImagePlugin._accept(prefix) + + # Assert + assert output + + +def test__accept_false(): + """Check invalid prefix""" + # Arrange + prefix = b"something invalid" + + # Act + output = DdsImagePlugin._accept(prefix) + + # Assert + assert not output + + +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + DdsImagePlugin.DdsImageFile(invalid_file) + + +def test_short_header(): + """Check a short header""" + with open(TEST_FILE_DXT5, "rb") as f: + img_file = f.read() + + def short_header(): + with Image.open(BytesIO(img_file[:119])): + pass # pragma: no cover + + with pytest.raises(OSError): + short_header() + + +def test_short_file(): + """Check that the appropriate error is thrown for a short file""" + + with open(TEST_FILE_DXT5, "rb") as f: + img_file = f.read() + + def short_file(): + with Image.open(BytesIO(img_file[:-100])) as im: im.load() - self.assertRaises(IOError, short_file) + with pytest.raises(OSError): + short_file() + + +def test_dxt5_colorblock_alpha_issue_4142(): + """Check that colorblocks are decoded correctly in DXT5""" + + with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im: + px = im.getpixel((0, 0)) + assert px[0] != 0 + assert px[1] != 0 + assert px[2] != 0 + + px = im.getpixel((1, 0)) + assert px[0] != 0 + assert px[1] != 0 + assert px[2] != 0 + + +def test_unimplemented_pixel_format(): + with pytest.raises(NotImplementedError): + with Image.open("Tests/images/unimplemented_pixel_format.dds"): + pass + + +def test_save_unsupported_mode(tmp_path): + out = str(tmp_path / "temp.dds") + im = hopper("HSV") + with pytest.raises(OSError): + im.save(out) + + +@pytest.mark.parametrize( + ("mode", "test_file"), + [ + ("RGB", "Tests/images/hopper.png"), + ("RGBA", "Tests/images/pil123rgba.png"), + ], +) +def test_save(mode, test_file, tmp_path): + out = str(tmp_path / "temp.dds") + with Image.open(test_file) as im: + assert im.mode == mode + im.save(out) -if __name__ == '__main__': - unittest.main() + with Image.open(out) as reloaded: + assert_image_equal(im, reloaded) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 2313b292c0f..015dda992c6 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,293 +1,295 @@ -from helper import unittest, PillowTestCase, hopper - -from PIL import Image, EpsImagePlugin import io +import pytest + +from PIL import EpsImagePlugin, Image, features + +from .helper import ( + assert_image_similar, + assert_image_similar_tofile, + hopper, + mark_if_feature_version, + skip_unless_feature, +) + +HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript() + # Our two EPS test files (they are identical except for their bounding boxes) -file1 = "Tests/images/zero_bb.eps" -file2 = "Tests/images/non_zero_bb.eps" +FILE1 = "Tests/images/zero_bb.eps" +FILE2 = "Tests/images/non_zero_bb.eps" # Due to palletization, we'll need to convert these to RGB after load -file1_compare = "Tests/images/zero_bb.png" -file1_compare_scale2 = "Tests/images/zero_bb_scale2.png" +FILE1_COMPARE = "Tests/images/zero_bb.png" +FILE1_COMPARE_SCALE2 = "Tests/images/zero_bb_scale2.png" -file2_compare = "Tests/images/non_zero_bb.png" -file2_compare_scale2 = "Tests/images/non_zero_bb_scale2.png" +FILE2_COMPARE = "Tests/images/non_zero_bb.png" +FILE2_COMPARE_SCALE2 = "Tests/images/non_zero_bb_scale2.png" # EPS test files with binary preview -file3 = "Tests/images/binary_preview_map.eps" +FILE3 = "Tests/images/binary_preview_map.eps" -class TestFileEps(PillowTestCase): - - def setUp(self): - if not EpsImagePlugin.has_ghostscript(): - self.skipTest("Ghostscript not available") - - def test_sanity(self): - # Regular scale - image1 = Image.open(file1) +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_sanity(): + # Regular scale + with Image.open(FILE1) as image1: image1.load() - self.assertEqual(image1.mode, "RGB") - self.assertEqual(image1.size, (460, 352)) - self.assertEqual(image1.format, "EPS") + assert image1.mode == "RGB" + assert image1.size == (460, 352) + assert image1.format == "EPS" - image2 = Image.open(file2) + with Image.open(FILE2) as image2: image2.load() - self.assertEqual(image2.mode, "RGB") - self.assertEqual(image2.size, (360, 252)) - self.assertEqual(image2.format, "EPS") + assert image2.mode == "RGB" + assert image2.size == (360, 252) + assert image2.format == "EPS" - # Double scale - image1_scale2 = Image.open(file1) + # Double scale + with Image.open(FILE1) as image1_scale2: image1_scale2.load(scale=2) - self.assertEqual(image1_scale2.mode, "RGB") - self.assertEqual(image1_scale2.size, (920, 704)) - self.assertEqual(image1_scale2.format, "EPS") + assert image1_scale2.mode == "RGB" + assert image1_scale2.size == (920, 704) + assert image1_scale2.format == "EPS" - image2_scale2 = Image.open(file2) + with Image.open(FILE2) as image2_scale2: image2_scale2.load(scale=2) - self.assertEqual(image2_scale2.mode, "RGB") - self.assertEqual(image2_scale2.size, (720, 504)) - self.assertEqual(image2_scale2.format, "EPS") + assert image2_scale2.mode == "RGB" + assert image2_scale2.size == (720, 504) + assert image2_scale2.format == "EPS" + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_load(): + with Image.open(FILE1) as im: + assert im.load()[0, 0] == (255, 255, 255) - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" + # Test again now that it has already been loaded once + assert im.load()[0, 0] == (255, 255, 255) - self.assertRaises(SyntaxError, - EpsImagePlugin.EpsImageFile, invalid_file) - def test_cmyk(self): - cmyk_image = Image.open("Tests/images/pil_sample_cmyk.eps") +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - self.assertEqual(cmyk_image.mode, "CMYK") - self.assertEqual(cmyk_image.size, (100, 100)) - self.assertEqual(cmyk_image.format, "EPS") + with pytest.raises(SyntaxError): + EpsImagePlugin.EpsImageFile(invalid_file) + + +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_cmyk(): + with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: + + assert cmyk_image.mode == "CMYK" + assert cmyk_image.size == (100, 100) + assert cmyk_image.format == "EPS" cmyk_image.load() - self.assertEqual(cmyk_image.mode, "RGB") - - if 'jpeg_decoder' in dir(Image.core): - target = Image.open('Tests/images/pil_sample_rgb.jpg') - self.assert_image_similar(cmyk_image, target, 10) - - def test_showpage(self): - # See https://github.com/python-pillow/Pillow/issues/2615 - plot_image = Image.open("Tests/images/reqd_showpage.eps") - target = Image.open("Tests/images/reqd_showpage.png") - - #should not crash/hang - plot_image.load() - # fonts could be slightly different - self.assert_image_similar(plot_image, target, 6) - - def test_file_object(self): - # issue 479 - image1 = Image.open(file1) - with open(self.tempfile('temp_file.eps'), 'wb') as fh: - image1.save(fh, 'EPS') - - def test_iobase_object(self): - # issue 479 - image1 = Image.open(file1) - with io.open(self.tempfile('temp_iobase.eps'), 'wb') as fh: - image1.save(fh, 'EPS') - - def test_bytesio_object(self): - with open(file1, 'rb') as f: - img_bytes = io.BytesIO(f.read()) - - img = Image.open(img_bytes) + assert cmyk_image.mode == "RGB" + + if features.check("jpg"): + assert_image_similar_tofile( + cmyk_image, "Tests/images/pil_sample_rgb.jpg", 10 + ) + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_showpage(): + # See https://github.com/python-pillow/Pillow/issues/2615 + with Image.open("Tests/images/reqd_showpage.eps") as plot_image: + with Image.open("Tests/images/reqd_showpage.png") as target: + # should not crash/hang + plot_image.load() + # fonts could be slightly different + assert_image_similar(plot_image, target, 6) + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_transparency(): + with Image.open("Tests/images/reqd_showpage.eps") as plot_image: + plot_image.load(transparency=True) + assert plot_image.mode == "RGBA" + + with Image.open("Tests/images/reqd_showpage_transparency.png") as target: + # fonts could be slightly different + assert_image_similar(plot_image, target, 6) + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_file_object(tmp_path): + # issue 479 + with Image.open(FILE1) as image1: + with open(str(tmp_path / "temp.eps"), "wb") as fh: + image1.save(fh, "EPS") + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_bytesio_object(): + with open(FILE1, "rb") as f: + img_bytes = io.BytesIO(f.read()) + + with Image.open(img_bytes) as img: img.load() - image1_scale1_compare = Image.open(file1_compare).convert("RGB") + with Image.open(FILE1_COMPARE) as image1_scale1_compare: + image1_scale1_compare = image1_scale1_compare.convert("RGB") image1_scale1_compare.load() - self.assert_image_similar(img, image1_scale1_compare, 5) + assert_image_similar(img, image1_scale1_compare, 5) + + +def test_1_mode(): + with Image.open("Tests/images/1.eps") as im: + assert im.mode == "1" - def test_image_mode_not_supported(self): - im = hopper("RGBA") - tmpfile = self.tempfile('temp.eps') - self.assertRaises(ValueError, im.save, tmpfile) - def test_render_scale1(self): - # We need png support for these render test - codecs = dir(Image.core) - if "zip_encoder" not in codecs or "zip_decoder" not in codecs: - self.skipTest("zip/deflate support not available") +def test_image_mode_not_supported(tmp_path): + im = hopper("RGBA") + tmpfile = str(tmp_path / "temp.eps") + with pytest.raises(ValueError): + im.save(tmpfile) - # Zero bounding box - image1_scale1 = Image.open(file1) + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@skip_unless_feature("zlib") +def test_render_scale1(): + # We need png support for these render test + + # Zero bounding box + with Image.open(FILE1) as image1_scale1: image1_scale1.load() - image1_scale1_compare = Image.open(file1_compare).convert("RGB") + with Image.open(FILE1_COMPARE) as image1_scale1_compare: + image1_scale1_compare = image1_scale1_compare.convert("RGB") image1_scale1_compare.load() - self.assert_image_similar(image1_scale1, image1_scale1_compare, 5) + assert_image_similar(image1_scale1, image1_scale1_compare, 5) - # Non-Zero bounding box - image2_scale1 = Image.open(file2) + # Non-Zero bounding box + with Image.open(FILE2) as image2_scale1: image2_scale1.load() - image2_scale1_compare = Image.open(file2_compare).convert("RGB") + with Image.open(FILE2_COMPARE) as image2_scale1_compare: + image2_scale1_compare = image2_scale1_compare.convert("RGB") image2_scale1_compare.load() - self.assert_image_similar(image2_scale1, image2_scale1_compare, 10) + assert_image_similar(image2_scale1, image2_scale1_compare, 10) - def test_render_scale2(self): - # We need png support for these render test - codecs = dir(Image.core) - if "zip_encoder" not in codecs or "zip_decoder" not in codecs: - self.skipTest("zip/deflate support not available") - # Zero bounding box - image1_scale2 = Image.open(file1) +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@skip_unless_feature("zlib") +def test_render_scale2(): + # We need png support for these render test + + # Zero bounding box + with Image.open(FILE1) as image1_scale2: image1_scale2.load(scale=2) - image1_scale2_compare = Image.open(file1_compare_scale2).convert("RGB") + with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare: + image1_scale2_compare = image1_scale2_compare.convert("RGB") image1_scale2_compare.load() - self.assert_image_similar(image1_scale2, image1_scale2_compare, 5) + assert_image_similar(image1_scale2, image1_scale2_compare, 5) - # Non-Zero bounding box - image2_scale2 = Image.open(file2) + # Non-Zero bounding box + with Image.open(FILE2) as image2_scale2: image2_scale2.load(scale=2) - image2_scale2_compare = Image.open(file2_compare_scale2).convert("RGB") + with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: + image2_scale2_compare = image2_scale2_compare.convert("RGB") image2_scale2_compare.load() - self.assert_image_similar(image2_scale2, image2_scale2_compare, 10) + assert_image_similar(image2_scale2, image2_scale2_compare, 10) - def test_resize(self): - # Arrange - image1 = Image.open(file1) - image2 = Image.open(file2) - image3 = Image.open("Tests/images/illu10_preview.eps") - new_size = (100, 100) - # Act - image1 = image1.resize(new_size) - image2 = image2.resize(new_size) - image3 = image3.resize(new_size) - - # Assert - self.assertEqual(image1.size, new_size) - self.assertEqual(image2.size, new_size) - self.assertEqual(image3.size, new_size) - - def test_thumbnail(self): - # Issue #619 - # Arrange - image1 = Image.open(file1) - image2 = Image.open(file2) +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) +def test_resize(filename): + with Image.open(filename) as im: new_size = (100, 100) + im = im.resize(new_size) + assert im.size == new_size - # Act - image1.thumbnail(new_size) - image2.thumbnail(new_size) - - # Assert - self.assertEqual(max(image1.size), max(new_size)) - self.assertEqual(max(image2.size), max(new_size)) - - def test_read_binary_preview(self): - # Issue 302 - # open image with binary preview - Image.open(file3) - - def _test_readline(self, t, ending): - ending = "Failure with line ending: %s" % ("".join( - "%s" % ord(s) - for s in ending)) - self.assertEqual(t.readline().strip('\r\n'), 'something', ending) - self.assertEqual(t.readline().strip('\r\n'), 'else', ending) - self.assertEqual(t.readline().strip('\r\n'), 'baz', ending) - self.assertEqual(t.readline().strip('\r\n'), 'bif', ending) - - def _test_readline_stringio(self, test_string, ending): - # check all the freaking line endings possible - try: - import StringIO - except ImportError: - # don't skip, it skips everything in the parent test - return - t = StringIO.StringIO(test_string) - self._test_readline(t, ending) - - def _test_readline_io(self, test_string, ending): - if str is bytes: - t = io.StringIO(unicode(test_string)) - else: - t = io.StringIO(test_string) - self._test_readline(t, ending) - - def _test_readline_file_universal(self, test_string, ending): - f = self.tempfile('temp.txt') - with open(f, 'wb') as w: - if str is bytes: - w.write(test_string) - else: - w.write(test_string.encode('UTF-8')) - - with open(f, 'rU') as t: - self._test_readline(t, ending) - - def _test_readline_file_psfile(self, test_string, ending): - f = self.tempfile('temp.txt') - with open(f, 'wb') as w: - if str is bytes: - w.write(test_string) - else: - w.write(test_string.encode('UTF-8')) - - with open(f, 'rb') as r: + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@pytest.mark.parametrize("filename", (FILE1, FILE2)) +def test_thumbnail(filename): + # Issue #619 + # Arrange + with Image.open(filename) as im: + new_size = (100, 100) + im.thumbnail(new_size) + assert max(im.size) == max(new_size) + + +def test_read_binary_preview(): + # Issue 302 + # open image with binary preview + with Image.open(FILE3): + pass + + +def test_readline(tmp_path): + # check all the freaking line endings possible from the spec + # test_string = u'something\r\nelse\n\rbaz\rbif\n' + line_endings = ["\r\n", "\n", "\n\r", "\r"] + strings = ["something", "else", "baz", "bif"] + + def _test_readline(t, ending): + ending = "Failure with line ending: %s" % ( + "".join("%s" % ord(s) for s in ending) + ) + assert t.readline().strip("\r\n") == "something", ending + assert t.readline().strip("\r\n") == "else", ending + assert t.readline().strip("\r\n") == "baz", ending + assert t.readline().strip("\r\n") == "bif", ending + + def _test_readline_io_psfile(test_string, ending): + f = io.BytesIO(test_string.encode("latin-1")) + t = EpsImagePlugin.PSFile(f) + _test_readline(t, ending) + + def _test_readline_file_psfile(test_string, ending): + f = str(tmp_path / "temp.txt") + with open(f, "wb") as w: + w.write(test_string.encode("latin-1")) + + with open(f, "rb") as r: t = EpsImagePlugin.PSFile(r) - self._test_readline(t, ending) - - def test_readline(self): - # check all the freaking line endings possible from the spec - # test_string = u'something\r\nelse\n\rbaz\rbif\n' - line_endings = ['\r\n', '\n'] - not_working_endings = ['\n\r', '\r'] - strings = ['something', 'else', 'baz', 'bif'] - - for ending in line_endings: - s = ending.join(strings) - # Native Python versions will pass these endings. - # self._test_readline_stringio(s, ending) - # self._test_readline_io(s, ending) - # self._test_readline_file_universal(s, ending) - - self._test_readline_file_psfile(s, ending) - - for ending in not_working_endings: - # these only work with the PSFile, while they're in spec, - # they're not likely to be used - s = ending.join(strings) - - # Native Python versions may fail on these endings. - # self._test_readline_stringio(s, ending) - # self._test_readline_io(s, ending) - # self._test_readline_file_universal(s, ending) - - self._test_readline_file_psfile(s, ending) - - def test_open_eps(self): - # https://github.com/python-pillow/Pillow/issues/1104 - # Arrange - FILES = ["Tests/images/illu10_no_preview.eps", - "Tests/images/illu10_preview.eps", - "Tests/images/illuCS6_no_preview.eps", - "Tests/images/illuCS6_preview.eps"] - - # Act - for filename in FILES: - img = Image.open(filename) - - # Assert - self.assertEqual(img.mode, "RGB") - - def test_emptyline(self): - # Test file includes an empty line in the header data - emptyline_file = "Tests/images/zero_bb_emptyline.eps" - - image = Image.open(emptyline_file) + _test_readline(t, ending) + + for ending in line_endings: + s = ending.join(strings) + _test_readline_io_psfile(s, ending) + _test_readline_file_psfile(s, ending) + + +@pytest.mark.parametrize( + "filename", + ( + "Tests/images/illu10_no_preview.eps", + "Tests/images/illu10_preview.eps", + "Tests/images/illuCS6_no_preview.eps", + "Tests/images/illuCS6_preview.eps", + ), +) +def test_open_eps(filename): + # https://github.com/python-pillow/Pillow/issues/1104 + with Image.open(filename) as img: + assert img.mode == "RGB" + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_emptyline(): + # Test file includes an empty line in the header data + emptyline_file = "Tests/images/zero_bb_emptyline.eps" + + with Image.open(emptyline_file) as image: image.load() - self.assertEqual(image.mode, "RGB") - self.assertEqual(image.size, (460, 352)) - self.assertEqual(image.format, "EPS") - - -if __name__ == '__main__': - unittest.main() + assert image.mode == "RGB" + assert image.size == (460, 352) + assert image.format == "EPS" + + +@pytest.mark.timeout(timeout=5) +@pytest.mark.parametrize( + "test_file", + ["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], +) +def test_timeout(test_file): + with open(test_file, "rb") as f: + with pytest.raises(Image.UnidentifiedImageError): + with Image.open(f): + pass diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py new file mode 100644 index 00000000000..447888acd8d --- /dev/null +++ b/Tests/test_file_fits.py @@ -0,0 +1,80 @@ +from io import BytesIO + +import pytest + +from PIL import FitsImagePlugin, FitsStubImagePlugin, Image + +from .helper import assert_image_equal, hopper + +TEST_FILE = "Tests/images/hopper.fits" + + +def test_open(): + # Act + with Image.open(TEST_FILE) as im: + + # Assert + assert im.format == "FITS" + assert im.size == (128, 128) + assert im.mode == "L" + + assert_image_equal(im, hopper("L")) + + +def test_invalid_file(): + # Arrange + invalid_file = "Tests/images/flower.jpg" + + # Act / Assert + with pytest.raises(SyntaxError): + FitsImagePlugin.FitsImageFile(invalid_file) + + +def test_truncated_fits(): + # No END to headers + image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE" + with pytest.raises(OSError): + FitsImagePlugin.FitsImageFile(BytesIO(image_data)) + + +def test_naxis_zero(): + # This test image has been manually hexedited + # to set the number of data axes to zero + with pytest.raises(ValueError): + with Image.open("Tests/images/hopper_naxis_zero.fits"): + pass + + +def test_stub_deprecated(): + class Handler: + opened = False + loaded = False + + def open(self, im): + self.opened = True + + def load(self, im): + self.loaded = True + return Image.new("RGB", (1, 1)) + + handler = Handler() + with pytest.warns(DeprecationWarning): + FitsStubImagePlugin.register_handler(handler) + + with Image.open(TEST_FILE) as im: + assert im.format == "FITS" + assert im.size == (128, 128) + assert im.mode == "L" + + assert handler.opened + assert not handler.loaded + + im.load() + assert handler.loaded + + FitsStubImagePlugin._handler = None + Image.register_open( + FitsImagePlugin.FitsImageFile.format, + FitsImagePlugin.FitsImageFile, + FitsImagePlugin._accept, + ) diff --git a/Tests/test_file_fitsstub.py b/Tests/test_file_fitsstub.py deleted file mode 100644 index d74e983ce95..00000000000 --- a/Tests/test_file_fitsstub.py +++ /dev/null @@ -1,50 +0,0 @@ -from helper import unittest, PillowTestCase - -from PIL import FitsStubImagePlugin, Image - -TEST_FILE = "Tests/images/hopper.fits" - - -class TestFileFitsStub(PillowTestCase): - - def test_open(self): - # Act - im = Image.open(TEST_FILE) - - # Assert - self.assertEqual(im.format, "FITS") - - # Dummy data from the stub - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (1, 1)) - - def test_invalid_file(self): - # Arrange - invalid_file = "Tests/images/flower.jpg" - - # Act / Assert - self.assertRaises(SyntaxError, - FitsStubImagePlugin.FITSStubImageFile, invalid_file) - - def test_load(self): - # Arrange - im = Image.open(TEST_FILE) - - # Act / Assert: stub cannot load without an implemented handler - self.assertRaises(IOError, im.load) - - def test_save(self): - # Arrange - im = Image.open(TEST_FILE) - dummy_fp = None - dummy_filename = "dummy.filename" - - # Act / Assert: stub cannot save without an implemented handler - self.assertRaises(IOError, im.save, dummy_filename) - self.assertRaises( - IOError, - FitsStubImagePlugin._save, im, dummy_fp, dummy_filename) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 142af3cec40..b8b999d70d0 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,6 +1,10 @@ -from helper import unittest, PillowTestCase +import warnings -from PIL import Image, FliImagePlugin +import pytest + +from PIL import FliImagePlugin, Image + +from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy # created as an export of a palette image from Gimp2.6 # save as...-> hopper.fli, default options. @@ -10,81 +14,153 @@ animated_test_file = "Tests/images/a.fli" -class TestFileFli(PillowTestCase): - - def test_sanity(self): +def test_sanity(): + with Image.open(static_test_file) as im: + im.load() + assert im.mode == "P" + assert im.size == (128, 128) + assert im.format == "FLI" + assert not im.is_animated + + with Image.open(animated_test_file) as im: + assert im.mode == "P" + assert im.size == (320, 200) + assert im.format == "FLI" + assert im.info["duration"] == 71 + assert im.is_animated + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): im = Image.open(static_test_file) im.load() - self.assertEqual(im.mode, "P") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "FLI") - self.assertFalse(im.is_animated) - - im = Image.open(animated_test_file) - self.assertEqual(im.mode, "P") - self.assertEqual(im.size, (320, 200)) - self.assertEqual(im.format, "FLI") - self.assertEqual(im.info["duration"], 71) - self.assertTrue(im.is_animated) - - def test_tell(self): - # Arrange + + pytest.warns(ResourceWarning, open) + + +def test_closed_file(): + with warnings.catch_warnings(): im = Image.open(static_test_file) + im.load() + im.close() + + +def test_seek_after_close(): + im = Image.open(animated_test_file) + im.seek(1) + im.close() + + with pytest.raises(ValueError): + im.seek(0) + + +def test_context_manager(): + with warnings.catch_warnings(): + with Image.open(static_test_file) as im: + im.load() + + +def test_tell(): + # Arrange + with Image.open(static_test_file) as im: # Act frame = im.tell() # Assert - self.assertEqual(frame, 0) + assert frame == 0 - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - FliImagePlugin.FliImageFile, invalid_file) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - def test_n_frames(self): - im = Image.open(static_test_file) - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) + with pytest.raises(SyntaxError): + FliImagePlugin.FliImageFile(invalid_file) + + +def test_palette_chunk_second(): + with Image.open("Tests/images/hopper_palette_chunk_second.fli") as im: + with Image.open(static_test_file) as expected: + assert_image_equal(im.convert("RGB"), expected.convert("RGB")) - im = Image.open(animated_test_file) - self.assertEqual(im.n_frames, 384) - self.assertTrue(im.is_animated) - def test_eoferror(self): - im = Image.open(animated_test_file) +def test_n_frames(): + with Image.open(static_test_file) as im: + assert im.n_frames == 1 + assert not im.is_animated + + with Image.open(animated_test_file) as im: + assert im.n_frames == 384 + assert im.is_animated + + +def test_eoferror(): + with Image.open(animated_test_file) as im: n_frames = im.n_frames # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames # Test that seeking to the last frame does not raise an error - im.seek(n_frames-1) + im.seek(n_frames - 1) - def test_seek_tell(self): - im = Image.open(animated_test_file) + +def test_seek_tell(): + with Image.open(animated_test_file) as im: layer_number = im.tell() - self.assertEqual(layer_number, 0) + assert layer_number == 0 im.seek(0) layer_number = im.tell() - self.assertEqual(layer_number, 0) + assert layer_number == 0 im.seek(1) layer_number = im.tell() - self.assertEqual(layer_number, 1) + assert layer_number == 1 im.seek(2) layer_number = im.tell() - self.assertEqual(layer_number, 2) + assert layer_number == 2 im.seek(1) layer_number = im.tell() - self.assertEqual(layer_number, 1) - - -if __name__ == '__main__': - unittest.main() + assert layer_number == 1 + + +def test_seek(): + with Image.open(animated_test_file) as im: + im.seek(50) + + assert_image_equal_tofile(im, "Tests/images/a_fli.png") + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli", + "Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli", + ], +) +@pytest.mark.timeout(timeout=3) +def test_timeouts(test_file): + with open(test_file, "rb") as f: + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/crash-5762152299364352.fli", + ], +) +def test_crash(test_file): + with open(test_file, "rb") as f: + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index 441a3e635b3..fa22e90f660 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -1,27 +1,36 @@ -from helper import unittest, PillowTestCase +import pytest -try: - from PIL import FpxImagePlugin -except ImportError: - olefile_installed = False -else: - olefile_installed = True +from PIL import Image +from .helper import assert_image_equal_tofile -@unittest.skipUnless(olefile_installed, "olefile package not installed") -class TestFileFpx(PillowTestCase): +FpxImagePlugin = pytest.importorskip( + "PIL.FpxImagePlugin", reason="olefile not installed" +) - def test_invalid_file(self): - # Test an invalid OLE file - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - FpxImagePlugin.FpxImageFile, invalid_file) - # Test a valid OLE file, but not an FPX file - ole_file = "Tests/images/test-ole-file.doc" - self.assertRaises(SyntaxError, - FpxImagePlugin.FpxImageFile, ole_file) +def test_sanity(): + with Image.open("Tests/images/input_bw_one_band.fpx") as im: + assert im.mode == "L" + assert im.size == (70, 46) + assert im.format == "FPX" + assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png") -if __name__ == '__main__': - unittest.main() + +def test_invalid_file(): + # Test an invalid OLE file + invalid_file = "Tests/images/flower.jpg" + with pytest.raises(SyntaxError): + FpxImagePlugin.FpxImageFile(invalid_file) + + # Test a valid OLE file, but not an FPX file + ole_file = "Tests/images/test-ole-file.doc" + with pytest.raises(SyntaxError): + FpxImagePlugin.FpxImageFile(ole_file) + + +def test_fpx_invalid_number_of_bands(): + with pytest.raises(OSError, match="Invalid number of bands"): + with Image.open("Tests/images/input_bw_five_bands.fpx"): + pass diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index ed1116ad591..cae20fa46eb 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -1,19 +1,32 @@ -from helper import unittest, PillowTestCase -from PIL import Image +import pytest +from PIL import FtexImagePlugin, Image -class TestFileFtex(PillowTestCase): +from .helper import assert_image_equal_tofile, assert_image_similar - def test_load_raw(self): - im = Image.open('Tests/images/ftex_uncompressed.ftu') - target = Image.open('Tests/images/ftex_uncompressed.png') - self.assert_image_equal(im, target) +def test_load_raw(): + with Image.open("Tests/images/ftex_uncompressed.ftu") as im: + assert_image_equal_tofile(im, "Tests/images/ftex_uncompressed.png") - def test_load_dxt1(self): - im = Image.open('Tests/images/ftex_dxt1.ftc') - target = Image.open('Tests/images/ftex_dxt1.png') - self.assert_image_similar(im, target.convert('RGBA'), 15) -if __name__ == '__main__': - unittest.main() +def test_load_dxt1(): + with Image.open("Tests/images/ftex_dxt1.ftc") as im: + with Image.open("Tests/images/ftex_dxt1.png") as target: + assert_image_similar(im, target.convert("RGBA"), 15) + + +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + FtexImagePlugin.FtexImageFile(invalid_file) + + +def test_constants_deprecation(): + for enum, prefix in { + FtexImagePlugin.Format: "FORMAT_", + }.items(): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(FtexImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index aacc193f46d..1ea8af8ee34 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -1,22 +1,32 @@ -from helper import unittest, PillowTestCase +import pytest -from PIL import Image, GbrImagePlugin +from PIL import GbrImagePlugin, Image +from .helper import assert_image_equal_tofile -class TestFileGbr(PillowTestCase): - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" +def test_gbr_file(): + with Image.open("Tests/images/gbr.gbr") as im: + assert_image_equal_tofile(im, "Tests/images/gbr.png") - self.assertRaises(SyntaxError, - GbrImagePlugin.GbrImageFile, invalid_file) - def test_gbr_file(self): - im = Image.open('Tests/images/gbr.gbr') +def test_load(): + with Image.open("Tests/images/gbr.gbr") as im: + assert im.load()[0, 0] == (0, 0, 0, 0) - target = Image.open('Tests/images/gbr.png') + # Test again now that it has already been loaded once + assert im.load()[0, 0] == (0, 0, 0, 0) - self.assert_image_equal(target, im) -if __name__ == '__main__': - unittest.main() +def test_multiple_load_operations(): + with Image.open("Tests/images/gbr.gbr") as im: + im.load() + im.load() + assert_image_equal_tofile(im, "Tests/images/gbr.png") + + +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + GbrImagePlugin.GbrImageFile(invalid_file) diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py new file mode 100644 index 00000000000..5594e5bbb9e --- /dev/null +++ b/Tests/test_file_gd.py @@ -0,0 +1,23 @@ +import pytest + +from PIL import GdImageFile, UnidentifiedImageError + +TEST_GD_FILE = "Tests/images/hopper.gd" + + +def test_sanity(): + with GdImageFile.open(TEST_GD_FILE) as im: + assert im.size == (128, 128) + assert im.format == "GD" + + +def test_bad_mode(): + with pytest.raises(ValueError): + GdImageFile.open(TEST_GD_FILE, "bad mode") + + +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(UnidentifiedImageError): + GdImageFile.open(invalid_file) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 22a2c007261..926f5c1eea8 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,10 +1,18 @@ -from helper import unittest, PillowTestCase, hopper, netpbm_available +import warnings +from io import BytesIO -from PIL import Image, ImagePalette, GifImagePlugin +import pytest -from io import BytesIO +from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, ImageSequence, features -codecs = dir(Image.core) +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + hopper, + is_pypy, + netpbm_available, +) # sample gif stream TEST_GIF = "Tests/images/hopper.gif" @@ -13,580 +21,1218 @@ data = f.read() -class TestFileGif(PillowTestCase): +def test_sanity(): + with Image.open(TEST_GIF) as im: + im.load() + assert im.mode == "P" + assert im.size == (128, 128) + assert im.format == "GIF" + assert im.info["version"] == b"GIF89a" - def setUp(self): - if "gif_encoder" not in codecs or "gif_decoder" not in codecs: - self.skipTest("gif support not available") # can this happen? - def test_sanity(self): +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): im = Image.open(TEST_GIF) im.load() - self.assertEqual(im.mode, "P") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "GIF") - self.assertEqual(im.info["version"], b"GIF89a") - - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - - self.assertRaises(SyntaxError, - GifImagePlugin.GifImageFile, invalid_file) - - def test_optimize(self): - def test_grayscale(optimize): - im = Image.new("L", (1, 1), 0) - filename = BytesIO() - im.save(filename, "GIF", optimize=optimize) - return len(filename.getvalue()) - - def test_bilevel(optimize): - im = Image.new("1", (1, 1), 0) - test_file = BytesIO() - im.save(test_file, "GIF", optimize=optimize) - return len(test_file.getvalue()) - - self.assertEqual(test_grayscale(0), 800) - self.assertEqual(test_grayscale(1), 38) - self.assertEqual(test_bilevel(0), 800) - self.assertEqual(test_bilevel(1), 800) - - def test_optimize_correctness(self): - # 256 color Palette image, posterize to > 128 and < 128 levels - # Size bigger and smaller than 512x512 - # Check the palette for number of colors allocated. - # Check for correctness after conversion back to RGB - def check(colors, size, expected_palette_length): - # make an image with empty colors in the start of the palette range - im = Image.frombytes('P', (colors, colors), - bytes(bytearray(range(256-colors, 256))*colors)) - im = im.resize((size, size)) - outfile = BytesIO() - im.save(outfile, 'GIF') - outfile.seek(0) - reloaded = Image.open(outfile) - # check palette length - palette_length = max(i+1 for i, v in enumerate(reloaded.histogram()) if v) - self.assertEqual(expected_palette_length, palette_length) + pytest.warns(ResourceWarning, open) + + +def test_closed_file(): + with warnings.catch_warnings(): + im = Image.open(TEST_GIF) + im.load() + im.close() + + +def test_seek_after_close(): + im = Image.open("Tests/images/iss634.gif") + im.load() + im.close() + + with pytest.raises(ValueError): + im.is_animated + with pytest.raises(ValueError): + im.n_frames + with pytest.raises(ValueError): + im.seek(1) + + +def test_context_manager(): + with warnings.catch_warnings(): + with Image.open(TEST_GIF) as im: + im.load() + + +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + GifImagePlugin.GifImageFile(invalid_file) - self.assert_image_equal(im.convert('RGB'), reloaded.convert('RGB')) - # These do optimize the palette - check(128, 511, 128) - check(64, 511, 64) - check(4, 511, 4) +def test_l_mode_transparency(): + with Image.open("Tests/images/no_palette_with_transparency.gif") as im: + assert im.mode == "L" + assert im.load()[0, 0] == 128 + assert im.info["transparency"] == 255 + + im.seek(1) + assert im.mode == "L" + assert im.load()[0, 0] == 128 + + +def test_l_mode_after_rgb(): + with Image.open("Tests/images/no_palette_after_rgb.gif") as im: + im.seek(1) + assert im.mode == "RGB" + + im.seek(2) + assert im.mode == "RGB" + + +def test_palette_not_needed_for_second_frame(): + with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im: + im.seek(1) + assert_image_similar(im, hopper("L").convert("RGB"), 8) + + +def test_strategy(): + with Image.open("Tests/images/iss634.gif") as im: + expected_rgb_always = im.convert("RGB") + + with Image.open("Tests/images/chi.gif") as im: + expected_rgb_always_rgba = im.convert("RGBA") + + im.seek(1) + expected_different = im.convert("RGB") + + try: + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS + with Image.open("Tests/images/iss634.gif") as im: + assert im.mode == "RGB" + assert_image_equal(im, expected_rgb_always) + + with Image.open("Tests/images/chi.gif") as im: + assert im.mode == "RGBA" + assert_image_equal(im, expected_rgb_always_rgba) + + GifImagePlugin.LOADING_STRATEGY = ( + GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY + ) + # Stay in P mode with only a global palette + with Image.open("Tests/images/chi.gif") as im: + assert im.mode == "P" + + im.seek(1) + assert im.mode == "P" + assert_image_equal(im.convert("RGB"), expected_different) - # These don't optimize the palette - check(128, 513, 256) - check(64, 513, 256) - check(4, 513, 256) + # Change to RGB mode when a frame has an individual palette + with Image.open("Tests/images/iss634.gif") as im: + assert im.mode == "P" - # other limits that don't optimize the palette - check(129, 511, 256) - check(255, 511, 256) - check(256, 511, 256) + im.seek(1) + assert im.mode == "RGB" + finally: + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST - def test_optimize_full_l(self): - im = Image.frombytes("L", (16, 16), bytes(bytearray(range(256)))) + +def test_optimize(): + def test_grayscale(optimize): + im = Image.new("L", (1, 1), 0) + filename = BytesIO() + im.save(filename, "GIF", optimize=optimize) + return len(filename.getvalue()) + + def test_bilevel(optimize): + im = Image.new("1", (1, 1), 0) test_file = BytesIO() - im.save(test_file, "GIF", optimize=True) - self.assertEqual(im.mode, "L") + im.save(test_file, "GIF", optimize=optimize) + return len(test_file.getvalue()) + + assert test_grayscale(0) == 799 + assert test_grayscale(1) == 43 + assert test_bilevel(0) == 799 + assert test_bilevel(1) == 799 + + +def test_optimize_correctness(): + # 256 color Palette image, posterize to > 128 and < 128 levels + # Size bigger and smaller than 512x512 + # Check the palette for number of colors allocated. + # Check for correctness after conversion back to RGB + def check(colors, size, expected_palette_length): + # make an image with empty colors in the start of the palette range + im = Image.frombytes( + "P", (colors, colors), bytes(range(256 - colors, 256)) * colors + ) + im = im.resize((size, size)) + outfile = BytesIO() + im.save(outfile, "GIF") + outfile.seek(0) + with Image.open(outfile) as reloaded: + # check palette length + palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v) + assert expected_palette_length == palette_length - def test_roundtrip(self): - out = self.tempfile('temp.gif') - im = hopper() - im.save(out) - reread = Image.open(out) + assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) - self.assert_image_similar(reread.convert('RGB'), im, 50) + # These do optimize the palette + check(256, 511, 256) + check(255, 511, 255) + check(129, 511, 129) + check(128, 511, 128) + check(64, 511, 64) + check(4, 511, 4) - def test_roundtrip2(self): - # see https://github.com/python-pillow/Pillow/issues/403 - out = self.tempfile('temp.gif') - im = Image.open(TEST_GIF) + # These don't optimize the palette + check(128, 513, 256) + check(64, 513, 256) + check(4, 513, 256) + + +def test_optimize_full_l(): + im = Image.frombytes("L", (16, 16), bytes(range(256))) + test_file = BytesIO() + im.save(test_file, "GIF", optimize=True) + assert im.mode == "L" + + +def test_optimize_if_palette_can_be_reduced_by_half(): + with Image.open("Tests/images/test.colors.gif") as im: + # Reduce dimensions because original is too big for _get_optimize() + im = im.resize((591, 443)) + im_rgb = im.convert("RGB") + + for (optimize, colors) in ((False, 256), (True, 8)): + out = BytesIO() + im_rgb.save(out, "GIF", optimize=optimize) + with Image.open(out) as reloaded: + assert len(reloaded.palette.palette) // 3 == colors + + +def test_roundtrip(tmp_path): + out = str(tmp_path / "temp.gif") + im = hopper() + im.save(out) + with Image.open(out) as reread: + + assert_image_similar(reread.convert("RGB"), im, 50) + + +def test_roundtrip2(tmp_path): + # see https://github.com/python-pillow/Pillow/issues/403 + out = str(tmp_path / "temp.gif") + with Image.open(TEST_GIF) as im: im2 = im.copy() im2.save(out) - reread = Image.open(out) + with Image.open(out) as reread: - self.assert_image_similar(reread.convert('RGB'), hopper(), 50) + assert_image_similar(reread.convert("RGB"), hopper(), 50) - def test_roundtrip_save_all(self): - # Single frame image - out = self.tempfile('temp.gif') - im = hopper() - im.save(out, save_all=True) - reread = Image.open(out) - self.assert_image_similar(reread.convert('RGB'), im, 50) +def test_roundtrip_save_all(tmp_path): + # Single frame image + out = str(tmp_path / "temp.gif") + im = hopper() + im.save(out, save_all=True) + with Image.open(out) as reread: - # Multiframe image - im = Image.open("Tests/images/dispose_bgnd.gif") + assert_image_similar(reread.convert("RGB"), im, 50) - out = self.tempfile('temp.gif') + # Multiframe image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + out = str(tmp_path / "temp.gif") im.save(out, save_all=True) - reread = Image.open(out) - self.assertEqual(reread.n_frames, 5) + with Image.open(out) as reread: + assert reread.n_frames == 5 + + +@pytest.mark.parametrize( + "path, mode", + ( + ("Tests/images/dispose_bgnd.gif", "RGB"), + # Hexeditted copy of dispose_bgnd to add transparency + ("Tests/images/dispose_bgnd_rgba.gif", "RGBA"), + ), +) +def test_loading_multiple_palettes(path, mode): + with Image.open(path) as im: + assert im.mode == "P" + first_frame_colors = im.palette.colors.keys() + original_color = im.convert("RGB").load()[0, 0] + + im.seek(1) + assert im.mode == mode + if mode == "RGBA": + im = im.convert("RGB") + + # Check a color only from the old palette + assert im.load()[0, 0] == original_color + + # Check a color from the new palette + assert im.load()[24, 24] not in first_frame_colors + - def test_headers_saving_for_animated_gifs(self): - important_headers = ['background', 'version', 'duration', 'loop'] - # Multiframe image - im = Image.open("Tests/images/dispose_bgnd.gif") +def test_headers_saving_for_animated_gifs(tmp_path): + important_headers = ["background", "version", "duration", "loop"] + # Multiframe image + with Image.open("Tests/images/dispose_bgnd.gif") as im: - out = self.tempfile('temp.gif') + info = im.info.copy() + + out = str(tmp_path / "temp.gif") im.save(out, save_all=True) - reread = Image.open(out) + with Image.open(out) as reread: for header in important_headers: - self.assertEqual( - im.info[header], - reread.info[header] - ) + assert info[header] == reread.info[header] - def test_palette_handling(self): - # see https://github.com/python-pillow/Pillow/issues/513 - im = Image.open(TEST_GIF) - im = im.convert('RGB') +def test_palette_handling(tmp_path): + # see https://github.com/python-pillow/Pillow/issues/513 + + with Image.open(TEST_GIF) as im: + im = im.convert("RGB") - im = im.resize((100, 100), Image.LANCZOS) - im2 = im.convert('P', palette=Image.ADAPTIVE, colors=256) + im = im.resize((100, 100), Image.Resampling.LANCZOS) + im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) - f = self.tempfile('temp.gif') + f = str(tmp_path / "temp.gif") im2.save(f, optimize=True) - reloaded = Image.open(f) + with Image.open(f) as reloaded: - self.assert_image_similar(im, reloaded.convert('RGB'), 10) + assert_image_similar(im, reloaded.convert("RGB"), 10) - def test_palette_434(self): - # see https://github.com/python-pillow/Pillow/issues/434 - def roundtrip(im, *args, **kwargs): - out = self.tempfile('temp.gif') - im.copy().save(out, *args, **kwargs) - reloaded = Image.open(out) +def test_palette_434(tmp_path): + # see https://github.com/python-pillow/Pillow/issues/434 - return reloaded + def roundtrip(im, *args, **kwargs): + out = str(tmp_path / "temp.gif") + im.copy().save(out, *args, **kwargs) + reloaded = Image.open(out) + + return reloaded - orig = "Tests/images/test.colors.gif" - im = Image.open(orig) + orig = "Tests/images/test.colors.gif" + with Image.open(orig) as im: - self.assert_image_similar(im, roundtrip(im), 1) - self.assert_image_similar(im, roundtrip(im, optimize=True), 1) + with roundtrip(im) as reloaded: + assert_image_similar(im, reloaded, 1) + with roundtrip(im, optimize=True) as reloaded: + assert_image_similar(im, reloaded, 1) im = im.convert("RGB") # check automatic P conversion - reloaded = roundtrip(im).convert('RGB') - self.assert_image_equal(im, reloaded) + with roundtrip(im) as reloaded: + reloaded = reloaded.convert("RGB") + assert_image_equal(im, reloaded) + - @unittest.skipUnless(netpbm_available(), "netpbm not available") - def test_save_netpbm_bmp_mode(self): - img = Image.open(TEST_GIF).convert("RGB") +@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") +def test_save_netpbm_bmp_mode(tmp_path): + with Image.open(TEST_GIF) as img: + img = img.convert("RGB") - tempfile = self.tempfile("temp.gif") + tempfile = str(tmp_path / "temp.gif") GifImagePlugin._save_netpbm(img, 0, tempfile) - self.assert_image_similar(img, Image.open(tempfile).convert("RGB"), 0) + with Image.open(tempfile) as reloaded: + assert_image_similar(img, reloaded.convert("RGB"), 0) - @unittest.skipUnless(netpbm_available(), "netpbm not available") - def test_save_netpbm_l_mode(self): - img = Image.open(TEST_GIF).convert("L") - tempfile = self.tempfile("temp.gif") +@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") +def test_save_netpbm_l_mode(tmp_path): + with Image.open(TEST_GIF) as img: + img = img.convert("L") + + tempfile = str(tmp_path / "temp.gif") GifImagePlugin._save_netpbm(img, 0, tempfile) - self.assert_image_similar(img, Image.open(tempfile).convert("L"), 0) + with Image.open(tempfile) as reloaded: + assert_image_similar(img, reloaded.convert("L"), 0) + - def test_seek(self): - img = Image.open("Tests/images/dispose_none.gif") - framecount = 0 +def test_seek(): + with Image.open("Tests/images/dispose_none.gif") as img: + frame_count = 0 try: while True: - framecount += 1 + frame_count += 1 img.seek(img.tell() + 1) except EOFError: - self.assertEqual(framecount, 5) - - def test_n_frames(self): - for path, n_frames in [ - [TEST_GIF, 1], - ['Tests/images/iss634.gif', 42] - ]: - # Test is_animated before n_frames - im = Image.open(path) - self.assertEqual(im.is_animated, n_frames != 1) - - # Test is_animated after n_frames - im = Image.open(path) - self.assertEqual(im.n_frames, n_frames) - self.assertEqual(im.is_animated, n_frames != 1) - - def test_eoferror(self): - im = Image.open(TEST_GIF) + assert frame_count == 5 + + +def test_seek_info(): + with Image.open("Tests/images/iss634.gif") as im: + info = im.info.copy() + + im.seek(1) + im.seek(0) + + assert im.info == info + + +def test_seek_rewind(): + with Image.open("Tests/images/iss634.gif") as im: + im.seek(2) + im.seek(1) + + with Image.open("Tests/images/iss634.gif") as expected: + expected.seek(1) + assert_image_equal(im, expected) + + +@pytest.mark.parametrize( + "path, n_frames", + ( + (TEST_GIF, 1), + ("Tests/images/comment_after_last_frame.gif", 2), + ("Tests/images/iss634.gif", 42), + ), +) +def test_n_frames(path, n_frames): + # Test is_animated before n_frames + with Image.open(path) as im: + assert im.is_animated == (n_frames != 1) + + # Test is_animated after n_frames + with Image.open(path) as im: + assert im.n_frames == n_frames + assert im.is_animated == (n_frames != 1) + + +def test_no_change(): + # Test n_frames does not change the image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + im.seek(1) + expected = im.copy() + assert im.n_frames == 5 + assert_image_equal(im, expected) + + # Test is_animated does not change the image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + im.seek(3) + expected = im.copy() + assert im.is_animated + assert_image_equal(im, expected) + + with Image.open("Tests/images/comment_after_only_frame.gif") as im: + expected = Image.new("P", (1, 1)) + assert not im.is_animated + assert_image_equal(im, expected) + + +def test_eoferror(): + with Image.open(TEST_GIF) as im: n_frames = im.n_frames # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames # Test that seeking to the last frame does not raise an error - im.seek(n_frames-1) + im.seek(n_frames - 1) - def test_dispose_none(self): - img = Image.open("Tests/images/dispose_none.gif") + +def test_first_frame_transparency(): + with Image.open("Tests/images/first_frame_transparency.gif") as im: + px = im.load() + assert px[0, 0] == im.info["transparency"] + + +def test_dispose_none(): + with Image.open("Tests/images/dispose_none.gif") as img: try: while True: img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, 1) + assert img.disposal_method == 1 except EOFError: pass - def test_dispose_background(self): - img = Image.open("Tests/images/dispose_bgnd.gif") + +def test_dispose_none_load_end(): + # Test image created with: + # + # im = Image.open("transparent.gif") + # im_rotated = im.rotate(180) + # im.save("dispose_none_load_end.gif", + # save_all=True, append_images=[im_rotated], disposal=[1,2]) + with Image.open("Tests/images/dispose_none_load_end.gif") as img: + img.seek(1) + + assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.png") + + +def test_dispose_background(): + with Image.open("Tests/images/dispose_bgnd.gif") as img: try: while True: img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, 2) + assert img.disposal_method == 2 except EOFError: pass - def test_dispose_previous(self): - img = Image.open("Tests/images/dispose_prev.gif") + +def test_dispose_background_transparency(): + with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img: + img.seek(2) + px = img.load() + assert px[35, 30][3] == 0 + + +@pytest.mark.parametrize( + "loading_strategy, expected_colors", + ( + ( + GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST, + ( + (2, 1, 2), + ((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)), + ((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)), + ), + ), + ( + GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY, + ( + (2, 1, 2), + (0, 1, 0), + (2, 1, 2), + ), + ), + ), +) +def test_transparent_dispose(loading_strategy, expected_colors): + GifImagePlugin.LOADING_STRATEGY = loading_strategy + try: + with Image.open("Tests/images/transparent_dispose.gif") as img: + for frame in range(3): + img.seek(frame) + for x in range(3): + color = img.getpixel((x, 0)) + assert color == expected_colors[frame][x] + finally: + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST + + +def test_dispose_previous(): + with Image.open("Tests/images/dispose_prev.gif") as img: try: while True: img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, 3) + assert img.disposal_method == 3 except EOFError: pass - def test_save_dispose(self): - out = self.tempfile('temp.gif') - im_list = [ - Image.new('L', (100, 100), '#000'), - Image.new('L', (100, 100), '#111'), - Image.new('L', (100, 100), '#222'), - ] - for method in range(0,4): - im_list[0].save( - out, - save_all=True, - append_images=im_list[1:], - disposal=method - ) - img = Image.open(out) + +def test_dispose_previous_first_frame(): + with Image.open("Tests/images/dispose_prev_first_frame.gif") as im: + im.seek(1) + assert_image_equal_tofile( + im, "Tests/images/dispose_prev_first_frame_seeked.png" + ) + + +def test_previous_frame_loaded(): + with Image.open("Tests/images/dispose_none.gif") as img: + img.load() + img.seek(1) + img.load() + img.seek(2) + with Image.open("Tests/images/dispose_none.gif") as img_skipped: + img_skipped.seek(2) + assert_image_equal(img_skipped, img) + + +def test_save_dispose(tmp_path): + out = str(tmp_path / "temp.gif") + im_list = [ + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#111"), + Image.new("L", (100, 100), "#222"), + ] + for method in range(0, 4): + im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method) + with Image.open(out) as img: for _ in range(2): img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, method) - + assert img.disposal_method == method - # check per frame disposal - im_list[0].save( - out, - save_all=True, - append_images=im_list[1:], - disposal=tuple(range(len(im_list))) - ) + # Check per frame disposal + im_list[0].save( + out, + save_all=True, + append_images=im_list[1:], + disposal=tuple(range(len(im_list))), + ) - img = Image.open(out) + with Image.open(out) as img: for i in range(2): img.seek(img.tell() + 1) - self.assertEqual(img.disposal_method, i+1) + assert img.disposal_method == i + 1 - def test_iss634(self): - img = Image.open("Tests/images/iss634.gif") - # seek to the second frame - img.seek(img.tell() + 1) - # all transparent pixels should be replaced with the color from the - # first frame - self.assertEqual(img.histogram()[img.info['transparency']], 0) - - def test_duration(self): - duration = 1000 - - out = self.tempfile('temp.gif') - im = Image.new('L', (100, 100), '#000') - im.save(out, duration=duration) - reread = Image.open(out) - - self.assertEqual(reread.info['duration'], duration) - - def test_multiple_duration(self): - duration_list = [1000, 2000, 3000] - - out = self.tempfile('temp.gif') - im_list = [ - Image.new('L', (100, 100), '#000'), - Image.new('L', (100, 100), '#111'), - Image.new('L', (100, 100), '#222') - ] - - # duration as list - im_list[0].save( - out, - save_all=True, - append_images=im_list[1:], - duration=duration_list + +def test_dispose2_palette(tmp_path): + out = str(tmp_path / "temp.gif") + + # Four colors: white, grey, black, red + circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] + + im_list = [] + for circle in circles: + # Red background + img = Image.new("RGB", (100, 100), (255, 0, 0)) + + # Circle in center of each frame + d = ImageDraw.Draw(img) + d.ellipse([(40, 40), (60, 60)], fill=circle) + + im_list.append(img) + + im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) + + with Image.open(out) as img: + for i, circle in enumerate(circles): + img.seek(i) + rgb_img = img.convert("RGB") + + # Check top left pixel matches background + assert rgb_img.getpixel((0, 0)) == (255, 0, 0) + + # Center remains red every frame + assert rgb_img.getpixel((50, 50)) == circle + + +def test_dispose2_diff(tmp_path): + out = str(tmp_path / "temp.gif") + + # 4 frames: red/blue, red/red, blue/blue, red/blue + circles = [ + ((255, 0, 0, 255), (0, 0, 255, 255)), + ((255, 0, 0, 255), (255, 0, 0, 255)), + ((0, 0, 255, 255), (0, 0, 255, 255)), + ((255, 0, 0, 255), (0, 0, 255, 255)), + ] + + im_list = [] + for i in range(len(circles)): + # Transparent BG + img = Image.new("RGBA", (100, 100), (255, 255, 255, 0)) + + # Two circles per frame + d = ImageDraw.Draw(img) + d.ellipse([(0, 30), (40, 70)], fill=circles[i][0]) + d.ellipse([(60, 30), (100, 70)], fill=circles[i][1]) + + im_list.append(img) + + im_list[0].save( + out, save_all=True, append_images=im_list[1:], disposal=2, transparency=0 + ) + + with Image.open(out) as img: + for i, colours in enumerate(circles): + img.seek(i) + rgb_img = img.convert("RGBA") + + # Check left circle is correct colour + assert rgb_img.getpixel((20, 50)) == colours[0] + + # Check right circle is correct colour + assert rgb_img.getpixel((80, 50)) == colours[1] + + # Check BG is correct colour + assert rgb_img.getpixel((1, 1)) == (255, 255, 255, 0) + + +def test_dispose2_background(tmp_path): + out = str(tmp_path / "temp.gif") + + im_list = [] + + im = Image.new("P", (100, 100)) + d = ImageDraw.Draw(im) + d.rectangle([(50, 0), (100, 100)], fill="#f00") + d.rectangle([(0, 0), (50, 100)], fill="#0f0") + im_list.append(im) + + im = Image.new("P", (100, 100)) + d = ImageDraw.Draw(im) + d.rectangle([(0, 0), (100, 50)], fill="#f00") + d.rectangle([(0, 50), (100, 100)], fill="#0f0") + im_list.append(im) + + im_list[0].save( + out, save_all=True, append_images=im_list[1:], disposal=[0, 2], background=1 + ) + + with Image.open(out) as im: + im.seek(1) + assert im.getpixel((0, 0)) == (255, 0, 0) + + +def test_transparency_in_second_frame(tmp_path): + out = str(tmp_path / "temp.gif") + with Image.open("Tests/images/different_transparency.gif") as im: + assert im.info["transparency"] == 0 + + # Seek to the second frame + im.seek(im.tell() + 1) + assert "transparency" not in im.info + + assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png") + + im.save(out, save_all=True) + + with Image.open(out) as reread: + reread.seek(reread.tell() + 1) + assert_image_equal_tofile( + reread, "Tests/images/different_transparency_merged.png" ) - reread = Image.open(out) + + +def test_no_transparency_in_second_frame(): + with Image.open("Tests/images/iss634.gif") as img: + # Seek to the second frame + img.seek(img.tell() + 1) + assert "transparency" not in img.info + + # All transparent pixels should be replaced with the color from the first frame + assert img.histogram()[255] == 0 + + +def test_remapped_transparency(tmp_path): + out = str(tmp_path / "temp.gif") + + im = Image.new("P", (1, 2)) + im2 = im.copy() + + # Add transparency at a higher index + # so that it will be optimized to a lower index + im.putpixel((0, 1), 5) + im.info["transparency"] = 5 + im.save(out, save_all=True, append_images=[im2]) + + with Image.open(out) as reloaded: + assert reloaded.info["transparency"] == reloaded.getpixel((0, 1)) + + +def test_duration(tmp_path): + duration = 1000 + + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + + # Check that the argument has priority over the info settings + im.info["duration"] = 100 + im.save(out, duration=duration) + + with Image.open(out) as reread: + assert reread.info["duration"] == duration + + +def test_multiple_duration(tmp_path): + duration_list = [1000, 2000, 3000] + + out = str(tmp_path / "temp.gif") + im_list = [ + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#111"), + Image.new("L", (100, 100), "#222"), + ] + + # Duration as list + im_list[0].save( + out, save_all=True, append_images=im_list[1:], duration=duration_list + ) + with Image.open(out) as reread: for duration in duration_list: - self.assertEqual(reread.info['duration'], duration) + assert reread.info["duration"] == duration try: reread.seek(reread.tell() + 1) except EOFError: pass - # duration as tuple - im_list[0].save( - out, - save_all=True, - append_images=im_list[1:], - duration=tuple(duration_list) - ) - reread = Image.open(out) + # Duration as tuple + im_list[0].save( + out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list) + ) + with Image.open(out) as reread: for duration in duration_list: - self.assertEqual(reread.info['duration'], duration) + assert reread.info["duration"] == duration try: reread.seek(reread.tell() + 1) except EOFError: pass - def test_identical_frames(self): - duration_list = [1000, 1500, 2000, 4000] - - out = self.tempfile('temp.gif') - im_list = [ - Image.new('L', (100, 100), '#000'), - Image.new('L', (100, 100), '#000'), - Image.new('L', (100, 100), '#000'), - Image.new('L', (100, 100), '#111') - ] - - # duration as list - im_list[0].save( - out, - save_all=True, - append_images=im_list[1:], - duration=duration_list - ) - reread = Image.open(out) + +def test_roundtrip_info_duration(tmp_path): + duration_list = [100, 500, 500] + + out = str(tmp_path / "temp.gif") + with Image.open("Tests/images/transparent_dispose.gif") as im: + assert [ + frame.info["duration"] for frame in ImageSequence.Iterator(im) + ] == duration_list + + im.save(out, save_all=True) + + with Image.open(out) as reloaded: + assert [ + frame.info["duration"] for frame in ImageSequence.Iterator(reloaded) + ] == duration_list + + +def test_identical_frames(tmp_path): + duration_list = [1000, 1500, 2000, 4000] + + out = str(tmp_path / "temp.gif") + im_list = [ + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#111"), + ] + + # Duration as list + im_list[0].save( + out, save_all=True, append_images=im_list[1:], duration=duration_list + ) + with Image.open(out) as reread: # Assert that the first three frames were combined - self.assertEqual(reread.n_frames, 2) + assert reread.n_frames == 2 # Assert that the new duration is the total of the identical frames - self.assertEqual(reread.info['duration'], 4500) + assert reread.info["duration"] == 4500 - def test_number_of_loops(self): - number_of_loops = 2 - out = self.tempfile('temp.gif') - im = Image.new('L', (100, 100), '#000') - im.save(out, loop=number_of_loops) - reread = Image.open(out) +@pytest.mark.parametrize( + "duration", ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500) +) +def test_identical_frames_to_single_frame(duration, tmp_path): + out = str(tmp_path / "temp.gif") + im_list = [ + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + ] - self.assertEqual(reread.info['loop'], number_of_loops) + im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration) + with Image.open(out) as reread: + # Assert that all frames were combined + assert reread.n_frames == 1 - def test_background(self): - out = self.tempfile('temp.gif') - im = Image.new('L', (100, 100), '#000') - im.info['background'] = 1 - im.save(out) - reread = Image.open(out) + # Assert that the new duration is the total of the identical frames + assert reread.info["duration"] == 8500 - self.assertEqual(reread.info['background'], im.info['background']) - def test_comment(self): - im = Image.open(TEST_GIF) - self.assertEqual(im.info['comment'], b"File written by Adobe Photoshop\xa8 4.0") +def test_number_of_loops(tmp_path): + number_of_loops = 2 - out = self.tempfile('temp.gif') - im = Image.new('L', (100, 100), '#000') - im.info['comment'] = b"Test comment text" - im.save(out) - reread = Image.open(out) + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + im.save(out, loop=number_of_loops) + with Image.open(out) as reread: + assert reread.info["loop"] == number_of_loops + + # Check that even if a subsequent GIF frame has the number of loops specified, + # only the value from the first frame is used + with Image.open("Tests/images/duplicate_number_of_loops.gif") as im: + assert im.info["loop"] == 2 + + im.seek(1) + assert im.info["loop"] == 2 - self.assertEqual(reread.info['comment'], im.info['comment']) - def test_version(self): - out = self.tempfile('temp.gif') +def test_background(tmp_path): + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + im.info["background"] = 1 + im.save(out) + with Image.open(out) as reread: - def assertVersionAfterSave(im, version): + assert reread.info["background"] == im.info["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) im.save(out) - reread = Image.open(out) - self.assertEqual(reread.info["version"], version) - # Test that GIF87a is used by default - im = Image.new('L', (100, 100), '#000') - assertVersionAfterSave(im, b"GIF87a") - # Test setting the version to 89a - im = Image.new('L', (100, 100), '#000') - im.info["version"] = b"89a" - assertVersionAfterSave(im, b"GIF89a") +def test_comment(tmp_path): + with Image.open(TEST_GIF) as im: + assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" + + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + im.info["comment"] = b"Test comment text" + im.save(out) + with Image.open(out) as reread: + assert reread.info["comment"] == im.info["comment"] + + im.info["comment"] = "Test comment text" + im.save(out) + with Image.open(out) as reread: + assert reread.info["comment"] == im.info["comment"].encode() + + # Test that GIF89a is used for comments + assert reread.info["version"] == b"GIF89a" + + +def test_comment_over_255(tmp_path): + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + comment = b"Test comment text" + while len(comment) < 256: + comment += comment + im.info["comment"] = comment + im.save(out) + with Image.open(out) as reread: + assert reread.info["comment"] == comment + + # Test that GIF89a is used for comments + assert reread.info["version"] == b"GIF89a" + + +def test_zero_comment_subblocks(): + with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im: + assert_image_equal_tofile(im, TEST_GIF) + + +def test_read_multiple_comment_blocks(): + with Image.open("Tests/images/multiple_comments.gif") as im: + # Multiple comment blocks in a frame are separated not concatenated + assert im.info["comment"] == b"Test comment 1\nTest comment 2" + + +def test_empty_string_comment(tmp_path): + out = str(tmp_path / "temp.gif") + with Image.open("Tests/images/chi.gif") as im: + assert "comment" in im.info + + # Empty string comment should suppress existing comment + im.save(out, save_all=True, comment="") + + with Image.open(out) as reread: + for frame in ImageSequence.Iterator(reread): + assert "comment" not in frame.info + + +def test_retain_comment_in_subsequent_frames(tmp_path): + # Test that a comment block at the beginning is kept + with Image.open("Tests/images/chi.gif") as im: + for frame in ImageSequence.Iterator(im): + assert frame.info["comment"] == b"Created with GIMP" + + with Image.open("Tests/images/second_frame_comment.gif") as im: + assert "comment" not in im.info - # Test that adding a GIF89a feature changes the version - im.info["transparency"] = 1 - assertVersionAfterSave(im, b"GIF89a") + # Test that a comment in the middle is read + im.seek(1) + assert im.info["comment"] == b"Comment in the second frame" - # Test that a GIF87a image is also saved in that format - im = Image.open("Tests/images/test.colors.gif") - assertVersionAfterSave(im, b"GIF87a") + # Test that it is still present in a later frame + im.seek(2) + assert im.info["comment"] == b"Comment in the second frame" + + # Test that rewinding removes the comment + im.seek(0) + assert "comment" not in im.info + + # Test that a saved image keeps the comment + out = str(tmp_path / "temp.gif") + with Image.open("Tests/images/dispose_prev.gif") as im: + im.save(out, save_all=True, comment="Test") + + with Image.open(out) as reread: + for frame in ImageSequence.Iterator(reread): + assert frame.info["comment"] == b"Test" + + +def test_version(tmp_path): + out = str(tmp_path / "temp.gif") + + def assert_version_after_save(im, version): + im.save(out) + with Image.open(out) as reread: + assert reread.info["version"] == version + + # Test that GIF87a is used by default + im = Image.new("L", (100, 100), "#000") + assert_version_after_save(im, b"GIF87a") + + # Test setting the version to 89a + im = Image.new("L", (100, 100), "#000") + im.info["version"] = b"89a" + assert_version_after_save(im, b"GIF89a") + + # Test that adding a GIF89a feature changes the version + im.info["transparency"] = 1 + assert_version_after_save(im, b"GIF89a") + + # Test that a GIF87a image is also saved in that format + with Image.open("Tests/images/test.colors.gif") as im: + assert_version_after_save(im, b"GIF87a") # Test that a GIF89a image is also saved in that format im.info["version"] = b"GIF89a" - assertVersionAfterSave(im, b"GIF87a") + assert_version_after_save(im, b"GIF87a") - def test_append_images(self): - out = self.tempfile('temp.gif') - # Test appending single frame images - im = Image.new('RGB', (100, 100), '#f00') - ims = [Image.new('RGB', (100, 100), color) for color - in ['#0f0', '#00f']] - im.copy().save(out, save_all=True, append_images=ims) +def test_append_images(tmp_path): + out = str(tmp_path / "temp.gif") - reread = Image.open(out) - self.assertEqual(reread.n_frames, 3) + # Test appending single frame images + im = Image.new("RGB", (100, 100), "#f00") + ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] + im.copy().save(out, save_all=True, append_images=ims) - # Tests appending using a generator - def imGenerator(ims): - for im in ims: - yield im - im.save(out, save_all=True, append_images=imGenerator(ims)) + with Image.open(out) as reread: + assert reread.n_frames == 3 - reread = Image.open(out) - self.assertEqual(reread.n_frames, 3) + # Tests appending using a generator + def im_generator(ims): + yield from ims - # Tests appending single and multiple frame images - im = Image.open("Tests/images/dispose_none.gif") - ims = [Image.open("Tests/images/dispose_prev.gif")] - im.save(out, save_all=True, append_images=ims) + im.save(out, save_all=True, append_images=im_generator(ims)) - reread = Image.open(out) - self.assertEqual(reread.n_frames, 10) + with Image.open(out) as reread: + assert reread.n_frames == 3 - def test_transparent_optimize(self): - # from issue #2195, if the transparent color is incorrectly - # optimized out, gif loses transparency - # Need a palette that isn't using the 0 color, and one - # that's > 128 items where the transparent color is actually - # the top palette entry to trigger the bug. + # Tests appending single and multiple frame images + with Image.open("Tests/images/dispose_none.gif") as im: + with Image.open("Tests/images/dispose_prev.gif") as im2: + im.save(out, save_all=True, append_images=[im2]) - from PIL import ImagePalette + with Image.open(out) as reread: + assert reread.n_frames == 10 - data = bytes(bytearray(range(1, 254))) - palette = ImagePalette.ImagePalette("RGB", list(range(256))*3) - im = Image.new('L', (253, 1)) - im.frombytes(data) - im.putpalette(palette) +def test_transparent_optimize(tmp_path): + # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses + # transparency. + # Need a palette that isn't using the 0 color, + # where the transparent color is actually the top palette entry to trigger the bug. - out = self.tempfile('temp.gif') - im.save(out, transparency=253) - reloaded = Image.open(out) + data = bytes(range(1, 254)) + palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) - self.assertEqual(reloaded.info['transparency'], 253) + im = Image.new("L", (253, 1)) + im.frombytes(data) + im.putpalette(palette) - def test_bbox(self): - out = self.tempfile('temp.gif') + out = str(tmp_path / "temp.gif") + im.save(out, transparency=im.getpixel((252, 0))) - im = Image.new('RGB', (100, 100), '#fff') - ims = [Image.new("RGB", (100, 100), '#000')] - im.save(out, save_all=True, append_images=ims) + with Image.open(out) as reloaded: + assert reloaded.info["transparency"] == reloaded.getpixel((252, 0)) - reread = Image.open(out) - self.assertEqual(reread.n_frames, 2) - def test_palette_save_L(self): - # generate an L mode image with a separate palette +def test_rgb_transparency(tmp_path): + out = str(tmp_path / "temp.gif") - im = hopper('P') - im_l = Image.frombytes('L', im.size, im.tobytes()) - palette = bytes(bytearray(im.getpalette())) + # Single frame + im = Image.new("RGB", (1, 1)) + im.info["transparency"] = (255, 0, 0) + im.save(out) - out = self.tempfile('temp.gif') - im_l.save(out, palette=palette) + with Image.open(out) as reloaded: + assert "transparency" in reloaded.info - reloaded = Image.open(out) + # Multiple frames + im = Image.new("RGB", (1, 1)) + im.info["transparency"] = b"" + ims = [Image.new("RGB", (1, 1))] + pytest.warns(UserWarning, im.save, out, save_all=True, append_images=ims) - self.assert_image_equal(reloaded.convert('RGB'), im.convert('RGB')) + with Image.open(out) as reloaded: + assert "transparency" not in reloaded.info - def test_palette_save_P(self): - # pass in a different palette, then construct what the image - # would look like. - # Forcing a non-straight grayscale palette. - im = hopper('P') - palette = bytes(bytearray([255-i//3 for i in range(768)])) +def test_rgba_transparency(tmp_path): + out = str(tmp_path / "temp.gif") - out = self.tempfile('temp.gif') - im.save(out, palette=palette) + im = hopper("P") + im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)]) - reloaded = Image.open(out) + with Image.open(out) as reloaded: + reloaded.seek(1) + assert_image_equal(hopper("P").convert("RGB"), reloaded) + + +def test_bbox(tmp_path): + out = str(tmp_path / "temp.gif") + + im = Image.new("RGB", (100, 100), "#fff") + ims = [Image.new("RGB", (100, 100), "#000")] + im.save(out, save_all=True, append_images=ims) + + with Image.open(out) as reread: + assert reread.n_frames == 2 + + +def test_palette_save_L(tmp_path): + # Generate an L mode image with a separate palette + + im = hopper("P") + im_l = Image.frombytes("L", im.size, im.tobytes()) + palette = bytes(im.getpalette()) + + out = str(tmp_path / "temp.gif") + im_l.save(out, palette=palette) + + with Image.open(out) as reloaded: + assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) + + +def test_palette_save_P(tmp_path): + # Pass in a different palette, then construct what the image would look like. + # Forcing a non-straight grayscale palette. + + im = hopper("P") + palette = bytes(255 - i // 3 for i in range(768)) + + out = str(tmp_path / "temp.gif") + im.save(out, palette=palette) + + with Image.open(out) as reloaded: im.putpalette(palette) - self.assert_image_equal(reloaded, im) + assert_image_equal(reloaded, im) - def test_palette_save_ImagePalette(self): - # pass in a different palette, as an ImagePalette.ImagePalette - # effectively the same as test_palette_save_P - im = hopper('P') - palette = ImagePalette.ImagePalette('RGB', list(range(256))[::-1]*3) +def test_palette_save_duplicate_entries(tmp_path): + im = Image.new("P", (1, 2)) + im.putpixel((0, 1), 1) - out = self.tempfile('temp.gif') - im.save(out, palette=palette) + im.putpalette((0, 0, 0, 0, 0, 0)) - reloaded = Image.open(out) + out = str(tmp_path / "temp.gif") + im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1]) + + with Image.open(out) as reloaded: + assert reloaded.convert("RGB").getpixel((0, 1)) == (0, 0, 0) + + +def test_palette_save_all_P(tmp_path): + frames = [] + colors = ((255, 0, 0), (0, 255, 0)) + for color in colors: + frame = Image.new("P", (100, 100)) + frame.putpalette(color) + frames.append(frame) + + out = str(tmp_path / "temp.gif") + frames[0].save( + out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:] + ) + + with Image.open(out) as im: + # Assert that the frames are correct, and each frame has the same palette + assert_image_equal(im.convert("RGB"), frames[0].convert("RGB")) + assert im.palette.palette == im.global_palette.palette + + im.seek(1) + assert_image_equal(im.convert("RGB"), frames[1].convert("RGB")) + assert im.palette.palette == im.global_palette.palette + + +def test_palette_save_ImagePalette(tmp_path): + # Pass in a different palette, as an ImagePalette.ImagePalette + # effectively the same as test_palette_save_P + + im = hopper("P") + palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3) + + out = str(tmp_path / "temp.gif") + im.save(out, palette=palette) + + with Image.open(out) as reloaded: im.putpalette(palette) - self.assert_image_equal(reloaded, im) + assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) - def test_save_I(self): - # Test saving something that would trigger the auto-convert to 'L' - im = hopper('I') +def test_save_I(tmp_path): + # Test saving something that would trigger the auto-convert to 'L' - out = self.tempfile('temp.gif') - im.save(out) + im = hopper("I") - reloaded = Image.open(out) - self.assert_image_equal(reloaded.convert('L'), im.convert('L')) + out = str(tmp_path / "temp.gif") + im.save(out) - def test_getdata(self): - # test getheader/getdata against legacy values - # Create a 'P' image with holes in the palette - im = Image._wedge().resize((16, 16)) - im.putpalette(ImagePalette.ImagePalette('RGB')) - im.info = {'background': 0} + with Image.open(out) as reloaded: + assert_image_equal(reloaded.convert("L"), im.convert("L")) - passed_palette = bytes(bytearray([255-i//3 for i in range(768)])) - GifImagePlugin._FORCE_OPTIMIZE = True - try: - h = GifImagePlugin.getheader(im, passed_palette) - d = GifImagePlugin.getdata(im) - - import pickle - # Enable to get target values on pre-refactor version - # with open('Tests/images/gif_header_data.pkl', 'wb') as f: - # pickle.dump((h, d), f, 1) - with open('Tests/images/gif_header_data.pkl', 'rb') as f: - (h_target, d_target) = pickle.load(f) - - self.assertEqual(h, h_target) - self.assertEqual(d, d_target) - finally: - GifImagePlugin._FORCE_OPTIMIZE = False - - def test_lzw_bits(self): - # see https://github.com/python-pillow/Pillow/issues/2811 - im = Image.open('Tests/images/issue_2811.gif') - - self.assertEqual(im.tile[0][3][0], 11) # LZW bits +def test_getdata(): + # Test getheader/getdata against legacy values. + # Create a 'P' image with holes in the palette. + im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST) + im.putpalette(ImagePalette.ImagePalette("RGB")) + im.info = {"background": 0} + + passed_palette = bytes(255 - i // 3 for i in range(768)) + + GifImagePlugin._FORCE_OPTIMIZE = True + try: + h = GifImagePlugin.getheader(im, passed_palette) + d = GifImagePlugin.getdata(im) + + import pickle + + # Enable to get target values on pre-refactor version + # with open('Tests/images/gif_header_data.pkl', 'wb') as f: + # pickle.dump((h, d), f, 1) + with open("Tests/images/gif_header_data.pkl", "rb") as f: + (h_target, d_target) = pickle.load(f) + + assert h == h_target + assert d == d_target + finally: + GifImagePlugin._FORCE_OPTIMIZE = False + + +def test_lzw_bits(): + # see https://github.com/python-pillow/Pillow/issues/2811 + with Image.open("Tests/images/issue_2811.gif") as im: + assert im.tile[0][3][0] == 11 # LZW bits # codec error prepatch im.load() -if __name__ == '__main__': - unittest.main() + +def test_extents(): + with Image.open("Tests/images/test_extents.gif") as im: + assert im.size == (100, 100) + + # Check that n_frames does not change the size + assert im.n_frames == 2 + assert im.size == (100, 100) + + im.seek(1) + assert im.size == (150, 150) + + +def test_missing_background(): + # The Global Color Table Flag isn't set, so there is no background color index, + # but the disposal method is "Restore to background color" + with Image.open("Tests/images/missing_background.gif") as im: + im.seek(1) + assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.png") + + +def test_saving_rgba(tmp_path): + out = str(tmp_path / "temp.gif") + with Image.open("Tests/images/transparent.png") as im: + im.save(out) + + with Image.open(out) as reloaded: + reloaded_rgba = reloaded.convert("RGBA") + assert reloaded_rgba.load()[0, 0][3] == 0 diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py index b29f6f13b0a..3f056fdae1d 100644 --- a/Tests/test_file_gimpgradient.py +++ b/Tests/test_file_gimpgradient.py @@ -1,125 +1,124 @@ -from helper import unittest, PillowTestCase +from PIL import GimpGradientFile, ImagePalette -from PIL import GimpGradientFile +def test_linear_pos_le_middle(): + # Arrange + middle = 0.5 + pos = 0.25 -class TestImage(PillowTestCase): + # Act + ret = GimpGradientFile.linear(middle, pos) - def test_linear_pos_le_middle(self): - # Arrange - middle = 0.5 - pos = 0.25 + # Assert + assert ret == 0.25 - # Act - ret = GimpGradientFile.linear(middle, pos) - # Assert - self.assertEqual(ret, 0.25) +def test_linear_pos_le_small_middle(): + # Arrange + middle = 1e-11 + pos = 1e-12 - def test_linear_pos_le_small_middle(self): - # Arrange - middle = 1e-11 - pos = 1e-12 + # Act + ret = GimpGradientFile.linear(middle, pos) - # Act - ret = GimpGradientFile.linear(middle, pos) + # Assert + assert ret == 0.0 - # Assert - self.assertEqual(ret, 0.0) - def test_linear_pos_gt_middle(self): - # Arrange - middle = 0.5 - pos = 0.75 +def test_linear_pos_gt_middle(): + # Arrange + middle = 0.5 + pos = 0.75 - # Act - ret = GimpGradientFile.linear(middle, pos) + # Act + ret = GimpGradientFile.linear(middle, pos) - # Assert - self.assertEqual(ret, 0.75) + # Assert + assert ret == 0.75 - def test_linear_pos_gt_small_middle(self): - # Arrange - middle = 1 - 1e-11 - pos = 1 - 1e-12 - # Act - ret = GimpGradientFile.linear(middle, pos) +def test_linear_pos_gt_small_middle(): + # Arrange + middle = 1 - 1e-11 + pos = 1 - 1e-12 - # Assert - self.assertEqual(ret, 1.0) + # Act + ret = GimpGradientFile.linear(middle, pos) - def test_curved(self): - # Arrange - middle = 0.5 - pos = 0.75 + # Assert + assert ret == 1.0 - # Act - ret = GimpGradientFile.curved(middle, pos) - # Assert - self.assertEqual(ret, 0.75) +def test_curved(): + # Arrange + middle = 0.5 + pos = 0.75 - def test_sine(self): - # Arrange - middle = 0.5 - pos = 0.75 + # Act + ret = GimpGradientFile.curved(middle, pos) - # Act - ret = GimpGradientFile.sine(middle, pos) + # Assert + assert ret == 0.75 - # Assert - self.assertEqual(ret, 0.8535533905932737) - def test_sphere_increasing(self): - # Arrange - middle = 0.5 - pos = 0.75 +def test_sine(): + # Arrange + middle = 0.5 + pos = 0.75 - # Act - ret = GimpGradientFile.sphere_increasing(middle, pos) + # Act + ret = GimpGradientFile.sine(middle, pos) - # Assert - self.assertAlmostEqual(ret, 0.9682458365518543) + # Assert + assert ret == 0.8535533905932737 - def test_sphere_decreasing(self): - # Arrange - middle = 0.5 - pos = 0.75 - # Act - ret = GimpGradientFile.sphere_decreasing(middle, pos) +def test_sphere_increasing(): + # Arrange + middle = 0.5 + pos = 0.75 - # Assert - self.assertEqual(ret, 0.3385621722338523) + # Act + ret = GimpGradientFile.sphere_increasing(middle, pos) - def test_load_via_imagepalette(self): - # Arrange - from PIL import ImagePalette - test_file = "Tests/images/gimp_gradient.ggr" + # Assert + assert round(abs(ret - 0.9682458365518543), 7) == 0 - # Act - palette = ImagePalette.load(test_file) - # Assert - # load returns raw palette information - self.assertEqual(len(palette[0]), 1024) - self.assertEqual(palette[1], "RGBA") +def test_sphere_decreasing(): + # Arrange + middle = 0.5 + pos = 0.75 - def test_load_1_3_via_imagepalette(self): - # Arrange - from PIL import ImagePalette - # GIMP 1.3 gradient files contain a name field - test_file = "Tests/images/gimp_gradient_with_name.ggr" + # Act + ret = GimpGradientFile.sphere_decreasing(middle, pos) - # Act - palette = ImagePalette.load(test_file) + # Assert + assert ret == 0.3385621722338523 - # Assert - # load returns raw palette information - self.assertEqual(len(palette[0]), 1024) - self.assertEqual(palette[1], "RGBA") +def test_load_via_imagepalette(): + # Arrange + test_file = "Tests/images/gimp_gradient.ggr" -if __name__ == '__main__': - unittest.main() + # Act + palette = ImagePalette.load(test_file) + + # Assert + # load returns raw palette information + assert len(palette[0]) == 1024 + assert palette[1] == "RGBA" + + +def test_load_1_3_via_imagepalette(): + # Arrange + # GIMP 1.3 gradient files contain a name field + test_file = "Tests/images/gimp_gradient_with_name.ggr" + + # Act + palette = ImagePalette.load(test_file) + + # Assert + # load returns raw palette information + assert len(palette[0]) == 1024 + assert palette[1] == "RGBA" diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index 4ee5323bc4b..caec9cf2115 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -1,34 +1,32 @@ -from helper import unittest, PillowTestCase +import pytest from PIL.GimpPaletteFile import GimpPaletteFile -class TestImage(PillowTestCase): +def test_sanity(): + with open("Tests/images/test.gpl", "rb") as fp: + GimpPaletteFile(fp) - def test_sanity(self): - with open('Tests/images/test.gpl', 'rb') as fp: + with open("Tests/images/hopper.jpg", "rb") as fp: + with pytest.raises(SyntaxError): GimpPaletteFile(fp) - with open('Tests/images/hopper.jpg', 'rb') as fp: - self.assertRaises(SyntaxError, GimpPaletteFile, fp) - - with open('Tests/images/bad_palette_file.gpl', 'rb') as fp: - self.assertRaises(SyntaxError, GimpPaletteFile, fp) - - with open('Tests/images/bad_palette_entry.gpl', 'rb') as fp: - self.assertRaises(ValueError, GimpPaletteFile, fp) + with open("Tests/images/bad_palette_file.gpl", "rb") as fp: + with pytest.raises(SyntaxError): + GimpPaletteFile(fp) - def test_get_palette(self): - # Arrange - with open('Tests/images/custom_gimp_palette.gpl', 'rb') as fp: - palette_file = GimpPaletteFile(fp) + with open("Tests/images/bad_palette_entry.gpl", "rb") as fp: + with pytest.raises(ValueError): + GimpPaletteFile(fp) - # Act - palette, mode = palette_file.getpalette() - # Assert - self.assertEqual(mode, "RGB") +def test_get_palette(): + # Arrange + with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: + palette_file = GimpPaletteFile(fp) + # Act + palette, mode = palette_file.getpalette() -if __name__ == '__main__': - unittest.main() + # Assert + assert mode == "RGB" diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index b3a6f1a5a4b..fd427746e96 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -1,46 +1,79 @@ -from helper import unittest, PillowTestCase, hopper +import pytest from PIL import GribStubImagePlugin, Image -TEST_FILE = "Tests/images/WAlaska.wind.7days.grb" +from .helper import hopper +TEST_FILE = "Tests/images/WAlaska.wind.7days.grb" -class TestFileGribStub(PillowTestCase): - def test_open(self): - # Act - im = Image.open(TEST_FILE) +def test_open(): + # Act + with Image.open(TEST_FILE) as im: # Assert - self.assertEqual(im.format, "GRIB") + assert im.format == "GRIB" # Dummy data from the stub - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (1, 1)) + assert im.mode == "F" + assert im.size == (1, 1) + - def test_invalid_file(self): - # Arrange - invalid_file = "Tests/images/flower.jpg" +def test_invalid_file(): + # Arrange + invalid_file = "Tests/images/flower.jpg" - # Act / Assert - self.assertRaises(SyntaxError, - GribStubImagePlugin.GribStubImageFile, invalid_file) + # Act / Assert + with pytest.raises(SyntaxError): + GribStubImagePlugin.GribStubImageFile(invalid_file) - def test_load(self): - # Arrange - im = Image.open(TEST_FILE) + +def test_load(): + # Arrange + with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler - self.assertRaises(IOError, im.load) + with pytest.raises(OSError): + im.load() + + +def test_save(tmp_path): + # Arrange + im = hopper() + tmpfile = str(tmp_path / "temp.grib") + + # Act / Assert: stub cannot save without an implemented handler + with pytest.raises(OSError): + im.save(tmpfile) + + +def test_handler(tmp_path): + class TestHandler: + opened = False + loaded = False + saved = False + + def open(self, im): + self.opened = True + + def load(self, im): + self.loaded = True + return Image.new("RGB", (1, 1)) + + def save(self, im, fp, filename): + self.saved = True - def test_save(self): - # Arrange - im = hopper() - tmpfile = self.tempfile("temp.grib") + handler = TestHandler() + GribStubImagePlugin.register_handler(handler) + with Image.open(TEST_FILE) as im: + assert handler.opened + assert not handler.loaded - # Act / Assert: stub cannot save without an implemented handler - self.assertRaises(IOError, im.save, tmpfile) + im.load() + assert handler.loaded + temp_file = str(tmp_path / "temp.grib") + im.save(temp_file) + assert handler.saved -if __name__ == '__main__': - unittest.main() + GribStubImagePlugin._handler = None diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 6cddd8d7bed..20b4b9619af 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -1,50 +1,80 @@ -from helper import unittest, PillowTestCase +import pytest from PIL import Hdf5StubImagePlugin, Image TEST_FILE = "Tests/images/hdf5.h5" -class TestFileHdf5Stub(PillowTestCase): - - def test_open(self): - # Act - im = Image.open(TEST_FILE) +def test_open(): + # Act + with Image.open(TEST_FILE) as im: # Assert - self.assertEqual(im.format, "HDF5") + assert im.format == "HDF5" # Dummy data from the stub - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (1, 1)) + assert im.mode == "F" + assert im.size == (1, 1) + + +def test_invalid_file(): + # Arrange + invalid_file = "Tests/images/flower.jpg" - def test_invalid_file(self): - # Arrange - invalid_file = "Tests/images/flower.jpg" + # Act / Assert + with pytest.raises(SyntaxError): + Hdf5StubImagePlugin.HDF5StubImageFile(invalid_file) - # Act / Assert - self.assertRaises(SyntaxError, - Hdf5StubImagePlugin.HDF5StubImageFile, invalid_file) - def test_load(self): - # Arrange - im = Image.open(TEST_FILE) +def test_load(): + # Arrange + with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler - self.assertRaises(IOError, im.load) + with pytest.raises(OSError): + im.load() - def test_save(self): - # Arrange - im = Image.open(TEST_FILE) + +def test_save(): + # Arrange + with Image.open(TEST_FILE) as im: dummy_fp = None dummy_filename = "dummy.filename" # Act / Assert: stub cannot save without an implemented handler - self.assertRaises(IOError, im.save, dummy_filename) - self.assertRaises( - IOError, - Hdf5StubImagePlugin._save, im, dummy_fp, dummy_filename) + with pytest.raises(OSError): + im.save(dummy_filename) + with pytest.raises(OSError): + Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename) + + +def test_handler(tmp_path): + class TestHandler: + opened = False + loaded = False + saved = False + + def open(self, im): + self.opened = True + + def load(self, im): + self.loaded = True + return Image.new("RGB", (1, 1)) + + def save(self, im, fp, filename): + self.saved = True + + handler = TestHandler() + Hdf5StubImagePlugin.register_handler(handler) + with Image.open(TEST_FILE) as im: + assert handler.opened + assert not handler.loaded + + im.load() + assert handler.loaded + temp_file = str(tmp_path / "temp.h5") + im.save(temp_file) + assert handler.saved -if __name__ == '__main__': - unittest.main() + Hdf5StubImagePlugin._handler = None diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index d8508e57917..55632909c6b 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,105 +1,154 @@ -from helper import unittest, PillowTestCase +import io +import os +import warnings -from PIL import Image, IcnsImagePlugin +import pytest -import io -import sys +from PIL import IcnsImagePlugin, Image, _binary + +from .helper import assert_image_equal, assert_image_similar_tofile, skip_unless_feature # sample icon file TEST_FILE = "Tests/images/pillow.icns" -enable_jpeg2k = hasattr(Image.core, 'jp2klib_version') + +def test_sanity(): + # Loading this icon by default should result in the largest size + # (512x512@2x) being loaded + with Image.open(TEST_FILE) as im: + + # Assert that there is no unclosed file warning + with warnings.catch_warnings(): + im.load() + + assert im.mode == "RGBA" + assert im.size == (1024, 1024) + assert im.format == "ICNS" -class TestFileIcns(PillowTestCase): +def test_load(): + with Image.open(TEST_FILE) as im: + assert im.load()[0, 0] == (0, 0, 0, 0) - def test_sanity(self): - # Loading this icon by default should result in the largest size - # (512x512@2x) being loaded - im = Image.open(TEST_FILE) - im.load() - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (1024, 1024)) - self.assertEqual(im.format, "ICNS") + # Test again now that it has already been loaded once + assert im.load()[0, 0] == (0, 0, 0, 0) - @unittest.skipIf(sys.platform != 'darwin', - "requires MacOS") - def test_save(self): - im = Image.open(TEST_FILE) - temp_file = self.tempfile("temp.icns") +def test_save(tmp_path): + temp_file = str(tmp_path / "temp.icns") + + with Image.open(TEST_FILE) as im: im.save(temp_file) - reread = Image.open(temp_file) + with Image.open(temp_file) as reread: + assert reread.mode == "RGBA" + assert reread.size == (1024, 1024) + assert reread.format == "ICNS" + + file_length = os.path.getsize(temp_file) + with open(temp_file, "rb") as fp: + fp.seek(4) + assert _binary.i32be(fp.read(4)) == file_length + + +def test_save_append_images(tmp_path): + temp_file = str(tmp_path / "temp.icns") + provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) + + with Image.open(TEST_FILE) as im: + im.save(temp_file, append_images=[provided_im]) + + assert_image_similar_tofile(im, temp_file, 1) - self.assertEqual(reread.mode, "RGBA") - self.assertEqual(reread.size, (1024, 1024)) - self.assertEqual(reread.format, "ICNS") + with Image.open(temp_file) as reread: + reread.size = (16, 16, 2) + reread.load() + assert_image_equal(reread, provided_im) - def test_sizes(self): - # Check that we can load all of the sizes, and that the final pixel - # dimensions are as expected - im = Image.open(TEST_FILE) - for w, h, r in im.info['sizes']: + +def test_save_fp(): + fp = io.BytesIO() + + with Image.open(TEST_FILE) as im: + im.save(fp, format="ICNS") + + with Image.open(fp) as reread: + assert reread.mode == "RGBA" + assert reread.size == (1024, 1024) + assert reread.format == "ICNS" + + +def test_sizes(): + # Check that we can load all of the sizes, and that the final pixel + # dimensions are as expected + with Image.open(TEST_FILE) as im: + for w, h, r in im.info["sizes"]: wr = w * r hr = h * r - im2 = Image.open(TEST_FILE) - im2.size = (w, h, r) - im2.load() - self.assertEqual(im2.mode, 'RGBA') - self.assertEqual(im2.size, (wr, hr)) - - def test_older_icon(self): - # This icon was made with Icon Composer rather than iconutil; it still - # uses PNG rather than JP2, however (since it was made on 10.9). - im = Image.open('Tests/images/pillow2.icns') - for w, h, r in im.info['sizes']: + im.size = (w, h, r) + im.load() + assert im.mode == "RGBA" + assert im.size == (wr, hr) + + # Check that we cannot load an incorrect size + with pytest.raises(ValueError): + im.size = (1, 1) + + +def test_older_icon(): + # This icon was made with Icon Composer rather than iconutil; it still + # uses PNG rather than JP2, however (since it was made on 10.9). + with Image.open("Tests/images/pillow2.icns") as im: + for w, h, r in im.info["sizes"]: wr = w * r hr = h * r - im2 = Image.open('Tests/images/pillow2.icns') - im2.size = (w, h, r) - im2.load() - self.assertEqual(im2.mode, 'RGBA') - self.assertEqual(im2.size, (wr, hr)) - - def test_jp2_icon(self): - # This icon was made by using Uli Kusterer's oldiconutil to replace - # the PNG images with JPEG 2000 ones. The advantage of doing this is - # that OS X 10.5 supports JPEG 2000 but not PNG; some commercial - # software therefore does just this. + with Image.open("Tests/images/pillow2.icns") as im2: + im2.size = (w, h, r) + im2.load() + assert im2.mode == "RGBA" + assert im2.size == (wr, hr) - # (oldiconutil is here: https://github.com/uliwitness/oldiconutil) - if not enable_jpeg2k: - return +@skip_unless_feature("jpg_2000") +def test_jp2_icon(): + # This icon uses JPEG 2000 images instead of the PNG images. + # The advantage of doing this is that OS X 10.5 supports JPEG 2000 + # but not PNG; some commercial software therefore does just this. - im = Image.open('Tests/images/pillow3.icns') - for w, h, r in im.info['sizes']: + with Image.open("Tests/images/pillow3.icns") as im: + for w, h, r in im.info["sizes"]: wr = w * r hr = h * r - im2 = Image.open('Tests/images/pillow3.icns') - im2.size = (w, h, r) - im2.load() - self.assertEqual(im2.mode, 'RGBA') - self.assertEqual(im2.size, (wr, hr)) + with Image.open("Tests/images/pillow3.icns") as im2: + im2.size = (w, h, r) + im2.load() + assert im2.mode == "RGBA" + assert im2.size == (wr, hr) + + +def test_getimage(): + with open(TEST_FILE, "rb") as fp: + icns_file = IcnsImagePlugin.IcnsFile(fp) - def test_getimage(self): - with open(TEST_FILE, 'rb') as fp: - icns_file = IcnsImagePlugin.IcnsFile(fp) + im = icns_file.getimage() + assert im.mode == "RGBA" + assert im.size == (1024, 1024) - im = icns_file.getimage() - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (1024, 1024)) + im = icns_file.getimage((512, 512)) + assert im.mode == "RGBA" + assert im.size == (512, 512) - im = icns_file.getimage((512, 512)) - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (512, 512)) - def test_not_an_icns_file(self): - with io.BytesIO(b'invalid\n') as fp: - self.assertRaises(SyntaxError, - IcnsImagePlugin.IcnsFile, fp) +def test_not_an_icns_file(): + with io.BytesIO(b"invalid\n") as fp: + with pytest.raises(SyntaxError): + IcnsImagePlugin.IcnsFile(fp) -if __name__ == '__main__': - unittest.main() +@skip_unless_feature("jpg_2000") +def test_icns_decompression_bomb(): + with Image.open( + "Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns" + ) as im: + with pytest.raises(Image.DecompressionBombError): + im.load() diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 20f21db57af..3fcd5c61f0d 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -1,83 +1,219 @@ -from helper import unittest, PillowTestCase, hopper - import io -from PIL import Image, IcoImagePlugin +import os -TEST_ICO_FILE = "Tests/images/hopper.ico" +import pytest +from PIL import IcoImagePlugin, Image, ImageDraw -class TestFileIco(PillowTestCase): +from .helper import assert_image_equal, assert_image_equal_tofile, hopper + +TEST_ICO_FILE = "Tests/images/hopper.ico" - def test_sanity(self): - im = Image.open(TEST_ICO_FILE) + +def test_sanity(): + with Image.open(TEST_ICO_FILE) as im: im.load() - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (16, 16)) - self.assertEqual(im.format, "ICO") - - def test_invalid_file(self): - with open("Tests/images/flower.jpg", "rb") as fp: - self.assertRaises(SyntaxError, - IcoImagePlugin.IcoImageFile, fp) - - def test_save_to_bytes(self): - output = io.BytesIO() - im = hopper() - im.save(output, "ico", sizes=[(32, 32), (64, 64)]) - - # the default image - output.seek(0) - reloaded = Image.open(output) - self.assertEqual(reloaded.info['sizes'], set([(32, 32), (64, 64)])) - - self.assertEqual(im.mode, reloaded.mode) - self.assertEqual((64, 64), reloaded.size) - self.assertEqual(reloaded.format, "ICO") - self.assert_image_equal(reloaded, - hopper().resize((64, 64), Image.LANCZOS)) - - # the other one - output.seek(0) - reloaded = Image.open(output) + assert im.mode == "RGBA" + assert im.size == (16, 16) + assert im.format == "ICO" + assert im.get_format_mimetype() == "image/x-icon" + + +def test_load(): + with Image.open(TEST_ICO_FILE) as im: + assert im.load()[0, 0] == (1, 1, 9, 255) + + +def test_mask(): + with Image.open("Tests/images/hopper_mask.ico") as im: + assert_image_equal_tofile(im, "Tests/images/hopper_mask.png") + + +def test_black_and_white(): + with Image.open("Tests/images/black_and_white.ico") as im: + assert im.mode == "RGBA" + assert im.size == (16, 16) + + +def test_invalid_file(): + with open("Tests/images/flower.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + IcoImagePlugin.IcoImageFile(fp) + + +def test_save_to_bytes(): + output = io.BytesIO() + im = hopper() + im.save(output, "ico", sizes=[(32, 32), (64, 64)]) + + # The default image + output.seek(0) + with Image.open(output) as reloaded: + assert reloaded.info["sizes"] == {(32, 32), (64, 64)} + + assert im.mode == reloaded.mode + assert (64, 64) == reloaded.size + assert reloaded.format == "ICO" + assert_image_equal( + reloaded, hopper().resize((64, 64), Image.Resampling.LANCZOS) + ) + + # The other one + output.seek(0) + with Image.open(output) as reloaded: reloaded.size = (32, 32) - self.assertEqual(im.mode, reloaded.mode) - self.assertEqual((32, 32), reloaded.size) - self.assertEqual(reloaded.format, "ICO") - self.assert_image_equal(reloaded, - hopper().resize((32, 32), Image.LANCZOS)) + assert im.mode == reloaded.mode + assert (32, 32) == reloaded.size + assert reloaded.format == "ICO" + assert_image_equal( + reloaded, hopper().resize((32, 32), Image.Resampling.LANCZOS) + ) + + +def test_no_duplicates(tmp_path): + temp_file = str(tmp_path / "temp.ico") + temp_file2 = str(tmp_path / "temp2.ico") + + im = hopper() + sizes = [(32, 32), (64, 64)] + im.save(temp_file, "ico", sizes=sizes) + + sizes.append(sizes[-1]) + im.save(temp_file2, "ico", sizes=sizes) + + assert os.path.getsize(temp_file) == os.path.getsize(temp_file2) + + +def test_different_bit_depths(tmp_path): + temp_file = str(tmp_path / "temp.ico") + temp_file2 = str(tmp_path / "temp2.ico") + + im = hopper() + im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)]) - def test_save_256x256(self): - """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" - # Arrange - im = Image.open("Tests/images/hopper_256x256.ico") - outfile = self.tempfile("temp_saved_hopper_256x256.ico") + hopper("1").save( + temp_file2, + "ico", + bitmap_format="bmp", + sizes=[(128, 128)], + append_images=[im], + ) + + assert os.path.getsize(temp_file) != os.path.getsize(temp_file2) + + # Test that only matching sizes of different bit depths are saved + temp_file3 = str(tmp_path / "temp3.ico") + temp_file4 = str(tmp_path / "temp4.ico") + + im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)]) + im.save( + temp_file4, + "ico", + bitmap_format="bmp", + sizes=[(128, 128)], + append_images=[Image.new("P", (64, 64))], + ) + + assert os.path.getsize(temp_file3) == os.path.getsize(temp_file4) + + +@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) +def test_save_to_bytes_bmp(mode): + output = io.BytesIO() + im = hopper(mode) + im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) + + # The default image + output.seek(0) + with Image.open(output) as reloaded: + assert reloaded.info["sizes"] == {(32, 32), (64, 64)} + + assert "RGBA" == reloaded.mode + assert (64, 64) == reloaded.size + assert reloaded.format == "ICO" + im = hopper(mode).resize((64, 64), Image.Resampling.LANCZOS).convert("RGBA") + assert_image_equal(reloaded, im) + + # The other one + output.seek(0) + with Image.open(output) as reloaded: + reloaded.size = (32, 32) + + assert "RGBA" == reloaded.mode + assert (32, 32) == reloaded.size + assert reloaded.format == "ICO" + im = hopper(mode).resize((32, 32), Image.Resampling.LANCZOS).convert("RGBA") + assert_image_equal(reloaded, im) + + +def test_incorrect_size(): + with Image.open(TEST_ICO_FILE) as im: + with pytest.raises(ValueError): + im.size = (1, 1) + + +def test_save_256x256(tmp_path): + """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" + # Arrange + with Image.open("Tests/images/hopper_256x256.ico") as im: + outfile = str(tmp_path / "temp_saved_hopper_256x256.ico") # Act im.save(outfile) - im_saved = Image.open(outfile) + with Image.open(outfile) as im_saved: # Assert - self.assertEqual(im_saved.size, (256, 256)) + assert im_saved.size == (256, 256) - def test_only_save_relevant_sizes(self): - """Issue #2266 https://github.com/python-pillow/Pillow/issues/2266 - Should save in 16x16, 24x24, 32x32, 48x48 sizes - and not in 16x16, 24x24, 32x32, 48x48, 48x48, 48x48, 48x48 sizes - """ - # Arrange - im = Image.open("Tests/images/python.ico") # 16x16, 32x32, 48x48 - outfile = self.tempfile("temp_saved_python.ico") +def test_only_save_relevant_sizes(tmp_path): + """Issue #2266 https://github.com/python-pillow/Pillow/issues/2266 + Should save in 16x16, 24x24, 32x32, 48x48 sizes + and not in 16x16, 24x24, 32x32, 48x48, 48x48, 48x48, 48x48 sizes + """ + # Arrange + with Image.open("Tests/images/python.ico") as im: # 16x16, 32x32, 48x48 + outfile = str(tmp_path / "temp_saved_python.ico") # Act im.save(outfile) - im_saved = Image.open(outfile) + with Image.open(outfile) as im_saved: # Assert - self.assertEqual( - im_saved.info['sizes'], - set([(16, 16), (24, 24), (32, 32), (48, 48)])) + assert im_saved.info["sizes"] == {(16, 16), (24, 24), (32, 32), (48, 48)} + + +def test_save_append_images(tmp_path): + # append_images should be used for scaled down versions of the image + im = hopper("RGBA") + provided_im = Image.new("RGBA", (32, 32), (255, 0, 0)) + outfile = str(tmp_path / "temp_saved_multi_icon.ico") + im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im]) + + with Image.open(outfile) as reread: + assert_image_equal(reread, hopper("RGBA")) + + reread.size = (32, 32) + assert_image_equal(reread, provided_im) -if __name__ == '__main__': - unittest.main() +def test_unexpected_size(): + # This image has been manually hexedited to state that it is 16x32 + # while the image within is still 16x16 + def open(): + with Image.open("Tests/images/hopper_unexpected.ico") as im: + assert im.size == (16, 16) + + pytest.warns(UserWarning, open) + + +def test_draw_reloaded(tmp_path): + with Image.open(TEST_ICO_FILE) as im: + outfile = str(tmp_path / "temp_saved_hopper_draw.ico") + + draw = ImageDraw.Draw(im) + draw.line((0, 0) + im.size, "#f00") + im.save(outfile) + + with Image.open(outfile) as im: + assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico") diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index c9992476774..5cf93713be4 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -1,69 +1,116 @@ -from helper import unittest, PillowTestCase, hopper +import filecmp +import warnings + +import pytest from PIL import Image, ImImagePlugin +from .helper import assert_image_equal_tofile, hopper, is_pypy + # sample im TEST_IM = "Tests/images/hopper.im" -class TestFileIm(PillowTestCase): +def test_sanity(): + with Image.open(TEST_IM) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "IM" + + +def test_name_limit(tmp_path): + out = str(tmp_path / ("name_limit_test" * 7 + ".im")) + with Image.open(TEST_IM) as im: + im.save(out) + assert filecmp.cmp(out, "Tests/images/hopper_long_name.im") + - def test_sanity(self): +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): im = Image.open(TEST_IM) im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "IM") - def test_tell(self): - # Arrange + pytest.warns(ResourceWarning, open) + + +def test_closed_file(): + with warnings.catch_warnings(): im = Image.open(TEST_IM) + im.load() + im.close() + + +def test_context_manager(): + with warnings.catch_warnings(): + with Image.open(TEST_IM) as im: + im.load() + + +def test_tell(): + # Arrange + with Image.open(TEST_IM) as im: # Act frame = im.tell() - # Assert - self.assertEqual(frame, 0) + # Assert + assert frame == 0 - def test_n_frames(self): - im = Image.open(TEST_IM) - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) - def test_eoferror(self): - im = Image.open(TEST_IM) +def test_n_frames(): + with Image.open(TEST_IM) as im: + assert im.n_frames == 1 + assert not im.is_animated + + +def test_eoferror(): + with Image.open(TEST_IM) as im: n_frames = im.n_frames # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames # Test that seeking to the last frame does not raise an error - im.seek(n_frames-1) + im.seek(n_frames - 1) + + +@pytest.mark.parametrize("mode", ("RGB", "P", "PA")) +def test_roundtrip(mode, tmp_path): + out = str(tmp_path / "temp.im") + im = hopper(mode) + im.save(out) + assert_image_equal_tofile(im, out) + + +def test_small_palette(tmp_path): + im = Image.new("P", (1, 1)) + colors = [0, 1, 2] + im.putpalette(colors) + + out = str(tmp_path / "temp.im") + im.save(out) - def test_roundtrip(self): - for mode in ["RGB", "P"]: - out = self.tempfile('temp.im') - im = hopper(mode) - im.save(out) - reread = Image.open(out) + with Image.open(out) as reloaded: + assert reloaded.getpalette() == colors + [0] * 765 - self.assert_image_equal(reread, im) - def test_save_unsupported_mode(self): - out = self.tempfile('temp.im') - im = hopper("HSV") - self.assertRaises(ValueError, im.save, out) +def test_save_unsupported_mode(tmp_path): + out = str(tmp_path / "temp.im") + im = hopper("HSV") + with pytest.raises(ValueError): + im.save(out) - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - ImImagePlugin.ImImageFile, invalid_file) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - def test_number(self): - self.assertEqual(1.2, ImImagePlugin.number("1.2")) + with pytest.raises(SyntaxError): + ImImagePlugin.ImImageFile(invalid_file) -if __name__ == '__main__': - unittest.main() +def test_number(): + assert ImImagePlugin.number("1.2") == 1.2 diff --git a/Tests/test_file_imt.py b/Tests/test_file_imt.py new file mode 100644 index 00000000000..f56acc42949 --- /dev/null +++ b/Tests/test_file_imt.py @@ -0,0 +1,19 @@ +import io + +import pytest + +from PIL import Image, ImtImagePlugin + +from .helper import assert_image_equal_tofile + + +def test_sanity(): + with Image.open("Tests/images/bw_gradient.imt") as im: + assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") + + +@pytest.mark.parametrize("data", (b"\n", b"\n-", b"width 1\n")) +def test_invalid_file(data): + with io.BytesIO(data) as fp: + with pytest.raises(SyntaxError): + ImtImagePlugin.ImtImageFile(fp) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index e08d994a2b3..2d0e6977a70 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -1,75 +1,71 @@ -from helper import unittest, PillowTestCase, hopper +import sys +from io import StringIO from PIL import Image, IptcImagePlugin -TEST_FILE = "Tests/images/iptc.jpg" +from .helper import hopper +TEST_FILE = "Tests/images/iptc.jpg" -class TestFileIptc(PillowTestCase): - def test_getiptcinfo_jpg_none(self): - # Arrange - im = hopper() +def test_getiptcinfo_jpg_none(): + # Arrange + with hopper() as im: # Act iptc = IptcImagePlugin.getiptcinfo(im) - # Assert - self.assertIsNone(iptc) + # Assert + assert iptc is None - def test_getiptcinfo_jpg_found(self): - # Arrange - im = Image.open(TEST_FILE) + +def test_getiptcinfo_jpg_found(): + # Arrange + with Image.open(TEST_FILE) as im: # Act iptc = IptcImagePlugin.getiptcinfo(im) - # Assert - self.assertIsInstance(iptc, dict) - self.assertEqual(iptc[(2, 90)], b"Budapest") - self.assertEqual(iptc[(2, 101)], b"Hungary") + # Assert + assert isinstance(iptc, dict) + assert iptc[(2, 90)] == b"Budapest" + assert iptc[(2, 101)] == b"Hungary" + - def test_getiptcinfo_tiff_none(self): - # Arrange - im = Image.open("Tests/images/hopper.tif") +def test_getiptcinfo_tiff_none(): + # Arrange + with Image.open("Tests/images/hopper.tif") as im: # Act iptc = IptcImagePlugin.getiptcinfo(im) - # Assert - self.assertIsNone(iptc) + # Assert + assert iptc is None - def test_i(self): - # Arrange - c = b"a" - # Act - ret = IptcImagePlugin.i(c) - - # Assert - self.assertEqual(ret, 97) - - def test_dump(self): - # Arrange - c = b"abc" - # Temporarily redirect stdout - try: - from cStringIO import StringIO - except ImportError: - from io import StringIO - import sys - old_stdout = sys.stdout - sys.stdout = mystdout = StringIO() +def test_i(): + # Arrange + c = b"a" + + # Act + ret = IptcImagePlugin.i(c) + + # Assert + assert ret == 97 - # Act - IptcImagePlugin.dump(c) - # Reset stdout - sys.stdout = old_stdout +def test_dump(): + # Arrange + c = b"abc" + # Temporarily redirect stdout + old_stdout = sys.stdout + sys.stdout = mystdout = StringIO() - # Assert - self.assertEqual(mystdout.getvalue(), "61 62 63 \n") + # Act + IptcImagePlugin.dump(c) + # Reset stdout + sys.stdout = old_stdout -if __name__ == '__main__': - unittest.main() + # Assert + assert mystdout.getvalue() == "61 62 63 \n" diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 747c3d7dea3..fa96e425b8c 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,25 +1,44 @@ -from helper import unittest, PillowTestCase, hopper -from helper import djpeg_available, cjpeg_available - -from io import BytesIO import os -import sys - -from PIL import Image -from PIL import ImageFile -from PIL import JpegImagePlugin +import re +import warnings +from io import BytesIO -codecs = dir(Image.core) +import pytest + +from PIL import ( + ExifTags, + Image, + ImageFile, + ImageOps, + JpegImagePlugin, + UnidentifiedImageError, + features, +) + +from .helper import ( + assert_image, + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + assert_image_similar_tofile, + cjpeg_available, + djpeg_available, + hopper, + is_win32, + mark_if_feature_version, + skip_unless_feature, +) + +try: + from defusedxml import ElementTree +except ImportError: + ElementTree = None TEST_FILE = "Tests/images/hopper.jpg" -class TestFileJpeg(PillowTestCase): - - def setUp(self): - if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: - self.skipTest("jpeg support not available") - +@skip_unless_feature("jpg") +class TestFileJpeg: def roundtrip(self, im, **options): out = BytesIO() im.save(out, "JPEG", **options) @@ -29,158 +48,188 @@ def roundtrip(self, im, **options): im.bytes = test_bytes # for testing only return im - def gen_random_image(self, size, mode='RGB'): - """ Generates a very hard to compress file + def gen_random_image(self, size, mode="RGB"): + """Generates a very hard to compress file :param size: tuple :param mode: optional image mode """ - return Image.frombytes(mode, size, - os.urandom(size[0]*size[1]*len(mode))) + return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode))) def test_sanity(self): # internal version number - self.assertRegexpMatches(Image.core.jpeglib_version, r"\d+\.\d+$") - - im = Image.open(TEST_FILE) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "JPEG") + assert re.search(r"\d+\.\d+$", features.version_codec("jpg")) + + with Image.open(TEST_FILE) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "JPEG" + assert im.get_format_mimetype() == "image/jpeg" + + @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) + def test_zero(self, size, tmp_path): + f = str(tmp_path / "temp.jpg") + im = Image.new("RGB", size) + with pytest.raises(ValueError): + im.save(f) def test_app(self): # Test APP/COM reader (@PIL135) - im = Image.open(TEST_FILE) - self.assertEqual( - im.applist[0], - ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00")) - self.assertEqual(im.applist[1], ( - "COM", b"File written by Adobe Photoshop\xa8 4.0\x00")) - self.assertEqual(len(im.applist), 2) + with Image.open(TEST_FILE) as im: + assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") + assert im.applist[1] == ( + "COM", + b"File written by Adobe Photoshop\xa8 4.0\x00", + ) + assert len(im.applist) == 2 + + assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" def test_cmyk(self): # Test CMYK handling. Thanks to Tim and Charlie for test data, # Michael for getting me to look one more time. f = "Tests/images/pil_sample_cmyk.jpg" - im = Image.open(f) - # the source image has red pixels in the upper left corner. - c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))] - self.assertEqual(c, 0.0) - self.assertGreater(m, 0.8) - self.assertGreater(y, 0.8) - self.assertEqual(k, 0.0) - # the opposite corner is black - c, m, y, k = [x / 255.0 for x in im.getpixel(( - im.size[0]-1, im.size[1]-1))] - self.assertGreater(k, 0.9) - # roundtrip, and check again - im = self.roundtrip(im) - c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))] - self.assertEqual(c, 0.0) - self.assertGreater(m, 0.8) - self.assertGreater(y, 0.8) - self.assertEqual(k, 0.0) - c, m, y, k = [x / 255.0 for x in im.getpixel(( - im.size[0]-1, im.size[1]-1))] - self.assertGreater(k, 0.9) - - def test_dpi(self): + with Image.open(f) as im: + # the source image has red pixels in the upper left corner. + c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0))) + assert c == 0.0 + assert m > 0.8 + assert y > 0.8 + assert k == 0.0 + # the opposite corner is black + c, m, y, k = ( + x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) + ) + assert k > 0.9 + # roundtrip, and check again + im = self.roundtrip(im) + c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0))) + assert c == 0.0 + assert m > 0.8 + assert y > 0.8 + assert k == 0.0 + c, m, y, k = ( + x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) + ) + assert k > 0.9 + + @pytest.mark.parametrize( + "test_image_path", + [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], + ) + def test_dpi(self, test_image_path): def test(xdpi, ydpi=None): - im = Image.open(TEST_FILE) - im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) + with Image.open(test_image_path) as im: + im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) return im.info.get("dpi") - self.assertEqual(test(72), (72, 72)) - self.assertEqual(test(300), (300, 300)) - self.assertEqual(test(100, 200), (100, 200)) - self.assertIsNone(test(0)) # square pixels - def test_icc(self): - # Test ICC support - im1 = Image.open("Tests/images/rgb.jpg") - icc_profile = im1.info["icc_profile"] - self.assertEqual(len(icc_profile), 3144) - # Roundtrip via physical file. - f = self.tempfile("temp.jpg") - im1.save(f, icc_profile=icc_profile) - im2 = Image.open(f) - self.assertEqual(im2.info.get("icc_profile"), icc_profile) - # Roundtrip via memory buffer. - im1 = self.roundtrip(hopper()) - im2 = self.roundtrip(hopper(), icc_profile=icc_profile) - self.assert_image_equal(im1, im2) - self.assertFalse(im1.info.get("icc_profile")) - self.assertTrue(im2.info.get("icc_profile")) + assert test(72) == (72, 72) + assert test(300) == (300, 300) + assert test(100, 200) == (100, 200) + assert test(0) is None # square pixels - def test_icc_big(self): + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_icc(self, tmp_path): + # Test ICC support + with Image.open("Tests/images/rgb.jpg") as im1: + icc_profile = im1.info["icc_profile"] + assert len(icc_profile) == 3144 + # Roundtrip via physical file. + f = str(tmp_path / "temp.jpg") + im1.save(f, icc_profile=icc_profile) + with Image.open(f) as im2: + assert im2.info.get("icc_profile") == icc_profile + # Roundtrip via memory buffer. + im1 = self.roundtrip(hopper()) + im2 = self.roundtrip(hopper(), icc_profile=icc_profile) + assert_image_equal(im1, im2) + assert not im1.info.get("icc_profile") + assert im2.info.get("icc_profile") + + @pytest.mark.parametrize( + "n", + ( + 0, + 1, + 3, + 4, + 5, + 65533 - 14, # full JPEG marker block + 65533 - 14 + 1, # full block plus one byte + ImageFile.MAXBLOCK, # full buffer block + ImageFile.MAXBLOCK + 1, # full buffer block plus one byte + ImageFile.MAXBLOCK * 4 + 3, # large block + ), + ) + def test_icc_big(self, n): # Make sure that the "extra" support handles large blocks - def test(n): - # The ICC APP marker can store 65519 bytes per marker, so - # using a 4-byte test code should allow us to detect out of - # order issues. - icc_profile = (b"Test"*int(n/4+1))[:n] - assert len(icc_profile) == n # sanity - im1 = self.roundtrip(hopper(), icc_profile=icc_profile) - self.assertEqual(im1.info.get("icc_profile"), icc_profile or None) - test(0) - test(1) - test(3) - test(4) - test(5) - test(65533-14) # full JPEG marker block - test(65533-14+1) # full block plus one byte - test(ImageFile.MAXBLOCK) # full buffer block - test(ImageFile.MAXBLOCK+1) # full buffer block plus one byte - test(ImageFile.MAXBLOCK*4+3) # large block - - def test_large_icc_meta(self): + # The ICC APP marker can store 65519 bytes per marker, so + # using a 4-byte test code should allow us to detect out of + # order issues. + icc_profile = (b"Test" * int(n / 4 + 1))[:n] + assert len(icc_profile) == n # sanity + im1 = self.roundtrip(hopper(), icc_profile=icc_profile) + assert im1.info.get("icc_profile") == (icc_profile or None) + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_large_icc_meta(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/148 # Sometimes the meta data on the icc_profile block is bigger than # Image.MAXBLOCK or the image size. - im = Image.open('Tests/images/icc_profile_big.jpg') - f = self.tempfile("temp.jpg") - icc_profile = im.info["icc_profile"] - try: - im.save(f, format='JPEG', progressive=True,quality=95, - icc_profile=icc_profile, optimize=True) - except IOError: - self.fail("Failed saving image with icc larger than image size") + with Image.open("Tests/images/icc_profile_big.jpg") as im: + f = str(tmp_path / "temp.jpg") + icc_profile = im.info["icc_profile"] + # Should not raise OSError for image with icc larger than image size. + im.save( + f, + format="JPEG", + progressive=True, + quality=95, + icc_profile=icc_profile, + optimize=True, + ) def test_optimize(self): im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), optimize=0) im3 = self.roundtrip(hopper(), optimize=1) - self.assert_image_equal(im1, im2) - self.assert_image_equal(im1, im3) - self.assertGreaterEqual(im1.bytes, im2.bytes) - self.assertGreaterEqual(im1.bytes, im3.bytes) + assert_image_equal(im1, im2) + assert_image_equal(im1, im3) + assert im1.bytes >= im2.bytes + assert im1.bytes >= im3.bytes - def test_optimize_large_buffer(self): + def test_optimize_large_buffer(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/148 - f = self.tempfile('temp.jpg') + f = str(tmp_path / "temp.jpg") # this requires ~ 1.5x Image.MAXBLOCK - im = Image.new("RGB", (4096, 4096), 0xff3333) + im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", optimize=True) def test_progressive(self): im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), progressive=False) im3 = self.roundtrip(hopper(), progressive=True) - self.assertFalse(im1.info.get("progressive")) - self.assertFalse(im2.info.get("progressive")) - self.assertTrue(im3.info.get("progressive")) + assert not im1.info.get("progressive") + assert not im2.info.get("progressive") + assert im3.info.get("progressive") - self.assert_image_equal(im1, im3) - self.assertGreaterEqual(im1.bytes, im3.bytes) + assert_image_equal(im1, im3) + assert im1.bytes >= im3.bytes - def test_progressive_large_buffer(self): - f = self.tempfile('temp.jpg') + def test_progressive_large_buffer(self, tmp_path): + f = str(tmp_path / "temp.jpg") # this requires ~ 1.5x Image.MAXBLOCK - im = Image.new("RGB", (4096, 4096), 0xff3333) + im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", progressive=True) - def test_progressive_large_buffer_highest_quality(self): - f = self.tempfile('temp.jpg') + def test_progressive_large_buffer_highest_quality(self, tmp_path): + f = str(tmp_path / "temp.jpg") im = self.gen_random_image((255, 255)) # this requires more bytes than pixels in the image im.save(f, format="JPEG", progressive=True, quality=100) @@ -188,408 +237,689 @@ def test_progressive_large_buffer_highest_quality(self): def test_progressive_cmyk_buffer(self): # Issue 2272, quality 90 cmyk image is tripping the large buffer bug. f = BytesIO() - im = self.gen_random_image((256, 256), 'CMYK') - im.save(f, format='JPEG', progressive=True, quality=94) + im = self.gen_random_image((256, 256), "CMYK") + im.save(f, format="JPEG", progressive=True, quality=94) - def test_large_exif(self): + def test_large_exif(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/148 - f = self.tempfile('temp.jpg') + f = str(tmp_path / "temp.jpg") im = hopper() - im.save(f, 'JPEG', quality=90, exif=b"1"*65532) + im.save(f, "JPEG", quality=90, exif=b"1" * 65532) def test_exif_typeerror(self): - im = Image.open('Tests/images/exif_typeerror.jpg') - # Should not raise a TypeError - im._getexif() + with Image.open("Tests/images/exif_typeerror.jpg") as im: + # Should not raise a TypeError + im._getexif() - def test_exif_gps(self): - # Arrange - im = Image.open('Tests/images/exif_gps.jpg') - gps_index = 34853 + def test_exif_gps(self, tmp_path): expected_exif_gps = { - 0: b'\x00\x00\x00\x01', - 2: (4294967295, 1), - 5: b'\x01', + 0: b"\x00\x00\x00\x01", + 2: 4294967295, + 5: b"\x01", 30: 65535, - 29: '1999:99:99 99:99:99'} - - # Act - exif = im._getexif() + 29: "1999:99:99 99:99:99", + } + gps_index = 34853 - # Assert - self.assertEqual(exif[gps_index], expected_exif_gps) + # Reading + with Image.open("Tests/images/exif_gps.jpg") as im: + exif = im._getexif() + assert exif[gps_index] == expected_exif_gps + + # Writing + f = str(tmp_path / "temp.jpg") + exif = Image.Exif() + exif[gps_index] = expected_exif_gps + hopper().save(f, exif=exif) + + with Image.open(f) as reloaded: + exif = reloaded._getexif() + assert exif[gps_index] == expected_exif_gps + + def test_empty_exif_gps(self): + with Image.open("Tests/images/empty_gps_ifd.jpg") as im: + exif = im.getexif() + del exif[0x8769] + + # Assert that it needs to be transposed + assert exif[0x0112] == Image.Transpose.TRANSVERSE + + # Assert that the GPS IFD is present and empty + assert exif.get_ifd(0x8825) == {} + + transposed = ImageOps.exif_transpose(im) + exif = transposed.getexif() + assert exif.get_ifd(0x8825) == {} + + # Assert that it was transposed + assert 0x0112 not in exif + + def test_exif_equality(self): + # In 7.2.0, Exif rationals were changed to be read as + # TiffImagePlugin.IFDRational. This class had a bug in __eq__, + # breaking the self-equality of Exif data + exifs = [] + for i in range(2): + with Image.open("Tests/images/exif-200dpcm.jpg") as im: + exifs.append(im._getexif()) + assert exifs[0] == exifs[1] def test_exif_rollback(self): # rolling back exif support in 3.1 to pre-3.0 formatting. # expected from 2.9, with b/u qualifiers switched for 3.2 compatibility # this test passes on 2.9 and 3.1, but not 3.0 - expected_exif = {34867: 4294967295, - 258: (24, 24, 24), - 36867: '2099:09:29 10:10:10', - 34853: {0: b'\x00\x00\x00\x01', - 2: (4294967295, 1), - 5: b'\x01', - 30: 65535, - 29: '1999:99:99 99:99:99'}, - 296: 65535, - 34665: 185, - 41994: 65535, - 514: 4294967295, - 271: 'Make', - 272: 'XXX-XXX', - 305: 'PIL', - 42034: ((1, 1), (1, 1), (1, 1), (1, 1)), - 42035: 'LensMake', - 34856: b'\xaa\xaa\xaa\xaa\xaa\xaa', - 282: (4294967295, 1), - 33434: (4294967295, 1)} - - im = Image.open('Tests/images/exif_gps.jpg') - exif = im._getexif() + expected_exif = { + 34867: 4294967295, + 258: (24, 24, 24), + 36867: "2099:09:29 10:10:10", + 34853: { + 0: b"\x00\x00\x00\x01", + 2: 4294967295, + 5: b"\x01", + 30: 65535, + 29: "1999:99:99 99:99:99", + }, + 296: 65535, + 34665: 185, + 41994: 65535, + 514: 4294967295, + 271: "Make", + 272: "XXX-XXX", + 305: "PIL", + 42034: (1, 1, 1, 1), + 42035: "LensMake", + 34856: b"\xaa\xaa\xaa\xaa\xaa\xaa", + 282: 4294967295, + 33434: 4294967295, + } + + with Image.open("Tests/images/exif_gps.jpg") as im: + exif = im._getexif() for tag, value in expected_exif.items(): - self.assertEqual(value, exif[tag]) + assert value == exif[tag] def test_exif_gps_typeerror(self): - im = Image.open('Tests/images/exif_gps_typeerror.jpg') + with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: - # Should not raise a TypeError - im._getexif() + # Should not raise a TypeError + im._getexif() def test_progressive_compat(self): im1 = self.roundtrip(hopper()) - self.assertFalse(im1.info.get("progressive")) - self.assertFalse(im1.info.get("progression")) + assert not im1.info.get("progressive") + assert not im1.info.get("progression") im2 = self.roundtrip(hopper(), progressive=0) im3 = self.roundtrip(hopper(), progression=0) # compatibility - self.assertFalse(im2.info.get("progressive")) - self.assertFalse(im2.info.get("progression")) - self.assertFalse(im3.info.get("progressive")) - self.assertFalse(im3.info.get("progression")) + assert not im2.info.get("progressive") + assert not im2.info.get("progression") + assert not im3.info.get("progressive") + assert not im3.info.get("progression") im2 = self.roundtrip(hopper(), progressive=1) im3 = self.roundtrip(hopper(), progression=1) # compatibility - self.assert_image_equal(im1, im2) - self.assert_image_equal(im1, im3) - self.assertTrue(im2.info.get("progressive")) - self.assertTrue(im2.info.get("progression")) - self.assertTrue(im3.info.get("progressive")) - self.assertTrue(im3.info.get("progression")) + assert_image_equal(im1, im2) + assert_image_equal(im1, im3) + assert im2.info.get("progressive") + assert im2.info.get("progression") + assert im3.info.get("progressive") + assert im3.info.get("progression") def test_quality(self): im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), quality=50) - self.assert_image(im1, im2.mode, im2.size) - self.assertGreaterEqual(im1.bytes, im2.bytes) + assert_image(im1, im2.mode, im2.size) + assert im1.bytes >= im2.bytes + + im3 = self.roundtrip(hopper(), quality=0) + assert_image(im1, im3.mode, im3.size) + assert im2.bytes > im3.bytes def test_smooth(self): im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), smooth=100) - self.assert_image(im1, im2.mode, im2.size) + assert_image(im1, im2.mode, im2.size) def test_subsampling(self): def getsampling(im): layer = im.layer return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] + # experimental API im = self.roundtrip(hopper(), subsampling=-1) # default - self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) im = self.roundtrip(hopper(), subsampling=0) # 4:4:4 - self.assertEqual(getsampling(im), (1, 1, 1, 1, 1, 1)) + assert getsampling(im) == (1, 1, 1, 1, 1, 1) im = self.roundtrip(hopper(), subsampling=1) # 4:2:2 - self.assertEqual(getsampling(im), (2, 1, 1, 1, 1, 1)) + assert getsampling(im) == (2, 1, 1, 1, 1, 1) im = self.roundtrip(hopper(), subsampling=2) # 4:2:0 - self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) im = self.roundtrip(hopper(), subsampling=3) # default (undefined) - self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) im = self.roundtrip(hopper(), subsampling="4:4:4") - self.assertEqual(getsampling(im), (1, 1, 1, 1, 1, 1)) + assert getsampling(im) == (1, 1, 1, 1, 1, 1) im = self.roundtrip(hopper(), subsampling="4:2:2") - self.assertEqual(getsampling(im), (2, 1, 1, 1, 1, 1)) + assert getsampling(im) == (2, 1, 1, 1, 1, 1) im = self.roundtrip(hopper(), subsampling="4:2:0") - self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) im = self.roundtrip(hopper(), subsampling="4:1:1") - self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) - self.assertRaises( - TypeError, self.roundtrip, hopper(), subsampling="1:1:1") + with pytest.raises(TypeError): + self.roundtrip(hopper(), subsampling="1:1:1") def test_exif(self): - im = Image.open("Tests/images/pil_sample_rgb.jpg") - info = im._getexif() - self.assertEqual(info[305], 'Adobe Photoshop CS Macintosh') + with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + info = im._getexif() + assert info[305] == "Adobe Photoshop CS Macintosh" def test_mp(self): - im = Image.open("Tests/images/pil_sample_rgb.jpg") - self.assertIsNone(im._getmp()) + with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + assert im._getmp() is None - def test_quality_keep(self): + def test_quality_keep(self, tmp_path): # RGB - im = Image.open("Tests/images/hopper.jpg") - f = self.tempfile('temp.jpg') - im.save(f, quality='keep') + with Image.open("Tests/images/hopper.jpg") as im: + f = str(tmp_path / "temp.jpg") + im.save(f, quality="keep") # Grayscale - im = Image.open("Tests/images/hopper_gray.jpg") - f = self.tempfile('temp.jpg') - im.save(f, quality='keep') + with Image.open("Tests/images/hopper_gray.jpg") as im: + f = str(tmp_path / "temp.jpg") + im.save(f, quality="keep") # CMYK - im = Image.open("Tests/images/pil_sample_cmyk.jpg") - f = self.tempfile('temp.jpg') - im.save(f, quality='keep') + with Image.open("Tests/images/pil_sample_cmyk.jpg") as im: + f = str(tmp_path / "temp.jpg") + im.save(f, quality="keep") def test_junk_jpeg_header(self): # https://github.com/python-pillow/Pillow/issues/630 filename = "Tests/images/junk_jpeg_header.jpg" - Image.open(filename) + with Image.open(filename): + pass def test_ff00_jpeg_header(self): filename = "Tests/images/jpeg_ff00_header.jpg" - Image.open(filename) - - def _n_qtables_helper(self, n, test_file): - im = Image.open(test_file) - f = self.tempfile('temp.jpg') - im.save(f, qtables=[[n]*64]*n) - im = Image.open(f) - self.assertEqual(len(im.quantization), n) - reloaded = self.roundtrip(im, qtables="keep") - self.assertEqual(im.quantization, reloaded.quantization) - - def test_qtables(self): - im = Image.open("Tests/images/hopper.jpg") - qtables = im.quantization - reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) - self.assertEqual(im.quantization, reloaded.quantization) - self.assert_image_similar(im, self.roundtrip(im, qtables='web_low'), - 30) - self.assert_image_similar(im, self.roundtrip(im, qtables='web_high'), - 30) - self.assert_image_similar(im, self.roundtrip(im, qtables='keep'), 30) - - # valid bounds for baseline qtable - bounds_qtable = [int(s) for s in ("255 1 " * 32).split(None)] - self.roundtrip(im, qtables=[bounds_qtable]) - - # values from wizard.txt in jpeg9-a src package. - standard_l_qtable = [int(s) for s in """ - 16 11 10 16 24 40 51 61 - 12 12 14 19 26 58 60 55 - 14 13 16 24 40 57 69 56 - 14 17 22 29 51 87 80 62 - 18 22 37 56 68 109 103 77 - 24 35 55 64 81 104 113 92 - 49 64 78 87 103 121 120 101 - 72 92 95 98 112 100 103 99 - """.split(None)] - - standard_chrominance_qtable = [int(s) for s in """ - 17 18 24 47 99 99 99 99 - 18 21 26 66 99 99 99 99 - 24 26 56 99 99 99 99 99 - 47 66 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - """.split(None)] - # list of qtable lists - self.assert_image_similar( - im, self.roundtrip( - im, qtables=[standard_l_qtable, standard_chrominance_qtable]), - 30) - - # tuple of qtable lists - self.assert_image_similar( - im, self.roundtrip( - im, qtables=(standard_l_qtable, standard_chrominance_qtable)), - 30) - - # dict of qtable lists - self.assert_image_similar(im, - self.roundtrip(im, qtables={ - 0: standard_l_qtable, - 1: standard_chrominance_qtable - }), 30) - - self._n_qtables_helper(1, "Tests/images/hopper_gray.jpg") - self._n_qtables_helper(1, "Tests/images/pil_sample_rgb.jpg") - self._n_qtables_helper(2, "Tests/images/pil_sample_rgb.jpg") - self._n_qtables_helper(3, "Tests/images/pil_sample_rgb.jpg") - self._n_qtables_helper(1, "Tests/images/pil_sample_cmyk.jpg") - self._n_qtables_helper(2, "Tests/images/pil_sample_cmyk.jpg") - self._n_qtables_helper(3, "Tests/images/pil_sample_cmyk.jpg") - self._n_qtables_helper(4, "Tests/images/pil_sample_cmyk.jpg") - - # not a sequence - self.assertRaises(Exception, self.roundtrip, im, qtables='a') - # sequence wrong length - self.assertRaises(Exception, self.roundtrip, im, qtables=[]) - # sequence wrong length - self.assertRaises(Exception, - self.roundtrip, im, qtables=[1, 2, 3, 4, 5]) - - # qtable entry not a sequence - self.assertRaises(Exception, self.roundtrip, im, qtables=[1]) - # qtable entry has wrong number of items - self.assertRaises(Exception, - self.roundtrip, im, qtables=[[1, 2, 3, 4]]) - - @unittest.skipUnless(djpeg_available(), "djpeg not available") - def test_load_djpeg(self): - img = Image.open(TEST_FILE) - img.load_djpeg() - self.assert_image_similar(img, Image.open(TEST_FILE), 0) - - @unittest.skipUnless(cjpeg_available(), "cjpeg not available") - def test_save_cjpeg(self): - img = Image.open(TEST_FILE) + with Image.open(filename): + pass + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_truncated_jpeg_should_read_all_the_data(self): + filename = "Tests/images/truncated_jpeg.jpg" + ImageFile.LOAD_TRUNCATED_IMAGES = True + with Image.open(filename) as im: + im.load() + ImageFile.LOAD_TRUNCATED_IMAGES = False + assert im.getbbox() is not None + + def test_truncated_jpeg_throws_oserror(self): + filename = "Tests/images/truncated_jpeg.jpg" + with Image.open(filename) as im: + with pytest.raises(OSError): + im.load() + + # Test that the error is raised if loaded a second time + with pytest.raises(OSError): + im.load() + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_qtables(self, tmp_path): + def _n_qtables_helper(n, test_file): + with Image.open(test_file) as im: + f = str(tmp_path / "temp.jpg") + im.save(f, qtables=[[n] * 64] * n) + with Image.open(f) as im: + assert len(im.quantization) == n + reloaded = self.roundtrip(im, qtables="keep") + assert im.quantization == reloaded.quantization + assert max(reloaded.quantization[0]) <= 255 - tempfile = self.tempfile("temp.jpg") - JpegImagePlugin._save_cjpeg(img, 0, tempfile) - # Default save quality is 75%, so a tiny bit of difference is alright - self.assert_image_similar(img, Image.open(tempfile), 17) + with Image.open("Tests/images/hopper.jpg") as im: + qtables = im.quantization + reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) + assert im.quantization == reloaded.quantization + assert_image_similar(im, self.roundtrip(im, qtables="web_low"), 30) + assert_image_similar(im, self.roundtrip(im, qtables="web_high"), 30) + assert_image_similar(im, self.roundtrip(im, qtables="keep"), 30) + + # valid bounds for baseline qtable + bounds_qtable = [int(s) for s in ("255 1 " * 32).split(None)] + im2 = self.roundtrip(im, qtables=[bounds_qtable]) + assert im2.quantization == {0: bounds_qtable} + + # values from wizard.txt in jpeg9-a src package. + standard_l_qtable = [ + int(s) + for s in """ + 16 11 10 16 24 40 51 61 + 12 12 14 19 26 58 60 55 + 14 13 16 24 40 57 69 56 + 14 17 22 29 51 87 80 62 + 18 22 37 56 68 109 103 77 + 24 35 55 64 81 104 113 92 + 49 64 78 87 103 121 120 101 + 72 92 95 98 112 100 103 99 + """.split( + None + ) + ] + + standard_chrominance_qtable = [ + int(s) + for s in """ + 17 18 24 47 99 99 99 99 + 18 21 26 66 99 99 99 99 + 24 26 56 99 99 99 99 99 + 47 66 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + """.split( + None + ) + ] + # list of qtable lists + assert_image_similar( + im, + self.roundtrip( + im, qtables=[standard_l_qtable, standard_chrominance_qtable] + ), + 30, + ) + + # tuple of qtable lists + assert_image_similar( + im, + self.roundtrip( + im, qtables=(standard_l_qtable, standard_chrominance_qtable) + ), + 30, + ) + + # dict of qtable lists + assert_image_similar( + im, + self.roundtrip( + im, qtables={0: standard_l_qtable, 1: standard_chrominance_qtable} + ), + 30, + ) + + _n_qtables_helper(1, "Tests/images/hopper_gray.jpg") + _n_qtables_helper(1, "Tests/images/pil_sample_rgb.jpg") + _n_qtables_helper(2, "Tests/images/pil_sample_rgb.jpg") + _n_qtables_helper(3, "Tests/images/pil_sample_rgb.jpg") + _n_qtables_helper(1, "Tests/images/pil_sample_cmyk.jpg") + _n_qtables_helper(2, "Tests/images/pil_sample_cmyk.jpg") + _n_qtables_helper(3, "Tests/images/pil_sample_cmyk.jpg") + _n_qtables_helper(4, "Tests/images/pil_sample_cmyk.jpg") + + # not a sequence + with pytest.raises(ValueError): + self.roundtrip(im, qtables="a") + # sequence wrong length + with pytest.raises(ValueError): + self.roundtrip(im, qtables=[]) + # sequence wrong length + with pytest.raises(ValueError): + self.roundtrip(im, qtables=[1, 2, 3, 4, 5]) + + # qtable entry not a sequence + with pytest.raises(ValueError): + self.roundtrip(im, qtables=[1]) + # qtable entry has wrong number of items + with pytest.raises(ValueError): + self.roundtrip(im, qtables=[[1, 2, 3, 4]]) + + def test_load_16bit_qtables(self): + with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + assert len(im.quantization) == 2 + assert len(im.quantization[0]) == 64 + assert max(im.quantization[0]) > 255 + + def test_save_multiple_16bit_qtables(self): + with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + im2 = self.roundtrip(im, qtables="keep") + assert im.quantization == im2.quantization + + def test_save_single_16bit_qtable(self): + with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + im2 = self.roundtrip(im, qtables={0: im.quantization[0]}) + assert len(im2.quantization) == 1 + assert im2.quantization[0] == im.quantization[0] + + def test_save_low_quality_baseline_qtables(self): + with Image.open(TEST_FILE) as im: + im2 = self.roundtrip(im, quality=10) + assert len(im2.quantization) == 2 + assert max(im2.quantization[0]) <= 255 + assert max(im2.quantization[1]) <= 255 + + def test_convert_dict_qtables_deprecation(self): + with pytest.warns(DeprecationWarning): + qtable = {0: [1, 2, 3, 4]} + qtable2 = JpegImagePlugin.convert_dict_qtables(qtable) + assert qtable == qtable2 + + @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") + def test_load_djpeg(self): + with Image.open(TEST_FILE) as img: + img.load_djpeg() + assert_image_similar_tofile(img, TEST_FILE, 5) + + @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") + def test_save_cjpeg(self, tmp_path): + with Image.open(TEST_FILE) as img: + tempfile = str(tmp_path / "temp.jpg") + JpegImagePlugin._save_cjpeg(img, 0, tempfile) + # Default save quality is 75%, so a tiny bit of difference is alright + assert_image_similar_tofile(img, tempfile, 17) def test_no_duplicate_0x1001_tag(self): # Arrange - from PIL import ExifTags tag_ids = {v: k for k, v in ExifTags.TAGS.items()} # Assert - self.assertEqual(tag_ids['RelatedImageWidth'], 0x1001) - self.assertEqual(tag_ids['RelatedImageLength'], 0x1002) + assert tag_ids["RelatedImageWidth"] == 0x1001 + assert tag_ids["RelatedImageLength"] == 0x1002 - def test_MAXBLOCK_scaling(self): + def test_MAXBLOCK_scaling(self, tmp_path): im = self.gen_random_image((512, 512)) - f = self.tempfile("temp.jpeg") + f = str(tmp_path / "temp.jpeg") im.save(f, quality=100, optimize=True) - reloaded = Image.open(f) - - # none of these should crash - reloaded.save(f, quality='keep') - reloaded.save(f, quality='keep', progressive=True) - reloaded.save(f, quality='keep', optimize=True) + with Image.open(f) as reloaded: + # none of these should crash + reloaded.save(f, quality="keep") + reloaded.save(f, quality="keep", progressive=True) + reloaded.save(f, quality="keep", optimize=True) def test_bad_mpo_header(self): - """ Treat unknown MPO as JPEG """ + """Treat unknown MPO as JPEG""" # Arrange # Act # Shouldn't raise error fn = "Tests/images/sugarshack_bad_mpo_header.jpg" - im = self.assert_warning(UserWarning, Image.open, fn) + with pytest.warns(UserWarning, Image.open, fn) as im: - # Assert - self.assertEqual(im.format, "JPEG") + # Assert + assert im.format == "JPEG" - def test_save_correct_modes(self): + @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) + def test_save_correct_modes(self, mode): out = BytesIO() - for mode in ['1', 'L', 'RGB', 'RGBX', 'CMYK', 'YCbCr']: - img = Image.new(mode, (20, 20)) - img.save(out, "JPEG") + img = Image.new(mode, (20, 20)) + img.save(out, "JPEG") - def test_save_wrong_modes(self): + @pytest.mark.parametrize("mode", ("LA", "La", "RGBA", "RGBa", "P")) + def test_save_wrong_modes(self, mode): # ref https://github.com/python-pillow/Pillow/issues/2005 out = BytesIO() - for mode in ['LA', 'La', 'RGBA', 'RGBa', 'P']: - img = Image.new(mode, (20, 20)) - self.assertRaises(IOError, img.save, out, "JPEG") + img = Image.new(mode, (20, 20)) + with pytest.raises(OSError): + img.save(out, "JPEG") - def test_save_tiff_with_dpi(self): + def test_save_tiff_with_dpi(self, tmp_path): # Arrange - outfile = self.tempfile("temp.tif") - im = Image.open("Tests/images/hopper.tif") + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/hopper.tif") as im: - # Act - im.save(outfile, 'JPEG', dpi=im.info['dpi']) + # Act + im.save(outfile, "JPEG", dpi=im.info["dpi"]) - # Assert - reloaded = Image.open(outfile) - reloaded.load() - self.assertEqual(im.info['dpi'], reloaded.info['dpi']) + # Assert + with Image.open(outfile) as reloaded: + reloaded.load() + assert im.info["dpi"] == reloaded.info["dpi"] + + def test_save_dpi_rounding(self, tmp_path): + outfile = str(tmp_path / "temp.jpg") + with Image.open("Tests/images/hopper.jpg") as im: + im.save(outfile, dpi=(72.2, 72.2)) + + with Image.open(outfile) as reloaded: + assert reloaded.info["dpi"] == (72, 72) + + im.save(outfile, dpi=(72.8, 72.8)) + + with Image.open(outfile) as reloaded: + assert reloaded.info["dpi"] == (73, 73) def test_dpi_tuple_from_exif(self): # Arrange # This Photoshop CC 2017 image has DPI in EXIF not metadata # EXIF XResolution is (2000000, 10000) - im = Image.open("Tests/images/photoshop-200dpi.jpg") + with Image.open("Tests/images/photoshop-200dpi.jpg") as im: - # Act / Assert - self.assertEqual(im.info.get("dpi"), (200, 200)) + # Act / Assert + assert im.info.get("dpi") == (200, 200) def test_dpi_int_from_exif(self): # Arrange # This image has DPI in EXIF not metadata # EXIF XResolution is 72 - im = Image.open("Tests/images/exif-72dpi-int.jpg") + with Image.open("Tests/images/exif-72dpi-int.jpg") as im: - # Act / Assert - self.assertEqual(im.info.get("dpi"), (72, 72)) + # Act / Assert + assert im.info.get("dpi") == (72, 72) def test_dpi_from_dpcm_exif(self): # Arrange # This is photoshop-200dpi.jpg with EXIF resolution unit set to cm: # exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg - im = Image.open("Tests/images/exif-200dpcm.jpg") + with Image.open("Tests/images/exif-200dpcm.jpg") as im: - # Act / Assert - self.assertEqual(im.info.get("dpi"), (508, 508)) + # Act / Assert + assert im.info.get("dpi") == (508, 508) def test_dpi_exif_zero_division(self): # Arrange # This is photoshop-200dpi.jpg with EXIF resolution set to 0/0: # exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg - im = Image.open("Tests/images/exif-dpi-zerodivision.jpg") + with Image.open("Tests/images/exif-dpi-zerodivision.jpg") as im: + + # Act / Assert + # This should return the default, and not raise a ZeroDivisionError + assert im.info.get("dpi") == (72, 72) - # Act / Assert - # This should return the default, and not raise a ZeroDivisionError - self.assertEqual(im.info.get("dpi"), (72, 72)) + def test_dpi_exif_string(self): + # Arrange + # 0x011A tag in this exif contains string '300300\x02' + with Image.open("Tests/images/broken_exif_dpi.jpg") as im: + + # Act / Assert + # This should return the default + assert im.info.get("dpi") == (72, 72) def test_no_dpi_in_exif(self): # Arrange # This is photoshop-200dpi.jpg with resolution removed from EXIF: # exiftool "-*resolution*"= photoshop-200dpi.jpg - im = Image.open("Tests/images/no-dpi-in-exif.jpg") + with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: - # Act / Assert - # "When the image resolution is unknown, 72 [dpi] is designated." - # http://www.exiv2.org/tags.html - self.assertEqual(im.info.get("dpi"), (72, 72)) + # Act / Assert + # "When the image resolution is unknown, 72 [dpi] is designated." + # https://exiv2.org/tags.html + assert im.info.get("dpi") == (72, 72) def test_invalid_exif(self): # This is no-dpi-in-exif with the tiff header of the exif block # hexedited from MM * to FF FF FF FF - im = Image.open("Tests/images/invalid-exif.jpg") + with Image.open("Tests/images/invalid-exif.jpg") as im: + + # This should return the default, and not a SyntaxError or + # OSError for unidentified image. + assert im.info.get("dpi") == (72, 72) - # This should return the default, and not a SyntaxError or - # OSError for unidentified image. - self.assertEqual(im.info.get("dpi"), (72, 72)) + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_exif_x_resolution(self, tmp_path): + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + assert exif[282] == 180 + out = str(tmp_path / "out.jpg") + with warnings.catch_warnings(): + im.save(out, exif=exif) -@unittest.skipUnless(sys.platform.startswith('win32'), "Windows only") -class TestFileCloseW32(PillowTestCase): - def setUp(self): - if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: - self.skipTest("jpeg support not available") + with Image.open(out) as reloaded: + assert reloaded.getexif()[282] == 180 - def test_fd_leak(self): - tmpfile = self.tempfile("temp.jpg") - import os + def test_invalid_exif_x_resolution(self): + # When no x or y resolution is defined in EXIF + with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im: + + # This should return the default, and not a ValueError or + # OSError for an unidentified image. + assert im.info.get("dpi") == (72, 72) + + def test_ifd_offset_exif(self): + # Arrange + # This image has been manually hexedited to have an IFD offset of 10, + # in contrast to normal 8 + with Image.open("Tests/images/exif-ifd-offset.jpg") as im: + + # Act / Assert + assert im._getexif()[306] == "2017:03:13 23:03:09" + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_photoshop(self): + with Image.open("Tests/images/photoshop-200dpi.jpg") as im: + assert im.info["photoshop"][0x03ED] == { + "XResolution": 200.0, + "DisplayedUnitsX": 1, + "YResolution": 200.0, + "DisplayedUnitsY": 1, + } + + # Test that the image can still load, even with broken Photoshop data + # This image had the APP13 length hexedited to be smaller + assert_image_equal_tofile(im, "Tests/images/photoshop-200dpi-broken.jpg") + + # This image does not contain a Photoshop header string + with Image.open("Tests/images/app13.jpg") as im: + assert "photoshop" not in im.info + + def test_photoshop_malformed_and_multiple(self): + with Image.open("Tests/images/app13-multiple.jpg") as im: + assert "photoshop" in im.info + assert 24 == len(im.info["photoshop"]) + apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] + assert [65504, 24] == apps_13_lengths + + def test_adobe_transform(self): + with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + assert im.info["adobe_transform"] == 1 + + with Image.open("Tests/images/pil_sample_cmyk.jpg") as im: + assert im.info["adobe_transform"] == 2 + + # This image has been manually hexedited + # so that the APP14 reports its length to be 11, + # leaving no room for "adobe_transform" + with Image.open("Tests/images/truncated_app14.jpg") as im: + assert "adobe" in im.info + assert "adobe_transform" not in im.info + + def test_icc_after_SOF(self): + with Image.open("Tests/images/icc-after-SOF.jpg") as im: + assert im.info["icc_profile"] == b"profile" + + def test_jpeg_magic_number(self): + size = 4097 + buffer = BytesIO(b"\xFF" * size) # Many xFF bytes + buffer.max_pos = 0 + orig_read = buffer.read + + def read(n=-1): + res = orig_read(n) + buffer.max_pos = max(buffer.max_pos, buffer.tell()) + return res + + buffer.read = read + with pytest.raises(UnidentifiedImageError): + with Image.open(buffer): + pass + + # Assert the entire file has not been read + assert 0 < buffer.max_pos < size + + def test_getxmp(self): + with Image.open("Tests/images/xmp_test.jpg") as im: + if ElementTree is None: + with pytest.warns(UserWarning): + assert im.getxmp() == {} + else: + xmp = im.getxmp() + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description["DerivedFrom"] == { + "documentID": "8367D410E636EA95B7DE7EBA1C43A412", + "originalDocumentID": "8367D410E636EA95B7DE7EBA1C43A412", + } + assert description["Look"]["Description"]["Group"]["Alt"]["li"] == { + "lang": "x-default", + "text": "Profiles", + } + assert description["ToneCurve"]["Seq"]["li"] == ["0, 0", "255, 255"] + + # Attribute + assert description["Version"] == "10.4" + + if ElementTree is not None: + with Image.open("Tests/images/hopper.jpg") as im: + assert im.getxmp() == {} + + @pytest.mark.timeout(timeout=1) + def test_eof(self): + # Even though this decoder never says that it is finished + # the image should still end when there is no new data + class InfiniteMockPyDecoder(ImageFile.PyDecoder): + def decode(self, buffer): + return 0, 0 + + decoder = InfiniteMockPyDecoder(None) + + def closure(mode, *args): + decoder.__init__(mode, *args) + return decoder + + Image.register_decoder("INFINITE", closure) + + with Image.open(TEST_FILE) as im: + im.tile = [ + ("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), + ] + ImageFile.LOAD_TRUNCATED_IMAGES = True + im.load() + ImageFile.LOAD_TRUNCATED_IMAGES = False + + +@pytest.mark.skipif(not is_win32(), reason="Windows only") +@skip_unless_feature("jpg") +class TestFileCloseW32: + def test_fd_leak(self, tmp_path): + tmpfile = str(tmp_path / "temp.jpg") with Image.open("Tests/images/hopper.jpg") as im: im.save(tmpfile) im = Image.open(tmpfile) fp = im.fp - self.assertFalse(fp.closed) - self.assertRaises(Exception, os.remove, tmpfile) + assert not fp.closed + with pytest.raises(OSError): + os.remove(tmpfile) im.load() - self.assertTrue(fp.closed) + assert fp.closed # this should not fail, as load should have closed the file. os.remove(tmpfile) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 753b505987c..cd142e67fc7 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,11 +1,23 @@ -from helper import unittest, PillowTestCase - -from PIL import Image, Jpeg2KImagePlugin +import os +import re from io import BytesIO -codecs = dir(Image.core) +import pytest + +from PIL import Image, ImageFile, Jpeg2KImagePlugin, UnidentifiedImageError, features + +from .helper import ( + assert_image_equal, + assert_image_similar, + assert_image_similar_tofile, + skip_unless_feature, +) + +EXTRA_DIR = "Tests/images/jpeg2000" -test_card = Image.open('Tests/images/test-card.png') +pytestmark = skip_unless_feature("jpg_2000") + +test_card = Image.open("Tests/images/test-card.png") test_card.load() # OpenJPEG 2.0.0 outputs this debugging message sometimes; we should @@ -13,168 +25,335 @@ # 'Not enough memory to handle tile data' -class TestFileJpeg2k(PillowTestCase): - - def setUp(self): - if "jpeg2k_encoder" not in codecs or "jpeg2k_decoder" not in codecs: - self.skipTest('JPEG 2000 support not available') - - def roundtrip(self, im, **options): - out = BytesIO() - im.save(out, "JPEG2000", **options) - test_bytes = out.tell() - out.seek(0) - im = Image.open(out) +def roundtrip(im, **options): + out = BytesIO() + im.save(out, "JPEG2000", **options) + test_bytes = out.tell() + out.seek(0) + with Image.open(out) as im: im.bytes = test_bytes # for testing only im.load() - return im + return im - def test_sanity(self): - # Internal version number - self.assertRegexpMatches(Image.core.jp2klib_version, r'\d+\.\d+\.\d+$') - im = Image.open('Tests/images/test-card-lossless.jp2') +def test_sanity(): + # Internal version number + assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("jpg_2000")) + + with Image.open("Tests/images/test-card-lossless.jp2") as im: px = im.load() - self.assertEqual(px[0, 0], (0, 0, 0)) - self.assertEqual(im.mode, 'RGB') - self.assertEqual(im.size, (640, 480)) - self.assertEqual(im.format, 'JPEG2000') + assert px[0, 0] == (0, 0, 0) + assert im.mode == "RGB" + assert im.size == (640, 480) + assert im.format == "JPEG2000" + assert im.get_format_mimetype() == "image/jp2" - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - Jpeg2KImagePlugin.Jpeg2KImageFile, invalid_file) +def test_jpf(): + with Image.open("Tests/images/balloon.jpf") as im: + assert im.format == "JPEG2000" + assert im.get_format_mimetype() == "image/jpx" - def test_bytesio(self): - with open('Tests/images/test-card-lossless.jp2', 'rb') as f: - data = BytesIO(f.read()) - im = Image.open(data) - im.load() - self.assert_image_similar(im, test_card, 1.0e-3) - # These two test pre-written JPEG 2000 files that were not written with - # PIL (they were made using Adobe Photoshop) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) + + +def test_bytesio(): + with open("Tests/images/test-card-lossless.jp2", "rb") as f: + data = BytesIO(f.read()) + assert_image_similar_tofile(test_card, data, 1.0e-3) + + +# These two test pre-written JPEG 2000 files that were not written with +# PIL (they were made using Adobe Photoshop) + - def test_lossless(self): - im = Image.open('Tests/images/test-card-lossless.jp2') +def test_lossless(tmp_path): + with Image.open("Tests/images/test-card-lossless.jp2") as im: im.load() - outfile = self.tempfile('temp_test-card.png') + outfile = str(tmp_path / "temp_test-card.png") im.save(outfile) - self.assert_image_similar(im, test_card, 1.0e-3) + assert_image_similar(im, test_card, 1.0e-3) + + +def test_lossy_tiled(): + assert_image_similar_tofile( + test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0 + ) + + +def test_lossless_rt(): + im = roundtrip(test_card) + assert_image_equal(im, test_card) + + +def test_lossy_rt(): + im = roundtrip(test_card, quality_layers=[20]) + assert_image_similar(im, test_card, 2.0) + + +def test_tiled_rt(): + im = roundtrip(test_card, tile_size=(128, 128)) + assert_image_equal(im, test_card) + + +def test_tiled_offset_rt(): + im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) + assert_image_equal(im, test_card) + + +def test_tiled_offset_too_small(): + with pytest.raises(ValueError): + roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) + + +def test_irreversible_rt(): + im = roundtrip(test_card, irreversible=True, quality_layers=[20]) + assert_image_similar(im, test_card, 2.0) - def test_lossy_tiled(self): - im = Image.open('Tests/images/test-card-lossy-tiled.jp2') - im.load() - self.assert_image_similar(im, test_card, 2.0) - def test_lossless_rt(self): - im = self.roundtrip(test_card) - self.assert_image_equal(im, test_card) +def test_prog_qual_rt(): + im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") + assert_image_similar(im, test_card, 2.0) - def test_lossy_rt(self): - im = self.roundtrip(test_card, quality_layers=[20]) - self.assert_image_similar(im, test_card, 2.0) - def test_tiled_rt(self): - im = self.roundtrip(test_card, tile_size=(128, 128)) - self.assert_image_equal(im, test_card) +def test_prog_res_rt(): + im = roundtrip(test_card, num_resolutions=8, progression="RLCP") + assert_image_equal(im, test_card) - def test_tiled_offset_rt(self): - im = self.roundtrip( - test_card, tile_size=(128, 128), - tile_offset=(0, 0), offset=(32, 32)) - self.assert_image_equal(im, test_card) - def test_irreversible_rt(self): - im = self.roundtrip(test_card, irreversible=True, quality_layers=[20]) - self.assert_image_similar(im, test_card, 2.0) +@pytest.mark.parametrize("num_resolutions", range(2, 6)) +def test_default_num_resolutions(num_resolutions): + d = 1 << (num_resolutions - 1) + im = test_card.resize((d - 1, d - 1)) + with pytest.raises(OSError): + roundtrip(im, num_resolutions=num_resolutions) + reloaded = roundtrip(im) + assert_image_equal(im, reloaded) - def test_prog_qual_rt(self): - im = self.roundtrip( - test_card, quality_layers=[60, 40, 20], progression='LRCP') - self.assert_image_similar(im, test_card, 2.0) - def test_prog_res_rt(self): - im = self.roundtrip(test_card, num_resolutions=8, progression='RLCP') - self.assert_image_equal(im, test_card) +def test_reduce(): + with Image.open("Tests/images/test-card-lossless.jp2") as im: + assert callable(im.reduce) - def test_reduce(self): - im = Image.open('Tests/images/test-card-lossless.jp2') im.reduce = 2 + assert im.reduce == 2 + im.load() - self.assertEqual(im.size, (160, 120)) + assert im.size == (160, 120) + + im.thumbnail((40, 40)) + assert im.size == (40, 30) + + +def test_load_dpi(): + with Image.open("Tests/images/test-card-lossless.jp2") as im: + assert im.info["dpi"] == (71.9836, 71.9836) + + with Image.open("Tests/images/zero_dpi.jp2") as im: + assert "dpi" not in im.info + + +def test_restricted_icc_profile(): + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + # JPEG2000 image with a restricted ICC profile and a known colorspace + with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im: + assert im.mode == "RGB" + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_layers(self): - out = BytesIO() - test_card.save(out, 'JPEG2000', quality_layers=[100, 50, 10], - progression='LRCP') - out.seek(0) - im = Image.open(out) +def test_header_errors(): + for path in ( + "Tests/images/invalid_header_length.jp2", + "Tests/images/not_enough_data.jp2", + ): + with pytest.raises(UnidentifiedImageError): + with Image.open(path): + pass + + with pytest.raises(OSError): + with Image.open("Tests/images/expected_to_read.jp2"): + pass + + +def test_layers_type(tmp_path): + outfile = str(tmp_path / "temp_layers.jp2") + for quality_layers in [[100, 50, 10], (100, 50, 10), None]: + test_card.save(outfile, quality_layers=quality_layers) + + for quality_layers in ["quality_layers", ("100", "50", "10")]: + with pytest.raises(ValueError): + test_card.save(outfile, quality_layers=quality_layers) + + +def test_layers(): + out = BytesIO() + test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") + out.seek(0) + + with Image.open(out) as im: im.layers = 1 im.load() - self.assert_image_similar(im, test_card, 13) + assert_image_similar(im, test_card, 13) - out.seek(0) - im = Image.open(out) + out.seek(0) + with Image.open(out) as im: im.layers = 3 im.load() - self.assert_image_similar(im, test_card, 0.4) + assert_image_similar(im, test_card, 0.4) + + +@pytest.mark.parametrize( + "name, args, offset, data", + ( + ("foo.j2k", {}, 0, b"\xff\x4f"), + ("foo.jp2", {}, 4, b"jP"), + (None, {"no_jp2": True}, 0, b"\xff\x4f"), + ("foo.j2k", {"no_jp2": True}, 0, b"\xff\x4f"), + ("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"), + ("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"), + ("foo.jp2", {"no_jp2": False}, 4, b"jP"), + ("foo.jp2", {"no_jp2": False}, 4, b"jP"), + ), +) +def test_no_jp2(name, args, offset, data): + out = BytesIO() + if name: + out.name = name + test_card.save(out, "JPEG2000", **args) + out.seek(offset) + assert out.read(2) == data + + +def test_mct(): + # Three component + for val in (0, 1): + out = BytesIO() + test_card.save(out, "JPEG2000", mct=val, no_jp2=True) + + assert out.getvalue()[59] == val + with Image.open(out) as im: + assert_image_similar(im, test_card, 1.0e-3) + + # Single component should have MCT disabled + for val in (0, 1): + out = BytesIO() + with Image.open("Tests/images/16bit.cropped.jp2") as jp2: + jp2.save(out, "JPEG2000", mct=val, no_jp2=True) + + assert out.getvalue()[53] == 0 + with Image.open(out) as im: + assert_image_similar(im, jp2, 1.0e-3) + + +def test_rgba(): + # Arrange + with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: + with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2: + + # Act + j2k.load() + jp2.load() + + # Assert + assert j2k.mode == "RGBA" + assert jp2.mode == "RGBA" + + +@pytest.mark.parametrize("ext", (".j2k", ".jp2")) +def test_16bit_monochrome_has_correct_mode(ext): + with Image.open("Tests/images/16bit.cropped" + ext) as im: + im.load() + assert im.mode == "I;16" + + +def test_16bit_monochrome_jp2_like_tiff(): + with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: + assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.jp2", 1e-3) + + +def test_16bit_monochrome_j2k_like_tiff(): + with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: + assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.j2k", 1e-3) - def test_rgba(self): - # Arrange - j2k = Image.open('Tests/images/rgb_trns_ycbc.j2k') - jp2 = Image.open('Tests/images/rgb_trns_ycbc.jp2') - # Act - j2k.load() - jp2.load() +def test_16bit_j2k_roundtrips(): + with Image.open("Tests/images/16bit.cropped.j2k") as j2k: + im = roundtrip(j2k) + assert_image_equal(im, j2k) - # Assert - self.assertEqual(j2k.mode, 'RGBA') - self.assertEqual(jp2.mode, 'RGBA') - def test_16bit_monochrome_has_correct_mode(self): +def test_16bit_jp2_roundtrips(): + with Image.open("Tests/images/16bit.cropped.jp2") as jp2: + im = roundtrip(jp2) + assert_image_equal(im, jp2) - j2k = Image.open('Tests/images/16bit.cropped.j2k') - jp2 = Image.open('Tests/images/16bit.cropped.jp2') - j2k.load() - jp2.load() +def test_issue_6194(): + with Image.open("Tests/images/issue_6194.j2k") as im: + assert im.getpixel((5, 5)) == 31 - self.assertEqual(j2k.mode, 'I;16') - self.assertEqual(jp2.mode, 'I;16') - def test_16bit_monchrome_jp2_like_tiff(self): +def test_unbound_local(): + # prepatch, a malformed jp2 file could cause an UnboundLocalError exception. + with pytest.raises(OSError): + with Image.open("Tests/images/unbound_variable.jp2"): + pass - tiff_16bit = Image.open('Tests/images/16bit.cropped.tif') - jp2 = Image.open('Tests/images/16bit.cropped.jp2') - self.assert_image_similar(jp2, tiff_16bit, 1e-3) - def test_16bit_monchrome_j2k_like_tiff(self): +def test_parser_feed(): + # Arrange + with open("Tests/images/test-card-lossless.jp2", "rb") as f: + data = f.read() - tiff_16bit = Image.open('Tests/images/16bit.cropped.tif') - j2k = Image.open('Tests/images/16bit.cropped.j2k') - self.assert_image_similar(j2k, tiff_16bit, 1e-3) + # Act + p = ImageFile.Parser() + p.feed(data) - def test_16bit_j2k_roundtrips(self): + # Assert + assert p.image.size == (640, 480) - j2k = Image.open('Tests/images/16bit.cropped.j2k') - im = self.roundtrip(j2k) - self.assert_image_equal(im, j2k) - def test_16bit_jp2_roundtrips(self): +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +@pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2")) +def test_subsampling_decode(name): + test = f"{EXTRA_DIR}/{name}.jp2" + reference = f"{EXTRA_DIR}/{name}.ppm" - jp2 = Image.open('Tests/images/16bit.cropped.jp2') - im = self.roundtrip(jp2) - self.assert_image_equal(im, jp2) + with Image.open(test) as im: + epsilon = 3 # for YCbCr images + with Image.open(reference) as im2: + width, height = im2.size + if name[-1] == "2": + # RGB reference images are downscaled + epsilon = 3e-3 + width, height = width * 2, height * 2 + expected = im2.resize((width, height), Image.Resampling.NEAREST) + assert_image_similar(im, expected, epsilon) - def test_unbound_local(self): - # prepatch, a malformed jp2 file could cause an UnboundLocalError - # exception. - with self.assertRaises(IOError): - Image.open('Tests/images/unbound_variable.jp2') -if __name__ == '__main__': - unittest.main() +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k", + "Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k", + "Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k", + "Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k", + ], +) +def test_crashes(test_file): + with open(test_file, "rb") as f: + with Image.open(f) as im: + # Valgrind should not complain here + try: + im.load() + except OSError: + pass diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index dc3b1084573..1109cd15e99 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,138 +1,146 @@ -from __future__ import print_function -from helper import unittest, PillowTestCase, hopper, py3 -from PIL import features - -from ctypes import c_float +import base64 import io -import logging import itertools import os +import re +import sys +from collections import namedtuple -from PIL import Image, TiffImagePlugin, TiffTags - -logger = logging.getLogger(__name__) +import pytest +from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags, features +from PIL.TiffImagePlugin import SAMPLEFORMAT, STRIPOFFSETS, SUBIFD -class LibTiffTestCase(PillowTestCase): +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + assert_image_similar_tofile, + hopper, + mark_if_feature_version, + skip_unless_feature, +) - def setUp(self): - if not features.check('libtiff'): - self.skipTest("tiff support not available") - def _assert_noerr(self, im): +@skip_unless_feature("libtiff") +class LibTiffTestCase: + def _assert_noerr(self, tmp_path, im): """Helper tests that assert basic sanity about the g4 tiff reading""" # 1 bit - self.assertEqual(im.mode, "1") + assert im.mode == "1" # Does the data actually load im.load() im.getdata() try: - self.assertEqual(im._compression, 'group4') - except: + assert im._compression == "group4" + except AttributeError: print("No _compression") print(dir(im)) # can we write it back out, in a different form. - out = self.tempfile("temp.png") + out = str(tmp_path / "temp.png") im.save(out) out_bytes = io.BytesIO() - im.save(out_bytes, format='tiff', compression='group4') + im.save(out_bytes, format="tiff", compression="group4") class TestFileLibTiff(LibTiffTestCase): + def test_version(self): + assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("libtiff")) - def test_g4_tiff(self): + def test_g4_tiff(self, tmp_path): """Test the ordinary file path load path""" test_file = "Tests/images/hopper_g4_500.tif" - im = Image.open(test_file) + with Image.open(test_file) as im: + assert im.size == (500, 500) + self._assert_noerr(tmp_path, im) - self.assertEqual(im.size, (500, 500)) - self._assert_noerr(im) - - def test_g4_large(self): + def test_g4_large(self, tmp_path): test_file = "Tests/images/pport_g4.tif" - im = Image.open(test_file) - self._assert_noerr(im) + with Image.open(test_file) as im: + self._assert_noerr(tmp_path, im) - def test_g4_tiff_file(self): + def test_g4_tiff_file(self, tmp_path): """Testing the string load path""" test_file = "Tests/images/hopper_g4_500.tif" - with open(test_file, 'rb') as f: - im = Image.open(f) - - self.assertEqual(im.size, (500, 500)) - self._assert_noerr(im) + with open(test_file, "rb") as f: + with Image.open(f) as im: + assert im.size == (500, 500) + self._assert_noerr(tmp_path, im) - def test_g4_tiff_bytesio(self): + def test_g4_tiff_bytesio(self, tmp_path): """Testing the stringio loading code path""" test_file = "Tests/images/hopper_g4_500.tif" s = io.BytesIO() - with open(test_file, 'rb') as f: + with open(test_file, "rb") as f: s.write(f.read()) s.seek(0) - im = Image.open(s) + with Image.open(s) as im: + assert im.size == (500, 500) + self._assert_noerr(tmp_path, im) - self.assertEqual(im.size, (500, 500)) - self._assert_noerr(im) + def test_g4_non_disk_file_object(self, tmp_path): + """Testing loading from non-disk non-BytesIO file object""" + test_file = "Tests/images/hopper_g4_500.tif" + s = io.BytesIO() + with open(test_file, "rb") as f: + s.write(f.read()) + s.seek(0) + r = io.BufferedReader(s) + with Image.open(r) as im: + assert im.size == (500, 500) + self._assert_noerr(tmp_path, im) def test_g4_eq_png(self): - """ Checking that we're actually getting the data that we expect""" - png = Image.open('Tests/images/hopper_bw_500.png') - g4 = Image.open('Tests/images/hopper_g4_500.tif') - - self.assert_image_equal(g4, png) + """Checking that we're actually getting the data that we expect""" + with Image.open("Tests/images/hopper_bw_500.png") as png: + assert_image_equal_tofile(png, "Tests/images/hopper_g4_500.tif") # see https://github.com/python-pillow/Pillow/issues/279 def test_g4_fillorder_eq_png(self): - """ Checking that we're actually getting the data that we expect""" - png = Image.open('Tests/images/g4-fillorder-test.png') - g4 = Image.open('Tests/images/g4-fillorder-test.tif') - - self.assert_image_equal(g4, png) + """Checking that we're actually getting the data that we expect""" + with Image.open("Tests/images/g4-fillorder-test.tif") as g4: + assert_image_equal_tofile(g4, "Tests/images/g4-fillorder-test.png") - def test_g4_write(self): + def test_g4_write(self, tmp_path): """Checking to see that the saved image is the same as what we wrote""" test_file = "Tests/images/hopper_g4_500.tif" - orig = Image.open(test_file) + with Image.open(test_file) as orig: + out = str(tmp_path / "temp.tif") + rot = orig.transpose(Image.Transpose.ROTATE_90) + assert rot.size == (500, 500) + rot.save(out) - out = self.tempfile("temp.tif") - rot = orig.transpose(Image.ROTATE_90) - self.assertEqual(rot.size, (500, 500)) - rot.save(out) + with Image.open(out) as reread: + assert reread.size == (500, 500) + self._assert_noerr(tmp_path, reread) + assert_image_equal(reread, rot) + assert reread.info["compression"] == "group4" - reread = Image.open(out) - self.assertEqual(reread.size, (500, 500)) - self._assert_noerr(reread) - self.assert_image_equal(reread, rot) - self.assertEqual(reread.info['compression'], 'group4') + assert reread.info["compression"] == orig.info["compression"] - self.assertEqual(reread.info['compression'], orig.info['compression']) - - self.assertNotEqual(orig.tobytes(), reread.tobytes()) + assert orig.tobytes() != reread.tobytes() def test_adobe_deflate_tiff(self): test_file = "Tests/images/tiff_adobe_deflate.tif" - im = Image.open(test_file) - - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (278, 374)) - self.assertEqual( - im.tile[0][:3], ('tiff_adobe_deflate', (0, 0, 278, 374), 0)) - im.load() - - self.assert_image_equal_tofile(im, 'Tests/images/tiff_adobe_deflate.png') + with Image.open(test_file) as im: + assert im.mode == "RGB" + assert im.size == (278, 374) + assert im.tile[0][:3] == ("libtiff", (0, 0, 278, 374), 0) + im.load() - def test_write_metadata(self): - """ Test metadata writing through libtiff """ - for legacy_api in [False, True]: - img = Image.open('Tests/images/hopper_g4.tif') - f = self.tempfile('temp.tiff') + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + @pytest.mark.parametrize("legacy_api", (False, True)) + def test_write_metadata(self, legacy_api, tmp_path): + """Test metadata writing through libtiff""" + f = str(tmp_path / "temp.tiff") + with Image.open("Tests/images/hopper_g4.tif") as img: img.save(f, tiffinfo=img.tag) if legacy_api: @@ -140,227 +148,375 @@ def test_write_metadata(self): else: original = img.tag_v2.named() - # PhotometricInterpretation is set from SAVE_INFO, - # not the original image. - ignored = ['StripByteCounts', 'RowsPerStrip', 'PageNumber', - 'PhotometricInterpretation'] + # PhotometricInterpretation is set from SAVE_INFO, + # not the original image. + ignored = [ + "StripByteCounts", + "RowsPerStrip", + "PageNumber", + "PhotometricInterpretation", + ] - loaded = Image.open(f) + with Image.open(f) as loaded: if legacy_api: reloaded = loaded.tag.named() else: reloaded = loaded.tag_v2.named() - for tag, value in itertools.chain(reloaded.items(), - original.items()): - if tag not in ignored: - val = original[tag] - if tag.endswith('Resolution'): - if legacy_api: - self.assertEqual( - c_float(val[0][0] / val[0][1]).value, - c_float(value[0][0] / value[0][1]).value, - msg="%s didn't roundtrip" % tag) - else: - self.assertEqual( - c_float(val).value, c_float(value).value, - msg="%s didn't roundtrip" % tag) + for tag, value in itertools.chain(reloaded.items(), original.items()): + if tag not in ignored: + val = original[tag] + if tag.endswith("Resolution"): + if legacy_api: + assert val[0][0] / val[0][1] == ( + 4294967295 / 113653537 + ), f"{tag} didn't roundtrip" else: - self.assertEqual( - val, value, msg="%s didn't roundtrip" % tag) + assert val == 37.79000115940079, f"{tag} didn't roundtrip" + else: + assert val == value, f"{tag} didn't roundtrip" - # https://github.com/python-pillow/Pillow/issues/1561 - requested_fields = ['StripByteCounts', - 'RowsPerStrip', - 'StripOffsets'] - for field in requested_fields: - self.assertIn(field, reloaded, "%s not in metadata" % field) + # https://github.com/python-pillow/Pillow/issues/1561 + requested_fields = ["StripByteCounts", "RowsPerStrip", "StripOffsets"] + for field in requested_fields: + assert field in reloaded, f"{field} not in metadata" - def test_additional_metadata(self): + @pytest.mark.valgrind_known_error(reason="Known invalid metadata") + def test_additional_metadata(self, tmp_path): # these should not crash. Seriously dummy data, most of it doesn't make # any sense, so we're running up against limits where we're asking # libtiff to do stupid things. # Get the list of the ones that we should be able to write - core_items = {tag: info for tag, info in ((s, TiffTags.lookup(s)) for s - in TiffTags.LIBTIFF_CORE) - if info.type is not None} + core_items = { + tag: info + for tag, info in ((s, TiffTags.lookup(s)) for s in TiffTags.LIBTIFF_CORE) + if info.type is not None + } # Exclude ones that have special meaning # that we're already testing them - im = Image.open('Tests/images/hopper_g4.tif') - for tag in im.tag_v2: - try: - del(core_items[tag]) - except: - pass - - # Type codes: - # 2: "ascii", - # 3: "short", - # 4: "long", - # 5: "rational", - # 12: "double", - # type: dummy value - values = {2: 'test', - 3: 1, - 4: 2**20, - 5: TiffImagePlugin.IFDRational(100, 1), - 12: 1.05} - - new_ifd = TiffImagePlugin.ImageFileDirectory_v2() - for tag, info in core_items.items(): - if info.length == 1: - new_ifd[tag] = values[info.type] - if info.length == 0: - new_ifd[tag] = tuple(values[info.type] for _ in range(3)) - else: - new_ifd[tag] = tuple(values[info.type] for _ in range(info.length)) + with Image.open("Tests/images/hopper_g4.tif") as im: + for tag in im.tag_v2: + try: + del core_items[tag] + except KeyError: + pass + del core_items[320] # colormap is special, tested below + + # Type codes: + # 2: "ascii", + # 3: "short", + # 4: "long", + # 5: "rational", + # 12: "double", + # Type: dummy value + values = { + 2: "test", + 3: 1, + 4: 2**20, + 5: TiffImagePlugin.IFDRational(100, 1), + 12: 1.05, + } + + new_ifd = TiffImagePlugin.ImageFileDirectory_v2() + for tag, info in core_items.items(): + if info.length == 1: + new_ifd[tag] = values[info.type] + if info.length == 0: + new_ifd[tag] = tuple(values[info.type] for _ in range(3)) + else: + new_ifd[tag] = tuple(values[info.type] for _ in range(info.length)) + + # Extra samples really doesn't make sense in this application. + del new_ifd[338] + + out = str(tmp_path / "temp.tif") + TiffImagePlugin.WRITE_LIBTIFF = True + + im.save(out, tiffinfo=new_ifd) - # Extra samples really doesn't make sense in this application. - del(new_ifd[338]) + TiffImagePlugin.WRITE_LIBTIFF = False - out = self.tempfile("temp.tif") - TiffImagePlugin.WRITE_LIBTIFF = True + def test_custom_metadata(self, tmp_path): + tc = namedtuple("test_case", "value,type,supported_by_default") + custom = { + 37000 + k: v + for k, v in enumerate( + [ + tc(4, TiffTags.SHORT, True), + tc(123456789, TiffTags.LONG, True), + tc(-4, TiffTags.SIGNED_BYTE, False), + tc(-4, TiffTags.SIGNED_SHORT, False), + tc(-123456789, TiffTags.SIGNED_LONG, False), + tc(TiffImagePlugin.IFDRational(4, 7), TiffTags.RATIONAL, True), + tc(4.25, TiffTags.FLOAT, True), + tc(4.25, TiffTags.DOUBLE, True), + tc("custom tag value", TiffTags.ASCII, True), + tc(b"custom tag value", TiffTags.BYTE, True), + tc((4, 5, 6), TiffTags.SHORT, True), + tc((123456789, 9, 34, 234, 219387, 92432323), TiffTags.LONG, True), + tc((-4, 9, 10), TiffTags.SIGNED_BYTE, False), + tc((-4, 5, 6), TiffTags.SIGNED_SHORT, False), + tc( + (-123456789, 9, 34, 234, 219387, -92432323), + TiffTags.SIGNED_LONG, + False, + ), + tc((4.25, 5.25), TiffTags.FLOAT, True), + tc((4.25, 5.25), TiffTags.DOUBLE, True), + # array of TIFF_BYTE requires bytes instead of tuple for backwards + # compatibility + tc(bytes([4]), TiffTags.BYTE, True), + tc(bytes((4, 9, 10)), TiffTags.BYTE, True), + ] + ) + } + + libtiffs = [False] + if Image.core.libtiff_support_custom_tags: + libtiffs.append(True) + + for libtiff in libtiffs: + TiffImagePlugin.WRITE_LIBTIFF = libtiff + + def check_tags(tiffinfo): + im = hopper() + + out = str(tmp_path / "temp.tif") + im.save(out, tiffinfo=tiffinfo) + + with Image.open(out) as reloaded: + for tag, value in tiffinfo.items(): + reloaded_value = reloaded.tag_v2[tag] + if ( + isinstance(reloaded_value, TiffImagePlugin.IFDRational) + and libtiff + ): + # libtiff does not support real RATIONALS + assert ( + round(abs(float(reloaded_value) - float(value)), 7) == 0 + ) + continue + + assert reloaded_value == value + + # Test with types + ifd = TiffImagePlugin.ImageFileDirectory_v2() + for tag, tagdata in custom.items(): + ifd[tag] = tagdata.value + ifd.tagtype[tag] = tagdata.type + check_tags(ifd) + + # Test without types. This only works for some types, int for example are + # always encoded as LONG and not SIGNED_LONG. + check_tags( + { + tag: tagdata.value + for tag, tagdata in custom.items() + if tagdata.supported_by_default + } + ) + TiffImagePlugin.WRITE_LIBTIFF = False - im.save(out, tiffinfo=new_ifd) + def test_subifd(self, tmp_path): + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/g4_orientation_6.tif") as im: + im.tag_v2[SUBIFD] = 10000 + # Should not segfault + im.save(outfile) + + def test_xmlpacket_tag(self, tmp_path): + TiffImagePlugin.WRITE_LIBTIFF = True + + out = str(tmp_path / "temp.tif") + hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) TiffImagePlugin.WRITE_LIBTIFF = False - def test_g3_compression(self): - i = Image.open('Tests/images/hopper_g4_500.tif') - out = self.tempfile("temp.tif") - i.save(out, compression='group3') - - reread = Image.open(out) - self.assertEqual(reread.info['compression'], 'group3') - self.assert_image_equal(reread, i) - - def test_little_endian(self): - im = Image.open('Tests/images/16bit.deflate.tif') - self.assertEqual(im.getpixel((0, 0)), 480) - self.assertEqual(im.mode, 'I;16') - - b = im.tobytes() - # Bytes are in image native order (little endian) - if py3: - self.assertEqual(b[0], ord(b'\xe0')) - self.assertEqual(b[1], ord(b'\x01')) - else: - self.assertEqual(b[0], b'\xe0') - self.assertEqual(b[1], b'\x01') - - out = self.tempfile("temp.tif") - # out = "temp.le.tif" - im.save(out) - reread = Image.open(out) + with Image.open(out) as reloaded: + if 700 in reloaded.tag_v2: + assert reloaded.tag_v2[700] == b"xmlpacket tag" - self.assertEqual(reread.info['compression'], im.info['compression']) - self.assertEqual(reread.getpixel((0, 0)), 480) + def test_int_dpi(self, tmp_path): + # issue #1765 + im = hopper("RGB") + out = str(tmp_path / "temp.tif") + TiffImagePlugin.WRITE_LIBTIFF = True + im.save(out, dpi=(72, 72)) + TiffImagePlugin.WRITE_LIBTIFF = False + with Image.open(out) as reloaded: + assert reloaded.info["dpi"] == (72.0, 72.0) + + def test_g3_compression(self, tmp_path): + with Image.open("Tests/images/hopper_g4_500.tif") as i: + out = str(tmp_path / "temp.tif") + i.save(out, compression="group3") + + with Image.open(out) as reread: + assert reread.info["compression"] == "group3" + assert_image_equal(reread, i) + + def test_little_endian(self, tmp_path): + with Image.open("Tests/images/16bit.deflate.tif") as im: + assert im.getpixel((0, 0)) == 480 + assert im.mode == "I;16" + + b = im.tobytes() + # Bytes are in image native order (little endian) + assert b[0] == ord(b"\xe0") + assert b[1] == ord(b"\x01") + + out = str(tmp_path / "temp.tif") + # out = "temp.le.tif" + im.save(out) + with Image.open(out) as reread: + assert reread.info["compression"] == im.info["compression"] + assert reread.getpixel((0, 0)) == 480 # UNDONE - libtiff defaults to writing in native endian, so # on big endian, we'll get back mode = 'I;16B' here. - def test_big_endian(self): - im = Image.open('Tests/images/16bit.MM.deflate.tif') + def test_big_endian(self, tmp_path): + with Image.open("Tests/images/16bit.MM.deflate.tif") as im: + assert im.getpixel((0, 0)) == 480 + assert im.mode == "I;16B" - self.assertEqual(im.getpixel((0, 0)), 480) - self.assertEqual(im.mode, 'I;16B') + b = im.tobytes() - b = im.tobytes() + # Bytes are in image native order (big endian) + assert b[0] == ord(b"\x01") + assert b[1] == ord(b"\xe0") - # Bytes are in image native order (big endian) - if py3: - self.assertEqual(b[0], ord(b'\x01')) - self.assertEqual(b[1], ord(b'\xe0')) - else: - self.assertEqual(b[0], b'\x01') - self.assertEqual(b[1], b'\xe0') + out = str(tmp_path / "temp.tif") + im.save(out) + with Image.open(out) as reread: + assert reread.info["compression"] == im.info["compression"] + assert reread.getpixel((0, 0)) == 480 - out = self.tempfile("temp.tif") - im.save(out) - reread = Image.open(out) - - self.assertEqual(reread.info['compression'], im.info['compression']) - self.assertEqual(reread.getpixel((0, 0)), 480) - - def test_g4_string_info(self): + def test_g4_string_info(self, tmp_path): """Tests String data in info directory""" test_file = "Tests/images/hopper_g4_500.tif" - orig = Image.open(test_file) + with Image.open(test_file) as orig: + out = str(tmp_path / "temp.tif") - out = self.tempfile("temp.tif") + orig.tag[269] = "temp.tif" + orig.save(out) - orig.tag[269] = 'temp.tif' - orig.save(out) - - reread = Image.open(out) - self.assertEqual('temp.tif', reread.tag_v2[269]) - self.assertEqual('temp.tif', reread.tag[269][0]) + with Image.open(out) as reread: + assert "temp.tif" == reread.tag_v2[269] + assert "temp.tif" == reread.tag[269][0] def test_12bit_rawmode(self): - """ Are we generating the same interpretation - of the image as Imagemagick is? """ + """Are we generating the same interpretation + of the image as Imagemagick is?""" TiffImagePlugin.READ_LIBTIFF = True - im = Image.open('Tests/images/12bit.cropped.tif') - im.load() - TiffImagePlugin.READ_LIBTIFF = False - # to make the target -- - # convert 12bit.cropped.tif -depth 16 tmp.tif - # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif - # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0, - # so we need to unshift so that the integer values are the same. + with Image.open("Tests/images/12bit.cropped.tif") as im: + im.load() + TiffImagePlugin.READ_LIBTIFF = False + # to make the target -- + # convert 12bit.cropped.tif -depth 16 tmp.tif + # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif + # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0, + # so we need to unshift so that the integer values are the same. - self.assert_image_equal_tofile(im, 'Tests/images/12in16bit.tif') + assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") - def test_blur(self): + def test_blur(self, tmp_path): # test case from irc, how to do blur on b/w image # and save to compressed tif. - from PIL import ImageFilter - out = self.tempfile('temp.tif') - im = Image.open('Tests/images/pport_g4.tif') - im = im.convert('L') + out = str(tmp_path / "temp.tif") + with Image.open("Tests/images/pport_g4.tif") as im: + im = im.convert("L") im = im.filter(ImageFilter.GaussianBlur(4)) - im.save(out, compression='tiff_adobe_deflate') - - im2 = Image.open(out) - im2.load() + im.save(out, compression="tiff_adobe_deflate") - self.assert_image_equal(im, im2) + assert_image_equal_tofile(im, out) - def test_compressions(self): - im = hopper('RGB') - out = self.tempfile('temp.tif') + def test_compressions(self, tmp_path): + # Test various tiff compressions and assert similar image content but reduced + # file sizes. + im = hopper("RGB") + out = str(tmp_path / "temp.tif") + im.save(out) + size_raw = os.path.getsize(out) - for compression in ('packbits', 'tiff_lzw'): + for compression in ("packbits", "tiff_lzw"): im.save(out, compression=compression) - im2 = Image.open(out) - self.assert_image_equal(im, im2) + size_compressed = os.path.getsize(out) + assert_image_equal_tofile(im, out) + + im.save(out, compression="jpeg") + size_jpeg = os.path.getsize(out) + with Image.open(out) as im2: + assert_image_similar(im, im2, 30) + + im.save(out, compression="jpeg", quality=30) + size_jpeg_30 = os.path.getsize(out) + assert_image_similar_tofile(im2, out, 30) + + assert size_raw > size_compressed + assert size_compressed > size_jpeg + assert size_jpeg > size_jpeg_30 + + def test_tiff_jpeg_compression(self, tmp_path): + im = hopper("RGB") + out = str(tmp_path / "temp.tif") + im.save(out, compression="tiff_jpeg") + + with Image.open(out) as reloaded: + assert reloaded.info["compression"] == "jpeg" + + def test_tiff_deflate_compression(self, tmp_path): + im = hopper("RGB") + out = str(tmp_path / "temp.tif") + im.save(out, compression="tiff_deflate") + + with Image.open(out) as reloaded: + assert reloaded.info["compression"] == "tiff_adobe_deflate" + + def test_quality(self, tmp_path): + im = hopper("RGB") + out = str(tmp_path / "temp.tif") + + with pytest.raises(ValueError): + im.save(out, compression="tiff_lzw", quality=50) + with pytest.raises(ValueError): + im.save(out, compression="jpeg", quality=-1) + with pytest.raises(ValueError): + im.save(out, compression="jpeg", quality=101) + with pytest.raises(ValueError): + im.save(out, compression="jpeg", quality="good") + im.save(out, compression="jpeg", quality=0) + im.save(out, compression="jpeg", quality=100) + + def test_cmyk_save(self, tmp_path): + im = hopper("CMYK") + out = str(tmp_path / "temp.tif") + + im.save(out, compression="tiff_adobe_deflate") + assert_image_equal_tofile(im, out) + + @pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000"))) + def test_palette_save(self, im, tmp_path): + out = str(tmp_path / "temp.tif") - im.save(out, compression='jpeg') - im2 = Image.open(out) - self.assert_image_similar(im, im2, 30) - - def test_cmyk_save(self): - im = hopper('CMYK') - out = self.tempfile('temp.tif') - - im.save(out, compression='tiff_adobe_deflate') - im2 = Image.open(out) - self.assert_image_equal(im, im2) + TiffImagePlugin.WRITE_LIBTIFF = True + im.save(out) + TiffImagePlugin.WRITE_LIBTIFF = False - def xtest_bw_compression_w_rgb(self): - """ This test passes, but when running all tests causes a failure due - to output on stderr from the error thrown by libtiff. We need to - capture that but not now""" + with Image.open(out) as reloaded: + # colormap/palette tag + assert len(reloaded.tag_v2[320]) == 768 - im = hopper('RGB') - out = self.tempfile('temp.tif') + @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) + def test_bw_compression_w_rgb(self, compression, tmp_path): + im = hopper("RGB") + out = str(tmp_path / "temp.tif") - self.assertRaises(IOError, im.save, out, compression='tiff_ccitt') - self.assertRaises(IOError, im.save, out, compression='group3') - self.assertRaises(IOError, im.save, out, compression='group4') + with pytest.raises(OSError): + im.save(out, compression=compression) def test_fp_leak(self): im = Image.open("Tests/images/hopper_g4_500.tif") @@ -368,53 +524,67 @@ def test_fp_leak(self): os.fstat(fn) im.load() # this should close it. - self.assertRaises(OSError, os.fstat, fn) + with pytest.raises(OSError): + os.fstat(fn) im = None # this should force even more closed. - self.assertRaises(OSError, os.fstat, fn) - self.assertRaises(OSError, os.close, fn) + with pytest.raises(OSError): + os.fstat(fn) + with pytest.raises(OSError): + os.close(fn) def test_multipage(self): # issue #862 TiffImagePlugin.READ_LIBTIFF = True - im = Image.open('Tests/images/multipage.tiff') - # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue + with Image.open("Tests/images/multipage.tiff") as im: + # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue - im.seek(0) - self.assertEqual(im.size, (10, 10)) - self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 128, 0)) - self.assertTrue(im.tag.next) + im.seek(0) + assert im.size == (10, 10) + assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) + assert im.tag.next - im.seek(1) - self.assertEqual(im.size, (10, 10)) - self.assertEqual(im.convert('RGB').getpixel((0, 0)), (255, 0, 0)) - self.assertTrue(im.tag.next) + im.seek(1) + assert im.size == (10, 10) + assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) + assert im.tag.next - im.seek(2) - self.assertFalse(im.tag.next) - self.assertEqual(im.size, (20, 20)) - self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 0, 255)) + im.seek(2) + assert not im.tag.next + assert im.size == (20, 20) + assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) TiffImagePlugin.READ_LIBTIFF = False def test_multipage_nframes(self): # issue #862 TiffImagePlugin.READ_LIBTIFF = True - im = Image.open('Tests/images/multipage.tiff') - frames = im.n_frames - self.assertEqual(frames, 3) - for idx in range(frames): - im.seek(0) - # Should not raise ValueError: I/O operation on closed file + with Image.open("Tests/images/multipage.tiff") as im: + frames = im.n_frames + assert frames == 3 + for _ in range(frames): + im.seek(0) + # Should not raise ValueError: I/O operation on closed file + im.load() + + TiffImagePlugin.READ_LIBTIFF = False + + def test_multipage_seek_backwards(self): + TiffImagePlugin.READ_LIBTIFF = True + with Image.open("Tests/images/multipage.tiff") as im: + im.seek(1) im.load() + im.seek(0) + assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) + TiffImagePlugin.READ_LIBTIFF = False def test__next(self): TiffImagePlugin.READ_LIBTIFF = True - im = Image.open('Tests/images/hopper.tif') - self.assertFalse(im.tag.next) - im.load() - self.assertFalse(im.tag.next) + with Image.open("Tests/images/hopper.tif") as im: + assert not im.tag.next + im.load() + assert not im.tag.next def test_4bit(self): # Arrange @@ -423,13 +593,13 @@ def test_4bit(self): # Act TiffImagePlugin.READ_LIBTIFF = True - im = Image.open(test_file) - TiffImagePlugin.READ_LIBTIFF = False + with Image.open(test_file) as im: + TiffImagePlugin.READ_LIBTIFF = False - # Assert - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.mode, "L") - self.assert_image_similar(im, original, 7.3) + # Assert + assert im.size == (128, 128) + assert im.mode == "L" + assert_image_similar(im, original, 7.3) def test_gray_semibyte_per_pixel(self): test_files = ( @@ -440,7 +610,7 @@ def test_gray_semibyte_per_pixel(self): "Tests/images/tiff_gray_2_4_bpp/hopper2I.tif", "Tests/images/tiff_gray_2_4_bpp/hopper2R.tif", "Tests/images/tiff_gray_2_4_bpp/hopper2IR.tif", - ) + ), ), ( 7.3, # epsilon @@ -449,20 +619,20 @@ def test_gray_semibyte_per_pixel(self): "Tests/images/tiff_gray_2_4_bpp/hopper4I.tif", "Tests/images/tiff_gray_2_4_bpp/hopper4R.tif", "Tests/images/tiff_gray_2_4_bpp/hopper4IR.tif", - ) + ), ), ) original = hopper("L") for epsilon, group in test_files: - im = Image.open(group[0]) - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.mode, "L") - self.assert_image_similar(im, original, epsilon) + with Image.open(group[0]) as im: + assert im.size == (128, 128) + assert im.mode == "L" + assert_image_similar(im, original, epsilon) for file in group[1:]: - im2 = Image.open(file) - self.assertEqual(im2.size, (128, 128)) - self.assertEqual(im2.mode, "L") - self.assert_image_equal(im, im2) + with Image.open(file) as im2: + assert im2.size == (128, 128) + assert im2.mode == "L" + assert_image_equal(im, im2) def test_save_bytesio(self): # PR 1011 @@ -480,143 +650,420 @@ def save_bytesio(compression=None): pilim.save(buffer_io, format="tiff", compression=compression) buffer_io.seek(0) - pilim_load = Image.open(buffer_io) - self.assert_image_similar(pilim, pilim_load, 0) + assert_image_similar_tofile(pilim, buffer_io, 0) - # save_bytesio() - save_bytesio('raw') + save_bytesio() + save_bytesio("raw") save_bytesio("packbits") save_bytesio("tiff_lzw") TiffImagePlugin.WRITE_LIBTIFF = False TiffImagePlugin.READ_LIBTIFF = False - def test_crashing_metadata(self): + def test_save_ycbcr(self, tmp_path): + im = hopper("YCbCr") + outfile = str(tmp_path / "temp.tif") + im.save(outfile, compression="jpeg") + + with Image.open(outfile) as reloaded: + assert reloaded.tag_v2[530] == (1, 1) + assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) + + def test_crashing_metadata(self, tmp_path): # issue 1597 - im = Image.open('Tests/images/rdf.tif') - out = self.tempfile('temp.tif') + with Image.open("Tests/images/rdf.tif") as im: + out = str(tmp_path / "temp.tif") - TiffImagePlugin.WRITE_LIBTIFF = True - # this shouldn't crash - im.save(out, format='TIFF') + TiffImagePlugin.WRITE_LIBTIFF = True + # this shouldn't crash + im.save(out, format="TIFF") TiffImagePlugin.WRITE_LIBTIFF = False - def test_page_number_x_0(self): + def test_page_number_x_0(self, tmp_path): # Issue 973 # Test TIFF with tag 297 (Page Number) having value of 0 0. # The first number is the current page number. # The second is the total number of pages, zero means not available. - outfile = self.tempfile("temp.tif") + outfile = str(tmp_path / "temp.tif") # Created by printing a page in Chrome to PDF, then: # /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif # -dNOPAUSE /tmp/test.pdf -c quit infile = "Tests/images/total-pages-zero.tif" - im = Image.open(infile) - # Should not divide by zero - im.save(outfile) + with Image.open(infile) as im: + # Should not divide by zero + im.save(outfile) - def test_fd_duplication(self): + def test_fd_duplication(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/1651 - tmpfile = self.tempfile("temp.tif") - with open(tmpfile, 'wb') as f: - with open("Tests/images/g4-multi.tiff", 'rb') as src: + tmpfile = str(tmp_path / "temp.tif") + with open(tmpfile, "wb") as f: + with open("Tests/images/g4-multi.tiff", "rb") as src: f.write(src.read()) im = Image.open(tmpfile) - count = im.n_frames + im.n_frames im.close() - try: - os.remove(tmpfile) # Windows PermissionError here! - except: - self.fail("Should not get permission error here") + # Should not raise PermissionError. + os.remove(tmpfile) def test_read_icc(self): with Image.open("Tests/images/hopper.iccprofile.tif") as img: - icc = img.info.get('icc_profile') - self.assertNotEqual(icc, None) + icc = img.info.get("icc_profile") + assert icc is not None TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/hopper.iccprofile.tif") as img: - icc_libtiff = img.info.get('icc_profile') - self.assertNotEqual(icc_libtiff, None) + icc_libtiff = img.info.get("icc_profile") + assert icc_libtiff is not None TiffImagePlugin.READ_LIBTIFF = False - self.assertEqual(icc, icc_libtiff) + assert icc == icc_libtiff + + def test_write_icc(self, tmp_path): + def check_write(libtiff): + TiffImagePlugin.WRITE_LIBTIFF = libtiff + + with Image.open("Tests/images/hopper.iccprofile.tif") as img: + icc_profile = img.info["icc_profile"] + + out = str(tmp_path / "temp.tif") + img.save(out, icc_profile=icc_profile) + with Image.open(out) as reloaded: + assert icc_profile == reloaded.info["icc_profile"] + + libtiffs = [] + if Image.core.libtiff_support_custom_tags: + libtiffs.append(True) + libtiffs.append(False) + + for libtiff in libtiffs: + check_write(libtiff) def test_multipage_compression(self): - im = Image.open('Tests/images/compression.tif') + with Image.open("Tests/images/compression.tif") as im: - im.seek(0) - self.assertEqual(im._compression, 'tiff_ccitt') - self.assertEqual(im.size, (10, 10)) + im.seek(0) + assert im._compression == "tiff_ccitt" + assert im.size == (10, 10) - im.seek(1) - self.assertEqual(im._compression, 'packbits') - self.assertEqual(im.size, (10, 10)) - im.load() + im.seek(1) + assert im._compression == "packbits" + assert im.size == (10, 10) + im.load() - im.seek(0) - self.assertEqual(im._compression, 'tiff_ccitt') - self.assertEqual(im.size, (10, 10)) - im.load() + im.seek(0) + assert im._compression == "tiff_ccitt" + assert im.size == (10, 10) + im.load() - def test_save_tiff_with_jpegtables(self): + def test_save_tiff_with_jpegtables(self, tmp_path): # Arrange - outfile = self.tempfile("temp.tif") + outfile = str(tmp_path / "temp.tif") # Created with ImageMagick: convert hopper.jpg hopper_jpg.tif # Contains JPEGTables (347) tag infile = "Tests/images/hopper_jpg.tif" - im = Image.open(infile) + with Image.open(infile) as im: + # Act / Assert + # Should not raise UnicodeDecodeError or anything else + im.save(outfile) + + def test_16bit_RGB_tiff(self): + with Image.open("Tests/images/tiff_16bit_RGB.tiff") as im: + assert im.mode == "RGB" + assert im.size == (100, 40) + assert im.tile, [ + ( + "libtiff", + (0, 0, 100, 40), + 0, + ("RGB;16N", "tiff_adobe_deflate", False, 8), + ) + ] + im.load() - # Act / Assert - # Should not raise UnicodeDecodeError or anything else - im.save(outfile) + assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") def test_16bit_RGBa_tiff(self): - im = Image.open("Tests/images/tiff_16bit_RGBa.tiff") - - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (100, 40)) - self.assertEqual(im.tile, [('tiff_lzw', (0, 0, 100, 40), 0, ('RGBa;16N', 'tiff_lzw', False))]) - im.load() + with Image.open("Tests/images/tiff_16bit_RGBa.tiff") as im: + assert im.mode == "RGBA" + assert im.size == (100, 40) + assert im.tile, [ + ("libtiff", (0, 0, 100, 40), 0, ("RGBa;16N", "tiff_lzw", False, 38236)) + ] + im.load() - self.assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") + assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") + @skip_unless_feature("jpg") def test_gimp_tiff(self): # Read TIFF JPEG images from GIMP [@PIL168] - - codecs = dir(Image.core) - if "jpeg_decoder" not in codecs: - self.skipTest("jpeg support not available") - filename = "Tests/images/pil168.tif" - im = Image.open(filename) - - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (256, 256)) - self.assertEqual( - im.tile, [('jpeg', (0, 0, 256, 256), 0, ('RGB', 'jpeg', False))] - ) - im.load() + with Image.open(filename) as im: + assert im.mode == "RGB" + assert im.size == (256, 256) + assert im.tile == [ + ("libtiff", (0, 0, 256, 256), 0, ("RGB", "jpeg", False, 5122)) + ] + im.load() - self.assert_image_equal_tofile(im, "Tests/images/pil168.png") + assert_image_equal_tofile(im, "Tests/images/pil168.png") def test_sampleformat(self): # https://github.com/python-pillow/Pillow/issues/1466 - im = Image.open("Tests/images/copyleft.tiff") - self.assertEqual(im.mode, 'RGB') + with Image.open("Tests/images/copyleft.tiff") as im: + assert im.mode == "RGB" + + assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB") - self.assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode='RGB') + def test_sampleformat_write(self, tmp_path): + im = Image.new("F", (1, 1)) + out = str(tmp_path / "temp.tif") + TiffImagePlugin.WRITE_LIBTIFF = True + im.save(out) + TiffImagePlugin.WRITE_LIBTIFF = False + + with Image.open(out) as reloaded: + assert reloaded.mode == "F" + assert reloaded.getexif()[SAMPLEFORMAT] == 3 + + def test_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): - im = Image.open("Tests/images/hopper_lzw.tif") + with Image.open("Tests/images/hopper_lzw.tif") as im: + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "TIFF" + im2 = hopper() + assert_image_similar(im, im2, 5) + + def test_strip_cmyk_jpeg(self): + infile = "Tests/images/tiff_strip_cmyk_jpeg.tif" + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) + + def test_strip_cmyk_16l_jpeg(self): + infile = "Tests/images/tiff_strip_cmyk_16l_jpeg.tif" + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_strip_ycbcr_jpeg_2x2_sampling(self): + infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.2) + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_strip_ycbcr_jpeg_1x1_sampling(self): + infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif" + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) + + def test_tiled_cmyk_jpeg(self): + infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif" + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_tiled_ycbcr_jpeg_1x1_sampling(self): + infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif" + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_tiled_ycbcr_jpeg_2x2_sampling(self): + infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif" + with Image.open(infile) as im: + assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.5) + + def test_strip_planar_rgb(self): + # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ + # tiff_strip_raw.tif tiff_strip_planar_lzw.tiff + infile = "Tests/images/tiff_strip_planar_lzw.tiff" + with Image.open(infile) as im: + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + + def test_tiled_planar_rgb(self): + # gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \ + # tiff_tiled_raw.tif tiff_tiled_planar_lzw.tiff + infile = "Tests/images/tiff_tiled_planar_lzw.tiff" + with Image.open(infile) as im: + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + + def test_tiled_planar_16bit_RGB(self): + # gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \ + # tiff_16bit_RGB.tiff tiff_tiled_planar_16bit_RGB.tiff + with Image.open("Tests/images/tiff_tiled_planar_16bit_RGB.tiff") as im: + assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") + + def test_strip_planar_16bit_RGB(self): + # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ + # tiff_16bit_RGB.tiff tiff_strip_planar_16bit_RGB.tiff + with Image.open("Tests/images/tiff_strip_planar_16bit_RGB.tiff") as im: + assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") + + def test_tiled_planar_16bit_RGBa(self): + # gdal_translate -co TILED=yes \ + # -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \ + # tiff_16bit_RGBa.tiff tiff_tiled_planar_16bit_RGBa.tiff + with Image.open("Tests/images/tiff_tiled_planar_16bit_RGBa.tiff") as im: + assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") + + def test_strip_planar_16bit_RGBa(self): + # gdal_translate -co TILED=no \ + # -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \ + # tiff_16bit_RGBa.tiff tiff_strip_planar_16bit_RGBa.tiff + with Image.open("Tests/images/tiff_strip_planar_16bit_RGBa.tiff") as im: + assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") + + @pytest.mark.parametrize("compression", (None, "jpeg")) + def test_block_tile_tags(self, compression, tmp_path): + im = hopper() + out = str(tmp_path / "temp.tif") + + tags = { + TiffImagePlugin.TILEWIDTH: 256, + TiffImagePlugin.TILELENGTH: 256, + TiffImagePlugin.TILEOFFSETS: 256, + TiffImagePlugin.TILEBYTECOUNTS: 256, + } + im.save(out, exif=tags, compression=compression) + + with Image.open(out) as reloaded: + for tag in tags: + assert tag not in reloaded.getexif() + + def test_old_style_jpeg(self): + with Image.open("Tests/images/old-style-jpeg-compression.tif") as im: + assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") + + def test_open_missing_samplesperpixel(self): + with Image.open( + "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif" + ) as im: + assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") + + def test_no_rows_per_strip(self): + # This image does not have a RowsPerStrip TIFF tag + infile = "Tests/images/no_rows_per_strip.tif" + with Image.open(infile) as im: + im.load() + assert im.size == (950, 975) + + def test_orientation(self): + with Image.open("Tests/images/g4_orientation_1.tif") as base_im: + for i in range(2, 9): + with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: + im.load() + + assert_image_similar(base_im, im, 0.7) + + @pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") + def test_sampleformat_not_corrupted(self): + # Assert that a TIFF image with SampleFormat=UINT tag is not corrupted + # when saving to a new file. + # Pillow 6.0 fails with "OSError: cannot identify image file". + tiff = io.BytesIO( + base64.b64decode( + b"SUkqAAgAAAAPAP4ABAABAAAAAAAAAAABBAABAAAAAQAAAAEBBAABAAAAAQAA" + b"AAIBAwADAAAAwgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAA" + b"4AAAABUBAwABAAAAAwAAABYBBAABAAAAAQAAABcBBAABAAAACwAAABoBBQAB" + b"AAAAyAAAABsBBQABAAAA0AAAABwBAwABAAAAAQAAACgBAwABAAAAAQAAAFMB" + b"AwADAAAA2AAAAAAAAAAIAAgACAABAAAAAQAAAAEAAAABAAAAAQABAAEAAAB4" + b"nGNgYAAAAAMAAQ==" + ) + ) + out = io.BytesIO() + with Image.open(tiff) as im: + im.save(out, format="tiff") + out.seek(0) + with Image.open(out) as im: + im.load() + + def test_realloc_overflow(self): + TiffImagePlugin.READ_LIBTIFF = True + with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im: + with pytest.raises(OSError) as e: + im.load() + + # Assert that the error code is IMAGING_CODEC_MEMORY + assert str(e.value) == "-9" + TiffImagePlugin.READ_LIBTIFF = False - self.assertEqual(im.mode, 'RGB') - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "TIFF") - im2 = hopper() - self.assert_image_similar(im, im2, 5) + @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) + def test_save_multistrip(self, compression, tmp_path): + im = hopper("RGB").resize((256, 256)) + out = str(tmp_path / "temp.tif") + im.save(out, compression=compression) + with Image.open(out) as im: + # Assert that there are multiple strips + assert len(im.tag_v2[STRIPOFFSETS]) > 1 -if __name__ == '__main__': - unittest.main() + @pytest.mark.parametrize("argument", (True, False)) + def test_save_single_strip(self, argument, tmp_path): + im = hopper("RGB").resize((256, 256)) + out = str(tmp_path / "temp.tif") + + if not argument: + TiffImagePlugin.STRIP_SIZE = 2**18 + try: + arguments = {"compression": "tiff_adobe_deflate"} + if argument: + arguments["strip_size"] = 2**18 + im.save(out, **arguments) + + with Image.open(out) as im: + assert len(im.tag_v2[STRIPOFFSETS]) == 1 + finally: + TiffImagePlugin.STRIP_SIZE = 65536 + + @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) + def test_save_zero(self, compression, tmp_path): + im = Image.new("RGB", (0, 0)) + out = str(tmp_path / "temp.tif") + with pytest.raises(SystemError): + im.save(out, compression=compression) diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index c402673d826..03137c8b603 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -1,52 +1,44 @@ -from helper import unittest +from io import BytesIO from PIL import Image -from test_file_libtiff import LibTiffTestCase +from .test_file_libtiff import LibTiffTestCase class TestFileLibTiffSmall(LibTiffTestCase): - """ The small lena image was failing on open in the libtiff - decoder because the file pointer was set to the wrong place - by a spurious seek. It wasn't failing with the byteio method. + """The small lena image was failing on open in the libtiff + decoder because the file pointer was set to the wrong place + by a spurious seek. It wasn't failing with the byteio method. - It was fixed by forcing an lseek to the beginning of the - file just before reading in libtiff. These tests remain - to ensure that it stays fixed. """ + It was fixed by forcing an lseek to the beginning of the + file just before reading in libtiff. These tests remain + to ensure that it stays fixed.""" - def test_g4_hopper_file(self): + def test_g4_hopper_file(self, tmp_path): """Testing the open file load path""" test_file = "Tests/images/hopper_g4.tif" - with open(test_file, 'rb') as f: - im = Image.open(f) + with open(test_file, "rb") as f: + with Image.open(f) as im: + assert im.size == (128, 128) + self._assert_noerr(tmp_path, im) - self.assertEqual(im.size, (128, 128)) - self._assert_noerr(im) - - def test_g4_hopper_bytesio(self): + def test_g4_hopper_bytesio(self, tmp_path): """Testing the bytesio loading code path""" - from io import BytesIO test_file = "Tests/images/hopper_g4.tif" s = BytesIO() - with open(test_file, 'rb') as f: + with open(test_file, "rb") as f: s.write(f.read()) s.seek(0) - im = Image.open(s) - - self.assertEqual(im.size, (128, 128)) - self._assert_noerr(im) + with Image.open(s) as im: + assert im.size == (128, 128) + self._assert_noerr(tmp_path, im) - def test_g4_hopper(self): + def test_g4_hopper(self, tmp_path): """The 128x128 lena image failed for some reason.""" test_file = "Tests/images/hopper_g4.tif" - im = Image.open(test_file) - - self.assertEqual(im.size, (128, 128)) - self._assert_noerr(im) - - -if __name__ == '__main__': - unittest.main() + with Image.open(test_file) as im: + assert im.size == (128, 128) + self._assert_noerr(tmp_path, im) diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py index 491d8ea03b2..41f22cf0c7d 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -1,34 +1,30 @@ -from helper import unittest, PillowTestCase +import pytest from PIL import Image, McIdasImagePlugin +from .helper import assert_image_equal_tofile -class TestFileMcIdas(PillowTestCase): - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - McIdasImagePlugin.McIdasImageFile, invalid_file) + with pytest.raises(SyntaxError): + McIdasImagePlugin.McIdasImageFile(invalid_file) - def test_valid_file(self): - # Arrange - # https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8 - # https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/ - test_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara" - saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png" - # Act - im = Image.open(test_file) +def test_valid_file(): + # Arrange + # https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8 + # https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/ + test_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara" + saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png" + + # Act + with Image.open(test_file) as im: im.load() # Assert - self.assertEqual(im.format, "MCIDAS") - self.assertEqual(im.mode, "I") - self.assertEqual(im.size, (1800, 400)) - im2 = Image.open(saved_file) - self.assert_image_equal(im, im2) - - -if __name__ == '__main__': - unittest.main() + assert im.format == "MCIDAS" + assert im.mode == "I" + assert im.size == (1800, 400) + assert_image_equal_tofile(im, saved_file) diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index f4059f9c903..464d138e2af 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -1,70 +1,63 @@ -from helper import unittest, PillowTestCase, hopper +import pytest -from PIL import Image, ImagePalette, features +from PIL import Image, ImagePalette -try: - from PIL import MicImagePlugin -except ImportError: - olefile_installed = False -else: - olefile_installed = True +from .helper import assert_image_similar, hopper, skip_unless_feature +MicImagePlugin = pytest.importorskip( + "PIL.MicImagePlugin", reason="olefile not installed" +) +pytestmark = skip_unless_feature("libtiff") TEST_FILE = "Tests/images/hopper.mic" -@unittest.skipUnless(olefile_installed, "olefile package not installed") -@unittest.skipUnless(features.check('libtiff'), "libtiff not installed") -class TestFileMic(PillowTestCase): - - def test_sanity(self): - im = Image.open(TEST_FILE) +def test_sanity(): + with Image.open(TEST_FILE) as im: im.load() - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "MIC") + assert im.mode == "RGBA" + assert im.size == (128, 128) + assert im.format == "MIC" # Adjust for the gamma of 2.2 encoded into the file - lut = ImagePalette.make_gamma_lut(1/2.2) - im = Image.merge('RGBA', [chan.point(lut) for chan in im.split()]) + lut = ImagePalette.make_gamma_lut(1 / 2.2) + im = Image.merge("RGBA", [chan.point(lut) for chan in im.split()]) im2 = hopper("RGBA") - self.assert_image_similar(im, im2, 10) + assert_image_similar(im, im2, 10) - def test_n_frames(self): - im = Image.open(TEST_FILE) - self.assertEqual(im.n_frames, 1) +def test_n_frames(): + with Image.open(TEST_FILE) as im: + assert im.n_frames == 1 - def test_is_animated(self): - im = Image.open(TEST_FILE) - self.assertFalse(im.is_animated) +def test_is_animated(): + with Image.open(TEST_FILE) as im: + assert not im.is_animated - def test_tell(self): - im = Image.open(TEST_FILE) - self.assertEqual(im.tell(), 0) +def test_tell(): + with Image.open(TEST_FILE) as im: + assert im.tell() == 0 - def test_seek(self): - im = Image.open(TEST_FILE) +def test_seek(): + with Image.open(TEST_FILE) as im: im.seek(0) - self.assertEqual(im.tell(), 0) - - self.assertRaises(EOFError, im.seek, 99) - self.assertEqual(im.tell(), 0) + assert im.tell() == 0 - def test_invalid_file(self): - # Test an invalid OLE file - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - MicImagePlugin.MicImageFile, invalid_file) + with pytest.raises(EOFError): + im.seek(99) + assert im.tell() == 0 - # Test a valid OLE file, but not a MIC file - ole_file = "Tests/images/test-ole-file.doc" - self.assertRaises(SyntaxError, - MicImagePlugin.MicImageFile, ole_file) +def test_invalid_file(): + # Test an invalid OLE file + invalid_file = "Tests/images/flower.jpg" + with pytest.raises(SyntaxError): + MicImagePlugin.MicImageFile(invalid_file) -if __name__ == '__main__': - unittest.main() + # Test a valid OLE file, but not a MIC file + ole_file = "Tests/images/test-ole-file.doc" + with pytest.raises(SyntaxError): + MicImagePlugin.MicImageFile(ole_file) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 70bb9b10544..d94bdaa96c9 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -1,142 +1,277 @@ -from helper import unittest, PillowTestCase +import warnings from io import BytesIO + +import pytest + from PIL import Image +from .helper import ( + assert_image_equal, + assert_image_similar, + is_pypy, + skip_unless_feature, +) test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] +pytestmark = skip_unless_feature("jpg") + + +def roundtrip(im, **options): + out = BytesIO() + im.save(out, "MPO", **options) + test_bytes = out.tell() + out.seek(0) + im = Image.open(out) + im.bytes = test_bytes # for testing only + return im + + +@pytest.mark.parametrize("test_file", test_files) +def test_sanity(test_file): + with Image.open(test_file) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (640, 480) + assert im.format == "MPO" + -class TestFileMpo(PillowTestCase): +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): + im = Image.open(test_files[0]) + im.load() - def setUp(self): - codecs = dir(Image.core) - if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: - self.skipTest("jpeg support not available") + pytest.warns(ResourceWarning, open) - def frame_roundtrip(self, im, **options): - # Note that for now, there is no MPO saving functionality - out = BytesIO() - im.save(out, "MPO", **options) - test_bytes = out.tell() - out.seek(0) - im = Image.open(out) - im.bytes = test_bytes # for testing only - return im - def test_sanity(self): - for test_file in test_files: - im = Image.open(test_file) +def test_closed_file(): + with warnings.catch_warnings(): + im = Image.open(test_files[0]) + im.load() + im.close() + + +def test_seek_after_close(): + im = Image.open(test_files[0]) + im.close() + + with pytest.raises(ValueError): + im.seek(1) + + +def test_context_manager(): + with warnings.catch_warnings(): + with Image.open(test_files[0]) as im: im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (640, 480)) - self.assertEqual(im.format, "MPO") - - def test_app(self): - for test_file in test_files: - # Test APP/COM reader (@PIL135) - im = Image.open(test_file) - self.assertEqual(im.applist[0][0], 'APP1') - self.assertEqual(im.applist[1][0], 'APP2') - self.assertEqual(im.applist[1][1][:16], - b'MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00') - self.assertEqual(len(im.applist), 2) - - def test_exif(self): - for test_file in test_files: - im = Image.open(test_file) - info = im._getexif() - self.assertEqual(info[272], 'Nintendo 3DS') - self.assertEqual(info[296], 2) - self.assertEqual(info[34665], 188) - - def test_mp(self): - for test_file in test_files: - im = Image.open(test_file) - mpinfo = im._getmp() - self.assertEqual(mpinfo[45056], b'0100') - self.assertEqual(mpinfo[45057], 2) - - def test_mp_attribute(self): - for test_file in test_files: - im = Image.open(test_file) - mpinfo = im._getmp() - frameNumber = 0 - for mpentry in mpinfo[45058]: - mpattr = mpentry['Attribute'] - if frameNumber: - self.assertFalse(mpattr['RepresentativeImageFlag']) - else: - self.assertTrue(mpattr['RepresentativeImageFlag']) - self.assertFalse(mpattr['DependentParentImageFlag']) - self.assertFalse(mpattr['DependentChildImageFlag']) - self.assertEqual(mpattr['ImageDataFormat'], 'JPEG') - self.assertEqual(mpattr['MPType'], - 'Multi-Frame Image: (Disparity)') - self.assertEqual(mpattr['Reserved'], 0) - frameNumber += 1 - - def test_seek(self): - for test_file in test_files: - im = Image.open(test_file) - self.assertEqual(im.tell(), 0) - # prior to first image raises an error, both blatant and borderline - self.assertRaises(EOFError, im.seek, -1) - self.assertRaises(EOFError, im.seek, -523) - # after the final image raises an error, - # both blatant and borderline - self.assertRaises(EOFError, im.seek, 2) - self.assertRaises(EOFError, im.seek, 523) - # bad calls shouldn't change the frame - self.assertEqual(im.tell(), 0) - # this one will work + + +@pytest.mark.parametrize("test_file", test_files) +def test_app(test_file): + # Test APP/COM reader (@PIL135) + with Image.open(test_file) as im: + assert im.applist[0][0] == "APP1" + assert im.applist[1][0] == "APP2" + assert ( + im.applist[1][1][:16] == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00" + ) + assert len(im.applist) == 2 + + +@pytest.mark.parametrize("test_file", test_files) +def test_exif(test_file): + with Image.open(test_file) as im: + info = im._getexif() + assert info[272] == "Nintendo 3DS" + assert info[296] == 2 + assert info[34665] == 188 + + +def test_frame_size(): + # This image has been hexedited to contain a different size + # in the EXIF data of the second frame + with Image.open("Tests/images/sugarshack_frame_size.mpo") as im: + assert im.size == (640, 480) + + im.seek(1) + assert im.size == (680, 480) + + im.seek(0) + assert im.size == (640, 480) + + +def test_ignore_frame_size(): + # Ignore the different size of the second frame + # since this is not a "Large Thumbnail" image + with Image.open("Tests/images/ignore_frame_size.mpo") as im: + assert im.size == (64, 64) + + im.seek(1) + assert ( + im.mpinfo[0xB002][1]["Attribute"]["MPType"] + == "Multi-Frame Image: (Disparity)" + ) + assert im.size == (64, 64) + + +def test_parallax(): + # Nintendo + with Image.open("Tests/images/sugarshack.mpo") as im: + exif = im.getexif() + assert exif.get_ifd(0x927C)[0x1101]["Parallax"] == -44.798187255859375 + + # Fujifilm + with Image.open("Tests/images/fujifilm.mpo") as im: + im.seek(1) + exif = im.getexif() + assert exif.get_ifd(0x927C)[0xB211] == -3.125 + + +def test_reload_exif_after_seek(): + with Image.open("Tests/images/sugarshack.mpo") as im: + exif = im.getexif() + del exif[296] + + im.seek(1) + assert 296 in exif + + +@pytest.mark.parametrize("test_file", test_files) +def test_mp(test_file): + with Image.open(test_file) as im: + mpinfo = im._getmp() + assert mpinfo[45056] == b"0100" + assert mpinfo[45057] == 2 + + +def test_mp_offset(): + # This image has been manually hexedited to have an IFD offset of 10 + # in APP2 data, in contrast to normal 8 + with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im: + mpinfo = im._getmp() + assert mpinfo[45056] == b"0100" + assert mpinfo[45057] == 2 + + +def test_mp_no_data(): + # This image has been manually hexedited to have the second frame + # beyond the end of the file + with Image.open("Tests/images/sugarshack_no_data.mpo") as im: + with pytest.raises(ValueError): im.seek(1) - self.assertEqual(im.tell(), 1) - # and this one, too - im.seek(0) - self.assertEqual(im.tell(), 0) - def test_n_frames(self): - im = Image.open("Tests/images/sugarshack.mpo") - self.assertEqual(im.n_frames, 2) - self.assertTrue(im.is_animated) - def test_eoferror(self): - im = Image.open("Tests/images/sugarshack.mpo") +@pytest.mark.parametrize("test_file", test_files) +def test_mp_attribute(test_file): + with Image.open(test_file) as im: + mpinfo = im._getmp() + frame_number = 0 + for mpentry in mpinfo[0xB002]: + mpattr = mpentry["Attribute"] + if frame_number: + assert not mpattr["RepresentativeImageFlag"] + else: + assert mpattr["RepresentativeImageFlag"] + assert not mpattr["DependentParentImageFlag"] + assert not mpattr["DependentChildImageFlag"] + assert mpattr["ImageDataFormat"] == "JPEG" + assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)" + assert mpattr["Reserved"] == 0 + frame_number += 1 + + +@pytest.mark.parametrize("test_file", test_files) +def test_seek(test_file): + with Image.open(test_file) as im: + assert im.tell() == 0 + # prior to first image raises an error, both blatant and borderline + with pytest.raises(EOFError): + im.seek(-1) + with pytest.raises(EOFError): + im.seek(-523) + # after the final image raises an error, + # both blatant and borderline + with pytest.raises(EOFError): + im.seek(2) + with pytest.raises(EOFError): + im.seek(523) + # bad calls shouldn't change the frame + assert im.tell() == 0 + # this one will work + im.seek(1) + assert im.tell() == 1 + # and this one, too + im.seek(0) + assert im.tell() == 0 + + +def test_n_frames(): + with Image.open("Tests/images/sugarshack.mpo") as im: + assert im.n_frames == 2 + assert im.is_animated + + +def test_eoferror(): + with Image.open("Tests/images/sugarshack.mpo") as im: n_frames = im.n_frames # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames # Test that seeking to the last frame does not raise an error - im.seek(n_frames-1) + im.seek(n_frames - 1) + + +@pytest.mark.parametrize("test_file", test_files) +def test_image_grab(test_file): + with Image.open(test_file) as im: + assert im.tell() == 0 + im0 = im.tobytes() + im.seek(1) + assert im.tell() == 1 + im1 = im.tobytes() + im.seek(0) + assert im.tell() == 0 + im02 = im.tobytes() + assert im0 == im02 + assert im0 != im1 + + +@pytest.mark.parametrize("test_file", test_files) +def test_save(test_file): + with Image.open(test_file) as im: + assert im.tell() == 0 + jpg0 = roundtrip(im) + assert_image_similar(im, jpg0, 30) + im.seek(1) + assert im.tell() == 1 + jpg1 = roundtrip(im) + assert_image_similar(im, jpg1, 30) + + +def test_save_all(): + for test_file in test_files: + with Image.open(test_file) as im: + im_reloaded = roundtrip(im, save_all=True) - def test_image_grab(self): - for test_file in test_files: - im = Image.open(test_file) - self.assertEqual(im.tell(), 0) - im0 = im.tobytes() - im.seek(1) - self.assertEqual(im.tell(), 1) - im1 = im.tobytes() im.seek(0) - self.assertEqual(im.tell(), 0) - im02 = im.tobytes() - self.assertEqual(im0, im02) - self.assertNotEqual(im0, im1) - - def test_save(self): - # Note that only individual frames can be saved at present - for test_file in test_files: - im = Image.open(test_file) - self.assertEqual(im.tell(), 0) - jpg0 = self.frame_roundtrip(im) - self.assert_image_similar(im, jpg0, 30) + assert_image_similar(im, im_reloaded, 30) + im.seek(1) - self.assertEqual(im.tell(), 1) - jpg1 = self.frame_roundtrip(im) - self.assert_image_similar(im, jpg1, 30) + im_reloaded.seek(1) + assert_image_similar(im, im_reloaded, 30) + + im = Image.new("RGB", (1, 1)) + im2 = Image.new("RGB", (1, 1), "#f00") + im_reloaded = roundtrip(im, save_all=True, append_images=[im2]) + + assert_image_equal(im, im_reloaded) + im_reloaded.seek(1) + assert_image_similar(im2, im_reloaded, 1) -if __name__ == '__main__': - unittest.main() + # Test that a single frame image will not be saved as an MPO + jpg = roundtrip(im, save_all=True) + assert "mp" not in jpg.info diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index 4aac880922c..50d7c590b7a 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,84 +1,90 @@ -from helper import unittest, PillowTestCase, hopper +import os + +import pytest from PIL import Image, MspImagePlugin -import os +from .helper import assert_image_equal, assert_image_equal_tofile, hopper TEST_FILE = "Tests/images/hopper.msp" EXTRA_DIR = "Tests/images/picins" YA_EXTRA_DIR = "Tests/images/msp" -class TestFileMsp(PillowTestCase): +def test_sanity(tmp_path): + test_file = str(tmp_path / "temp.msp") - def test_sanity(self): - file = self.tempfile("temp.msp") + hopper("1").save(test_file) - hopper("1").save(file) - - im = Image.open(file) + with Image.open(test_file) as im: im.load() - self.assertEqual(im.mode, "1") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "MSP") + assert im.mode == "1" + assert im.size == (128, 128) + assert im.format == "MSP" + - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - MspImagePlugin.MspImageFile, invalid_file) + with pytest.raises(SyntaxError): + MspImagePlugin.MspImageFile(invalid_file) - def test_bad_checksum(self): - # Arrange - # This was created by forcing Pillow to save with checksum=0 - bad_checksum = "Tests/images/hopper_bad_checksum.msp" - # Act / Assert - self.assertRaises(SyntaxError, - MspImagePlugin.MspImageFile, bad_checksum) +def test_bad_checksum(): + # Arrange + # This was created by forcing Pillow to save with checksum=0 + bad_checksum = "Tests/images/hopper_bad_checksum.msp" - def test_open_windows_v1(self): - # Arrange - # Act - im = Image.open(TEST_FILE) + # Act / Assert + with pytest.raises(SyntaxError): + MspImagePlugin.MspImageFile(bad_checksum) + + +def test_open_windows_v1(): + # Arrange + # Act + with Image.open(TEST_FILE) as im: # Assert - self.assert_image_equal(im, hopper("1")) - self.assertIsInstance(im, MspImagePlugin.MspImageFile) - - def _assert_file_image_equal(self, source_path, target_path): - with Image.open(source_path) as im: - target = Image.open(target_path) - self.assert_image_equal(im, target) - - @unittest.skipIf(not os.path.exists(EXTRA_DIR), - "Extra image files not installed") - def test_open_windows_v2(self): - - files = (os.path.join(EXTRA_DIR, f) for f in os.listdir(EXTRA_DIR) - if os.path.splitext(f)[1] == '.msp') - for path in files: - self._assert_file_image_equal(path, - path.replace('.msp', '.png')) - - @unittest.skipIf(not os.path.exists(YA_EXTRA_DIR), - "Even More Extra image files not installed") - def test_msp_v2(self): - for f in os.listdir(YA_EXTRA_DIR): - if '.MSP' not in f: - continue - path = os.path.join(YA_EXTRA_DIR, f) - self._assert_file_image_equal(path, - path.replace('.MSP', '.png')) - - def test_cannot_save_wrong_mode(self): - # Arrange - im = hopper() - filename = self.tempfile("temp.msp") - - # Act/Assert - self.assertRaises(IOError, im.save, filename) - - -if __name__ == '__main__': - unittest.main() + assert_image_equal(im, hopper("1")) + assert isinstance(im, MspImagePlugin.MspImageFile) + + +def _assert_file_image_equal(source_path, target_path): + with Image.open(source_path) as im: + assert_image_equal_tofile(im, target_path) + + +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +def test_open_windows_v2(): + + files = ( + os.path.join(EXTRA_DIR, f) + for f in os.listdir(EXTRA_DIR) + if os.path.splitext(f)[1] == ".msp" + ) + for path in files: + _assert_file_image_equal(path, path.replace(".msp", ".png")) + + +@pytest.mark.skipif( + not os.path.exists(YA_EXTRA_DIR), reason="Even More Extra image files not installed" +) +def test_msp_v2(): + for f in os.listdir(YA_EXTRA_DIR): + if ".MSP" not in f: + continue + path = os.path.join(YA_EXTRA_DIR, f) + _assert_file_image_equal(path, path.replace(".MSP", ".png")) + + +def test_cannot_save_wrong_mode(tmp_path): + # Arrange + im = hopper() + filename = str(tmp_path / "temp.msp") + + # Act/Assert + with pytest.raises(OSError): + im.save(filename) diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index b97a9b19eb3..be7c8d0c86a 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -1,58 +1,69 @@ -from helper import unittest, PillowTestCase, hopper, imagemagick_available - import os.path +import subprocess + +import pytest + +from PIL import Image + +from .helper import assert_image_equal, hopper, magick_command + + +def helper_save_as_palm(tmp_path, mode): + # Arrange + im = hopper(mode) + outfile = str(tmp_path / ("temp_" + mode + ".palm")) + # Act + im.save(outfile) -class TestFilePalm(PillowTestCase): - _roundtrip = imagemagick_available() + # Assert + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 - def helper_save_as_palm(self, mode): - # Arrange - im = hopper(mode) - outfile = self.tempfile("temp_" + mode + ".palm") - # Act - im.save(outfile) +def open_with_magick(magick, tmp_path, f): + outfile = str(tmp_path / "temp.png") + rc = subprocess.call( + magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + if rc: + raise OSError + return Image.open(outfile) - # Assert - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) - def roundtrip(self, mode): - if not self._roundtrip: - return +def roundtrip(tmp_path, mode): + magick = magick_command() + if not magick: + return - im = hopper(mode) - outfile = self.tempfile("temp.palm") + im = hopper(mode) + outfile = str(tmp_path / "temp.palm") - im.save(outfile) - converted = self.open_withImagemagick(outfile) - self.assert_image_equal(converted, im) + im.save(outfile) + converted = open_with_magick(magick, tmp_path, outfile) + assert_image_equal(converted, im) - def test_monochrome(self): - # Arrange - mode = "1" - # Act / Assert - self.helper_save_as_palm(mode) - self.roundtrip(mode) +def test_monochrome(tmp_path): + # Arrange + mode = "1" - def test_p_mode(self): - # Arrange - mode = "P" + # Act / Assert + helper_save_as_palm(tmp_path, mode) + roundtrip(tmp_path, mode) - # Act / Assert - self.helper_save_as_palm(mode) - self.skipKnownBadTest("Palm P image is wrong") - self.roundtrip(mode) - def test_rgb_ioerror(self): - # Arrange - mode = "RGB" +@pytest.mark.xfail(reason="Palm P image is wrong") +def test_p_mode(tmp_path): + # Arrange + mode = "P" - # Act / Assert - self.assertRaises(IOError, self.helper_save_as_palm, mode) + # Act / Assert + helper_save_as_palm(tmp_path, mode) + roundtrip(tmp_path, mode) -if __name__ == '__main__': - unittest.main() +@pytest.mark.parametrize("mode", ("L", "RGB")) +def test_oserror(tmp_path, mode): + with pytest.raises(OSError): + helper_save_as_palm(tmp_path, mode) diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index 06fd3304352..dc45a48c1cb 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -1,22 +1,15 @@ -from helper import unittest, PillowTestCase from PIL import Image -class TestFilePcd(PillowTestCase): - - def test_load_raw(self): - im = Image.open('Tests/images/hopper.pcd') +def test_load_raw(): + with Image.open("Tests/images/hopper.pcd") as im: im.load() # should not segfault. - # Note that this image was created with a resized hopper - # image, which was then converted to pcd with imagemagick - # and the colors are wonky in Pillow. It's unclear if this - # is a pillow or a convert issue, as other images not generated - # from convert look find on pillow and not imagemagick. - - # target = hopper().resize((768,512)) - # self.assert_image_similar(im, target, 10) - + # Note that this image was created with a resized hopper + # image, which was then converted to pcd with imagemagick + # and the colors are wonky in Pillow. It's unclear if this + # is a pillow or a convert issue, as other images not generated + # from convert look find on pillow and not imagemagick. -if __name__ == '__main__': - unittest.main() + # target = hopper().resize((768,512)) + # assert_image_similar(im, target, 10) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 415827e49e9..485adf7853e 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,134 +1,156 @@ -from helper import unittest, PillowTestCase, hopper +import pytest from PIL import Image, ImageFile, PcxImagePlugin +from .helper import assert_image_equal, hopper -class TestFilePcx(PillowTestCase): - def _roundtrip(self, im): - f = self.tempfile("temp.pcx") +def _roundtrip(tmp_path, im): + f = str(tmp_path / "temp.pcx") + im.save(f) + with Image.open(f) as im2: + assert im2.mode == im.mode + assert im2.size == im.size + assert im2.format == "PCX" + assert im2.get_format_mimetype() == "image/x-pcx" + assert_image_equal(im2, im) + + +def test_sanity(tmp_path): + for mode in ("1", "L", "P", "RGB"): + _roundtrip(tmp_path, hopper(mode)) + + # Test a palette with less than 256 colors + im = Image.new("P", (1, 1)) + im.putpalette((255, 0, 0)) + _roundtrip(tmp_path, im) + + # Test an unsupported mode + f = str(tmp_path / "temp.pcx") + im = hopper("RGBA") + with pytest.raises(ValueError): im.save(f) - im2 = Image.open(f) - self.assertEqual(im2.mode, im.mode) - self.assertEqual(im2.size, im.size) - self.assertEqual(im2.format, "PCX") - self.assert_image_equal(im2, im) - def test_sanity(self): - for mode in ('1', 'L', 'P', 'RGB'): - self._roundtrip(hopper(mode)) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - # Test an unsupported mode - f = self.tempfile("temp.pcx") - im = hopper("RGBA") - self.assertRaises(ValueError, im.save, f) + with pytest.raises(SyntaxError): + PcxImagePlugin.PcxImageFile(invalid_file) - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - PcxImagePlugin.PcxImageFile, invalid_file) +@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) +def test_odd(tmp_path, mode): + # See issue #523, odd sized images should have a stride that's even. + # Not that ImageMagick or GIMP write PCX that way. + # We were not handling properly. + # larger, odd sized images are better here to ensure that + # we handle interrupted scan lines properly. + _roundtrip(tmp_path, hopper(mode).resize((511, 511))) - def test_odd(self): - # see issue #523, odd sized images should have a stride that's even. - # not that imagemagick or gimp write pcx that way. - # we were not handling properly. - for mode in ('1', 'L', 'P', 'RGB'): - # larger, odd sized images are better here to ensure that - # we handle interrupted scan lines properly. - self._roundtrip(hopper(mode).resize((511, 511))) - def test_pil184(self): - # Check reading of files where xmin/xmax is not zero. +def test_odd_read(): + # Reading an image with an odd stride, making it malformed + with Image.open("Tests/images/odd_stride.pcx") as im: + im.load() - test_file = "Tests/images/pil184.pcx" - im = Image.open(test_file) + assert im.size == (371, 150) - self.assertEqual(im.size, (447, 144)) - self.assertEqual(im.tile[0][1], (0, 0, 447, 144)) + +def test_pil184(): + # Check reading of files where xmin/xmax is not zero. + + test_file = "Tests/images/pil184.pcx" + with Image.open(test_file) as im: + assert im.size == (447, 144) + assert im.tile[0][1] == (0, 0, 447, 144) # Make sure all pixels are either 0 or 255. - self.assertEqual(im.histogram()[0] + im.histogram()[255], 447*144) - - def test_1px_width(self): - im = Image.new('L', (1, 256)) - px = im.load() - for y in range(256): - px[0, y] = y - self._roundtrip(im) - - def test_large_count(self): - im = Image.new('L', (256, 1)) - px = im.load() + assert im.histogram()[0] + im.histogram()[255] == 447 * 144 + + +def test_1px_width(tmp_path): + im = Image.new("L", (1, 256)) + px = im.load() + for y in range(256): + px[0, y] = y + _roundtrip(tmp_path, im) + + +def test_large_count(tmp_path): + im = Image.new("L", (256, 1)) + px = im.load() + for x in range(256): + px[x, 0] = x // 67 * 67 + _roundtrip(tmp_path, im) + + +def _test_buffer_overflow(tmp_path, im, size=1024): + _last = ImageFile.MAXBLOCK + ImageFile.MAXBLOCK = size + try: + _roundtrip(tmp_path, im) + finally: + ImageFile.MAXBLOCK = _last + + +def test_break_in_count_overflow(tmp_path): + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(4): + for x in range(256): + px[x, y] = x % 128 + _test_buffer_overflow(tmp_path, im) + + +def test_break_one_in_loop(tmp_path): + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(5): + for x in range(256): + px[x, y] = x % 128 + _test_buffer_overflow(tmp_path, im) + + +def test_break_many_in_loop(tmp_path): + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(4): + for x in range(256): + px[x, y] = x % 128 + for x in range(8): + px[x, 4] = 16 + _test_buffer_overflow(tmp_path, im) + + +def test_break_one_at_end(tmp_path): + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(5): + for x in range(256): + px[x, y] = x % 128 + px[0, 3] = 128 + 64 + _test_buffer_overflow(tmp_path, im) + + +def test_break_many_at_end(tmp_path): + im = Image.new("L", (256, 5)) + px = im.load() + for y in range(5): for x in range(256): - px[x, 0] = x // 67 * 67 - self._roundtrip(im) - - def _test_buffer_overflow(self, im, size=1024): - _last = ImageFile.MAXBLOCK - ImageFile.MAXBLOCK = size - try: - self._roundtrip(im) - finally: - ImageFile.MAXBLOCK = _last - - def test_break_in_count_overflow(self): - im = Image.new('L', (256, 5)) - px = im.load() - for y in range(4): - for x in range(256): - px[x, y] = x % 128 - self._test_buffer_overflow(im) - - def test_break_one_in_loop(self): - im = Image.new('L', (256, 5)) - px = im.load() - for y in range(5): - for x in range(256): - px[x, y] = x % 128 - self._test_buffer_overflow(im) - - def test_break_many_in_loop(self): - im = Image.new('L', (256, 5)) - px = im.load() - for y in range(4): - for x in range(256): - px[x, y] = x % 128 - for x in range(8): - px[x, 4] = 16 - self._test_buffer_overflow(im) - - def test_break_one_at_end(self): - im = Image.new('L', (256, 5)) - px = im.load() - for y in range(5): - for x in range(256): - px[x, y] = x % 128 - px[0, 3] = 128 + 64 - self._test_buffer_overflow(im) - - def test_break_many_at_end(self): - im = Image.new('L', (256, 5)) - px = im.load() - for y in range(5): - for x in range(256): - px[x, y] = x % 128 - for x in range(4): - px[x * 2, 3] = 128 + 64 - px[x + 256 - 4, 3] = 0 - self._test_buffer_overflow(im) - - def test_break_padding(self): - im = Image.new('L', (257, 5)) - px = im.load() - for y in range(5): - for x in range(257): - px[x, y] = x % 128 - for x in range(5): - px[x, 3] = 0 - self._test_buffer_overflow(im) - - -if __name__ == '__main__': - unittest.main() + px[x, y] = x % 128 + for x in range(4): + px[x * 2, 3] = 128 + 64 + px[x + 256 - 4, 3] = 0 + _test_buffer_overflow(tmp_path, im) + + +def test_break_padding(tmp_path): + im = Image.new("L", (257, 5)) + px = im.load() + for y in range(5): + for x in range(257): + px[x, y] = x % 128 + for x in range(5): + px[x, 3] = 0 + _test_buffer_overflow(tmp_path, im) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index ee02d0694fa..9667b6a4aad 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -1,102 +1,296 @@ -from helper import unittest, PillowTestCase, hopper -from PIL import Image +import io +import os import os.path +import tempfile +import time +import pytest -class TestFilePdf(PillowTestCase): +from PIL import Image, PdfParser, features - def helper_save_as_pdf(self, mode, save_all=False): - # Arrange - im = hopper(mode) - outfile = self.tempfile("temp_" + mode + ".pdf") +from .helper import hopper, mark_if_feature_version - # Act - if save_all: - im.save(outfile, save_all=True) + +def helper_save_as_pdf(tmp_path, mode, **kwargs): + # Arrange + im = hopper(mode) + outfile = str(tmp_path / ("temp_" + mode + ".pdf")) + + # Act + im.save(outfile, **kwargs) + + # Assert + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 + with PdfParser.PdfParser(outfile) as pdf: + if kwargs.get("append_images", False) or kwargs.get("append", False): + assert len(pdf.pages) > 1 else: - im.save(outfile) + assert len(pdf.pages) > 0 + with open(outfile, "rb") as fp: + contents = fp.read() + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert im.size == size + + return outfile + - # Assert - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) +@pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK")) +def test_save(tmp_path, mode): + helper_save_as_pdf(tmp_path, mode) - def test_monochrome(self): - # Arrange - mode = "1" - # Act / Assert - self.helper_save_as_pdf(mode) +def test_monochrome(tmp_path): + # Arrange + mode = "1" - def test_greyscale(self): - # Arrange - mode = "L" + # Act / Assert + outfile = helper_save_as_pdf(tmp_path, mode) + assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000) - # Act / Assert - self.helper_save_as_pdf(mode) - def test_rgb(self): - # Arrange - mode = "RGB" +def test_unsupported_mode(tmp_path): + im = hopper("LA") + outfile = str(tmp_path / "temp_LA.pdf") - # Act / Assert - self.helper_save_as_pdf(mode) + with pytest.raises(ValueError): + im.save(outfile) - def test_p_mode(self): - # Arrange - mode = "P" - # Act / Assert - self.helper_save_as_pdf(mode) +def test_resolution(tmp_path): + im = hopper() - def test_cmyk_mode(self): - # Arrange - mode = "CMYK" + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, resolution=150) - # Act / Assert - self.helper_save_as_pdf(mode) + with open(outfile, "rb") as fp: + contents = fp.read() - def test_unsupported_mode(self): - im = hopper("LA") - outfile = self.tempfile("temp_LA.pdf") + size = tuple( + float(d) + for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") + ) + assert size == (61.44, 61.44) - self.assertRaises(ValueError, im.save, outfile) + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert size == (61.44, 61.44) - def test_save_all(self): - # Single frame image - self.helper_save_as_pdf("RGB", save_all=True) - # Multiframe image - im = Image.open("Tests/images/dispose_bgnd.gif") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) +def test_save_all(tmp_path): + # Single frame image + helper_save_as_pdf(tmp_path, "RGB", save_all=True) - outfile = self.tempfile('temp.pdf') + # Multiframe image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + + outfile = str(tmp_path / "temp.pdf") im.save(outfile, save_all=True) - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 # Append images ims = [hopper()] im.copy().save(outfile, save_all=True, append_images=ims) - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 # Test appending using a generator - def imGenerator(ims): - for im in ims: - yield im - im.save(outfile, save_all=True, append_images=imGenerator(ims)) + def im_generator(ims): + yield from ims + + im.save(outfile, save_all=True, append_images=im_generator(ims)) - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 - # Append JPEG images - jpeg = Image.open("Tests/images/flower.jpg") + # Append JPEG images + with Image.open("Tests/images/flower.jpg") as jpeg: jpeg.save(outfile, save_all=True, append_images=[jpeg.copy()]) - self.assertTrue(os.path.isfile(outfile)) - self.assertGreater(os.path.getsize(outfile), 0) + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 + + +def test_multiframe_normal_save(tmp_path): + # Test saving a multiframe image without save_all + with Image.open("Tests/images/dispose_bgnd.gif") as im: + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile) + + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 + + +def test_pdf_open(tmp_path): + # fail on a buffer full of null bytes + with pytest.raises(PdfParser.PdfFormatError): + PdfParser.PdfParser(buf=bytearray(65536)) + + # make an empty PDF object + with PdfParser.PdfParser() as empty_pdf: + assert len(empty_pdf.pages) == 0 + assert len(empty_pdf.info) == 0 + assert not empty_pdf.should_close_buf + assert not empty_pdf.should_close_file + + # make a PDF file + pdf_filename = helper_save_as_pdf(tmp_path, "RGB") + + # open the PDF file + with PdfParser.PdfParser(filename=pdf_filename) as hopper_pdf: + assert len(hopper_pdf.pages) == 1 + assert hopper_pdf.should_close_buf + assert hopper_pdf.should_close_file + + # read a PDF file from a buffer with a non-zero offset + with open(pdf_filename, "rb") as f: + content = b"xyzzy" + f.read() + with PdfParser.PdfParser(buf=content, start_offset=5) as hopper_pdf: + assert len(hopper_pdf.pages) == 1 + assert not hopper_pdf.should_close_buf + assert not hopper_pdf.should_close_file + + # read a PDF file from an already open file + with open(pdf_filename, "rb") as f: + with PdfParser.PdfParser(f=f) as hopper_pdf: + assert len(hopper_pdf.pages) == 1 + assert hopper_pdf.should_close_buf + assert not hopper_pdf.should_close_file + + +def test_pdf_append_fails_on_nonexistent_file(): + im = hopper("RGB") + with tempfile.TemporaryDirectory() as temp_dir: + with pytest.raises(OSError): + im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True) + + +def check_pdf_pages_consistency(pdf): + pages_info = pdf.read_indirect(pdf.pages_ref) + assert b"Parent" not in pages_info + assert b"Kids" in pages_info + kids_not_used = pages_info[b"Kids"] + for page_ref in pdf.pages: + while True: + if page_ref in kids_not_used: + kids_not_used.remove(page_ref) + page_info = pdf.read_indirect(page_ref) + assert b"Parent" in page_info + page_ref = page_info[b"Parent"] + if page_ref == pdf.pages_ref: + break + assert pdf.pages_ref == page_info[b"Parent"] + assert kids_not_used == [] + + +def test_pdf_append(tmp_path): + # make a PDF file + pdf_filename = helper_save_as_pdf(tmp_path, "RGB", producer="PdfParser") + + # open it, check pages and info + with PdfParser.PdfParser(pdf_filename, mode="r+b") as pdf: + assert len(pdf.pages) == 1 + assert len(pdf.info) == 4 + assert pdf.info.Title == os.path.splitext(os.path.basename(pdf_filename))[0] + assert pdf.info.Producer == "PdfParser" + assert b"CreationDate" in pdf.info + assert b"ModDate" in pdf.info + check_pdf_pages_consistency(pdf) + + # append some info + pdf.info.Title = "abc" + pdf.info.Author = "def" + pdf.info.Subject = "ghi\uABCD" + pdf.info.Keywords = "qw)e\\r(ty" + pdf.info.Creator = "hopper()" + pdf.start_writing() + pdf.write_xref_and_trailer() + + # open it again, check pages and info again + with PdfParser.PdfParser(pdf_filename) as pdf: + assert len(pdf.pages) == 1 + assert len(pdf.info) == 8 + assert pdf.info.Title == "abc" + assert b"CreationDate" in pdf.info + assert b"ModDate" in pdf.info + check_pdf_pages_consistency(pdf) + + # append two images + mode_cmyk = hopper("CMYK") + mode_p = hopper("P") + mode_cmyk.save(pdf_filename, append=True, save_all=True, append_images=[mode_p]) + + # open the PDF again, check pages and info again + with PdfParser.PdfParser(pdf_filename) as pdf: + assert len(pdf.pages) == 3 + assert len(pdf.info) == 8 + assert PdfParser.decode_text(pdf.info[b"Title"]) == "abc" + assert pdf.info.Title == "abc" + assert pdf.info.Producer == "PdfParser" + assert pdf.info.Keywords == "qw)e\\r(ty" + assert pdf.info.Subject == "ghi\uABCD" + assert b"CreationDate" in pdf.info + assert b"ModDate" in pdf.info + check_pdf_pages_consistency(pdf) + + +def test_pdf_info(tmp_path): + # make a PDF file + pdf_filename = helper_save_as_pdf( + tmp_path, + "RGB", + title="title", + author="author", + subject="subject", + keywords="keywords", + creator="creator", + producer="producer", + creationDate=time.strptime("2000", "%Y"), + modDate=time.strptime("2001", "%Y"), + ) + + # open it, check pages and info + with PdfParser.PdfParser(pdf_filename) as pdf: + assert len(pdf.info) == 8 + assert pdf.info.Title == "title" + assert pdf.info.Author == "author" + assert pdf.info.Subject == "subject" + assert pdf.info.Keywords == "keywords" + assert pdf.info.Creator == "creator" + assert pdf.info.Producer == "producer" + assert pdf.info.CreationDate == time.strptime("2000", "%Y") + assert pdf.info.ModDate == time.strptime("2001", "%Y") + check_pdf_pages_consistency(pdf) + + +def test_pdf_append_to_bytesio(): + im = hopper("RGB") + f = io.BytesIO() + im.save(f, format="PDF") + initial_size = len(f.getvalue()) + assert initial_size > 0 + im = hopper("P") + f = io.BytesIO(f.getvalue()) + im.save(f, format="PDF", append=True) + assert len(f.getvalue()) > initial_size + +@pytest.mark.timeout(1) +@pytest.mark.parametrize("newline", (b"\r", b"\n")) +def test_redos(newline): + malicious = b" trailer<<>>" + newline * 3456 -if __name__ == '__main__': - unittest.main() + # This particular exception isn't relevant here. + # The important thing is it doesn't timeout, cause a ReDoS (CVE-2021-25292). + with pytest.raises(PdfParser.PdfFormatError): + PdfParser.PdfParser(buf=malicious) diff --git a/Tests/test_file_pixar.py b/Tests/test_file_pixar.py index ae8c7d5f54f..315ea4676e1 100644 --- a/Tests/test_file_pixar.py +++ b/Tests/test_file_pixar.py @@ -1,29 +1,26 @@ -from helper import hopper, unittest, PillowTestCase +import pytest from PIL import Image, PixarImagePlugin -TEST_FILE = "Tests/images/hopper.pxr" +from .helper import assert_image_similar, hopper +TEST_FILE = "Tests/images/hopper.pxr" -class TestFilePixar(PillowTestCase): - def test_sanity(self): - im = Image.open(TEST_FILE) +def test_sanity(): + with Image.open(TEST_FILE) as im: im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "PIXAR") + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "PIXAR" + assert im.get_format_mimetype() is None im2 = hopper() - self.assert_image_similar(im, im2, 4.8) - - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" + assert_image_similar(im, im2, 4.8) - self.assertRaises( - SyntaxError, - PixarImagePlugin.PixarImageFile, invalid_file) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" -if __name__ == '__main__': - unittest.main() + with pytest.raises(SyntaxError): + PixarImagePlugin.PixarImageFile(invalid_file) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ce2b3e60872..37235fe6f02 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,12 +1,28 @@ -from helper import unittest, PillowTestCase, PillowLeakTestCase, hopper -from PIL import Image, ImageFile, PngImagePlugin - -from io import BytesIO -import zlib +import re import sys +import warnings +import zlib +from io import BytesIO + +import pytest -codecs = dir(Image.core) +from PIL import Image, ImageFile, PngImagePlugin, features +from .helper import ( + PillowLeakTestCase, + assert_image, + assert_image_equal, + assert_image_equal_tofile, + hopper, + is_win32, + mark_if_feature_version, + skip_unless_feature, +) + +try: + from defusedxml import ElementTree +except ImportError: + ElementTree = None # sample png stream @@ -22,9 +38,10 @@ def chunk(cid, *data): PngImagePlugin.putchunk(*(test_file, cid) + data) return test_file.getvalue() + o32 = PngImagePlugin.o32 -IHDR = chunk(b"IHDR", o32(1), o32(1), b'\x08\x02', b'\0\0\0') +IHDR = chunk(b"IHDR", o32(1), o32(1), b"\x08\x02", b"\0\0\0") IDAT = chunk(b"IDAT") IEND = chunk(b"IEND") @@ -43,12 +60,8 @@ def roundtrip(im, **options): return Image.open(out) -class TestFilePng(PillowTestCase): - - def setUp(self): - if "zip_encoder" not in codecs or "zip_decoder" not in codecs: - self.skipTest("zip/deflate support not available") - +@skip_unless_feature("zlib") +class TestFilePng: def get_chunks(self, filename): chunks = [] with open(filename, "rb") as fp: @@ -64,322 +77,336 @@ def get_chunks(self, filename): png.crc(cid, s) return chunks - def test_sanity(self): + def test_sanity(self, tmp_path): # internal version number - self.assertRegexpMatches( - Image.core.zlib_version, r"\d+\.\d+\.\d+(\.\d+)?$") + assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib")) - test_file = self.tempfile("temp.png") + test_file = str(tmp_path / "temp.png") hopper("RGB").save(test_file) - im = Image.open(test_file) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "PNG") - - hopper("1").save(test_file) - im = Image.open(test_file) - - hopper("L").save(test_file) - im = Image.open(test_file) - - hopper("P").save(test_file) - im = Image.open(test_file) - - hopper("RGB").save(test_file) - im = Image.open(test_file) - - hopper("I").save(test_file) - im = Image.open(test_file) + with Image.open(test_file) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "PNG" + assert im.get_format_mimetype() == "image/png" + + for mode in ["1", "L", "P", "RGB", "I", "I;16"]: + im = hopper(mode) + im.save(test_file) + with Image.open(test_file) as reloaded: + if mode == "I;16": + reloaded = reloaded.convert(mode) + assert_image_equal(reloaded, im) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - PngImagePlugin.PngImageFile, invalid_file) + with pytest.raises(SyntaxError): + PngImagePlugin.PngImageFile(invalid_file) def test_broken(self): # Check reading of totally broken files. In this case, the test # file was checked into Subversion as a text file. test_file = "Tests/images/broken.png" - self.assertRaises(IOError, Image.open, test_file) + with pytest.raises(OSError): + with Image.open(test_file): + pass def test_bad_text(self): # Make sure PIL can read malformed tEXt chunks (@PIL152) - im = load(HEAD + chunk(b'tEXt') + TAIL) - self.assertEqual(im.info, {}) + im = load(HEAD + chunk(b"tEXt") + TAIL) + assert im.info == {} - im = load(HEAD + chunk(b'tEXt', b'spam') + TAIL) - self.assertEqual(im.info, {'spam': ''}) + im = load(HEAD + chunk(b"tEXt", b"spam") + TAIL) + assert im.info == {"spam": ""} - im = load(HEAD + chunk(b'tEXt', b'spam\0') + TAIL) - self.assertEqual(im.info, {'spam': ''}) + im = load(HEAD + chunk(b"tEXt", b"spam\0") + TAIL) + assert im.info == {"spam": ""} - im = load(HEAD + chunk(b'tEXt', b'spam\0egg') + TAIL) - self.assertEqual(im.info, {'spam': 'egg'}) + im = load(HEAD + chunk(b"tEXt", b"spam\0egg") + TAIL) + assert im.info == {"spam": "egg"} - im = load(HEAD + chunk(b'tEXt', b'spam\0egg\0') + TAIL) - self.assertEqual(im.info, {'spam': 'egg\x00'}) + im = load(HEAD + chunk(b"tEXt", b"spam\0egg\0") + TAIL) + assert im.info == {"spam": "egg\x00"} def test_bad_ztxt(self): # Test reading malformed zTXt chunks (python-pillow/Pillow#318) - im = load(HEAD + chunk(b'zTXt') + TAIL) - self.assertEqual(im.info, {}) + im = load(HEAD + chunk(b"zTXt") + TAIL) + assert im.info == {} - im = load(HEAD + chunk(b'zTXt', b'spam') + TAIL) - self.assertEqual(im.info, {'spam': ''}) + im = load(HEAD + chunk(b"zTXt", b"spam") + TAIL) + assert im.info == {"spam": ""} - im = load(HEAD + chunk(b'zTXt', b'spam\0') + TAIL) - self.assertEqual(im.info, {'spam': ''}) + im = load(HEAD + chunk(b"zTXt", b"spam\0") + TAIL) + assert im.info == {"spam": ""} - im = load(HEAD + chunk(b'zTXt', b'spam\0\0') + TAIL) - self.assertEqual(im.info, {'spam': ''}) + im = load(HEAD + chunk(b"zTXt", b"spam\0\0") + TAIL) + assert im.info == {"spam": ""} - im = load(HEAD + chunk( - b'zTXt', b'spam\0\0' + zlib.compress(b'egg')[:1]) + TAIL) - self.assertEqual(im.info, {'spam': ''}) + im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")[:1]) + TAIL) + assert im.info == {"spam": ""} - im = load( - HEAD + chunk(b'zTXt', b'spam\0\0' + zlib.compress(b'egg')) + TAIL) - self.assertEqual(im.info, {'spam': 'egg'}) + im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")) + TAIL) + assert im.info == {"spam": "egg"} def test_bad_itxt(self): - im = load(HEAD + chunk(b'iTXt') + TAIL) - self.assertEqual(im.info, {}) + im = load(HEAD + chunk(b"iTXt") + TAIL) + assert im.info == {} - im = load(HEAD + chunk(b'iTXt', b'spam') + TAIL) - self.assertEqual(im.info, {}) + im = load(HEAD + chunk(b"iTXt", b"spam") + TAIL) + assert im.info == {} - im = load(HEAD + chunk(b'iTXt', b'spam\0') + TAIL) - self.assertEqual(im.info, {}) + im = load(HEAD + chunk(b"iTXt", b"spam\0") + TAIL) + assert im.info == {} - im = load(HEAD + chunk(b'iTXt', b'spam\0\x02') + TAIL) - self.assertEqual(im.info, {}) + im = load(HEAD + chunk(b"iTXt", b"spam\0\x02") + TAIL) + assert im.info == {} - im = load(HEAD + chunk(b'iTXt', b'spam\0\0\0foo\0') + TAIL) - self.assertEqual(im.info, {}) + im = load(HEAD + chunk(b"iTXt", b"spam\0\0\0foo\0") + TAIL) + assert im.info == {} - im = load(HEAD + chunk(b'iTXt', b'spam\0\0\0en\0Spam\0egg') + TAIL) - self.assertEqual(im.info, {"spam": "egg"}) - self.assertEqual(im.info["spam"].lang, "en") - self.assertEqual(im.info["spam"].tkey, "Spam") + im = load(HEAD + chunk(b"iTXt", b"spam\0\0\0en\0Spam\0egg") + TAIL) + assert im.info == {"spam": "egg"} + assert im.info["spam"].lang == "en" + assert im.info["spam"].tkey == "Spam" - im = load(HEAD + chunk(b'iTXt', b'spam\0\1\0en\0Spam\0' + - zlib.compress(b"egg")[:1]) + TAIL) - self.assertEqual(im.info, {'spam': ''}) + im = load( + HEAD + + chunk(b"iTXt", b"spam\0\1\0en\0Spam\0" + zlib.compress(b"egg")[:1]) + + TAIL + ) + assert im.info == {"spam": ""} - im = load(HEAD + chunk(b'iTXt', b'spam\0\1\1en\0Spam\0' + - zlib.compress(b"egg")) + TAIL) - self.assertEqual(im.info, {}) + im = load( + HEAD + + chunk(b"iTXt", b"spam\0\1\1en\0Spam\0" + zlib.compress(b"egg")) + + TAIL + ) + assert im.info == {} - im = load(HEAD + chunk(b'iTXt', b'spam\0\1\0en\0Spam\0' + - zlib.compress(b"egg")) + TAIL) - self.assertEqual(im.info, {"spam": "egg"}) - self.assertEqual(im.info["spam"].lang, "en") - self.assertEqual(im.info["spam"].tkey, "Spam") + im = load( + HEAD + + chunk(b"iTXt", b"spam\0\1\0en\0Spam\0" + zlib.compress(b"egg")) + + TAIL + ) + assert im.info == {"spam": "egg"} + assert im.info["spam"].lang == "en" + assert im.info["spam"].tkey == "Spam" def test_interlace(self): test_file = "Tests/images/pil123p.png" - im = Image.open(test_file) - - self.assert_image(im, "P", (162, 150)) - self.assertTrue(im.info.get("interlace")) + with Image.open(test_file) as im: + assert_image(im, "P", (162, 150)) + assert im.info.get("interlace") - im.load() + im.load() test_file = "Tests/images/pil123rgba.png" - im = Image.open(test_file) - - self.assert_image(im, "RGBA", (162, 150)) - self.assertTrue(im.info.get("interlace")) + with Image.open(test_file) as im: + assert_image(im, "RGBA", (162, 150)) + assert im.info.get("interlace") - im.load() + im.load() def test_load_transparent_p(self): test_file = "Tests/images/pil123p.png" - im = Image.open(test_file) - - self.assert_image(im, "P", (162, 150)) - im = im.convert("RGBA") - self.assert_image(im, "RGBA", (162, 150)) + with Image.open(test_file) as im: + assert_image(im, "P", (162, 150)) + im = im.convert("RGBA") + assert_image(im, "RGBA", (162, 150)) # image has 124 unique alpha values - self.assertEqual(len(im.getchannel('A').getcolors()), 124) + assert len(im.getchannel("A").getcolors()) == 124 def test_load_transparent_rgb(self): test_file = "Tests/images/rgb_trns.png" - im = Image.open(test_file) - self.assertEqual(im.info["transparency"], (0, 255, 52)) + with Image.open(test_file) as im: + assert im.info["transparency"] == (0, 255, 52) - self.assert_image(im, "RGB", (64, 64)) - im = im.convert("RGBA") - self.assert_image(im, "RGBA", (64, 64)) + assert_image(im, "RGB", (64, 64)) + im = im.convert("RGBA") + assert_image(im, "RGBA", (64, 64)) # image has 876 transparent pixels - self.assertEqual(im.getchannel('A').getcolors()[0][0], 876) + assert im.getchannel("A").getcolors()[0][0] == 876 - def test_save_p_transparent_palette(self): + def test_save_p_transparent_palette(self, tmp_path): in_file = "Tests/images/pil123p.png" - im = Image.open(in_file) + with Image.open(in_file) as im: + # 'transparency' contains a byte string with the opacity for + # each palette entry + assert len(im.info["transparency"]) == 256 - # 'transparency' contains a byte string with the opacity for - # each palette entry - self.assertEqual(len(im.info["transparency"]), 256) - - test_file = self.tempfile("temp.png") - im.save(test_file) + test_file = str(tmp_path / "temp.png") + im.save(test_file) # check if saved image contains same transparency - im = Image.open(test_file) - self.assertEqual(len(im.info["transparency"]), 256) + with Image.open(test_file) as im: + assert len(im.info["transparency"]) == 256 - self.assert_image(im, "P", (162, 150)) - im = im.convert("RGBA") - self.assert_image(im, "RGBA", (162, 150)) + assert_image(im, "P", (162, 150)) + im = im.convert("RGBA") + assert_image(im, "RGBA", (162, 150)) # image has 124 unique alpha values - self.assertEqual(len(im.getchannel('A').getcolors()), 124) + assert len(im.getchannel("A").getcolors()) == 124 - def test_save_p_single_transparency(self): + def test_save_p_single_transparency(self, tmp_path): in_file = "Tests/images/p_trns_single.png" - im = Image.open(in_file) - - # pixel value 164 is full transparent - self.assertEqual(im.info["transparency"], 164) - self.assertEqual(im.getpixel((31, 31)), 164) + with Image.open(in_file) as im: + # pixel value 164 is full transparent + assert im.info["transparency"] == 164 + assert im.getpixel((31, 31)) == 164 - test_file = self.tempfile("temp.png") - im.save(test_file) + test_file = str(tmp_path / "temp.png") + im.save(test_file) # check if saved image contains same transparency - im = Image.open(test_file) - self.assertEqual(im.info["transparency"], 164) - self.assertEqual(im.getpixel((31, 31)), 164) - self.assert_image(im, "P", (64, 64)) - im = im.convert("RGBA") - self.assert_image(im, "RGBA", (64, 64)) + with Image.open(test_file) as im: + assert im.info["transparency"] == 164 + assert im.getpixel((31, 31)) == 164 + assert_image(im, "P", (64, 64)) + im = im.convert("RGBA") + assert_image(im, "RGBA", (64, 64)) - self.assertEqual(im.getpixel((31, 31)), (0, 255, 52, 0)) + assert im.getpixel((31, 31)) == (0, 255, 52, 0) # image has 876 transparent pixels - self.assertEqual(im.getchannel('A').getcolors()[0][0], 876) + assert im.getchannel("A").getcolors()[0][0] == 876 - def test_save_p_transparent_black(self): + def test_save_p_transparent_black(self, tmp_path): # check if solid black image with full transparency # is supported (check for #1838) im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) - self.assertEqual(im.getcolors(), [(100, (0, 0, 0, 0))]) + assert im.getcolors() == [(100, (0, 0, 0, 0))] im = im.convert("P") - test_file = self.tempfile("temp.png") + test_file = str(tmp_path / "temp.png") im.save(test_file) # check if saved image contains same transparency - im = Image.open(test_file) - self.assertEqual(len(im.info["transparency"]), 256) - self.assert_image(im, "P", (10, 10)) - im = im.convert("RGBA") - self.assert_image(im, "RGBA", (10, 10)) - self.assertEqual(im.getcolors(), [(100, (0, 0, 0, 0))]) - - def test_save_l_transparency(self): - in_file = "Tests/images/l_trns.png" - im = Image.open(in_file) - - test_file = self.tempfile("temp.png") - im.save(test_file) - - # There are 559 transparent pixels. - im = im.convert('RGBA') - self.assertEqual(im.getchannel('A').getcolors()[0][0], 559) - - def test_save_rgb_single_transparency(self): + with Image.open(test_file) as im: + assert len(im.info["transparency"]) == 256 + assert_image(im, "P", (10, 10)) + im = im.convert("RGBA") + assert_image(im, "RGBA", (10, 10)) + assert im.getcolors() == [(100, (0, 0, 0, 0))] + + def test_save_greyscale_transparency(self, tmp_path): + for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items(): + in_file = "Tests/images/" + mode.lower() + "_trns.png" + with Image.open(in_file) as im: + assert im.mode == mode + assert im.info["transparency"] == 255 + + im_rgba = im.convert("RGBA") + assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent + + test_file = str(tmp_path / "temp.png") + im.save(test_file) + + with Image.open(test_file) as test_im: + assert test_im.mode == mode + assert test_im.info["transparency"] == 255 + assert_image_equal(im, test_im) + + test_im_rgba = test_im.convert("RGBA") + assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent + + def test_save_rgb_single_transparency(self, tmp_path): in_file = "Tests/images/caption_6_33_22.png" - im = Image.open(in_file) - - test_file = self.tempfile("temp.png") - im.save(test_file) + with Image.open(in_file) as im: + test_file = str(tmp_path / "temp.png") + im.save(test_file) def test_load_verify(self): # Check open/load/verify exception (@PIL150) - im = Image.open(TEST_PNG_FILE) - im.verify() + with Image.open(TEST_PNG_FILE) as im: + # Assert that there is no unclosed file warning + with warnings.catch_warnings(): + im.verify() - im = Image.open(TEST_PNG_FILE) - im.load() - self.assertRaises(RuntimeError, im.verify) + with Image.open(TEST_PNG_FILE) as im: + im.load() + with pytest.raises(RuntimeError): + im.verify() def test_verify_struct_error(self): # Check open/load/verify exception (#1755) - # offsets to test, -10: breaks in i32() in read. (IOError) + # offsets to test, -10: breaks in i32() in read. (OSError) # -13: breaks in crc, txt chunk. # -14: malformed chunk for offset in (-10, -13, -14): - with open(TEST_PNG_FILE, 'rb') as f: + with open(TEST_PNG_FILE, "rb") as f: test_file = f.read()[:offset] - im = Image.open(BytesIO(test_file)) - self.assertIsNotNone(im.fp) - self.assertRaises((IOError, SyntaxError), im.verify) + with Image.open(BytesIO(test_file)) as im: + assert im.fp is not None + with pytest.raises((OSError, SyntaxError)): + im.verify() def test_verify_ignores_crc_error(self): # check ignores crc errors in ancillary chunks - chunk_data = chunk(b'tEXt', b'spam') - broken_crc_chunk_data = chunk_data[:-1] + b'q' # break CRC + chunk_data = chunk(b"tEXt", b"spam") + broken_crc_chunk_data = chunk_data[:-1] + b"q" # break CRC image_data = HEAD + broken_crc_chunk_data + TAIL - self.assertRaises(SyntaxError, PngImagePlugin.PngImageFile, BytesIO(image_data)) + with pytest.raises(SyntaxError): + PngImagePlugin.PngImageFile(BytesIO(image_data)) ImageFile.LOAD_TRUNCATED_IMAGES = True try: im = load(image_data) - self.assertIsNotNone(im) + assert im is not None finally: ImageFile.LOAD_TRUNCATED_IMAGES = False def test_verify_not_ignores_crc_error_in_required_chunk(self): # check does not ignore crc errors in required chunks - image_data = MAGIC + IHDR[:-1] + b'q' + TAIL + image_data = MAGIC + IHDR[:-1] + b"q" + TAIL ImageFile.LOAD_TRUNCATED_IMAGES = True try: - self.assertRaises(SyntaxError, PngImagePlugin.PngImageFile, BytesIO(image_data)) + with pytest.raises(SyntaxError): + PngImagePlugin.PngImageFile(BytesIO(image_data)) finally: ImageFile.LOAD_TRUNCATED_IMAGES = False def test_roundtrip_dpi(self): # Check dpi roundtripping - im = Image.open(TEST_PNG_FILE) + with Image.open(TEST_PNG_FILE) as im: + im = roundtrip(im, dpi=(100.33, 100.33)) + assert im.info["dpi"] == (100.33, 100.33) - im = roundtrip(im, dpi=(100, 100)) - self.assertEqual(im.info["dpi"], (100, 100)) + def test_load_float_dpi(self): + with Image.open(TEST_PNG_FILE) as im: + assert im.info["dpi"] == (95.9866, 95.9866) def test_roundtrip_text(self): # Check text roundtripping - im = Image.open(TEST_PNG_FILE) - - info = PngImagePlugin.PngInfo() - info.add_text("TXT", "VALUE") - info.add_text("ZIP", "VALUE", zip=True) + with Image.open(TEST_PNG_FILE) as im: + info = PngImagePlugin.PngInfo() + info.add_text("TXT", "VALUE") + info.add_text("ZIP", "VALUE", zip=True) - im = roundtrip(im, pnginfo=info) - self.assertEqual(im.info, {'TXT': 'VALUE', 'ZIP': 'VALUE'}) - self.assertEqual(im.text, {'TXT': 'VALUE', 'ZIP': 'VALUE'}) + im = roundtrip(im, pnginfo=info) + assert im.info == {"TXT": "VALUE", "ZIP": "VALUE"} + assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} def test_roundtrip_itxt(self): # Check iTXt roundtripping @@ -387,16 +414,15 @@ def test_roundtrip_itxt(self): im = Image.new("RGB", (32, 32)) info = PngImagePlugin.PngInfo() info.add_itxt("spam", "Eggs", "en", "Spam") - info.add_text("eggs", PngImagePlugin.iTXt("Spam", "en", "Eggs"), - zip=True) + info.add_text("eggs", PngImagePlugin.iTXt("Spam", "en", "Eggs"), zip=True) im = roundtrip(im, pnginfo=info) - self.assertEqual(im.info, {"spam": "Eggs", "eggs": "Spam"}) - self.assertEqual(im.text, {"spam": "Eggs", "eggs": "Spam"}) - self.assertEqual(im.text["spam"].lang, "en") - self.assertEqual(im.text["spam"].tkey, "Spam") - self.assertEqual(im.text["eggs"].lang, "en") - self.assertEqual(im.text["eggs"].tkey, "Eggs") + assert im.info == {"spam": "Eggs", "eggs": "Spam"} + assert im.text == {"spam": "Eggs", "eggs": "Spam"} + assert im.text["spam"].lang == "en" + assert im.text["spam"].tkey == "Spam" + assert im.text["eggs"].lang == "en" + assert im.text["eggs"].tkey == "Eggs" def test_nonunicode_text(self): # Check so that non-Unicode text is saved as a tEXt rather than iTXt @@ -405,26 +431,23 @@ def test_nonunicode_text(self): info = PngImagePlugin.PngInfo() info.add_text("Text", "Ascii") im = roundtrip(im, pnginfo=info) - self.assertIsInstance(im.info["Text"], str) + assert isinstance(im.info["Text"], str) def test_unicode_text(self): - # Check preservation of non-ASCII characters on Python 3 - # This cannot really be meaningfully tested on Python 2, - # since it didn't preserve charsets to begin with. + # Check preservation of non-ASCII characters def rt_text(value): im = Image.new("RGB", (32, 32)) info = PngImagePlugin.PngInfo() info.add_text("Text", value) im = roundtrip(im, pnginfo=info) - self.assertEqual(im.info, {"Text": value}) + assert im.info == {"Text": value} - if str is not bytes: - rt_text(" Aa" + chr(0xa0) + chr(0xc4) + chr(0xff)) # Latin1 - rt_text(chr(0x400) + chr(0x472) + chr(0x4ff)) # Cyrillic - rt_text(chr(0x4e00) + chr(0x66f0) + # CJK - chr(0x9fba) + chr(0x3042) + chr(0xac00)) - rt_text("A" + chr(0xc4) + chr(0x472) + chr(0x3042)) # Combined + rt_text(" Aa" + chr(0xA0) + chr(0xC4) + chr(0xFF)) # Latin1 + rt_text(chr(0x400) + chr(0x472) + chr(0x4FF)) # Cyrillic + # CJK: + rt_text(chr(0x4E00) + chr(0x66F0) + chr(0x9FBA) + chr(0x3042) + chr(0xAC00)) + rt_text("A" + chr(0xC4) + chr(0x472) + chr(0x3042)) # Combined def test_scary(self): # Check reading of evil PNG file. For information, see: @@ -432,124 +455,331 @@ def test_scary(self): # The first byte is removed from pngtest_bad.png # to avoid classification as malware. - with open("Tests/images/pngtest_bad.png.bin", 'rb') as fd: - data = b'\x89' + fd.read() + with open("Tests/images/pngtest_bad.png.bin", "rb") as fd: + data = b"\x89" + fd.read() pngfile = BytesIO(data) - self.assertRaises(IOError, Image.open, pngfile) + with pytest.raises(OSError): + with Image.open(pngfile): + pass def test_trns_rgb(self): # Check writing and reading of tRNS chunks for RGB images. # Independent file sample provided by Sebastian Spaeth. test_file = "Tests/images/caption_6_33_22.png" - im = Image.open(test_file) - self.assertEqual(im.info["transparency"], (248, 248, 248)) + with Image.open(test_file) as im: + assert im.info["transparency"] == (248, 248, 248) - # check saving transparency by default - im = roundtrip(im) - self.assertEqual(im.info["transparency"], (248, 248, 248)) + # check saving transparency by default + im = roundtrip(im) + assert im.info["transparency"] == (248, 248, 248) im = roundtrip(im, transparency=(0, 1, 2)) - self.assertEqual(im.info["transparency"], (0, 1, 2)) + assert im.info["transparency"] == (0, 1, 2) - def test_trns_p(self): + def test_trns_p(self, tmp_path): # Check writing a transparency of 0, issue #528 - im = hopper('P') - im.info['transparency'] = 0 + im = hopper("P") + im.info["transparency"] = 0 - f = self.tempfile("temp.png") + f = str(tmp_path / "temp.png") im.save(f) - im2 = Image.open(f) - self.assertIn('transparency', im2.info) + with Image.open(f) as im2: + assert "transparency" in im2.info - self.assert_image_equal(im2.convert('RGBA'), - im.convert('RGBA')) + assert_image_equal(im2.convert("RGBA"), im.convert("RGBA")) def test_trns_null(self): # Check reading images with null tRNS value, issue #1239 test_file = "Tests/images/tRNS_null_1x1.png" - im = Image.open(test_file) + with Image.open(test_file) as im: - self.assertEqual(im.info["transparency"], 0) + assert im.info["transparency"] == 0 def test_save_icc_profile(self): - im = Image.open("Tests/images/icc_profile_none.png") - self.assertIsNone(im.info['icc_profile']) + with Image.open("Tests/images/icc_profile_none.png") as im: + assert im.info["icc_profile"] is None - with_icc = Image.open("Tests/images/icc_profile.png") - expected_icc = with_icc.info['icc_profile'] + with Image.open("Tests/images/icc_profile.png") as with_icc: + expected_icc = with_icc.info["icc_profile"] - im = roundtrip(im, icc_profile=expected_icc) - self.assertEqual(im.info['icc_profile'], expected_icc) + im = roundtrip(im, icc_profile=expected_icc) + assert im.info["icc_profile"] == expected_icc def test_discard_icc_profile(self): - im = Image.open('Tests/images/icc_profile.png') + with Image.open("Tests/images/icc_profile.png") as im: + assert "icc_profile" in im.info - im = roundtrip(im, icc_profile=None) - self.assertNotIn('icc_profile', im.info) + im = roundtrip(im, icc_profile=None) + assert "icc_profile" not in im.info def test_roundtrip_icc_profile(self): - im = Image.open('Tests/images/icc_profile.png') - expected_icc = im.info['icc_profile'] + with Image.open("Tests/images/icc_profile.png") as im: + expected_icc = im.info["icc_profile"] - im = roundtrip(im) - self.assertEqual(im.info['icc_profile'], expected_icc) + im = roundtrip(im) + assert im.info["icc_profile"] == expected_icc def test_roundtrip_no_icc_profile(self): - im = Image.open("Tests/images/icc_profile_none.png") - self.assertIsNone(im.info['icc_profile']) + with Image.open("Tests/images/icc_profile_none.png") as im: + assert im.info["icc_profile"] is None - im = roundtrip(im) - self.assertNotIn('icc_profile', im.info) + im = roundtrip(im) + assert "icc_profile" not in im.info def test_repr_png(self): im = hopper() - repr_png = Image.open(BytesIO(im._repr_png_())) - self.assertEqual(repr_png.format, 'PNG') - self.assert_image_equal(im, repr_png) + with Image.open(BytesIO(im._repr_png_())) as repr_png: + assert repr_png.format == "PNG" + assert_image_equal(im, repr_png) + + def test_repr_png_error(self): + im = hopper("F") - def test_chunk_order(self): - im = Image.open("Tests/images/icc_profile.png") - test_file = self.tempfile("temp.png") - im.convert("P").save(test_file, dpi=(100, 100)) + with pytest.raises(ValueError): + im._repr_png_() + + def test_chunk_order(self, tmp_path): + with Image.open("Tests/images/icc_profile.png") as im: + test_file = str(tmp_path / "temp.png") + im.convert("P").save(test_file, dpi=(100, 100)) chunks = self.get_chunks(test_file) # https://www.w3.org/TR/PNG/#5ChunkOrdering # IHDR - shall be first - self.assertEqual(chunks.index(b"IHDR"), 0) + assert chunks.index(b"IHDR") == 0 # PLTE - before first IDAT - self.assertLess(chunks.index(b"PLTE"), chunks.index(b"IDAT")) + assert chunks.index(b"PLTE") < chunks.index(b"IDAT") # iCCP - before PLTE and IDAT - self.assertLess(chunks.index(b"iCCP"), chunks.index(b"PLTE")) - self.assertLess(chunks.index(b"iCCP"), chunks.index(b"IDAT")) + assert chunks.index(b"iCCP") < chunks.index(b"PLTE") + assert chunks.index(b"iCCP") < chunks.index(b"IDAT") # tRNS - after PLTE, before IDAT - self.assertGreater(chunks.index(b"tRNS"), chunks.index(b"PLTE")) - self.assertLess(chunks.index(b"tRNS"), chunks.index(b"IDAT")) + assert chunks.index(b"tRNS") > chunks.index(b"PLTE") + assert chunks.index(b"tRNS") < chunks.index(b"IDAT") # pHYs - before IDAT - self.assertLess(chunks.index(b"pHYs"), chunks.index(b"IDAT")) + assert chunks.index(b"pHYs") < chunks.index(b"IDAT") def test_getchunks(self): im = hopper() chunks = PngImagePlugin.getchunks(im) - self.assertEqual(len(chunks), 3) + assert len(chunks) == 3 + def test_read_private_chunks(self): + with Image.open("Tests/images/exif.png") as im: + assert im.private_chunks == [(b"orNT", b"\x01")] -@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") -class TestTruncatedPngPLeaks(PillowLeakTestCase): - mem_limit = 2*1024 # max increase in K - iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs + def test_roundtrip_private_chunk(self): + # Check private chunk roundtripping + + with Image.open(TEST_PNG_FILE) as im: + info = PngImagePlugin.PngInfo() + info.add(b"prIV", b"VALUE") + info.add(b"atEC", b"VALUE2") + info.add(b"prIV", b"VALUE3", True) + + im = roundtrip(im, pnginfo=info) + assert im.private_chunks == [(b"prIV", b"VALUE"), (b"atEC", b"VALUE2")] + im.load() + assert im.private_chunks == [ + (b"prIV", b"VALUE"), + (b"atEC", b"VALUE2"), + (b"prIV", b"VALUE3", True), + ] + + def test_textual_chunks_after_idat(self): + with Image.open("Tests/images/hopper.png") as im: + assert "comment" in im.text.keys() + for k, v in { + "date:create": "2014-09-04T09:37:08+03:00", + "date:modify": "2014-09-04T09:37:08+03:00", + }.items(): + assert im.text[k] == v + + # Raises a SyntaxError in load_end + with Image.open("Tests/images/broken_data_stream.png") as im: + with pytest.raises(OSError): + assert isinstance(im.text, dict) + + # Raises a UnicodeDecodeError in load_end + with Image.open("Tests/images/truncated_image.png") as im: + # The file is truncated + with pytest.raises(OSError): + im.text() + ImageFile.LOAD_TRUNCATED_IMAGES = True + assert isinstance(im.text, dict) + ImageFile.LOAD_TRUNCATED_IMAGES = False + + # Raises an EOFError in load_end + with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: + assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} + + def test_padded_idat(self): + # This image has been manually hexedited + # so that the IDAT chunk has padding at the end + # Set MAXBLOCK to the length of the actual data + # so that the decoder finishes reading before the chunk ends + MAXBLOCK = ImageFile.MAXBLOCK + ImageFile.MAXBLOCK = 45 + ImageFile.LOAD_TRUNCATED_IMAGES = True + + with Image.open("Tests/images/padded_idat.png") as im: + im.load() + + ImageFile.MAXBLOCK = MAXBLOCK + ImageFile.LOAD_TRUNCATED_IMAGES = False + + assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") + + @pytest.mark.parametrize( + "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") + ) + def test_truncated_chunks(self, cid): + fp = BytesIO() + with PngImagePlugin.PngStream(fp) as png: + with pytest.raises(ValueError): + png.call(cid, 0, 0) + + ImageFile.LOAD_TRUNCATED_IMAGES = True + png.call(cid, 0, 0) + ImageFile.LOAD_TRUNCATED_IMAGES = False + + def test_specify_bits(self, tmp_path): + im = hopper("P") + + out = str(tmp_path / "temp.png") + im.save(out, bits=4) + + with Image.open(out) as reloaded: + assert len(reloaded.png.im_palette[1]) == 48 + + def test_plte_length(self, tmp_path): + im = Image.new("P", (1, 1)) + im.putpalette((1, 1, 1)) + + out = str(tmp_path / "temp.png") + im.save(str(tmp_path / "temp.png")) + + with Image.open(out) as reloaded: + assert len(reloaded.png.im_palette[1]) == 3 + + def test_getxmp(self): + with Image.open("Tests/images/color_snakes.png") as im: + if ElementTree is None: + with pytest.warns(UserWarning): + assert im.getxmp() == {} + else: + xmp = im.getxmp() - def setUp(self): - if "zip_encoder" not in codecs or "zip_decoder" not in codecs: - self.skipTest("zip/deflate support not available") + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description["PixelXDimension"] == "10" + assert description["subject"]["Seq"] is None + + def test_exif(self): + # With an EXIF chunk + with Image.open("Tests/images/exif.png") as im: + exif = im._getexif() + assert exif[274] == 1 + + # With an ImageMagick zTXt chunk + with Image.open("Tests/images/exif_imagemagick.png") as im: + exif = im._getexif() + assert exif[274] == 1 + + # Assert that info still can be extracted + # when the image is no longer a PngImageFile instance + exif = im.copy().getexif() + assert exif[274] == 1 + + # With a tEXt chunk + with Image.open("Tests/images/exif_text.png") as im: + exif = im._getexif() + assert exif[274] == 1 + + # With XMP tags + with Image.open("Tests/images/xmp_tags_orientation.png") as im: + exif = im.getexif() + assert exif[274] == 3 + + def test_exif_save(self, tmp_path): + 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: + exif = reloaded._getexif() + assert exif[274] == 1 + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_exif_from_jpg(self, tmp_path): + with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + test_file = str(tmp_path / "temp.png") + im.save(test_file) + + with Image.open(test_file) as reloaded: + exif = reloaded._getexif() + assert exif[305] == "Adobe Photoshop CS Macintosh" + + def test_exif_argument(self, tmp_path): + with Image.open(TEST_PNG_FILE) as im: + test_file = str(tmp_path / "temp.png") + im.save(test_file, exif=b"exifstring") + + with Image.open(test_file) as reloaded: + assert reloaded.info["exif"] == b"Exif\x00\x00exifstring" + + def test_tell(self): + with Image.open(TEST_PNG_FILE) as im: + assert im.tell() == 0 + + def test_seek(self): + with Image.open(TEST_PNG_FILE) as im: + im.seek(0) + + with pytest.raises(EOFError): + im.seek(1) + + @pytest.mark.parametrize("buffer", (True, False)) + def test_save_stdout(self, buffer): + old_stdout = sys.stdout + + if buffer: + + class MyStdOut: + buffer = BytesIO() + + mystdout = MyStdOut() + else: + mystdout = BytesIO() + + sys.stdout = mystdout + + with Image.open(TEST_PNG_FILE) as im: + im.save(sys.stdout, "PNG") + + # Reset stdout + sys.stdout = old_stdout + + if buffer: + mystdout = mystdout.buffer + with Image.open(mystdout) as reloaded: + assert_image_equal_tofile(reloaded, TEST_PNG_FILE) + + +@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") +@skip_unless_feature("zlib") +class TestTruncatedPngPLeaks(PillowLeakTestCase): + mem_limit = 2 * 1024 # max increase in K + iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs def test_leak_load(self): - with open('Tests/images/hopper.png', 'rb') as f: + with open("Tests/images/hopper.png", "rb") as f: DATA = BytesIO(f.read(16 * 1024)) ImageFile.LOAD_TRUNCATED_IMAGES = True @@ -564,7 +794,3 @@ def core(): self._test_leak(core) finally: ImageFile.LOAD_TRUNCATED_IMAGES = False - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 937a9dc322d..fbcbea6c691 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,55 +1,321 @@ -from helper import unittest, PillowTestCase +import sys +from io import BytesIO -from PIL import Image +import pytest + +from PIL import Image, PpmImagePlugin + +from .helper import assert_image_equal_tofile, assert_image_similar, hopper # sample ppm stream -test_file = "Tests/images/hopper.ppm" +TEST_FILE = "Tests/images/hopper.ppm" + + +def test_sanity(): + with Image.open(TEST_FILE) as im: + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "PPM" + assert im.get_format_mimetype() == "image/x-portable-pixmap" + + +@pytest.mark.parametrize( + "data, mode, pixels", + ( + (b"P2 3 1 4 0 2 4", "L", (0, 128, 255)), + (b"P2 3 1 257 0 128 257", "I", (0, 32640, 65535)), + # P3 with maxval < 255 + ( + b"P3 3 1 17 0 1 2 8 9 10 15 16 17", + "RGB", + ((0, 15, 30), (120, 135, 150), (225, 240, 255)), + ), + # P3 with maxval > 255 + # Scale down to 255, since there is no RGB mode with more than 8-bit + ( + b"P3 3 1 257 0 1 2 128 129 130 256 257 257", + "RGB", + ((0, 1, 2), (127, 128, 129), (254, 255, 255)), + ), + (b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)), + (b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)), + # P6 with maxval < 255 + ( + b"P6 3 1 17 \x00\x01\x02\x08\x09\x0A\x0F\x10\x11", + "RGB", + ( + (0, 15, 30), + (120, 135, 150), + (225, 240, 255), + ), + ), + # P6 with maxval > 255 + ( + b"P6 3 1 257 \x00\x00\x00\x01\x00\x02" + b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF", + "RGB", + ( + (0, 1, 2), + (127, 128, 129), + (254, 255, 255), + ), + ), + ), +) +def test_arbitrary_maxval(data, mode, pixels): + fp = BytesIO(data) + with Image.open(fp) as im: + assert im.size == (3, 1) + assert im.mode == mode + + px = im.load() + assert tuple(px[x, 0] for x in range(3)) == pixels + + +def test_16bit_pgm(): + with Image.open("Tests/images/16_bit_binary.pgm") as im: + assert im.mode == "I" + assert im.size == (20, 100) + assert im.get_format_mimetype() == "image/x-portable-graymap" + + assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.png") + + +def test_16bit_pgm_write(tmp_path): + with Image.open("Tests/images/16_bit_binary.pgm") as im: + f = str(tmp_path / "temp.pgm") + im.save(f, "PPM") + + assert_image_equal_tofile(im, f) + + +def test_pnm(tmp_path): + with Image.open("Tests/images/hopper.pnm") as im: + assert_image_similar(im, hopper(), 0.0001) + + f = str(tmp_path / "temp.pnm") + im.save(f) + + assert_image_equal_tofile(im, f) + + +@pytest.mark.parametrize( + "plain_path, raw_path", + ( + ( + "Tests/images/hopper_1bit_plain.pbm", # P1 + "Tests/images/hopper_1bit.pbm", # P4 + ), + ( + "Tests/images/hopper_8bit_plain.pgm", # P2 + "Tests/images/hopper_8bit.pgm", # P5 + ), + ( + "Tests/images/hopper_8bit_plain.ppm", # P3 + "Tests/images/hopper_8bit.ppm", # P6 + ), + ), +) +def test_plain(plain_path, raw_path): + with Image.open(plain_path) as im: + assert_image_equal_tofile(im, raw_path) + + +def test_16bit_plain_pgm(): + # P2 with maxval 2 ** 16 - 1 + with Image.open("Tests/images/hopper_16bit_plain.pgm") as im: + assert im.mode == "I" + assert im.size == (128, 128) + assert im.get_format_mimetype() == "image/x-portable-graymap" + + # P5 with maxval 2 ** 16 - 1 + assert_image_equal_tofile(im, "Tests/images/hopper_16bit.pgm") + + +@pytest.mark.parametrize( + "header, data, comment_count", + ( + (b"P1\n2 2", b"1010", 10**6), + (b"P2\n3 1\n4", b"0 2 4", 1), + (b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6), + ), +) +def test_plain_data_with_comment(tmp_path, header, data, comment_count): + path1 = str(tmp_path / "temp1.ppm") + path2 = str(tmp_path / "temp2.ppm") + comment = b"# comment" * comment_count + with open(path1, "wb") as f1, open(path2, "wb") as f2: + f1.write(header + b"\n\n" + data) + f2.write(header + b"\n" + comment + b"\n" + data + comment) + + with Image.open(path1) as im: + assert_image_equal_tofile(im, path2) + + +@pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) +def test_plain_truncated_data(tmp_path, data): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(data) + + with Image.open(path) as im: + with pytest.raises(ValueError): + im.load() + + +@pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) +def test_plain_invalid_data(tmp_path, data): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(data) + + with Image.open(path) as im: + with pytest.raises(ValueError): + im.load() + + +@pytest.mark.parametrize( + "data", + ( + b"P3\n128 128\n255\n012345678910", # half token too long + b"P3\n128 128\n255\n012345678910 0", # token too long + ), +) +def test_plain_ppm_token_too_long(tmp_path, data): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(data) + + with Image.open(path) as im: + with pytest.raises(ValueError): + im.load() + + +def test_plain_ppm_value_too_large(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P3\n128 128\n255\n256") + + with Image.open(path) as im: + with pytest.raises(ValueError): + im.load() + + +def test_magic(): + with pytest.raises(SyntaxError): + PpmImagePlugin.PpmImageFile(fp=BytesIO(b"PyInvalid")) + + +def test_header_with_comments(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") + + with Image.open(path) as im: + assert im.size == (128, 128) + + +def test_non_integer_token(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P6\nTEST") + + with pytest.raises(ValueError): + with Image.open(path): + pass + + +def test_header_token_too_long(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P6\n 01234567890") + + with pytest.raises(ValueError) as e: + with Image.open(path): + pass + + assert str(e.value) == "Token too long in file header: 01234567890" + + +def test_truncated_file(tmp_path): + # Test EOF in header + path = str(tmp_path / "temp.pgm") + with open(path, "wb") as f: + f.write(b"P6") + + with pytest.raises(ValueError) as e: + with Image.open(path): + pass + + assert str(e.value) == "Reached EOF while reading header" + + # Test EOF for PyDecoder + fp = BytesIO(b"P5 3 1 4") + with Image.open(fp) as im: + with pytest.raises(ValueError): + im.load() + + +@pytest.mark.parametrize("maxval", (b"0", b"65536")) +def test_invalid_maxval(maxval, tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P6\n3 1 " + maxval) + + with pytest.raises(ValueError) as e: + with Image.open(path): + pass + + assert str(e.value) == "maxval must be greater than 0 and less than 65536" + + +def test_neg_ppm(): + # Storage.c accepted negative values for xsize, ysize. the + # internal open_ppm function didn't check for sanity but it + # has been removed. The default opener doesn't accept negative + # sizes. + with pytest.raises(OSError): + with Image.open("Tests/images/negative_size.ppm"): + pass -class TestFilePpm(PillowTestCase): - def test_sanity(self): - im = Image.open(test_file) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "PPM") +def test_mimetypes(tmp_path): + path = str(tmp_path / "temp.pgm") - def test_16bit_pgm(self): - im = Image.open('Tests/images/16_bit_binary.pgm') - im.load() - self.assertEqual(im.mode, 'I') - self.assertEqual(im.size, (20, 100)) + with open(path, "wb") as f: + f.write(b"P4\n128 128\n255") + with Image.open(path) as im: + assert im.get_format_mimetype() == "image/x-portable-bitmap" - tgt = Image.open('Tests/images/16_bit_binary_pgm.png') - self.assert_image_equal(im, tgt) + with open(path, "wb") as f: + f.write(b"PyCMYK\n128 128\n255") + with Image.open(path) as im: + assert im.get_format_mimetype() == "image/x-portable-anymap" - def test_16bit_pgm_write(self): - im = Image.open('Tests/images/16_bit_binary.pgm') - im.load() - f = self.tempfile('temp.pgm') - im.save(f, 'PPM') +@pytest.mark.parametrize("buffer", (True, False)) +def test_save_stdout(buffer): + old_stdout = sys.stdout - reloaded = Image.open(f) - self.assert_image_equal(im, reloaded) + if buffer: - def test_truncated_file(self): - path = self.tempfile('temp.pgm') - with open(path, 'w') as f: - f.write('P6') + class MyStdOut: + buffer = BytesIO() - self.assertRaises(ValueError, Image.open, path) + mystdout = MyStdOut() + else: + mystdout = BytesIO() - def test_neg_ppm(self): - # Storage.c accepted negative values for xsize, ysize. the - # internal open_ppm function didn't check for sanity but it - # has been removed. The default opener doesn't accept negative - # sizes. + sys.stdout = mystdout - with self.assertRaises(IOError): - Image.open('Tests/images/negative_size.ppm') + with Image.open(TEST_FILE) as im: + im.save(sys.stdout, "PPM") + # Reset stdout + sys.stdout = old_stdout -if __name__ == '__main__': - unittest.main() + if buffer: + mystdout = mystdout.buffer + with Image.open(mystdout) as reloaded: + assert_image_equal_tofile(reloaded, TEST_FILE) diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index afc69694d77..4f934375c7c 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,82 +1,158 @@ -from helper import hopper, unittest, PillowTestCase +import warnings + +import pytest from PIL import Image, PsdImagePlugin -test_file = "Tests/images/hopper.psd" +from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy +test_file = "Tests/images/hopper.psd" -class TestImagePsd(PillowTestCase): - def test_sanity(self): - im = Image.open(test_file) +def test_sanity(): + with Image.open(test_file) as im: im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "PSD") + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "PSD" + assert im.get_format_mimetype() == "image/vnd.adobe.photoshop" im2 = hopper() - self.assert_image_similar(im, im2, 4.8) + assert_image_similar(im, im2, 4.8) - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - PsdImagePlugin.PsdImageFile, invalid_file) +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): + im = Image.open(test_file) + im.load() - def test_n_frames(self): - im = Image.open("Tests/images/hopper_merged.psd") - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) + pytest.warns(ResourceWarning, open) - im = Image.open(test_file) - self.assertEqual(im.n_frames, 2) - self.assertTrue(im.is_animated) - def test_eoferror(self): +def test_closed_file(): + with warnings.catch_warnings(): im = Image.open(test_file) + im.load() + im.close() + + +def test_context_manager(): + with warnings.catch_warnings(): + with Image.open(test_file) as im: + im.load() + + +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + PsdImagePlugin.PsdImageFile(invalid_file) + + +def test_n_frames(): + with Image.open("Tests/images/hopper_merged.psd") as im: + assert im.n_frames == 1 + assert not im.is_animated + + for path in [test_file, "Tests/images/negative_layer_count.psd"]: + with Image.open(path) as im: + assert im.n_frames == 2 + assert im.is_animated + + +def test_eoferror(): + with Image.open(test_file) as im: # PSD seek index starts at 1 rather than 0 - n_frames = im.n_frames+1 + n_frames = im.n_frames + 1 # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames # Test that seeking to the last frame does not raise an error - im.seek(n_frames-1) + im.seek(n_frames - 1) - def test_seek_tell(self): - im = Image.open(test_file) + +def test_seek_tell(): + with Image.open(test_file) as im: layer_number = im.tell() - self.assertEqual(layer_number, 1) + assert layer_number == 1 - self.assertRaises(EOFError, im.seek, 0) + with pytest.raises(EOFError): + im.seek(0) im.seek(1) layer_number = im.tell() - self.assertEqual(layer_number, 1) + assert layer_number == 1 im.seek(2) layer_number = im.tell() - self.assertEqual(layer_number, 2) + assert layer_number == 2 - def test_seek_eoferror(self): - im = Image.open(test_file) - self.assertRaises(EOFError, im.seek, -1) +def test_seek_eoferror(): + with Image.open(test_file) as im: - def test_icc_profile(self): - im = Image.open(test_file) - self.assertIn("icc_profile", im.info) + with pytest.raises(EOFError): + im.seek(-1) - icc_profile = im.info["icc_profile"] - self.assertEqual(len(icc_profile), 3144) - def test_no_icc_profile(self): - im = Image.open("Tests/images/hopper_merged.psd") +def test_open_after_exclusive_load(): + with Image.open(test_file) as im: + im.load() + im.seek(im.tell() + 1) + im.load() - self.assertNotIn("icc_profile", im.info) +def test_rgba(): + with Image.open("Tests/images/rgba.psd") as im: + assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png") -if __name__ == '__main__': - unittest.main() + +def test_icc_profile(): + with Image.open(test_file) as im: + assert "icc_profile" in im.info + + icc_profile = im.info["icc_profile"] + assert len(icc_profile) == 3144 + + +def test_no_icc_profile(): + with Image.open("Tests/images/hopper_merged.psd") as im: + assert "icc_profile" not in im.info + + +def test_combined_larger_than_size(): + # The combined size of the individual parts is larger than the + # declared 'size' of the extra data field, resulting in a backwards seek. + + # If we instead take the 'size' of the extra data field as the source of truth, + # then the seek can't be negative + with pytest.raises(OSError): + with Image.open("Tests/images/combined_larger_than_size.psd"): + pass + + +@pytest.mark.parametrize( + "test_file,raises", + [ + ( + "Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd", + Image.UnidentifiedImageError, + ), + ( + "Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd", + Image.UnidentifiedImageError, + ), + ("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError), + ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), + ], +) +def test_crashes(test_file, raises): + with open(test_file, "rb") as f: + with pytest.raises(raises): + with Image.open(f): + pass diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index f18fb13decd..6a5d8887d33 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,92 +1,105 @@ -from helper import unittest, PillowTestCase, hopper +import pytest from PIL import Image, SgiImagePlugin +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + hopper, +) -class TestFileSgi(PillowTestCase): - def test_rgb(self): - # Created with ImageMagick then renamed: - # convert hopper.ppm -compress None sgi:hopper.rgb - test_file = "Tests/images/hopper.rgb" +def test_rgb(): + # Created with ImageMagick then renamed: + # convert hopper.ppm -compress None sgi:hopper.rgb + test_file = "Tests/images/hopper.rgb" - im = Image.open(test_file) - self.assert_image_equal(im, hopper()) + with Image.open(test_file) as im: + assert_image_equal(im, hopper()) + assert im.get_format_mimetype() == "image/rgb" - def test_rgb16(self): - test_file = "Tests/images/hopper16.rgb" - im = Image.open(test_file) - self.assert_image_equal(im, hopper()) +def test_rgb16(): + assert_image_equal_tofile(hopper(), "Tests/images/hopper16.rgb") - def test_l(self): - # Created with ImageMagick - # convert hopper.ppm -monochrome -compress None sgi:hopper.bw - test_file = "Tests/images/hopper.bw" - im = Image.open(test_file) - self.assert_image_similar(im, hopper('L'), 2) +def test_l(): + # Created with ImageMagick + # convert hopper.ppm -monochrome -compress None sgi:hopper.bw + test_file = "Tests/images/hopper.bw" - def test_rgba(self): - # Created with ImageMagick: - # convert transparent.png -compress None transparent.sgi - test_file = "Tests/images/transparent.sgi" + with Image.open(test_file) as im: + assert_image_similar(im, hopper("L"), 2) + assert im.get_format_mimetype() == "image/sgi" - im = Image.open(test_file) - target = Image.open('Tests/images/transparent.png') - self.assert_image_equal(im, target) - def test_rle(self): - # Created with ImageMagick: - # convert hopper.ppm hopper.sgi - test_file = "Tests/images/hopper.sgi" +def test_rgba(): + # Created with ImageMagick: + # convert transparent.png -compress None transparent.sgi + test_file = "Tests/images/transparent.sgi" - im = Image.open(test_file) - target = Image.open('Tests/images/hopper.rgb') - self.assert_image_equal(im, target) + with Image.open(test_file) as im: + assert_image_equal_tofile(im, "Tests/images/transparent.png") + assert im.get_format_mimetype() == "image/sgi" - def test_rle16(self): - test_file = "Tests/images/tv16.sgi" - im = Image.open(test_file) - target = Image.open('Tests/images/tv.rgb') - self.assert_image_equal(im, target) +def test_rle(): + # Created with ImageMagick: + # convert hopper.ppm hopper.sgi + test_file = "Tests/images/hopper.sgi" - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" + with Image.open(test_file) as im: + assert_image_equal_tofile(im, "Tests/images/hopper.rgb") - self.assertRaises(ValueError, - SgiImagePlugin.SgiImageFile, invalid_file) - def test_write(self): - def roundtrip(img): - out = self.tempfile('temp.sgi') - img.save(out, format='sgi') - reloaded = Image.open(out) - self.assert_image_equal(img, reloaded) +def test_rle16(): + test_file = "Tests/images/tv16.sgi" - for mode in ('L', 'RGB', 'RGBA'): - roundtrip(hopper(mode)) + with Image.open(test_file) as im: + assert_image_equal_tofile(im, "Tests/images/tv.rgb") - # Test 1 dimension for an L mode image - roundtrip(Image.new('L', (10, 1))) - def test_write16(self): - test_file = "Tests/images/hopper16.rgb" +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - im = Image.open(test_file) - out = self.tempfile('temp.sgi') - im.save(out, format='sgi', bpc=2) + with pytest.raises(ValueError): + SgiImagePlugin.SgiImageFile(invalid_file) - reloaded = Image.open(out) - self.assert_image_equal(im, reloaded) - - def test_unsupported_mode(self): - im = hopper('LA') - out = self.tempfile('temp.sgi') - self.assertRaises(ValueError, im.save, out, format='sgi') +def test_write(tmp_path): + def roundtrip(img): + out = str(tmp_path / "temp.sgi") + img.save(out, format="sgi") + assert_image_equal_tofile(img, out) + out = str(tmp_path / "fp.sgi") + with open(out, "wb") as fp: + img.save(fp) + assert_image_equal_tofile(img, out) -if __name__ == '__main__': - unittest.main() + assert not fp.closed + + for mode in ("L", "RGB", "RGBA"): + roundtrip(hopper(mode)) + + # Test 1 dimension for an L mode image + roundtrip(Image.new("L", (10, 1))) + + +def test_write16(tmp_path): + test_file = "Tests/images/hopper16.rgb" + + with Image.open(test_file) as im: + out = str(tmp_path / "temp.sgi") + im.save(out, format="sgi", bpc=2) + + assert_image_equal_tofile(im, out) + + +def test_unsupported_mode(tmp_path): + im = hopper("LA") + out = str(tmp_path / "temp.sgi") + + with pytest.raises(ValueError): + im.save(out, format="sgi") diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index b54b92e04bb..0e3b705a295 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,119 +1,160 @@ -from helper import unittest, PillowTestCase, hopper +import tempfile +import warnings +from io import BytesIO -from PIL import Image -from PIL import ImageSequence -from PIL import SpiderImagePlugin +import pytest -import tempfile +from PIL import Image, ImageSequence, SpiderImagePlugin + +from .helper import assert_image_equal_tofile, hopper, is_pypy TEST_FILE = "Tests/images/hopper.spider" -class TestImageSpider(PillowTestCase): +def test_sanity(): + with Image.open(TEST_FILE) as im: + im.load() + assert im.mode == "F" + assert im.size == (128, 128) + assert im.format == "SPIDER" + - def test_sanity(self): +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): im = Image.open(TEST_FILE) im.load() - self.assertEqual(im.mode, "F") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "SPIDER") - def test_save(self): - # Arrange - temp = self.tempfile('temp.spider') - im = hopper() + pytest.warns(ResourceWarning, open) - # Act - im.save(temp, "SPIDER") - # Assert - im2 = Image.open(temp) - self.assertEqual(im2.mode, "F") - self.assertEqual(im2.size, (128, 128)) - self.assertEqual(im2.format, "SPIDER") +def test_closed_file(): + with warnings.catch_warnings(): + im = Image.open(TEST_FILE) + im.load() + im.close() - def test_tempfile(self): - # Arrange - im = hopper() - # Act - with tempfile.TemporaryFile() as fp: - im.save(fp, "SPIDER") +def test_context_manager(): + with warnings.catch_warnings(): + with Image.open(TEST_FILE) as im: + im.load() - # Assert - fp.seek(0) - reloaded = Image.open(fp) - self.assertEqual(reloaded.mode, "F") - self.assertEqual(reloaded.size, (128, 128)) - self.assertEqual(reloaded.format, "SPIDER") - def test_isSpiderImage(self): - self.assertTrue(SpiderImagePlugin.isSpiderImage(TEST_FILE)) +def test_save(tmp_path): + # Arrange + temp = str(tmp_path / "temp.spider") + im = hopper() - def test_tell(self): - # Arrange - im = Image.open(TEST_FILE) + # Act + im.save(temp, "SPIDER") - # Act - index = im.tell() + # Assert + with Image.open(temp) as im2: + assert im2.mode == "F" + assert im2.size == (128, 128) + assert im2.format == "SPIDER" + + +def test_tempfile(): + # Arrange + im = hopper() + + # Act + with tempfile.TemporaryFile() as fp: + im.save(fp, "SPIDER") # Assert - self.assertEqual(index, 0) + fp.seek(0) + with Image.open(fp) as reloaded: + assert reloaded.mode == "F" + assert reloaded.size == (128, 128) + assert reloaded.format == "SPIDER" + + +def test_is_spider_image(): + assert SpiderImagePlugin.isSpiderImage(TEST_FILE) - def test_n_frames(self): - im = Image.open(TEST_FILE) - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) - def test_loadImageSeries(self): - # Arrange - not_spider_file = "Tests/images/hopper.ppm" - file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"] +def test_tell(): + # Arrange + with Image.open(TEST_FILE) as im: # Act - img_list = SpiderImagePlugin.loadImageSeries(file_list) + index = im.tell() # Assert - self.assertEqual(len(img_list), 1) - self.assertIsInstance(img_list[0], Image.Image) - self.assertEqual(img_list[0].size, (128, 128)) + assert index == 0 - def test_loadImageSeries_no_input(self): - # Arrange - file_list = None - # Act - img_list = SpiderImagePlugin.loadImageSeries(file_list) +def test_n_frames(): + with Image.open(TEST_FILE) as im: + assert im.n_frames == 1 + assert not im.is_animated - # Assert - self.assertIsNone(img_list) - def test_isInt_not_a_number(self): - # Arrange - not_a_number = "a" +def test_load_image_series(): + # Arrange + not_spider_file = "Tests/images/hopper.ppm" + file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"] - # Act - ret = SpiderImagePlugin.isInt(not_a_number) + # Act + img_list = SpiderImagePlugin.loadImageSeries(file_list) - # Assert - self.assertEqual(ret, 0) + # Assert + assert len(img_list) == 1 + assert isinstance(img_list[0], Image.Image) + assert img_list[0].size == (128, 128) - def test_invalid_file(self): - invalid_file = "Tests/images/invalid.spider" - self.assertRaises(IOError, Image.open, invalid_file) +def test_load_image_series_no_input(): + # Arrange + file_list = None - def test_nonstack_file(self): - im = Image.open(TEST_FILE) + # Act + img_list = SpiderImagePlugin.loadImageSeries(file_list) - self.assertRaises(EOFError, im.seek, 0) + # Assert + assert img_list is None - def test_nonstack_dos(self): - im = Image.open(TEST_FILE) + +def test_is_int_not_a_number(): + # Arrange + not_a_number = "a" + + # Act + ret = SpiderImagePlugin.isInt(not_a_number) + + # Assert + assert ret == 0 + + +def test_invalid_file(): + invalid_file = "Tests/images/invalid.spider" + + with pytest.raises(OSError): + with Image.open(invalid_file): + pass + + +def test_nonstack_file(): + with Image.open(TEST_FILE) as im: + with pytest.raises(EOFError): + im.seek(0) + + +def test_nonstack_dos(): + with Image.open(TEST_FILE) as im: for i, frame in enumerate(ImageSequence.Iterator(im)): - if i > 1: - self.fail("Non-stack DOS file test failed") + assert i <= 1, "Non-stack DOS file test failed" + +# for issue #4093 +def test_odd_size(): + data = BytesIO() + width = 100 + im = Image.new("F", (width, 64)) + im.save(data, format="SPIDER") -if __name__ == '__main__': - unittest.main() + data.seek(0) + assert_image_equal_tofile(im, data) diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 6bb6b98d47e..05c78c3161b 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -1,50 +1,48 @@ -from helper import unittest, PillowTestCase, hopper +import os -from PIL import Image, SunImagePlugin +import pytest -import os +from PIL import Image, SunImagePlugin -EXTRA_DIR = 'Tests/images/sunraster' +from .helper import assert_image_equal_tofile, assert_image_similar, hopper +EXTRA_DIR = "Tests/images/sunraster" -class TestFileSun(PillowTestCase): - def test_sanity(self): - # Arrange - # Created with ImageMagick: convert hopper.jpg hopper.ras - test_file = "Tests/images/hopper.ras" +def test_sanity(): + # Arrange + # Created with ImageMagick: convert hopper.jpg hopper.ras + test_file = "Tests/images/hopper.ras" - # Act - im = Image.open(test_file) + # Act + with Image.open(test_file) as im: # Assert - self.assertEqual(im.size, (128, 128)) - - self.assert_image_similar(im, hopper(), 5) # visually verified - - invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - SunImagePlugin.SunImageFile, invalid_file) - - def test_im1(self): - im = Image.open('Tests/images/sunraster.im1') - target = Image.open('Tests/images/sunraster.im1.png') - self.assert_image_equal(im, target) - - @unittest.skipIf(not os.path.exists(EXTRA_DIR), - "Extra image files not installed") - def test_others(self): - files = (os.path.join(EXTRA_DIR, f) for f in - os.listdir(EXTRA_DIR) if os.path.splitext(f)[1] - in ('.sun', '.SUN', '.ras')) - for path in files: - with Image.open(path) as im: - im.load() - self.assertIsInstance(im, SunImagePlugin.SunImageFile) - target_path = "%s.png" % os.path.splitext(path)[0] - # im.save(target_file) - with Image.open(target_path) as target: - self.assert_image_equal(im, target) - -if __name__ == '__main__': - unittest.main() + assert im.size == (128, 128) + + assert_image_similar(im, hopper(), 5) # visually verified + + invalid_file = "Tests/images/flower.jpg" + with pytest.raises(SyntaxError): + SunImagePlugin.SunImageFile(invalid_file) + + +def test_im1(): + with Image.open("Tests/images/sunraster.im1") as im: + assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png") + + +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +def test_others(): + files = ( + os.path.join(EXTRA_DIR, f) + for f in os.listdir(EXTRA_DIR) + if os.path.splitext(f)[1] in (".sun", ".SUN", ".ras") + ) + for path in files: + with Image.open(path) as im: + im.load() + assert isinstance(im, SunImagePlugin.SunImageFile) + assert_image_equal_tofile(im, f"{os.path.splitext(path)[0]}.png") diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 3dd075042cc..5daab47fca3 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,36 +1,44 @@ -from helper import unittest, PillowTestCase +import warnings -from PIL import Image, TarIO +import pytest -codecs = dir(Image.core) +from PIL import Image, TarIO, features + +from .helper import is_pypy # Sample tar archive TEST_TAR_FILE = "Tests/images/hopper.tar" -class TestFileTar(PillowTestCase): +def test_sanity(): + for codec, test_path, format in [ + ["zlib", "hopper.png", "PNG"], + ["jpg", "hopper.jpg", "JPEG"], + ]: + if features.check(codec): + with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: + with Image.open(tar) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == format + + +@pytest.mark.skipif(is_pypy(), reason="Requires CPython") +def test_unclosed_file(): + def open(): + TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") - def setUp(self): - if "zip_decoder" not in codecs and "jpeg_decoder" not in codecs: - self.skipTest("neither jpeg nor zip support not available") + pytest.warns(ResourceWarning, open) - def test_sanity(self): - if "zip_decoder" in codecs: - tar = TarIO.TarIO(TEST_TAR_FILE, 'hopper.png') - im = Image.open(tar) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "PNG") - if "jpeg_decoder" in codecs: - tar = TarIO.TarIO(TEST_TAR_FILE, 'hopper.jpg') - im = Image.open(tar) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "JPEG") +def test_close(): + with warnings.catch_warnings(): + tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") + tar.close() -if __name__ == '__main__': - unittest.main() +def test_contextmanager(): + with warnings.catch_warnings(): + with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): + pass diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index ef3acfe6549..7d8b5139aa9 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -1,68 +1,250 @@ -from helper import unittest, PillowTestCase +import os +from glob import glob +from itertools import product + +import pytest from PIL import Image +from .helper import assert_image_equal, assert_image_equal_tofile, hopper + +_TGA_DIR = os.path.join("Tests", "images", "tga") +_TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") + + +_MODES = ("L", "LA", "P", "RGB", "RGBA") +_ORIGINS = ("tl", "bl") + +_ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} + + +@pytest.mark.parametrize("mode", _MODES) +def test_sanity(mode, tmp_path): + def roundtrip(original_im): + out = str(tmp_path / "temp.tga") + + original_im.save(out, rle=rle) + with Image.open(out) as saved_im: + if rle: + assert saved_im.info["compression"] == original_im.info["compression"] + assert saved_im.info["orientation"] == original_im.info["orientation"] + if mode == "P": + assert saved_im.getpalette() == original_im.getpalette() + + assert_image_equal(saved_im, original_im) + + png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png")) + + for png_path in png_paths: + with Image.open(png_path) as reference_im: + assert reference_im.mode == mode + + path_no_ext = os.path.splitext(png_path)[0] + for origin, rle in product(_ORIGINS, (True, False)): + tga_path = "{}_{}_{}.tga".format( + path_no_ext, origin, "rle" if rle else "raw" + ) + + with Image.open(tga_path) as original_im: + assert original_im.format == "TGA" + assert original_im.get_format_mimetype() == "image/x-tga" + if rle: + assert original_im.info["compression"] == "tga_rle" + assert ( + original_im.info["orientation"] + == _ORIGIN_TO_ORIENTATION[origin] + ) + if mode == "P": + assert original_im.getpalette() == reference_im.getpalette() -class TestFileTga(PillowTestCase): + assert_image_equal(original_im, reference_im) - def test_id_field(self): - # tga file with id field - test_file = "Tests/images/tga_id_field.tga" + roundtrip(original_im) - # Act - im = Image.open(test_file) + +def test_palette_depth_16(tmp_path): + with Image.open("Tests/images/p_16.tga") as im: + assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png") + + out = str(tmp_path / "temp.png") + im.save(out) + with Image.open(out) as reloaded: + assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png") + + +def test_id_field(): + # tga file with id field + test_file = "Tests/images/tga_id_field.tga" + + # Act + with Image.open(test_file) as im: # Assert - self.assertEqual(im.size, (100, 100)) + assert im.size == (100, 100) + - def test_id_field_rle(self): - # tga file with id field - test_file = "Tests/images/rgb32rle.tga" +def test_id_field_rle(): + # tga file with id field + test_file = "Tests/images/rgb32rle.tga" - # Act - im = Image.open(test_file) + # Act + with Image.open(test_file) as im: # Assert - self.assertEqual(im.size, (199, 199)) + assert im.size == (199, 199) + - def test_save(self): - test_file = "Tests/images/tga_id_field.tga" - im = Image.open(test_file) +def test_cross_scan_line(): + with Image.open("Tests/images/cross_scan_line.tga") as im: + assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png") - test_file = self.tempfile("temp.tga") + with Image.open("Tests/images/cross_scan_line_truncated.tga") as im: + with pytest.raises(OSError): + im.load() + + +def test_save(tmp_path): + test_file = "Tests/images/tga_id_field.tga" + with Image.open(test_file) as im: + out = str(tmp_path / "temp.tga") # Save - im.save(test_file) - test_im = Image.open(test_file) - self.assertEqual(test_im.size, (100, 100)) + im.save(out) + with Image.open(out) as test_im: + assert test_im.size == (100, 100) + assert test_im.info["id_section"] == im.info["id_section"] # RGBA save - im.convert("RGBA").save(test_file) - test_im = Image.open(test_file) - self.assertEqual(test_im.size, (100, 100)) + im.convert("RGBA").save(out) + with Image.open(out) as test_im: + assert test_im.size == (100, 100) + + +def test_small_palette(tmp_path): + im = Image.new("P", (1, 1)) + colors = [0, 0, 0] + im.putpalette(colors) + + out = str(tmp_path / "temp.tga") + im.save(out) + + with Image.open(out) as reloaded: + assert reloaded.getpalette() == colors - # Unsupported mode save - self.assertRaises(IOError, lambda: im.convert("LA").save(test_file)) - def test_save_rle(self): - test_file = "Tests/images/rgb32rle.tga" - im = Image.open(test_file) +def test_save_wrong_mode(tmp_path): + im = hopper("PA") + out = str(tmp_path / "temp.tga") - test_file = self.tempfile("temp.tga") + with pytest.raises(OSError): + im.save(out) + + +def test_save_mapdepth(): + # This image has been manually hexedited from 200x32_p_bl_raw.tga + # to include an origin + test_file = "Tests/images/200x32_p_bl_raw_origin.tga" + with Image.open(test_file) as im: + assert_image_equal_tofile(im, "Tests/images/tga/common/200x32_p.png") + + +def test_save_id_section(tmp_path): + test_file = "Tests/images/rgb32rle.tga" + with Image.open(test_file) as im: + out = str(tmp_path / "temp.tga") + + # Check there is no id section + im.save(out) + with Image.open(out) as test_im: + assert "id_section" not in test_im.info + + # Save with custom id section + im.save(out, id_section=b"Test content") + with Image.open(out) as test_im: + assert test_im.info["id_section"] == b"Test content" + + # Save with custom id section greater than 255 characters + id_section = b"Test content" * 25 + pytest.warns(UserWarning, lambda: im.save(out, id_section=id_section)) + with Image.open(out) as test_im: + assert test_im.info["id_section"] == id_section[:255] + + test_file = "Tests/images/tga_id_field.tga" + with Image.open(test_file) as im: + + # Save with no id section + im.save(out, id_section="") + with Image.open(out) as test_im: + assert "id_section" not in test_im.info + + +def test_save_orientation(tmp_path): + test_file = "Tests/images/rgb32rle.tga" + out = str(tmp_path / "temp.tga") + with Image.open(test_file) as im: + assert im.info["orientation"] == -1 + + im.save(out, orientation=1) + with Image.open(out) as test_im: + assert test_im.info["orientation"] == 1 + + +def test_horizontal_orientations(): + # These images have been manually hexedited to have the relevant orientations + with Image.open("Tests/images/rgb32rle_top_right.tga") as im: + assert im.load()[90, 90][:3] == (0, 0, 0) + + with Image.open("Tests/images/rgb32rle_bottom_right.tga") as im: + assert im.load()[90, 90][:3] == (0, 255, 0) + + +def test_save_rle(tmp_path): + test_file = "Tests/images/rgb32rle.tga" + with Image.open(test_file) as im: + assert im.info["compression"] == "tga_rle" + + out = str(tmp_path / "temp.tga") # Save - im.save(test_file) - test_im = Image.open(test_file) - self.assertEqual(test_im.size, (199, 199)) + im.save(out) + with Image.open(out) as test_im: + assert test_im.size == (199, 199) + assert test_im.info["compression"] == "tga_rle" - # RGBA save - im.convert("RGBA").save(test_file) - test_im = Image.open(test_file) - self.assertEqual(test_im.size, (199, 199)) + # Save without compression + im.save(out, compression=None) + with Image.open(out) as test_im: + assert "compression" not in test_im.info + + # RGBA save + im.convert("RGBA").save(out) + with Image.open(out) as test_im: + assert test_im.size == (199, 199) + + test_file = "Tests/images/tga_id_field.tga" + with Image.open(test_file) as im: + assert "compression" not in im.info + + # Save with compression + im.save(out, compression="tga_rle") + with Image.open(out) as test_im: + assert test_im.info["compression"] == "tga_rle" + + +def test_save_l_transparency(tmp_path): + # There are 559 transparent pixels in la.tga. + num_transparent = 559 + + in_file = "Tests/images/la.tga" + with Image.open(in_file) as im: + assert im.mode == "LA" + assert im.getchannel("A").getcolors()[0][0] == num_transparent - # Unsupported mode save - self.assertRaises(IOError, lambda: im.convert("LA").save(test_file)) + out = str(tmp_path / "temp.tga") + im.save(out) + with Image.open(out) as test_im: + assert test_im.mode == "LA" + assert test_im.getchannel("A").getcolors()[0][0] == num_transparent -if __name__ == '__main__': - unittest.main() + assert_image_equal(im, test_im) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 6edc94aed7d..4f3c8e39010 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,353 +1,596 @@ -import logging +import os +import warnings from io import BytesIO -import struct -import sys -from helper import unittest, PillowTestCase, hopper, py3 +import pytest -from PIL import Image, TiffImagePlugin -from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION, RESOLUTION_UNIT +from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError +from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION -logger = logging.getLogger(__name__) +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + assert_image_similar_tofile, + hopper, + is_pypy, + is_win32, +) +try: + from defusedxml import ElementTree +except ImportError: + ElementTree = None -class TestFileTiff(PillowTestCase): - def test_sanity(self): +class TestFileTiff: + def test_sanity(self, tmp_path): - filename = self.tempfile("temp.tif") + filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename) - im = Image.open(filename) - im.load() - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "TIFF") + with Image.open(filename) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "TIFF" hopper("1").save(filename) - im = Image.open(filename) + with Image.open(filename): + pass hopper("L").save(filename) - im = Image.open(filename) + with Image.open(filename): + pass hopper("P").save(filename) - im = Image.open(filename) + with Image.open(filename): + pass hopper("RGB").save(filename) - im = Image.open(filename) + with Image.open(filename): + pass hopper("I").save(filename) - im = Image.open(filename) + with Image.open(filename): + pass + + @pytest.mark.skipif(is_pypy(), reason="Requires CPython") + def test_unclosed_file(self): + def open(): + im = Image.open("Tests/images/multipage.tiff") + im.load() + + pytest.warns(ResourceWarning, open) + + def test_closed_file(self): + with warnings.catch_warnings(): + im = Image.open("Tests/images/multipage.tiff") + im.load() + im.close() + + def test_seek_after_close(self): + im = Image.open("Tests/images/multipage.tiff") + im.close() + + with pytest.raises(ValueError): + im.n_frames + with pytest.raises(ValueError): + im.seek(1) + + def test_context_manager(self): + with warnings.catch_warnings(): + with Image.open("Tests/images/multipage.tiff") as im: + im.load() + + @pytest.mark.parametrize( + "path, sizes", + ( + ("Tests/images/hopper.tif", ()), + ("Tests/images/child_ifd.tiff", (16, 8)), + ("Tests/images/child_ifd_jpeg.tiff", (20,)), + ), + ) + def test_get_child_images(self, path, sizes): + with Image.open(path) as im: + ims = im.get_child_images() + + assert len(ims) == len(sizes) + for i, im in enumerate(ims): + w = sizes[i] + expected = Image.new("RGB", (w, w), "#f00") + assert_image_similar(im, expected, 1) def test_mac_tiff(self): # Read RGBa images from macOS [@PIL136] filename = "Tests/images/pil136.tiff" - im = Image.open(filename) - - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (55, 43)) - self.assertEqual(im.tile, [('raw', (0, 0, 55, 43), 8, ('RGBa', 0, 1))]) - im.load() + with Image.open(filename) as im: + assert im.mode == "RGBA" + assert im.size == (55, 43) + assert im.tile == [("raw", (0, 0, 55, 43), 8, ("RGBa", 0, 1))] + im.load() - self.assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) + assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) - def test_wrong_bits_per_sample(self): - im = Image.open("Tests/images/tiff_wrong_bits_per_sample.tiff") + def test_bigtiff(self): + with Image.open("Tests/images/hopper_bigtiff.tif") as im: + assert_image_equal_tofile(im, "Tests/images/hopper.tif") - self.assertEqual(im.mode, "RGBA") - self.assertEqual(im.size, (52, 53)) - self.assertEqual(im.tile, [('raw', (0, 0, 52, 53), 160, ('RGBA', 0, 1))]) - im.load() + @pytest.mark.parametrize( + "file_name,mode,size,tile", + [ + ( + "tiff_wrong_bits_per_sample.tiff", + "RGBA", + (52, 53), + [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))], + ), + ( + "tiff_wrong_bits_per_sample_2.tiff", + "RGB", + (16, 16), + [("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))], + ), + ( + "tiff_wrong_bits_per_sample_3.tiff", + "RGBA", + (512, 256), + [("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))], + ), + ], + ) + def test_wrong_bits_per_sample(self, file_name, mode, size, tile): + with Image.open("Tests/images/" + file_name) as im: + assert im.mode == mode + assert im.size == size + assert im.tile == tile + im.load() def test_set_legacy_api(self): - with self.assertRaises(Exception): - ImageFileDirectory_v2.legacy_api = None + ifd = TiffImagePlugin.ImageFileDirectory_v2() + with pytest.raises(Exception) as e: + ifd.legacy_api = None + assert str(e.value) == "Not allowing setting of legacy api" def test_xyres_tiff(self): filename = "Tests/images/pil168.tif" - im = Image.open(filename) + with Image.open(filename) as im: - # legacy api - self.assertIsInstance(im.tag[X_RESOLUTION][0], tuple) - self.assertIsInstance(im.tag[Y_RESOLUTION][0], tuple) + # legacy api + assert isinstance(im.tag[X_RESOLUTION][0], tuple) + assert isinstance(im.tag[Y_RESOLUTION][0], tuple) - # v2 api - self.assertIsInstance(im.tag_v2[X_RESOLUTION], - TiffImagePlugin.IFDRational) - self.assertIsInstance(im.tag_v2[Y_RESOLUTION], - TiffImagePlugin.IFDRational) + # v2 api + assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) + assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) - self.assertEqual(im.info['dpi'], (72., 72.)) + assert im.info["dpi"] == (72.0, 72.0) def test_xyres_fallback_tiff(self): filename = "Tests/images/compression.tif" - im = Image.open(filename) + with Image.open(filename) as im: - # v2 api - self.assertIsInstance(im.tag_v2[X_RESOLUTION], - TiffImagePlugin.IFDRational) - self.assertIsInstance(im.tag_v2[Y_RESOLUTION], - TiffImagePlugin.IFDRational) - self.assertRaises(KeyError, - lambda: im.tag_v2[RESOLUTION_UNIT]) + # v2 api + assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) + assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) + with pytest.raises(KeyError): + im.tag_v2[RESOLUTION_UNIT] - # Legacy. - self.assertEqual(im.info['resolution'], (100., 100.)) - # Fallback "inch". - self.assertEqual(im.info['dpi'], (100., 100.)) + # Legacy. + assert im.info["resolution"] == (100.0, 100.0) + # Fallback "inch". + assert im.info["dpi"] == (100.0, 100.0) def test_int_resolution(self): filename = "Tests/images/pil168.tif" - im = Image.open(filename) - - # Try to read a file where X,Y_RESOLUTION are ints - im.tag_v2[X_RESOLUTION] = 71 - im.tag_v2[Y_RESOLUTION] = 71 - im._setup() - self.assertEqual(im.info['dpi'], (71., 71.)) + with Image.open(filename) as im: + + # Try to read a file where X,Y_RESOLUTION are ints + im.tag_v2[X_RESOLUTION] = 71 + im.tag_v2[Y_RESOLUTION] = 71 + im._setup() + assert im.info["dpi"] == (71.0, 71.0) + + @pytest.mark.parametrize( + "resolution_unit, dpi", + [(None, 72.8), (2, 72.8), (3, 184.912)], + ) + def test_load_float_dpi(self, resolution_unit, dpi): + with Image.open( + "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" + ) as im: + assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit + assert im.info["dpi"] == (dpi, dpi) + + def test_save_float_dpi(self, tmp_path): + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/hopper.tif") as im: + dpi = (72.2, 72.2) + im.save(outfile, dpi=dpi) + + with Image.open(outfile) as reloaded: + assert reloaded.info["dpi"] == dpi def test_save_setting_missing_resolution(self): b = BytesIO() - Image.open("Tests/images/10ct_32bit_128.tiff").save( - b, format="tiff", resolution=123.45) - im = Image.open(b) - self.assertEqual(float(im.tag_v2[X_RESOLUTION]), 123.45) - self.assertEqual(float(im.tag_v2[Y_RESOLUTION]), 123.45) + with Image.open("Tests/images/10ct_32bit_128.tiff") as im: + im.save(b, format="tiff", resolution=123.45) + with Image.open(b) as im: + assert im.tag_v2[X_RESOLUTION] == 123.45 + assert im.tag_v2[Y_RESOLUTION] == 123.45 def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - TiffImagePlugin.TiffImageFile, invalid_file) + with pytest.raises(SyntaxError): + TiffImagePlugin.TiffImageFile(invalid_file) TiffImagePlugin.PREFIXES.append(b"\xff\xd8\xff\xe0") - self.assertRaises(SyntaxError, - TiffImagePlugin.TiffImageFile, invalid_file) + with pytest.raises(SyntaxError): + TiffImagePlugin.TiffImageFile(invalid_file) TiffImagePlugin.PREFIXES.pop() def test_bad_exif(self): - i = Image.open('Tests/images/hopper_bad_exif.jpg') - try: - self.assert_warning(UserWarning, i._getexif) - except struct.error: - self.fail( - "Bad EXIF data passed incorrect values to _binary unpack") - - def test_save_rgba(self): + with Image.open("Tests/images/hopper_bad_exif.jpg") as i: + # Should not raise struct.error. + pytest.warns(UserWarning, i._getexif) + + def test_save_rgba(self, tmp_path): im = hopper("RGBA") - outfile = self.tempfile("temp.tif") + outfile = str(tmp_path / "temp.tif") im.save(outfile) - def test_save_unsupported_mode(self): + def test_save_unsupported_mode(self, tmp_path): im = hopper("HSV") - outfile = self.tempfile("temp.tif") - self.assertRaises(IOError, im.save, outfile) + outfile = str(tmp_path / "temp.tif") + with pytest.raises(OSError): + im.save(outfile) def test_little_endian(self): - im = Image.open('Tests/images/16bit.cropped.tif') - self.assertEqual(im.getpixel((0, 0)), 480) - self.assertEqual(im.mode, 'I;16') + with Image.open("Tests/images/16bit.cropped.tif") as im: + assert im.getpixel((0, 0)) == 480 + assert im.mode == "I;16" - b = im.tobytes() + b = im.tobytes() # Bytes are in image native order (little endian) - if py3: - self.assertEqual(b[0], ord(b'\xe0')) - self.assertEqual(b[1], ord(b'\x01')) - else: - self.assertEqual(b[0], b'\xe0') - self.assertEqual(b[1], b'\x01') + assert b[0] == ord(b"\xe0") + assert b[1] == ord(b"\x01") def test_big_endian(self): - im = Image.open('Tests/images/16bit.MM.cropped.tif') - self.assertEqual(im.getpixel((0, 0)), 480) - self.assertEqual(im.mode, 'I;16B') - - b = im.tobytes() + with Image.open("Tests/images/16bit.MM.cropped.tif") as im: + assert im.getpixel((0, 0)) == 480 + assert im.mode == "I;16B" + b = im.tobytes() # Bytes are in image native order (big endian) - if py3: - self.assertEqual(b[0], ord(b'\x01')) - self.assertEqual(b[1], ord(b'\xe0')) - else: - self.assertEqual(b[0], b'\x01') - self.assertEqual(b[1], b'\xe0') + assert b[0] == ord(b"\x01") + assert b[1] == ord(b"\xe0") + + def test_16bit_r(self): + with Image.open("Tests/images/16bit.r.tif") as im: + assert im.getpixel((0, 0)) == 480 + assert im.mode == "I;16" + + b = im.tobytes() + assert b[0] == ord(b"\xe0") + assert b[1] == ord(b"\x01") def test_16bit_s(self): - im = Image.open('Tests/images/16bit.s.tif') - im.load() - self.assertEqual(im.mode, 'I') - self.assertEqual(im.getpixel((0,0)),32767) - self.assertEqual(im.getpixel((0,1)),0) + with Image.open("Tests/images/16bit.s.tif") as im: + im.load() + assert im.mode == "I" + assert im.getpixel((0, 0)) == 32767 + assert im.getpixel((0, 1)) == 0 def test_12bit_rawmode(self): - """ Are we generating the same interpretation - of the image as Imagemagick is? """ - - im = Image.open('Tests/images/12bit.cropped.tif') + """Are we generating the same interpretation + of the image as Imagemagick is?""" - # to make the target -- - # convert 12bit.cropped.tif -depth 16 tmp.tif - # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif - # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0, - # so we need to unshift so that the integer values are the same. + with Image.open("Tests/images/12bit.cropped.tif") as im: + # to make the target -- + # convert 12bit.cropped.tif -depth 16 tmp.tif + # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif + # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0, + # so we need to unshift so that the integer values are the same. - self.assert_image_equal_tofile(im, 'Tests/images/12in16bit.tif') + assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") def test_32bit_float(self): # Issue 614, specific 32-bit float format - path = 'Tests/images/10ct_32bit_128.tiff' - im = Image.open(path) - im.load() - - self.assertEqual(im.getpixel((0, 0)), -0.4526388943195343) - self.assertEqual( - im.getextrema(), (-3.140936851501465, 3.140684127807617)) - - def test_n_frames(self): - for path, n_frames in [ - ['Tests/images/multipage-lastframe.tif', 1], - ['Tests/images/multipage.tiff', 3] - ]: - # Test is_animated before n_frames - im = Image.open(path) - self.assertEqual(im.is_animated, n_frames != 1) + path = "Tests/images/10ct_32bit_128.tiff" + with Image.open(path) as im: + im.load() - # Test is_animated after n_frames - im = Image.open(path) - self.assertEqual(im.n_frames, n_frames) - self.assertEqual(im.is_animated, n_frames != 1) + assert im.getpixel((0, 0)) == -0.4526388943195343 + assert im.getextrema() == (-3.140936851501465, 3.140684127807617) + + def test_unknown_pixel_mode(self): + with pytest.raises(OSError): + with Image.open("Tests/images/hopper_unknown_pixel_mode.tif"): + pass + + @pytest.mark.parametrize( + "path, n_frames", + ( + ("Tests/images/multipage-lastframe.tif", 1), + ("Tests/images/multipage.tiff", 3), + ), + ) + def test_n_frames(self, path, n_frames): + with Image.open(path) as im: + assert im.n_frames == n_frames + assert im.is_animated == (n_frames != 1) def test_eoferror(self): - im = Image.open('Tests/images/multipage-lastframe.tif') - n_frames = im.n_frames + with Image.open("Tests/images/multipage-lastframe.tif") as im: + n_frames = im.n_frames - # Test seeking past the last frame - self.assertRaises(EOFError, im.seek, n_frames) - self.assertLess(im.tell(), n_frames) + # Test seeking past the last frame + with pytest.raises(EOFError): + im.seek(n_frames) + assert im.tell() < n_frames - # Test that seeking to the last frame does not raise an error - im.seek(n_frames-1) + # Test that seeking to the last frame does not raise an error + im.seek(n_frames - 1) def test_multipage(self): # issue #862 - im = Image.open('Tests/images/multipage.tiff') - # file is a multipage tiff: 10x10 green, 10x10 red, 20x20 blue + with Image.open("Tests/images/multipage.tiff") as im: + # file is a multipage tiff: 10x10 green, 10x10 red, 20x20 blue - im.seek(0) - self.assertEqual(im.size, (10, 10)) - self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 128, 0)) + im.seek(0) + assert im.size == (10, 10) + assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) - im.seek(1) - im.load() - self.assertEqual(im.size, (10, 10)) - self.assertEqual(im.convert('RGB').getpixel((0, 0)), (255, 0, 0)) + im.seek(1) + im.load() + assert im.size == (10, 10) + assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) - im.seek(2) - im.load() - self.assertEqual(im.size, (20, 20)) - self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 0, 255)) + im.seek(0) + im.load() + assert im.size == (10, 10) + assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) + + im.seek(2) + im.load() + assert im.size == (20, 20) + assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) def test_multipage_last_frame(self): - im = Image.open('Tests/images/multipage-lastframe.tif') - im.load() - self.assertEqual(im.size, (20, 20)) - self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 0, 255)) + with Image.open("Tests/images/multipage-lastframe.tif") as im: + im.load() + assert im.size == (20, 20) + assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) + + def test_frame_order(self): + # A frame can't progress to itself after reading + with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: + assert im.n_frames == 1 + + # A frame can't progress to a frame that has already been read + with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im: + assert im.n_frames == 2 + + # Frames don't have to be in sequence + with Image.open("Tests/images/multipage_out_of_order.tiff") as im: + assert im.n_frames == 3 def test___str__(self): filename = "Tests/images/pil136.tiff" - im = Image.open(filename) + with Image.open(filename) as im: - # Act - ret = str(im.ifd) + # Act + ret = str(im.ifd) - # Assert - self.assertIsInstance(ret, str) + # Assert + assert isinstance(ret, str) def test_dict(self): # Arrange filename = "Tests/images/pil136.tiff" - im = Image.open(filename) - - # v2 interface - v2_tags = {256: 55, 257: 43, 258: (8, 8, 8, 8), 259: 1, - 262: 2, 296: 2, 273: (8,), 338: (1,), 277: 4, - 279: (9460,), 282: 72.0, 283: 72.0, 284: 1} - self.assertEqual(dict(im.tag_v2), v2_tags) - - # legacy interface - legacy_tags = {256: (55,), 257: (43,), 258: (8, 8, 8, 8), 259: (1,), - 262: (2,), 296: (2,), 273: (8,), 338: (1,), 277: (4,), - 279: (9460,), 282: ((720000, 10000),), - 283: ((720000, 10000),), 284: (1,)} - self.assertEqual(dict(im.tag), legacy_tags) + with Image.open(filename) as im: + + # v2 interface + v2_tags = { + 256: 55, + 257: 43, + 258: (8, 8, 8, 8), + 259: 1, + 262: 2, + 296: 2, + 273: (8,), + 338: (1,), + 277: 4, + 279: (9460,), + 282: 72.0, + 283: 72.0, + 284: 1, + } + assert dict(im.tag_v2) == v2_tags + + # legacy interface + legacy_tags = { + 256: (55,), + 257: (43,), + 258: (8, 8, 8, 8), + 259: (1,), + 262: (2,), + 296: (2,), + 273: (8,), + 338: (1,), + 277: (4,), + 279: (9460,), + 282: ((720000, 10000),), + 283: ((720000, 10000),), + 284: (1,), + } + assert dict(im.tag) == legacy_tags def test__delitem__(self): filename = "Tests/images/pil136.tiff" - im = Image.open(filename) - len_before = len(dict(im.ifd)) - del im.ifd[256] - len_after = len(dict(im.ifd)) - self.assertEqual(len_before, len_after + 1) - - def test_load_byte(self): - for legacy_api in [False, True]: - ifd = TiffImagePlugin.ImageFileDirectory_v2() - data = b"abc" - ret = ifd.load_byte(data, legacy_api) - self.assertEqual(ret, b"abc") + with Image.open(filename) as im: + len_before = len(dict(im.ifd)) + del im.ifd[256] + len_after = len(dict(im.ifd)) + assert len_before == len_after + 1 + + @pytest.mark.parametrize("legacy_api", (False, True)) + def test_load_byte(self, legacy_api): + ifd = TiffImagePlugin.ImageFileDirectory_v2() + data = b"abc" + ret = ifd.load_byte(data, legacy_api) + assert ret == b"abc" def test_load_string(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc\0" ret = ifd.load_string(data, False) - self.assertEqual(ret, "abc") + assert ret == "abc" def test_load_float(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdabcd" ret = ifd.load_float(data, False) - self.assertEqual(ret, (1.6777999408082104e+22, 1.6777999408082104e+22)) + assert ret == (1.6777999408082104e22, 1.6777999408082104e22) def test_load_double(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdefghabcdefgh" ret = ifd.load_double(data, False) - self.assertEqual(ret, (8.540883223036124e+194, 8.540883223036124e+194)) + assert ret == (8.540883223036124e194, 8.540883223036124e194) + + def test_ifd_tag_type(self): + with Image.open("Tests/images/ifd_tag_type.tiff") as im: + assert 0x8825 in im.tag_v2 + + def test_exif(self, tmp_path): + def check_exif(exif): + assert sorted(exif.keys()) == [ + 256, + 257, + 258, + 259, + 262, + 271, + 272, + 273, + 277, + 278, + 279, + 282, + 283, + 284, + 296, + 297, + 305, + 339, + 700, + 34665, + 34853, + 50735, + ] + assert exif[256] == 640 + assert exif[271] == "FLIR" + + gps = exif.get_ifd(0x8825) + assert list(gps.keys()) == [0, 1, 2, 3, 4, 5, 6, 18] + assert gps[0] == b"\x03\x02\x00\x00" + assert gps[18] == "WGS-84" + + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/ifd_tag_type.tiff") as im: + exif = im.getexif() + check_exif(exif) + + im.save(outfile, exif=exif) + + outfile2 = str(tmp_path / "temp2.tif") + with Image.open(outfile) as im: + exif = im.getexif() + check_exif(exif) + + im.save(outfile2, exif=exif.tobytes()) + + with Image.open(outfile2) as im: + exif = im.getexif() + check_exif(exif) + + def test_modify_exif(self, tmp_path): + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/ifd_tag_type.tiff") as im: + exif = im.getexif() + exif[256] = 100 + + im.save(outfile, exif=exif) + + with Image.open(outfile) as im: + exif = im.getexif() + assert exif[256] == 100 + + def test_reload_exif_after_seek(self): + with Image.open("Tests/images/multipage.tiff") as im: + exif = im.getexif() + del exif[256] + im.seek(1) + + assert 256 in exif + + def test_exif_frames(self): + # Test that EXIF data can change across frames + with Image.open("Tests/images/g4-multi.tiff") as im: + assert im.getexif()[273] == (328, 815) + + im.seek(1) + assert im.getexif()[273] == (1408, 1907) + + @pytest.mark.parametrize("mode", ("1", "L")) + def test_photometric(self, mode, tmp_path): + filename = str(tmp_path / "temp.tif") + im = hopper(mode) + im.save(filename, tiffinfo={262: 0}) + with Image.open(filename) as reloaded: + assert reloaded.tag_v2[262] == 0 + assert_image_equal(im, reloaded) def test_seek(self): filename = "Tests/images/pil136.tiff" - im = Image.open(filename) - im.seek(0) - self.assertEqual(im.tell(), 0) + with Image.open(filename) as im: + im.seek(0) + assert im.tell() == 0 def test_seek_eof(self): filename = "Tests/images/pil136.tiff" - im = Image.open(filename) - self.assertEqual(im.tell(), 0) - self.assertRaises(EOFError, im.seek, -1) - self.assertRaises(EOFError, im.seek, 1) + with Image.open(filename) as im: + assert im.tell() == 0 + with pytest.raises(EOFError): + im.seek(-1) + with pytest.raises(EOFError): + im.seek(1) def test__limit_rational_int(self): from PIL.TiffImagePlugin import _limit_rational + value = 34 ret = _limit_rational(value, 65536) - self.assertEqual(ret, (34, 1)) + assert ret == (34, 1) def test__limit_rational_float(self): from PIL.TiffImagePlugin import _limit_rational + value = 22.3 ret = _limit_rational(value, 65536) - self.assertEqual(ret, (223, 10)) + assert ret == (223, 10) def test_4bit(self): test_file = "Tests/images/hopper_gray_4bpp.tif" original = hopper("L") - im = Image.open(test_file) - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.mode, "L") - self.assert_image_similar(im, original, 7.3) + with Image.open(test_file) as im: + assert im.size == (128, 128) + assert im.mode == "L" + assert_image_similar(im, original, 7.3) def test_gray_semibyte_per_pixel(self): test_files = ( @@ -358,7 +601,7 @@ def test_gray_semibyte_per_pixel(self): "Tests/images/tiff_gray_2_4_bpp/hopper2I.tif", "Tests/images/tiff_gray_2_4_bpp/hopper2R.tif", "Tests/images/tiff_gray_2_4_bpp/hopper2IR.tif", - ) + ), ), ( 7.3, # epsilon @@ -367,134 +610,272 @@ def test_gray_semibyte_per_pixel(self): "Tests/images/tiff_gray_2_4_bpp/hopper4I.tif", "Tests/images/tiff_gray_2_4_bpp/hopper4R.tif", "Tests/images/tiff_gray_2_4_bpp/hopper4IR.tif", - ) + ), ), ) original = hopper("L") for epsilon, group in test_files: - im = Image.open(group[0]) - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.mode, "L") - self.assert_image_similar(im, original, epsilon) - for file in group[1:]: - im2 = Image.open(file) - self.assertEqual(im2.size, (128, 128)) - self.assertEqual(im2.mode, "L") - self.assert_image_equal(im, im2) - - def test_with_underscores(self): - kwargs = {'resolution_unit': 'inch', - 'x_resolution': 72, - 'y_resolution': 36} - filename = self.tempfile("temp.tif") + with Image.open(group[0]) as im: + assert im.size == (128, 128) + assert im.mode == "L" + assert_image_similar(im, original, epsilon) + for file in group[1:]: + with Image.open(file) as im2: + assert im2.size == (128, 128) + assert im2.mode == "L" + assert_image_equal(im, im2) + + def test_with_underscores(self, tmp_path): + kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} + filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename, **kwargs) - from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION - im = Image.open(filename) + with Image.open(filename) as im: - # legacy interface - self.assertEqual(im.tag[X_RESOLUTION][0][0], 72) - self.assertEqual(im.tag[Y_RESOLUTION][0][0], 36) + # legacy interface + assert im.tag[X_RESOLUTION][0][0] == 72 + assert im.tag[Y_RESOLUTION][0][0] == 36 - # v2 interface - self.assertEqual(im.tag_v2[X_RESOLUTION], 72) - self.assertEqual(im.tag_v2[Y_RESOLUTION], 36) + # v2 interface + assert im.tag_v2[X_RESOLUTION] == 72 + assert im.tag_v2[Y_RESOLUTION] == 36 - - def test_roundtrip_tiff_uint16(self): + def test_roundtrip_tiff_uint16(self, tmp_path): # Test an image of all '0' values pixel_value = 0x1234 infile = "Tests/images/uint16_1_4660.tif" - im = Image.open(infile) - self.assertEqual(im.getpixel((0, 0)), pixel_value) + with Image.open(infile) as im: + assert im.getpixel((0, 0)) == pixel_value - tmpfile = self.tempfile("temp.tif") - im.save(tmpfile) + tmpfile = str(tmp_path / "temp.tif") + im.save(tmpfile) - reloaded = Image.open(tmpfile) + assert_image_equal_tofile(im, tmpfile) + + def test_strip_raw(self): + infile = "Tests/images/tiff_strip_raw.tif" + with Image.open(infile) as im: + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + + def test_strip_planar_raw(self): + # gdal_translate -of GTiff -co INTERLEAVE=BAND \ + # tiff_strip_raw.tif tiff_strip_planar_raw.tiff + infile = "Tests/images/tiff_strip_planar_raw.tif" + with Image.open(infile) as im: + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + + def test_strip_planar_raw_with_overviews(self): + # gdaladdo tiff_strip_planar_raw2.tif 2 4 8 16 + infile = "Tests/images/tiff_strip_planar_raw_with_overviews.tif" + with Image.open(infile) as im: + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + + def test_tiled_planar_raw(self): + # gdal_translate -of GTiff -co TILED=YES -co BLOCKXSIZE=32 \ + # -co BLOCKYSIZE=32 -co INTERLEAVE=BAND \ + # tiff_tiled_raw.tif tiff_tiled_planar_raw.tiff + infile = "Tests/images/tiff_tiled_planar_raw.tif" + with Image.open(infile) as im: + assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + + def test_planar_configuration_save(self, tmp_path): + infile = "Tests/images/tiff_tiled_planar_raw.tif" + with Image.open(infile) as im: + assert im._planar_configuration == 2 + + outfile = str(tmp_path / "temp.tif") + im.save(outfile) + + with Image.open(outfile) as reloaded: + assert_image_equal_tofile(reloaded, infile) + + @pytest.mark.parametrize("mode", ("P", "PA")) + def test_palette(self, mode, tmp_path): + outfile = str(tmp_path / "temp.tif") + + im = hopper(mode) + im.save(outfile) - self.assert_image_equal(im, reloaded) + with Image.open(outfile) as reloaded: + assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) def test_tiff_save_all(self): - import io - import os - - mp = io.BytesIO() + mp = BytesIO() with Image.open("Tests/images/multipage.tiff") as im: im.save(mp, format="tiff", save_all=True) mp.seek(0, os.SEEK_SET) with Image.open(mp) as im: - self.assertEqual(im.n_frames, 3) + assert im.n_frames == 3 # Test appending images - mp = io.BytesIO() - im = Image.new('RGB', (100, 100), '#f00') - ims = [Image.new('RGB', (100, 100), color) for color - in ['#0f0', '#00f']] + mp = BytesIO() + im = Image.new("RGB", (100, 100), "#f00") + ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] im.copy().save(mp, format="TIFF", save_all=True, append_images=ims) mp.seek(0, os.SEEK_SET) - reread = Image.open(mp) - self.assertEqual(reread.n_frames, 3) + with Image.open(mp) as reread: + assert reread.n_frames == 3 # Test appending using a generator - def imGenerator(ims): - for im in ims: - yield im - mp = io.BytesIO() - im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims)) + def im_generator(ims): + yield from ims - mp.seek(0, os.SEEK_SET) - reread = Image.open(mp) - self.assertEqual(reread.n_frames, 3) + mp = BytesIO() + im.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims)) + mp.seek(0, os.SEEK_SET) + with Image.open(mp) as reread: + assert reread.n_frames == 3 - def test_saving_icc_profile(self): + def test_saving_icc_profile(self, tmp_path): # Tests saving TIFF with icc_profile set. # At the time of writing this will only work for non-compressed tiffs # as libtiff does not support embedded ICC profiles, # ImageFile._save(..) however does. - im = Image.new('RGB', (1, 1)) - im.info['icc_profile'] = 'Dummy value' + im = Image.new("RGB", (1, 1)) + im.info["icc_profile"] = "Dummy value" # Try save-load round trip to make sure both handle icc_profile. - tmpfile = self.tempfile('temp.tif') - im.save(tmpfile, 'TIFF', compression='raw') - reloaded = Image.open(tmpfile) - - self.assertEqual(b'Dummy value', reloaded.info['icc_profile']) - - def test_close_on_load_exclusive(self): + tmpfile = str(tmp_path / "temp.tif") + im.save(tmpfile, "TIFF", compression="raw") + with Image.open(tmpfile) as reloaded: + assert b"Dummy value" == reloaded.info["icc_profile"] + + def test_save_icc_profile(self, tmp_path): + im = hopper() + assert "icc_profile" not in im.info + + outfile = str(tmp_path / "temp.tif") + icc_profile = b"Dummy value" + im.save(outfile, icc_profile=icc_profile) + + with Image.open(outfile) as reloaded: + assert reloaded.info["icc_profile"] == icc_profile + + def test_save_bmp_compression(self, tmp_path): + with Image.open("Tests/images/hopper.bmp") as im: + assert im.info["compression"] == 0 + + outfile = str(tmp_path / "temp.tif") + im.save(outfile) + + def test_discard_icc_profile(self, tmp_path): + outfile = str(tmp_path / "temp.tif") + + with Image.open("Tests/images/icc_profile.png") as im: + assert "icc_profile" in im.info + + im.save(outfile, icc_profile=None) + + with Image.open(outfile) as reloaded: + assert "icc_profile" not in reloaded.info + + def test_getxmp(self): + with Image.open("Tests/images/lab.tif") as im: + if ElementTree is None: + with pytest.warns(UserWarning): + assert im.getxmp() == {} + else: + xmp = im.getxmp() + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description[0]["format"] == "image/tiff" + assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] + + def test_get_photoshop_blocks(self): + with Image.open("Tests/images/lab.tif") as im: + assert list(im.get_photoshop_blocks().keys()) == [ + 1061, + 1002, + 1005, + 1062, + 1037, + 1049, + 1011, + 1034, + 10000, + 1013, + 1016, + 1032, + 1054, + 1050, + 1064, + 1041, + 1044, + 1036, + 1057, + 4000, + 4001, + ] + + def test_close_on_load_exclusive(self, tmp_path): # similar to test_fd_leak, but runs on unixlike os - tmpfile = self.tempfile("temp.tif") + tmpfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/uint16_1_4660.tif") as im: im.save(tmpfile) im = Image.open(tmpfile) fp = im.fp - self.assertFalse(fp.closed) + assert not fp.closed im.load() - self.assertTrue(fp.closed) + assert fp.closed - def test_close_on_load_nonexclusive(self): - tmpfile = self.tempfile("temp.tif") + def test_close_on_load_nonexclusive(self, tmp_path): + tmpfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/uint16_1_4660.tif") as im: im.save(tmpfile) - with open(tmpfile, 'rb') as f: + with open(tmpfile, "rb") as f: im = Image.open(f) fp = im.fp - self.assertFalse(fp.closed) + assert not fp.closed im.load() - self.assertFalse(fp.closed) - -@unittest.skipUnless(sys.platform.startswith('win32'), "Windows only") -class TestFileTiffW32(PillowTestCase): - def test_fd_leak(self): - tmpfile = self.tempfile("temp.tif") - import os + assert not fp.closed + + # Ignore this UserWarning which triggers for four tags: + # "Possibly corrupt EXIF data. Expecting to read 50404352 bytes but..." + @pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") + # Ignore this UserWarning: + @pytest.mark.filterwarnings("ignore:Truncated File Read") + @pytest.mark.skipif( + not os.path.exists("Tests/images/string_dimension.tiff"), + reason="Extra image files not installed", + ) + def test_string_dimension(self): + # Assert that an error is raised if one of the dimensions is a string + with Image.open("Tests/images/string_dimension.tiff") as im: + with pytest.raises(OSError): + im.load() + + @pytest.mark.timeout(6) + @pytest.mark.filterwarnings("ignore:Truncated File Read") + def test_timeout(self): + with Image.open("Tests/images/timeout-6646305047838720") as im: + ImageFile.LOAD_TRUNCATED_IMAGES = True + im.load() + ImageFile.LOAD_TRUNCATED_IMAGES = False + + @pytest.mark.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: + def test_fd_leak(self, tmp_path): + tmpfile = str(tmp_path / "temp.tif") # this is an mmaped file. with Image.open("Tests/images/uint16_1_4660.tif") as im: @@ -502,10 +883,11 @@ def test_fd_leak(self): im = Image.open(tmpfile) fp = im.fp - self.assertFalse(fp.closed) - self.assertRaises(Exception, os.remove, tmpfile) + assert not fp.closed + with pytest.raises(OSError): + os.remove(tmpfile) im.load() - self.assertTrue(fp.closed) + assert fp.closed # this closes the mmap im.close() @@ -513,7 +895,3 @@ def test_fd_leak(self): # this should not fail, as load should have closed the file pointer, # and close should have closed the mmap os.remove(tmpfile) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index bb5768046e1..d38c1c523ea 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,256 +1,421 @@ import io import struct -from helper import unittest, PillowTestCase, hopper +import pytest from PIL import Image, TiffImagePlugin, TiffTags -from PIL.TiffImagePlugin import _limit_rational, IFDRational - -tag_ids = {info.name: info.value for info in TiffTags.TAGS_V2.values()} - - -class TestFileTiffMetadata(PillowTestCase): - - def test_rt_metadata(self): - """ Test writing arbitrary metadata into the tiff image directory - Use case is ImageJ private tags, one numeric, one arbitrary - data. https://github.com/python-pillow/Pillow/issues/291 - """ - - img = hopper() - - # Behaviour change: re #1416 - # Pre ifd rewrite, ImageJMetaData was being written as a string(2), - # Post ifd rewrite, it's defined as arbitrary bytes(7). It should - # roundtrip with the actual bytes, rather than stripped text - # of the premerge tests. - # - # For text items, we still have to decode('ascii','replace') because - # the tiff file format can't take 8 bit bytes in that field. - - basetextdata = "This is some arbitrary metadata for a text field" - bindata = basetextdata.encode('ascii') + b" \xff" - textdata = basetextdata + " " + chr(255) - reloaded_textdata = basetextdata + " ?" - floatdata = 12.345 - doubledata = 67.89 - info = TiffImagePlugin.ImageFileDirectory() - - ImageJMetaData = tag_ids['ImageJMetaData'] - ImageJMetaDataByteCounts = tag_ids['ImageJMetaDataByteCounts'] - ImageDescription = tag_ids['ImageDescription'] - - info[ImageJMetaDataByteCounts] = len(bindata) - info[ImageJMetaData] = bindata - info[tag_ids['RollAngle']] = floatdata - info.tagtype[tag_ids['RollAngle']] = 11 - info[tag_ids['YawAngle']] = doubledata - info.tagtype[tag_ids['YawAngle']] = 12 - - info[ImageDescription] = textdata - - f = self.tempfile("temp.tif") - - img.save(f, tiffinfo=info) - - loaded = Image.open(f) - - self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (len(bindata),)) - self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], (len(bindata),)) - - self.assertEqual(loaded.tag[ImageJMetaData], bindata) - self.assertEqual(loaded.tag_v2[ImageJMetaData], bindata) - - self.assertEqual(loaded.tag[ImageDescription], (reloaded_textdata,)) - self.assertEqual(loaded.tag_v2[ImageDescription], reloaded_textdata) - - loaded_float = loaded.tag[tag_ids['RollAngle']][0] - self.assertAlmostEqual(loaded_float, floatdata, places=5) - loaded_double = loaded.tag[tag_ids['YawAngle']][0] - self.assertAlmostEqual(loaded_double, doubledata) - - # check with 2 element ImageJMetaDataByteCounts, issue #2006 - - info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8) - img.save(f, tiffinfo=info) - loaded = Image.open(f) - - self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (8, len(bindata) - 8)) - self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], (8, len(bindata) - 8)) - - - def test_read_metadata(self): - img = Image.open('Tests/images/hopper_g4.tif') - - self.assertEqual({'YResolution': IFDRational(4294967295, 113653537), - 'PlanarConfiguration': 1, - 'BitsPerSample': (1,), - 'ImageLength': 128, - 'Compression': 4, - 'FillOrder': 1, - 'RowsPerStrip': 128, - 'ResolutionUnit': 3, - 'PhotometricInterpretation': 0, - 'PageNumber': (0, 1), - 'XResolution': IFDRational(4294967295, 113653537), - 'ImageWidth': 128, - 'Orientation': 1, - 'StripByteCounts': (1968,), - 'SamplesPerPixel': 1, - 'StripOffsets': (8,) - }, img.tag_v2.named()) - - self.assertEqual({'YResolution': ((4294967295, 113653537),), - 'PlanarConfiguration': (1,), - 'BitsPerSample': (1,), - 'ImageLength': (128,), - 'Compression': (4,), - 'FillOrder': (1,), - 'RowsPerStrip': (128,), - 'ResolutionUnit': (3,), - 'PhotometricInterpretation': (0,), - 'PageNumber': (0, 1), - 'XResolution': ((4294967295, 113653537),), - 'ImageWidth': (128,), - 'Orientation': (1,), - 'StripByteCounts': (1968,), - 'SamplesPerPixel': (1,), - 'StripOffsets': (8,) - }, img.tag.named()) - - def test_write_metadata(self): - """ Test metadata writing through the python code """ - img = Image.open('Tests/images/hopper.tif') - - f = self.tempfile('temp.tiff') +from PIL.TiffImagePlugin import IFDRational + +from .helper import assert_deep_equal, hopper + +TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()} + + +def test_rt_metadata(tmp_path): + """Test writing arbitrary metadata into the tiff image directory + Use case is ImageJ private tags, one numeric, one arbitrary + data. https://github.com/python-pillow/Pillow/issues/291 + """ + + img = hopper() + + # Behaviour change: re #1416 + # Pre ifd rewrite, ImageJMetaData was being written as a string(2), + # Post ifd rewrite, it's defined as arbitrary bytes(7). It should + # roundtrip with the actual bytes, rather than stripped text + # of the premerge tests. + # + # For text items, we still have to decode('ascii','replace') because + # the tiff file format can't take 8 bit bytes in that field. + + base_text_data = "This is some arbitrary metadata for a text field" + bin_data = base_text_data.encode("ascii") + b" \xff" + text_data = base_text_data + " " + chr(255) + reloaded_text_data = base_text_data + " ?" + float_data = 12.345 + double_data = 67.89 + info = TiffImagePlugin.ImageFileDirectory() + + ImageJMetaData = TAG_IDS["ImageJMetaData"] + ImageJMetaDataByteCounts = TAG_IDS["ImageJMetaDataByteCounts"] + ImageDescription = TAG_IDS["ImageDescription"] + + info[ImageJMetaDataByteCounts] = len(bin_data) + info[ImageJMetaData] = bin_data + info[TAG_IDS["RollAngle"]] = float_data + info.tagtype[TAG_IDS["RollAngle"]] = 11 + info[TAG_IDS["YawAngle"]] = double_data + info.tagtype[TAG_IDS["YawAngle"]] = 12 + + info[ImageDescription] = text_data + + f = str(tmp_path / "temp.tif") + + img.save(f, tiffinfo=info) + + with Image.open(f) as loaded: + + assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) + assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),) + + assert loaded.tag[ImageJMetaData] == bin_data + assert loaded.tag_v2[ImageJMetaData] == bin_data + + assert loaded.tag[ImageDescription] == (reloaded_text_data,) + assert loaded.tag_v2[ImageDescription] == reloaded_text_data + + loaded_float = loaded.tag[TAG_IDS["RollAngle"]][0] + assert round(abs(loaded_float - float_data), 5) == 0 + loaded_double = loaded.tag[TAG_IDS["YawAngle"]][0] + assert round(abs(loaded_double - double_data), 7) == 0 + + # check with 2 element ImageJMetaDataByteCounts, issue #2006 + + info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8) + img.save(f, tiffinfo=info) + with Image.open(f) as loaded: + + assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) + assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) + + +def test_read_metadata(): + with Image.open("Tests/images/hopper_g4.tif") as img: + + assert { + "YResolution": IFDRational(4294967295, 113653537), + "PlanarConfiguration": 1, + "BitsPerSample": (1,), + "ImageLength": 128, + "Compression": 4, + "FillOrder": 1, + "RowsPerStrip": 128, + "ResolutionUnit": 3, + "PhotometricInterpretation": 0, + "PageNumber": (0, 1), + "XResolution": IFDRational(4294967295, 113653537), + "ImageWidth": 128, + "Orientation": 1, + "StripByteCounts": (1968,), + "SamplesPerPixel": 1, + "StripOffsets": (8,), + } == img.tag_v2.named() + + assert { + "YResolution": ((4294967295, 113653537),), + "PlanarConfiguration": (1,), + "BitsPerSample": (1,), + "ImageLength": (128,), + "Compression": (4,), + "FillOrder": (1,), + "RowsPerStrip": (128,), + "ResolutionUnit": (3,), + "PhotometricInterpretation": (0,), + "PageNumber": (0, 1), + "XResolution": ((4294967295, 113653537),), + "ImageWidth": (128,), + "Orientation": (1,), + "StripByteCounts": (1968,), + "SamplesPerPixel": (1,), + "StripOffsets": (8,), + } == img.tag.named() + + +def test_write_metadata(tmp_path): + """Test metadata writing through the python code""" + with Image.open("Tests/images/hopper.tif") as img: + f = str(tmp_path / "temp.tiff") img.save(f, tiffinfo=img.tag) - loaded = Image.open(f) - original = img.tag_v2.named() + + with Image.open(f) as loaded: reloaded = loaded.tag_v2.named() - for k, v in original.items(): - if isinstance(v, IFDRational): - original[k] = IFDRational(*_limit_rational(v, 2**31)) - if isinstance(v, tuple) and isinstance(v[0], IFDRational): - original[k] = tuple([IFDRational( - *_limit_rational(elt, 2**31)) for elt in v]) - - ignored = ['StripByteCounts', 'RowsPerStrip', - 'PageNumber', 'StripOffsets'] - - for tag, value in reloaded.items(): - if tag in ignored: - continue - if (isinstance(original[tag], tuple) - and isinstance(original[tag][0], IFDRational)): - # Need to compare element by element in the tuple, - # not comparing tuples of object references - self.assert_deep_equal(original[tag], - value, - "%s didn't roundtrip, %s, %s" % - (tag, original[tag], value)) - else: - self.assertEqual(original[tag], - value, - "%s didn't roundtrip, %s, %s" % - (tag, original[tag], value)) - - for tag, value in original.items(): - if tag not in ignored: - self.assertEqual( - value, reloaded[tag], "%s didn't roundtrip" % tag) - - def test_no_duplicate_50741_tag(self): - self.assertEqual(tag_ids['MakerNoteSafety'], 50741) - self.assertEqual(tag_ids['BestQualityScale'], 50780) - - def test_empty_metadata(self): - f = io.BytesIO(b'II*\x00\x08\x00\x00\x00') - head = f.read(8) - info = TiffImagePlugin.ImageFileDirectory(head) - try: - self.assert_warning(UserWarning, info.load, f) - except struct.error: - self.fail("Should not be struct errors there.") - - def test_iccprofile(self): - # https://github.com/python-pillow/Pillow/issues/1462 - im = Image.open('Tests/images/hopper.iccprofile.tif') - out = self.tempfile('temp.tiff') + ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"] + + for tag, value in reloaded.items(): + if tag in ignored: + continue + if isinstance(original[tag], tuple) and isinstance( + original[tag][0], IFDRational + ): + # Need to compare element by element in the tuple, + # not comparing tuples of object references + assert_deep_equal( + original[tag], + value, + f"{tag} didn't roundtrip, {original[tag]}, {value}", + ) + else: + assert ( + original[tag] == value + ), f"{tag} didn't roundtrip, {original[tag]}, {value}" + + for tag, value in original.items(): + if tag not in ignored: + assert value == reloaded[tag], f"{tag} didn't roundtrip" + + +def test_change_stripbytecounts_tag_type(tmp_path): + out = str(tmp_path / "temp.tiff") + with Image.open("Tests/images/hopper.tif") as im: + info = im.tag_v2 + + # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT + im = im.resize((500, 500)) + + # STRIPBYTECOUNTS can be a SHORT or a LONG + info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT + + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG + + +def test_no_duplicate_50741_tag(): + assert TAG_IDS["MakerNoteSafety"] == 50741 + assert TAG_IDS["BestQualityScale"] == 50780 + + +def test_iptc(tmp_path): + out = str(tmp_path / "temp.tiff") + with Image.open("Tests/images/hopper.Lab.tif") as im: + im.save(out) + + +def test_writing_bytes_to_ascii(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + tag = TiffTags.TAGS_V2[271] + assert tag.type == TiffTags.ASCII + + info[271] = b"test" + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[271] == "test" + + +def test_undefined_zero(tmp_path): + # Check that the tag has not been changed since this test was created + tag = TiffTags.TAGS_V2[45059] + assert tag.type == TiffTags.UNDEFINED + assert tag.length == 0 + + info = TiffImagePlugin.ImageFileDirectory(b"II*\x00\x08\x00\x00\x00") + info[45059] = b"test" + + # Assert that the tag value does not change by setting it to itself + original = info[45059] + info[45059] = info[45059] + assert info[45059] == original + + +def test_empty_metadata(): + f = io.BytesIO(b"II*\x00\x08\x00\x00\x00") + head = f.read(8) + info = TiffImagePlugin.ImageFileDirectory(head) + # Should not raise struct.error. + pytest.warns(UserWarning, info.load, f) + + +def test_iccprofile(tmp_path): + # https://github.com/python-pillow/Pillow/issues/1462 + out = str(tmp_path / "temp.tiff") + with Image.open("Tests/images/hopper.iccprofile.tif") as im: im.save(out) - reloaded = Image.open(out) - self.assertNotIsInstance(im.info['icc_profile'], tuple) - self.assertEqual(im.info['icc_profile'], reloaded.info['icc_profile']) - def test_iccprofile_binary(self): - # https://github.com/python-pillow/Pillow/issues/1526 - # We should be able to load this, but probably won't be able to save it. + with Image.open(out) as reloaded: + assert not isinstance(im.info["icc_profile"], tuple) + assert im.info["icc_profile"] == reloaded.info["icc_profile"] + - im = Image.open('Tests/images/hopper.iccprofile_binary.tif') - self.assertEqual(im.tag_v2.tagtype[34675], 1) - self.assertTrue(im.info['icc_profile']) +def test_iccprofile_binary(): + # https://github.com/python-pillow/Pillow/issues/1526 + # We should be able to load this, + # but probably won't be able to save it. - def test_iccprofile_save_png(self): - im = Image.open('Tests/images/hopper.iccprofile.tif') - outfile = self.tempfile('temp.png') + with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: + assert im.tag_v2.tagtype[34675] == 1 + assert im.info["icc_profile"] + + +def test_iccprofile_save_png(tmp_path): + with Image.open("Tests/images/hopper.iccprofile.tif") as im: + outfile = str(tmp_path / "temp.png") im.save(outfile) - def test_iccprofile_binary_save_png(self): - im = Image.open('Tests/images/hopper.iccprofile_binary.tif') - outfile = self.tempfile('temp.png') + +def test_iccprofile_binary_save_png(tmp_path): + with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: + outfile = str(tmp_path / "temp.png") im.save(outfile) - def test_exif_div_zero(self): - im = hopper() - info = TiffImagePlugin.ImageFileDirectory_v2() - info[41988] = TiffImagePlugin.IFDRational(0, 0) - - out = self.tempfile('temp.tiff') - im.save(out, tiffinfo=info, compression='raw') - - reloaded = Image.open(out) - self.assertEqual(0, reloaded.tag_v2[41988].numerator) - self.assertEqual(0, reloaded.tag_v2[41988].denominator) - - def test_expty_values(self): - data = io.BytesIO( - b'II*\x00\x08\x00\x00\x00\x03\x00\x1a\x01\x05\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x1b\x01\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x98\x82\x02\x00\x07\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00a ' - b'text\x00\x00') - head = data.read(8) - info = TiffImagePlugin.ImageFileDirectory_v2(head) - info.load(data) - try: - info = dict(info) - except ValueError: - self.fail("Should not be struct value error there.") - self.assertIn(33432, info) - - def test_PhotoshopInfo(self): - im = Image.open('Tests/images/issue_2278.tif') - - self.assertIsInstance(im.tag_v2[34377], bytes) - out = self.tempfile('temp.tiff') + +def test_exif_div_zero(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + info[41988] = TiffImagePlugin.IFDRational(0, 0) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert 0 == reloaded.tag_v2[41988].numerator + assert 0 == reloaded.tag_v2[41988].denominator + + +def test_ifd_unsigned_rational(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + max_long = 2**32 - 1 + + # 4 bytes unsigned long + numerator = max_long + + info[41493] = TiffImagePlugin.IFDRational(numerator, 1) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert max_long == reloaded.tag_v2[41493].numerator + assert 1 == reloaded.tag_v2[41493].denominator + + # out of bounds of 4 byte unsigned long + numerator = max_long + 1 + + info[41493] = TiffImagePlugin.IFDRational(numerator, 1) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert max_long == reloaded.tag_v2[41493].numerator + assert 1 == reloaded.tag_v2[41493].denominator + + +def test_ifd_signed_rational(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + # pair of 4 byte signed longs + numerator = 2**31 - 1 + denominator = -(2**31) + + info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert numerator == reloaded.tag_v2[37380].numerator + assert denominator == reloaded.tag_v2[37380].denominator + + numerator = -(2**31) + denominator = 2**31 - 1 + + info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert numerator == reloaded.tag_v2[37380].numerator + assert denominator == reloaded.tag_v2[37380].denominator + + # out of bounds of 4 byte signed long + numerator = -(2**31) - 1 + denominator = 1 + + info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert 2**31 - 1 == reloaded.tag_v2[37380].numerator + assert -1 == reloaded.tag_v2[37380].denominator + + +def test_ifd_signed_long(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + info[37000] = -60000 + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info, compression="raw") + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[37000] == -60000 + + +def test_empty_values(): + data = io.BytesIO( + b"II*\x00\x08\x00\x00\x00\x03\x00\x1a\x01\x05\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x1b\x01\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x98\x82\x02\x00\x07\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00a " + b"text\x00\x00" + ) + head = data.read(8) + info = TiffImagePlugin.ImageFileDirectory_v2(head) + info.load(data) + # Should not raise ValueError. + info = dict(info) + assert 33432 in info + + +def test_photoshop_info(tmp_path): + with Image.open("Tests/images/issue_2278.tif") as im: + assert len(im.tag_v2[34377]) == 70 + assert isinstance(im.tag_v2[34377], bytes) + out = str(tmp_path / "temp.tiff") im.save(out) - reloaded = Image.open(out) - self.assertIsInstance(reloaded.tag_v2[34377], bytes) + with Image.open(out) as reloaded: + assert len(reloaded.tag_v2[34377]) == 70 + assert isinstance(reloaded.tag_v2[34377], bytes) + + +def test_too_many_entries(): + ifd = TiffImagePlugin.ImageFileDirectory_v2() + + # 277: ("SamplesPerPixel", SHORT, 1), + ifd._tagdata[277] = struct.pack("hh", 4, 4) + ifd.tagtype[277] = TiffTags.SHORT + + # Should not raise ValueError. + pytest.warns(UserWarning, lambda: ifd[277]) + + +def test_tag_group_data(): + base_ifd = TiffImagePlugin.ImageFileDirectory_v2() + interop_ifd = TiffImagePlugin.ImageFileDirectory_v2(group=40965) + for ifd in (base_ifd, interop_ifd): + ifd[2] = "test" + ifd[256] = 10 + + assert base_ifd.tagtype[256] == 4 + assert interop_ifd.tagtype[256] != base_ifd.tagtype[256] - def test_too_many_entries(self): - ifd = TiffImagePlugin.ImageFileDirectory_v2() + assert interop_ifd.tagtype[2] == 7 + assert base_ifd.tagtype[2] != interop_ifd.tagtype[256] - # 277: ("SamplesPerPixel", SHORT, 1), - ifd._tagdata[277] = struct.pack('hh', 4,4) - ifd.tagtype[277] = TiffTags.SHORT - try: - self.assert_warning(UserWarning, lambda: ifd[277]) - except ValueError: - self.fail("Invalid Metadata count should not cause a Value Error.") +def test_empty_subifd(tmp_path): + out = str(tmp_path / "temp.jpg") + im = hopper() + exif = im.getexif() + exif[TiffImagePlugin.EXIFIFD] = {} + im.save(out, exif=exif) -if __name__ == '__main__': - unittest.main() + with Image.open(out) as reloaded: + exif = reloaded.getexif() + assert exif.get_ifd(TiffImagePlugin.EXIFIFD) == {} diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py new file mode 100644 index 00000000000..4be46e9d673 --- /dev/null +++ b/Tests/test_file_wal.py @@ -0,0 +1,25 @@ +from PIL import WalImageFile + +from .helper import assert_image_equal_tofile + +TEST_FILE = "Tests/images/hopper.wal" + + +def test_open(): + with WalImageFile.open(TEST_FILE) as im: + assert im.format == "WAL" + assert im.format_description == "Quake2 Texture" + assert im.mode == "P" + assert im.size == (128, 128) + + assert isinstance(im, WalImageFile.WalImageFile) + + assert_image_equal_tofile(im, "Tests/images/hopper_wal.png") + + +def test_load(): + with WalImageFile.open(TEST_FILE) as im: + assert im.load()[0, 0] == 122 + + # Test again now that it has already been loaded once + assert im.load()[0, 0] == 122 diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 06e274d0a1e..f1bdc59cf34 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,27 +1,49 @@ -from helper import unittest, PillowTestCase, hopper +import io +import re +import sys +import warnings -from PIL import Image +import pytest + +from PIL import Image, WebPImagePlugin, features + +from .helper import ( + assert_image_equal, + assert_image_similar, + assert_image_similar_tofile, + hopper, + skip_unless_feature, +) try: from PIL import _webp + HAVE_WEBP = True except ImportError: HAVE_WEBP = False -class TestFileWebp(PillowTestCase): +class TestUnsupportedWebp: + def test_unsupported(self): + if HAVE_WEBP: + WebPImagePlugin.SUPPORTED = False + + file_path = "Tests/images/hopper.webp" + pytest.warns(UserWarning, lambda: pytest.raises(OSError, Image.open, file_path)) + + if HAVE_WEBP: + WebPImagePlugin.SUPPORTED = True - def setUp(self): - if not HAVE_WEBP: - self.skipTest('WebP support not installed') - return - # WebPAnimDecoder only returns RGBA or RGBX, never RGB - self.rgb_mode = "RGBX" if _webp.HAVE_WEBPANIM else "RGB" +@skip_unless_feature("webp") +class TestFileWebp: + def setup_method(self): + self.rgb_mode = "RGB" def test_version(self): _webp.WebPDecoderVersion() _webp.WebPDecoderBuggyAlpha() + assert re.search(r"\d+\.\d+\.\d+$", features.version_module("webp")) def test_read_rgb(self): """ @@ -29,93 +51,104 @@ def test_read_rgb(self): Does it have the bits we expect? """ - file_path = "Tests/images/hopper.webp" - image = Image.open(file_path) - - self.assertEqual(image.mode, self.rgb_mode) - self.assertEqual(image.size, (128, 128)) - self.assertEqual(image.format, "WEBP") - image.load() - image.getdata() - - # generated with: - # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm - target = Image.open('Tests/images/hopper_webp_bits.ppm') - target = target.convert(self.rgb_mode) - self.assert_image_similar(image, target, 20.0) - - def test_write_rgb(self): + with Image.open("Tests/images/hopper.webp") as image: + assert image.mode == self.rgb_mode + assert image.size == (128, 128) + assert image.format == "WEBP" + image.load() + image.getdata() + + # generated with: + # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm + assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0) + + def _roundtrip(self, tmp_path, mode, epsilon, args={}): + temp_file = str(tmp_path / "temp.webp") + + hopper(mode).save(temp_file, **args) + with Image.open(temp_file) as image: + assert image.mode == self.rgb_mode + assert image.size == (128, 128) + assert image.format == "WEBP" + image.load() + image.getdata() + + if mode == self.rgb_mode: + # generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm + assert_image_similar_tofile( + image, "Tests/images/hopper_webp_write.ppm", 12.0 + ) + + # This test asserts that the images are similar. If the average pixel + # difference between the two images is less than the epsilon value, + # then we're going to accept that it's a reasonable lossy version of + # the image. + target = hopper(mode) + if mode != self.rgb_mode: + target = target.convert(self.rgb_mode) + assert_image_similar(image, target, epsilon) + + def test_write_rgb(self, tmp_path): """ - Can we write a RGB mode file to webp without error. + Can we write a RGB mode file to webp without error? Does it have the bits we expect? """ - temp_file = self.tempfile("temp.webp") + self._roundtrip(tmp_path, self.rgb_mode, 12.5) - hopper(self.rgb_mode).save(temp_file) - image = Image.open(temp_file) + def test_write_method(self, tmp_path): + self._roundtrip(tmp_path, self.rgb_mode, 12.0, {"method": 6}) - self.assertEqual(image.mode, self.rgb_mode) - self.assertEqual(image.size, (128, 128)) - self.assertEqual(image.format, "WEBP") - image.load() - image.getdata() + buffer_no_args = io.BytesIO() + hopper().save(buffer_no_args, format="WEBP") - # If we're using the exact same version of WebP, this test should pass. - # but it doesn't if the WebP is generated on Ubuntu and tested on - # Fedora. + buffer_method = io.BytesIO() + hopper().save(buffer_method, format="WEBP", method=6) + assert buffer_no_args.getbuffer() != buffer_method.getbuffer() - # generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm - # target = Image.open('Tests/images/hopper_webp_write.ppm') - # self.assert_image_equal(image, target) + @skip_unless_feature("webp_anim") + def test_save_all(self, tmp_path): + temp_file = str(tmp_path / "temp.webp") + im = Image.new("RGB", (1, 1)) + im2 = Image.new("RGB", (1, 1), "#f00") + im.save(temp_file, save_all=True, append_images=[im2]) - # This test asserts that the images are similar. If the average pixel - # difference between the two images is less than the epsilon value, - # then we're going to accept that it's a reasonable lossy version of - # the image. The old lena images for WebP are showing ~16 on - # Ubuntu, the jpegs are showing ~18. - target = hopper(self.rgb_mode) - self.assert_image_similar(image, target, 12.0) + with Image.open(temp_file) as reloaded: + assert_image_equal(im, reloaded) - def test_write_unsupported_mode_L(self): + reloaded.seek(1) + assert_image_similar(im2, reloaded, 1) + + def test_icc_profile(self, tmp_path): + self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) + if _webp.HAVE_WEBPANIM: + self._roundtrip( + tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} + ) + + def test_write_unsupported_mode_L(self, tmp_path): """ Saving a black-and-white file to WebP format should work, and be similar to the original file. """ - temp_file = self.tempfile("temp.webp") - hopper("L").save(temp_file) - image = Image.open(temp_file) - - self.assertEqual(image.mode, self.rgb_mode) - self.assertEqual(image.size, (128, 128)) - self.assertEqual(image.format, "WEBP") - - image.load() - image.getdata() - target = hopper("L").convert(self.rgb_mode) + self._roundtrip(tmp_path, "L", 10.0) - self.assert_image_similar(image, target, 10.0) - - def test_write_unsupported_mode_P(self): + def test_write_unsupported_mode_P(self, tmp_path): """ Saving a palette-based file to WebP format should work, and be similar to the original file. """ - temp_file = self.tempfile("temp.webp") - hopper("P").save(temp_file) - image = Image.open(temp_file) - - self.assertEqual(image.mode, self.rgb_mode) - self.assertEqual(image.size, (128, 128)) - self.assertEqual(image.format, "WEBP") - - image.load() - image.getdata() - target = hopper("P").convert(self.rgb_mode) + self._roundtrip(tmp_path, "P", 50.0) - self.assert_image_similar(image, target, 50.0) + @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") + def test_write_encoding_error_message(self, tmp_path): + temp_file = str(tmp_path / "temp.webp") + im = Image.new("RGB", (15000, 15000)) + with pytest.raises(ValueError) as e: + im.save(temp_file, method=0) + assert str(e.value) == "encoding error 6" def test_WebPEncode_with_invalid_args(self): """ @@ -123,8 +156,10 @@ def test_WebPEncode_with_invalid_args(self): """ if _webp.HAVE_WEBPANIM: - self.assertRaises(TypeError, _webp.WebPAnimEncoder) - self.assertRaises(TypeError, _webp.WebPEncode) + with pytest.raises(TypeError): + _webp.WebPAnimEncoder() + with pytest.raises(TypeError): + _webp.WebPEncode() def test_WebPDecode_with_invalid_args(self): """ @@ -132,9 +167,68 @@ def test_WebPDecode_with_invalid_args(self): """ if _webp.HAVE_WEBPANIM: - self.assertRaises(TypeError, _webp.WebPAnimDecoder) - self.assertRaises(TypeError, _webp.WebPDecode) + with pytest.raises(TypeError): + _webp.WebPAnimDecoder() + with pytest.raises(TypeError): + _webp.WebPDecode() + def test_no_resource_warning(self, tmp_path): + file_path = "Tests/images/hopper.webp" + with Image.open(file_path) as image: + temp_file = str(tmp_path / "temp.webp") + with warnings.catch_warnings(): + image.save(temp_file) -if __name__ == '__main__': - unittest.main() + def test_file_pointer_could_be_reused(self): + file_path = "Tests/images/hopper.webp" + with open(file_path, "rb") as blob: + Image.open(blob).load() + Image.open(blob).load() + + @pytest.mark.parametrize( + "background", + (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)), + ) + @skip_unless_feature("webp_anim") + def test_invalid_background(self, background, tmp_path): + temp_file = str(tmp_path / "temp.webp") + im = hopper() + with pytest.raises(OSError): + im.save(temp_file, save_all=True, append_images=[im], background=background) + + @skip_unless_feature("webp_anim") + def test_background_from_gif(self, tmp_path): + # Save L mode GIF with background + with Image.open("Tests/images/no_palette_with_background.gif") as im: + out_webp = str(tmp_path / "temp.webp") + im.save(out_webp, save_all=True) + + # Save P mode GIF with background + with Image.open("Tests/images/chi.gif") as im: + original_value = im.convert("RGB").getpixel((1, 1)) + + # Save as WEBP + out_webp = str(tmp_path / "temp.webp") + im.save(out_webp, save_all=True) + + # Save as GIF + out_gif = str(tmp_path / "temp.gif") + with Image.open(out_webp) as im: + im.save(out_gif) + + with Image.open(out_gif) as reread: + reread_value = reread.convert("RGB").getpixel((1, 1)) + difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3)) + assert difference < 5 + + @skip_unless_feature("webp_anim") + def test_duration(self, tmp_path): + with Image.open("Tests/images/dispose_bgnd.gif") as im: + assert im.info["duration"] == 1000 + + out_webp = str(tmp_path / "temp.webp") + im.save(out_webp, save_all=True) + + with Image.open(out_webp) as reloaded: + reloaded.load() + assert reloaded.info["duration"] == 1000 diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 60a324d182c..dc82fb742b2 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -1,126 +1,120 @@ -from helper import unittest, PillowTestCase, hopper +import pytest from PIL import Image -try: - from PIL import _webp -except ImportError: - pass - # Skip in setUp() +from .helper import ( + assert_image_equal, + assert_image_similar, + assert_image_similar_tofile, + hopper, +) +_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") -class TestFileWebpAlpha(PillowTestCase): - def setUp(self): - try: - from PIL import _webp - except ImportError: - self.skipTest('WebP support not installed') +def setup_module(): + if _webp.WebPDecoderBuggyAlpha(): + pytest.skip("Buggy early version of WebP installed, not testing transparency") - if _webp.WebPDecoderBuggyAlpha(self): - self.skipTest("Buggy early version of WebP installed, " - "not testing transparency") - def test_read_rgba(self): - """ - Can we read an RGBA mode file without error? - Does it have the bits we expect? - """ +def test_read_rgba(): + """ + Can we read an RGBA mode file without error? + Does it have the bits we expect? + """ - # Generated with `cwebp transparent.png -o transparent.webp` - file_path = "Tests/images/transparent.webp" - image = Image.open(file_path) - - self.assertEqual(image.mode, "RGBA") - self.assertEqual(image.size, (200, 150)) - self.assertEqual(image.format, "WEBP") + # Generated with `cwebp transparent.png -o transparent.webp` + file_path = "Tests/images/transparent.webp" + with Image.open(file_path) as image: + assert image.mode == "RGBA" + assert image.size == (200, 150) + assert image.format == "WEBP" image.load() image.getdata() image.tobytes() - target = Image.open('Tests/images/transparent.png') - self.assert_image_similar(image, target, 20.0) + assert_image_similar_tofile(image, "Tests/images/transparent.png", 20.0) + - def test_write_lossless_rgb(self): - """ - Can we write an RGBA mode file with lossless compression without - error? Does it have the bits we expect? - """ +def test_write_lossless_rgb(tmp_path): + """ + Can we write an RGBA mode file with lossless compression without error? + Does it have the bits we expect? + """ - temp_file = self.tempfile("temp.webp") - # temp_file = "temp.webp" + temp_file = str(tmp_path / "temp.webp") + # temp_file = "temp.webp" - pil_image = hopper('RGBA') + pil_image = hopper("RGBA") - mask = Image.new("RGBA", (64, 64), (128, 128, 128, 128)) - # Add some partially transparent bits: - pil_image.paste(mask, (0, 0), mask) + mask = Image.new("RGBA", (64, 64), (128, 128, 128, 128)) + # Add some partially transparent bits: + pil_image.paste(mask, (0, 0), mask) - pil_image.save(temp_file, lossless=True) + pil_image.save(temp_file, lossless=True) - image = Image.open(temp_file) + with Image.open(temp_file) as image: image.load() - self.assertEqual(image.mode, "RGBA") - self.assertEqual(image.size, pil_image.size) - self.assertEqual(image.format, "WEBP") + assert image.mode == "RGBA" + assert image.size == pil_image.size + assert image.format == "WEBP" image.load() image.getdata() - self.assert_image_equal(image, pil_image) + assert_image_equal(image, pil_image) + - def test_write_rgba(self): - """ - Can we write a RGBA mode file to webp without error. - Does it have the bits we expect? - """ +def test_write_rgba(tmp_path): + """ + Can we write a RGBA mode file to WebP without error. + Does it have the bits we expect? + """ - temp_file = self.tempfile("temp.webp") + temp_file = str(tmp_path / "temp.webp") - pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) - pil_image.save(temp_file) + pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) + pil_image.save(temp_file) - if _webp.WebPDecoderBuggyAlpha(self): - return + if _webp.WebPDecoderBuggyAlpha(): + return - image = Image.open(temp_file) + with Image.open(temp_file) as image: image.load() - self.assertEqual(image.mode, "RGBA") - self.assertEqual(image.size, (10, 10)) - self.assertEqual(image.format, "WEBP") + assert image.mode == "RGBA" + assert image.size == (10, 10) + assert image.format == "WEBP" image.load() image.getdata() - # early versions of webp are known to produce higher deviations: + # Early versions of WebP are known to produce higher deviations: # deal with it - if _webp.WebPDecoderVersion(self) <= 0x201: - self.assert_image_similar(image, pil_image, 3.0) + if _webp.WebPDecoderVersion() <= 0x201: + assert_image_similar(image, pil_image, 3.0) else: - self.assert_image_similar(image, pil_image, 1.0) + assert_image_similar(image, pil_image, 1.0) - def test_write_unsupported_mode_PA(self): - """ - Saving a palette-based file with transparency to WebP format - should work, and be similar to the original file. - """ - temp_file = self.tempfile("temp.webp") - file_path = "Tests/images/transparent.gif" - Image.open(file_path).save(temp_file) - image = Image.open(temp_file) +def test_write_unsupported_mode_PA(tmp_path): + """ + Saving a palette-based file with transparency to WebP format + should work, and be similar to the original file. + """ - self.assertEqual(image.mode, "RGBA") - self.assertEqual(image.size, (200, 150)) - self.assertEqual(image.format, "WEBP") + temp_file = str(tmp_path / "temp.webp") + file_path = "Tests/images/transparent.gif" + with Image.open(file_path) as im: + im.save(temp_file) + with Image.open(temp_file) as image: + assert image.mode == "RGBA" + assert image.size == (200, 150) + assert image.format == "WEBP" image.load() image.getdata() - target = Image.open(file_path).convert("RGBA") - - self.assert_image_similar(image, target, 25.0) - + with Image.open(file_path) as im: + target = im.convert("RGBA") -if __name__ == '__main__': - unittest.main() + assert_image_similar(image, target, 25.0) diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index f98cde764d5..c621df0d99c 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -1,155 +1,174 @@ -from helper import unittest, PillowTestCase +import pytest +from packaging.version import parse as parse_version -from PIL import Image +from PIL import Image, features -try: - from PIL import _webp - HAVE_WEBP = True -except ImportError: - HAVE_WEBP = False +from .helper import ( + assert_image_equal, + assert_image_similar, + is_big_endian, + skip_unless_feature, +) +pytestmark = [ + skip_unless_feature("webp"), + skip_unless_feature("webp_anim"), +] -class TestFileWebpAnimation(PillowTestCase): - def setUp(self): - if not HAVE_WEBP: - self.skipTest('WebP support not installed') - return +def test_n_frames(): + """Ensure that WebP format sets n_frames and is_animated attributes correctly.""" - if not _webp.HAVE_WEBPANIM: - self.skipTest("WebP library does not contain animation support, " - "not testing animation") + with Image.open("Tests/images/hopper.webp") as im: + assert im.n_frames == 1 + assert not im.is_animated - def test_n_frames(self): - """ - Ensure that WebP format sets n_frames and is_animated - attributes correctly. - """ + with Image.open("Tests/images/iss634.webp") as im: + assert im.n_frames == 42 + assert im.is_animated - im = Image.open("Tests/images/hopper.webp") - self.assertEqual(im.n_frames, 1) - self.assertFalse(im.is_animated) - im = Image.open("Tests/images/iss634.webp") - self.assertEqual(im.n_frames, 42) - self.assertTrue(im.is_animated) +def test_write_animation_L(tmp_path): + """ + Convert an animated GIF to animated WebP, then compare the frame count, and first + and last frames to ensure they're visually similar. + """ - def test_write_animation_L(self): - """ - Convert an animated GIF to animated WebP, then compare the - frame count, and first and last frames to ensure they're - visually similar. - """ + with Image.open("Tests/images/iss634.gif") as orig: + assert orig.n_frames > 1 - orig = Image.open("Tests/images/iss634.gif") - self.assertGreater(orig.n_frames, 1) - - temp_file = self.tempfile("temp.webp") + temp_file = str(tmp_path / "temp.webp") orig.save(temp_file, save_all=True) - im = Image.open(temp_file) - self.assertEqual(im.n_frames, orig.n_frames) - - # Compare first and last frames to the original animated GIF - orig.load() - im.load() - self.assert_image_similar(im, orig.convert("RGBA"), 25.0) - orig.seek(orig.n_frames-1) - im.seek(im.n_frames-1) - orig.load() - im.load() - self.assert_image_similar(im, orig.convert("RGBA"), 25.0) - - def test_write_animation_RGB(self): - """ - Write an animated WebP from RGB frames, and ensure the frames - are visually similar to the originals. - """ - - def check(temp_file): - im = Image.open(temp_file) - self.assertEqual(im.n_frames, 2) + with Image.open(temp_file) as im: + assert im.n_frames == orig.n_frames + + # Compare first and last frames to the original animated GIF + orig.load() + im.load() + assert_image_similar(im, orig.convert("RGBA"), 32.9) + + if is_big_endian(): + webp = parse_version(features.version_module("webp")) + if webp < parse_version("1.2.2"): + pytest.skip("Fails with libwebp earlier than 1.2.2") + orig.seek(orig.n_frames - 1) + im.seek(im.n_frames - 1) + orig.load() + im.load() + assert_image_similar(im, orig.convert("RGBA"), 32.9) + + +def test_write_animation_RGB(tmp_path): + """ + Write an animated WebP from RGB frames, and ensure the frames + are visually similar to the originals. + """ + + def check(temp_file): + with Image.open(temp_file) as im: + assert im.n_frames == 2 # Compare first frame to original im.load() - self.assert_image_equal(im, frame1.convert("RGBA")) + assert_image_equal(im, frame1.convert("RGBA")) # Compare second frame to original + if is_big_endian(): + webp = parse_version(features.version_module("webp")) + if webp < parse_version("1.2.2"): + pytest.skip("Fails with libwebp earlier than 1.2.2") im.seek(1) im.load() - self.assert_image_equal(im, frame2.convert("RGBA")) - - frame1 = Image.open('Tests/images/anim_frame1.webp') - frame2 = Image.open('Tests/images/anim_frame2.webp') - - temp_file1 = self.tempfile("temp.webp") - frame1.copy().save(temp_file1, - save_all=True, append_images=[frame2], lossless=True) - check(temp_file1) - - # Tests appending using a generator - def imGenerator(ims): - for im in ims: - yield im - temp_file2 = self.tempfile("temp_generator.webp") - frame1.copy().save(temp_file2, - save_all=True, append_images=imGenerator([frame2]), lossless=True) - check(temp_file2) - - def test_timestamp_and_duration(self): - """ - Try passing a list of durations, and make sure the encoded - timestamps and durations are correct. - """ - - durations = [0, 10, 20, 30, 40] - temp_file = self.tempfile("temp.webp") - frame1 = Image.open('Tests/images/anim_frame1.webp') - frame2 = Image.open('Tests/images/anim_frame2.webp') - frame1.save(temp_file, save_all=True, - append_images=[frame2, frame1, frame2, frame1], - duration=durations) - - im = Image.open(temp_file) - self.assertEqual(im.n_frames, 5) - self.assertTrue(im.is_animated) + assert_image_equal(im, frame2.convert("RGBA")) + + with Image.open("Tests/images/anim_frame1.webp") as frame1: + with Image.open("Tests/images/anim_frame2.webp") as frame2: + temp_file1 = str(tmp_path / "temp.webp") + frame1.copy().save( + temp_file1, save_all=True, append_images=[frame2], lossless=True + ) + check(temp_file1) + + # Tests appending using a generator + def im_generator(ims): + yield from ims + + temp_file2 = str(tmp_path / "temp_generator.webp") + frame1.copy().save( + temp_file2, + save_all=True, + append_images=im_generator([frame2]), + lossless=True, + ) + check(temp_file2) + + +def test_timestamp_and_duration(tmp_path): + """ + Try passing a list of durations, and make sure the encoded + timestamps and durations are correct. + """ + + durations = [0, 10, 20, 30, 40] + temp_file = str(tmp_path / "temp.webp") + with Image.open("Tests/images/anim_frame1.webp") as frame1: + with Image.open("Tests/images/anim_frame2.webp") as frame2: + frame1.save( + temp_file, + save_all=True, + append_images=[frame2, frame1, frame2, frame1], + duration=durations, + ) + + with Image.open(temp_file) as im: + assert im.n_frames == 5 + assert im.is_animated # Check that timestamps and durations match original values specified ts = 0 for frame in range(im.n_frames): im.seek(frame) im.load() - self.assertEqual(im.info["duration"], durations[frame]) - self.assertEqual(im.info["timestamp"], ts) + assert im.info["duration"] == durations[frame] + assert im.info["timestamp"] == ts ts += durations[frame] - def test_seeking(self): - """ - Create an animated WebP file, and then try seeking through - frames in reverse-order, verifying the timestamps and durations - are correct. - """ - - dur = 33 - temp_file = self.tempfile("temp.webp") - frame1 = Image.open('Tests/images/anim_frame1.webp') - frame2 = Image.open('Tests/images/anim_frame2.webp') - frame1.save(temp_file, save_all=True, - append_images=[frame2, frame1, frame2, frame1], - duration=dur) - - im = Image.open(temp_file) - self.assertEqual(im.n_frames, 5) - self.assertTrue(im.is_animated) + +def test_seeking(tmp_path): + """ + Create an animated WebP file, and then try seeking through frames in reverse-order, + verifying the timestamps and durations are correct. + """ + + dur = 33 + temp_file = str(tmp_path / "temp.webp") + with Image.open("Tests/images/anim_frame1.webp") as frame1: + with Image.open("Tests/images/anim_frame2.webp") as frame2: + frame1.save( + temp_file, + save_all=True, + append_images=[frame2, frame1, frame2, frame1], + duration=dur, + ) + + with Image.open(temp_file) as im: + assert im.n_frames == 5 + assert im.is_animated # Traverse frames in reverse, checking timestamps and durations - ts = dur * (im.n_frames-1) + ts = dur * (im.n_frames - 1) for frame in reversed(range(im.n_frames)): im.seek(frame) im.load() - self.assertEqual(im.info["duration"], dur) - self.assertEqual(im.info["timestamp"], ts) + assert im.info["duration"] == dur + assert im.info["timestamp"] == ts ts -= dur -if __name__ == '__main__': - unittest.main() +def test_seek_errors(): + with Image.open("Tests/images/iss634.webp") as im: + with pytest.raises(EOFError): + im.seek(-1) + + with pytest.raises(EOFError): + im.seek(42) diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 10354c55fcd..2da443628d7 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -1,43 +1,28 @@ -from helper import unittest, PillowTestCase, hopper +import pytest from PIL import Image -try: - from PIL import _webp - HAVE_WEBP = True -except ImportError: - HAVE_WEBP = False +from .helper import assert_image_equal, hopper +_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") +RGB_MODE = "RGB" -class TestFileWebpLossless(PillowTestCase): - def setUp(self): - if not HAVE_WEBP: - self.skipTest('WebP support not installed') - return +def test_write_lossless_rgb(tmp_path): + if _webp.WebPDecoderVersion() < 0x0200: + pytest.skip("lossless not included") - if (_webp.WebPDecoderVersion() < 0x0200): - self.skipTest('lossless not included') + temp_file = str(tmp_path / "temp.webp") - # WebPAnimDecoder only returns RGBA or RGBX, never RGB - self.rgb_mode = "RGBX" if _webp.HAVE_WEBPANIM else "RGB" + hopper(RGB_MODE).save(temp_file, lossless=True) - def test_write_lossless_rgb(self): - temp_file = self.tempfile("temp.webp") - - hopper(self.rgb_mode).save(temp_file, lossless=True) - - image = Image.open(temp_file) + with Image.open(temp_file) as image: image.load() - self.assertEqual(image.mode, self.rgb_mode) - self.assertEqual(image.size, (128, 128)) - self.assertEqual(image.format, "WEBP") + assert image.mode == RGB_MODE + assert image.size == (128, 128) + assert image.format == "WEBP" image.load() image.getdata() - self.assert_image_equal(image, hopper(self.rgb_mode)) - - -if __name__ == '__main__': - unittest.main() + assert_image_equal(image, hopper(RGB_MODE)) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index c04443f467b..f77a245c035 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -1,139 +1,137 @@ -from helper import unittest, PillowTestCase +from io import BytesIO -from PIL import Image - -try: - from PIL import _webp - HAVE_WEBP = True -except ImportError: - HAVE_WEBP = False +import pytest +from PIL import Image -class TestFileWebpMetadata(PillowTestCase): +from .helper import mark_if_feature_version, skip_unless_feature - def setUp(self): - if not HAVE_WEBP: - self.skipTest('WebP support not installed') - return +pytestmark = [ + skip_unless_feature("webp"), + skip_unless_feature("webp_mux"), +] - if not _webp.HAVE_WEBPMUX: - self.skipTest('WebPMux support not installed') - def test_read_exif_metadata(self): +def test_read_exif_metadata(): - file_path = "Tests/images/flower.webp" - image = Image.open(file_path) + file_path = "Tests/images/flower.webp" + with Image.open(file_path) as image: - self.assertEqual(image.format, "WEBP") + assert image.format == "WEBP" exif_data = image.info.get("exif", None) - self.assertTrue(exif_data) + assert exif_data exif = image._getexif() - # camera make - self.assertEqual(exif[271], "Canon") - - jpeg_image = Image.open('Tests/images/flower.jpg') - expected_exif = jpeg_image.info['exif'] + # Camera make + assert exif[271] == "Canon" - self.assertEqual(exif_data, expected_exif) + with Image.open("Tests/images/flower.jpg") as jpeg_image: + expected_exif = jpeg_image.info["exif"] - def test_write_exif_metadata(self): - from io import BytesIO + assert exif_data == expected_exif - file_path = "Tests/images/flower.jpg" - image = Image.open(file_path) - expected_exif = image.info['exif'] - test_buffer = BytesIO() +def test_read_exif_metadata_without_prefix(): + with Image.open("Tests/images/flower2.webp") as im: + # Assert prefix is not present + assert im.info["exif"][:6] != b"Exif\x00\x00" - image.save(test_buffer, "webp", exif=expected_exif) + exif = im.getexif() + assert exif[305] == "Adobe Photoshop CS6 (Macintosh)" - test_buffer.seek(0) - webp_image = Image.open(test_buffer) - webp_exif = webp_image.info.get('exif', None) - self.assertTrue(webp_exif) - if webp_exif: - self.assertEqual( - webp_exif, expected_exif, "WebP EXIF didn't match") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) +def test_write_exif_metadata(): + file_path = "Tests/images/flower.jpg" + test_buffer = BytesIO() + with Image.open(file_path) as image: + expected_exif = image.info["exif"] - def test_read_icc_profile(self): + image.save(test_buffer, "webp", exif=expected_exif) - file_path = "Tests/images/flower2.webp" - image = Image.open(file_path) + test_buffer.seek(0) + with Image.open(test_buffer) as webp_image: + webp_exif = webp_image.info.get("exif", None) + assert webp_exif == expected_exif[6:], "WebP EXIF didn't match" - self.assertEqual(image.format, "WEBP") - self.assertTrue(image.info.get("icc_profile", None)) - icc = image.info['icc_profile'] +def test_read_icc_profile(): - jpeg_image = Image.open('Tests/images/flower2.jpg') - expected_icc = jpeg_image.info['icc_profile'] + file_path = "Tests/images/flower2.webp" + with Image.open(file_path) as image: - self.assertEqual(icc, expected_icc) + assert image.format == "WEBP" + assert image.info.get("icc_profile", None) - def test_write_icc_metadata(self): - from io import BytesIO + icc = image.info["icc_profile"] - file_path = "Tests/images/flower2.jpg" - image = Image.open(file_path) - expected_icc_profile = image.info['icc_profile'] + with Image.open("Tests/images/flower2.jpg") as jpeg_image: + expected_icc = jpeg_image.info["icc_profile"] - test_buffer = BytesIO() + assert icc == expected_icc - image.save(test_buffer, "webp", icc_profile=expected_icc_profile) - test_buffer.seek(0) - webp_image = Image.open(test_buffer) +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) +def test_write_icc_metadata(): + file_path = "Tests/images/flower2.jpg" + test_buffer = BytesIO() + with Image.open(file_path) as image: + expected_icc_profile = image.info["icc_profile"] - webp_icc_profile = webp_image.info.get('icc_profile', None) + image.save(test_buffer, "webp", icc_profile=expected_icc_profile) - self.assertTrue(webp_icc_profile) - if webp_icc_profile: - self.assertEqual( - webp_icc_profile, expected_icc_profile, - "Webp ICC didn't match") + test_buffer.seek(0) + with Image.open(test_buffer) as webp_image: + webp_icc_profile = webp_image.info.get("icc_profile", None) - def test_read_no_exif(self): - from io import BytesIO + assert webp_icc_profile + if webp_icc_profile: + assert webp_icc_profile == expected_icc_profile, "Webp ICC didn't match" - file_path = "Tests/images/flower.jpg" - image = Image.open(file_path) - self.assertIn('exif', image.info) - test_buffer = BytesIO() +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) +def test_read_no_exif(): + file_path = "Tests/images/flower.jpg" + test_buffer = BytesIO() + with Image.open(file_path) as image: + assert "exif" in image.info image.save(test_buffer, "webp") - test_buffer.seek(0) - webp_image = Image.open(test_buffer) - - self.assertFalse(webp_image._getexif()) - - def test_write_animated_metadata(self): - if not _webp.HAVE_WEBPANIM: - self.skipTest('WebP animation support not available') - - iccp_data = ''.encode('utf-8') - exif_data = ''.encode('utf-8') - xmp_data = ''.encode('utf-8') - - temp_file = self.tempfile("temp.webp") - frame1 = Image.open('Tests/images/anim_frame1.webp') - frame2 = Image.open('Tests/images/anim_frame2.webp') - frame1.save(temp_file, save_all=True, - append_images=[frame2, frame1, frame2], - icc_profile=iccp_data, exif=exif_data, xmp=xmp_data) - - image = Image.open(temp_file) - self.assertIn('icc_profile', image.info) - self.assertIn('exif', image.info) - self.assertIn('xmp', image.info) - self.assertEqual(iccp_data, image.info.get('icc_profile', None)) - self.assertEqual(exif_data, image.info.get('exif', None)) - self.assertEqual(xmp_data, image.info.get('xmp', None)) - - -if __name__ == '__main__': - unittest.main() + test_buffer.seek(0) + with Image.open(test_buffer) as webp_image: + assert not webp_image._getexif() + + +@skip_unless_feature("webp_anim") +def test_write_animated_metadata(tmp_path): + iccp_data = b"" + exif_data = b"" + xmp_data = b"" + + temp_file = str(tmp_path / "temp.webp") + with Image.open("Tests/images/anim_frame1.webp") as frame1: + with Image.open("Tests/images/anim_frame2.webp") as frame2: + frame1.save( + temp_file, + save_all=True, + append_images=[frame2, frame1, frame2], + icc_profile=iccp_data, + exif=exif_data, + xmp=xmp_data, + ) + + with Image.open(temp_file) as image: + assert "icc_profile" in image.info + assert "exif" in image.info + assert "xmp" in image.info + assert iccp_data == image.info.get("icc_profile", None) + assert exif_data == image.info.get("exif", None) + assert xmp_data == image.info.get("xmp", None) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 1a15a514ffa..439cb15bca9 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,57 +1,75 @@ -from helper import unittest, PillowTestCase, hopper +import pytest -from PIL import Image -from PIL import WmfImagePlugin +from PIL import Image, WmfImagePlugin +from .helper import assert_image_similar_tofile, hopper -class TestFileWmf(PillowTestCase): - def test_load_raw(self): +def test_load_raw(): - # Test basic EMF open and rendering - im = Image.open('Tests/images/drawing.emf') + # Test basic EMF open and rendering + with Image.open("Tests/images/drawing.emf") as im: if hasattr(Image.core, "drawwmf"): # Currently, support for WMF/EMF is Windows-only im.load() # Compare to reference rendering - imref = Image.open('Tests/images/drawing_emf_ref.png') - imref.load() - self.assert_image_similar(im, imref, 0) + assert_image_similar_tofile(im, "Tests/images/drawing_emf_ref.png", 0) - # Test basic WMF open and rendering - im = Image.open('Tests/images/drawing.wmf') + # Test basic WMF open and rendering + with Image.open("Tests/images/drawing.wmf") as im: if hasattr(Image.core, "drawwmf"): # Currently, support for WMF/EMF is Windows-only im.load() # Compare to reference rendering - imref = Image.open('Tests/images/drawing_wmf_ref.png') - imref.load() - self.assert_image_similar(im, imref, 2.0) + assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref.png", 2.0) - def test_register_handler(self): - class TestHandler: - methodCalled = False - def save(self, im, fp, filename): - self.methodCalled = True - handler = TestHandler() - WmfImagePlugin.register_handler(handler) +def test_load(): + with Image.open("Tests/images/drawing.emf") as im: + if hasattr(Image.core, "drawwmf"): + assert im.load()[0, 0] == (255, 255, 255) - im = hopper() - tmpfile = self.tempfile("temp.wmf") - im.save(tmpfile) - self.assertTrue(handler.methodCalled) - # Restore the state before this test - WmfImagePlugin.register_handler(None) +def test_register_handler(tmp_path): + class TestHandler: + methodCalled = False + + def save(self, im, fp, filename): + self.methodCalled = True + + handler = TestHandler() + original_handler = WmfImagePlugin._handler + WmfImagePlugin.register_handler(handler) + + im = hopper() + tmpfile = str(tmp_path / "temp.wmf") + im.save(tmpfile) + assert handler.methodCalled + + # Restore the state before this test + WmfImagePlugin.register_handler(original_handler) + - def test_save(self): - im = hopper() +def test_load_float_dpi(): + with Image.open("Tests/images/drawing.emf") as im: + assert im.info["dpi"] == 1423.7668161434979 - for ext in [".wmf", ".emf"]: - tmpfile = self.tempfile("temp"+ext) - self.assertRaises(IOError, im.save, tmpfile) +def test_load_set_dpi(): + with Image.open("Tests/images/drawing.wmf") as im: + assert im.size == (82, 82) -if __name__ == '__main__': - unittest.main() + if hasattr(Image.core, "drawwmf"): + im.load(144) + assert im.size == (164, 164) + + assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1) + + +@pytest.mark.parametrize("ext", (".wmf", ".emf")) +def test_save(ext, tmp_path): + im = hopper() + + tmpfile = str(tmp_path / ("temp" + ext)) + with pytest.raises(OSError): + im.save(tmpfile) diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 398dae98c11..9c54c675560 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -1,6 +1,10 @@ -from helper import unittest, PillowTestCase +from io import BytesIO -from PIL import Image +import pytest + +from PIL import Image, XbmImagePlugin + +from .helper import hopper PIL151 = b""" #define basic_width 32 @@ -26,41 +30,60 @@ """ -class TestFileXbm(PillowTestCase): +def test_pil151(): + with Image.open(BytesIO(PIL151)) as im: + im.load() + assert im.mode == "1" + assert im.size == (32, 32) - def test_pil151(self): - from io import BytesIO - im = Image.open(BytesIO(PIL151)) +def test_open(): + # Arrange + # Created with `convert hopper.png hopper.xbm` + filename = "Tests/images/hopper.xbm" + + # Act + with Image.open(filename) as im: + + # Assert + assert im.mode == "1" + assert im.size == (128, 128) - im.load() - self.assertEqual(im.mode, '1') - self.assertEqual(im.size, (32, 32)) - def test_open(self): - # Arrange - # Created with `convert hopper.png hopper.xbm` - filename = "Tests/images/hopper.xbm" +def test_open_filename_with_underscore(): + # Arrange + # Created with `convert hopper.png hopper_underscore.xbm` + filename = "Tests/images/hopper_underscore.xbm" - # Act - im = Image.open(filename) + # Act + with Image.open(filename) as im: # Assert - self.assertEqual(im.mode, '1') - self.assertEqual(im.size, (128, 128)) + assert im.mode == "1" + assert im.size == (128, 128) - def test_open_filename_with_underscore(self): - # Arrange - # Created with `convert hopper.png hopper_underscore.xbm` - filename = "Tests/images/hopper_underscore.xbm" - # Act - im = Image.open(filename) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - # Assert - self.assertEqual(im.mode, '1') - self.assertEqual(im.size, (128, 128)) + with pytest.raises(SyntaxError): + XbmImagePlugin.XbmImageFile(invalid_file) + + +def test_save_wrong_mode(tmp_path): + im = hopper() + out = str(tmp_path / "temp.xbm") + + with pytest.raises(OSError): + im.save(out) + + +def test_hotspot(tmp_path): + im = hopper("1") + out = str(tmp_path / "temp.xbm") + hotspot = (0, 7) + im.save(out, hotspot=hotspot) -if __name__ == '__main__': - unittest.main() + with Image.open(out) as reloaded: + assert reloaded.info["hotspot"] == hotspot diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 4fa3f743ff9..8595b07eb91 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,39 +1,37 @@ -from helper import unittest, PillowTestCase, hopper +import pytest from PIL import Image, XpmImagePlugin -TEST_FILE = "Tests/images/hopper.xpm" +from .helper import assert_image_similar, hopper +TEST_FILE = "Tests/images/hopper.xpm" -class TestFileXpm(PillowTestCase): - def test_sanity(self): - im = Image.open(TEST_FILE) +def test_sanity(): + with Image.open(TEST_FILE) as im: im.load() - self.assertEqual(im.mode, "P") - self.assertEqual(im.size, (128, 128)) - self.assertEqual(im.format, "XPM") + assert im.mode == "P" + assert im.size == (128, 128) + assert im.format == "XPM" # large error due to quantization->44 colors. - self.assert_image_similar(im.convert('RGB'), hopper('RGB'), 60) + assert_image_similar(im.convert("RGB"), hopper("RGB"), 60) + - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" - self.assertRaises(SyntaxError, - XpmImagePlugin.XpmImageFile, invalid_file) + with pytest.raises(SyntaxError): + XpmImagePlugin.XpmImageFile(invalid_file) - def test_load_read(self): - # Arrange - im = Image.open(TEST_FILE) + +def test_load_read(): + # Arrange + with Image.open(TEST_FILE) as im: dummy_bytes = 1 # Act data = im.load_read(dummy_bytes) - # Assert - self.assertEqual(len(data), 16384) - - -if __name__ == '__main__': - unittest.main() + # Assert + assert len(data) == 16384 diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py index d0256cabf7c..ae53d2b6357 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -1,40 +1,38 @@ -from helper import hopper, unittest, PillowTestCase +import pytest from PIL import Image, XVThumbImagePlugin -TEST_FILE = "Tests/images/hopper.p7" +from .helper import assert_image_similar, hopper +TEST_FILE = "Tests/images/hopper.p7" -class TestFileXVThumb(PillowTestCase): - def test_open(self): - # Act - im = Image.open(TEST_FILE) +def test_open(): + # Act + with Image.open(TEST_FILE) as im: # Assert - self.assertEqual(im.format, "XVThumb") + assert im.format == "XVThumb" # Create a Hopper image with a similar XV palette im_hopper = hopper().quantize(palette=im) - self.assert_image_similar(im, im_hopper, 9) + assert_image_similar(im, im_hopper, 9) - def test_unexpected_eof(self): - # Test unexpected EOF reading XV thumbnail file - # Arrange - bad_file = "Tests/images/hopper_bad.p7" - # Act / Assert - self.assertRaises(SyntaxError, - XVThumbImagePlugin.XVThumbImageFile, bad_file) +def test_unexpected_eof(): + # Test unexpected EOF reading XV thumbnail file + # Arrange + bad_file = "Tests/images/hopper_bad.p7" - def test_invalid_file(self): - # Arrange - invalid_file = "Tests/images/flower.jpg" + # Act / Assert + with pytest.raises(SyntaxError): + XVThumbImagePlugin.XVThumbImageFile(bad_file) - # Act / Assert - self.assertRaises(SyntaxError, - XVThumbImagePlugin.XVThumbImageFile, invalid_file) +def test_invalid_file(): + # Arrange + invalid_file = "Tests/images/flower.jpg" -if __name__ == '__main__': - unittest.main() + # Act / Assert + with pytest.raises(SyntaxError): + XVThumbImagePlugin.XVThumbImageFile(invalid_file) diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index 7c8fe579e57..1e7caee3297 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -1,24 +1,19 @@ -from helper import unittest, PillowTestCase +import pytest -from PIL import FontFile, BdfFontFile +from PIL import BdfFontFile, FontFile filename = "Tests/images/courB08.bdf" -class TestFontBdf(PillowTestCase): +def test_sanity(): + with open(filename, "rb") as test_file: + font = BdfFontFile.BdfFontFile(test_file) - def test_sanity(self): + assert isinstance(font, FontFile.FontFile) + assert len([_f for _f in font.glyph if _f]) == 190 - with open(filename, "rb") as test_file: - font = BdfFontFile.BdfFontFile(test_file) - self.assertIsInstance(font, FontFile.FontFile) - self.assertEqual(len([_f for _f in font.glyph if _f]), 190) - - def test_invalid_file(self): - with open("Tests/images/flower.jpg", "rb") as fp: - self.assertRaises(SyntaxError, BdfFontFile.BdfFontFile, fp) - - -if __name__ == '__main__': - unittest.main() +def test_invalid_file(): + with open("Tests/images/flower.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + BdfFontFile.BdfFontFile(fp) diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 709339233e0..38f7ddac5de 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,34 +1,33 @@ -from __future__ import division -from helper import unittest, PillowLeakTestCase -import sys -from PIL import Image, features, ImageDraw, ImageFont +from PIL import Image, ImageDraw, ImageFont + +from .helper import PillowLeakTestCase, skip_unless_feature + -@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") class TestTTypeFontLeak(PillowLeakTestCase): - # fails at iteration 3 in master + # fails at iteration 3 in main iterations = 10 - mem_limit = 4096 #k + mem_limit = 4096 # k def _test_font(self, font): - im = Image.new('RGB', (255,255), 'white') + im = Image.new("RGB", (255, 255), "white") draw = ImageDraw.ImageDraw(im) - self._test_leak(lambda: draw.text((0, 0), "some text "*1024, #~10k - font=font, fill="black")) - - @unittest.skipIf(not features.check('freetype2'), "Test requires freetype2") + self._test_leak( + lambda: draw.text( + (0, 0), "some text " * 1024, font=font, fill="black" # ~10k + ) + ) + + @skip_unless_feature("freetype2") def test_leak(self): - ttype = ImageFont.truetype('Tests/fonts/FreeMono.ttf', 20) + ttype = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) self._test_font(ttype) + class TestDefaultFontLeak(TestTTypeFontLeak): - # fails at iteration 37 in master + # fails at iteration 37 in main iterations = 100 - mem_limit = 1024 #k - + mem_limit = 1024 # k + def test_leak(self): default_font = ImageFont.load_default() self._test_font(default_font) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index dc4c586c06f..c217378fb74 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,85 +1,107 @@ -from helper import unittest, PillowTestCase +import os -from PIL import Image, FontFile, PcfFontFile -from PIL import ImageFont, ImageDraw +import pytest -codecs = dir(Image.core) +from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile + +from .helper import ( + assert_image_equal_tofile, + assert_image_similar_tofile, + skip_unless_feature, +) fontname = "Tests/fonts/10x20-ISO8859-1.pcf" message = "hello, world" -class TestFontPcf(PillowTestCase): - - def setUp(self): - if "zip_encoder" not in codecs or "zip_decoder" not in codecs: - self.skipTest("zlib support not available") - - def save_font(self): - with open(fontname, "rb") as test_file: - font = PcfFontFile.PcfFontFile(test_file) - self.assertIsInstance(font, FontFile.FontFile) - #check the number of characters in the font - self.assertEqual(len([_f for _f in font.glyph if _f]), 223) - - tempname = self.tempfile("temp.pil") - self.addCleanup(self.delete_tempfile, tempname[:-4]+'.pbm') - font.save(tempname) - - with Image.open(tempname.replace('.pil', '.pbm')) as loaded: - with Image.open('Tests/fonts/10x20.pbm') as target: - self.assert_image_equal(loaded, target) - - with open(tempname, 'rb') as f_loaded: - with open('Tests/fonts/10x20.pil', 'rb') as f_target: - self.assertEqual(f_loaded.read(), f_target.read()) - return tempname - - def test_sanity(self): - self.save_font() - - def test_invalid_file(self): - with open("Tests/images/flower.jpg", "rb") as fp: - self.assertRaises(SyntaxError, PcfFontFile.PcfFontFile, fp) - - def test_draw(self): - tempname = self.save_font() - font = ImageFont.load(tempname) - im = Image.new("L", (130,30), "white") - draw = ImageDraw.Draw(im) - draw.text((0, 0), message, 'black', font=font) - with Image.open('Tests/images/test_draw_pbm_target.png') as target: - self.assert_image_similar(im, target, 0) - - def test_textsize(self): - tempname = self.save_font() - font = ImageFont.load(tempname) - for i in range(255): - (dx,dy) = font.getsize(chr(i)) - self.assertEqual(dy, 20) - self.assertIn(dx, (0,10)) - for l in range(len(message)): - msg = message[:l+1] - self.assertEqual(font.getsize(msg), (len(msg)*10,20)) - - def _test_high_characters(self, message): - tempname = self.save_font() - font = ImageFont.load(tempname) - im = Image.new("L", (750,30) , "white") - draw = ImageDraw.Draw(im) - draw.text((0, 0), message, "black", font=font) - with Image.open('Tests/images/high_ascii_chars.png') as target: - self.assert_image_similar(im, target, 0) - - - def test_high_characters(self): - message = "".join(chr(i+1) for i in range(140, 232)) - self._test_high_characters(message) - # accept bytes instances in Py3. - if bytes is not str: - self._test_high_characters(message.encode('latin1')) - - -if __name__ == '__main__': - unittest.main() +pytestmark = skip_unless_feature("zlib") + + +def save_font(request, tmp_path): + with open(fontname, "rb") as test_file: + font = PcfFontFile.PcfFontFile(test_file) + assert isinstance(font, FontFile.FontFile) + # check the number of characters in the font + assert len([_f for _f in font.glyph if _f]) == 223 + + tempname = str(tmp_path / "temp.pil") + + def delete_tempfile(): + try: + os.remove(tempname[:-4] + ".pbm") + except OSError: + pass # report? + + request.addfinalizer(delete_tempfile) + font.save(tempname) + + with Image.open(tempname.replace(".pil", ".pbm")) as loaded: + assert_image_equal_tofile(loaded, "Tests/fonts/10x20.pbm") + + with open(tempname, "rb") as f_loaded: + with open("Tests/fonts/10x20.pil", "rb") as f_target: + assert f_loaded.read() == f_target.read() + return tempname + + +def test_sanity(request, tmp_path): + save_font(request, tmp_path) + + +def test_less_than_256_characters(): + with open("Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf", "rb") as test_file: + font = PcfFontFile.PcfFontFile(test_file) + assert isinstance(font, FontFile.FontFile) + # check the number of characters in the font + assert len([_f for _f in font.glyph if _f]) == 127 + + +def test_invalid_file(): + with open("Tests/images/flower.jpg", "rb") as fp: + with pytest.raises(SyntaxError): + PcfFontFile.PcfFontFile(fp) + + +def test_draw(request, tmp_path): + tempname = save_font(request, tmp_path) + font = ImageFont.load(tempname) + im = Image.new("L", (130, 30), "white") + draw = ImageDraw.Draw(im) + draw.text((0, 0), message, "black", font=font) + assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0) + + +def test_textsize(request, tmp_path): + tempname = save_font(request, tmp_path) + font = ImageFont.load(tempname) + for i in range(255): + (ox, oy, dx, dy) = font.getbbox(chr(i)) + assert ox == 0 + assert oy == 0 + assert dy == 20 + assert dx in (0, 10) + assert font.getlength(chr(i)) == dx + with pytest.warns(DeprecationWarning) as log: + assert font.getsize(chr(i)) == (dx, dy) + assert len(log) == 1 + for i in range(len(message)): + msg = message[: i + 1] + assert font.getlength(msg) == len(msg) * 10 + assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) + + +def _test_high_characters(request, tmp_path, message): + tempname = save_font(request, tmp_path) + font = ImageFont.load(tempname) + im = Image.new("L", (750, 30), "white") + draw = ImageDraw.Draw(im) + draw.text((0, 0), message, "black", font=font) + assert_image_similar_tofile(im, "Tests/images/high_ascii_chars.png", 0) + + +def test_high_characters(request, tmp_path): + message = "".join(chr(i + 1) for i in range(140, 232)) + _test_high_characters(request, tmp_path, message) + # accept bytes instances. + _test_high_characters(request, tmp_path, message.encode("latin1")) diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py new file mode 100644 index 00000000000..664663fd6bb --- /dev/null +++ b/Tests/test_font_pcf_charsets.py @@ -0,0 +1,95 @@ +import os + +import pytest + +from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile + +from .helper import ( + assert_image_equal_tofile, + assert_image_similar_tofile, + skip_unless_feature, +) + +fontname = "Tests/fonts/ter-x20b.pcf" + +charsets = { + "iso8859-1": { + "glyph_count": 223, + "message": "hello, world", + "image1": "Tests/images/test_draw_pbm_ter_en_target.png", + }, + "iso8859-2": { + "glyph_count": 223, + "message": "witaj świecie", + "image1": "Tests/images/test_draw_pbm_ter_pl_target.png", + }, + "cp1250": { + "glyph_count": 250, + "message": "witaj świecie", + "image1": "Tests/images/test_draw_pbm_ter_pl_target.png", + }, +} + + +pytestmark = skip_unless_feature("zlib") + + +def save_font(request, tmp_path, encoding): + with open(fontname, "rb") as test_file: + font = PcfFontFile.PcfFontFile(test_file, encoding) + assert isinstance(font, FontFile.FontFile) + # check the number of characters in the font + assert len([_f for _f in font.glyph if _f]) == charsets[encoding]["glyph_count"] + + tempname = str(tmp_path / "temp.pil") + + def delete_tempfile(): + try: + os.remove(tempname[:-4] + ".pbm") + except OSError: + pass # report? + + request.addfinalizer(delete_tempfile) + font.save(tempname) + + with Image.open(tempname.replace(".pil", ".pbm")) as loaded: + assert_image_equal_tofile(loaded, f"Tests/fonts/ter-x20b-{encoding}.pbm") + + with open(tempname, "rb") as f_loaded: + with open(f"Tests/fonts/ter-x20b-{encoding}.pil", "rb") as f_target: + assert f_loaded.read() == f_target.read() + return tempname + + +@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) +def test_sanity(request, tmp_path, encoding): + save_font(request, tmp_path, encoding) + + +@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) +def test_draw(request, tmp_path, encoding): + tempname = save_font(request, tmp_path, encoding) + font = ImageFont.load(tempname) + im = Image.new("L", (150, 30), "white") + draw = ImageDraw.Draw(im) + message = charsets[encoding]["message"].encode(encoding) + draw.text((0, 0), message, "black", font=font) + assert_image_similar_tofile(im, charsets[encoding]["image1"], 0) + + +@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) +def test_textsize(request, tmp_path, encoding): + tempname = save_font(request, tmp_path, encoding) + font = ImageFont.load(tempname) + for i in range(255): + (ox, oy, dx, dy) = font.getbbox(bytearray([i])) + assert ox == 0 + assert oy == 0 + assert dy == 20 + assert dx in (0, 10) + assert font.getlength(bytearray([i])) == dx + message = charsets[encoding]["message"].encode(encoding) + for i in range(len(message)): + msg = message[: i + 1] + assert font.getlength(msg) == len(msg) * 10 + assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 2cc54c910d6..b485e854f52 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -1,170 +1,151 @@ -from helper import unittest, PillowTestCase, hopper - -from PIL import Image - import colorsys import itertools +from PIL import Image -class TestFormatHSV(PillowTestCase): - - def int_to_float(self, i): - return float(i)/255.0 - - def str_to_float(self, i): - - return float(ord(i))/255.0 - - def to_int(self, f): - return int(f*255.0) +from .helper import assert_image_similar, hopper - def tuple_to_ints(self, tp): - x, y, z = tp - return (int(x*255.0), int(y*255.0), int(z*255.0)) - def test_sanity(self): - Image.new('HSV', (100, 100)) +def int_to_float(i): + return i / 255 - def wedge(self): - w = Image._wedge() - w90 = w.rotate(90) - (px, h) = w.size +def str_to_float(i): + return ord(i) / 255 - r = Image.new('L', (px*3, h)) - g = r.copy() - b = r.copy() - r.paste(w, (0, 0)) - r.paste(w90, (px, 0)) +def tuple_to_ints(tp): + x, y, z = tp + return int(x * 255.0), int(y * 255.0), int(z * 255.0) - g.paste(w90, (0, 0)) - g.paste(w, (2*px, 0)) - b.paste(w, (px, 0)) - b.paste(w90, (2*px, 0)) +def test_sanity(): + Image.new("HSV", (100, 100)) - img = Image.merge('RGB', (r, g, b)) - # print(("%d, %d -> "% (int(1.75*px),int(.25*px))) + \ - # "(%s, %s, %s)"%img.getpixel((1.75*px, .25*px))) - # print(("%d, %d -> "% (int(.75*px),int(.25*px))) + \ - # "(%s, %s, %s)"%img.getpixel((.75*px, .25*px))) - return img +def wedge(): + w = Image._wedge() + w90 = w.rotate(90) - def to_xxx_colorsys(self, im, func, mode): - # convert the hard way using the library colorsys routines. + (px, h) = w.size - (r, g, b) = im.split() + r = Image.new("L", (px * 3, h)) + g = r.copy() + b = r.copy() - if bytes is str: - conv_func = self.str_to_float - else: - conv_func = self.int_to_float + r.paste(w, (0, 0)) + r.paste(w90, (px, 0)) - if hasattr(itertools, 'izip'): - iter_helper = itertools.izip - else: - iter_helper = itertools.zip_longest + g.paste(w90, (0, 0)) + g.paste(w, (2 * px, 0)) - converted = [self.tuple_to_ints(func(conv_func(_r), conv_func(_g), - conv_func(_b))) - for (_r, _g, _b) in iter_helper(r.tobytes(), g.tobytes(), - b.tobytes())] + b.paste(w, (px, 0)) + b.paste(w90, (2 * px, 0)) - if str is bytes: - new_bytes = b''.join(chr(h)+chr(s)+chr(v) for ( - h, s, v) in converted) - else: - new_bytes = b''.join(bytes(chr(h)+chr(s)+chr(v), 'latin-1') for ( - h, s, v) in converted) + img = Image.merge("RGB", (r, g, b)) - hsv = Image.frombytes(mode, r.size, new_bytes) + return img - return hsv - def to_hsv_colorsys(self, im): - return self.to_xxx_colorsys(im, colorsys.rgb_to_hsv, 'HSV') +def to_xxx_colorsys(im, func, mode): + # convert the hard way using the library colorsys routines. - def to_rgb_colorsys(self, im): - return self.to_xxx_colorsys(im, colorsys.hsv_to_rgb, 'RGB') + (r, g, b) = im.split() - def test_wedge(self): - src = self.wedge().resize((3*32, 32), Image.BILINEAR) - im = src.convert('HSV') - comparable = self.to_hsv_colorsys(src) + conv_func = int_to_float - # print(im.getpixel((448, 64))) - # print(comparable.getpixel((448, 64))) + converted = [ + tuple_to_ints(func(conv_func(_r), conv_func(_g), conv_func(_b))) + for (_r, _g, _b) in itertools.zip_longest(r.tobytes(), g.tobytes(), b.tobytes()) + ] - # print(im.split()[0].histogram()) - # print(comparable.split()[0].histogram()) + new_bytes = b"".join( + bytes(chr(h) + chr(s) + chr(v), "latin-1") for (h, s, v) in converted + ) - # im.split()[0].show() - # comparable.split()[0].show() + hsv = Image.frombytes(mode, r.size, new_bytes) - self.assert_image_similar(im.getchannel(0), comparable.getchannel(0), - 1, "Hue conversion is wrong") - self.assert_image_similar(im.getchannel(1), comparable.getchannel(1), - 1, "Saturation conversion is wrong") - self.assert_image_similar(im.getchannel(2), comparable.getchannel(2), - 1, "Value conversion is wrong") + return hsv - # print(im.getpixel((192, 64))) - comparable = src - im = im.convert('RGB') +def to_hsv_colorsys(im): + return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV") - # im.split()[0].show() - # comparable.split()[0].show() - # print(im.getpixel((192, 64))) - # print(comparable.getpixel((192, 64))) - self.assert_image_similar(im.getchannel(0), comparable.getchannel(0), - 3, "R conversion is wrong") - self.assert_image_similar(im.getchannel(1), comparable.getchannel(1), - 3, "G conversion is wrong") - self.assert_image_similar(im.getchannel(2), comparable.getchannel(2), - 3, "B conversion is wrong") +def to_rgb_colorsys(im): + return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") - def test_convert(self): - im = hopper('RGB').convert('HSV') - comparable = self.to_hsv_colorsys(hopper('RGB')) -# print([ord(x) for x in im.split()[0].tobytes()[:80]]) -# print([ord(x) for x in comparable.split()[0].tobytes()[:80]]) +def test_wedge(): + src = wedge().resize((3 * 32, 32), Image.Resampling.BILINEAR) + im = src.convert("HSV") + comparable = to_hsv_colorsys(src) -# print(im.split()[0].histogram()) -# print(comparable.split()[0].histogram()) + assert_image_similar( + im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" + ) + assert_image_similar( + im.getchannel(1), + comparable.getchannel(1), + 1, + "Saturation conversion is wrong", + ) + assert_image_similar( + im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" + ) - self.assert_image_similar(im.getchannel(0), comparable.getchannel(0), - 1, "Hue conversion is wrong") - self.assert_image_similar(im.getchannel(1), comparable.getchannel(1), - 1, "Saturation conversion is wrong") - self.assert_image_similar(im.getchannel(2), comparable.getchannel(2), - 1, "Value conversion is wrong") + comparable = src + im = im.convert("RGB") - def test_hsv_to_rgb(self): - comparable = self.to_hsv_colorsys(hopper('RGB')) - converted = comparable.convert('RGB') - comparable = self.to_rgb_colorsys(comparable) - - # print(converted.split()[1].histogram()) - # print(target.split()[1].histogram()) + assert_image_similar( + im.getchannel(0), comparable.getchannel(0), 3, "R conversion is wrong" + ) + assert_image_similar( + im.getchannel(1), comparable.getchannel(1), 3, "G conversion is wrong" + ) + assert_image_similar( + im.getchannel(2), comparable.getchannel(2), 3, "B conversion is wrong" + ) - # print([ord(x) for x in target.split()[1].tobytes()[:80]]) - # print([ord(x) for x in converted.split()[1].tobytes()[:80]]) - - self.assert_image_similar(converted.getchannel(0), - comparable.getchannel(0), - 3, "R conversion is wrong") - self.assert_image_similar(converted.getchannel(1), - comparable.getchannel(1), - 3, "G conversion is wrong") - self.assert_image_similar(converted.getchannel(2), - comparable.getchannel(2), - 3, "B conversion is wrong") +def test_convert(): + im = hopper("RGB").convert("HSV") + comparable = to_hsv_colorsys(hopper("RGB")) -if __name__ == '__main__': - unittest.main() + assert_image_similar( + im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong" + ) + assert_image_similar( + im.getchannel(1), + comparable.getchannel(1), + 1, + "Saturation conversion is wrong", + ) + assert_image_similar( + im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong" + ) + + +def test_hsv_to_rgb(): + comparable = to_hsv_colorsys(hopper("RGB")) + converted = comparable.convert("RGB") + comparable = to_rgb_colorsys(comparable) + + assert_image_similar( + converted.getchannel(0), + comparable.getchannel(0), + 3, + "R conversion is wrong", + ) + assert_image_similar( + converted.getchannel(1), + comparable.getchannel(1), + 3, + "G conversion is wrong", + ) + assert_image_similar( + converted.getchannel(2), + comparable.getchannel(2), + 3, + "B conversion is wrong", + ) diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index a243afe626a..41c8efdf316 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -1,46 +1,38 @@ -from helper import unittest, PillowTestCase - from PIL import Image -class TestFormatLab(PillowTestCase): - - def test_white(self): - i = Image.open('Tests/images/lab.tif') - +def test_white(): + with Image.open("Tests/images/lab.tif") as i: i.load() - self.assertEqual(i.mode, 'LAB') + assert i.mode == "LAB" - self.assertEqual(i.getbands(), ('L', 'A', 'B')) + assert i.getbands() == ("L", "A", "B") k = i.getpixel((0, 0)) - self.assertEqual(k, (255, 128, 128)) L = i.getdata(0) a = i.getdata(1) b = i.getdata(2) - self.assertEqual(list(L), [255]*100) - self.assertEqual(list(a), [128]*100) - self.assertEqual(list(b), [128]*100) + assert k == (255, 128, 128) - def test_green(self): - # l= 50 (/100), a = -100 (-128 .. 128) b=0 in PS - # == RGB: 0, 152, 117 - i = Image.open('Tests/images/lab-green.tif') + assert list(L) == [255] * 100 + assert list(a) == [128] * 100 + assert list(b) == [128] * 100 - k = i.getpixel((0, 0)) - self.assertEqual(k, (128, 28, 128)) - - def test_red(self): - # l= 50 (/100), a = 100 (-128 .. 128) b=0 in PS - # == RGB: 255, 0, 124 - i = Image.open('Tests/images/lab-red.tif') +def test_green(): + # l= 50 (/100), a = -100 (-128 .. 128) b=0 in PS + # == RGB: 0, 152, 117 + with Image.open("Tests/images/lab-green.tif") as i: k = i.getpixel((0, 0)) - self.assertEqual(k, (128, 228, 128)) + assert k == (128, 28, 128) -if __name__ == '__main__': - unittest.main() +def test_red(): + # l= 50 (/100), a = 100 (-128 .. 128) b=0 in PS + # == RGB: 255, 0, 124 + with Image.open("Tests/images/lab-red.tif") as i: + k = i.getpixel((0, 0)) + assert k == (128, 228, 128) diff --git a/Tests/test_image.py b/Tests/test_image.py index 6f6d1983e39..e579034904d 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,94 +1,175 @@ -from helper import unittest, PillowTestCase, hopper - -from PIL import Image +import io import os - - -class TestImage(PillowTestCase): - - def test_image_modes_success(self): - for mode in [ - '1', 'P', 'PA', - 'L', 'LA', 'La', - 'F', 'I', 'I;16', 'I;16L', 'I;16B', 'I;16N', - 'RGB', 'RGBX', 'RGBA', 'RGBa', - 'CMYK', 'YCbCr', 'LAB', 'HSV', - ]: +import shutil +import sys +import tempfile +import warnings + +import pytest + +from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError, features + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar_tofile, + assert_not_all_same, + hopper, + is_win32, + mark_if_feature_version, + skip_unless_feature, +) + + +class TestImage: + @pytest.mark.parametrize( + "mode", + ( + "1", + "P", + "PA", + "L", + "LA", + "La", + "F", + "I", + "I;16", + "I;16L", + "I;16B", + "I;16N", + "RGB", + "RGBX", + "RGBA", + "RGBa", + "CMYK", + "YCbCr", + "LAB", + "HSV", + ), + ) + def test_image_modes_success(self, mode): + Image.new(mode, (1, 1)) + + @pytest.mark.parametrize( + "mode", ("", "bad", "very very long", "BGR;15", "BGR;16", "BGR;24", "BGR;32") + ) + def test_image_modes_fail(self, mode): + with pytest.raises(ValueError) as e: Image.new(mode, (1, 1)) + assert str(e.value) == "unrecognized image mode" - def test_image_modes_fail(self): - for mode in [ - '', 'bad', 'very very long', - 'BGR;15', 'BGR;16', 'BGR;24', 'BGR;32' - ]: - with self.assertRaises(ValueError) as e: - Image.new(mode, (1, 1)) - self.assertEqual(str(e.exception), 'unrecognized image mode') + def test_exception_inheritance(self): + assert issubclass(UnidentifiedImageError, OSError) def test_sanity(self): im = Image.new("L", (100, 100)) - self.assertEqual( - repr(im)[:45], "" + + def test_open_formats(self): + PNGFILE = "Tests/images/hopper.png" + JPGFILE = "Tests/images/hopper.jpg" + + with pytest.raises(TypeError): + with Image.open(PNGFILE, formats=123): + pass + + for formats in [["JPEG"], ("JPEG",), ["jpeg"], ["Jpeg"], ["jPeG"], ["JpEg"]]: + with pytest.raises(UnidentifiedImageError): + with Image.open(PNGFILE, formats=formats): + pass + + with Image.open(JPGFILE, formats=formats) as im: + assert im.mode == "RGB" + assert im.size == (128, 128) - self.assertRaises(ValueError, Image.new, "X", (100, 100)) - self.assertRaises(ValueError, Image.new, "", (100, 100)) - # self.assertRaises(MemoryError, Image.new, "L", (1000000, 1000000)) + for file in [PNGFILE, JPGFILE]: + with Image.open(file, formats=None) as im: + assert im.mode == "RGB" + assert im.size == (128, 128) def test_width_height(self): im = Image.new("RGB", (1, 2)) - self.assertEqual(im.width, 1) - self.assertEqual(im.height, 2) + assert im.width == 1 + assert im.height == 2 - im.size = (3, 4) - self.assertEqual(im.width, 3) - self.assertEqual(im.height, 4) + with pytest.raises(AttributeError): + im.size = (3, 4) def test_invalid_image(self): - if str is bytes: - import StringIO - im = StringIO.StringIO('') - else: - import io - im = io.BytesIO(b'') - self.assertRaises(IOError, Image.open, im) + im = io.BytesIO(b"") + with pytest.raises(UnidentifiedImageError): + with Image.open(im): + pass def test_bad_mode(self): - self.assertRaises(ValueError, Image.open, "filename", "bad mode") + with pytest.raises(ValueError): + with Image.open("filename", "bad mode"): + pass + + def test_stringio(self): + with pytest.raises(ValueError): + with Image.open(io.StringIO()): + pass - @unittest.skipUnless(Image.HAS_PATHLIB, "requires pathlib/pathlib2") - def test_pathlib(self): + def test_pathlib(self, tmp_path): from PIL.Image import Path - im = Image.open(Path("Tests/images/hopper.jpg")) - self.assertEqual(im.mode, "RGB") - self.assertEqual(im.size, (128, 128)) - temp_file = self.tempfile("temp.jpg") - if os.path.exists(temp_file): - os.remove(temp_file) - im.save(Path(temp_file)) + with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: + assert im.mode == "P" + assert im.size == (10, 10) - def test_fp_name(self): - temp_file = self.tempfile("temp.jpg") + with Image.open(Path("Tests/images/hopper.jpg")) as im: + assert im.mode == "RGB" + assert im.size == (128, 128) - class FP(object): - def write(a, b): + for ext in (".jpg", ".jp2"): + if ext == ".jp2" and not features.check_codec("jpg_2000"): + pytest.skip("jpg_2000 not available") + temp_file = str(tmp_path / ("temp." + ext)) + if os.path.exists(temp_file): + os.remove(temp_file) + im.save(Path(temp_file)) + + def test_fp_name(self, tmp_path): + temp_file = str(tmp_path / "temp.jpg") + + class FP: + def write(self, b): pass + fp = FP() fp.name = temp_file @@ -98,43 +179,61 @@ def write(a, b): def test_tempfile(self): # see #1460, pathlib support breaks tempfile.TemporaryFile on py27 # Will error out on save on 3.0.0 - import tempfile im = hopper() with tempfile.TemporaryFile() as fp: - im.save(fp, 'JPEG') + im.save(fp, "JPEG") fp.seek(0) - reloaded = Image.open(fp) - self.assert_image_similar(im, reloaded, 20) + assert_image_similar_tofile(im, fp, 20) - def test_unknown_extension(self): + def test_unknown_extension(self, tmp_path): im = hopper() - temp_file = self.tempfile("temp.unknown") - self.assertRaises(ValueError, im.save, temp_file) + temp_file = str(tmp_path / "temp.unknown") + with pytest.raises(ValueError): + im.save(temp_file) def test_internals(self): - im = Image.new("L", (100, 100)) im.readonly = 1 im._copy() - self.assertFalse(im.readonly) + assert not im.readonly im.readonly = 1 im.paste(0, (0, 0, 100, 100)) - self.assertFalse(im.readonly) + assert not im.readonly - test_file = self.tempfile("temp.ppm") - im._dump(test_file) + @pytest.mark.skipif(is_win32(), reason="Test requires opening tempfile twice") + @pytest.mark.skipif( + sys.platform == "cygwin", + reason="Test requires opening an mmaped file for writing", + ) + def test_readonly_save(self, tmp_path): + temp_file = str(tmp_path / "temp.bmp") + shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file) + + with Image.open(temp_file) as im: + assert im.readonly + im.save(temp_file) + + def test_dump(self, tmp_path): + im = Image.new("L", (10, 10)) + im._dump(str(tmp_path / "temp_L.ppm")) + + im = Image.new("RGB", (10, 10)) + im._dump(str(tmp_path / "temp_RGB.ppm")) + + im = Image.new("HSV", (10, 10)) + with pytest.raises(ValueError): + im._dump(str(tmp_path / "temp_HSV.ppm")) def test_comparison_with_other_type(self): # Arrange - item = Image.new('RGB', (25, 25), '#000') + item = Image.new("RGB", (25, 25), "#000") num = 12 # Act/Assert # Shouldn't cause AttributeError (#774) - self.assertFalse(item is None) - self.assertFalse(item == None) - self.assertFalse(item == num) + assert item is not None + assert item != num def test_expand_x(self): # Arrange @@ -146,8 +245,8 @@ def test_expand_x(self): im = im._expand(xmargin) # Assert - self.assertEqual(im.size[0], orig_size[0] + 2*xmargin) - self.assertEqual(im.size[1], orig_size[1] + 2*xmargin) + assert im.size[0] == orig_size[0] + 2 * xmargin + assert im.size[1] == orig_size[1] + 2 * xmargin def test_expand_xy(self): # Arrange @@ -160,32 +259,36 @@ def test_expand_xy(self): im = im._expand(xmargin, ymargin) # Assert - self.assertEqual(im.size[0], orig_size[0] + 2*xmargin) - self.assertEqual(im.size[1], orig_size[1] + 2*ymargin) + assert im.size[0] == orig_size[0] + 2 * xmargin + assert im.size[1] == orig_size[1] + 2 * ymargin def test_getbands(self): # Assert - self.assertEqual(hopper('RGB').getbands(), ('R', 'G', 'B')) - self.assertEqual(hopper('YCbCr').getbands(), ('Y', 'Cb', 'Cr')) + assert hopper("RGB").getbands() == ("R", "G", "B") + assert hopper("YCbCr").getbands() == ("Y", "Cb", "Cr") def test_getchannel_wrong_params(self): im = hopper() - self.assertRaises(ValueError, im.getchannel, -1) - self.assertRaises(ValueError, im.getchannel, 3) - self.assertRaises(ValueError, im.getchannel, 'Z') - self.assertRaises(ValueError, im.getchannel, '1') + with pytest.raises(ValueError): + im.getchannel(-1) + with pytest.raises(ValueError): + im.getchannel(3) + with pytest.raises(ValueError): + im.getchannel("Z") + with pytest.raises(ValueError): + im.getchannel("1") def test_getchannel(self): - im = hopper('YCbCr') + im = hopper("YCbCr") Y, Cb, Cr = im.split() - self.assert_image_equal(Y, im.getchannel(0)) - self.assert_image_equal(Y, im.getchannel('Y')) - self.assert_image_equal(Cb, im.getchannel(1)) - self.assert_image_equal(Cb, im.getchannel('Cb')) - self.assert_image_equal(Cr, im.getchannel(2)) - self.assert_image_equal(Cr, im.getchannel('Cr')) + assert_image_equal(Y, im.getchannel(0)) + assert_image_equal(Y, im.getchannel("Y")) + assert_image_equal(Cb, im.getchannel(1)) + assert_image_equal(Cb, im.getchannel("Cb")) + assert_image_equal(Cr, im.getchannel(2)) + assert_image_equal(Cr, im.getchannel("Cr")) def test_getbbox(self): # Arrange @@ -195,35 +298,36 @@ def test_getbbox(self): bbox = im.getbbox() # Assert - self.assertEqual(bbox, (0, 0, 128, 128)) + assert bbox == (0, 0, 128, 128) def test_ne(self): # Arrange - im1 = Image.new('RGB', (25, 25), 'black') - im2 = Image.new('RGB', (25, 25), 'white') + im1 = Image.new("RGB", (25, 25), "black") + im2 = Image.new("RGB", (25, 25), "white") # Act / Assert - self.assertNotEqual(im1, im2) + assert im1 != im2 def test_alpha_composite(self): # https://stackoverflow.com/questions/3374878 # Arrange - from PIL import ImageDraw - - expected_colors = sorted([ - (1122, (128, 127, 0, 255)), - (1089, (0, 255, 0, 255)), - (3300, (255, 0, 0, 255)), - (1156, (170, 85, 0, 192)), - (1122, (0, 255, 0, 128)), - (1122, (255, 0, 0, 128)), - (1089, (0, 255, 0, 0))]) - - dst = Image.new('RGBA', size=(100, 100), color=(0, 255, 0, 255)) + expected_colors = sorted( + [ + (1122, (128, 127, 0, 255)), + (1089, (0, 255, 0, 255)), + (3300, (255, 0, 0, 255)), + (1156, (170, 85, 0, 192)), + (1122, (0, 255, 0, 128)), + (1122, (255, 0, 0, 128)), + (1089, (0, 255, 0, 0)), + ] + ) + + dst = Image.new("RGBA", size=(100, 100), color=(0, 255, 0, 255)) draw = ImageDraw.Draw(dst) draw.rectangle((0, 33, 100, 66), fill=(0, 255, 0, 128)) draw.rectangle((0, 67, 100, 100), fill=(0, 255, 0, 0)) - src = Image.new('RGBA', size=(100, 100), color=(255, 0, 0, 255)) + src = Image.new("RGBA", size=(100, 100), color=(255, 0, 0, 255)) draw = ImageDraw.Draw(src) draw.rectangle((33, 0, 66, 100), fill=(255, 0, 0, 128)) draw.rectangle((67, 0, 100, 100), fill=(255, 0, 0, 0)) @@ -233,13 +337,13 @@ def test_alpha_composite(self): # Assert img_colors = sorted(img.getcolors()) - self.assertEqual(img_colors, expected_colors) + assert img_colors == expected_colors def test_alpha_inplace(self): - src = Image.new('RGBA', (128, 128), 'blue') + src = Image.new("RGBA", (128, 128), "blue") - over = Image.new('RGBA', (128, 128), 'red') - mask = hopper('L') + over = Image.new("RGBA", (128, 128), "red") + mask = hopper("L") over.putalpha(mask) target = Image.alpha_composite(src, over) @@ -247,46 +351,45 @@ def test_alpha_inplace(self): # basic full = src.copy() full.alpha_composite(over) - self.assert_image_equal(full, target) + assert_image_equal(full, target) # with offset down to right offset = src.copy() offset.alpha_composite(over, (64, 64)) - self.assert_image_equal(offset.crop((64, 64, 127, 127)), - target.crop((0, 0, 63, 63))) - self.assertEqual(offset.size, (128, 128)) + assert_image_equal(offset.crop((64, 64, 127, 127)), target.crop((0, 0, 63, 63))) + assert offset.size == (128, 128) + + # with negative offset + offset = src.copy() + offset.alpha_composite(over, (-64, -64)) + assert_image_equal(offset.crop((0, 0, 63, 63)), target.crop((64, 64, 127, 127))) + assert offset.size == (128, 128) # offset and crop box = src.copy() box.alpha_composite(over, (64, 64), (0, 0, 32, 32)) - self.assert_image_equal(box.crop((64, 64, 96, 96)), - target.crop((0, 0, 32, 32))) - self.assert_image_equal(box.crop((96, 96, 128, 128)), - src.crop((0, 0, 32, 32))) - self.assertEqual(box.size, (128, 128)) + assert_image_equal(box.crop((64, 64, 96, 96)), target.crop((0, 0, 32, 32))) + assert_image_equal(box.crop((96, 96, 128, 128)), src.crop((0, 0, 32, 32))) + assert box.size == (128, 128) # source point source = src.copy() source.alpha_composite(over, (32, 32), (32, 32, 96, 96)) - self.assert_image_equal(source.crop((32, 32, 96, 96)), - target.crop((32, 32, 96, 96))) - self.assertEqual(source.size, (128, 128)) + assert_image_equal(source.crop((32, 32, 96, 96)), target.crop((32, 32, 96, 96))) + assert source.size == (128, 128) # errors - self.assertRaises(ValueError, - source.alpha_composite, over, "invalid source") - self.assertRaises(ValueError, - source.alpha_composite, over, (0, 0), - "invalid destination") - self.assertRaises(ValueError, - source.alpha_composite, over, (0)) - self.assertRaises(ValueError, - source.alpha_composite, over, (0, 0), (0)) - self.assertRaises(ValueError, - source.alpha_composite, over, (0, -1)) - self.assertRaises(ValueError, - source.alpha_composite, over, (0, 0), (0, -1)) + with pytest.raises(ValueError): + source.alpha_composite(over, "invalid source") + with pytest.raises(ValueError): + source.alpha_composite(over, (0, 0), "invalid destination") + with pytest.raises(ValueError): + source.alpha_composite(over, 0) + with pytest.raises(ValueError): + source.alpha_composite(over, (0, 0), 0) + with pytest.raises(ValueError): + source.alpha_composite(over, (0, 0), (0, -1)) def test_registered_extensions_uninitialized(self): # Arrange @@ -298,24 +401,25 @@ def test_registered_extensions_uninitialized(self): Image.registered_extensions() # Assert - self.assertEqual(Image._initialized, 2) + assert Image._initialized == 2 # Restore the original state and assert Image.EXTENSION = extension - self.assertTrue(Image.EXTENSION) + assert Image.EXTENSION def test_registered_extensions(self): # Arrange # Open an image to trigger plugin registration - Image.open('Tests/images/rgb.jpg') + with Image.open("Tests/images/rgb.jpg"): + pass # Act extensions = Image.registered_extensions() # Assert - self.assertTrue(extensions) - for ext in ['.cur', '.icns', '.tif', '.tiff']: - self.assertIn(ext, extensions) + assert extensions + for ext in [".cur", ".icns", ".tif", ".tiff"]: + assert ext in extensions def test_effect_mandelbrot(self): # Arrange @@ -327,9 +431,8 @@ def test_effect_mandelbrot(self): im = Image.effect_mandelbrot(size, extent, quality) # Assert - self.assertEqual(im.size, (512, 512)) - im2 = Image.open('Tests/images/effect_mandelbrot.png') - self.assert_image_equal(im, im2) + assert im.size == (512, 512) + assert_image_equal_tofile(im, "Tests/images/effect_mandelbrot.png") def test_effect_mandelbrot_bad_arguments(self): # Arrange @@ -340,9 +443,8 @@ def test_effect_mandelbrot_bad_arguments(self): quality = 1 # Act/Assert - self.assertRaises( - ValueError, - Image.effect_mandelbrot, size, extent, quality) + with pytest.raises(ValueError): + Image.effect_mandelbrot(size, extent, quality) def test_effect_noise(self): # Arrange @@ -353,14 +455,14 @@ def test_effect_noise(self): im = Image.effect_noise(size, sigma) # Assert - self.assertEqual(im.size, (100, 100)) - self.assertEqual(im.mode, "L") + assert im.size == (100, 100) + assert im.mode == "L" p0 = im.getpixel((0, 0)) p1 = im.getpixel((0, 1)) p2 = im.getpixel((0, 2)) p3 = im.getpixel((0, 3)) p4 = im.getpixel((0, 4)) - self.assert_not_all_same([p0, p1, p2, p3, p4]) + assert_not_all_same([p0, p1, p2, p3, p4]) def test_effect_spread(self): # Arrange @@ -371,34 +473,43 @@ def test_effect_spread(self): im2 = im.effect_spread(distance) # Assert - self.assertEqual(im.size, (128, 128)) - im3 = Image.open('Tests/images/effect_spread.png') - self.assert_image_similar(im2, im3, 110) + assert im.size == (128, 128) + assert_image_similar_tofile(im2, "Tests/images/effect_spread.png", 110) + + def test_effect_spread_zero(self): + # Arrange + im = hopper() + distance = 0 + + # Act + im2 = im.effect_spread(distance) + + # Assert + assert_image_equal(im, im2) def test_check_size(self): - # Checking that the _check_size function throws value errors - # when we want it to. - with self.assertRaises(ValueError): - Image.new('RGB', 0) # not a tuple - with self.assertRaises(ValueError): - Image.new('RGB', (0,)) # Tuple too short - with self.assertRaises(ValueError): - Image.new('RGB', (-1, -1)) # w,h < 0 + # Checking that the _check_size function throws value errors when we want it to + with pytest.raises(ValueError): + Image.new("RGB", 0) # not a tuple + with pytest.raises(ValueError): + Image.new("RGB", (0,)) # Tuple too short + with pytest.raises(ValueError): + Image.new("RGB", (-1, -1)) # w,h < 0 # this should pass with 0 sized images, #2259 - im = Image.new('L', (0, 0)) - self.assertEqual(im.size, (0, 0)) + im = Image.new("L", (0, 0)) + assert im.size == (0, 0) - im = Image.new('L', (0, 100)) - self.assertEqual(im.size, (0, 100)) + im = Image.new("L", (0, 100)) + assert im.size == (0, 100) - im = Image.new('L', (100, 0)) - self.assertEqual(im.size, (100, 0)) + im = Image.new("L", (100, 0)) + assert im.size == (100, 0) - self.assertTrue(Image.new('RGB', (1, 1))) + assert Image.new("RGB", (1, 1)) # Should pass lists too - i = Image.new('RGB', [1, 1]) - self.assertIsInstance(i.size, tuple) + i = Image.new("RGB", [1, 1]) + assert isinstance(i.size, tuple) def test_storage_neg(self): # Storage.c accepted negative values for xsize, ysize. Was @@ -406,68 +517,64 @@ def test_storage_neg(self): # removed Calling directly into core to test the error in # Storage.c, rather than the size check above - with self.assertRaises(ValueError): - Image.core.fill('RGB', (2, -2), (0, 0, 0)) - - def test_offset_not_implemented(self): - # Arrange - im = hopper() - - # Act / Assert - self.assertRaises(NotImplementedError, im.offset, None) + with pytest.raises(ValueError): + Image.core.fill("RGB", (2, -2), (0, 0, 0)) - def test_fromstring(self): - self.assertRaises(NotImplementedError, Image.fromstring) + def test_one_item_tuple(self): + for mode in ("I", "F", "L"): + im = Image.new(mode, (100, 100), (5,)) + px = im.load() + assert px[0, 0] == 5 def test_linear_gradient_wrong_mode(self): # Arrange wrong_mode = "RGB" # Act / Assert - self.assertRaises(ValueError, - Image.linear_gradient, wrong_mode) - - def test_linear_gradient(self): + with pytest.raises(ValueError): + Image.linear_gradient(wrong_mode) + @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) + def test_linear_gradient(self, mode): # Arrange target_file = "Tests/images/linear_gradient.png" - for mode in ["L", "P"]: - # Act - im = Image.linear_gradient(mode) + # Act + im = Image.linear_gradient(mode) - # Assert - self.assertEqual(im.size, (256, 256)) - self.assertEqual(im.mode, mode) - self.assertEqual(im.getpixel((0, 0)), 0) - self.assertEqual(im.getpixel((255, 255)), 255) - target = Image.open(target_file).convert(mode) - self.assert_image_equal(im, target) + # Assert + assert im.size == (256, 256) + assert im.mode == mode + assert im.getpixel((0, 0)) == 0 + assert im.getpixel((255, 255)) == 255 + with Image.open(target_file) as target: + target = target.convert(mode) + assert_image_equal(im, target) def test_radial_gradient_wrong_mode(self): # Arrange wrong_mode = "RGB" # Act / Assert - self.assertRaises(ValueError, - Image.radial_gradient, wrong_mode) - - def test_radial_gradient(self): + with pytest.raises(ValueError): + Image.radial_gradient(wrong_mode) + @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) + def test_radial_gradient(self, mode): # Arrange target_file = "Tests/images/radial_gradient.png" - for mode in ["L", "P"]: - # Act - im = Image.radial_gradient(mode) + # Act + im = Image.radial_gradient(mode) - # Assert - self.assertEqual(im.size, (256, 256)) - self.assertEqual(im.mode, mode) - self.assertEqual(im.getpixel((0, 0)), 255) - self.assertEqual(im.getpixel((128, 128)), 0) - target = Image.open(target_file).convert(mode) - self.assert_image_equal(im, target) + # Assert + assert im.size == (256, 256) + assert im.mode == mode + assert im.getpixel((0, 0)) == 255 + assert im.getpixel((128, 128)) == 0 + with Image.open(target_file) as target: + target = target.convert(mode) + assert_image_equal(im, target) def test_register_extensions(self): test_format = "a" @@ -483,52 +590,361 @@ def test_register_extensions(self): for ext in exts: del Image.EXTENSION[ext] - self.assertEqual(ext_individual, ext_multiple) + assert ext_individual == ext_multiple def test_remap_palette(self): + # Test identity transform + with Image.open("Tests/images/hopper.gif") as im: + assert_image_equal(im, im.remap_palette(list(range(256)))) + + # Test identity transform with an RGBA palette + im = Image.new("P", (256, 1)) + for x in range(256): + im.putpixel((x, 0), x) + im.putpalette(list(range(256)) * 4, "RGBA") + im_remapped = im.remap_palette(list(range(256))) + assert_image_equal(im, im_remapped) + assert im.palette.palette == im_remapped.palette.palette + # Test illegal image mode - im = hopper() - self.assertRaises(ValueError, im.remap_palette, None) + with hopper() as im: + with pytest.raises(ValueError): + im.remap_palette(None) - def test__new(self): - from PIL import ImagePalette + def test_remap_palette_transparency(self): + im = Image.new("P", (1, 2)) + im.putpixel((0, 1), 1) + im.info["transparency"] = 0 - im = hopper('RGB') - im_p = hopper('P') + im_remapped = im.remap_palette([1, 0]) + assert im_remapped.info["transparency"] == 1 + assert len(im_remapped.getpalette()) == 6 + + # Test unused transparency + im.info["transparency"] = 2 + + im_remapped = im.remap_palette([1, 0]) + assert "transparency" not in im_remapped.info + + def test__new(self): + im = hopper("RGB") + im_p = hopper("P") - blank_p = Image.new('P', (10, 10)) - blank_pa = Image.new('PA', (10, 10)) + blank_p = Image.new("P", (10, 10)) + blank_pa = Image.new("PA", (10, 10)) blank_p.palette = None blank_pa.palette = None def _make_new(base_image, im, palette_result=None): new_im = base_image._new(im) - self.assertEqual(new_im.mode, im.mode) - self.assertEqual(new_im.size, im.size) - self.assertEqual(new_im.info, base_image.info) + assert new_im.mode == im.mode + assert new_im.size == im.size + assert new_im.info == base_image.info if palette_result is not None: - self.assertEqual(new_im.palette.tobytes(), - palette_result.tobytes()) + assert new_im.palette.tobytes() == palette_result.tobytes() else: - self.assertEqual(new_im.palette, None) + assert new_im.palette is None - _make_new(im, im_p, im_p.palette) + _make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3)) _make_new(im_p, im, None) _make_new(im, blank_p, ImagePalette.ImagePalette()) _make_new(im, blank_pa, ImagePalette.ImagePalette()) - def test_no_resource_warning_on_save(self): + def test_p_from_rgb_rgba(self): + for mode, color in [ + ("RGB", "#DDEEFF"), + ("RGB", (221, 238, 255)), + ("RGBA", (221, 238, 255, 255)), + ]: + im = Image.new("P", (100, 100), color) + expected = Image.new(mode, (100, 100), color) + assert_image_equal(im.convert(mode), expected) + + def test_no_resource_warning_on_save(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/835 # Arrange - test_file = 'Tests/images/hopper.png' - temp_file = self.tempfile("temp.jpg") + test_file = "Tests/images/hopper.png" + temp_file = str(tmp_path / "temp.jpg") # Act/Assert with Image.open(test_file) as im: - self.assert_warning(None, im.save, temp_file) - - -class MockEncoder(object): + with warnings.catch_warnings(): + im.save(temp_file) + + def test_no_new_file_on_error(self, tmp_path): + temp_file = str(tmp_path / "temp.jpg") + + im = Image.new("RGB", (0, 0)) + with pytest.raises(ValueError): + im.save(temp_file) + + assert not os.path.exists(temp_file) + + def test_load_on_nonexclusive_multiframe(self): + with open("Tests/images/frozenpond.mpo", "rb") as fp: + + def act(fp): + im = Image.open(fp) + im.load() + + act(fp) + + with Image.open(fp) as im: + im.load() + + assert not fp.closed + + def test_empty_exif(self): + with Image.open("Tests/images/exif.png") as im: + exif = im.getexif() + assert dict(exif) + + # Test that exif data is cleared after another load + exif.load(None) + assert not dict(exif) + + # Test loading just the EXIF header + exif.load(b"Exif\x00\x00") + assert not dict(exif) + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) + def test_exif_jpeg(self, tmp_path): + with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # Little endian + exif = im.getexif() + assert 258 not in exif + assert 274 in exif + assert 282 in exif + assert exif[296] == 2 + assert exif[11] == "gThumb 3.0.1" + + out = str(tmp_path / "temp.jpg") + exif[258] = 8 + del exif[274] + del exif[282] + exif[296] = 455 + exif[11] = "Pillow test" + im.save(out, exif=exif) + with Image.open(out) as reloaded: + reloaded_exif = reloaded.getexif() + assert reloaded_exif[258] == 8 + assert 274 not in reloaded_exif + assert 282 not in reloaded_exif + assert reloaded_exif[296] == 455 + assert reloaded_exif[11] == "Pillow test" + + with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: # Big endian + exif = im.getexif() + assert 258 not in exif + assert 306 in exif + assert exif[274] == 1 + assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)" + + out = str(tmp_path / "temp.jpg") + exif[258] = 8 + del exif[306] + exif[274] = 455 + exif[305] = "Pillow test" + im.save(out, exif=exif) + with Image.open(out) as reloaded: + reloaded_exif = reloaded.getexif() + assert reloaded_exif[258] == 8 + assert 306 not in reloaded_exif + assert reloaded_exif[274] == 455 + assert reloaded_exif[305] == "Pillow test" + + @skip_unless_feature("webp") + @skip_unless_feature("webp_anim") + def test_exif_webp(self, tmp_path): + with Image.open("Tests/images/hopper.webp") as im: + exif = im.getexif() + assert exif == {} + + out = str(tmp_path / "temp.webp") + exif[258] = 8 + exif[40963] = 455 + exif[305] = "Pillow test" + + def check_exif(): + with Image.open(out) as reloaded: + reloaded_exif = reloaded.getexif() + assert reloaded_exif[258] == 8 + assert reloaded_exif[40963] == 455 + assert reloaded_exif[305] == "Pillow test" + + im.save(out, exif=exif) + check_exif() + im.save(out, exif=exif, save_all=True) + check_exif() + + def test_exif_png(self, tmp_path): + with Image.open("Tests/images/exif.png") as im: + exif = im.getexif() + assert exif == {274: 1} + + out = str(tmp_path / "temp.png") + exif[258] = 8 + del exif[274] + exif[40963] = 455 + exif[305] = "Pillow test" + im.save(out, exif=exif) + + with Image.open(out) as reloaded: + reloaded_exif = reloaded.getexif() + assert reloaded_exif == {258: 8, 40963: 455, 305: "Pillow test"} + + def test_exif_interop(self): + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + assert exif.get_ifd(0xA005) == { + 1: "R98", + 2: b"0100", + 4097: 2272, + 4098: 1704, + } + + reloaded_exif = Image.Exif() + reloaded_exif.load(exif.tobytes()) + assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005) + + def test_exif_ifd(self): + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + del exif.get_ifd(0x8769)[0xA005] + + reloaded_exif = Image.Exif() + reloaded_exif.load(exif.tobytes()) + assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769) + + def test_exif_load_from_fp(self): + with Image.open("Tests/images/flower.jpg") as im: + data = im.info["exif"] + if data.startswith(b"Exif\x00\x00"): + data = data[6:] + fp = io.BytesIO(data) + + exif = Image.Exif() + exif.load_from_fp(fp) + assert exif == { + 271: "Canon", + 272: "Canon PowerShot S40", + 274: 1, + 282: 180.0, + 283: 180.0, + 296: 2, + 306: "2003:12:14 12:01:44", + 531: 1, + 34665: 196, + } + + @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) + def test_zero_tobytes(self, size): + im = Image.new("RGB", size) + assert im.tobytes() == b"" + + def test_apply_transparency(self): + im = Image.new("P", (1, 1)) + im.putpalette((0, 0, 0, 1, 1, 1)) + assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1} + + # Test that no transformation is applied without transparency + im.apply_transparency() + assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1} + + # Test that a transparency index is applied + im.info["transparency"] = 0 + im.apply_transparency() + assert "transparency" not in im.info + assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 255): 1} + + # Test that existing transparency is kept + im = Image.new("P", (1, 1)) + im.putpalette((0, 0, 0, 255, 1, 1, 1, 128), "RGBA") + im.info["transparency"] = 0 + im.apply_transparency() + assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 128): 1} + + # Test that transparency bytes are applied + with Image.open("Tests/images/pil123p.png") as im: + assert isinstance(im.info["transparency"], bytes) + assert im.palette.colors[(27, 35, 6)] == 24 + im.apply_transparency() + assert im.palette.colors[(27, 35, 6, 214)] == 24 + + def test_categories_deprecation(self): + with pytest.warns(DeprecationWarning): + assert hopper().category == 0 + + with pytest.warns(DeprecationWarning): + assert Image.NORMAL == 0 + with pytest.warns(DeprecationWarning): + assert Image.SEQUENCE == 1 + with pytest.warns(DeprecationWarning): + assert Image.CONTAINER == 2 + + def test_constants_deprecation(self): + with pytest.warns(DeprecationWarning): + assert Image.NEAREST == 0 + with pytest.warns(DeprecationWarning): + assert Image.NONE == 0 + + with pytest.warns(DeprecationWarning): + assert Image.LINEAR == Image.Resampling.BILINEAR + with pytest.warns(DeprecationWarning): + assert Image.CUBIC == Image.Resampling.BICUBIC + with pytest.warns(DeprecationWarning): + assert Image.ANTIALIAS == Image.Resampling.LANCZOS + + for enum in ( + Image.Transpose, + Image.Transform, + Image.Resampling, + Image.Dither, + Image.Palette, + Image.Quantize, + ): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(Image, name) == enum[name] + + @pytest.mark.parametrize( + "path", + [ + "fli_overrun.bin", + "sgi_overrun.bin", + "sgi_overrun_expandrow.bin", + "sgi_overrun_expandrow2.bin", + "pcx_overrun.bin", + "pcx_overrun2.bin", + "ossfuzz-4836216264589312.pcx", + "01r_00.pcx", + ], + ) + def test_overrun(self, path): + """For overrun completeness, test as: + valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c + """ + with Image.open(os.path.join("Tests/images", path)) as im: + try: + im.load() + assert False + except OSError as e: + buffer_overrun = str(e) == "buffer overrun when reading image file" + truncated = "image file is truncated" in str(e) + + assert buffer_overrun or truncated + + def test_fli_overrun2(self): + with Image.open("Tests/images/fli_overrun2.bin") as im: + try: + im.seek(1) + assert False + except OSError as e: + assert str(e) == "buffer overrun when reading image file" + + +class MockEncoder: pass @@ -538,24 +954,17 @@ def mock_encode(*args): return encoder -class TestRegistry(PillowTestCase): - +class TestRegistry: def test_encode_registry(self): - Image.register_encoder('MOCK', mock_encode) - self.assertIn('MOCK', Image.ENCODERS) + Image.register_encoder("MOCK", mock_encode) + assert "MOCK" in Image.ENCODERS - enc = Image._getencoder('RGB', 'MOCK', ('args',), extra=('extra',)) + enc = Image._getencoder("RGB", "MOCK", ("args",), extra=("extra",)) - self.assertIsInstance(enc, MockEncoder) - self.assertEqual(enc.args, ('RGB', 'args', 'extra')) + assert isinstance(enc, MockEncoder) + assert enc.args == ("RGB", "args", "extra") def test_encode_registry_fail(self): - self.assertRaises(IOError, Image._getencoder, 'RGB', - 'DoesNotExist', - ('args',), - extra=('extra',)) - - -if __name__ == '__main__': - unittest.main() + with pytest.raises(OSError): + Image._getencoder("RGB", "DoesNotExist", ("args",), extra=("extra",)) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index d0fcea1d444..955740b952a 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -1,26 +1,44 @@ -from helper import unittest, PillowTestCase, hopper, on_appveyor +import os +import subprocess +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 + +# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 +# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 +if os.environ.get("PYTHONOPTIMIZE") == "2": + cffi = None +else: + try: + import cffi + + from PIL import PyAccess + except ImportError: + cffi = None try: - from PIL import PyAccess + import numpy except ImportError: - # Skip in setUp() - pass + numpy = None -from PIL import Image -import sys -import os -class AccessTest(PillowTestCase): +class AccessTest: # initial value _init_cffi_access = Image.USE_CFFI_ACCESS _need_cffi_access = False @classmethod - def setUpClass(cls): + def setup_class(cls): Image.USE_CFFI_ACCESS = cls._need_cffi_access @classmethod - def tearDownClass(cls): + def teardown_class(cls): Image.USE_CFFI_ACCESS = cls._init_cffi_access @@ -34,7 +52,7 @@ def test_sanity(self): pos = x, y im2.putpixel(pos, im1.getpixel(pos)) - self.assert_image_equal(im1, im2) + assert_image_equal(im1, im2) im2 = Image.new(im1.mode, im1.size, 0) im2.readonly = 1 @@ -44,19 +62,67 @@ def test_sanity(self): pos = x, y im2.putpixel(pos, im1.getpixel(pos)) - self.assertFalse(im2.readonly) - self.assert_image_equal(im1, im2) + assert not im2.readonly + assert_image_equal(im1, im2) im2 = Image.new(im1.mode, im1.size, 0) pix1 = im1.load() pix2 = im2.load() + for x, y in ((0, "0"), ("0", 0)): + with pytest.raises(TypeError): + pix1[x, y] + for y in range(im1.size[1]): for x in range(im1.size[0]): pix2[x, y] = pix1[x, y] - self.assert_image_equal(im1, im2) + assert_image_equal(im1, im2) + + def test_sanity_negative_index(self): + im1 = hopper() + im2 = Image.new(im1.mode, im1.size, 0) + + width, height = im1.size + assert im1.getpixel((0, 0)) == im1.getpixel((-width, -height)) + assert im1.getpixel((-1, -1)) == im1.getpixel((width - 1, height - 1)) + + for y in range(-1, -im1.size[1] - 1, -1): + for x in range(-1, -im1.size[0] - 1, -1): + pos = x, y + im2.putpixel(pos, im1.getpixel(pos)) + + assert_image_equal(im1, im2) + + im2 = Image.new(im1.mode, im1.size, 0) + im2.readonly = 1 + + for y in range(-1, -im1.size[1] - 1, -1): + for x in range(-1, -im1.size[0] - 1, -1): + pos = x, y + im2.putpixel(pos, im1.getpixel(pos)) + + assert not im2.readonly + assert_image_equal(im1, im2) + + im2 = Image.new(im1.mode, im1.size, 0) + + pix1 = im1.load() + pix2 = im2.load() + + for y in range(-1, -im1.size[1] - 1, -1): + for x in range(-1, -im1.size[0] - 1, -1): + pix2[x, y] = pix1[x, y] + + assert_image_equal(im1, im2) + + @pytest.mark.skipif(numpy is None, reason="NumPy not installed") + def test_numpy(self): + im = hopper() + pix = im.load() + + assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59) class TestImageGetPixel(AccessTest): @@ -65,8 +131,7 @@ def color(mode): bands = Image.getmodebands(mode) if bands == 1: return 1 - else: - return tuple(range(1, bands + 1)) + return tuple(range(1, bands + 1)) def check(self, mode, c=None): if not c: @@ -75,75 +140,104 @@ def check(self, mode, c=None): # check putpixel im = Image.new(mode, (1, 1), None) im.putpixel((0, 0), c) - self.assertEqual( - im.getpixel((0, 0)), c, - "put/getpixel roundtrip failed for mode %s, color %s" % (mode, c)) + assert ( + im.getpixel((0, 0)) == c + ), f"put/getpixel roundtrip failed for mode {mode}, color {c}" + + # check putpixel negative index + im.putpixel((-1, -1), c) + assert ( + im.getpixel((-1, -1)) == c + ), f"put/getpixel roundtrip negative index failed for mode {mode}, color {c}" # Check 0 im = Image.new(mode, (0, 0), None) - with self.assertRaises(IndexError): + assert im.load() is not None + + error = ValueError if self._need_cffi_access else IndexError + with pytest.raises(error): im.putpixel((0, 0), c) - with self.assertRaises(IndexError): + with pytest.raises(error): im.getpixel((0, 0)) + # Check 0 negative index + with pytest.raises(error): + im.putpixel((-1, -1), c) + with pytest.raises(error): + im.getpixel((-1, -1)) # check initial color im = Image.new(mode, (1, 1), c) - self.assertEqual( - im.getpixel((0, 0)), c, - "initial color failed for mode %s, color %s " % (mode, c)) + assert ( + im.getpixel((0, 0)) == c + ), f"initial color failed for mode {mode}, color {c} " + # check initial color negative index + assert ( + im.getpixel((-1, -1)) == c + ), f"initial color failed with negative index for mode {mode}, color {c} " # Check 0 im = Image.new(mode, (0, 0), c) - with self.assertRaises(IndexError): + with pytest.raises(error): im.getpixel((0, 0)) - - def test_basic(self): - for mode in ("1", "L", "LA", "I", "I;16", "I;16B", "F", - "P", "PA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr"): - self.check(mode) - - def test_signedness(self): + # Check 0 negative index + with pytest.raises(error): + im.getpixel((-1, -1)) + + @pytest.mark.parametrize( + "mode", + ( + "1", + "L", + "LA", + "I", + "I;16", + "I;16B", + "F", + "P", + "PA", + "RGB", + "RGBA", + "RGBX", + "CMYK", + "YCbCr", + ), + ) + def test_basic(self, mode): + self.check(mode) + + @pytest.mark.parametrize("mode", ("I;16", "I;16B")) + def test_signedness(self, mode): # see https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* - for mode in ("I;16", "I;16B"): - self.check(mode, 2**15-1) - self.check(mode, 2**15) - self.check(mode, 2**15+1) - self.check(mode, 2**16-1) + self.check(mode, 2**15 - 1) + self.check(mode, 2**15) + self.check(mode, 2**15 + 1) + self.check(mode, 2**16 - 1) + + @pytest.mark.parametrize("mode", ("P", "PA")) + @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) + def test_p_putpixel_rgb_rgba(self, mode, color): + im = Image.new(mode, (1, 1)) + im.putpixel((0, 0), color) + + alpha = color[3] if len(color) == 4 and mode == "PA" else 255 + assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) +@pytest.mark.skipif(cffi is None, reason="No CFFI") class TestCffiPutPixel(TestImagePutPixel): _need_cffi_access = True - def setUp(self): - try: - import cffi - assert cffi # silence warning - except ImportError: - self.skipTest("No cffi") - +@pytest.mark.skipif(cffi is None, reason="No CFFI") class TestCffiGetPixel(TestImageGetPixel): _need_cffi_access = True - def setUp(self): - try: - import cffi - assert cffi # silence warning - except ImportError: - self.skipTest("No cffi") - +@pytest.mark.skipif(cffi is None, reason="No CFFI") class TestCffi(AccessTest): _need_cffi_access = True - def setUp(self): - try: - import cffi - assert cffi # silence warning - except ImportError: - self.skipTest("No cffi") - def _test_get_access(self, im): """Do we get the same thing as the old pixel access @@ -155,32 +249,32 @@ def _test_get_access(self, im): w, h = im.size for x in range(0, w, 10): for y in range(0, h, 10): - self.assertEqual(access[(x, y)], caccess[(x, y)]) + assert access[(x, y)] == caccess[(x, y)] # Access an out-of-range pixel - self.assertRaises(ValueError, - lambda: access[(access.xsize+1, access.ysize+1)]) + with pytest.raises(ValueError): + access[(access.xsize + 1, access.ysize + 1)] def test_get_vs_c(self): - rgb = hopper('RGB') + rgb = hopper("RGB") rgb.load() self._test_get_access(rgb) - self._test_get_access(hopper('RGBA')) - self._test_get_access(hopper('L')) - self._test_get_access(hopper('LA')) - self._test_get_access(hopper('1')) - self._test_get_access(hopper('P')) + self._test_get_access(hopper("RGBA")) + self._test_get_access(hopper("L")) + self._test_get_access(hopper("LA")) + self._test_get_access(hopper("1")) + self._test_get_access(hopper("P")) # self._test_get_access(hopper('PA')) # PA -- how do I make a PA image? - self._test_get_access(hopper('F')) + self._test_get_access(hopper("F")) - im = Image.new('I;16', (10, 10), 40000) + im = Image.new("I;16", (10, 10), 40000) self._test_get_access(im) - im = Image.new('I;16L', (10, 10), 40000) + im = Image.new("I;16L", (10, 10), 40000) self._test_get_access(im) - im = Image.new('I;16B', (10, 10), 40000) + im = Image.new("I;16B", (10, 10), 40000) self._test_get_access(im) - im = Image.new('I', (10, 10), 40000) + im = Image.new("I", (10, 10), 40000) self._test_get_access(im) # These don't actually appear to be modes that I can actually make, # as unpack sets them directly into the I mode. @@ -201,33 +295,33 @@ def _test_set_access(self, im, color): for x in range(0, w, 10): for y in range(0, h, 10): access[(x, y)] = color - self.assertEqual(color, caccess[(x, y)]) + assert color == caccess[(x, y)] # Attempt to set the value on a read-only image access = PyAccess.new(im, True) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): access[(0, 0)] = color def test_set_vs_c(self): - rgb = hopper('RGB') + rgb = hopper("RGB") rgb.load() self._test_set_access(rgb, (255, 128, 0)) - self._test_set_access(hopper('RGBA'), (255, 192, 128, 0)) - self._test_set_access(hopper('L'), 128) - self._test_set_access(hopper('LA'), (128, 128)) - self._test_set_access(hopper('1'), 255) - self._test_set_access(hopper('P'), 128) + self._test_set_access(hopper("RGBA"), (255, 192, 128, 0)) + self._test_set_access(hopper("L"), 128) + self._test_set_access(hopper("LA"), (128, 128)) + self._test_set_access(hopper("1"), 255) + self._test_set_access(hopper("P"), 128) # self._test_set_access(i, (128, 128)) #PA -- undone how to make - self._test_set_access(hopper('F'), 1024.0) + self._test_set_access(hopper("F"), 1024.0) - im = Image.new('I;16', (10, 10), 40000) + im = Image.new("I;16", (10, 10), 40000) self._test_set_access(im, 45000) - im = Image.new('I;16L', (10, 10), 40000) + im = Image.new("I;16L", (10, 10), 40000) self._test_set_access(im, 45000) - im = Image.new('I;16B', (10, 10), 40000) + im = Image.new("I;16B", (10, 10), 40000) self._test_set_access(im, 45000) - im = Image.new('I', (10, 10), 40000) + im = Image.new("I", (10, 10), 40000) self._test_set_access(im, 45000) # im = Image.new('I;32L', (10, 10), -(2**10)) # self._test_set_access(im, -(2**13)+1) @@ -235,7 +329,7 @@ def test_set_vs_c(self): # self._test_set_access(im, 2**13-1) def test_not_implemented(self): - self.assertIsNone(PyAccess.new(hopper("BGR;15"))) + assert PyAccess.new(hopper("BGR;15")) is None # ref https://github.com/python-pillow/Pillow/pull/2009 def test_reference_counting(self): @@ -243,37 +337,93 @@ def test_reference_counting(self): for _ in range(10): # Do not save references to the image, only to the access object - px = Image.new('L', (size, 1), 0).load() + px = Image.new("L", (size, 1), 0).load() for i in range(size): # pixels can contain garbage if image is released - self.assertEqual(px[i, 0], 0) - - -class TestEmbeddable(unittest.TestCase): - @unittest.skipIf(not sys.platform.startswith('win32') or - sys.version_info[:2] == (3, 4) or - on_appveyor(), # failing on appveyor when run from - # subprocess, not from shell - "requires Python 2.7 or >=3.5 for Windows") + assert px[i, 0] == 0 + + @pytest.mark.parametrize("mode", ("P", "PA")) + def test_p_putpixel_rgb_rgba(self, mode): + for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)): + im = Image.new(mode, (1, 1)) + access = PyAccess.new(im, False) + access.putpixel((0, 0), color) + + if len(color) == 3: + color += (255,) + assert im.convert("RGBA").getpixel((0, 0)) == color + + +class TestImagePutPixelError(AccessTest): + IMAGE_MODES1 = ["L", "LA", "RGB", "RGBA"] + IMAGE_MODES2 = ["I", "I;16", "BGR;15"] + INVALID_TYPES = ["foo", 1.0, None] + + @pytest.mark.parametrize("mode", IMAGE_MODES1) + def test_putpixel_type_error1(self, mode): + im = hopper(mode) + for v in self.INVALID_TYPES: + with pytest.raises(TypeError, match="color must be int or tuple"): + im.putpixel((0, 0), v) + + @pytest.mark.parametrize( + ("mode", "band_numbers", "match"), + ( + ("L", (0, 2), "color must be int or single-element tuple"), + ("LA", (0, 3), "color must be int, or tuple of one or two elements"), + ( + "RGB", + (0, 2, 5), + "color must be int, or tuple of one, three or four elements", + ), + ), + ) + def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match): + im = hopper(mode) + for band_number in band_numbers: + with pytest.raises(TypeError, match=match): + im.putpixel((0, 0), (0,) * band_number) + + @pytest.mark.parametrize("mode", IMAGE_MODES2) + def test_putpixel_type_error2(self, mode): + im = hopper(mode) + for v in self.INVALID_TYPES: + with pytest.raises( + TypeError, match="color must be int or single-element tuple" + ): + im.putpixel((0, 0), v) + + @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) + def test_putpixel_overflow_error(self, mode): + im = hopper(mode) + with pytest.raises(OverflowError): + im.putpixel((0, 0), 2**80) + + def test_putpixel_unrecognized_mode(self): + im = hopper("BGR;15") + with pytest.raises(ValueError, match="unrecognized image mode"): + im.putpixel((0, 0), 0) + + +class TestEmbeddable: + @pytest.mark.skipif( + not is_win32() or on_ci(), + reason="Failing on AppVeyor / GitHub Actions when run from subprocess, " + "not from shell", + ) def test_embeddable(self): - import subprocess import ctypes - import setuptools - from distutils import ccompiler, sysconfig - with open('embed_pil.c', 'w') as fh: - fh.write(""" + with open("embed_pil.c", "w", encoding="utf-8") as fh: + fh.write( + """ #include "Python.h" int main(int argc, char* argv[]) { char *home = "%s"; -#if PY_MAJOR_VERSION >= 3 wchar_t *whome = Py_DecodeLocale(home, NULL); Py_SetPythonHome(whome); -#else - Py_SetPythonHome(home); -#endif Py_InitializeEx(0); Py_DECREF(PyImport_ImportModule("PIL.Image")); @@ -283,32 +433,30 @@ def test_embeddable(self): Py_DECREF(PyImport_ImportModule("PIL.Image")); Py_Finalize(); -#if PY_MAJOR_VERSION >= 3 PyMem_RawFree(whome); -#endif return 0; } - """ % sys.prefix.replace('\\', '\\\\')) + """ + % sys.prefix.replace("\\", "\\\\") + ) - compiler = ccompiler.new_compiler() - compiler.add_include_dir(sysconfig.get_python_inc()) + compiler = new_compiler() + compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY")) - libdir = sysconfig.get_config_var('LIBDIR') or sysconfig.get_python_inc().replace('include', 'libs') - print (libdir) + libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var( + "INCLUDEPY" + ).replace("include", "libs") compiler.add_library_dir(libdir) - objects = compiler.compile(['embed_pil.c']) - compiler.link_executable(objects, 'embed_pil') + objects = compiler.compile(["embed_pil.c"]) + compiler.link_executable(objects, "embed_pil") env = os.environ.copy() - env["PATH"] = sys.prefix + ';' + env["PATH"] + env["PATH"] = sys.prefix + ";" + env["PATH"] # do not display the Windows Error Reporting dialog ctypes.windll.kernel32.SetErrorMode(0x0002) - process = subprocess.Popen(['embed_pil.exe'], env=env) + process = subprocess.Popen(["embed_pil.exe"], env=env) process.communicate() - self.assertEqual(process.returncode, 0) - -if __name__ == '__main__': - unittest.main() + assert process.returncode == 0 diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 11c2648bbc3..ae3518e44cb 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,59 +1,99 @@ -from helper import unittest, PillowTestCase, hopper +import pytest +from packaging.version import parse as parse_version from PIL import Image +from .helper import hopper + +numpy = pytest.importorskip("numpy", reason="NumPy not installed") + im = hopper().resize((128, 100)) -class TestImageArray(PillowTestCase): +def test_toarray(): + def test(mode): + ai = numpy.array(im.convert(mode)) + return ai.shape, ai.dtype.str, ai.nbytes + + def test_with_dtype(dtype): + ai = numpy.array(im, dtype=dtype) + assert ai.dtype == dtype + + # assert test("1") == ((100, 128), '|b1', 1600)) + assert test("L") == ((100, 128), "|u1", 12800) + + # FIXME: wrong? + assert test("I") == ((100, 128), Image._ENDIAN + "i4", 51200) + # FIXME: wrong? + assert test("F") == ((100, 128), Image._ENDIAN + "f4", 51200) + + assert test("LA") == ((100, 128, 2), "|u1", 25600) + assert test("RGB") == ((100, 128, 3), "|u1", 38400) + assert test("RGBA") == ((100, 128, 4), "|u1", 51200) + assert test("RGBX") == ((100, 128, 4), "|u1", 51200) + + test_with_dtype(numpy.float64) + test_with_dtype(numpy.uint8) + + with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated: + if parse_version(numpy.__version__) >= parse_version("1.23"): + with pytest.raises(OSError): + numpy.array(im_truncated) + else: + with pytest.warns(UserWarning): + numpy.array(im_truncated) - def test_toarray(self): - def test(mode): - ai = im.convert(mode).__array_interface__ - return ai['version'], ai["shape"], ai["typestr"], len(ai["data"]) - # self.assertEqual(test("1"), (3, (100, 128), '|b1', 1600)) - self.assertEqual(test("L"), (3, (100, 128), '|u1', 12800)) - # FIXME: wrong? - self.assertEqual(test("I"), (3, (100, 128), Image._ENDIAN + 'i4', 51200)) - # FIXME: wrong? - self.assertEqual(test("F"), (3, (100, 128), Image._ENDIAN + 'f4', 51200)) +def test_fromarray(): + class Wrapper: + """Class with API matching Image.fromarray""" - self.assertEqual(test("LA"), (3, (100, 128, 2), '|u1', 25600)) - self.assertEqual(test("RGB"), (3, (100, 128, 3), '|u1', 38400)) - self.assertEqual(test("RGBA"), (3, (100, 128, 4), '|u1', 51200)) - self.assertEqual(test("RGBX"), (3, (100, 128, 4), '|u1', 51200)) + def __init__(self, img, arr_params): + self.img = img + self.__array_interface__ = arr_params - def test_fromarray(self): + def tobytes(self): + return self.img.tobytes() - class Wrapper(object): - """ Class with API matching Image.fromarray """ + def test(mode): + i = im.convert(mode) + a = numpy.array(i) + # Make wrapper instance for image, new array interface + wrapped = Wrapper( + i, + { + "shape": a.shape, + "typestr": a.dtype.str, + "version": 3, + "data": a.data, + "strides": 1, # pretend it's non-contiguous + }, + ) + out = Image.fromarray(wrapped) + return out.mode, out.size, list(i.getdata()) == list(out.getdata()) - def __init__(self, img, arr_params): - self.img = img - self.__array_interface__ = arr_params + # assert test("1") == ("1", (128, 100), True) + assert test("L") == ("L", (128, 100), True) + assert test("I") == ("I", (128, 100), True) + assert test("F") == ("F", (128, 100), True) + assert test("LA") == ("LA", (128, 100), True) + assert test("RGB") == ("RGB", (128, 100), True) + assert test("RGBA") == ("RGBA", (128, 100), True) + assert test("RGBX") == ("RGBA", (128, 100), True) - def tobytes(self): - return self.img.tobytes() + # Test mode is None with no "typestr" in the array interface + with pytest.raises(TypeError): + wrapped = Wrapper(test("L"), {"shape": (100, 128)}) + Image.fromarray(wrapped) - def test(mode): - i = im.convert(mode) - a = i.__array_interface__ - a["strides"] = 1 # pretend it's non-contiguous - # Make wrapper instance for image, new array interface - wrapped = Wrapper(i, a) - out = Image.fromarray(wrapped) - return out.mode, out.size, list(i.getdata()) == list(out.getdata()) - # self.assertEqual(test("1"), ("1", (128, 100), True)) - self.assertEqual(test("L"), ("L", (128, 100), True)) - self.assertEqual(test("I"), ("I", (128, 100), True)) - self.assertEqual(test("F"), ("F", (128, 100), True)) - self.assertEqual(test("LA"), ("LA", (128, 100), True)) - self.assertEqual(test("RGB"), ("RGB", (128, 100), True)) - self.assertEqual(test("RGBA"), ("RGBA", (128, 100), True)) - self.assertEqual(test("RGBX"), ("RGBA", (128, 100), True)) +def test_fromarray_palette(): + # Arrange + i = im.convert("L") + a = numpy.array(i) + # Act + out = Image.fromarray(a, "P") -if __name__ == '__main__': - unittest.main() + # Assert that the Python and C palettes match + assert len(out.palette.colors) == len(out.im.getpalette()) / 3 diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 9fd0463d475..902d8bf8fa9 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -1,219 +1,340 @@ -from helper import unittest, PillowTestCase, hopper +import pytest from PIL import Image +from .helper import assert_image, assert_image_equal, assert_image_similar, hopper -class TestImageConvert(PillowTestCase): - def test_sanity(self): +def test_sanity(): + def convert(im, mode): + out = im.convert(mode) + assert out.mode == mode + assert out.size == im.size - def convert(im, mode): - out = im.convert(mode) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size) + modes = ( + "1", + "L", + "LA", + "P", + "PA", + "I", + "F", + "RGB", + "RGBA", + "RGBX", + "CMYK", + "YCbCr", + "HSV", + ) - modes = "1", "L", "I", "F", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr" + for input_mode in modes: + im = hopper(input_mode) + for output_mode in modes: + convert(im, output_mode) - for mode in modes: - im = hopper(mode) - for mode in modes: - convert(im, mode) + # Check 0 + im = Image.new(input_mode, (0, 0)) + for output_mode in modes: + convert(im, output_mode) - # Check 0 - im = Image.new(mode, (0, 0)) - for mode in modes: - convert(im, mode) - def test_default(self): +def test_unsupported_conversion(): + im = hopper() + with pytest.raises(ValueError): + im.convert("INVALID") - im = hopper("P") - self.assert_image(im, "P", im.size) - im = im.convert() - self.assert_image(im, "RGB", im.size) - im = im.convert() - self.assert_image(im, "RGB", im.size) - # ref https://github.com/python-pillow/Pillow/issues/274 +def test_default(): - def _test_float_conversion(self, im): - orig = im.getpixel((5, 5)) - converted = im.convert('F').getpixel((5, 5)) - self.assertEqual(orig, converted) + im = hopper("P") + assert im.mode == "P" + converted_im = im.convert() + assert_image(converted_im, "RGB", im.size) + converted_im = im.convert() + assert_image(converted_im, "RGB", im.size) - def test_8bit(self): - im = Image.open('Tests/images/hopper.jpg') - self._test_float_conversion(im.convert('L')) + im.info["transparency"] = 0 + converted_im = im.convert() + assert_image(converted_im, "RGBA", im.size) - def test_16bit(self): - im = Image.open('Tests/images/16bit.cropped.tif') - self._test_float_conversion(im) - def test_16bit_workaround(self): - im = Image.open('Tests/images/16bit.cropped.tif') - self._test_float_conversion(im.convert('I')) +# ref https://github.com/python-pillow/Pillow/issues/274 - def test_rgba_p(self): - im = hopper('RGBA') - im.putalpha(hopper('L')) - converted = im.convert('P') - comparable = converted.convert('RGBA') +def _test_float_conversion(im): + orig = im.getpixel((5, 5)) + converted = im.convert("F").getpixel((5, 5)) + assert orig == converted - self.assert_image_similar(im, comparable, 20) - def test_trns_p(self): - im = hopper('P') - im.info['transparency'] = 0 +def test_8bit(): + with Image.open("Tests/images/hopper.jpg") as im: + _test_float_conversion(im.convert("L")) - f = self.tempfile('temp.png') - l = im.convert('L') - self.assertEqual(l.info['transparency'], 0) # undone - l.save(f) +def test_16bit(): + with Image.open("Tests/images/16bit.cropped.tif") as im: + _test_float_conversion(im) - rgb = im.convert('RGB') - self.assertEqual(rgb.info['transparency'], (0, 0, 0)) # undone - rgb.save(f) + for color in (65535, 65536): + im = Image.new("I", (1, 1), color) + im_i16 = im.convert("I;16") + assert im_i16.getpixel((0, 0)) == 65535 - # ref https://github.com/python-pillow/Pillow/issues/664 - def test_trns_p_rgba(self): - # Arrange - im = hopper('P') - im.info['transparency'] = 128 +def test_16bit_workaround(): + with Image.open("Tests/images/16bit.cropped.tif") as im: + _test_float_conversion(im.convert("I")) - # Act - rgba = im.convert('RGBA') - # Assert - self.assertNotIn('transparency', rgba.info) +def test_opaque(): + alpha = hopper("P").convert("PA").getchannel("A") + + solid = Image.new("L", (128, 128), 255) + assert_image_equal(alpha, solid) + + +def test_rgba_p(): + im = hopper("RGBA") + im.putalpha(hopper("L")) + + converted = im.convert("P") + comparable = converted.convert("RGBA") + + assert_image_similar(im, comparable, 20) + + +def test_trns_p(tmp_path): + im = hopper("P") + im.info["transparency"] = 0 + + f = str(tmp_path / "temp.png") + + im_l = im.convert("L") + assert im_l.info["transparency"] == 1 # undone + im_l.save(f) + + im_rgb = im.convert("RGB") + assert im_rgb.info["transparency"] == (0, 1, 2) # undone + im_rgb.save(f) + + +# ref https://github.com/python-pillow/Pillow/issues/664 + + +@pytest.mark.parametrize("mode", ("LA", "PA", "RGBA")) +def test_trns_p_transparency(mode): + # Arrange + im = hopper("P") + im.info["transparency"] = 128 + + # Act + converted_im = im.convert(mode) + + # Assert + assert "transparency" not in converted_im.info + if mode == "PA": + assert converted_im.palette is not None + else: # https://github.com/python-pillow/Pillow/issues/2702 - self.assertEqual(rgba.palette, None) - - - def test_trns_l(self): - im = hopper('L') - im.info['transparency'] = 128 - - f = self.tempfile('temp.png') - - rgb = im.convert('RGB') - self.assertEqual(rgb.info['transparency'], (128, 128, 128)) # undone - rgb.save(f) - - p = im.convert('P') - self.assertIn('transparency', p.info) - p.save(f) - - p = self.assert_warning( - UserWarning, - im.convert, 'P', palette=Image.ADAPTIVE) - self.assertNotIn('transparency', p.info) - p.save(f) - - def test_trns_RGB(self): - im = hopper('RGB') - im.info['transparency'] = im.getpixel((0, 0)) - - f = self.tempfile('temp.png') - - l = im.convert('L') - self.assertEqual(l.info['transparency'], l.getpixel((0, 0))) # undone - l.save(f) - - p = im.convert('P') - self.assertIn('transparency', p.info) - p.save(f) - - p = im.convert('RGBA') - self.assertNotIn('transparency', p.info) - p.save(f) - - p = self.assert_warning( - UserWarning, - im.convert, 'P', palette=Image.ADAPTIVE) - self.assertNotIn('transparency', p.info) - p.save(f) - - def test_p_la(self): - im = hopper('RGBA') - alpha = hopper('L') - im.putalpha(alpha) - - comparable = im.convert('P').convert('LA').getchannel('A') - - self.assert_image_similar(alpha, comparable, 5) - - def test_matrix_illegal_conversion(self): - # Arrange - im = hopper('CMYK') - matrix = ( - 0.412453, 0.357580, 0.180423, 0, - 0.212671, 0.715160, 0.072169, 0, - 0.019334, 0.119193, 0.950227, 0) - self.assertNotEqual(im.mode, 'RGB') - - # Act / Assert - self.assertRaises(ValueError, - im.convert, mode='CMYK', matrix=matrix) - - def test_matrix_wrong_mode(self): - # Arrange - im = hopper('L') - matrix = ( - 0.412453, 0.357580, 0.180423, 0, - 0.212671, 0.715160, 0.072169, 0, - 0.019334, 0.119193, 0.950227, 0) - self.assertEqual(im.mode, 'L') - - # Act / Assert - self.assertRaises(ValueError, - im.convert, mode='L', matrix=matrix) - - def test_matrix_xyz(self): - - def matrix_convert(mode): - # Arrange - im = hopper('RGB') - matrix = ( - 0.412453, 0.357580, 0.180423, 0, - 0.212671, 0.715160, 0.072169, 0, - 0.019334, 0.119193, 0.950227, 0) - self.assertEqual(im.mode, 'RGB') - - # Act - # Convert an RGB image to the CIE XYZ colour space - converted_im = im.convert(mode=mode, matrix=matrix) - - # Assert - self.assertEqual(converted_im.mode, mode) - self.assertEqual(converted_im.size, im.size) - target = Image.open('Tests/images/hopper-XYZ.png') - if converted_im.mode == 'RGB': - self.assert_image_similar(converted_im, target, 3) - else: - self.assert_image_similar(converted_im, target.getchannel(0), 1) - - matrix_convert('RGB') - matrix_convert('L') - - def test_matrix_identity(self): - # Arrange - im = hopper('RGB') - identity_matrix = ( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0) - self.assertEqual(im.mode, 'RGB') - - # Act - # Convert with an identity matrix - converted_im = im.convert(mode='RGB', matrix=identity_matrix) - - # Assert - # No change - self.assert_image_equal(converted_im, im) - - -if __name__ == '__main__': - unittest.main() + assert converted_im.palette is None + + +def test_trns_l(tmp_path): + im = hopper("L") + im.info["transparency"] = 128 + + f = str(tmp_path / "temp.png") + + im_la = im.convert("LA") + assert "transparency" not in im_la.info + im_la.save(f) + + im_rgb = im.convert("RGB") + assert im_rgb.info["transparency"] == (128, 128, 128) # undone + im_rgb.save(f) + + im_p = im.convert("P") + assert "transparency" in im_p.info + im_p.save(f) + + im_p = im.convert("P", palette=Image.Palette.ADAPTIVE) + assert "transparency" in im_p.info + im_p.save(f) + + +def test_trns_RGB(tmp_path): + im = hopper("RGB") + im.info["transparency"] = im.getpixel((0, 0)) + + f = str(tmp_path / "temp.png") + + im_l = im.convert("L") + assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone + im_l.save(f) + + im_p = im.convert("P") + assert "transparency" in im_p.info + im_p.save(f) + + im_rgba = im.convert("RGBA") + assert "transparency" not in im_rgba.info + im_rgba.save(f) + + im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.Palette.ADAPTIVE) + assert "transparency" not in im_p.info + im_p.save(f) + + im = Image.new("RGB", (1, 1)) + im.info["transparency"] = im.getpixel((0, 0)) + im_p = im.convert("P", palette=Image.Palette.ADAPTIVE) + assert im_p.info["transparency"] == im_p.getpixel((0, 0)) + im_p.save(f) + + +@pytest.mark.parametrize("convert_mode", ("L", "LA", "I")) +def test_l_macro_rounding(convert_mode): + for mode in ("P", "PA"): + im = Image.new(mode, (1, 1)) + im.palette.getcolor((0, 1, 2)) + + converted_im = im.convert(convert_mode) + px = converted_im.load() + converted_color = px[0, 0] + if convert_mode == "LA": + converted_color = converted_color[0] + assert converted_color == 1 + + +def test_gif_with_rgba_palette_to_p(): + # See https://github.com/python-pillow/Pillow/issues/2433 + with Image.open("Tests/images/hopper.gif") as im: + im.info["transparency"] = 255 + im.load() + assert im.palette.mode == "RGB" + im_p = im.convert("P") + + # Should not raise ValueError: unrecognized raw mode + im_p.load() + + +def test_p_la(): + im = hopper("RGBA") + alpha = hopper("L") + im.putalpha(alpha) + + comparable = im.convert("P").convert("LA").getchannel("A") + + assert_image_similar(alpha, comparable, 5) + + +def test_p2pa_alpha(): + with Image.open("Tests/images/tiny.png") as im: + assert im.mode == "P" + + im_pa = im.convert("PA") + assert im_pa.mode == "PA" + + im_a = im_pa.getchannel("A") + for x in range(4): + alpha = 255 if x > 1 else 0 + for y in range(4): + assert im_a.getpixel((x, y)) == alpha + + +def test_p2pa_palette(): + with Image.open("Tests/images/tiny.png") as im: + im_pa = im.convert("PA") + assert im_pa.getpalette() == im.getpalette() + + +@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) +def test_rgb_lab(mode): + im = Image.new(mode, (1, 1)) + converted_im = im.convert("LAB") + assert converted_im.getpixel((0, 0)) == (0, 128, 128) + + im = Image.new("LAB", (1, 1), (255, 0, 0)) + converted_im = im.convert(mode) + assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) + + +def test_matrix_illegal_conversion(): + # Arrange + im = hopper("CMYK") + # fmt: off + matrix = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + # fmt: on + assert im.mode != "RGB" + + # Act / Assert + with pytest.raises(ValueError): + im.convert(mode="CMYK", matrix=matrix) + + +def test_matrix_wrong_mode(): + # Arrange + im = hopper("L") + # fmt: off + matrix = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + # fmt: on + assert im.mode == "L" + + # Act / Assert + with pytest.raises(ValueError): + im.convert(mode="L", matrix=matrix) + + +@pytest.mark.parametrize("mode", ("RGB", "L")) +def test_matrix_xyz(mode): + # Arrange + im = hopper("RGB") + im.info["transparency"] = (255, 0, 0) + # fmt: off + matrix = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + # fmt: on + assert im.mode == "RGB" + + # Act + # Convert an RGB image to the CIE XYZ colour space + converted_im = im.convert(mode=mode, matrix=matrix) + + # Assert + assert converted_im.mode == mode + assert converted_im.size == im.size + with Image.open("Tests/images/hopper-XYZ.png") as target: + if converted_im.mode == "RGB": + assert_image_similar(converted_im, target, 3) + assert converted_im.info["transparency"] == (105, 54, 4) + else: + assert_image_similar(converted_im, target.getchannel(0), 1) + assert converted_im.info["transparency"] == 105 + + +def test_matrix_identity(): + # Arrange + im = hopper("RGB") + # fmt: off + identity_matrix = ( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0) + # fmt: on + assert im.mode == "RGB" + + # Act + # Convert with an identity matrix + converted_im = im.convert(mode="RGB", matrix=identity_matrix) + + # Assert + # No change + assert_image_equal(converted_im, im) diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index bb1246a73f6..591832147d7 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -1,46 +1,44 @@ -from helper import unittest, PillowTestCase, hopper +import copy + +import pytest from PIL import Image -import copy +from .helper import hopper + + +@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) +def test_copy(mode): + cropped_coordinates = (10, 10, 20, 20) + cropped_size = (10, 10) + + # Internal copy method + im = hopper(mode) + out = im.copy() + assert out.mode == im.mode + assert out.size == im.size + + # Python's copy method + im = hopper(mode) + out = copy.copy(im) + assert out.mode == im.mode + assert out.size == im.size + + # Internal copy method on a cropped image + im = hopper(mode) + out = im.crop(cropped_coordinates).copy() + assert out.mode == im.mode + assert out.size == cropped_size + + # Python's copy method on a cropped image + im = hopper(mode) + out = copy.copy(im.crop(cropped_coordinates)) + assert out.mode == im.mode + assert out.size == cropped_size -class TestImageCopy(PillowTestCase): - - def test_copy(self): - croppedCoordinates = (10, 10, 20, 20) - croppedSize = (10, 10) - for mode in "1", "P", "L", "RGB", "I", "F": - # Internal copy method - im = hopper(mode) - out = im.copy() - self.assertEqual(out.mode, im.mode) - self.assertEqual(out.size, im.size) - - # Python's copy method - im = hopper(mode) - out = copy.copy(im) - self.assertEqual(out.mode, im.mode) - self.assertEqual(out.size, im.size) - - # Internal copy method on a cropped image - im = hopper(mode) - out = im.crop(croppedCoordinates).copy() - self.assertEqual(out.mode, im.mode) - self.assertEqual(out.size, croppedSize) - - # Python's copy method on a cropped image - im = hopper(mode) - out = copy.copy(im.crop(croppedCoordinates)) - self.assertEqual(out.mode, im.mode) - self.assertEqual(out.size, croppedSize) - - def test_copy_zero(self): - im = Image.new('RGB', (0, 0)) - out = im.copy() - self.assertEqual(out.mode, im.mode) - self.assertEqual(out.size, im.size) - - -if __name__ == '__main__': - unittest.main() +def test_copy_zero(): + im = Image.new("RGB", (0, 0)) + out = im.copy() + assert out.mode == im.mode + assert out.size == im.size diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index fe92dd865f0..4aa41de2792 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -1,108 +1,103 @@ -from helper import unittest, PillowTestCase, hopper +import pytest from PIL import Image +from .helper import assert_image_equal, hopper -class TestImageCrop(PillowTestCase): - def test_crop(self): - def crop(mode): - im = hopper(mode) - self.assert_image_equal(im.crop(), im) +@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) +def test_crop(mode): + im = hopper(mode) + assert_image_equal(im.crop(), im) - cropped = im.crop((50, 50, 100, 100)) - self.assertEqual(cropped.mode, mode) - self.assertEqual(cropped.size, (50, 50)) - for mode in "1", "P", "L", "RGB", "I", "F": - crop(mode) + cropped = im.crop((50, 50, 100, 100)) + assert cropped.mode == mode + assert cropped.size == (50, 50) - def test_wide_crop(self): - def crop(*bbox): - i = im.crop(bbox) - h = i.histogram() - while h and not h[-1]: - del h[-1] - return tuple(h) +def test_wide_crop(): + def crop(*bbox): + i = im.crop(bbox) + h = i.histogram() + while h and not h[-1]: + del h[-1] + return tuple(h) - im = Image.new("L", (100, 100), 1) + im = Image.new("L", (100, 100), 1) - self.assertEqual(crop(0, 0, 100, 100), (0, 10000)) - self.assertEqual(crop(25, 25, 75, 75), (0, 2500)) + assert crop(0, 0, 100, 100) == (0, 10000) + assert crop(25, 25, 75, 75) == (0, 2500) - # sides - self.assertEqual(crop(-25, 0, 25, 50), (1250, 1250)) - self.assertEqual(crop(0, -25, 50, 25), (1250, 1250)) - self.assertEqual(crop(75, 0, 125, 50), (1250, 1250)) - self.assertEqual(crop(0, 75, 50, 125), (1250, 1250)) + # sides + assert crop(-25, 0, 25, 50) == (1250, 1250) + assert crop(0, -25, 50, 25) == (1250, 1250) + assert crop(75, 0, 125, 50) == (1250, 1250) + assert crop(0, 75, 50, 125) == (1250, 1250) - self.assertEqual(crop(-25, 25, 125, 75), (2500, 5000)) - self.assertEqual(crop(25, -25, 75, 125), (2500, 5000)) + assert crop(-25, 25, 125, 75) == (2500, 5000) + assert crop(25, -25, 75, 125) == (2500, 5000) - # corners - self.assertEqual(crop(-25, -25, 25, 25), (1875, 625)) - self.assertEqual(crop(75, -25, 125, 25), (1875, 625)) - self.assertEqual(crop(75, 75, 125, 125), (1875, 625)) - self.assertEqual(crop(-25, 75, 25, 125), (1875, 625)) + # corners + assert crop(-25, -25, 25, 25) == (1875, 625) + assert crop(75, -25, 125, 25) == (1875, 625) + assert crop(75, 75, 125, 125) == (1875, 625) + assert crop(-25, 75, 25, 125) == (1875, 625) - def test_negative_crop(self): - # Check negative crop size (@PIL171) - im = Image.new("L", (512, 512)) - im = im.crop((400, 400, 200, 200)) +@pytest.mark.parametrize("box", ((8, 2, 2, 8), (2, 8, 8, 2), (8, 8, 2, 2))) +def test_negative_crop(box): + im = Image.new("RGB", (10, 10)) - self.assertEqual(im.size, (0, 0)) - self.assertEqual(len(im.getdata()), 0) - self.assertRaises(IndexError, lambda: im.getdata()[0]) + with pytest.raises(ValueError): + im.crop(box) - def test_crop_float(self): - # Check cropping floats are rounded to nearest integer - # https://github.com/python-pillow/Pillow/issues/1744 - # Arrange - im = Image.new("RGB", (10, 10)) - self.assertEqual(im.size, (10, 10)) +def test_crop_float(): + # Check cropping floats are rounded to nearest integer + # https://github.com/python-pillow/Pillow/issues/1744 - # Act - cropped = im.crop((0.9, 1.1, 4.2, 5.8)) + # Arrange + im = Image.new("RGB", (10, 10)) + assert im.size == (10, 10) - # Assert - self.assertEqual(cropped.size, (3, 5)) + # Act + cropped = im.crop((0.9, 1.1, 4.2, 5.8)) - def test_crop_crash(self): - # Image.crop crashes prepatch with an access violation - # apparently a use after free on windows, see - # https://github.com/python-pillow/Pillow/issues/1077 + # Assert + assert cropped.size == (3, 5) - test_img = 'Tests/images/bmp/g/pal8-0.bmp' - extents = (1, 1, 10, 10) - # works prepatch - img = Image.open(test_img) + +def test_crop_crash(): + # Image.crop crashes prepatch with an access violation + # apparently a use after free on Windows, see + # https://github.com/python-pillow/Pillow/issues/1077 + + test_img = "Tests/images/bmp/g/pal8-0.bmp" + extents = (1, 1, 10, 10) + # works prepatch + with Image.open(test_img) as img: img2 = img.crop(extents) - img2.load() + img2.load() - # fail prepatch - img = Image.open(test_img) + # fail prepatch + with Image.open(test_img) as img: img = img.crop(extents) - img.load() - - def test_crop_zero(self): + img.load() - im = Image.new('RGB', (0, 0), 'white') - cropped = im.crop((0, 0, 0, 0)) - self.assertEqual(cropped.size, (0, 0)) +def test_crop_zero(): - cropped = im.crop((10, 10, 20, 20)) - self.assertEqual(cropped.size, (10, 10)) - self.assertEqual(cropped.getdata()[0], (0, 0, 0)) + im = Image.new("RGB", (0, 0), "white") - im = Image.new('RGB', (0, 0)) + cropped = im.crop((0, 0, 0, 0)) + assert cropped.size == (0, 0) - cropped = im.crop((10, 10, 20, 20)) - self.assertEqual(cropped.size, (10, 10)) - self.assertEqual(cropped.getdata()[2], (0, 0, 0)) + cropped = im.crop((10, 10, 20, 20)) + assert cropped.size == (10, 10) + assert cropped.getdata()[0] == (0, 0, 0) + im = Image.new("RGB", (0, 0)) -if __name__ == '__main__': - unittest.main() + cropped = im.crop((10, 10, 20, 20)) + assert cropped.size == (10, 10) + assert cropped.getdata()[2] == (0, 0, 0) diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 12f5e0e9f38..8b4b447688f 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -1,74 +1,72 @@ -from helper import unittest, PillowTestCase, fromstring, tostring - from PIL import Image +from .helper import fromstring, skip_unless_feature, tostring -class TestImageDraft(PillowTestCase): - def setUp(self): - codecs = dir(Image.core) - if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: - self.skipTest("jpeg support not available") +pytestmark = skip_unless_feature("jpg") - def draft_roundtrip(self, in_mode, in_size, req_mode, req_size): - im = Image.new(in_mode, in_size) - data = tostring(im, 'JPEG') - im = fromstring(data) - im.draft(req_mode, req_size) - return im - def test_size(self): - for in_size, req_size, out_size in [ - ((435, 361), (2048, 2048), (435, 361)), # bigger - ((435, 361), (435, 361), (435, 361)), # same - ((128, 128), (64, 64), (64, 64)), - ((128, 128), (32, 32), (32, 32)), - ((128, 128), (16, 16), (16, 16)), +def draft_roundtrip(in_mode, in_size, req_mode, req_size): + im = Image.new(in_mode, in_size) + data = tostring(im, "JPEG") + im = fromstring(data) + mode, box = im.draft(req_mode, req_size) + scale, _ = im.decoderconfig + assert box[:2] == (0, 0) + assert (im.width - scale) < box[2] <= im.width + assert (im.height - scale) < box[3] <= im.height + return im - # large requested width - ((435, 361), (218, 128), (435, 361)), # almost 2x - ((435, 361), (217, 128), (218, 181)), # more than 2x - ((435, 361), (109, 64), (218, 181)), # almost 4x - ((435, 361), (108, 64), (109, 91)), # more than 4x - ((435, 361), (55, 32), (109, 91)), # almost 8x - ((435, 361), (54, 32), (55, 46)), # more than 8x - ((435, 361), (27, 16), (55, 46)), # more than 16x - # and vice versa - ((435, 361), (128, 181), (435, 361)), # almost 2x - ((435, 361), (128, 180), (218, 181)), # more than 2x - ((435, 361), (64, 91), (218, 181)), # almost 4x - ((435, 361), (64, 90), (109, 91)), # more than 4x - ((435, 361), (32, 46), (109, 91)), # almost 8x - ((435, 361), (32, 45), (55, 46)), # more than 8x - ((435, 361), (16, 22), (55, 46)), # more than 16x - ]: - im = self.draft_roundtrip('L', in_size, None, req_size) - im.load() - self.assertEqual(im.size, out_size) +def test_size(): + for in_size, req_size, out_size in [ + ((435, 361), (2048, 2048), (435, 361)), # bigger + ((435, 361), (435, 361), (435, 361)), # same + ((128, 128), (64, 64), (64, 64)), + ((128, 128), (32, 32), (32, 32)), + ((128, 128), (16, 16), (16, 16)), + # large requested width + ((435, 361), (218, 128), (435, 361)), # almost 2x + ((435, 361), (217, 128), (218, 181)), # more than 2x + ((435, 361), (109, 64), (218, 181)), # almost 4x + ((435, 361), (108, 64), (109, 91)), # more than 4x + ((435, 361), (55, 32), (109, 91)), # almost 8x + ((435, 361), (54, 32), (55, 46)), # more than 8x + ((435, 361), (27, 16), (55, 46)), # more than 16x + # and vice versa + ((435, 361), (128, 181), (435, 361)), # almost 2x + ((435, 361), (128, 180), (218, 181)), # more than 2x + ((435, 361), (64, 91), (218, 181)), # almost 4x + ((435, 361), (64, 90), (109, 91)), # more than 4x + ((435, 361), (32, 46), (109, 91)), # almost 8x + ((435, 361), (32, 45), (55, 46)), # more than 8x + ((435, 361), (16, 22), (55, 46)), # more than 16x + ]: + im = draft_roundtrip("L", in_size, None, req_size) + im.load() + assert im.size == out_size - def test_mode(self): - for in_mode, req_mode, out_mode in [ - ("RGB", "1", "RGB"), - ("RGB", "L", "L"), - ("RGB", "RGB", "RGB"), - ("RGB", "YCbCr", "YCbCr"), - ("L", "1", "L"), - ("L", "L", "L"), - ("L", "RGB", "L"), - ("L", "YCbCr", "L"), - ("CMYK", "1", "CMYK"), - ("CMYK", "L", "CMYK"), - ("CMYK", "RGB", "CMYK"), - ("CMYK", "YCbCr", "CMYK"), - ]: - im = self.draft_roundtrip(in_mode, (64, 64), req_mode, None) - im.load() - self.assertEqual(im.mode, out_mode) - def test_several_drafts(self): - im = self.draft_roundtrip('L', (128, 128), None, (64, 64)) - im.draft(None, (64, 64)) +def test_mode(): + for in_mode, req_mode, out_mode in [ + ("RGB", "1", "RGB"), + ("RGB", "L", "L"), + ("RGB", "RGB", "RGB"), + ("RGB", "YCbCr", "YCbCr"), + ("L", "1", "L"), + ("L", "L", "L"), + ("L", "RGB", "L"), + ("L", "YCbCr", "L"), + ("CMYK", "1", "CMYK"), + ("CMYK", "L", "CMYK"), + ("CMYK", "RGB", "CMYK"), + ("CMYK", "YCbCr", "CMYK"), + ]: + im = draft_roundtrip(in_mode, (64, 64), req_mode, None) im.load() + assert im.mode == out_mode + -if __name__ == '__main__': - unittest.main() +def test_several_drafts(): + im = draft_roundtrip("L", (128, 128), None, (64, 64)) + im.draft(None, (64, 64)) + im.load() diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py new file mode 100644 index 00000000000..ea5886e72db --- /dev/null +++ b/Tests/test_image_entropy.py @@ -0,0 +1,16 @@ +from .helper import hopper + + +def test_entropy(): + def entropy(mode): + return hopper(mode).entropy() + + assert round(abs(entropy("1") - 0.9138803254693582), 7) == 0 + assert round(abs(entropy("L") - 7.063008716585465), 7) == 0 + assert round(abs(entropy("I") - 7.063008716585465), 7) == 0 + assert round(abs(entropy("F") - 7.063008716585465), 7) == 0 + assert round(abs(entropy("P") - 5.082506854662517), 7) == 0 + assert round(abs(entropy("RGB") - 8.821286587714319), 7) == 0 + assert round(abs(entropy("RGBA") - 7.42724306524488), 7) == 0 + assert round(abs(entropy("CMYK") - 7.4272430652448795), 7) == 0 + assert round(abs(entropy("YCbCr") - 7.698360534903628), 7) == 0 diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 3636a73f77a..cfe46b65898 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -1,133 +1,175 @@ -from helper import unittest, PillowTestCase, hopper +import pytest from PIL import Image, ImageFilter - -class TestImageFilter(PillowTestCase): - - def test_sanity(self): - - def filter(filter): - for mode in ["L", "RGB", "CMYK"]: - im = hopper(mode) - out = im.filter(filter) - self.assertEqual(out.mode, im.mode) - self.assertEqual(out.size, im.size) - - filter(ImageFilter.BLUR) - filter(ImageFilter.CONTOUR) - filter(ImageFilter.DETAIL) - filter(ImageFilter.EDGE_ENHANCE) - filter(ImageFilter.EDGE_ENHANCE_MORE) - filter(ImageFilter.EMBOSS) - filter(ImageFilter.FIND_EDGES) - filter(ImageFilter.SMOOTH) - filter(ImageFilter.SMOOTH_MORE) - filter(ImageFilter.SHARPEN) - filter(ImageFilter.MaxFilter) - filter(ImageFilter.MedianFilter) - filter(ImageFilter.MinFilter) - filter(ImageFilter.ModeFilter) - filter(ImageFilter.GaussianBlur) - filter(ImageFilter.GaussianBlur(5)) - filter(ImageFilter.BoxBlur(5)) - filter(ImageFilter.UnsharpMask) - filter(ImageFilter.UnsharpMask(10)) - - self.assertRaises(TypeError, filter, "hello") - - def test_crash(self): - - # crashes on small images - im = Image.new("RGB", (1, 1)) - im.filter(ImageFilter.SMOOTH) - - im = Image.new("RGB", (2, 2)) - im.filter(ImageFilter.SMOOTH) - - im = Image.new("RGB", (3, 3)) - im.filter(ImageFilter.SMOOTH) - - def test_modefilter(self): - - def modefilter(mode): - im = Image.new(mode, (3, 3), None) - im.putdata(list(range(9))) - # image is: - # 0 1 2 - # 3 4 5 - # 6 7 8 - mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) - im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0 - mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) - return mod, mod2 - - self.assertEqual(modefilter("1"), (4, 0)) - self.assertEqual(modefilter("L"), (4, 0)) - self.assertEqual(modefilter("P"), (4, 0)) - self.assertEqual(modefilter("RGB"), ((4, 0, 0), (0, 0, 0))) - - def test_rankfilter(self): - - def rankfilter(mode): - im = Image.new(mode, (3, 3), None) - im.putdata(list(range(9))) - # image is: - # 0 1 2 - # 3 4 5 - # 6 7 8 - minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1)) - med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) - maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) - return minimum, med, maximum - - self.assertEqual(rankfilter("1"), (0, 4, 8)) - self.assertEqual(rankfilter("L"), (0, 4, 8)) - self.assertRaises(ValueError, rankfilter, "P") - self.assertEqual(rankfilter("RGB"), ((0, 0, 0), (4, 0, 0), (8, 0, 0))) - self.assertEqual(rankfilter("I"), (0, 4, 8)) - self.assertEqual(rankfilter("F"), (0.0, 4.0, 8.0)) - - def test_rankfilter_properties(self): - rankfilter = ImageFilter.RankFilter(1, 2) - - self.assertEqual(rankfilter.size, 1) - self.assertEqual(rankfilter.rank, 2) - - def test_consistency_3x3(self): - source = Image.open("Tests/images/hopper.bmp") - reference = Image.open("Tests/images/hopper_emboss.bmp") - kernel = ImageFilter.Kernel((3, 3), - (-1, -1, 0, - -1, 0, 1, - 0, 1, 1), .3) - source = source.split() * 2 - reference = reference.split() * 2 - - for mode in ['L', 'LA', 'RGB', 'CMYK']: - self.assert_image_equal( - Image.merge(mode, source[:len(mode)]).filter(kernel), - Image.merge(mode, reference[:len(mode)]), +from .helper import assert_image_equal, hopper + + +@pytest.mark.parametrize( + "filter_to_apply", + ( + ImageFilter.BLUR, + ImageFilter.CONTOUR, + ImageFilter.DETAIL, + ImageFilter.EDGE_ENHANCE, + ImageFilter.EDGE_ENHANCE_MORE, + ImageFilter.EMBOSS, + ImageFilter.FIND_EDGES, + ImageFilter.SMOOTH, + ImageFilter.SMOOTH_MORE, + ImageFilter.SHARPEN, + ImageFilter.MaxFilter, + ImageFilter.MedianFilter, + ImageFilter.MinFilter, + ImageFilter.ModeFilter, + ImageFilter.GaussianBlur, + ImageFilter.GaussianBlur(5), + ImageFilter.BoxBlur(5), + ImageFilter.UnsharpMask, + ImageFilter.UnsharpMask(10), + ), +) +@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK")) +def test_sanity(filter_to_apply, mode): + im = hopper(mode) + out = im.filter(filter_to_apply) + assert out.mode == im.mode + assert out.size == im.size + + +@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK")) +def test_sanity_error(mode): + with pytest.raises(TypeError): + im = hopper(mode) + im.filter("hello") + + +# crashes on small images +@pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3))) +def test_crash(size): + im = Image.new("RGB", size) + im.filter(ImageFilter.SMOOTH) + + +@pytest.mark.parametrize( + "mode, expected", + ( + ("1", (4, 0)), + ("L", (4, 0)), + ("P", (4, 0)), + ("RGB", ((4, 0, 0), (0, 0, 0))), + ), +) +def test_modefilter(mode, expected): + im = Image.new(mode, (3, 3), None) + im.putdata(list(range(9))) + # image is: + # 0 1 2 + # 3 4 5 + # 6 7 8 + mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) + im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0 + mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) + assert (mod, mod2) == expected + + +@pytest.mark.parametrize( + "mode, expected", + ( + ("1", (0, 4, 8)), + ("L", (0, 4, 8)), + ("RGB", ((0, 0, 0), (4, 0, 0), (8, 0, 0))), + ("I", (0, 4, 8)), + ("F", (0.0, 4.0, 8.0)), + ), +) +def test_rankfilter(mode, expected): + im = Image.new(mode, (3, 3), None) + im.putdata(list(range(9))) + # image is: + # 0 1 2 + # 3 4 5 + # 6 7 8 + minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1)) + med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) + maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) + assert (minimum, med, maximum) == expected + + +@pytest.mark.parametrize( + "filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter) +) +def test_rankfilter_error(filter): + with pytest.raises(ValueError): + im = Image.new("P", (3, 3), None) + im.putdata(list(range(9))) + # image is: + # 0 1 2 + # 3 4 5 + # 6 7 8 + im.filter(filter).getpixel((1, 1)) + + +def test_rankfilter_properties(): + rankfilter = ImageFilter.RankFilter(1, 2) + + assert rankfilter.size == 1 + assert rankfilter.rank == 2 + + +def test_builtinfilter_p(): + builtin_filter = ImageFilter.BuiltinFilter() + + with pytest.raises(ValueError): + builtin_filter.filter(hopper("P")) + + +def test_kernel_not_enough_coefficients(): + with pytest.raises(ValueError): + ImageFilter.Kernel((3, 3), (0, 0)) + + +@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK")) +def test_consistency_3x3(mode): + with Image.open("Tests/images/hopper.bmp") as source: + with Image.open("Tests/images/hopper_emboss.bmp") as reference: + kernel = ImageFilter.Kernel( + (3, 3), + # fmt: off + (-1, -1, 0, + -1, 0, 1, + 0, 1, 1), + # fmt: on + 0.3, ) + source = source.split() * 2 + reference = reference.split() * 2 - def test_consistency_5x5(self): - source = Image.open("Tests/images/hopper.bmp") - reference = Image.open("Tests/images/hopper_emboss_more.bmp") - kernel = ImageFilter.Kernel((5, 5), - (-1, -1, -1, -1, 0, - -1, -1, -1, 0, 1, - -1, -1, 0, 1, 1, - -1, 0, 1, 1, 1, - 0, 1, 1, 1, 1), 0.3) - source = source.split() * 2 - reference = reference.split() * 2 - - for mode in ['L', 'LA', 'RGB', 'CMYK']: - self.assert_image_equal( - Image.merge(mode, source[:len(mode)]).filter(kernel), - Image.merge(mode, reference[:len(mode)]), + assert_image_equal( + Image.merge(mode, source[: len(mode)]).filter(kernel), + Image.merge(mode, reference[: len(mode)]), ) -if __name__ == '__main__': - unittest.main() +@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK")) +def test_consistency_5x5(mode): + with Image.open("Tests/images/hopper.bmp") as source: + with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: + kernel = ImageFilter.Kernel( + (5, 5), + # fmt: off + (-1, -1, -1, -1, 0, + -1, -1, -1, 0, 1, + -1, -1, 0, 1, 1, + -1, 0, 1, 1, 1, + 0, 1, 1, 1, 1), + # fmt: on + 0.3, + ) + source = source.split() * 2 + reference = reference.split() * 2 + + assert_image_equal( + Image.merge(mode, source[: len(mode)]).filter(kernel), + Image.merge(mode, reference[: len(mode)]), + ) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 2d48bb6b86c..7fb05cda7b2 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -1,19 +1,10 @@ -from helper import unittest, PillowTestCase, hopper - from PIL import Image +from .helper import assert_image_equal, hopper -class TestImageFromBytes(PillowTestCase): - - def test_sanity(self): - im1 = hopper() - im2 = Image.frombytes(im1.mode, im1.size, im1.tobytes()) - - self.assert_image_equal(im1, im2) - - def test_not_implemented(self): - self.assertRaises(NotImplementedError, Image.fromstring) +def test_sanity(): + im1 = hopper() + im2 = Image.frombytes(im1.mode, im1.size, im1.tobytes()) -if __name__ == '__main__': - unittest.main() + assert_image_equal(im1, im2) diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py index 2e5d95aa74c..7fe992353bc 100644 --- a/Tests/test_image_fromqimage.py +++ b/Tests/test_image_fromqimage.py @@ -1,48 +1,66 @@ -from helper import unittest, PillowTestCase, hopper -from test_imageqt import PillowQtTestCase +import warnings -from PIL import ImageQt, Image +import pytest +from PIL import Image -class TestFromQImage(PillowQtTestCase, PillowTestCase): +with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=DeprecationWarning) + from PIL import ImageQt - files_to_test = [ +from .helper import assert_image_equal, hopper + +pytestmark = pytest.mark.skipif( + not ImageQt.qt_is_installed, reason="Qt bindings are not installed" +) + + +@pytest.fixture +def test_images(): + ims = [ hopper(), - Image.open('Tests/images/transparent.png'), - Image.open('Tests/images/7x13.png'), + Image.open("Tests/images/transparent.png"), + Image.open("Tests/images/7x13.png"), ] + try: + yield ims + finally: + for im in ims: + im.close() + + +def roundtrip(expected): + # PIL -> Qt + intermediate = expected.toqimage() + # Qt -> PIL + result = ImageQt.fromqimage(intermediate) + + if intermediate.hasAlphaChannel(): + assert_image_equal(result, expected.convert("RGBA")) + else: + assert_image_equal(result, expected.convert("RGB")) + - def roundtrip(self, expected): - # PIL -> Qt - intermediate = expected.toqimage() - # Qt -> PIL - result = ImageQt.fromqimage(intermediate) +def test_sanity_1(test_images): + for im in test_images: + roundtrip(im.convert("1")) - if intermediate.hasAlphaChannel(): - self.assert_image_equal(result, expected.convert('RGBA')) - else: - self.assert_image_equal(result, expected.convert('RGB')) - def test_sanity_1(self): - for im in self.files_to_test: - self.roundtrip(im.convert('1')) +def test_sanity_rgb(test_images): + for im in test_images: + roundtrip(im.convert("RGB")) - def test_sanity_rgb(self): - for im in self.files_to_test: - self.roundtrip(im.convert('RGB')) - def test_sanity_rgba(self): - for im in self.files_to_test: - self.roundtrip(im.convert('RGBA')) +def test_sanity_rgba(test_images): + for im in test_images: + roundtrip(im.convert("RGBA")) - def test_sanity_l(self): - for im in self.files_to_test: - self.roundtrip(im.convert('L')) - def test_sanity_p(self): - for im in self.files_to_test: - self.roundtrip(im.convert('P')) +def test_sanity_l(test_images): + for im in test_images: + roundtrip(im.convert("L")) -if __name__ == '__main__': - unittest.main() +def test_sanity_p(test_images): + for im in test_images: + roundtrip(im.convert("P")) diff --git a/Tests/test_image_fromqpixmap.py b/Tests/test_image_fromqpixmap.py deleted file mode 100644 index 543b74bbf24..00000000000 --- a/Tests/test_image_fromqpixmap.py +++ /dev/null @@ -1,32 +0,0 @@ -from helper import unittest, PillowTestCase, hopper -from test_imageqt import PillowQtTestCase, PillowQPixmapTestCase - -from PIL import ImageQt - - -class TestFromQPixmap(PillowQPixmapTestCase, PillowTestCase): - - def roundtrip(self, expected): - PillowQtTestCase.setUp(self) - result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) - # Qt saves all pixmaps as rgb - self.assert_image_equal(result, expected.convert('RGB')) - - def test_sanity_1(self): - self.roundtrip(hopper('1')) - - def test_sanity_rgb(self): - self.roundtrip(hopper('RGB')) - - def test_sanity_rgba(self): - self.roundtrip(hopper('RGBA')) - - def test_sanity_l(self): - self.roundtrip(hopper('L')) - - def test_sanity_p(self): - self.roundtrip(hopper('P')) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_getbands.py b/Tests/test_image_getbands.py index 5eecbf044b5..08fc12c1cf4 100644 --- a/Tests/test_image_getbands.py +++ b/Tests/test_image_getbands.py @@ -1,24 +1,13 @@ -from helper import unittest, PillowTestCase - from PIL import Image -class TestImageGetBands(PillowTestCase): - - def test_getbands(self): - self.assertEqual(Image.new("1", (1, 1)).getbands(), ("1",)) - self.assertEqual(Image.new("L", (1, 1)).getbands(), ("L",)) - self.assertEqual(Image.new("I", (1, 1)).getbands(), ("I",)) - self.assertEqual(Image.new("F", (1, 1)).getbands(), ("F",)) - self.assertEqual(Image.new("P", (1, 1)).getbands(), ("P",)) - self.assertEqual(Image.new("RGB", (1, 1)).getbands(), ("R", "G", "B")) - self.assertEqual( - Image.new("RGBA", (1, 1)).getbands(), ("R", "G", "B", "A")) - self.assertEqual( - Image.new("CMYK", (1, 1)).getbands(), ("C", "M", "Y", "K")) - self.assertEqual( - Image.new("YCbCr", (1, 1)).getbands(), ("Y", "Cb", "Cr")) - - -if __name__ == '__main__': - unittest.main() +def test_getbands(): + assert Image.new("1", (1, 1)).getbands() == ("1",) + assert Image.new("L", (1, 1)).getbands() == ("L",) + assert Image.new("I", (1, 1)).getbands() == ("I",) + assert Image.new("F", (1, 1)).getbands() == ("F",) + assert Image.new("P", (1, 1)).getbands() == ("P",) + assert Image.new("RGB", (1, 1)).getbands() == ("R", "G", "B") + assert Image.new("RGBA", (1, 1)).getbands() == ("R", "G", "B", "A") + assert Image.new("CMYK", (1, 1)).getbands() == ("C", "M", "Y", "K") + assert Image.new("YCbCr", (1, 1)).getbands() == ("Y", "Cb", "Cr") diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index f290321435f..c86e33eb2fb 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -1,43 +1,41 @@ -from helper import unittest, PillowTestCase, hopper - from PIL import Image +from .helper import hopper -class TestImageGetBbox(PillowTestCase): - - def test_sanity(self): - - bbox = hopper().getbbox() - self.assertIsInstance(bbox, tuple) - def test_bbox(self): +def test_sanity(): - # 8-bit mode - im = Image.new("L", (100, 100), 0) - self.assertIsNone(im.getbbox()) + bbox = hopper().getbbox() + assert isinstance(bbox, tuple) - im.paste(255, (10, 25, 90, 75)) - self.assertEqual(im.getbbox(), (10, 25, 90, 75)) - im.paste(255, (25, 10, 75, 90)) - self.assertEqual(im.getbbox(), (10, 10, 90, 90)) +def test_bbox(): + def check(im, fill_color): + assert im.getbbox() is None - im.paste(255, (-10, -10, 110, 110)) - self.assertEqual(im.getbbox(), (0, 0, 100, 100)) + im.paste(fill_color, (10, 25, 90, 75)) + assert im.getbbox() == (10, 25, 90, 75) - # 32-bit mode - im = Image.new("RGB", (100, 100), 0) - self.assertIsNone(im.getbbox()) + im.paste(fill_color, (25, 10, 75, 90)) + assert im.getbbox() == (10, 10, 90, 90) - im.paste(255, (10, 25, 90, 75)) - self.assertEqual(im.getbbox(), (10, 25, 90, 75)) + im.paste(fill_color, (-10, -10, 110, 110)) + assert im.getbbox() == (0, 0, 100, 100) - im.paste(255, (25, 10, 75, 90)) - self.assertEqual(im.getbbox(), (10, 10, 90, 90)) + # 8-bit mode + im = Image.new("L", (100, 100), 0) + check(im, 255) - im.paste(255, (-10, -10, 110, 110)) - self.assertEqual(im.getbbox(), (0, 0, 100, 100)) + # 32-bit mode + im = Image.new("RGB", (100, 100), 0) + check(im, 255) + for mode in ("RGBA", "RGBa"): + for color in ((0, 0, 0, 0), (127, 127, 127, 0), (255, 255, 255, 0)): + im = Image.new(mode, (100, 100), color) + check(im, (255, 255, 255, 255)) -if __name__ == '__main__': - unittest.main() + for mode in ("La", "LA", "PA"): + for color in ((0, 0), (127, 0), (255, 0)): + im = Image.new(mode, (100, 100), color) + check(im, (255, 255)) diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py index ca7a9d93d75..7fd0398f947 100644 --- a/Tests/test_image_getcolors.py +++ b/Tests/test_image_getcolors.py @@ -1,71 +1,68 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import hopper -class TestImageGetColors(PillowTestCase): +def test_getcolors(): + def getcolors(mode, limit=None): + im = hopper(mode) + if limit: + colors = im.getcolors(limit) + else: + colors = im.getcolors() + if colors: + return len(colors) + return None - def test_getcolors(self): + assert getcolors("1") == 2 + assert getcolors("L") == 255 + assert getcolors("I") == 255 + assert getcolors("F") == 255 + assert getcolors("P") == 96 # fixed palette + assert getcolors("RGB") is None + assert getcolors("RGBA") is None + assert getcolors("CMYK") is None + assert getcolors("YCbCr") is None - def getcolors(mode, limit=None): - im = hopper(mode) - if limit: - colors = im.getcolors(limit) - else: - colors = im.getcolors() - if colors: - return len(colors) - return None + assert getcolors("L", 128) is None + assert getcolors("L", 1024) == 255 - self.assertEqual(getcolors("1"), 2) - self.assertEqual(getcolors("L"), 255) - self.assertEqual(getcolors("I"), 255) - self.assertEqual(getcolors("F"), 255) - self.assertEqual(getcolors("P"), 90) # fixed palette - self.assertIsNone(getcolors("RGB")) - self.assertIsNone(getcolors("RGBA")) - self.assertIsNone(getcolors("CMYK")) - self.assertIsNone(getcolors("YCbCr")) + assert getcolors("RGB", 8192) is None + assert getcolors("RGB", 16384) == 10100 + assert getcolors("RGB", 100000) == 10100 - self.assertIsNone(getcolors("L", 128)) - self.assertEqual(getcolors("L", 1024), 255) + assert getcolors("RGBA", 16384) == 10100 + assert getcolors("CMYK", 16384) == 10100 + assert getcolors("YCbCr", 16384) == 9329 - self.assertIsNone(getcolors("RGB", 8192)) - self.assertEqual(getcolors("RGB", 16384), 10100) - self.assertEqual(getcolors("RGB", 100000), 10100) - self.assertEqual(getcolors("RGBA", 16384), 10100) - self.assertEqual(getcolors("CMYK", 16384), 10100) - self.assertEqual(getcolors("YCbCr", 16384), 9329) +# -------------------------------------------------------------------- - # -------------------------------------------------------------------- - def test_pack(self): - # Pack problems for small tables (@PIL209) +def test_pack(): + # Pack problems for small tables (@PIL209) - im = hopper().quantize(3).convert("RGB") + im = hopper().quantize(3).convert("RGB") - expected = [(4039, (172, 166, 181)), - (4385, (124, 113, 134)), - (7960, (31, 20, 33))] + expected = [ + (4039, (172, 166, 181)), + (4385, (124, 113, 134)), + (7960, (31, 20, 33)), + ] - A = im.getcolors(maxcolors=2) - self.assertIsNone(A) + A = im.getcolors(maxcolors=2) + assert A is None - A = im.getcolors(maxcolors=3) - A.sort() - self.assertEqual(A, expected) + A = im.getcolors(maxcolors=3) + A.sort() + assert A == expected - A = im.getcolors(maxcolors=4) - A.sort() - self.assertEqual(A, expected) + A = im.getcolors(maxcolors=4) + A.sort() + assert A == expected - A = im.getcolors(maxcolors=8) - A.sort() - self.assertEqual(A, expected) + A = im.getcolors(maxcolors=8) + A.sort() + assert A == expected - A = im.getcolors(maxcolors=16) - A.sort() - self.assertEqual(A, expected) - - -if __name__ == '__main__': - unittest.main() + A = im.getcolors(maxcolors=16) + A.sort() + assert A == expected diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index ef07844df5e..36c81b40f07 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -1,33 +1,28 @@ -from helper import unittest, PillowTestCase, hopper +from PIL import Image +from .helper import hopper -class TestImageGetData(PillowTestCase): - def test_sanity(self): +def test_sanity(): + data = hopper().getdata() - data = hopper().getdata() + len(data) + list(data) - len(data) - list(data) + assert data[0] == (20, 20, 70) - self.assertEqual(data[0], (20, 20, 70)) - def test_roundtrip(self): +def test_roundtrip(): + def getdata(mode): + im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST) + data = im.getdata() + return data[0], len(data), len(list(data)) - def getdata(mode): - im = hopper(mode).resize((32, 30)) - data = im.getdata() - return data[0], len(data), len(list(data)) - - self.assertEqual(getdata("1"), (0, 960, 960)) - self.assertEqual(getdata("L"), (16, 960, 960)) - self.assertEqual(getdata("I"), (16, 960, 960)) - self.assertEqual(getdata("F"), (16.0, 960, 960)) - self.assertEqual(getdata("RGB"), (((11, 13, 52), 960, 960))) - self.assertEqual(getdata("RGBA"), ((11, 13, 52, 255), 960, 960)) - self.assertEqual(getdata("CMYK"), ((244, 242, 203, 0), 960, 960)) - self.assertEqual(getdata("YCbCr"), ((16, 147, 123), 960, 960)) - - -if __name__ == '__main__': - unittest.main() + assert getdata("1") == (0, 960, 960) + assert getdata("L") == (17, 960, 960) + assert getdata("I") == (17, 960, 960) + assert getdata("F") == (17.0, 960, 960) + assert getdata("RGB") == ((11, 13, 52), 960, 960) + assert getdata("RGBA") == ((11, 13, 52, 255), 960, 960) + assert getdata("CMYK") == ((244, 242, 203, 0), 960, 960) + assert getdata("YCbCr") == ((16, 147, 123), 960, 960) diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index 0b0c31b866b..710794da426 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -1,25 +1,25 @@ -from helper import unittest, PillowTestCase, hopper +from PIL import Image +from .helper import hopper -class TestImageGetExtrema(PillowTestCase): - def test_extrema(self): +def test_extrema(): + def extrema(mode): + return hopper(mode).getextrema() - def extrema(mode): - return hopper(mode).getextrema() + assert extrema("1") == (0, 255) + assert extrema("L") == (1, 255) + assert extrema("I") == (1, 255) + assert extrema("F") == (1, 255) + assert extrema("P") == (0, 225) # fixed palette + assert extrema("RGB") == ((0, 255), (0, 255), (0, 255)) + assert extrema("RGBA") == ((0, 255), (0, 255), (0, 255), (255, 255)) + assert extrema("CMYK") == ((0, 255), (0, 255), (0, 255), (0, 0)) + assert extrema("I;16") == (1, 255) - self.assertEqual(extrema("1"), (0, 255)) - self.assertEqual(extrema("L"), (0, 255)) - self.assertEqual(extrema("I"), (0, 255)) - self.assertEqual(extrema("F"), (0, 255)) - self.assertEqual(extrema("P"), (0, 225)) # fixed palette - self.assertEqual( - extrema("RGB"), ((0, 255), (0, 255), (0, 255))) - self.assertEqual( - extrema("RGBA"), ((0, 255), (0, 255), (0, 255), (255, 255))) - self.assertEqual( - extrema("CMYK"), (((0, 255), (0, 255), (0, 255), (0, 0)))) - -if __name__ == '__main__': - unittest.main() +def test_true_16(): + with Image.open("Tests/images/16_bit_noise.tif") as im: + assert im.mode == "I;16" + extrema = im.getextrema() + assert extrema == (106, 285) diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index bc562de5ab8..746e63b1551 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,17 +1,9 @@ -from helper import unittest, PillowTestCase, hopper, py3 +from .helper import hopper -class TestImageGetIm(PillowTestCase): +def test_sanity(): + im = hopper() + type_repr = repr(type(im.getim())) - def test_sanity(self): - im = hopper() - type_repr = repr(type(im.getim())) - - if py3: - self.assertIn("PyCapsule", type_repr) - - self.assertIsInstance(im.im.id, int) - - -if __name__ == '__main__': - unittest.main() + assert "PyCapsule" in type_repr + assert isinstance(im.im.id, int) diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index 01a6ac7ada5..58a6dacbbbb 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -1,24 +1,44 @@ -from helper import unittest, PillowTestCase, hopper - - -class TestImageGetPalette(PillowTestCase): - - def test_palette(self): - def palette(mode): - p = hopper(mode).getpalette() - if p: - return p[:10] - return None - self.assertIsNone(palette("1")) - self.assertIsNone(palette("L")) - self.assertIsNone(palette("I")) - self.assertIsNone(palette("F")) - self.assertEqual(palette("P"), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - self.assertIsNone(palette("RGB")) - self.assertIsNone(palette("RGBA")) - self.assertIsNone(palette("CMYK")) - self.assertIsNone(palette("YCbCr")) - - -if __name__ == '__main__': - unittest.main() +from PIL import Image + +from .helper import hopper + + +def test_palette(): + def palette(mode): + p = hopper(mode).getpalette() + if p: + return p[:10] + return None + + assert palette("1") is None + assert palette("L") is None + assert palette("I") is None + assert palette("F") is None + assert palette("P") == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + assert palette("RGB") is None + assert palette("RGBA") is None + assert palette("CMYK") is None + assert palette("YCbCr") is None + + +def test_palette_rawmode(): + im = Image.new("P", (1, 1)) + im.putpalette((1, 2, 3)) + + for rawmode in ("RGB", None): + rgb = im.getpalette(rawmode) + assert rgb == [1, 2, 3] + + # Convert the RGB palette to RGBA + rgba = im.getpalette("RGBA") + assert rgba == [1, 2, 3, 255] + + im.putpalette((1, 2, 3, 4), "RGBA") + + # Convert the RGBA palette to RGB + rgb = im.getpalette("RGB") + assert rgb == [1, 2, 3] + + for rawmode in ("RGBA", None): + rgba = im.getpalette(rawmode) + assert rgba == [1, 2, 3, 4] diff --git a/Tests/test_image_getprojection.py b/Tests/test_image_getprojection.py index 9d3f2d9edc0..f65d40708b1 100644 --- a/Tests/test_image_getprojection.py +++ b/Tests/test_image_getprojection.py @@ -1,36 +1,29 @@ -from helper import unittest, PillowTestCase, hopper - from PIL import Image +from .helper import hopper -class TestImageGetProjection(PillowTestCase): - - def test_sanity(self): - - im = hopper() - - projection = im.getprojection() - self.assertEqual(len(projection), 2) - self.assertEqual(len(projection[0]), im.size[0]) - self.assertEqual(len(projection[1]), im.size[1]) +def test_sanity(): + im = hopper() - # 8-bit image - im = Image.new("L", (10, 10)) - self.assertEqual(im.getprojection()[0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - self.assertEqual(im.getprojection()[1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - im.paste(255, (2, 4, 8, 6)) - self.assertEqual(im.getprojection()[0], [0, 0, 1, 1, 1, 1, 1, 1, 0, 0]) - self.assertEqual(im.getprojection()[1], [0, 0, 0, 0, 1, 1, 0, 0, 0, 0]) + projection = im.getprojection() - # 32-bit image - im = Image.new("RGB", (10, 10)) - self.assertEqual(im.getprojection()[0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - self.assertEqual(im.getprojection()[1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - im.paste(255, (2, 4, 8, 6)) - self.assertEqual(im.getprojection()[0], [0, 0, 1, 1, 1, 1, 1, 1, 0, 0]) - self.assertEqual(im.getprojection()[1], [0, 0, 0, 0, 1, 1, 0, 0, 0, 0]) + assert len(projection) == 2 + assert len(projection[0]) == im.size[0] + assert len(projection[1]) == im.size[1] + # 8-bit image + im = Image.new("L", (10, 10)) + assert im.getprojection()[0] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + assert im.getprojection()[1] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + im.paste(255, (2, 4, 8, 6)) + assert im.getprojection()[0] == [0, 0, 1, 1, 1, 1, 1, 1, 0, 0] + assert im.getprojection()[1] == [0, 0, 0, 0, 1, 1, 0, 0, 0, 0] -if __name__ == '__main__': - unittest.main() + # 32-bit image + im = Image.new("RGB", (10, 10)) + assert im.getprojection()[0] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + assert im.getprojection()[1] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + im.paste(255, (2, 4, 8, 6)) + assert im.getprojection()[0] == [0, 0, 1, 1, 1, 1, 1, 1, 0, 0] + assert im.getprojection()[1] == [0, 0, 0, 0, 1, 1, 0, 0, 0, 0] diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index 892e89328fd..0ee52e724e1 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -1,24 +1,17 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import hopper -class TestImageHistogram(PillowTestCase): +def test_histogram(): + def histogram(mode): + h = hopper(mode).histogram() + return len(h), min(h), max(h) - def test_histogram(self): - - def histogram(mode): - h = hopper(mode).histogram() - return len(h), min(h), max(h) - - self.assertEqual(histogram("1"), (256, 0, 10994)) - self.assertEqual(histogram("L"), (256, 0, 638)) - self.assertEqual(histogram("I"), (256, 0, 638)) - self.assertEqual(histogram("F"), (256, 0, 638)) - self.assertEqual(histogram("P"), (256, 0, 1871)) - self.assertEqual(histogram("RGB"), (768, 4, 675)) - self.assertEqual(histogram("RGBA"), (1024, 0, 16384)) - self.assertEqual(histogram("CMYK"), (1024, 0, 16384)) - self.assertEqual(histogram("YCbCr"), (768, 0, 1908)) - - -if __name__ == '__main__': - unittest.main() + assert histogram("1") == (256, 0, 10994) + assert histogram("L") == (256, 0, 662) + assert histogram("I") == (256, 0, 662) + assert histogram("F") == (256, 0, 662) + assert histogram("P") == (256, 0, 1551) + assert histogram("RGB") == (768, 4, 675) + assert histogram("RGBA") == (1024, 0, 16384) + assert histogram("CMYK") == (1024, 0, 16384) + assert histogram("YCbCr") == (768, 0, 1908) diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 15a92e339d5..f7fe99bb4c2 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -1,33 +1,50 @@ -from helper import unittest, PillowTestCase, hopper +import logging +import os + +import pytest from PIL import Image -import os +from .helper import hopper -class TestImageLoad(PillowTestCase): +def test_sanity(): + im = hopper() + pix = im.load() - def test_sanity(self): + assert pix[0, 0] == (20, 20, 70) - im = hopper() - pix = im.load() +def test_close(): + im = Image.open("Tests/images/hopper.gif") + im.close() + with pytest.raises(ValueError): + im.load() + with pytest.raises(ValueError): + im.getpixel((0, 0)) - self.assertEqual(pix[0, 0], (20, 20, 70)) - def test_close(self): - im = Image.open("Tests/images/hopper.gif") +def test_close_after_load(caplog): + im = Image.open("Tests/images/hopper.gif") + im.load() + with caplog.at_level(logging.DEBUG): im.close() - self.assertRaises(ValueError, im.load) - self.assertRaises(ValueError, im.getpixel, (0, 0)) + assert len(caplog.records) == 0 + + +def test_contextmanager(): + fn = None + with Image.open("Tests/images/hopper.gif") as im: + fn = im.fp.fileno() + os.fstat(fn) + + with pytest.raises(OSError): + os.fstat(fn) - def test_contextmanager(self): - fn = None - with Image.open("Tests/images/hopper.gif") as im: - fn = im.fp.fileno() - os.fstat(fn) - self.assertRaises(OSError, os.fstat, fn) +def test_contextmanager_non_exclusive_fp(): + with open("Tests/images/hopper.gif", "rb") as fp: + with Image.open(fp): + pass -if __name__ == '__main__': - unittest.main() + assert not fp.closed diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index 0596af3977f..670b2f4ebde 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -1,57 +1,71 @@ -from helper import unittest, PillowTestCase, hopper +from PIL import Image, ImageMode -from PIL import Image +from .helper import hopper -class TestImageMode(PillowTestCase): +def test_sanity(): - def test_sanity(self): - - im = hopper() + with hopper() as im: im.mode - from PIL import ImageMode - - ImageMode.getmode("1") - ImageMode.getmode("L") - ImageMode.getmode("P") - ImageMode.getmode("RGB") - ImageMode.getmode("I") - ImageMode.getmode("F") - - m = ImageMode.getmode("1") - self.assertEqual(m.mode, "1") - self.assertEqual(str(m), "1") - self.assertEqual(m.bands, ("1",)) - self.assertEqual(m.basemode, "L") - self.assertEqual(m.basetype, "L") - - m = ImageMode.getmode("RGB") - self.assertEqual(m.mode, "RGB") - self.assertEqual(str(m), "RGB") - self.assertEqual(m.bands, ("R", "G", "B")) - self.assertEqual(m.basemode, "RGB") - self.assertEqual(m.basetype, "L") - - def test_properties(self): - def check(mode, *result): - signature = ( - Image.getmodebase(mode), Image.getmodetype(mode), - Image.getmodebands(mode), Image.getmodebandnames(mode), - ) - self.assertEqual(signature, result) - check("1", "L", "L", 1, ("1",)) - check("L", "L", "L", 1, ("L",)) - check("P", "RGB", "L", 1, ("P",)) - check("I", "L", "I", 1, ("I",)) - check("F", "L", "F", 1, ("F",)) - check("RGB", "RGB", "L", 3, ("R", "G", "B")) - check("RGBA", "RGB", "L", 4, ("R", "G", "B", "A")) - check("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")) - check("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")) - check("CMYK", "RGB", "L", 4, ("C", "M", "Y", "K")) - check("YCbCr", "RGB", "L", 3, ("Y", "Cb", "Cr")) - - -if __name__ == '__main__': - unittest.main() + ImageMode.getmode("1") + ImageMode.getmode("L") + ImageMode.getmode("P") + ImageMode.getmode("RGB") + ImageMode.getmode("I") + ImageMode.getmode("F") + + m = ImageMode.getmode("1") + assert m.mode == "1" + assert str(m) == "1" + assert m.bands == ("1",) + assert m.basemode == "L" + assert m.basetype == "L" + assert m.typestr == "|b1" + + for mode in ( + "I;16", + "I;16S", + "I;16L", + "I;16LS", + "I;16B", + "I;16BS", + "I;16N", + "I;16NS", + ): + m = ImageMode.getmode(mode) + assert m.mode == mode + assert str(m) == mode + assert m.bands == ("I",) + assert m.basemode == "L" + assert m.basetype == "L" + + m = ImageMode.getmode("RGB") + assert m.mode == "RGB" + assert str(m) == "RGB" + assert m.bands == ("R", "G", "B") + assert m.basemode == "RGB" + assert m.basetype == "L" + assert m.typestr == "|u1" + + +def test_properties(): + def check(mode, *result): + signature = ( + Image.getmodebase(mode), + Image.getmodetype(mode), + Image.getmodebands(mode), + Image.getmodebandnames(mode), + ) + assert signature == result + + check("1", "L", "L", 1, ("1",)) + check("L", "L", "L", 1, ("L",)) + check("P", "P", "L", 1, ("P",)) + check("I", "L", "I", 1, ("I",)) + check("F", "L", "F", 1, ("F",)) + check("RGB", "RGB", "L", 3, ("R", "G", "B")) + check("RGBA", "RGB", "L", 4, ("R", "G", "B", "A")) + check("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")) + check("CMYK", "RGB", "L", 4, ("C", "M", "Y", "K")) + check("YCbCr", "RGB", "L", 3, ("Y", "Cb", "Cr")) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index e782008a7e0..1ab02017de1 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -1,18 +1,17 @@ -from helper import unittest, PillowTestCase, cached_property +import pytest from PIL import Image +from .helper import CachedProperty, assert_image_equal -class TestImagingPaste(PillowTestCase): + +class TestImagingPaste: masks = {} size = 128 def assert_9points_image(self, im, expected): expected = [ - point[0] - if im.mode == 'L' else - point[:len(im.mode)] - for point in expected + point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected ] px = im.load() actual = [ @@ -26,7 +25,7 @@ def assert_9points_image(self, im, expected): px[self.size // 2, self.size - 1], px[self.size - 1, self.size - 1], ] - self.assertEqual(actual, expected) + assert actual == expected def assert_9points_paste(self, im, im2, mask, expected): im3 = im.copy() @@ -37,70 +36,93 @@ def assert_9points_paste(self, im, im2, mask, expected): im.paste(im2, mask) self.assert_9points_image(im, expected) - @cached_property + @CachedProperty def mask_1(self): - mask = Image.new('1', (self.size, self.size)) + mask = Image.new("1", (self.size, self.size)) px = mask.load() for y in range(mask.height): for x in range(mask.width): px[y, x] = (x + y) % 2 return mask - @cached_property + @CachedProperty def mask_L(self): - return self.gradient_L.transpose(Image.ROTATE_270) + return self.gradient_L.transpose(Image.Transpose.ROTATE_270) - @cached_property + @CachedProperty def gradient_L(self): - gradient = Image.new('L', (self.size, self.size)) + gradient = Image.new("L", (self.size, self.size)) px = gradient.load() for y in range(gradient.height): for x in range(gradient.width): px[y, x] = (x + y) % 255 return gradient - @cached_property + @CachedProperty def gradient_RGB(self): - return Image.merge('RGB', [ - self.gradient_L, - self.gradient_L.transpose(Image.ROTATE_90), - self.gradient_L.transpose(Image.ROTATE_180), - ]) - - @cached_property + return Image.merge( + "RGB", + [ + self.gradient_L, + self.gradient_L.transpose(Image.Transpose.ROTATE_90), + self.gradient_L.transpose(Image.Transpose.ROTATE_180), + ], + ) + + @CachedProperty + def gradient_LA(self): + return Image.merge( + "LA", + [ + self.gradient_L, + self.gradient_L.transpose(Image.Transpose.ROTATE_90), + ], + ) + + @CachedProperty def gradient_RGBA(self): - return Image.merge('RGBA', [ - self.gradient_L, - self.gradient_L.transpose(Image.ROTATE_90), - self.gradient_L.transpose(Image.ROTATE_180), - self.gradient_L.transpose(Image.ROTATE_270), - ]) - - @cached_property + return Image.merge( + "RGBA", + [ + self.gradient_L, + self.gradient_L.transpose(Image.Transpose.ROTATE_90), + self.gradient_L.transpose(Image.Transpose.ROTATE_180), + self.gradient_L.transpose(Image.Transpose.ROTATE_270), + ], + ) + + @CachedProperty def gradient_RGBa(self): - return Image.merge('RGBa', [ - self.gradient_L, - self.gradient_L.transpose(Image.ROTATE_90), - self.gradient_L.transpose(Image.ROTATE_180), - self.gradient_L.transpose(Image.ROTATE_270), - ]) - - def test_image_solid(self): - for mode in ('RGBA', 'RGB', 'L'): - im = Image.new(mode, (200, 200), 'red') - im2 = getattr(self, 'gradient_' + mode) - - im.paste(im2, (12, 23)) - - im = im.crop((12, 23, im2.width + 12, im2.height + 23)) - self.assert_image_equal(im, im2) - - def test_image_mask_1(self): - for mode in ('RGBA', 'RGB', 'L'): - im = Image.new(mode, (200, 200), 'white') - im2 = getattr(self, 'gradient_' + mode) - - self.assert_9points_paste(im, im2, self.mask_1, [ + return Image.merge( + "RGBa", + [ + self.gradient_L, + self.gradient_L.transpose(Image.Transpose.ROTATE_90), + self.gradient_L.transpose(Image.Transpose.ROTATE_180), + self.gradient_L.transpose(Image.Transpose.ROTATE_270), + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_image_solid(self, mode): + im = Image.new(mode, (200, 200), "red") + im2 = getattr(self, "gradient_" + mode) + + im.paste(im2, (12, 23)) + + im = im.crop((12, 23, im2.width + 12, im2.height + 23)) + assert_image_equal(im, im2) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_image_mask_1(self, mode): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.mask_1, + [ (255, 255, 255, 255), (255, 255, 255, 255), (127, 254, 127, 0), @@ -110,14 +132,19 @@ def test_image_mask_1(self): (127, 0, 127, 254), (191, 64, 63, 190), (255, 255, 255, 255), - ]) - - def test_image_mask_L(self): - for mode in ('RGBA', 'RGB', 'L'): - im = Image.new(mode, (200, 200), 'white') - im2 = getattr(self, 'gradient_' + mode) - - self.assert_9points_paste(im, im2, self.mask_L, [ + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_image_mask_L(self, mode): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.mask_L, + [ (128, 191, 255, 191), (208, 239, 239, 208), (255, 255, 255, 255), @@ -127,14 +154,41 @@ def test_image_mask_L(self): (128, 1, 128, 254), (207, 113, 112, 207), (255, 191, 128, 191), - ]) - - def test_image_mask_RGBA(self): - for mode in ('RGBA', 'RGB', 'L'): - im = Image.new(mode, (200, 200), 'white') - im2 = getattr(self, 'gradient_' + mode) - - self.assert_9points_paste(im, im2, self.gradient_RGBA, [ + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_image_mask_LA(self, mode): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.gradient_LA, + [ + (128, 191, 255, 191), + (112, 207, 206, 111), + (128, 254, 128, 1), + (208, 208, 239, 239), + (192, 191, 191, 191), + (207, 207, 112, 113), + (255, 255, 255, 255), + (239, 207, 207, 239), + (255, 191, 128, 191), + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_image_mask_RGBA(self, mode): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.gradient_RGBA, + [ (128, 191, 255, 191), (208, 239, 239, 208), (255, 255, 255, 255), @@ -144,14 +198,19 @@ def test_image_mask_RGBA(self): (128, 1, 128, 254), (207, 113, 112, 207), (255, 191, 128, 191), - ]) - - def test_image_mask_RGBa(self): - for mode in ('RGBA', 'RGB', 'L'): - im = Image.new(mode, (200, 200), 'white') - im2 = getattr(self, 'gradient_' + mode) - - self.assert_9points_paste(im, im2, self.gradient_RGBa, [ + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_image_mask_RGBa(self, mode): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.gradient_RGBa, + [ (128, 255, 126, 255), (0, 127, 126, 255), (126, 253, 126, 255), @@ -161,27 +220,32 @@ def test_image_mask_RGBa(self): (128, 1, 128, 255), (0, 129, 128, 255), (126, 255, 128, 255), - ]) - - def test_color_solid(self): - for mode in ('RGBA', 'RGB', 'L'): - im = Image.new(mode, (200, 200), 'black') - - rect = (12, 23, 128 + 12, 128 + 23) - im.paste('white', rect) - - hist = im.crop(rect).histogram() - while hist: - head, hist = hist[:256], hist[256:] - self.assertEqual(head[255], 128 * 128) - self.assertEqual(sum(head[:255]), 0) - - def test_color_mask_1(self): - for mode in ('RGBA', 'RGB', 'L'): - im = Image.new(mode, (200, 200), (50, 60, 70, 80)[:len(mode)]) - color = (10, 20, 30, 40)[:len(mode)] - - self.assert_9points_paste(im, color, self.mask_1, [ + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_color_solid(self, mode): + im = Image.new(mode, (200, 200), "black") + + rect = (12, 23, 128 + 12, 128 + 23) + im.paste("white", rect) + + hist = im.crop(rect).histogram() + while hist: + head, hist = hist[:256], hist[256:] + assert head[255] == 128 * 128 + assert sum(head[:255]) == 0 + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_color_mask_1(self, mode): + im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) + color = (10, 20, 30, 40)[: len(mode)] + + self.assert_9points_paste( + im, + color, + self.mask_1, + [ (50, 60, 70, 80), (50, 60, 70, 80), (10, 20, 30, 40), @@ -191,14 +255,19 @@ def test_color_mask_1(self): (10, 20, 30, 40), (10, 20, 30, 40), (50, 60, 70, 80), - ]) - - def test_color_mask_L(self): - for mode in ('RGBA', 'RGB', 'L'): - im = getattr(self, 'gradient_' + mode).copy() - color = 'white' - - self.assert_9points_paste(im, color, self.mask_L, [ + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_color_mask_L(self, mode): + im = getattr(self, "gradient_" + mode).copy() + color = "white" + + self.assert_9points_paste( + im, + color, + self.mask_L, + [ (127, 191, 254, 191), (111, 207, 206, 110), (127, 254, 127, 0), @@ -208,14 +277,19 @@ def test_color_mask_L(self): (254, 254, 254, 255), (239, 206, 206, 238), (254, 191, 127, 191), - ]) - - def test_color_mask_RGBA(self): - for mode in ('RGBA', 'RGB', 'L'): - im = getattr(self, 'gradient_' + mode).copy() - color = 'white' - - self.assert_9points_paste(im, color, self.gradient_RGBA, [ + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_color_mask_RGBA(self, mode): + im = getattr(self, "gradient_" + mode).copy() + color = "white" + + self.assert_9points_paste( + im, + color, + self.gradient_RGBA, + [ (127, 191, 254, 191), (111, 207, 206, 110), (127, 254, 127, 0), @@ -225,14 +299,19 @@ def test_color_mask_RGBA(self): (254, 254, 254, 255), (239, 206, 206, 238), (254, 191, 127, 191), - ]) - - def test_color_mask_RGBa(self): - for mode in ('RGBA', 'RGB', 'L'): - im = getattr(self, 'gradient_' + mode).copy() - color = 'white' - - self.assert_9points_paste(im, color, self.gradient_RGBa, [ + ], + ) + + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) + def test_color_mask_RGBa(self, mode): + im = getattr(self, "gradient_" + mode).copy() + color = "white" + + self.assert_9points_paste( + im, + color, + self.gradient_RGBa, + [ (255, 63, 126, 63), (47, 143, 142, 46), (126, 253, 126, 255), @@ -241,16 +320,13 @@ def test_color_mask_RGBa(self): (142, 141, 46, 47), (255, 255, 255, 0), (48, 15, 15, 47), - (126, 63, 255, 63) - ]) + (126, 63, 255, 63), + ], + ) def test_different_sizes(self): - im = Image.new('RGB', (100, 100)) - im2 = Image.new('RGB', (50, 50)) + im = Image.new("RGB", (100, 100)) + im2 = Image.new("RGB", (50, 50)) im.copy().paste(im2) im.copy().paste(im2, (0, 0)) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 977e98e83d4..157ecb120f0 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -1,44 +1,69 @@ -from helper import unittest, PillowTestCase, hopper +import pytest +from PIL import Image -class TestImagePoint(PillowTestCase): +from .helper import assert_image_equal, hopper - def test_sanity(self): - im = hopper() - self.assertRaises(ValueError, im.point, list(range(256))) - im.point(list(range(256))*3) - im.point(lambda x: x) +def test_sanity(): + im = hopper() - im = im.convert("I") - self.assertRaises(ValueError, im.point, list(range(256))) - im.point(lambda x: x*1) - im.point(lambda x: x+1) - im.point(lambda x: x*1+1) - self.assertRaises(TypeError, im.point, lambda x: x-1) - self.assertRaises(TypeError, im.point, lambda x: x/1) + with pytest.raises(ValueError): + im.point(list(range(256))) + im.point(list(range(256)) * 3) + im.point(lambda x: x) + im.point(lambda x: x * 1.2) - def test_16bit_lut(self): - """ Tests for 16 bit -> 8 bit lut for converting I->L images - see https://github.com/python-pillow/Pillow/issues/440 - """ - im = hopper("I") - im.point(list(range(256))*256, 'L') + im = im.convert("I") + with pytest.raises(ValueError): + im.point(list(range(256))) + im.point(lambda x: x * 1) + im.point(lambda x: x + 1) + im.point(lambda x: x - 1) + im.point(lambda x: x * 1 + 1) + im.point(lambda x: 0.1 + 0.2 * x) + im.point(lambda x: -x) + im.point(lambda x: x - 0.5) + im.point(lambda x: 1 - x / 2) + im.point(lambda x: (2 + x) / 3) + im.point(lambda x: 0.5) + im.point(lambda x: x / 1) + im.point(lambda x: x + x) + with pytest.raises(TypeError): + im.point(lambda x: x * x) + with pytest.raises(TypeError): + im.point(lambda x: x / x) + with pytest.raises(TypeError): + im.point(lambda x: 1 / x) + with pytest.raises(TypeError): + im.point(lambda x: x // 2) - def test_f_lut(self): - """ Tests for floating point lut of 8bit gray image """ - im = hopper('L') - lut = [0.5 * float(x) for x in range(256)] - out = im.point(lut, 'F') +def test_16bit_lut(): + """Tests for 16 bit -> 8 bit lut for converting I->L images + see https://github.com/python-pillow/Pillow/issues/440 + """ + im = hopper("I") + im.point(list(range(256)) * 256, "L") - int_lut = [x//2 for x in range(256)] - self.assert_image_equal(out.convert('L'), im.point(int_lut, 'L')) - def test_f_mode(self): - im = hopper('F') - self.assertRaises(ValueError, im.point, None) +def test_f_lut(): + """Tests for floating point lut of 8bit gray image""" + im = hopper("L") + lut = [0.5 * float(x) for x in range(256)] + out = im.point(lut, "F") -if __name__ == '__main__': - unittest.main() + int_lut = [x // 2 for x in range(256)] + assert_image_equal(out.convert("L"), im.point(int_lut, "L")) + + +def test_f_mode(): + im = hopper("F") + with pytest.raises(ValueError): + im.point(None) + + +def test_coerce_e_deprecation(): + with pytest.warns(DeprecationWarning): + assert Image.coerce_e(2).data == 2 diff --git a/Tests/test_image_putalpha.py b/Tests/test_image_putalpha.py index 823e0612fe6..e2dcead34c5 100644 --- a/Tests/test_image_putalpha.py +++ b/Tests/test_image_putalpha.py @@ -1,50 +1,48 @@ -from helper import unittest, PillowTestCase - from PIL import Image -class TestImagePutAlpha(PillowTestCase): - - def test_interface(self): - - im = Image.new("RGBA", (1, 1), (1, 2, 3, 0)) - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 0)) +def test_interface(): + im = Image.new("RGBA", (1, 1), (1, 2, 3, 0)) + assert im.getpixel((0, 0)) == (1, 2, 3, 0) - im = Image.new("RGBA", (1, 1), (1, 2, 3)) - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 255)) + im = Image.new("RGBA", (1, 1), (1, 2, 3)) + assert im.getpixel((0, 0)) == (1, 2, 3, 255) - im.putalpha(Image.new("L", im.size, 4)) - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 4)) + im.putalpha(Image.new("L", im.size, 4)) + assert im.getpixel((0, 0)) == (1, 2, 3, 4) - im.putalpha(5) - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 5)) + im.putalpha(5) + assert im.getpixel((0, 0)) == (1, 2, 3, 5) - def test_promote(self): - im = Image.new("L", (1, 1), 1) - self.assertEqual(im.getpixel((0, 0)), 1) +def test_promote(): + im = Image.new("L", (1, 1), 1) + assert im.getpixel((0, 0)) == 1 - im.putalpha(2) - self.assertEqual(im.mode, 'LA') - self.assertEqual(im.getpixel((0, 0)), (1, 2)) + im.putalpha(2) + assert im.mode == "LA" + assert im.getpixel((0, 0)) == (1, 2) - im = Image.new("RGB", (1, 1), (1, 2, 3)) - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3)) + im = Image.new("P", (1, 1), 1) + assert im.getpixel((0, 0)) == 1 - im.putalpha(4) - self.assertEqual(im.mode, 'RGBA') - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 4)) + im.putalpha(2) + assert im.mode == "PA" + assert im.getpixel((0, 0)) == (1, 2) - def test_readonly(self): + im = Image.new("RGB", (1, 1), (1, 2, 3)) + assert im.getpixel((0, 0)) == (1, 2, 3) - im = Image.new("RGB", (1, 1), (1, 2, 3)) - im.readonly = 1 + im.putalpha(4) + assert im.mode == "RGBA" + assert im.getpixel((0, 0)) == (1, 2, 3, 4) - im.putalpha(4) - self.assertFalse(im.readonly) - self.assertEqual(im.mode, 'RGBA') - self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 4)) +def test_readonly(): + im = Image.new("RGB", (1, 1), (1, 2, 3)) + im.readonly = 1 -if __name__ == '__main__': - unittest.main() + im.putalpha(4) + assert not im.readonly + assert im.mode == "RGBA" + assert im.getpixel((0, 0)) == (1, 2, 3, 4) diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 1a7a6e7c78f..3d60e52a294 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -1,88 +1,112 @@ -from helper import unittest, PillowTestCase, hopper +import sys from array import array -import sys +import pytest from PIL import Image +from .helper import assert_image_equal, hopper + + +def test_sanity(): + im1 = hopper() + + data = list(im1.getdata()) + + im2 = Image.new(im1.mode, im1.size, 0) + im2.putdata(data) + + assert_image_equal(im1, im2) + + # readonly + im2 = Image.new(im1.mode, im2.size, 0) + im2.readonly = 1 + im2.putdata(data) + + assert not im2.readonly + assert_image_equal(im1, im2) + + +def test_long_integers(): + # see bug-200802-systemerror + def put(value): + im = Image.new("RGBA", (1, 1)) + im.putdata([value]) + return im.getpixel((0, 0)) + + assert put(0xFFFFFFFF) == (255, 255, 255, 255) + assert put(0xFFFFFFFF) == (255, 255, 255, 255) + assert put(-1) == (255, 255, 255, 255) + assert put(-1) == (255, 255, 255, 255) + if sys.maxsize > 2**32: + assert put(sys.maxsize) == (255, 255, 255, 255) + else: + assert put(sys.maxsize) == (255, 255, 255, 127) -class TestImagePutData(PillowTestCase): - def test_sanity(self): +def test_pypy_performance(): + im = Image.new("L", (256, 256)) + im.putdata(list(range(256)) * 256) - im1 = hopper() - data = list(im1.getdata()) +def test_mode_with_L_with_float(): + im = Image.new("L", (1, 1), 0) + im.putdata([2.0]) + assert im.getpixel((0, 0)) == 2 - im2 = Image.new(im1.mode, im1.size, 0) - im2.putdata(data) - self.assert_image_equal(im1, im2) +def test_mode_i(): + src = hopper("L") + data = list(src.getdata()) + im = Image.new("I", src.size, 0) + im.putdata(data, 2, 256) - # readonly - im2 = Image.new(im1.mode, im2.size, 0) - im2.readonly = 1 - im2.putdata(data) + target = [2 * elt + 256 for elt in data] + assert list(im.getdata()) == target - self.assertFalse(im2.readonly) - self.assert_image_equal(im1, im2) - def test_long_integers(self): - # see bug-200802-systemerror - def put(value): - im = Image.new("RGBA", (1, 1)) - im.putdata([value]) - return im.getpixel((0, 0)) - self.assertEqual(put(0xFFFFFFFF), (255, 255, 255, 255)) - self.assertEqual(put(0xFFFFFFFF), (255, 255, 255, 255)) - self.assertEqual(put(-1), (255, 255, 255, 255)) - self.assertEqual(put(-1), (255, 255, 255, 255)) - if sys.maxsize > 2**32: - self.assertEqual(put(sys.maxsize), (255, 255, 255, 255)) - else: - self.assertEqual(put(sys.maxsize), (255, 255, 255, 127)) +def test_mode_F(): + src = hopper("L") + data = list(src.getdata()) + im = Image.new("F", src.size, 0) + im.putdata(data, 2.0, 256.0) - def test_pypy_performance(self): - im = Image.new('L', (256, 256)) - im.putdata(list(range(256))*256) + target = [2.0 * float(elt) + 256.0 for elt in data] + assert list(im.getdata()) == target - def test_mode_i(self): - src = hopper('L') - data = list(src.getdata()) - im = Image.new('I', src.size, 0) - im.putdata(data, 2, 256) - target = [2 * elt + 256 for elt in data] - self.assertEqual(list(im.getdata()), target) +def test_array_B(): + # shouldn't segfault + # see https://github.com/python-pillow/Pillow/issues/1008 - def test_mode_F(self): - src = hopper('L') - data = list(src.getdata()) - im = Image.new('F', src.size, 0) - im.putdata(data, 2.0, 256.0) + arr = array("B", [0]) * 15000 + im = Image.new("L", (150, 100)) + im.putdata(arr) - target = [2.0 * float(elt) + 256.0 for elt in data] - self.assertEqual(list(im.getdata()), target) + assert len(im.getdata()) == len(arr) - def test_array_B(self): - # shouldn't segfault - # see https://github.com/python-pillow/Pillow/issues/1008 - arr = array('B', [0])*15000 - im = Image.new('L', (150, 100)) - im.putdata(arr) +def test_array_F(): + # shouldn't segfault + # see https://github.com/python-pillow/Pillow/issues/1008 - self.assertEqual(len(im.getdata()), len(arr)) + im = Image.new("F", (150, 100)) + arr = array("f", [0.0]) * 15000 + im.putdata(arr) - def test_array_F(self): - # shouldn't segfault - # see https://github.com/python-pillow/Pillow/issues/1008 + assert len(im.getdata()) == len(arr) - im = Image.new('F', (150, 100)) - arr = array('f', [0.0])*15000 - im.putdata(arr) - self.assertEqual(len(im.getdata()), len(arr)) +def test_not_flattened(): + im = Image.new("L", (1, 1)) + with pytest.raises(TypeError): + im.putdata([[0]]) + with pytest.raises(TypeError): + im.putdata([[0]], 2) -if __name__ == '__main__': - unittest.main() + with pytest.raises(TypeError): + im = Image.new("I", (1, 1)) + im.putdata([[0]]) + with pytest.raises(TypeError): + im = Image.new("F", (1, 1)) + im.putdata([[0]]) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index e173f000081..3b29769a7a4 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -1,34 +1,78 @@ -from helper import unittest, PillowTestCase, hopper - -from PIL import ImagePalette - - -class TestImagePutPalette(PillowTestCase): - - def test_putpalette(self): - def palette(mode): - im = hopper(mode).copy() - im.putpalette(list(range(256))*3) - p = im.getpalette() - if p: - return im.mode, p[:10] - return im.mode - self.assertRaises(ValueError, palette, "1") - self.assertEqual(palette("L"), ("P", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) - self.assertEqual(palette("P"), ("P", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) - self.assertRaises(ValueError, palette, "I") - self.assertRaises(ValueError, palette, "F") - self.assertRaises(ValueError, palette, "RGB") - self.assertRaises(ValueError, palette, "RGBA") - self.assertRaises(ValueError, palette, "YCbCr") - - def test_imagepalette(self): - im = hopper("P") - im.putpalette(ImagePalette.negative()) - im.putpalette(ImagePalette.random()) - im.putpalette(ImagePalette.sepia()) - im.putpalette(ImagePalette.wedge()) - - -if __name__ == '__main__': - unittest.main() +import pytest + +from PIL import Image, ImagePalette + +from .helper import assert_image_equal, assert_image_equal_tofile, hopper + + +def test_putpalette(): + def palette(mode): + im = hopper(mode).copy() + im.putpalette(list(range(256)) * 3) + p = im.getpalette() + if p: + return im.mode, p[:10] + return im.mode + + with pytest.raises(ValueError): + palette("1") + for mode in ["L", "LA", "P", "PA"]: + assert palette(mode) == ( + "PA" if "A" in mode else "P", + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + ) + with pytest.raises(ValueError): + palette("I") + with pytest.raises(ValueError): + palette("F") + with pytest.raises(ValueError): + palette("RGB") + with pytest.raises(ValueError): + palette("RGBA") + with pytest.raises(ValueError): + palette("YCbCr") + + +def test_imagepalette(): + im = hopper("P") + im.putpalette(ImagePalette.negative()) + assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_negative.png") + + im.putpalette(ImagePalette.random()) + + im.putpalette(ImagePalette.sepia()) + assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_sepia.png") + + im.putpalette(ImagePalette.wedge()) + assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_wedge.png") + + +def test_putpalette_with_alpha_values(): + with Image.open("Tests/images/transparent.gif") as im: + expected = im.convert("RGBA") + + palette = im.getpalette() + transparency = im.info.pop("transparency") + + palette_with_alpha_values = [] + for i in range(256): + color = palette[i * 3 : i * 3 + 3] + alpha = 0 if i == transparency else 255 + palette_with_alpha_values += color + [alpha] + im.putpalette(palette_with_alpha_values, "RGBA") + + assert_image_equal(im.convert("RGBA"), expected) + + +@pytest.mark.parametrize( + "mode, palette", + ( + ("RGBA", (1, 2, 3, 4)), + ("RGBAX", (1, 2, 3, 4, 0)), + ), +) +def test_rgba_palette(mode, palette): + im = Image.new("P", (1, 1)) + im.putpalette(palette, mode) + assert im.getpalette() == [1, 2, 3] + assert im.palette.colors == {(1, 2, 3, 4): 0} diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index d9f52fb0398..981753eb9b7 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,52 +1,141 @@ -from helper import unittest, PillowTestCase, hopper - -from PIL import Image - - -class TestImageQuantize(PillowTestCase): - - def test_sanity(self): - image = hopper() - converted = image.quantize() - self.assert_image(converted, 'P', converted.size) - self.assert_image_similar(converted.convert('RGB'), image, 10) - - image = hopper() - converted = image.quantize(palette=hopper('P')) - self.assert_image(converted, 'P', converted.size) - self.assert_image_similar(converted.convert('RGB'), image, 60) - - def test_libimagequant_quantize(self): - image = hopper() - try: - converted = image.quantize(100, Image.LIBIMAGEQUANT) - except ValueError as ex: - if 'dependency' in str(ex).lower(): - self.skipTest('libimagequant support not available') - else: - raise - self.assert_image(converted, 'P', converted.size) - self.assert_image_similar(converted.convert('RGB'), image, 15) - assert len(converted.getcolors()) == 100 - - def test_octree_quantize(self): - image = hopper() - converted = image.quantize(100, Image.FASTOCTREE) - self.assert_image(converted, 'P', converted.size) - self.assert_image_similar(converted.convert('RGB'), image, 20) - assert len(converted.getcolors()) == 100 - - def test_rgba_quantize(self): - image = hopper('RGBA') - image.quantize() - self.assertRaises(Exception, image.quantize, method=0) - - def test_quantize(self): - image = Image.open('Tests/images/caption_6_33_22.png').convert('RGB') - converted = image.quantize() - self.assert_image(converted, 'P', converted.size) - self.assert_image_similar(converted.convert('RGB'), image, 1) - - -if __name__ == '__main__': - unittest.main() +import pytest +from packaging.version import parse as parse_version + +from PIL import Image, features + +from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature + + +def test_sanity(): + image = hopper() + converted = image.quantize() + assert converted.mode == "P" + assert_image_similar(converted.convert("RGB"), image, 10) + + image = hopper() + converted = image.quantize(palette=hopper("P")) + assert converted.mode == "P" + assert_image_similar(converted.convert("RGB"), image, 60) + + +@skip_unless_feature("libimagequant") +def test_libimagequant_quantize(): + image = hopper() + if is_ppc64le(): + libimagequant = parse_version(features.version_feature("libimagequant")) + if libimagequant < parse_version("4"): + pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le") + converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT) + assert converted.mode == "P" + assert_image_similar(converted.convert("RGB"), image, 15) + assert len(converted.getcolors()) == 100 + + +def test_octree_quantize(): + image = hopper() + converted = image.quantize(100, Image.Quantize.FASTOCTREE) + assert converted.mode == "P" + assert_image_similar(converted.convert("RGB"), image, 20) + assert len(converted.getcolors()) == 100 + + +def test_rgba_quantize(): + image = hopper("RGBA") + with pytest.raises(ValueError): + image.quantize(method=0) + + assert image.quantize().convert().mode == "RGBA" + + +def test_quantize(): + with Image.open("Tests/images/caption_6_33_22.png") as image: + image = image.convert("RGB") + converted = image.quantize() + assert converted.mode == "P" + assert_image_similar(converted.convert("RGB"), image, 1) + + +def test_quantize_no_dither(): + image = hopper() + with Image.open("Tests/images/caption_6_33_22.png") as palette: + palette = palette.convert("P") + + converted = image.quantize(dither=Image.Dither.NONE, palette=palette) + assert converted.mode == "P" + assert converted.palette.palette == palette.palette.palette + + +def test_quantize_no_dither2(): + im = Image.new("RGB", (9, 1)) + im.putdata(list((p,) * 3 for p in range(0, 36, 4))) + + palette = Image.new("P", (1, 1)) + data = (0, 0, 0, 32, 32, 32) + palette.putpalette(data) + quantized = im.quantize(dither=Image.Dither.NONE, palette=palette) + + assert tuple(quantized.palette.palette) == data + + px = quantized.load() + for x in range(9): + assert px[x, 0] == (0 if x < 5 else 1) + + +def test_quantize_dither_diff(): + image = hopper() + with Image.open("Tests/images/caption_6_33_22.png") as palette: + palette = palette.convert("P") + + dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette) + nodither = image.quantize(dither=Image.Dither.NONE, palette=palette) + + assert dither.tobytes() != nodither.tobytes() + + +def test_colors(): + im = hopper() + colors = 2 + converted = im.quantize(colors) + assert len(converted.palette.palette) == colors * len("RGB") + + +def test_transparent_colors_equal(): + im = Image.new("RGBA", (1, 2), (0, 0, 0, 0)) + px = im.load() + px[0, 1] = (255, 255, 255, 0) + + converted = im.quantize() + converted_px = converted.load() + assert converted_px[0, 0] == converted_px[0, 1] + + +@pytest.mark.parametrize( + "method, color", + ( + (Image.Quantize.MEDIANCUT, (0, 0, 0)), + (Image.Quantize.MAXCOVERAGE, (0, 0, 0)), + (Image.Quantize.FASTOCTREE, (0, 0, 0)), + (Image.Quantize.FASTOCTREE, (0, 0, 0, 0)), + ), +) +def test_palette(method, color): + im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color) + + converted = im.quantize(method=method) + converted_px = converted.load() + assert converted_px[0, 0] == converted.palette.colors[color] + + +def test_small_palette(): + # Arrange + im = hopper() + + colors = (255, 0, 0, 0, 0, 255) + p = Image.new("P", (1, 1)) + p.putpalette(colors) + + # Act + im = im.quantize(palette=p) + + # Assert + assert len(im.getcolors()) == 2 diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py new file mode 100644 index 00000000000..ae8d740a027 --- /dev/null +++ b/Tests/test_image_reduce.py @@ -0,0 +1,274 @@ +import pytest + +from PIL import Image, ImageMath, ImageMode + +from .helper import convert_to_comparable, skip_unless_feature + +codecs = dir(Image.core) + + +# There are several internal implementations +remarkable_factors = [ + # special implementations + 1, + 2, + 3, + 4, + 5, + 6, + # 1xN implementation + (1, 2), + (1, 3), + (1, 4), + (1, 7), + # Nx1 implementation + (2, 1), + (3, 1), + (4, 1), + (7, 1), + # general implementation with different paths + (4, 6), + (5, 6), + (4, 7), + (5, 7), + (19, 17), +] + +gradients_image = Image.open("Tests/images/radial_gradients.png") +gradients_image.load() + + +@pytest.mark.parametrize( + "size, expected", + ( + (3, (4, 4)), + ((3, 1), (4, 10)), + ((1, 3), (10, 4)), + ), +) +def test_args_factor(size, expected): + im = Image.new("L", (10, 10)) + assert expected == im.reduce(size).size + + +@pytest.mark.parametrize( + "size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) +) +def test_args_factor_error(size, expected_error): + im = Image.new("L", (10, 10)) + with pytest.raises(expected_error): + im.reduce(size) + + +@pytest.mark.parametrize( + "size, expected", + ( + ((0, 0, 10, 10), (5, 5)), + ((5, 5, 6, 6), (1, 1)), + ), +) +def test_args_box(size, expected): + im = Image.new("L", (10, 10)) + assert expected == im.reduce(2, size).size + + +@pytest.mark.parametrize( + "size, expected_error", + ( + ("stri", TypeError), + ((0, 0, 11, 10), ValueError), + ((0, 0, 10, 11), ValueError), + ((-1, 0, 10, 10), ValueError), + ((0, -1, 10, 10), ValueError), + ((0, 5, 10, 5), ValueError), + ((5, 0, 5, 10), ValueError), + ), +) +def test_args_box_error(size, expected_error): + im = Image.new("L", (10, 10)) + with pytest.raises(expected_error): + im.reduce(2, size).size + + +@pytest.mark.parametrize("mode", ("P", "1", "I;16")) +def test_unsupported_modes(mode): + im = Image.new("P", (10, 10)) + with pytest.raises(ValueError): + im.reduce(3) + + +def get_image(mode): + mode_info = ImageMode.getmode(mode) + if mode_info.basetype == "L": + bands = [gradients_image] + for _ in mode_info.bands[1:]: + # rotate previous image + band = bands[-1].transpose(Image.Transpose.ROTATE_90) + bands.append(band) + # Correct alpha channel by transforming completely transparent pixels. + # Low alpha values also emphasize error after alpha multiplication. + if mode.endswith("A"): + bands[-1] = bands[-1].point(lambda x: int(85 + x / 1.5)) + im = Image.merge(mode, bands) + else: + assert len(mode_info.bands) == 1 + im = gradients_image.convert(mode) + # change the height to make a not-square image + return im.crop((0, 0, im.width, im.height - 5)) + + +def compare_reduce_with_box(im, factor): + box = (11, 13, 146, 164) + reduced = im.reduce(factor, box=box) + reference = im.crop(box).reduce(factor) + assert reduced == reference + + +def compare_reduce_with_reference(im, factor, average_diff=0.4, max_diff=1): + """Image.reduce() should look very similar to Image.resize(BOX). + + A reference image is compiled from a large source area + and possible last column and last row. + +-----------+ + |..........c| + |..........c| + |..........c| + |rrrrrrrrrrp| + +-----------+ + """ + reduced = im.reduce(factor) + + if not isinstance(factor, (list, tuple)): + factor = (factor, factor) + + reference = Image.new(im.mode, reduced.size) + area_size = (im.size[0] // factor[0], im.size[1] // factor[1]) + area_box = (0, 0, area_size[0] * factor[0], area_size[1] * factor[1]) + area = im.resize(area_size, Image.Resampling.BOX, area_box) + reference.paste(area, (0, 0)) + + if area_size[0] < reduced.size[0]: + assert reduced.size[0] - area_size[0] == 1 + last_column_box = (area_box[2], 0, im.size[0], area_box[3]) + last_column = im.resize( + (1, area_size[1]), Image.Resampling.BOX, last_column_box + ) + reference.paste(last_column, (area_size[0], 0)) + + if area_size[1] < reduced.size[1]: + assert reduced.size[1] - area_size[1] == 1 + last_row_box = (0, area_box[3], area_box[2], im.size[1]) + last_row = im.resize((area_size[0], 1), Image.Resampling.BOX, last_row_box) + reference.paste(last_row, (0, area_size[1])) + + if area_size[0] < reduced.size[0] and area_size[1] < reduced.size[1]: + last_pixel_box = (area_box[2], area_box[3], im.size[0], im.size[1]) + last_pixel = im.resize((1, 1), Image.Resampling.BOX, last_pixel_box) + reference.paste(last_pixel, area_size) + + assert_compare_images(reduced, reference, average_diff, max_diff) + + +def assert_compare_images(a, b, max_average_diff, max_diff=255): + assert a.mode == b.mode, f"got mode {repr(a.mode)}, expected {repr(b.mode)}" + assert a.size == b.size, f"got size {repr(a.size)}, expected {repr(b.size)}" + + a, b = convert_to_comparable(a, b) + + bands = ImageMode.getmode(a.mode).bands + for band, ach, bch in zip(bands, a.split(), b.split()): + ch_diff = ImageMath.eval("convert(abs(a - b), 'L')", a=ach, b=bch) + ch_hist = ch_diff.histogram() + + average_diff = sum(i * num for i, num in enumerate(ch_hist)) / ( + a.size[0] * a.size[1] + ) + msg = ( + f"average pixel value difference {average_diff:.4f} > " + f"expected {max_average_diff:.4f} for '{band}' band" + ) + assert max_average_diff >= average_diff, msg + + last_diff = [i for i, num in enumerate(ch_hist) if num > 0][-1] + assert max_diff >= last_diff, ( + f"max pixel value difference {last_diff} > expected {max_diff} " + f"for '{band}' band" + ) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_L(factor): + im = get_image("L") + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_LA(factor): + im = get_image("LA") + compare_reduce_with_reference(im, factor, 0.8, 5) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_LA_opaque(factor): + im = get_image("LA") + # With opaque alpha, an error should be way smaller. + im.putalpha(Image.new("L", im.size, 255)) + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_La(factor): + im = get_image("La") + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_RGB(factor): + im = get_image("RGB") + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_RGBA(factor): + im = get_image("RGBA") + compare_reduce_with_reference(im, factor, 0.8, 5) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_RGBA_opaque(factor): + im = get_image("RGBA") + # With opaque alpha, an error should be way smaller. + im.putalpha(Image.new("L", im.size, 255)) + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_RGBa(factor): + im = get_image("RGBa") + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_I(factor): + im = get_image("I") + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_F(factor): + im = get_image("F") + compare_reduce_with_reference(im, factor, 0, 0) + compare_reduce_with_box(im, factor) + + +@skip_unless_feature("jpg_2000") +def test_jpeg2k(): + with Image.open("Tests/images/test-card-lossless.jp2") as im: + assert im.reduce(2).size == (320, 240) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index ee5a062c652..53ceb6df030 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,20 +1,30 @@ -from __future__ import division, print_function - from contextlib import contextmanager -from helper import unittest, PillowTestCase, hopper +import pytest + from PIL import Image, ImageDraw +from .helper import ( + assert_image_equal, + assert_image_similar, + hopper, + mark_if_feature_version, +) + -class TestImagingResampleVulnerability(PillowTestCase): +class TestImagingResampleVulnerability: # see https://github.com/python-pillow/Pillow/issues/1710 def test_overflow(self): - im = hopper('L') - xsize = 0x100000008 // 4 - ysize = 1000 # unimportant - with self.assertRaises(MemoryError): - # any resampling filter will do here - im.im.resize((xsize, ysize), Image.BILINEAR) + im = hopper("L") + size_too_large = 0x100000008 // 4 + size_normal = 1000 # unimportant + for xsize, ysize in ( + (size_too_large, size_normal), + (size_normal, size_too_large), + ): + with pytest.raises(MemoryError): + # any resampling filter will do here + im.im.resize((xsize, ysize), Image.Resampling.BILINEAR) def test_invalid_size(self): im = hopper() @@ -22,23 +32,23 @@ def test_invalid_size(self): # Should not crash im.resize((100, 100)) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): im.resize((-100, 100)) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): im.resize((100, -100)) def test_modify_after_resizing(self): - im = hopper('RGB') + im = hopper("RGB") # get copy with same size copy = im.resize(im.size) # some in-place operation - copy.paste('black', (0, 0, im.width // 2, im.height // 2)) + copy.paste("black", (0, 0, im.width // 2, im.height // 2)) # image should be different - self.assertNotEqual(im.tobytes(), copy.tobytes()) + assert im.tobytes() != copy.tobytes() -class TestImagingCoreResampleAccuracy(PillowTestCase): +class TestImagingCoreResampleAccuracy: def make_case(self, mode, size, color): """Makes a sample image with two dark and two bright squares. For example: @@ -47,7 +57,7 @@ def make_case(self, mode, size, color): 1f 1f e0 e0 1f 1f e0 e0 """ - case = Image.new('L', size, 255 - color) + case = Image.new("L", size, 255 - color) rectangle = ImageDraw.Draw(case).rectangle rectangle((0, 0, size[0] // 2 - 1, size[1] // 2 - 1), color) rectangle((size[0] // 2, size[1] // 2, size[0], size[1]), color) @@ -58,13 +68,13 @@ def make_sample(self, data, size): """Restores a sample image from given data string which contains hex-encoded pixels from the top left fourth of a sample. """ - data = data.replace(' ', '') - sample = Image.new('L', size) + data = data.replace(" ", "") + sample = Image.new("L", size) s_px = sample.load() w, h = size[0] // 2, size[1] // 2 for y in range(h): for x in range(w): - val = int(data[(y * w + x) * 2:(y * w + x + 1) * 2], 16) + val = int(data[(y * w + x) * 2 : (y * w + x + 1) * 2], 16) s_px[x, y] = val s_px[size[0] - x - 1, size[1] - y - 1] = val s_px[x, size[1] - y - 1] = 255 - val @@ -77,126 +87,151 @@ def check_case(self, case, sample): for y in range(case.size[1]): for x in range(case.size[0]): if c_px[x, y] != s_px[x, y]: - message = '\nHave: \n{}\n\nExpected: \n{}'.format( - self.serialize_image(case), - self.serialize_image(sample), + message = ( + f"\nHave: \n{self.serialize_image(case)}\n" + f"\nExpected: \n{self.serialize_image(sample)}" ) - self.assertEqual(s_px[x, y], c_px[x, y], message) + assert s_px[x, y] == c_px[x, y], message def serialize_image(self, image): s_px = image.load() - return '\n'.join( - ' '.join( - '{:02x}'.format(s_px[x, y]) - for x in range(image.size[0]) - ) + return "\n".join( + " ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0])) for y in range(image.size[1]) ) - def test_reduce_box(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (8, 8), 0xe1) - case = case.resize((4, 4), Image.BOX) - data = ('e1 e1' - 'e1 e1') - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - def test_reduce_bilinear(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (8, 8), 0xe1) - case = case.resize((4, 4), Image.BILINEAR) - data = ('e1 c9' - 'c9 b7') - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - def test_reduce_hamming(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (8, 8), 0xe1) - case = case.resize((4, 4), Image.HAMMING) - data = ('e1 da' - 'da d3') - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - def test_reduce_bicubic(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (12, 12), 0xe1) - case = case.resize((6, 6), Image.BICUBIC) - data = ('e1 e3 d4' - 'e3 e5 d6' - 'd4 d6 c9') + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_box(self, mode): + case = self.make_case(mode, (8, 8), 0xE1) + case = case.resize((4, 4), Image.Resampling.BOX) + # fmt: off + data = ("e1 e1" + "e1 e1") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_bilinear(self, mode): + case = self.make_case(mode, (8, 8), 0xE1) + case = case.resize((4, 4), Image.Resampling.BILINEAR) + # fmt: off + data = ("e1 c9" + "c9 b7") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_hamming(self, mode): + case = self.make_case(mode, (8, 8), 0xE1) + case = case.resize((4, 4), Image.Resampling.HAMMING) + # fmt: off + data = ("e1 da" + "da d3") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_bicubic(self, mode): + for mode in ["RGBX", "RGB", "La", "L"]: + case = self.make_case(mode, (12, 12), 0xE1) + case = case.resize((6, 6), Image.Resampling.BICUBIC) + # fmt: off + data = ("e1 e3 d4" + "e3 e5 d6" + "d4 d6 c9") + # fmt: on for channel in case.split(): self.check_case(channel, self.make_sample(data, (6, 6))) - def test_reduce_lanczos(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (16, 16), 0xe1) - case = case.resize((8, 8), Image.LANCZOS) - data = ('e1 e0 e4 d7' - 'e0 df e3 d6' - 'e4 e3 e7 da' - 'd7 d6 d9 ce') - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (8, 8))) - - def test_enlarge_box(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (2, 2), 0xe1) - case = case.resize((4, 4), Image.BOX) - data = ('e1 e1' - 'e1 e1') - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - def test_enlarge_bilinear(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (2, 2), 0xe1) - case = case.resize((4, 4), Image.BILINEAR) - data = ('e1 b0' - 'b0 98') - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - def test_enlarge_hamming(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (2, 2), 0xe1) - case = case.resize((4, 4), Image.HAMMING) - data = ('e1 d2' - 'd2 c5') - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - def test_enlarge_bicubic(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (4, 4), 0xe1) - case = case.resize((8, 8), Image.BICUBIC) - data = ('e1 e5 ee b9' - 'e5 e9 f3 bc' - 'ee f3 fd c1' - 'b9 bc c1 a2') - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (8, 8))) - - def test_enlarge_lanczos(self): - for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (6, 6), 0xe1) - case = case.resize((12, 12), Image.LANCZOS) - data = ('e1 e0 db ed f5 b8' - 'e0 df da ec f3 b7' - 'db db d6 e7 ee b5' - 'ed ec e6 fb ff bf' - 'f5 f4 ee ff ff c4' - 'b8 b7 b4 bf c4 a0') - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (12, 12))) + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_lanczos(self, mode): + case = self.make_case(mode, (16, 16), 0xE1) + case = case.resize((8, 8), Image.Resampling.LANCZOS) + # fmt: off + data = ("e1 e0 e4 d7" + "e0 df e3 d6" + "e4 e3 e7 da" + "d7 d6 d9 ce") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (8, 8))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_box(self, mode): + case = self.make_case(mode, (2, 2), 0xE1) + case = case.resize((4, 4), Image.Resampling.BOX) + # fmt: off + data = ("e1 e1" + "e1 e1") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_bilinear(self, mode): + case = self.make_case(mode, (2, 2), 0xE1) + case = case.resize((4, 4), Image.Resampling.BILINEAR) + # fmt: off + data = ("e1 b0" + "b0 98") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_hamming(self, mode): + case = self.make_case(mode, (2, 2), 0xE1) + case = case.resize((4, 4), Image.Resampling.HAMMING) + # fmt: off + data = ("e1 d2" + "d2 c5") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_bicubic(self, mode): + case = self.make_case(mode, (4, 4), 0xE1) + case = case.resize((8, 8), Image.Resampling.BICUBIC) + # fmt: off + data = ("e1 e5 ee b9" + "e5 e9 f3 bc" + "ee f3 fd c1" + "b9 bc c1 a2") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (8, 8))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_lanczos(self, mode): + case = self.make_case(mode, (6, 6), 0xE1) + case = case.resize((12, 12), Image.Resampling.LANCZOS) + data = ( + "e1 e0 db ed f5 b8" + "e0 df da ec f3 b7" + "db db d6 e7 ee b5" + "ed ec e6 fb ff bf" + "f5 f4 ee ff ff c4" + "b8 b7 b4 bf c4 a0" + ) + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (12, 12))) + def test_box_filter_correct_range(self): + im = Image.new("RGB", (8, 8), "#1688ff").resize( + (100, 100), Image.Resampling.BOX + ) + ref = Image.new("RGB", (100, 100), "#1688ff") + assert_image_equal(im, ref) -class CoreResampleConsistencyTest(PillowTestCase): + +class TestCoreResampleConsistency: def make_case(self, mode, fill): im = Image.new(mode, (512, 9), fill) - return (im.resize((9, 512), Image.LANCZOS), im.load()[0, 0]) + return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] def run_case(self, case): channel, color = case @@ -204,32 +239,31 @@ def run_case(self, case): for x in range(channel.size[0]): for y in range(channel.size[1]): if px[x, y] != color: - message = "{} != {} for pixel {}".format( - px[x, y], color, (x, y)) - self.assertEqual(px[x, y], color, message) + message = f"{px[x, y]} != {color} for pixel {(x, y)}" + assert px[x, y] == color, message def test_8u(self): - im, color = self.make_case('RGB', (0, 64, 255)) + im, color = self.make_case("RGB", (0, 64, 255)) r, g, b = im.split() self.run_case((r, color[0])) self.run_case((g, color[1])) self.run_case((b, color[2])) - self.run_case(self.make_case('L', 12)) + self.run_case(self.make_case("L", 12)) def test_32i(self): - self.run_case(self.make_case('I', 12)) - self.run_case(self.make_case('I', 0x7fffffff)) - self.run_case(self.make_case('I', -12)) - self.run_case(self.make_case('I', -1 << 31)) + self.run_case(self.make_case("I", 12)) + self.run_case(self.make_case("I", 0x7FFFFFFF)) + self.run_case(self.make_case("I", -12)) + self.run_case(self.make_case("I", -1 << 31)) def test_32f(self): - self.run_case(self.make_case('F', 1)) - self.run_case(self.make_case('F', 3.40282306074e+38)) - self.run_case(self.make_case('F', 1.175494e-38)) - self.run_case(self.make_case('F', 1.192093e-07)) + self.run_case(self.make_case("F", 1)) + self.run_case(self.make_case("F", 3.40282306074e38)) + self.run_case(self.make_case("F", 1.175494e-38)) + self.run_case(self.make_case("F", 1.192093e-07)) -class CoreResampleAlphaCorrectTest(PillowTestCase): +class TestCoreResampleAlphaCorrect: def make_levels_case(self, mode): i = Image.new(mode, (256, 16)) px = i.load() @@ -244,27 +278,28 @@ def run_levels_case(self, i): px = i.load() for y in range(i.size[1]): used_colors = {px[x, y][0] for x in range(i.size[0])} - self.assertEqual(256, len(used_colors), - 'All colors should present in resized image. ' - 'Only {} on {} line.'.format(len(used_colors), y)) + assert 256 == len(used_colors), ( + "All colors should be present in resized image. " + f"Only {len(used_colors)} on {y} line." + ) - @unittest.skip("current implementation isn't precise enough") + @pytest.mark.xfail(reason="Current implementation isn't precise enough") def test_levels_rgba(self): - case = self.make_levels_case('RGBA') - self.run_levels_case(case.resize((512, 32), Image.BOX)) - self.run_levels_case(case.resize((512, 32), Image.BILINEAR)) - self.run_levels_case(case.resize((512, 32), Image.HAMMING)) - self.run_levels_case(case.resize((512, 32), Image.BICUBIC)) - self.run_levels_case(case.resize((512, 32), Image.LANCZOS)) - - @unittest.skip("current implementation isn't precise enough") + case = self.make_levels_case("RGBA") + self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.HAMMING)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) + + @pytest.mark.xfail(reason="Current implementation isn't precise enough") def test_levels_la(self): - case = self.make_levels_case('LA') - self.run_levels_case(case.resize((512, 32), Image.BOX)) - self.run_levels_case(case.resize((512, 32), Image.BILINEAR)) - self.run_levels_case(case.resize((512, 32), Image.HAMMING)) - self.run_levels_case(case.resize((512, 32), Image.BICUBIC)) - self.run_levels_case(case.resize((512, 32), Image.LANCZOS)) + case = self.make_levels_case("LA") + self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.HAMMING)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) def make_dirty_case(self, mode, clean_pixel, dirty_pixel): i = Image.new(mode, (64, 64), dirty_pixel) @@ -281,183 +316,213 @@ def run_dirty_case(self, i, clean_pixel): for y in range(i.size[1]): for x in range(i.size[0]): if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel: - message = 'pixel at ({}, {}) is differ:\n{}\n{}'\ - .format(x, y, px[x, y], clean_pixel) - self.assertEqual(px[x, y][:3], clean_pixel, message) + message = ( + f"pixel at ({x}, {y}) is different:\n" + f"{px[x, y]}\n{clean_pixel}" + ) + assert px[x, y][:3] == clean_pixel, message def test_dirty_pixels_rgba(self): - case = self.make_dirty_case('RGBA', (255, 255, 0, 128), (0, 0, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.BOX), (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.HAMMING), (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.BICUBIC), (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255, 255, 0)) + case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255, 255, 0)) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.BILINEAR), (255, 255, 0) + ) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.HAMMING), (255, 255, 0) + ) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.BICUBIC), (255, 255, 0) + ) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.LANCZOS), (255, 255, 0) + ) def test_dirty_pixels_la(self): - case = self.make_dirty_case('LA', (255, 128), (0, 0)) - self.run_dirty_case(case.resize((20, 20), Image.BOX), (255,)) - self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), (255,)) - self.run_dirty_case(case.resize((20, 20), Image.HAMMING), (255,)) - self.run_dirty_case(case.resize((20, 20), Image.BICUBIC), (255,)) - self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255,)) + case = self.make_dirty_case("LA", (255, 128), (0, 0)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.BILINEAR), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.HAMMING), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.BICUBIC), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.LANCZOS), (255,)) -class CoreResamplePassesTest(PillowTestCase): +class TestCoreResamplePasses: @contextmanager def count(self, diff): - count = Image.core.get_stats()['new_count'] + count = Image.core.get_stats()["new_count"] yield - self.assertEqual(Image.core.get_stats()['new_count'] - count, diff) + assert Image.core.get_stats()["new_count"] - count == diff def test_horizontal(self): - im = hopper('L') + im = hopper("L") with self.count(1): - im.resize((im.size[0] - 10, im.size[1]), Image.BILINEAR) + im.resize((im.size[0] - 10, im.size[1]), Image.Resampling.BILINEAR) def test_vertical(self): - im = hopper('L') + im = hopper("L") with self.count(1): - im.resize((im.size[0], im.size[1] - 10), Image.BILINEAR) + im.resize((im.size[0], im.size[1] - 10), Image.Resampling.BILINEAR) def test_both(self): - im = hopper('L') + im = hopper("L") with self.count(2): - im.resize((im.size[0] - 10, im.size[1] - 10), Image.BILINEAR) + im.resize((im.size[0] - 10, im.size[1] - 10), Image.Resampling.BILINEAR) def test_box_horizontal(self): - im = hopper('L') + im = hopper("L") box = (20, 0, im.size[0] - 20, im.size[1]) with self.count(1): # the same size, but different box - with_box = im.resize(im.size, Image.BILINEAR, box) + with_box = im.resize(im.size, Image.Resampling.BILINEAR, box) with self.count(2): - cropped = im.crop(box).resize(im.size, Image.BILINEAR) - self.assert_image_similar(with_box, cropped, 0.1) + cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR) + assert_image_similar(with_box, cropped, 0.1) def test_box_vertical(self): - im = hopper('L') + im = hopper("L") box = (0, 20, im.size[0], im.size[1] - 20) with self.count(1): # the same size, but different box - with_box = im.resize(im.size, Image.BILINEAR, box) + with_box = im.resize(im.size, Image.Resampling.BILINEAR, box) with self.count(2): - cropped = im.crop(box).resize(im.size, Image.BILINEAR) - self.assert_image_similar(with_box, cropped, 0.1) + cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR) + assert_image_similar(with_box, cropped, 0.1) -class CoreResampleCoefficientsTest(PillowTestCase): +class TestCoreResampleCoefficients: def test_reduce(self): test_color = 254 - # print() for size in range(400000, 400010, 2): - # print(size) - i = Image.new('L', (size, 1), 0) + i = Image.new("L", (size, 1), 0) draw = ImageDraw.Draw(i) draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color) - px = i.resize((5, i.size[1]), Image.BICUBIC).load() + px = i.resize((5, i.size[1]), Image.Resampling.BICUBIC).load() if px[2, 0] != test_color // 2: - self.assertEqual(test_color // 2, px[2, 0]) - # print('>', size, test_color // 2, px[2, 0]) + assert test_color // 2 == px[2, 0] def test_nonzero_coefficients(self): # regression test for the wrong coefficients calculation # due to bug https://github.com/python-pillow/Pillow/issues/2161 - im = Image.new('RGBA', (1280, 1280), (0x20, 0x40, 0x60, 0xff)) - histogram = im.resize((256, 256), Image.BICUBIC).histogram() - - self.assertEqual(histogram[0x100 * 0 + 0x20], 0x10000) # first channel - self.assertEqual(histogram[0x100 * 1 + 0x40], 0x10000) # second channel - self.assertEqual(histogram[0x100 * 2 + 0x60], 0x10000) # third channel - self.assertEqual(histogram[0x100 * 3 + 0xff], 0x10000) # fourth channel - - -class CoreResampleBoxTest(PillowTestCase): - def test_wrong_arguments(self): + im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF)) + histogram = im.resize((256, 256), Image.Resampling.BICUBIC).histogram() + + # first channel + assert histogram[0x100 * 0 + 0x20] == 0x10000 + # second channel + assert histogram[0x100 * 1 + 0x40] == 0x10000 + # third channel + assert histogram[0x100 * 2 + 0x60] == 0x10000 + # fourth channel + assert histogram[0x100 * 3 + 0xFF] == 0x10000 + + +class TestCoreResampleBox: + @pytest.mark.parametrize( + "resample", + ( + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, + ), + ) + def test_wrong_arguments(self, resample): im = hopper() - for resample in (Image.NEAREST, Image.BOX, Image.BILINEAR, Image.HAMMING, - Image.BICUBIC, Image.LANCZOS): - im.resize((32, 32), resample, (0, 0, im.width, im.height)) - im.resize((32, 32), resample, (20, 20, im.width, im.height)) - im.resize((32, 32), resample, (20, 20, 20, 100)) - im.resize((32, 32), resample, (20, 20, 100, 20)) - - with self.assertRaisesRegexp(TypeError, "must be sequence of length 4"): - im.resize((32, 32), resample, (im.width, im.height)) - - with self.assertRaisesRegexp(ValueError, "can't be negative"): - im.resize((32, 32), resample, (-20, 20, 100, 100)) - with self.assertRaisesRegexp(ValueError, "can't be negative"): - im.resize((32, 32), resample, (20, -20, 100, 100)) - - with self.assertRaisesRegexp(ValueError, "can't be empty"): - im.resize((32, 32), resample, (20.1, 20, 20, 100)) - with self.assertRaisesRegexp(ValueError, "can't be empty"): - im.resize((32, 32), resample, (20, 20.1, 100, 20)) - with self.assertRaisesRegexp(ValueError, "can't be empty"): - im.resize((32, 32), resample, (20.1, 20.1, 20, 20)) - - with self.assertRaisesRegexp(ValueError, "can't exceed"): - im.resize((32, 32), resample, (0, 0, im.width + 1, im.height)) - with self.assertRaisesRegexp(ValueError, "can't exceed"): - im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) + im.resize((32, 32), resample, (0, 0, im.width, im.height)) + im.resize((32, 32), resample, (20, 20, im.width, im.height)) + im.resize((32, 32), resample, (20, 20, 20, 100)) + im.resize((32, 32), resample, (20, 20, 100, 20)) + + with pytest.raises(TypeError, match="must be sequence of length 4"): + im.resize((32, 32), resample, (im.width, im.height)) + + with pytest.raises(ValueError, match="can't be negative"): + im.resize((32, 32), resample, (-20, 20, 100, 100)) + with pytest.raises(ValueError, match="can't be negative"): + im.resize((32, 32), resample, (20, -20, 100, 100)) + + with pytest.raises(ValueError, match="can't be empty"): + im.resize((32, 32), resample, (20.1, 20, 20, 100)) + with pytest.raises(ValueError, match="can't be empty"): + im.resize((32, 32), resample, (20, 20.1, 100, 20)) + with pytest.raises(ValueError, match="can't be empty"): + im.resize((32, 32), resample, (20.1, 20.1, 20, 20)) + + with pytest.raises(ValueError, match="can't exceed"): + im.resize((32, 32), resample, (0, 0, im.width + 1, im.height)) + with pytest.raises(ValueError, match="can't exceed"): + im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) def resize_tiled(self, im, dst_size, xtiles, ytiles): def split_range(size, tiles): scale = size / tiles for i in range(tiles): - yield (int(round(scale * i)), int(round(scale * (i + 1)))) + yield int(round(scale * i)), int(round(scale * (i + 1))) tiled = Image.new(im.mode, dst_size) scale = (im.size[0] / tiled.size[0], im.size[1] / tiled.size[1]) for y0, y1 in split_range(dst_size[1], ytiles): for x0, x1 in split_range(dst_size[0], xtiles): - box = (x0 * scale[0], y0 * scale[1], - x1 * scale[0], y1 * scale[1]) - tile = im.resize((x1 - x0, y1 - y0), Image.BICUBIC, box) + box = (x0 * scale[0], y0 * scale[1], x1 * scale[0], y1 * scale[1]) + tile = im.resize((x1 - x0, y1 - y0), Image.Resampling.BICUBIC, box) tiled.paste(tile, (x0, y0)) return tiled + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_tiles(self): - im = Image.open("Tests/images/flower.jpg") - assert im.size == (480, 360) - dst_size = (251, 188) - reference = im.resize(dst_size, Image.BICUBIC) - - for tiles in [(1, 1), (3, 3), (9, 7), (100, 100)]: - tiled = self.resize_tiled(im, dst_size, *tiles) - self.assert_image_similar(reference, tiled, 0.01) - + with Image.open("Tests/images/flower.jpg") as im: + assert im.size == (480, 360) + dst_size = (251, 188) + reference = im.resize(dst_size, Image.Resampling.BICUBIC) + + for tiles in [(1, 1), (3, 3), (9, 7), (100, 100)]: + tiled = self.resize_tiled(im, dst_size, *tiles) + assert_image_similar(reference, tiled, 0.01) + + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_subsample(self): # This test shows advantages of the subpixel resizing # after supersampling (e.g. during JPEG decoding). - im = Image.open("Tests/images/flower.jpg") - assert im.size == (480, 360) - dst_size = (48, 36) - # Reference is cropped image resized to destination - reference = im.crop((0, 0, 473, 353)).resize(dst_size, Image.BICUBIC) - # Image.BOX emulates supersampling (480 / 8 = 60, 360 / 8 = 45) - supersampled = im.resize((60, 45), Image.BOX) - - with_box = supersampled.resize(dst_size, Image.BICUBIC, - (0, 0, 59.125, 44.125)) - without_box = supersampled.resize(dst_size, Image.BICUBIC) + with Image.open("Tests/images/flower.jpg") as im: + assert im.size == (480, 360) + dst_size = (48, 36) + # Reference is cropped image resized to destination + reference = im.crop((0, 0, 473, 353)).resize( + dst_size, Image.Resampling.BICUBIC + ) + # Image.Resampling.BOX emulates supersampling (480 / 8 = 60, 360 / 8 = 45) + supersampled = im.resize((60, 45), Image.Resampling.BOX) + + with_box = supersampled.resize( + dst_size, Image.Resampling.BICUBIC, (0, 0, 59.125, 44.125) + ) + without_box = supersampled.resize(dst_size, Image.Resampling.BICUBIC) # error with box should be much smaller than without - self.assert_image_similar(reference, with_box, 6) - with self.assertRaisesRegexp(AssertionError, "difference 29\."): - self.assert_image_similar(reference, without_box, 5) - - def test_formats(self): - for resample in [Image.NEAREST, Image.BILINEAR]: - for mode in ['RGB', 'L', 'RGBA', 'LA', 'I', '']: - im = hopper(mode) - box = (20, 20, im.size[0] - 20, im.size[1] - 20) - with_box = im.resize((32, 32), resample, box) - cropped = im.crop(box).resize((32, 32), resample) - self.assert_image_similar(cropped, with_box, 0.4) + assert_image_similar(reference, with_box, 6) + with pytest.raises(AssertionError, match=r"difference 29\."): + assert_image_similar(reference, without_box, 5) + + @pytest.mark.parametrize("mode", ("RGB", "L", "RGBA", "LA", "I", "")) + @pytest.mark.parametrize( + "resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR) + ) + def test_formats(self, mode, resample): + im = hopper(mode) + box = (20, 20, im.size[0] - 20, im.size[1] - 20) + with_box = im.resize((32, 32), resample, box) + cropped = im.crop(box).resize((32, 32), resample) + assert_image_similar(cropped, with_box, 0.4) def test_passthrough(self): # When no resize is required @@ -469,13 +534,9 @@ def test_passthrough(self): ((40, 50), (10, 0, 50, 50)), ((40, 50), (10, 20, 50, 70)), ]: - try: - res = im.resize(size, Image.LANCZOS, box) - self.assertEqual(res.size, size) - self.assert_image_equal(res, im.crop(box)) - except AssertionError: - print('>>>', size, box) - raise + res = im.resize(size, Image.Resampling.LANCZOS, box) + assert res.size == size + assert_image_equal(res, im.crop(box), f">>> {size} {box}") def test_no_passthrough(self): # When resize is required @@ -487,58 +548,54 @@ def test_no_passthrough(self): ((40, 50), (10.4, 0.4, 50.4, 50.4)), ((40, 50), (10.4, 20.4, 50.4, 70.4)), ]: - try: - res = im.resize(size, Image.LANCZOS, box) - self.assertEqual(res.size, size) - with self.assertRaisesRegexp(AssertionError, "difference \d"): - # check that the difference at least that much - self.assert_image_similar(res, im.crop(box), 20) - except AssertionError: - print('>>>', size, box) - raise - - def test_skip_horizontal(self): + res = im.resize(size, Image.Resampling.LANCZOS, box) + assert res.size == size + with pytest.raises(AssertionError, match=r"difference \d"): + # check that the difference at least that much + assert_image_similar(res, im.crop(box), 20, f">>> {size} {box}") + + @pytest.mark.parametrize( + "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) + ) + def test_skip_horizontal(self, flt): # Can skip resize for one dimension im = hopper() - for flt in [Image.NEAREST, Image.BICUBIC]: - for size, box in [ - ((40, 50), (0, 0, 40, 90)), - ((40, 50), (0, 20, 40, 90)), - ((40, 50), (10, 0, 50, 90)), - ((40, 50), (10, 20, 50, 90)), - ]: - try: - res = im.resize(size, flt, box) - self.assertEqual(res.size, size) - # Borders should be slightly different - self.assert_image_similar( - res, im.crop(box).resize(size, flt), 0.4) - except AssertionError: - print('>>>', size, box, flt) - raise - - def test_skip_vertical(self): + for size, box in [ + ((40, 50), (0, 0, 40, 90)), + ((40, 50), (0, 20, 40, 90)), + ((40, 50), (10, 0, 50, 90)), + ((40, 50), (10, 20, 50, 90)), + ]: + res = im.resize(size, flt, box) + assert res.size == size + # Borders should be slightly different + assert_image_similar( + res, + im.crop(box).resize(size, flt), + 0.4, + f">>> {size} {box} {flt}", + ) + + @pytest.mark.parametrize( + "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) + ) + def test_skip_vertical(self, flt): # Can skip resize for one dimension im = hopper() - for flt in [Image.NEAREST, Image.BICUBIC]: - for size, box in [ - ((40, 50), (0, 0, 90, 50)), - ((40, 50), (20, 0, 90, 50)), - ((40, 50), (0, 10, 90, 60)), - ((40, 50), (20, 10, 90, 60)), - ]: - try: - res = im.resize(size, flt, box) - self.assertEqual(res.size, size) - # Borders should be slightly different - self.assert_image_similar( - res, im.crop(box).resize(size, flt), 0.4) - except AssertionError: - print('>>>', size, box, flt) - raise - - -if __name__ == '__main__': - unittest.main() + for size, box in [ + ((40, 50), (0, 0, 90, 50)), + ((40, 50), (20, 0, 90, 50)), + ((40, 50), (0, 10, 90, 60)), + ((40, 50), (20, 10, 90, 60)), + ]: + res = im.resize(size, flt, box) + assert res.size == size + # Borders should be slightly different + assert_image_similar( + res, + im.crop(box).resize(size, flt), + 0.4, + f">>> {size} {box} {flt}", + ) diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 8d14b900823..83c54cf6211 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -3,56 +3,101 @@ """ from itertools import permutations -from helper import unittest, PillowTestCase, hopper +import pytest from PIL import Image +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + hopper, + skip_unless_feature, +) -class TestImagingCoreResize(PillowTestCase): +class TestImagingCoreResize: def resize(self, im, size, f): # Image class independent version of resize. im.load() return im._new(im.im.resize(size, f)) - def test_nearest_mode(self): - for mode in ["1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr", - "I;16"]: # exotic mode - im = hopper(mode) - r = self.resize(im, (15, 12), Image.NEAREST) - self.assertEqual(r.mode, mode) - self.assertEqual(r.size, (15, 12)) - self.assertEqual(r.im.bands, im.im.bands) + @pytest.mark.parametrize( + "mode", ("1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr", "I;16") + ) + def test_nearest_mode(self, mode): + im = hopper(mode) + r = self.resize(im, (15, 12), Image.Resampling.NEAREST) + assert r.mode == mode + assert r.size == (15, 12) + assert r.im.bands == im.im.bands def test_convolution_modes(self): - self.assertRaises(ValueError, self.resize, hopper("1"), - (15, 12), Image.BILINEAR) - self.assertRaises(ValueError, self.resize, hopper("P"), - (15, 12), Image.BILINEAR) - self.assertRaises(ValueError, self.resize, hopper("I;16"), - (15, 12), Image.BILINEAR) + with pytest.raises(ValueError): + self.resize(hopper("1"), (15, 12), Image.Resampling.BILINEAR) + with pytest.raises(ValueError): + self.resize(hopper("P"), (15, 12), Image.Resampling.BILINEAR) + with pytest.raises(ValueError): + self.resize(hopper("I;16"), (15, 12), Image.Resampling.BILINEAR) for mode in ["L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr"]: im = hopper(mode) - r = self.resize(im, (15, 12), Image.BILINEAR) - self.assertEqual(r.mode, mode) - self.assertEqual(r.size, (15, 12)) - self.assertEqual(r.im.bands, im.im.bands) - - def test_reduce_filters(self): - for f in [Image.NEAREST, Image.BOX, Image.BILINEAR, Image.HAMMING, - Image.BICUBIC, Image.LANCZOS]: - r = self.resize(hopper("RGB"), (15, 12), f) - self.assertEqual(r.mode, "RGB") - self.assertEqual(r.size, (15, 12)) - - def test_enlarge_filters(self): - for f in [Image.NEAREST, Image.BOX, Image.BILINEAR, Image.HAMMING, - Image.BICUBIC, Image.LANCZOS]: - r = self.resize(hopper("RGB"), (212, 195), f) - self.assertEqual(r.mode, "RGB") - self.assertEqual(r.size, (212, 195)) - - def test_endianness(self): + r = self.resize(im, (15, 12), Image.Resampling.BILINEAR) + assert r.mode == mode + assert r.size == (15, 12) + assert r.im.bands == im.im.bands + + @pytest.mark.parametrize( + "resample", + ( + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, + ), + ) + def test_reduce_filters(self, resample): + r = self.resize(hopper("RGB"), (15, 12), resample) + assert r.mode == "RGB" + assert r.size == (15, 12) + + @pytest.mark.parametrize( + "resample", + ( + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, + ), + ) + def test_enlarge_filters(self, resample): + r = self.resize(hopper("RGB"), (212, 195), resample) + assert r.mode == "RGB" + assert r.size == (212, 195) + + @pytest.mark.parametrize( + "resample", + ( + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, + ), + ) + @pytest.mark.parametrize( + "mode, channels_set", + ( + ("RGB", ("blank", "filled", "dirty")), + ("RGBA", ("blank", "blank", "filled", "dirty")), + ("LA", ("filled", "dirty")), + ), + ) + def test_endianness(self, resample, mode, channels_set): # Make an image with one colored pixel, in one channel. # When resized, that channel should be the same as a GS image. # Other channels should be unaffected. @@ -60,62 +105,185 @@ def test_endianness(self): # an endianness issues. samples = { - 'blank': Image.new('L', (2, 2), 0), - 'filled': Image.new('L', (2, 2), 255), - 'dirty': Image.new('L', (2, 2), 0), + "blank": Image.new("L", (2, 2), 0), + "filled": Image.new("L", (2, 2), 255), + "dirty": Image.new("L", (2, 2), 0), } - samples['dirty'].putpixel((1, 1), 128) - - for f in [Image.NEAREST, Image.BOX, Image.BILINEAR, Image.HAMMING, - Image.BICUBIC, Image.LANCZOS]: - # samples resized with current filter - references = { - name: self.resize(ch, (4, 4), f) - for name, ch in samples.items() - } - - for mode, channels_set in [ - ('RGB', ('blank', 'filled', 'dirty')), - ('RGBA', ('blank', 'blank', 'filled', 'dirty')), - ('LA', ('filled', 'dirty')), - ]: - for channels in set(permutations(channels_set)): - # compile image from different channels permutations - im = Image.merge(mode, [samples[ch] for ch in channels]) - resized = self.resize(im, (4, 4), f) - - for i, ch in enumerate(resized.split()): - # check what resized channel in image is the same - # as separately resized channel - self.assert_image_equal(ch, references[channels[i]]) - - def test_enlarge_zero(self): - for f in [Image.NEAREST, Image.BOX, Image.BILINEAR, Image.HAMMING, - Image.BICUBIC, Image.LANCZOS]: - r = self.resize(Image.new('RGB', (0, 0), "white"), (212, 195), f) - self.assertEqual(r.mode, "RGB") - self.assertEqual(r.size, (212, 195)) - self.assertEqual(r.getdata()[0], (0, 0, 0)) + samples["dirty"].putpixel((1, 1), 128) + + # samples resized with current filter + references = { + name: self.resize(ch, (4, 4), resample) for name, ch in samples.items() + } + + for channels in set(permutations(channels_set)): + # compile image from different channels permutations + im = Image.merge(mode, [samples[ch] for ch in channels]) + resized = self.resize(im, (4, 4), resample) + + for i, ch in enumerate(resized.split()): + # check what resized channel in image is the same + # as separately resized channel + assert_image_equal(ch, references[channels[i]]) + + @pytest.mark.parametrize( + "resample", + ( + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, + ), + ) + def test_enlarge_zero(self, resample): + r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample) + assert r.mode == "RGB" + assert r.size == (212, 195) + assert r.getdata()[0] == (0, 0, 0) def test_unknown_filter(self): - self.assertRaises(ValueError, self.resize, hopper(), (10, 10), 9) + with pytest.raises(ValueError): + self.resize(hopper(), (10, 10), 9) + + def test_cross_platform(self, tmp_path): + # This test is intended for only check for consistent behaviour across + # platforms. So if a future Pillow change requires that the test file + # be updated, that is okay. + im = hopper().resize((64, 64)) + temp_file = str(tmp_path / "temp.gif") + im.save(temp_file) + + with Image.open(temp_file) as reloaded: + assert_image_equal_tofile(reloaded, "Tests/images/hopper_resized.gif") + + +@pytest.fixture +def gradients_image(): + with Image.open("Tests/images/radial_gradients.png") as im: + im.load() + try: + yield im + finally: + im.close() + + +class TestReducingGapResize: + def test_reducing_gap_values(self, gradients_image): + ref = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, reducing_gap=None + ) + im = gradients_image.resize((52, 34), Image.Resampling.BICUBIC) + assert_image_equal(ref, im) + with pytest.raises(ValueError): + gradients_image.resize((52, 34), Image.Resampling.BICUBIC, reducing_gap=0) -class TestImageResize(PillowTestCase): + with pytest.raises(ValueError): + gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, reducing_gap=0.99 + ) + @pytest.mark.parametrize( + "box, epsilon", + ((None, 4), ((1.1, 2.2, 510.8, 510.9), 4), ((3, 10, 410, 256), 10)), + ) + def test_reducing_gap_1(self, gradients_image, box, epsilon): + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 + ) + + with pytest.raises(AssertionError): + assert_image_equal(ref, im) + + assert_image_similar(ref, im, epsilon) + + @pytest.mark.parametrize( + "box, epsilon", + ((None, 1.5), ((1.1, 2.2, 510.8, 510.9), 1.5), ((3, 10, 410, 256), 1)), + ) + def test_reducing_gap_2(self, gradients_image, box, epsilon): + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 + ) + + with pytest.raises(AssertionError): + assert_image_equal(ref, im) + + assert_image_similar(ref, im, epsilon) + + @pytest.mark.parametrize( + "box, epsilon", + ((None, 1), ((1.1, 2.2, 510.8, 510.9), 1), ((3, 10, 410, 256), 0.5)), + ) + def test_reducing_gap_3(self, gradients_image, box, epsilon): + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 + ) + + with pytest.raises(AssertionError): + assert_image_equal(ref, im) + + assert_image_similar(ref, im, epsilon) + + @pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256))) + def test_reducing_gap_8(self, gradients_image, box): + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0 + ) + + assert_image_equal(ref, im) + + @pytest.mark.parametrize( + "box, epsilon", + (((0, 0, 512, 512), 5.5), ((0.9, 1.7, 128, 128), 9.5)), + ) + def test_box_filter(self, gradients_image, box, epsilon): + ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0 + ) + + assert_image_similar(ref, im, epsilon) + + +class TestImageResize: def test_resize(self): def resize(mode, size): out = hopper(mode).resize(size) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, size) + assert out.mode == mode + assert out.size == size + for mode in "1", "P", "L", "RGB", "I", "F": resize(mode, (112, 103)) resize(mode, (188, 214)) # Test unknown resampling filter - im = hopper() - self.assertRaises(ValueError, im.resize, (10, 10), "unknown") + with hopper() as im: + with pytest.raises(ValueError): + im.resize((10, 10), "unknown") + + @skip_unless_feature("libtiff") + def test_load_first(self): + # load() may change the size of the image + # Test that resize() is calling it before getting the size + with Image.open("Tests/images/g4_orientation_5.tif") as im: + im = im.resize((64, 64)) + assert im.size == (64, 64) + @pytest.mark.parametrize("mode", ("L", "RGB", "I", "F")) + def test_default_filter_bicubic(self, mode): + im = hopper(mode) + assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) -if __name__ == '__main__': - unittest.main() + @pytest.mark.parametrize( + "mode", ("1", "P", "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16") + ) + def test_default_filter_nearest(self, mode): + im = hopper(mode) + assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index fbcf9008d83..a19f19831fd 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -1,102 +1,148 @@ -from helper import unittest, PillowTestCase, hopper -from PIL import Image +import pytest +from PIL import Image -class TestImageRotate(PillowTestCase): - - def rotate(self, im, mode, angle, center=None, translate=None): - out = im.rotate(angle, center=center, translate=translate) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size) # default rotate clips output - out = im.rotate(angle, center=center, translate=translate, expand=1) - self.assertEqual(out.mode, mode) - if angle % 180 == 0: - self.assertEqual(out.size, im.size) - elif im.size == (0, 0): - self.assertEqual(out.size, im.size) - else: - self.assertNotEqual(out.size, im.size) - - def test_mode(self): - for mode in ("1", "P", "L", "RGB", "I", "F"): - im = hopper(mode) - self.rotate(im, mode, 45) - - def test_angle(self): - for angle in (0, 90, 180, 270): - im = Image.open('Tests/images/test-card.png') - self.rotate(im, im.mode, angle) - - def test_zero(self): - for angle in (0, 45, 90, 180, 270): - im = Image.new('RGB', (0, 0)) - self.rotate(im, im.mode, angle) - - def test_resample(self): - # Target image creation, inspected by eye. - # >>> im = Image.open('Tests/images/hopper.ppm') - # >>> im = im.rotate(45, resample=Image.BICUBIC, expand=True) - # >>> im.save('Tests/images/hopper_45.png') - - target = Image.open('Tests/images/hopper_45.png') - for (resample, epsilon) in ((Image.NEAREST, 10), - (Image.BILINEAR, 5), - (Image.BICUBIC, 0)): +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + hopper, +) + + +def rotate(im, mode, angle, center=None, translate=None): + out = im.rotate(angle, center=center, translate=translate) + assert out.mode == mode + assert out.size == im.size # default rotate clips output + out = im.rotate(angle, center=center, translate=translate, expand=1) + assert out.mode == mode + if angle % 180 == 0: + assert out.size == im.size + elif im.size == (0, 0): + assert out.size == im.size + else: + assert out.size != im.size + + +@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) +def test_mode(mode): + im = hopper(mode) + rotate(im, mode, 45) + + +@pytest.mark.parametrize("angle", (0, 90, 180, 270)) +def test_angle(angle): + with Image.open("Tests/images/test-card.png") as im: + rotate(im, im.mode, angle) + + im = hopper() + assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1)) + + +@pytest.mark.parametrize("angle", (0, 45, 90, 180, 270)) +def test_zero(angle): + im = Image.new("RGB", (0, 0)) + rotate(im, im.mode, angle) + + +def test_resample(): + # Target image creation, inspected by eye. + # >>> im = Image.open('Tests/images/hopper.ppm') + # >>> im = im.rotate(45, resample=Image.Resampling.BICUBIC, expand=True) + # >>> im.save('Tests/images/hopper_45.png') + + with Image.open("Tests/images/hopper_45.png") as target: + for (resample, epsilon) in ( + (Image.Resampling.NEAREST, 10), + (Image.Resampling.BILINEAR, 5), + (Image.Resampling.BICUBIC, 0), + ): im = hopper() im = im.rotate(45, resample=resample, expand=True) - self.assert_image_similar(im, target, epsilon) + assert_image_similar(im, target, epsilon) + + +def test_center_0(): + im = hopper() + im = im.rotate(45, center=(0, 0), resample=Image.Resampling.BICUBIC) - def test_center_0(self): - im = hopper() - target = Image.open('Tests/images/hopper_45.png') - target_origin = target.size[1]/2 + with Image.open("Tests/images/hopper_45.png") as target: + target_origin = target.size[1] / 2 target = target.crop((0, target_origin, 128, target_origin + 128)) - im = im.rotate(45, center=(0, 0), resample=Image.BICUBIC) + assert_image_similar(im, target, 15) - self.assert_image_similar(im, target, 15) - def test_center_14(self): - im = hopper() - target = Image.open('Tests/images/hopper_45.png') +def test_center_14(): + im = hopper() + im = im.rotate(45, center=(14, 14), resample=Image.Resampling.BICUBIC) + + with Image.open("Tests/images/hopper_45.png") as target: target_origin = target.size[1] / 2 - 14 target = target.crop((6, target_origin, 128 + 6, target_origin + 128)) - im = im.rotate(45, center=(14, 14), resample=Image.BICUBIC) + assert_image_similar(im, target, 10) - self.assert_image_similar(im, target, 10) - def test_translate(self): - im = hopper() - target = Image.open('Tests/images/hopper_45.png') +def test_translate(): + im = hopper() + with Image.open("Tests/images/hopper_45.png") as target: target_origin = (target.size[1] / 2 - 64) - 5 - target = target.crop((target_origin, target_origin, - target_origin + 128, target_origin + 128)) + target = target.crop( + (target_origin, target_origin, target_origin + 128, target_origin + 128) + ) + + im = im.rotate(45, translate=(5, 5), resample=Image.Resampling.BICUBIC) + + assert_image_similar(im, target, 1) + + +def test_fastpath_center(): + # if the center is -1,-1 and we rotate by 90<=x<=270 the + # resulting image should be black + for angle in (90, 180, 270): + im = hopper().rotate(angle, center=(-1, -1)) + assert_image_equal(im, Image.new("RGB", im.size, "black")) + + +def test_fastpath_translate(): + # if we post-translate by -128 + # resulting image should be black + for angle in (0, 90, 180, 270): + im = hopper().rotate(angle, translate=(-128, -128)) + assert_image_equal(im, Image.new("RGB", im.size, "black")) + + +def test_center(): + im = hopper() + rotate(im, im.mode, 45, center=(0, 0)) + rotate(im, im.mode, 45, translate=(im.size[0] / 2, 0)) + rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] / 2, 0)) + - im = im.rotate(45, translate=(5, 5), resample=Image.BICUBIC) +def test_rotate_no_fill(): + im = Image.new("RGB", (100, 100), "green") + im = im.rotate(45) + assert_image_equal_tofile(im, "Tests/images/rotate_45_no_fill.png") - self.assert_image_similar(im, target, 1) - def test_fastpath_center(self): - # if the center is -1,-1 and we rotate by 90<=x<=270 the - # resulting image should be black - for angle in (90, 180, 270): - im = hopper().rotate(angle, center=(-1, -1)) - self.assert_image_equal(im, Image.new('RGB', im.size, 'black')) +def test_rotate_with_fill(): + im = Image.new("RGB", (100, 100), "green") + im = im.rotate(45, fillcolor="white") + assert_image_equal_tofile(im, "Tests/images/rotate_45_with_fill.png") - def test_fastpath_translate(self): - # if we post-translate by -128 - # resulting image should be black - for angle in (0, 90, 180, 270): - im = hopper().rotate(angle, translate=(-128, -128)) - self.assert_image_equal(im, Image.new('RGB', im.size, 'black')) - def test_center(self): - im = hopper() - self.rotate(im, im.mode, 45, center=(0, 0)) - self.rotate(im, im.mode, 45, translate=(im.size[0]/2, 0)) - self.rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0]/2, 0)) +def test_alpha_rotate_no_fill(): + # Alpha images are handled differently internally + im = Image.new("RGBA", (10, 10), "green") + im = im.rotate(45, expand=1) + corner = im.getpixel((0, 0)) + assert corner == (0, 0, 0, 0) -if __name__ == '__main__': - unittest.main() +def test_alpha_rotate_with_fill(): + # Alpha images are handled differently internally + im = Image.new("RGBA", (10, 10), "green") + im = im.rotate(45, expand=1, fillcolor=(255, 0, 0, 255)) + corner = im.getpixel((0, 0)) + assert corner == (255, 0, 0, 255) diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index 6f312ff8092..5cb7c9a8be8 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,65 +1,58 @@ -from helper import unittest, PillowTestCase, hopper - -from PIL import Image - - -class TestImageSplit(PillowTestCase): - - def test_split(self): - def split(mode): - layers = hopper(mode).split() - return [(i.mode, i.size[0], i.size[1]) for i in layers] - self.assertEqual(split("1"), [('1', 128, 128)]) - self.assertEqual(split("L"), [('L', 128, 128)]) - self.assertEqual(split("I"), [('I', 128, 128)]) - self.assertEqual(split("F"), [('F', 128, 128)]) - self.assertEqual(split("P"), [('P', 128, 128)]) - self.assertEqual( - split("RGB"), [('L', 128, 128), ('L', 128, 128), ('L', 128, 128)]) - self.assertEqual( - split("RGBA"), - [('L', 128, 128), ('L', 128, 128), - ('L', 128, 128), ('L', 128, 128)]) - self.assertEqual( - split("CMYK"), - [('L', 128, 128), ('L', 128, 128), - ('L', 128, 128), ('L', 128, 128)]) - self.assertEqual( - split("YCbCr"), - [('L', 128, 128), ('L', 128, 128), ('L', 128, 128)]) - - def test_split_merge(self): - def split_merge(mode): - return Image.merge(mode, hopper(mode).split()) - self.assert_image_equal(hopper("1"), split_merge("1")) - self.assert_image_equal(hopper("L"), split_merge("L")) - self.assert_image_equal(hopper("I"), split_merge("I")) - self.assert_image_equal(hopper("F"), split_merge("F")) - self.assert_image_equal(hopper("P"), split_merge("P")) - self.assert_image_equal(hopper("RGB"), split_merge("RGB")) - self.assert_image_equal(hopper("RGBA"), split_merge("RGBA")) - self.assert_image_equal(hopper("CMYK"), split_merge("CMYK")) - self.assert_image_equal(hopper("YCbCr"), split_merge("YCbCr")) - - def test_split_open(self): - codecs = dir(Image.core) - - if 'zip_encoder' in codecs: - test_file = self.tempfile("temp.png") - else: - test_file = self.tempfile("temp.pcx") - - def split_open(mode): - hopper(mode).save(test_file) - im = Image.open(test_file) +import pytest + +from PIL import Image, features + +from .helper import assert_image_equal, hopper + + +def test_split(): + def split(mode): + layers = hopper(mode).split() + return [(i.mode, i.size[0], i.size[1]) for i in layers] + + assert split("1") == [("1", 128, 128)] + assert split("L") == [("L", 128, 128)] + assert split("I") == [("I", 128, 128)] + assert split("F") == [("F", 128, 128)] + assert split("P") == [("P", 128, 128)] + assert split("RGB") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] + assert split("RGBA") == [ + ("L", 128, 128), + ("L", 128, 128), + ("L", 128, 128), + ("L", 128, 128), + ] + assert split("CMYK") == [ + ("L", 128, 128), + ("L", 128, 128), + ("L", 128, 128), + ("L", 128, 128), + ] + assert split("YCbCr") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] + + +@pytest.mark.parametrize( + "mode", ("1", "L", "I", "F", "P", "RGB", "RGBA", "CMYK", "YCbCr") +) +def test_split_merge(mode): + expected = Image.merge(mode, hopper(mode).split()) + assert_image_equal(hopper(mode), expected) + + +def test_split_open(tmp_path): + if features.check("zlib"): + test_file = str(tmp_path / "temp.png") + else: + test_file = str(tmp_path / "temp.pcx") + + def split_open(mode): + hopper(mode).save(test_file) + with Image.open(test_file) as im: return len(im.split()) - self.assertEqual(split_open("1"), 1) - self.assertEqual(split_open("L"), 1) - self.assertEqual(split_open("P"), 1) - self.assertEqual(split_open("RGB"), 3) - if 'zip_encoder' in codecs: - self.assertEqual(split_open("RGBA"), 4) - -if __name__ == '__main__': - unittest.main() + assert split_open("1") == 1 + assert split_open("L") == 1 + assert split_open("P") == 1 + assert split_open("RGB") == 3 + if features.check("zlib"): + assert split_open("RGBA") == 4 diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 6b92dbb2411..4fd07a2b4d2 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -1,41 +1,165 @@ -from helper import unittest, PillowTestCase, hopper +import pytest +from PIL import Image -class TestImageThumbnail(PillowTestCase): +from .helper import ( + assert_image_equal, + assert_image_similar, + fromstring, + hopper, + skip_unless_feature, + tostring, +) - def test_sanity(self): - im = hopper() - im.thumbnail((100, 100)) +def test_sanity(): + im = hopper() + assert im.thumbnail((100, 100)) is None - self.assert_image(im, im.mode, (100, 100)) + assert im.size == (100, 100) - def test_aspect(self): - im = hopper() - im.thumbnail((100, 100)) - self.assert_image(im, im.mode, (100, 100)) +def test_aspect(): + im = Image.new("L", (128, 128)) + im.thumbnail((100, 100)) + assert im.size == (100, 100) - im = hopper().resize((128, 256)) - im.thumbnail((100, 100)) - self.assert_image(im, im.mode, (50, 100)) + im = Image.new("L", (128, 256)) + im.thumbnail((100, 100)) + assert im.size == (50, 100) - im = hopper().resize((128, 256)) - im.thumbnail((50, 100)) - self.assert_image(im, im.mode, (50, 100)) + im = Image.new("L", (128, 256)) + im.thumbnail((50, 100)) + assert im.size == (50, 100) - im = hopper().resize((256, 128)) - im.thumbnail((100, 100)) - self.assert_image(im, im.mode, (100, 50)) + im = Image.new("L", (256, 128)) + im.thumbnail((100, 100)) + assert im.size == (100, 50) - im = hopper().resize((256, 128)) - im.thumbnail((100, 50)) - self.assert_image(im, im.mode, (100, 50)) + im = Image.new("L", (256, 128)) + im.thumbnail((100, 50)) + assert im.size == (100, 50) - im = hopper().resize((128, 128)) - im.thumbnail((100, 100)) - self.assert_image(im, im.mode, (100, 100)) + im = Image.new("L", (64, 64)) + im.thumbnail((100, 100)) + assert im.size == (64, 64) + im = Image.new("L", (256, 162)) # ratio is 1.5802469136 + im.thumbnail((33, 33)) + assert im.size == (33, 21) # ratio is 1.5714285714 -if __name__ == '__main__': - unittest.main() + im = Image.new("L", (162, 256)) # ratio is 0.6328125 + im.thumbnail((33, 33)) + assert im.size == (21, 33) # ratio is 0.6363636364 + + im = Image.new("L", (145, 100)) # ratio is 1.45 + im.thumbnail((50, 50)) + assert im.size == (50, 34) # ratio is 1.47058823529 + + im = Image.new("L", (100, 145)) # ratio is 0.689655172414 + im.thumbnail((50, 50)) + assert im.size == (34, 50) # ratio is 0.68 + + im = Image.new("L", (100, 30)) # ratio is 3.333333333333 + im.thumbnail((75, 75)) + assert im.size == (75, 23) # ratio is 3.260869565217 + + +def test_division_by_zero(): + im = Image.new("L", (200, 2)) + im.thumbnail((75, 75)) + assert im.size == (75, 1) + + +def test_float(): + im = Image.new("L", (128, 128)) + im.thumbnail((99.9, 99.9)) + assert im.size == (99, 99) + + +def test_no_resize(): + # Check that draft() can resize the image to the destination size + with Image.open("Tests/images/hopper.jpg") as im: + im.draft(None, (64, 64)) + assert im.size == (64, 64) + + # Test thumbnail(), where only draft() is necessary to resize the image + with Image.open("Tests/images/hopper.jpg") as im: + im.thumbnail((64, 64)) + assert im.size == (64, 64) + + +@skip_unless_feature("libtiff") +def test_load_first(): + # load() may change the size of the image + # Test that thumbnail() is calling it before performing size calculations + with Image.open("Tests/images/g4_orientation_5.tif") as im: + im.thumbnail((64, 64)) + assert im.size == (64, 10) + + # Test thumbnail(), without draft(), + # on an image that is large enough once load() has changed the size + with Image.open("Tests/images/g4_orientation_5.tif") as im: + im.thumbnail((590, 88), reducing_gap=None) + assert im.size == (590, 88) + + +def test_load_first_unless_jpeg(): + # Test that thumbnail() still uses draft() for JPEG + with Image.open("Tests/images/hopper.jpg") as im: + draft = im.draft + + def im_draft(mode, size): + result = draft(mode, size) + assert result is not None + + return result + + im.draft = im_draft + + im.thumbnail((64, 64)) + + +# valgrind test is failing with memory allocated in libjpeg +@pytest.mark.valgrind_known_error(reason="Known Failing") +def test_DCT_scaling_edges(): + # Make an image with red borders and size (N * 8) + 1 to cross DCT grid + im = Image.new("RGB", (257, 257), "red") + im.paste(Image.new("RGB", (235, 235)), (11, 11)) + + thumb = fromstring(tostring(im, "JPEG", quality=99, subsampling=0)) + # small reducing_gap to amplify the effect + thumb.thumbnail((32, 32), Image.Resampling.BICUBIC, reducing_gap=1.0) + + ref = im.resize((32, 32), Image.Resampling.BICUBIC) + # This is still JPEG, some error is present. Without the fix it is 11.5 + assert_image_similar(thumb, ref, 1.5) + + +def test_reducing_gap_values(): + im = hopper() + im.thumbnail((18, 18), Image.Resampling.BICUBIC) + + ref = hopper() + ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=2.0) + # reducing_gap=2.0 should be the default + assert_image_equal(ref, im) + + ref = hopper() + ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None) + with pytest.raises(AssertionError): + assert_image_equal(ref, im) + + assert_image_similar(ref, im, 3.5) + + +def test_reducing_gap_for_DCT_scaling(): + with Image.open("Tests/images/hopper.jpg") as ref: + # thumbnail should call draft with reducing_gap scale + ref.draft(None, (18 * 3, 18 * 3)) + ref = ref.resize((18, 18), Image.Resampling.BICUBIC) + + with Image.open("Tests/images/hopper.jpg") as im: + im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0) + + assert_image_similar(ref, im, 1.4) diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index 5c47eade729..178cfcef359 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -1,20 +1,16 @@ -from helper import unittest, PillowTestCase, hopper, fromstring +import pytest +from .helper import assert_image_equal, fromstring, hopper -class TestImageToBitmap(PillowTestCase): - def test_sanity(self): +def test_sanity(): - self.assertRaises(ValueError, lambda: hopper().tobitmap()) - hopper().convert("1").tobitmap() + with pytest.raises(ValueError): + hopper().tobitmap() - im1 = hopper().convert("1") + im1 = hopper().convert("1") - bitmap = im1.tobitmap() + bitmap = im1.tobitmap() - self.assertIsInstance(bitmap, bytes) - self.assert_image_equal(im1, fromstring(bitmap)) - - -if __name__ == '__main__': - unittest.main() + assert isinstance(bitmap, bytes) + assert_image_equal(im1, fromstring(bitmap)) diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py index 2cae05e6667..31e1c0080c6 100644 --- a/Tests/test_image_tobytes.py +++ b/Tests/test_image_tobytes.py @@ -1,11 +1,6 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import hopper -class TestImageToBytes(PillowTestCase): - - def test_sanity(self): - data = hopper().tobytes() - self.assertIsInstance(data, bytes) - -if __name__ == '__main__': - unittest.main() +def test_sanity(): + data = hopper().tobytes() + assert isinstance(data, bytes) diff --git a/Tests/test_image_toqimage.py b/Tests/test_image_toqimage.py deleted file mode 100644 index 6d7715c80a1..00000000000 --- a/Tests/test_image_toqimage.py +++ /dev/null @@ -1,91 +0,0 @@ -from helper import unittest, PillowTestCase, hopper -from test_imageqt import PillowQtTestCase - -from PIL import ImageQt, Image - - -if ImageQt.qt_is_installed: - from PIL.ImageQt import QImage - - try: - from PyQt5 import QtGui - from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QApplication - QT_VERSION = 5 - except (ImportError, RuntimeError): - try: - from PyQt4 import QtGui - from PyQt4.QtGui import QWidget, QHBoxLayout, QLabel, QApplication - QT_VERSION = 4 - except (ImportError, RuntimeError): - from PySide import QtGui - from PySide.QtGui import QWidget, QHBoxLayout, QLabel, QApplication - QT_VERSION = 4 - - -class TestToQImage(PillowQtTestCase, PillowTestCase): - - def test_sanity(self): - PillowQtTestCase.setUp(self) - for mode in ('RGB', 'RGBA', 'L', 'P', '1'): - src = hopper(mode) - data = ImageQt.toqimage(src) - - self.assertIsInstance(data, QImage) - self.assertFalse(data.isNull()) - - # reload directly from the qimage - rt = ImageQt.fromqimage(data) - if mode in ('L', 'P', '1'): - self.assert_image_equal(rt, src.convert('RGB')) - else: - self.assert_image_equal(rt, src) - - if mode == '1': - # BW appears to not save correctly on QT4 and QT5 - # kicks out errors on console: - # libpng warning: Invalid color type/bit depth combination in IHDR - # libpng error: Invalid IHDR data - continue - - # Test saving the file - tempfile = self.tempfile('temp_{}.png'.format(mode)) - data.save(tempfile) - - # Check that it actually worked. - reloaded = Image.open(tempfile) - # Gray images appear to come back in palette mode. - # They're roughly equivalent - if QT_VERSION == 4 and mode == 'L': - src = src.convert('P') - self.assert_image_equal(reloaded, src) - - def test_segfault(self): - PillowQtTestCase.setUp(self) - - app = QApplication([]) - ex = Example() - assert(app) # Silence warning - assert(ex) # Silence warning - - -if ImageQt.qt_is_installed: - class Example(QWidget): - - def __init__(self): - super(Example, self).__init__() - - img = hopper().resize((1000, 1000)) - - qimage = ImageQt.ImageQt(img) - - pixmap1 = QtGui.QPixmap.fromImage(qimage) - - hbox = QHBoxLayout(self) - - lbl = QLabel(self) - # Segfault in the problem - lbl.setPixmap(pixmap1.copy()) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_toqpixmap.py b/Tests/test_image_toqpixmap.py deleted file mode 100644 index c6555d7ff4a..00000000000 --- a/Tests/test_image_toqpixmap.py +++ /dev/null @@ -1,27 +0,0 @@ -from helper import unittest, PillowTestCase, hopper -from test_imageqt import PillowQtTestCase, PillowQPixmapTestCase - -from PIL import ImageQt - -if ImageQt.qt_is_installed: - from PIL.ImageQt import QPixmap - - -class TestToQPixmap(PillowQPixmapTestCase, PillowTestCase): - - def test_sanity(self): - PillowQtTestCase.setUp(self) - - for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): - data = ImageQt.toqpixmap(hopper(mode)) - - self.assertIsInstance(data, QPixmap) - self.assertFalse(data.isNull()) - - # Test saving the file - tempfile = self.tempfile('temp_{}.png'.format(mode)) - data.save(tempfile) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index df8fc83e84a..a78349801fc 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,15 +1,14 @@ import math -from helper import unittest, PillowTestCase, hopper +import pytest -from PIL import Image +from PIL import Image, ImageTransform +from .helper import assert_image_equal, assert_image_similar, hopper -class TestImageTransform(PillowTestCase): +class TestImageTransform: def test_sanity(self): - from PIL import ImageTransform - im = Image.new("L", (100, 100)) seq = tuple(range(10)) @@ -23,109 +22,181 @@ def test_sanity(self): transform = ImageTransform.MeshTransform([(seq[:4], seq[:8])]) im.transform((100, 100), transform) + def test_info(self): + comment = b"File written by Adobe Photoshop\xa8 4.0" + + with Image.open("Tests/images/hopper.gif") as im: + assert im.info["comment"] == comment + + transform = ImageTransform.ExtentTransform((0, 0, 0, 0)) + new_im = im.transform((100, 100), transform) + assert new_im.info["comment"] == comment + + def test_palette(self): + with Image.open("Tests/images/hopper.gif") as im: + transformed = im.transform( + im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0] + ) + assert im.palette.palette == transformed.palette.palette + def test_extent(self): - im = hopper('RGB') + im = hopper("RGB") (w, h) = im.size - transformed = im.transform(im.size, Image.EXTENT, + # fmt: off + transformed = im.transform(im.size, Image.Transform.EXTENT, (0, 0, w//2, h//2), # ul -> lr - Image.BILINEAR) + Image.Resampling.BILINEAR) + # fmt: on - scaled = im.resize((w*2, h*2), Image.BILINEAR).crop((0, 0, w, h)) + scaled = im.resize((w * 2, h * 2), Image.Resampling.BILINEAR).crop((0, 0, w, h)) # undone -- precision? - self.assert_image_similar(transformed, scaled, 23) + assert_image_similar(transformed, scaled, 23) def test_quad(self): # one simple quad transform, equivalent to scale & crop upper left quad - im = hopper('RGB') + im = hopper("RGB") (w, h) = im.size - transformed = im.transform(im.size, Image.QUAD, + # fmt: off + transformed = im.transform(im.size, Image.Transform.QUAD, (0, 0, 0, h//2, # ul -> ccw around quad: w//2, h//2, w//2, 0), - Image.BILINEAR) - - scaled = im.transform((w, h), Image.AFFINE, - (.5, 0, 0, 0, .5, 0), - Image.BILINEAR) - - self.assert_image_equal(transformed, scaled) - - def test_fill(self): - im = hopper('RGB') + Image.Resampling.BILINEAR) + # fmt: on + + scaled = im.transform( + (w, h), + Image.Transform.AFFINE, + (0.5, 0, 0, 0, 0.5, 0), + Image.Resampling.BILINEAR, + ) + + assert_image_equal(transformed, scaled) + + @pytest.mark.parametrize( + "mode, expected_pixel", + ( + ("RGB", (255, 0, 0)), + ("RGBA", (255, 0, 0, 255)), + ("LA", (76, 0)), + ), + ) + def test_fill(self, mode, expected_pixel): + im = hopper(mode) (w, h) = im.size - transformed = im.transform(im.size, Image.EXTENT, - (0, 0, - w*2, h*2), - Image.BILINEAR, - fillcolor = 'red') - - self.assertEqual(transformed.getpixel((w-1,h-1)), (255,0,0)) + transformed = im.transform( + im.size, + Image.Transform.EXTENT, + (0, 0, w * 2, h * 2), + Image.Resampling.BILINEAR, + fillcolor="red", + ) + assert transformed.getpixel((w - 1, h - 1)) == expected_pixel def test_mesh(self): # this should be a checkerboard of halfsized hoppers in ul, lr - im = hopper('RGBA') + im = hopper("RGBA") (w, h) = im.size - transformed = im.transform(im.size, Image.MESH, + # fmt: off + transformed = im.transform(im.size, Image.Transform.MESH, [((0, 0, w//2, h//2), # box (0, 0, 0, h, w, h, w, 0)), # ul -> ccw around quad ((w//2, h//2, w, h), # box (0, 0, 0, h, w, h, w, 0))], # ul -> ccw around quad - Image.BILINEAR) + Image.Resampling.BILINEAR) + # fmt: on - scaled = im.transform((w//2, h//2), Image.AFFINE, - (2, 0, 0, 0, 2, 0), - Image.BILINEAR) + scaled = im.transform( + (w // 2, h // 2), + Image.Transform.AFFINE, + (2, 0, 0, 0, 2, 0), + Image.Resampling.BILINEAR, + ) - checker = Image.new('RGBA', im.size) + checker = Image.new("RGBA", im.size) checker.paste(scaled, (0, 0)) - checker.paste(scaled, (w//2, h//2)) + checker.paste(scaled, (w // 2, h // 2)) - self.assert_image_equal(transformed, checker) + assert_image_equal(transformed, checker) # now, check to see that the extra area is (0, 0, 0, 0) - blank = Image.new('RGBA', (w//2, h//2), (0, 0, 0, 0)) + blank = Image.new("RGBA", (w // 2, h // 2), (0, 0, 0, 0)) - self.assert_image_equal(blank, transformed.crop((w//2, 0, w, h//2))) - self.assert_image_equal(blank, transformed.crop((0, h//2, w//2, h))) + assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2))) + assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h))) def _test_alpha_premult(self, op): # create image with half white, half black, # with the black half transparent. # do op, # there should be no darkness in the white section. - im = Image.new('RGBA', (10, 10), (0, 0, 0, 0)) - im2 = Image.new('RGBA', (5, 10), (255, 255, 255, 255)) + im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) + im2 = Image.new("RGBA", (5, 10), (255, 255, 255, 255)) im.paste(im2, (0, 0)) im = op(im, (40, 10)) - im_background = Image.new('RGB', (40, 10), (255, 255, 255)) + im_background = Image.new("RGB", (40, 10), (255, 255, 255)) im_background.paste(im, (0, 0), im) hist = im_background.histogram() - self.assertEqual(40*10, hist[-1]) + assert 40 * 10 == hist[-1] def test_alpha_premult_resize(self): - def op(im, sz): - return im.resize(sz, Image.BILINEAR) + return im.resize(sz, Image.Resampling.BILINEAR) self._test_alpha_premult(op) def test_alpha_premult_transform(self): - def op(im, sz): (w, h) = im.size - return im.transform(sz, Image.EXTENT, - (0, 0, - w, h), - Image.BILINEAR) + return im.transform( + sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR + ) self._test_alpha_premult(op) + def _test_nearest(self, op, mode): + # create white image with half transparent, + # do op, + # the image should remain white with half transparent + transparent, opaque = { + "RGBA": ((255, 255, 255, 0), (255, 255, 255, 255)), + "LA": ((255, 0), (255, 255)), + }[mode] + im = Image.new(mode, (10, 10), transparent) + im2 = Image.new(mode, (5, 10), opaque) + im.paste(im2, (0, 0)) + + im = op(im, (40, 10)) + + colors = im.getcolors() + assert colors == [ + (20 * 10, opaque), + (20 * 10, transparent), + ] + + @pytest.mark.parametrize("mode", ("RGBA", "LA")) + def test_nearest_resize(self, mode): + def op(im, sz): + return im.resize(sz, Image.Resampling.NEAREST) + + self._test_nearest(op, mode) + + @pytest.mark.parametrize("mode", ("RGBA", "LA")) + def test_nearest_transform(self, mode): + def op(im, sz): + (w, h) = im.size + return im.transform( + sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST + ) + + self._test_nearest(op, mode) + def test_blank_fill(self): # attempting to hit # https://github.com/python-pillow/Pillow/issues/254 reported @@ -141,36 +212,56 @@ def test_blank_fill(self): # Running by default, but I'd totally understand not doing it in # the future - pattern = [ - Image.new('RGBA', (1024, 1024), (a, a, a, a)) - for a in range(1, 65) - ] + pattern = [Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65)] # Yeah. Watch some JIT optimize this out. - pattern = None + pattern = None # noqa: F841 self.test_mesh() def test_missing_method_data(self): - im = hopper() - self.assertRaises(ValueError, im.transform, (100, 100), None) + with hopper() as im: + with pytest.raises(ValueError): + im.transform((100, 100), None) + + @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) + def test_unknown_resampling_filter(self, resample): + with hopper() as im: + (w, h) = im.size + with pytest.raises(ValueError): + im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) -class TestImageTransformAffine(PillowTestCase): - transform = Image.AFFINE +class TestImageTransformAffine: + transform = Image.Transform.AFFINE def _test_image(self): - im = hopper('RGB') + im = hopper("RGB") return im.crop((10, 20, im.width - 10, im.height - 20)) - def _test_rotate(self, deg, transpose): + @pytest.mark.parametrize( + "deg, transpose", + ( + (0, None), + (90, Image.Transpose.ROTATE_90), + (180, Image.Transpose.ROTATE_180), + (270, Image.Transpose.ROTATE_270), + ), + ) + def test_rotate(self, deg, transpose): im = self._test_image() - angle = - math.radians(deg) + angle = -math.radians(deg) matrix = [ - round(math.cos(angle), 15), round(math.sin(angle), 15), 0.0, - round(-math.sin(angle), 15), round(math.cos(angle), 15), 0.0, - 0, 0] + round(math.cos(angle), 15), + round(math.sin(angle), 15), + 0.0, + round(-math.sin(angle), 15), + round(math.cos(angle), 15), + 0.0, + 0, + 0, + ] matrix[2] = (1 - matrix[0] - matrix[1]) * im.width / 2 matrix[5] = (1 - matrix[3] - matrix[4]) * im.height / 2 @@ -179,93 +270,77 @@ def _test_rotate(self, deg, transpose): else: transposed = im - for resample in [Image.NEAREST, Image.BILINEAR, Image.BICUBIC]: - transformed = im.transform(transposed.size, self.transform, - matrix, resample) - self.assert_image_equal(transposed, transformed) - - def test_rotate_0_deg(self): - self._test_rotate(0, None) - - def test_rotate_90_deg(self): - self._test_rotate(90, Image.ROTATE_90) - - def test_rotate_180_deg(self): - self._test_rotate(180, Image.ROTATE_180) - - def test_rotate_270_deg(self): - self._test_rotate(270, Image.ROTATE_270) - - def _test_resize(self, scale, epsilonscale): + for resample in [ + Image.Resampling.NEAREST, + Image.Resampling.BILINEAR, + Image.Resampling.BICUBIC, + ]: + transformed = im.transform( + transposed.size, self.transform, matrix, resample + ) + assert_image_equal(transposed, transformed) + + @pytest.mark.parametrize( + "scale, epsilon_scale", + ( + (1.1, 6.9), + (1.5, 5.5), + (2.0, 5.5), + (2.3, 3.7), + (2.5, 3.7), + ), + ) + @pytest.mark.parametrize( + "resample,epsilon", + ( + (Image.Resampling.NEAREST, 0), + (Image.Resampling.BILINEAR, 2), + (Image.Resampling.BICUBIC, 1), + ), + ) + def test_resize(self, scale, epsilon_scale, resample, epsilon): im = self._test_image() size_up = int(round(im.width * scale)), int(round(im.height * scale)) - matrix_up = [ - 1 / scale, 0, 0, - 0, 1 / scale, 0, - 0, 0] - matrix_down = [ - scale, 0, 0, - 0, scale, 0, - 0, 0] - - for resample, epsilon in [(Image.NEAREST, 0), - (Image.BILINEAR, 2), (Image.BICUBIC, 1)]: - transformed = im.transform( - size_up, self.transform, matrix_up, resample) - transformed = transformed.transform( - im.size, self.transform, matrix_down, resample) - self.assert_image_similar(transformed, im, epsilon * epsilonscale) - - def test_resize_1_1x(self): - self._test_resize(1.1, 6.9) - - def test_resize_1_5x(self): - self._test_resize(1.5, 5.5) - - def test_resize_2_0x(self): - self._test_resize(2.0, 5.5) - - def test_resize_2_3x(self): - self._test_resize(2.3, 3.7) - - def test_resize_2_5x(self): - self._test_resize(2.5, 3.7) - - def _test_translate(self, x, y, epsilonscale): + matrix_up = [1 / scale, 0, 0, 0, 1 / scale, 0, 0, 0] + matrix_down = [scale, 0, 0, 0, scale, 0, 0, 0] + + transformed = im.transform(size_up, self.transform, matrix_up, resample) + transformed = transformed.transform( + im.size, self.transform, matrix_down, resample + ) + assert_image_similar(transformed, im, epsilon * epsilon_scale) + + @pytest.mark.parametrize( + "x, y, epsilon_scale", + ( + (0.1, 0, 3.7), + (0.6, 0, 9.1), + (50, 50, 0), + ), + ) + @pytest.mark.parametrize( + "resample, epsilon", + ( + (Image.Resampling.NEAREST, 0), + (Image.Resampling.BILINEAR, 1.5), + (Image.Resampling.BICUBIC, 1), + ), + ) + def test_translate(self, x, y, epsilon_scale, resample, epsilon): im = self._test_image() size_up = int(round(im.width + x)), int(round(im.height + y)) - matrix_up = [ - 1, 0, -x, - 0, 1, -y, - 0, 0] - matrix_down = [ - 1, 0, x, - 0, 1, y, - 0, 0] - - for resample, epsilon in [(Image.NEAREST, 0), - (Image.BILINEAR, 1.5), (Image.BICUBIC, 1)]: - transformed = im.transform( - size_up, self.transform, matrix_up, resample) - transformed = transformed.transform( - im.size, self.transform, matrix_down, resample) - self.assert_image_similar(transformed, im, epsilon * epsilonscale) + matrix_up = [1, 0, -x, 0, 1, -y, 0, 0] + matrix_down = [1, 0, x, 0, 1, y, 0, 0] - def test_translate_0_1(self): - self._test_translate(.1, 0, 3.7) - - def test_translate_0_6(self): - self._test_translate(.6, 0, 9.1) - - def test_translate_50(self): - self._test_translate(50, 50, 0) + transformed = im.transform(size_up, self.transform, matrix_up, resample) + transformed = transformed.transform( + im.size, self.transform, matrix_down, resample + ) + assert_image_similar(transformed, im, epsilon * epsilon_scale) class TestImageTransformPerspective(TestImageTransformAffine): # Repeat all tests for AFFINE transformations with PERSPECTIVE - transform = Image.PERSPECTIVE - -if __name__ == '__main__': - unittest.main() + transform = Image.Transform.PERSPECTIVE diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index a6b1191db3b..877f439ca26 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -1,152 +1,146 @@ -import helper -from helper import unittest, PillowTestCase - -from PIL.Image import (FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM, ROTATE_90, ROTATE_180, - ROTATE_270, TRANSPOSE, TRANSVERSE) - - -class TestImageTranspose(PillowTestCase): - - hopper = { - 'L': helper.hopper('L').crop((0, 0, 121, 127)).copy(), - 'RGB': helper.hopper('RGB').crop((0, 0, 121, 127)).copy(), - } - - def test_flip_left_right(self): - def transpose(mode): - im = self.hopper[mode] - out = im.transpose(FLIP_LEFT_RIGHT) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size) - - x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((x-2, 1))) - self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((1, 1))) - self.assertEqual(im.getpixel((1, y-2)), out.getpixel((x-2, y-2))) - self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, y-2))) - - for mode in ("L", "RGB"): - transpose(mode) - - def test_flip_top_bottom(self): - def transpose(mode): - im = self.hopper[mode] - out = im.transpose(FLIP_TOP_BOTTOM) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size) - - x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((1, y-2))) - self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((x-2, y-2))) - self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, 1))) - self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((x-2, 1))) - - for mode in ("L", "RGB"): - transpose(mode) - - def test_rotate_90(self): - def transpose(mode): - im = self.hopper[mode] - out = im.transpose(ROTATE_90) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size[::-1]) - - x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((1, x-2))) - self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((1, 1))) - self.assertEqual(im.getpixel((1, y-2)), out.getpixel((y-2, x-2))) - self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((y-2, 1))) - - for mode in ("L", "RGB"): - transpose(mode) - - def test_rotate_180(self): - def transpose(mode): - im = self.hopper[mode] - out = im.transpose(ROTATE_180) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size) - - x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((x-2, y-2))) - self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((1, y-2))) - self.assertEqual(im.getpixel((1, y-2)), out.getpixel((x-2, 1))) - self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, 1))) - - for mode in ("L", "RGB"): - transpose(mode) - - def test_rotate_270(self): - def transpose(mode): - im = self.hopper[mode] - out = im.transpose(ROTATE_270) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size[::-1]) - - x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((y-2, 1))) - self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((y-2, x-2))) - self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, 1))) - self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, x-2))) - - for mode in ("L", "RGB"): - transpose(mode) - - def test_transpose(self): - def transpose(mode): - im = self.hopper[mode] - out = im.transpose(TRANSPOSE) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size[::-1]) - - x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((1, 1))) - self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((1, x-2))) - self.assertEqual(im.getpixel((1, y-2)), out.getpixel((y-2, 1))) - self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((y-2, x-2))) - - for mode in ("L", "RGB"): - transpose(mode) - - def test_tranverse(self): - def transpose(mode): - im = self.hopper[mode] - out = im.transpose(TRANSVERSE) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size[::-1]) - - x, y = im.size - self.assertEqual(im.getpixel((1, 1)), out.getpixel((y-2, x-2))) - self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((y-2, 1))) - self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, x-2))) - self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, 1))) - - for mode in ("L", "RGB"): - transpose(mode) - - def test_roundtrip(self): - im = self.hopper['L'] - - def transpose(first, second): - return im.transpose(first).transpose(second) - - self.assert_image_equal( - im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT)) - self.assert_image_equal( - im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM)) - self.assert_image_equal(im, transpose(ROTATE_90, ROTATE_270)) - self.assert_image_equal(im, transpose(ROTATE_180, ROTATE_180)) - self.assert_image_equal( - im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM)) - self.assert_image_equal( - im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT)) - self.assert_image_equal( - im.transpose(TRANSVERSE), transpose(ROTATE_90, FLIP_LEFT_RIGHT)) - self.assert_image_equal( - im.transpose(TRANSVERSE), transpose(ROTATE_270, FLIP_TOP_BOTTOM)) - self.assert_image_equal( - im.transpose(TRANSVERSE), transpose(ROTATE_180, TRANSPOSE)) - - -if __name__ == '__main__': - unittest.main() +import pytest + +from PIL.Image import Transpose + +from . import helper +from .helper import assert_image_equal + +HOPPER = { + mode: helper.hopper(mode).crop((0, 0, 121, 127)).copy() + for mode in ["L", "RGB", "I;16", "I;16L", "I;16B"] +} + + +@pytest.mark.parametrize("mode", HOPPER) +def test_flip_left_right(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.FLIP_LEFT_RIGHT) + assert out.mode == mode + assert out.size == im.size + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((x - 2, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, y - 2)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_flip_top_bottom(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.FLIP_TOP_BOTTOM) + assert out.mode == mode + assert out.size == im.size + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((1, y - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((x - 2, 1)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_rotate_90(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.ROTATE_90) + assert out.mode == mode + assert out.size == im.size[::-1] + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((1, x - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, 1)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_rotate_180(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.ROTATE_180) + assert out.mode == mode + assert out.size == im.size + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, y - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_rotate_270(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.ROTATE_270) + assert out.mode == mode + assert out.size == im.size[::-1] + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((y - 2, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, x - 2)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_transpose(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.TRANSPOSE) + assert out.mode == mode + assert out.size == im.size[::-1] + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, x - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, x - 2)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_tranverse(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.TRANSVERSE) + assert out.mode == mode + assert out.size == im.size[::-1] + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, x - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_roundtrip(mode): + im = HOPPER[mode] + + def transpose(first, second): + return im.transpose(first).transpose(second) + + assert_image_equal( + im, transpose(Transpose.FLIP_LEFT_RIGHT, Transpose.FLIP_LEFT_RIGHT) + ) + assert_image_equal( + im, transpose(Transpose.FLIP_TOP_BOTTOM, Transpose.FLIP_TOP_BOTTOM) + ) + assert_image_equal(im, transpose(Transpose.ROTATE_90, Transpose.ROTATE_270)) + assert_image_equal(im, transpose(Transpose.ROTATE_180, Transpose.ROTATE_180)) + assert_image_equal( + im.transpose(Transpose.TRANSPOSE), + transpose(Transpose.ROTATE_90, Transpose.FLIP_TOP_BOTTOM), + ) + assert_image_equal( + im.transpose(Transpose.TRANSPOSE), + transpose(Transpose.ROTATE_270, Transpose.FLIP_LEFT_RIGHT), + ) + assert_image_equal( + im.transpose(Transpose.TRANSVERSE), + transpose(Transpose.ROTATE_90, Transpose.FLIP_LEFT_RIGHT), + ) + assert_image_equal( + im.transpose(Transpose.TRANSVERSE), + transpose(Transpose.ROTATE_270, Transpose.FLIP_TOP_BOTTOM), + ) + assert_image_equal( + im.transpose(Transpose.TRANSVERSE), + transpose(Transpose.ROTATE_180, Transpose.TRANSPOSE), + ) diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 4e30dc1752c..b839a7b140a 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,72 +1,428 @@ -from helper import unittest, PillowTestCase, hopper - -from PIL import Image -from PIL import ImageChops - - -class TestImageChops(PillowTestCase): - - def test_sanity(self): - - im = hopper("L") - - ImageChops.constant(im, 128) - ImageChops.duplicate(im) - ImageChops.invert(im) - ImageChops.lighter(im, im) - ImageChops.darker(im, im) - ImageChops.difference(im, im) - ImageChops.multiply(im, im) - ImageChops.screen(im, im) - - ImageChops.add(im, im) - ImageChops.add(im, im, 2.0) - ImageChops.add(im, im, 2.0, 128) - ImageChops.subtract(im, im) - ImageChops.subtract(im, im, 2.0) - ImageChops.subtract(im, im, 2.0, 128) - - ImageChops.add_modulo(im, im) - ImageChops.subtract_modulo(im, im) - - ImageChops.blend(im, im, 0.5) - ImageChops.composite(im, im, im) - - ImageChops.offset(im, 10) - ImageChops.offset(im, 10, 20) - - def test_logical(self): - - def table(op, a, b): - out = [] - for x in (a, b): - imx = Image.new("1", (1, 1), x) - for y in (a, b): - imy = Image.new("1", (1, 1), y) - out.append(op(imx, imy).getpixel((0, 0))) - return tuple(out) - - self.assertEqual( - table(ImageChops.logical_and, 0, 1), (0, 0, 0, 255)) - self.assertEqual( - table(ImageChops.logical_or, 0, 1), (0, 255, 255, 255)) - self.assertEqual( - table(ImageChops.logical_xor, 0, 1), (0, 255, 255, 0)) - - self.assertEqual( - table(ImageChops.logical_and, 0, 128), (0, 0, 0, 255)) - self.assertEqual( - table(ImageChops.logical_or, 0, 128), (0, 255, 255, 255)) - self.assertEqual( - table(ImageChops.logical_xor, 0, 128), (0, 255, 255, 0)) - - self.assertEqual( - table(ImageChops.logical_and, 0, 255), (0, 0, 0, 255)) - self.assertEqual( - table(ImageChops.logical_or, 0, 255), (0, 255, 255, 255)) - self.assertEqual( - table(ImageChops.logical_xor, 0, 255), (0, 255, 255, 0)) - - -if __name__ == '__main__': - unittest.main() +from PIL import Image, ImageChops + +from .helper import assert_image_equal, hopper + +BLACK = (0, 0, 0) +BROWN = (127, 64, 0) +CYAN = (0, 255, 255) +DARK_GREEN = (0, 128, 0) +GREEN = (0, 255, 0) +ORANGE = (255, 128, 0) +WHITE = (255, 255, 255) + +GREY = 128 + + +def test_sanity(): + im = hopper("L") + + ImageChops.constant(im, 128) + ImageChops.duplicate(im) + ImageChops.invert(im) + ImageChops.lighter(im, im) + ImageChops.darker(im, im) + ImageChops.difference(im, im) + ImageChops.multiply(im, im) + ImageChops.screen(im, im) + + ImageChops.add(im, im) + ImageChops.add(im, im, 2.0) + ImageChops.add(im, im, 2.0, 128) + ImageChops.subtract(im, im) + ImageChops.subtract(im, im, 2.0) + ImageChops.subtract(im, im, 2.0, 128) + + ImageChops.add_modulo(im, im) + ImageChops.subtract_modulo(im, im) + + ImageChops.blend(im, im, 0.5) + ImageChops.composite(im, im, im) + + ImageChops.soft_light(im, im) + ImageChops.hard_light(im, im) + ImageChops.overlay(im, im) + + ImageChops.offset(im, 10) + ImageChops.offset(im, 10, 20) + + +def test_add(): + # Arrange + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: + + # Act + new = ImageChops.add(im1, im2) + + # Assert + assert new.getbbox() == (25, 25, 76, 76) + assert new.getpixel((50, 50)) == ORANGE + + +def test_add_scale_offset(): + # Arrange + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: + + # Act + new = ImageChops.add(im1, im2, scale=2.5, offset=100) + + # Assert + assert new.getbbox() == (0, 0, 100, 100) + assert new.getpixel((50, 50)) == (202, 151, 100) + + +def test_add_clip(): + # Arrange + im = hopper() + + # Act + new = ImageChops.add(im, im) + + # Assert + assert new.getpixel((50, 50)) == (255, 255, 254) + + +def test_add_modulo(): + # Arrange + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: + + # Act + new = ImageChops.add_modulo(im1, im2) + + # Assert + assert new.getbbox() == (25, 25, 76, 76) + assert new.getpixel((50, 50)) == ORANGE + + +def test_add_modulo_no_clip(): + # Arrange + im = hopper() + + # Act + new = ImageChops.add_modulo(im, im) + + # Assert + assert new.getpixel((50, 50)) == (224, 76, 254) + + +def test_blend(): + # Arrange + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: + + # Act + new = ImageChops.blend(im1, im2, 0.5) + + # Assert + assert new.getbbox() == (25, 25, 76, 76) + assert new.getpixel((50, 50)) == BROWN + + +def test_constant(): + # Arrange + im = Image.new("RGB", (20, 10)) + + # Act + new = ImageChops.constant(im, GREY) + + # Assert + assert new.size == im.size + assert new.getpixel((0, 0)) == GREY + assert new.getpixel((19, 9)) == GREY + + +def test_darker_image(): + # Arrange + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: + + # Act + new = ImageChops.darker(im1, im2) + + # Assert + assert_image_equal(new, im2) + + +def test_darker_pixel(): + # Arrange + im1 = hopper() + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: + + # Act + new = ImageChops.darker(im1, im2) + + # Assert + assert new.getpixel((50, 50)) == (240, 166, 0) + + +def test_difference(): + # Arrange + with Image.open("Tests/images/imagedraw_arc_end_le_start.png") as im1: + with Image.open("Tests/images/imagedraw_arc_no_loops.png") as im2: + + # Act + new = ImageChops.difference(im1, im2) + + # Assert + assert new.getbbox() == (25, 25, 76, 76) + + +def test_difference_pixel(): + # Arrange + im1 = hopper() + with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2: + + # Act + new = ImageChops.difference(im1, im2) + + # Assert + assert new.getpixel((50, 50)) == (240, 166, 128) + + +def test_duplicate(): + # Arrange + im = hopper() + + # Act + new = ImageChops.duplicate(im) + + # Assert + assert_image_equal(new, im) + + +def test_invert(): + # Arrange + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: + + # Act + new = ImageChops.invert(im) + + # Assert + assert new.getbbox() == (0, 0, 100, 100) + assert new.getpixel((0, 0)) == WHITE + assert new.getpixel((50, 50)) == CYAN + + +def test_lighter_image(): + # Arrange + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: + + # Act + new = ImageChops.lighter(im1, im2) + + # Assert + assert_image_equal(new, im1) + + +def test_lighter_pixel(): + # Arrange + im1 = hopper() + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: + + # Act + new = ImageChops.lighter(im1, im2) + + # Assert + assert new.getpixel((50, 50)) == (255, 255, 127) + + +def test_multiply_black(): + """If you multiply an image with a solid black image, + the result is black.""" + # Arrange + im1 = hopper() + black = Image.new("RGB", im1.size, "black") + + # Act + new = ImageChops.multiply(im1, black) + + # Assert + assert_image_equal(new, black) + + +def test_multiply_green(): + # Arrange + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: + green = Image.new("RGB", im.size, "green") + + # Act + new = ImageChops.multiply(im, green) + + # Assert + assert new.getbbox() == (25, 25, 76, 76) + assert new.getpixel((25, 25)) == DARK_GREEN + assert new.getpixel((50, 50)) == BLACK + + +def test_multiply_white(): + """If you multiply with a solid white image, the image is unaffected.""" + # Arrange + im1 = hopper() + white = Image.new("RGB", im1.size, "white") + + # Act + new = ImageChops.multiply(im1, white) + + # Assert + assert_image_equal(new, im1) + + +def test_offset(): + # Arrange + xoffset = 45 + yoffset = 20 + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im: + + # Act + new = ImageChops.offset(im, xoffset, yoffset) + + # Assert + assert new.getbbox() == (0, 45, 100, 96) + assert new.getpixel((50, 50)) == BLACK + assert new.getpixel((50 + xoffset, 50 + yoffset)) == DARK_GREEN + + # Test no yoffset + assert ImageChops.offset(im, xoffset) == ImageChops.offset(im, xoffset, xoffset) + + +def test_screen(): + # Arrange + with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: + + # Act + new = ImageChops.screen(im1, im2) + + # Assert + assert new.getbbox() == (25, 25, 76, 76) + assert new.getpixel((50, 50)) == ORANGE + + +def test_subtract(): + # Arrange + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: + + # Act + new = ImageChops.subtract(im1, im2) + + # Assert + assert new.getbbox() == (25, 50, 76, 76) + assert new.getpixel((50, 51)) == GREEN + assert new.getpixel((50, 52)) == BLACK + + +def test_subtract_scale_offset(): + # Arrange + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: + + # Act + new = ImageChops.subtract(im1, im2, scale=2.5, offset=100) + + # Assert + assert new.getbbox() == (0, 0, 100, 100) + assert new.getpixel((50, 50)) == (100, 202, 100) + + +def test_subtract_clip(): + # Arrange + im1 = hopper() + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: + + # Act + new = ImageChops.subtract(im1, im2) + + # Assert + assert new.getpixel((50, 50)) == (0, 0, 127) + + +def test_subtract_modulo(): + # Arrange + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: + with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: + + # Act + new = ImageChops.subtract_modulo(im1, im2) + + # Assert + assert new.getbbox() == (25, 50, 76, 76) + assert new.getpixel((50, 51)) == GREEN + assert new.getpixel((50, 52)) == BLACK + + +def test_subtract_modulo_no_clip(): + # Arrange + im1 = hopper() + with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: + + # Act + new = ImageChops.subtract_modulo(im1, im2) + + # Assert + assert new.getpixel((50, 50)) == (241, 167, 127) + + +def test_soft_light(): + # Arrange + with Image.open("Tests/images/hopper.png") as im1: + with Image.open("Tests/images/hopper-XYZ.png") as im2: + + # Act + new = ImageChops.soft_light(im1, im2) + + # Assert + assert new.getpixel((64, 64)) == (163, 54, 32) + assert new.getpixel((15, 100)) == (1, 1, 3) + + +def test_hard_light(): + # Arrange + with Image.open("Tests/images/hopper.png") as im1: + with Image.open("Tests/images/hopper-XYZ.png") as im2: + + # Act + new = ImageChops.hard_light(im1, im2) + + # Assert + assert new.getpixel((64, 64)) == (144, 50, 27) + assert new.getpixel((15, 100)) == (1, 1, 2) + + +def test_overlay(): + # Arrange + with Image.open("Tests/images/hopper.png") as im1: + with Image.open("Tests/images/hopper-XYZ.png") as im2: + + # Act + new = ImageChops.overlay(im1, im2) + + # Assert + assert new.getpixel((64, 64)) == (159, 50, 27) + assert new.getpixel((15, 100)) == (1, 1, 2) + + +def test_logical(): + def table(op, a, b): + out = [] + for x in (a, b): + imx = Image.new("1", (1, 1), x) + for y in (a, b): + imy = Image.new("1", (1, 1), y) + out.append(op(imx, imy).getpixel((0, 0))) + return tuple(out) + + assert table(ImageChops.logical_and, 0, 1) == (0, 0, 0, 255) + assert table(ImageChops.logical_or, 0, 1) == (0, 255, 255, 255) + assert table(ImageChops.logical_xor, 0, 1) == (0, 255, 255, 0) + + assert table(ImageChops.logical_and, 0, 128) == (0, 0, 0, 255) + assert table(ImageChops.logical_or, 0, 128) == (0, 255, 255, 255) + assert table(ImageChops.logical_xor, 0, 128) == (0, 255, 255, 0) + + assert table(ImageChops.logical_and, 0, 255) == (0, 0, 0, 255) + assert table(ImageChops.logical_or, 0, 255) == (0, 255, 255, 255) + assert table(ImageChops.logical_xor, 0, 255) == (0, 255, 255, 0) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 9e304ae01ef..3d8dbe6bbf5 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -1,17 +1,28 @@ -from helper import unittest, PillowTestCase, hopper import datetime +import os +import re +import shutil +from io import BytesIO -from PIL import Image, ImageMode +import pytest -from io import BytesIO -import os +from PIL import Image, ImageMode, features + +from .helper import ( + assert_image, + assert_image_equal, + assert_image_similar, + assert_image_similar_tofile, + hopper, +) try: from PIL import ImageCms from PIL.ImageCms import ImageCmsProfile + ImageCms.core.profile_open -except ImportError as v: - # Skipped via setUp() +except ImportError: + # Skipped via setup_module() pass @@ -19,423 +30,598 @@ HAVE_PROFILE = os.path.exists(SRGB) -class TestImageCms(PillowTestCase): +def setup_module(): + try: + from PIL import ImageCms - def setUp(self): - try: - from PIL import ImageCms - # need to hit getattr to trigger the delayed import error - ImageCms.core.profile_open - except ImportError as v: - self.skipTest(v) + # need to hit getattr to trigger the delayed import error + ImageCms.core.profile_open + except ImportError as v: + pytest.skip(str(v)) - def skip_missing(self): - if not HAVE_PROFILE: - self.skipTest("SRGB profile not available") - def test_sanity(self): +def skip_missing(): + if not HAVE_PROFILE: + pytest.skip("SRGB profile not available") - # basic smoke test. - # this mostly follows the cms_test outline. - v = ImageCms.versions() # should return four strings - self.assertEqual(v[0], '1.0.0 pil') - self.assertEqual(list(map(type, v)), [str, str, str, str]) +def test_sanity(): + # basic smoke test. + # this mostly follows the cms_test outline. - # internal version number - self.assertRegexpMatches(ImageCms.core.littlecms_version, r"\d+\.\d+$") + v = ImageCms.versions() # should return four strings + assert v[0] == "1.0.0 pil" + assert list(map(type, v)) == [str, str, str, str] - self.skip_missing() - i = ImageCms.profileToProfile(hopper(), SRGB, SRGB) - self.assert_image(i, "RGB", (128, 128)) + # internal version number + assert re.search(r"\d+\.\d+(\.\d+)?$", features.version_module("littlecms2")) - i = hopper() - ImageCms.profileToProfile(i, SRGB, SRGB, inPlace=True) - self.assert_image(i, "RGB", (128, 128)) + skip_missing() + i = ImageCms.profileToProfile(hopper(), SRGB, SRGB) + assert_image(i, "RGB", (128, 128)) - t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") - i = ImageCms.applyTransform(hopper(), t) - self.assert_image(i, "RGB", (128, 128)) + i = hopper() + ImageCms.profileToProfile(i, SRGB, SRGB, inPlace=True) + assert_image(i, "RGB", (128, 128)) - i = hopper() + t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") + i = ImageCms.applyTransform(hopper(), t) + assert_image(i, "RGB", (128, 128)) + + with hopper() as i: t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") ImageCms.applyTransform(hopper(), t, inPlace=True) - self.assert_image(i, "RGB", (128, 128)) - - p = ImageCms.createProfile("sRGB") - o = ImageCms.getOpenProfile(SRGB) - t = ImageCms.buildTransformFromOpenProfiles(p, o, "RGB", "RGB") - i = ImageCms.applyTransform(hopper(), t) - self.assert_image(i, "RGB", (128, 128)) - - t = ImageCms.buildProofTransform(SRGB, SRGB, SRGB, "RGB", "RGB") - self.assertEqual(t.inputMode, "RGB") - self.assertEqual(t.outputMode, "RGB") - i = ImageCms.applyTransform(hopper(), t) - self.assert_image(i, "RGB", (128, 128)) - - # test PointTransform convenience API - hopper().point(t) - - def test_name(self): - self.skip_missing() - # get profile information for file - self.assertEqual( - ImageCms.getProfileName(SRGB).strip(), - 'IEC 61966-2-1 Default RGB Colour Space - sRGB') - - def test_info(self): - self.skip_missing() - self.assertEqual( - ImageCms.getProfileInfo(SRGB).splitlines(), [ - 'sRGB IEC61966-2-1 black scaled', '', - 'Copyright International Color Consortium, 2009', '']) - - def test_copyright(self): - self.skip_missing() - self.assertEqual( - ImageCms.getProfileCopyright(SRGB).strip(), - 'Copyright International Color Consortium, 2009') - - def test_manufacturer(self): - self.skip_missing() - self.assertEqual( - ImageCms.getProfileManufacturer(SRGB).strip(), - '') - - def test_model(self): - self.skip_missing() - self.assertEqual( - ImageCms.getProfileModel(SRGB).strip(), - 'IEC 61966-2-1 Default RGB Colour Space - sRGB') - - def test_description(self): - self.skip_missing() - self.assertEqual( - ImageCms.getProfileDescription(SRGB).strip(), - 'sRGB IEC61966-2-1 black scaled') - - def test_intent(self): - self.skip_missing() - self.assertEqual(ImageCms.getDefaultIntent(SRGB), 0) - self.assertEqual(ImageCms.isIntentSupported( - SRGB, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, - ImageCms.DIRECTION_INPUT), 1) - - def test_profile_object(self): - # same, using profile object - p = ImageCms.createProfile("sRGB") - # self.assertEqual(ImageCms.getProfileName(p).strip(), - # 'sRGB built-in - (lcms internal)') - # self.assertEqual(ImageCms.getProfileInfo(p).splitlines(), - # ['sRGB built-in', '', 'WhitePoint : D65 (daylight)', '', '']) - self.assertEqual(ImageCms.getDefaultIntent(p), 0) - self.assertEqual(ImageCms.isIntentSupported( - p, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, - ImageCms.DIRECTION_INPUT), 1) - - def test_extensions(self): - # extensions - - i = Image.open("Tests/images/rgb.jpg") + assert_image(i, "RGB", (128, 128)) + + p = ImageCms.createProfile("sRGB") + o = ImageCms.getOpenProfile(SRGB) + t = ImageCms.buildTransformFromOpenProfiles(p, o, "RGB", "RGB") + i = ImageCms.applyTransform(hopper(), t) + assert_image(i, "RGB", (128, 128)) + + t = ImageCms.buildProofTransform(SRGB, SRGB, SRGB, "RGB", "RGB") + assert t.inputMode == "RGB" + assert t.outputMode == "RGB" + i = ImageCms.applyTransform(hopper(), t) + assert_image(i, "RGB", (128, 128)) + + # test PointTransform convenience API + hopper().point(t) + + +def test_name(): + skip_missing() + # get profile information for file + assert ( + ImageCms.getProfileName(SRGB).strip() + == "IEC 61966-2-1 Default RGB Colour Space - sRGB" + ) + + +def test_info(): + skip_missing() + assert ImageCms.getProfileInfo(SRGB).splitlines() == [ + "sRGB IEC61966-2-1 black scaled", + "", + "Copyright International Color Consortium, 2009", + "", + ] + + +def test_copyright(): + skip_missing() + assert ( + ImageCms.getProfileCopyright(SRGB).strip() + == "Copyright International Color Consortium, 2009" + ) + + +def test_manufacturer(): + skip_missing() + assert ImageCms.getProfileManufacturer(SRGB).strip() == "" + + +def test_model(): + skip_missing() + assert ( + ImageCms.getProfileModel(SRGB).strip() + == "IEC 61966-2-1 Default RGB Colour Space - sRGB" + ) + + +def test_description(): + skip_missing() + assert ( + ImageCms.getProfileDescription(SRGB).strip() == "sRGB IEC61966-2-1 black scaled" + ) + + +def test_intent(): + skip_missing() + assert ImageCms.getDefaultIntent(SRGB) == 0 + support = ImageCms.isIntentSupported( + SRGB, ImageCms.Intent.ABSOLUTE_COLORIMETRIC, ImageCms.Direction.INPUT + ) + assert support == 1 + + +def test_profile_object(): + # same, using profile object + p = ImageCms.createProfile("sRGB") + # assert ImageCms.getProfileName(p).strip() == "sRGB built-in - (lcms internal)" + # assert ImageCms.getProfileInfo(p).splitlines() == + # ["sRGB built-in", "", "WhitePoint : D65 (daylight)", "", ""] + assert ImageCms.getDefaultIntent(p) == 0 + support = ImageCms.isIntentSupported( + p, ImageCms.Intent.ABSOLUTE_COLORIMETRIC, ImageCms.Direction.INPUT + ) + assert support == 1 + + +def test_extensions(): + # extensions + + with Image.open("Tests/images/rgb.jpg") as i: p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) - self.assertEqual( - ImageCms.getProfileName(p).strip(), - 'IEC 61966-2.1 Default RGB colour space - sRGB') - - def test_exceptions(self): - # Test mode mismatch - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") - self.assertRaises(ValueError, t.apply_in_place, hopper("RGBA")) - - # the procedural pyCMS API uses PyCMSError for all sorts of errors - self.assertRaises( - ImageCms.PyCMSError, - ImageCms.profileToProfile, hopper(), "foo", "bar") - self.assertRaises( - ImageCms.PyCMSError, - ImageCms.buildTransform, "foo", "bar", "RGB", "RGB") - self.assertRaises( - ImageCms.PyCMSError, - ImageCms.getProfileName, None) - self.skip_missing() - self.assertRaises( - ImageCms.PyCMSError, - ImageCms.isIntentSupported, SRGB, None, None) - - def test_display_profile(self): - # try fetching the profile for the current display device - ImageCms.get_display_profile() - - def test_lab_color_profile(self): - ImageCms.createProfile("LAB", 5000) - ImageCms.createProfile("LAB", 6500) - - def test_unsupported_color_space(self): - self.assertRaises(ImageCms.PyCMSError, - ImageCms.createProfile, "unsupported") - - def test_invalid_color_temperature(self): - self.assertRaises(ImageCms.PyCMSError, - ImageCms.createProfile, "LAB", "invalid") - - def test_simple_lab(self): - i = Image.new('RGB', (10, 10), (128, 128, 128)) - - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") - - i_lab = ImageCms.applyTransform(i, t) - - self.assertEqual(i_lab.mode, 'LAB') - - k = i_lab.getpixel((0, 0)) - # not a linear luminance map. so L != 128: - self.assertEqual(k, (137, 128, 128)) - - l = i_lab.getdata(0) - a = i_lab.getdata(1) - b = i_lab.getdata(2) - - self.assertEqual(list(l), [137] * 100) - self.assertEqual(list(a), [128] * 100) - self.assertEqual(list(b), [128] * 100) - - def test_lab_color(self): - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") - - # Need to add a type mapping for some PIL type to TYPE_Lab_8 in - # findLCMSType, and have that mapping work back to a PIL mode - # (likely RGB). - i = ImageCms.applyTransform(hopper(), t) - self.assert_image(i, "LAB", (128, 128)) - - # i.save('temp.lab.tif') # visually verified vs PS. - - target = Image.open('Tests/images/hopper.Lab.tif') - - self.assert_image_similar(i, target, 3.5) - - def test_lab_srgb(self): - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") - - img = Image.open('Tests/images/hopper.Lab.tif') + assert ( + ImageCms.getProfileName(p).strip() + == "IEC 61966-2.1 Default RGB colour space - sRGB" + ) + + +def test_exceptions(): + # Test mode mismatch + psRGB = ImageCms.createProfile("sRGB") + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") + with pytest.raises(ValueError, match="mode mismatch"): + t.apply_in_place(hopper("RGBA")) + + # the procedural pyCMS API uses PyCMSError for all sorts of errors + with hopper() as im: + with pytest.raises(ImageCms.PyCMSError, match="cannot open profile file"): + ImageCms.profileToProfile(im, "foo", "bar") + + with pytest.raises(ImageCms.PyCMSError, match="cannot open profile file"): + ImageCms.buildTransform("foo", "bar", "RGB", "RGB") + + with pytest.raises(ImageCms.PyCMSError, match="Invalid type for Profile"): + ImageCms.getProfileName(None) + skip_missing() + + # Python <= 3.9: "an integer is required (got type NoneType)" + # Python > 3.9: "'NoneType' object cannot be interpreted as an integer" + with pytest.raises(ImageCms.PyCMSError, match="integer"): + ImageCms.isIntentSupported(SRGB, None, None) + +def test_display_profile(): + # try fetching the profile for the current display device + ImageCms.get_display_profile() + + +def test_lab_color_profile(): + ImageCms.createProfile("LAB", 5000) + ImageCms.createProfile("LAB", 6500) + + +def test_unsupported_color_space(): + with pytest.raises( + ImageCms.PyCMSError, + match=re.escape( + "Color space not supported for on-the-fly profile creation (unsupported)" + ), + ): + ImageCms.createProfile("unsupported") + + +def test_invalid_color_temperature(): + with pytest.raises( + ImageCms.PyCMSError, + match='Color temperature must be numeric, "invalid" not valid', + ): + ImageCms.createProfile("LAB", "invalid") + + +@pytest.mark.parametrize("flag", ("my string", -1)) +def test_invalid_flag(flag): + with hopper() as im: + with pytest.raises( + ImageCms.PyCMSError, match="flags must be an integer between 0 and " + ): + ImageCms.profileToProfile(im, "foo", "bar", flags=flag) + + +def test_simple_lab(): + i = Image.new("RGB", (10, 10), (128, 128, 128)) + + psRGB = ImageCms.createProfile("sRGB") + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") + + i_lab = ImageCms.applyTransform(i, t) + + assert i_lab.mode == "LAB" + + k = i_lab.getpixel((0, 0)) + # not a linear luminance map. so L != 128: + assert k == (137, 128, 128) + + l_data = i_lab.getdata(0) + a_data = i_lab.getdata(1) + b_data = i_lab.getdata(2) + + assert list(l_data) == [137] * 100 + assert list(a_data) == [128] * 100 + assert list(b_data) == [128] * 100 + + +def test_lab_color(): + psRGB = ImageCms.createProfile("sRGB") + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") + + # Need to add a type mapping for some PIL type to TYPE_Lab_8 in findLCMSType, and + # have that mapping work back to a PIL mode (likely RGB). + i = ImageCms.applyTransform(hopper(), t) + assert_image(i, "LAB", (128, 128)) + + # i.save('temp.lab.tif') # visually verified vs PS. + + assert_image_similar_tofile(i, "Tests/images/hopper.Lab.tif", 3.5) + + +def test_lab_srgb(): + psRGB = ImageCms.createProfile("sRGB") + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") + + with Image.open("Tests/images/hopper.Lab.tif") as img: img_srgb = ImageCms.applyTransform(img, t) - # img_srgb.save('temp.srgb.tif') # visually verified vs ps. + # img_srgb.save('temp.srgb.tif') # visually verified vs ps. + + assert_image_similar(hopper(), img_srgb, 30) + assert img_srgb.info["icc_profile"] + + profile = ImageCmsProfile(BytesIO(img_srgb.info["icc_profile"])) + assert "sRGB" in ImageCms.getProfileDescription(profile) - self.assert_image_similar(hopper(), img_srgb, 30) - self.assertTrue(img_srgb.info['icc_profile']) - profile = ImageCmsProfile(BytesIO(img_srgb.info['icc_profile'])) - self.assertIn('sRGB', ImageCms.getProfileDescription(profile)) +def test_lab_roundtrip(): + # check to see if we're at least internally consistent. + psRGB = ImageCms.createProfile("sRGB") + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") - def test_lab_roundtrip(self): - # check to see if we're at least internally consistent. - psRGB = ImageCms.createProfile("sRGB") - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") + t2 = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") - t2 = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") + i = ImageCms.applyTransform(hopper(), t) - i = ImageCms.applyTransform(hopper(), t) + assert i.info["icc_profile"] == ImageCmsProfile(pLab).tobytes() - self.assertEqual(i.info['icc_profile'], - ImageCmsProfile(pLab).tobytes()) + out = ImageCms.applyTransform(i, t2) - out = ImageCms.applyTransform(i, t2) + assert_image_similar(hopper(), out, 2) - self.assert_image_similar(hopper(), out, 2) - def test_profile_tobytes(self): - i = Image.open("Tests/images/rgb.jpg") +def test_profile_tobytes(): + with Image.open("Tests/images/rgb.jpg") as i: p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) - p2 = ImageCms.getOpenProfile(BytesIO(p.tobytes())) - - # not the same bytes as the original icc_profile, - # but it does roundtrip - self.assertEqual(p.tobytes(), p2.tobytes()) - self.assertEqual(ImageCms.getProfileName(p), - ImageCms.getProfileName(p2)) - self.assertEqual(ImageCms.getProfileDescription(p), - ImageCms.getProfileDescription(p2)) - - def test_extended_information(self): - self.skip_missing() - o = ImageCms.getOpenProfile(SRGB) - p = o.profile - - def assert_truncated_tuple_equal(tup1, tup2, digits=10): - # Helper function to reduce precision of tuples of floats - # recursively and then check equality. - power = 10 ** digits - - def truncate_tuple(tuple_or_float): - return tuple( - truncate_tuple(val) if isinstance(val, tuple) - else int(val * power) / power for val in tuple_or_float) - self.assertEqual(truncate_tuple(tup1), truncate_tuple(tup2)) - - self.assertEqual(p.attributes, 4294967296) - assert_truncated_tuple_equal(p.blue_colorant, ((0.14306640625, 0.06060791015625, 0.7140960693359375), (0.1558847490315394, 0.06603820639433387, 0.06060791015625))) - assert_truncated_tuple_equal(p.blue_primary, ((0.14306641366715667, 0.06060790921083026, 0.7140960805782015), (0.15588475410450106, 0.06603820408959558, 0.06060790921083026))) - assert_truncated_tuple_equal(p.chromatic_adaptation, (((1.04791259765625, 0.0229339599609375, -0.050201416015625), (0.02960205078125, 0.9904632568359375, -0.0170745849609375), (-0.009246826171875, 0.0150604248046875, 0.7517852783203125)), ((1.0267159024652783, 0.022470062342089134, 0.0229339599609375), (0.02951378324103937, 0.9875098886387147, 0.9904632568359375), (-0.012205438066465256, 0.01987915407854985, 0.0150604248046875)))) - self.assertIsNone(p.chromaticity) - self.assertEqual(p.clut, {0: (False, False, True), 1: (False, False, True), 2: (False, False, True), 3: (False, False, True)}) - self.assertEqual(p.color_space, 'RGB') - self.assertIsNone(p.colorant_table) - self.assertIsNone(p.colorant_table_out) - self.assertIsNone(p.colorimetric_intent) - self.assertEqual(p.connection_space, 'XYZ ') - self.assertEqual(p.copyright, 'Copyright International Color Consortium, 2009') - self.assertEqual(p.creation_date, datetime.datetime(2009, 2, 27, 21, 36, 31)) - self.assertEqual(p.device_class, 'mntr') - assert_truncated_tuple_equal(p.green_colorant, ((0.3851470947265625, 0.7168731689453125, 0.097076416015625), (0.32119769927720654, 0.5978443449048152, 0.7168731689453125))) - assert_truncated_tuple_equal(p.green_primary, ((0.3851470888162112, 0.7168731974161346, 0.09707641738998518), (0.32119768793686687, 0.5978443567149709, 0.7168731974161346))) - self.assertEqual(p.header_flags, 0) - self.assertEqual(p.header_manufacturer, '\x00\x00\x00\x00') - self.assertEqual(p.header_model, '\x00\x00\x00\x00') - self.assertEqual(p.icc_measurement_condition, {'backing': (0.0, 0.0, 0.0), 'flare': 0.0, 'geo': 'unknown', 'observer': 1, 'illuminant_type': 'D65'}) - self.assertEqual(p.icc_version, 33554432) - self.assertIsNone(p.icc_viewing_condition) - self.assertEqual(p.intent_supported, {0: (True, True, True), 1: (True, True, True), 2: (True, True, True), 3: (True, True, True)}) - self.assertTrue(p.is_matrix_shaper) - self.assertEqual(p.luminance, ((0.0, 80.0, 0.0), (0.0, 1.0, 80.0))) - self.assertIsNone(p.manufacturer) - assert_truncated_tuple_equal(p.media_black_point, ((0.012054443359375, 0.0124969482421875, 0.01031494140625), (0.34573304157549234, 0.35842450765864337, 0.0124969482421875))) - assert_truncated_tuple_equal(p.media_white_point, ((0.964202880859375, 1.0, 0.8249053955078125), (0.3457029219802284, 0.3585375327567059, 1.0))) - assert_truncated_tuple_equal((p.media_white_point_temperature,), (5000.722328847392,)) - self.assertEqual(p.model, 'IEC 61966-2-1 Default RGB Colour Space - sRGB') - self.assertEqual(p.pcs, 'XYZ') - self.assertIsNone(p.perceptual_rendering_intent_gamut) - self.assertEqual(p.product_copyright, 'Copyright International Color Consortium, 2009') - self.assertEqual(p.product_desc, 'sRGB IEC61966-2-1 black scaled') - self.assertEqual(p.product_description, 'sRGB IEC61966-2-1 black scaled') - self.assertEqual(p.product_manufacturer, '') - self.assertEqual(p.product_model, 'IEC 61966-2-1 Default RGB Colour Space - sRGB') - self.assertEqual(p.profile_description, 'sRGB IEC61966-2-1 black scaled') - self.assertEqual(p.profile_id, b')\xf8=\xde\xaf\xf2U\xaexB\xfa\xe4\xca\x839\r') - assert_truncated_tuple_equal(p.red_colorant, ((0.436065673828125, 0.2224884033203125, 0.013916015625), (0.6484536316398539, 0.3308524880306778, 0.2224884033203125))) - assert_truncated_tuple_equal(p.red_primary, ((0.43606566581047446, 0.22248840582960838, 0.013916015621759925), (0.6484536250319214, 0.3308524944738204, 0.22248840582960838))) - self.assertEqual(p.rendering_intent, 0) - self.assertIsNone(p.saturation_rendering_intent_gamut) - self.assertIsNone(p.screening_description) - self.assertIsNone(p.target) - self.assertEqual(p.technology, 'CRT ') - self.assertEqual(p.version, 2.0) - self.assertEqual(p.viewing_condition, 'Reference Viewing Condition in IEC 61966-2-1') - self.assertEqual(p.xcolor_space, 'RGB ') - - def test_profile_typesafety(self): - """ Profile init type safety - - prepatch, these would segfault, postpatch they should emit a typeerror - """ - - with self.assertRaises(TypeError): - ImageCms.ImageCmsProfile(0).tobytes() - with self.assertRaises(TypeError): - ImageCms.ImageCmsProfile(1).tobytes() - - def assert_aux_channel_preserved(self, mode, transform_in_place, preserved_channel): - def create_test_image(): - # set up test image with something interesting in the tested aux - # channel. - nine_grid_deltas = [ - (-1, -1), (-1, 0), (-1, 1), - ( 0, -1), ( 0, 0), ( 0, 1), - ( 1, -1), ( 1, 0), ( 1, 1), - ] - chans = [] - bands = ImageMode.getmode(mode).bands - for band_ndx in range(len(bands)): - channel_type = 'L' # 8-bit unorm - channel_pattern = hopper(channel_type) - - # paste pattern with varying offsets to avoid correlation - # potentially hiding some bugs (like channels getting mixed). - paste_offset = ( - int(band_ndx / float(len(bands)) * channel_pattern.size[0]), - int(band_ndx / float(len(bands) * 2) * channel_pattern.size[1]) - ) - channel_data = Image.new(channel_type, channel_pattern.size) - for delta in nine_grid_deltas: - channel_data.paste(channel_pattern, tuple(paste_offset[c] + delta[c]*channel_pattern.size[c] for c in range(2))) - chans.append(channel_data) - return Image.merge(mode, chans) - - source_image = create_test_image() - source_image_aux = source_image.getchannel(preserved_channel) - - # create some transform, it doesn't matter which one - source_profile = ImageCms.createProfile("sRGB") - destination_profile = ImageCms.createProfile("sRGB") - t = ImageCms.buildTransform(source_profile, destination_profile, inMode=mode, outMode=mode) - - # apply transform - if transform_in_place: - ImageCms.applyTransform(source_image, t, inPlace=True) - result_image = source_image - else: - result_image = ImageCms.applyTransform(source_image, t, inPlace=False) - result_image_aux = result_image.getchannel(preserved_channel) - - self.assert_image_equal(source_image_aux, result_image_aux) - - def test_preserve_auxiliary_channels_rgba(self): - self.assert_aux_channel_preserved(mode='RGBA', transform_in_place=False, preserved_channel='A') - - def test_preserve_auxiliary_channels_rgba_in_place(self): - self.assert_aux_channel_preserved(mode='RGBA', transform_in_place=True, preserved_channel='A') - - def test_preserve_auxiliary_channels_rgbx(self): - self.assert_aux_channel_preserved(mode='RGBX', transform_in_place=False, preserved_channel='X') - - def test_preserve_auxiliary_channels_rgbx_in_place(self): - self.assert_aux_channel_preserved(mode='RGBX', transform_in_place=True, preserved_channel='X') - - def test_auxiliary_channels_isolated(self): - # test data in aux channels does not affect non-aux channels - aux_channel_formats = [ - # format, profile, color-only format, source test image - ('RGBA', 'sRGB', 'RGB', hopper('RGBA')), - ('RGBX', 'sRGB', 'RGB', hopper('RGBX')), - ('LAB', 'LAB', 'LAB', Image.open('Tests/images/hopper.Lab.tif')), + p2 = ImageCms.getOpenProfile(BytesIO(p.tobytes())) + + # not the same bytes as the original icc_profile, but it does roundtrip + assert p.tobytes() == p2.tobytes() + assert ImageCms.getProfileName(p) == ImageCms.getProfileName(p2) + assert ImageCms.getProfileDescription(p) == ImageCms.getProfileDescription(p2) + + +def test_extended_information(): + skip_missing() + o = ImageCms.getOpenProfile(SRGB) + p = o.profile + + def assert_truncated_tuple_equal(tup1, tup2, digits=10): + # Helper function to reduce precision of tuples of floats + # recursively and then check equality. + power = 10**digits + + def truncate_tuple(tuple_or_float): + return tuple( + truncate_tuple(val) + if isinstance(val, tuple) + else int(val * power) / power + for val in tuple_or_float + ) + + assert truncate_tuple(tup1) == truncate_tuple(tup2) + + assert p.attributes == 4294967296 + assert_truncated_tuple_equal( + p.blue_colorant, + ( + (0.14306640625, 0.06060791015625, 0.7140960693359375), + (0.1558847490315394, 0.06603820639433387, 0.06060791015625), + ), + ) + assert_truncated_tuple_equal( + p.blue_primary, + ( + (0.14306641366715667, 0.06060790921083026, 0.7140960805782015), + (0.15588475410450106, 0.06603820408959558, 0.06060790921083026), + ), + ) + assert_truncated_tuple_equal( + p.chromatic_adaptation, + ( + ( + (1.04791259765625, 0.0229339599609375, -0.050201416015625), + (0.02960205078125, 0.9904632568359375, -0.0170745849609375), + (-0.009246826171875, 0.0150604248046875, 0.7517852783203125), + ), + ( + (1.0267159024652783, 0.022470062342089134, 0.0229339599609375), + (0.02951378324103937, 0.9875098886387147, 0.9904632568359375), + (-0.012205438066465256, 0.01987915407854985, 0.0150604248046875), + ), + ), + ) + assert p.chromaticity is None + assert p.clut == { + 0: (False, False, True), + 1: (False, False, True), + 2: (False, False, True), + 3: (False, False, True), + } + + assert p.colorant_table is None + assert p.colorant_table_out is None + assert p.colorimetric_intent is None + assert p.connection_space == "XYZ " + assert p.copyright == "Copyright International Color Consortium, 2009" + assert p.creation_date == datetime.datetime(2009, 2, 27, 21, 36, 31) + assert p.device_class == "mntr" + assert_truncated_tuple_equal( + p.green_colorant, + ( + (0.3851470947265625, 0.7168731689453125, 0.097076416015625), + (0.32119769927720654, 0.5978443449048152, 0.7168731689453125), + ), + ) + assert_truncated_tuple_equal( + p.green_primary, + ( + (0.3851470888162112, 0.7168731974161346, 0.09707641738998518), + (0.32119768793686687, 0.5978443567149709, 0.7168731974161346), + ), + ) + assert p.header_flags == 0 + assert p.header_manufacturer == "\x00\x00\x00\x00" + assert p.header_model == "\x00\x00\x00\x00" + assert p.icc_measurement_condition == { + "backing": (0.0, 0.0, 0.0), + "flare": 0.0, + "geo": "unknown", + "observer": 1, + "illuminant_type": "D65", + } + assert p.icc_version == 33554432 + assert p.icc_viewing_condition is None + assert p.intent_supported == { + 0: (True, True, True), + 1: (True, True, True), + 2: (True, True, True), + 3: (True, True, True), + } + assert p.is_matrix_shaper + assert p.luminance == ((0.0, 80.0, 0.0), (0.0, 1.0, 80.0)) + assert p.manufacturer is None + assert_truncated_tuple_equal( + p.media_black_point, + ( + (0.012054443359375, 0.0124969482421875, 0.01031494140625), + (0.34573304157549234, 0.35842450765864337, 0.0124969482421875), + ), + ) + assert_truncated_tuple_equal( + p.media_white_point, + ( + (0.964202880859375, 1.0, 0.8249053955078125), + (0.3457029219802284, 0.3585375327567059, 1.0), + ), + ) + assert_truncated_tuple_equal( + (p.media_white_point_temperature,), (5000.722328847392,) + ) + assert p.model == "IEC 61966-2-1 Default RGB Colour Space - sRGB" + + assert p.perceptual_rendering_intent_gamut is None + + assert p.profile_description == "sRGB IEC61966-2-1 black scaled" + assert p.profile_id == b")\xf8=\xde\xaf\xf2U\xaexB\xfa\xe4\xca\x839\r" + assert_truncated_tuple_equal( + p.red_colorant, + ( + (0.436065673828125, 0.2224884033203125, 0.013916015625), + (0.6484536316398539, 0.3308524880306778, 0.2224884033203125), + ), + ) + assert_truncated_tuple_equal( + p.red_primary, + ( + (0.43606566581047446, 0.22248840582960838, 0.013916015621759925), + (0.6484536250319214, 0.3308524944738204, 0.22248840582960838), + ), + ) + assert p.rendering_intent == 0 + assert p.saturation_rendering_intent_gamut is None + assert p.screening_description is None + assert p.target is None + assert p.technology == "CRT " + assert p.version == 2.0 + assert p.viewing_condition == "Reference Viewing Condition in IEC 61966-2-1" + assert p.xcolor_space == "RGB " + + +def test_non_ascii_path(tmp_path): + skip_missing() + tempfile = str(tmp_path / ("temp_" + chr(128) + ".icc")) + try: + shutil.copy(SRGB, tempfile) + except UnicodeEncodeError: + pytest.skip("Non-ASCII path could not be created") + + o = ImageCms.getOpenProfile(tempfile) + p = o.profile + assert p.model == "IEC 61966-2-1 Default RGB Colour Space - sRGB" + + +def test_profile_typesafety(): + """Profile init type safety + + prepatch, these would segfault, postpatch they should emit a typeerror + """ + + with pytest.raises(TypeError, match="Invalid type for Profile"): + ImageCms.ImageCmsProfile(0).tobytes() + with pytest.raises(TypeError, match="Invalid type for Profile"): + ImageCms.ImageCmsProfile(1).tobytes() + + +def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel): + def create_test_image(): + # set up test image with something interesting in the tested aux channel. + # fmt: off + nine_grid_deltas = [ + (-1, -1), (-1, 0), (-1, 1), + (0, -1), (0, 0), (0, 1), + (1, -1), (1, 0), (1, 1), ] - for src_format in aux_channel_formats: - for dst_format in aux_channel_formats: - for transform_in_place in [True, False]: - # inplace only if format doesn't change - if transform_in_place and src_format[0] != dst_format[0]: - continue - - # convert with and without AUX data, test colors are equal - source_profile = ImageCms.createProfile(src_format[1]) - destination_profile = ImageCms.createProfile(dst_format[1]) - source_image = src_format[3] - test_transform = ImageCms.buildTransform(source_profile, destination_profile, inMode=src_format[0], outMode=dst_format[0]) - - # test conversion from aux-ful source - if transform_in_place: - test_image = source_image.copy() - ImageCms.applyTransform(test_image, test_transform, inPlace=True) - else: - test_image = ImageCms.applyTransform(source_image, test_transform, inPlace=False) - - # reference conversion from aux-less source - reference_transform = ImageCms.buildTransform(source_profile, destination_profile, inMode=src_format[2], outMode=dst_format[2]) - reference_image = ImageCms.applyTransform(source_image.convert(src_format[2]), reference_transform) - - self.assert_image_equal(test_image.convert(dst_format[2]), reference_image) - -if __name__ == '__main__': - unittest.main() + # fmt: on + chans = [] + bands = ImageMode.getmode(mode).bands + for band_ndx in range(len(bands)): + channel_type = "L" # 8-bit unorm + channel_pattern = hopper(channel_type) + + # paste pattern with varying offsets to avoid correlation + # potentially hiding some bugs (like channels getting mixed). + paste_offset = ( + int(band_ndx / len(bands) * channel_pattern.size[0]), + int(band_ndx / (len(bands) * 2) * channel_pattern.size[1]), + ) + channel_data = Image.new(channel_type, channel_pattern.size) + for delta in nine_grid_deltas: + channel_data.paste( + channel_pattern, + tuple( + paste_offset[c] + delta[c] * channel_pattern.size[c] + for c in range(2) + ), + ) + chans.append(channel_data) + return Image.merge(mode, chans) + + source_image = create_test_image() + source_image_aux = source_image.getchannel(preserved_channel) + + # create some transform, it doesn't matter which one + source_profile = ImageCms.createProfile("sRGB") + destination_profile = ImageCms.createProfile("sRGB") + t = ImageCms.buildTransform( + source_profile, destination_profile, inMode=mode, outMode=mode + ) + + # apply transform + if transform_in_place: + ImageCms.applyTransform(source_image, t, inPlace=True) + result_image = source_image + else: + result_image = ImageCms.applyTransform(source_image, t, inPlace=False) + result_image_aux = result_image.getchannel(preserved_channel) + + assert_image_equal(source_image_aux, result_image_aux) + + +def test_preserve_auxiliary_channels_rgba(): + assert_aux_channel_preserved( + mode="RGBA", transform_in_place=False, preserved_channel="A" + ) + + +def test_preserve_auxiliary_channels_rgba_in_place(): + assert_aux_channel_preserved( + mode="RGBA", transform_in_place=True, preserved_channel="A" + ) + + +def test_preserve_auxiliary_channels_rgbx(): + assert_aux_channel_preserved( + mode="RGBX", transform_in_place=False, preserved_channel="X" + ) + + +def test_preserve_auxiliary_channels_rgbx_in_place(): + assert_aux_channel_preserved( + mode="RGBX", transform_in_place=True, preserved_channel="X" + ) + + +def test_auxiliary_channels_isolated(): + # test data in aux channels does not affect non-aux channels + aux_channel_formats = [ + # format, profile, color-only format, source test image + ("RGBA", "sRGB", "RGB", hopper("RGBA")), + ("RGBX", "sRGB", "RGB", hopper("RGBX")), + ("LAB", "LAB", "LAB", Image.open("Tests/images/hopper.Lab.tif")), + ] + for src_format in aux_channel_formats: + for dst_format in aux_channel_formats: + for transform_in_place in [True, False]: + # inplace only if format doesn't change + if transform_in_place and src_format[0] != dst_format[0]: + continue + + # convert with and without AUX data, test colors are equal + source_profile = ImageCms.createProfile(src_format[1]) + destination_profile = ImageCms.createProfile(dst_format[1]) + source_image = src_format[3] + test_transform = ImageCms.buildTransform( + source_profile, + destination_profile, + inMode=src_format[0], + outMode=dst_format[0], + ) + + # test conversion from aux-ful source + if transform_in_place: + test_image = source_image.copy() + ImageCms.applyTransform(test_image, test_transform, inPlace=True) + else: + test_image = ImageCms.applyTransform( + source_image, test_transform, inPlace=False + ) + + # reference conversion from aux-less source + reference_transform = ImageCms.buildTransform( + source_profile, + destination_profile, + inMode=src_format[2], + outMode=dst_format[2], + ) + reference_image = ImageCms.applyTransform( + source_image.convert(src_format[2]), reference_transform + ) + + assert_image_equal(test_image.convert(dst_format[2]), reference_image) + + +def test_constants_deprecation(): + for enum, prefix in { + ImageCms.Intent: "INTENT_", + ImageCms.Direction: "DIRECTION_", + }.items(): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(ImageCms, prefix + name) == enum[name] diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index 64e88cf9c22..dcc44e6e342 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -1,165 +1,202 @@ -from helper import unittest, PillowTestCase - -from PIL import Image -from PIL import ImageColor - - -class TestImageColor(PillowTestCase): - - def test_hash(self): - # short 3 components - self.assertEqual((255, 0, 0), ImageColor.getrgb("#f00")) - self.assertEqual((0, 255, 0), ImageColor.getrgb("#0f0")) - self.assertEqual((0, 0, 255), ImageColor.getrgb("#00f")) - - # short 4 components - self.assertEqual((255, 0, 0, 0), ImageColor.getrgb("#f000")) - self.assertEqual((0, 255, 0, 0), ImageColor.getrgb("#0f00")) - self.assertEqual((0, 0, 255, 0), ImageColor.getrgb("#00f0")) - self.assertEqual((0, 0, 0, 255), ImageColor.getrgb("#000f")) - - # long 3 components - self.assertEqual((222, 0, 0), ImageColor.getrgb("#de0000")) - self.assertEqual((0, 222, 0), ImageColor.getrgb("#00de00")) - self.assertEqual((0, 0, 222), ImageColor.getrgb("#0000de")) - - # long 4 components - self.assertEqual((222, 0, 0, 0), ImageColor.getrgb("#de000000")) - self.assertEqual((0, 222, 0, 0), ImageColor.getrgb("#00de0000")) - self.assertEqual((0, 0, 222, 0), ImageColor.getrgb("#0000de00")) - self.assertEqual((0, 0, 0, 222), ImageColor.getrgb("#000000de")) - - # case insensitivity - self.assertEqual(ImageColor.getrgb("#DEF"), ImageColor.getrgb("#def")) - self.assertEqual(ImageColor.getrgb("#CDEF"), ImageColor.getrgb("#cdef")) - self.assertEqual(ImageColor.getrgb("#DEFDEF"), - ImageColor.getrgb("#defdef")) - self.assertEqual(ImageColor.getrgb("#CDEFCDEF"), - ImageColor.getrgb("#cdefcdef")) - - # not a number - self.assertRaises(ValueError, ImageColor.getrgb, "#fo0") - self.assertRaises(ValueError, ImageColor.getrgb, "#fo00") - self.assertRaises(ValueError, ImageColor.getrgb, "#fo0000") - self.assertRaises(ValueError, ImageColor.getrgb, "#fo000000") - - # wrong number of components - self.assertRaises(ValueError, ImageColor.getrgb, "#f0000") - self.assertRaises(ValueError, ImageColor.getrgb, "#f000000") - self.assertRaises(ValueError, ImageColor.getrgb, "#f00000000") - self.assertRaises(ValueError, ImageColor.getrgb, "#f000000000") - self.assertRaises(ValueError, ImageColor.getrgb, "#f00000 ") - - def test_colormap(self): - self.assertEqual((0, 0, 0), ImageColor.getrgb("black")) - self.assertEqual((255, 255, 255), ImageColor.getrgb("white")) - self.assertEqual((255, 255, 255), ImageColor.getrgb("WHITE")) - - self.assertRaises(ValueError, ImageColor.getrgb, "black ") - - def test_functions(self): - # rgb numbers - self.assertEqual((255, 0, 0), ImageColor.getrgb("rgb(255,0,0)")) - self.assertEqual((0, 255, 0), ImageColor.getrgb("rgb(0,255,0)")) - self.assertEqual((0, 0, 255), ImageColor.getrgb("rgb(0,0,255)")) - - # percents - self.assertEqual((255, 0, 0), ImageColor.getrgb("rgb(100%,0%,0%)")) - self.assertEqual((0, 255, 0), ImageColor.getrgb("rgb(0%,100%,0%)")) - self.assertEqual((0, 0, 255), ImageColor.getrgb("rgb(0%,0%,100%)")) - - # rgba numbers - self.assertEqual((255, 0, 0, 0), ImageColor.getrgb("rgba(255,0,0,0)")) - self.assertEqual((0, 255, 0, 0), ImageColor.getrgb("rgba(0,255,0,0)")) - self.assertEqual((0, 0, 255, 0), ImageColor.getrgb("rgba(0,0,255,0)")) - self.assertEqual((0, 0, 0, 255), ImageColor.getrgb("rgba(0,0,0,255)")) - - self.assertEqual((255, 0, 0), ImageColor.getrgb("hsl(0,100%,50%)")) - self.assertEqual((255, 0, 0), ImageColor.getrgb("hsl(360,100%,50%)")) - self.assertEqual((0, 255, 255), ImageColor.getrgb("hsl(180,100%,50%)")) - - # case insensitivity - self.assertEqual(ImageColor.getrgb("RGB(255,0,0)"), - ImageColor.getrgb("rgb(255,0,0)")) - self.assertEqual(ImageColor.getrgb("RGB(100%,0%,0%)"), - ImageColor.getrgb("rgb(100%,0%,0%)")) - self.assertEqual(ImageColor.getrgb("RGBA(255,0,0,0)"), - ImageColor.getrgb("rgba(255,0,0,0)")) - self.assertEqual(ImageColor.getrgb("HSL(0,100%,50%)"), - ImageColor.getrgb("hsl(0,100%,50%)")) - - # space agnosticism - self.assertEqual((255, 0, 0), - ImageColor.getrgb("rgb( 255 , 0 , 0 )")) - self.assertEqual((255, 0, 0), - ImageColor.getrgb("rgb( 100% , 0% , 0% )")) - self.assertEqual((255, 0, 0, 0), - ImageColor.getrgb("rgba( 255 , 0 , 0 , 0 )")) - self.assertEqual((255, 0, 0), - ImageColor.getrgb("hsl( 0 , 100% , 50% )")) - - # wrong number of components - self.assertRaises(ValueError, ImageColor.getrgb, "rgb(255,0)") - self.assertRaises(ValueError, ImageColor.getrgb, "rgb(255,0,0,0)") - - self.assertRaises(ValueError, ImageColor.getrgb, "rgb(100%,0%)") - self.assertRaises(ValueError, ImageColor.getrgb, "rgb(100%,0%,0)") - self.assertRaises(ValueError, ImageColor.getrgb, "rgb(100%,0%,0 %)") - self.assertRaises(ValueError, ImageColor.getrgb, "rgb(100%,0%,0%,0%)") - - self.assertRaises(ValueError, ImageColor.getrgb, "rgba(255,0,0)") - self.assertRaises(ValueError, ImageColor.getrgb, "rgba(255,0,0,0,0)") - - self.assertRaises(ValueError, ImageColor.getrgb, "hsl(0,100%)") - self.assertRaises(ValueError, ImageColor.getrgb, "hsl(0,100%,0%,0%)") - self.assertRaises(ValueError, ImageColor.getrgb, "hsl(0%,100%,50%)") - self.assertRaises(ValueError, ImageColor.getrgb, "hsl(0,100,50%)") - self.assertRaises(ValueError, ImageColor.getrgb, "hsl(0,100%,50)") - - # look for rounding errors (based on code by Tim Hatch) - def test_rounding_errors(self): - - for color in ImageColor.colormap: - expected = Image.new( - "RGB", (1, 1), color).convert("L").getpixel((0, 0)) - actual = ImageColor.getcolor(color, 'L') - self.assertEqual(expected, actual) - - self.assertEqual( - (0, 255, 115), ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGB")) - Image.new("RGB", (1, 1), "white") - - self.assertEqual((0, 0, 0, 255), ImageColor.getcolor("black", "RGBA")) - self.assertEqual( - (255, 255, 255, 255), ImageColor.getcolor("white", "RGBA")) - self.assertEqual( - (0, 255, 115, 33), - ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGBA")) - Image.new("RGBA", (1, 1), "white") - - self.assertEqual(0, ImageColor.getcolor("black", "L")) - self.assertEqual(255, ImageColor.getcolor("white", "L")) - self.assertEqual(162, - ImageColor.getcolor("rgba(0, 255, 115, 33)", "L")) - Image.new("L", (1, 1), "white") - - self.assertEqual(0, ImageColor.getcolor("black", "1")) - self.assertEqual(255, ImageColor.getcolor("white", "1")) - # The following test is wrong, but is current behavior - # The correct result should be 255 due to the mode 1 - self.assertEqual( - 162, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1")) - # Correct behavior - # self.assertEqual( - # 255, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1")) - Image.new("1", (1, 1), "white") - - self.assertEqual((0, 255), ImageColor.getcolor("black", "LA")) - self.assertEqual((255, 255), ImageColor.getcolor("white", "LA")) - self.assertEqual( - (162, 33), ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA")) - Image.new("LA", (1, 1), "white") - - -if __name__ == '__main__': - unittest.main() +import pytest + +from PIL import Image, ImageColor + + +def test_hash(): + # short 3 components + assert (255, 0, 0) == ImageColor.getrgb("#f00") + assert (0, 255, 0) == ImageColor.getrgb("#0f0") + assert (0, 0, 255) == ImageColor.getrgb("#00f") + + # short 4 components + assert (255, 0, 0, 0) == ImageColor.getrgb("#f000") + assert (0, 255, 0, 0) == ImageColor.getrgb("#0f00") + assert (0, 0, 255, 0) == ImageColor.getrgb("#00f0") + assert (0, 0, 0, 255) == ImageColor.getrgb("#000f") + + # long 3 components + assert (222, 0, 0) == ImageColor.getrgb("#de0000") + assert (0, 222, 0) == ImageColor.getrgb("#00de00") + assert (0, 0, 222) == ImageColor.getrgb("#0000de") + + # long 4 components + assert (222, 0, 0, 0) == ImageColor.getrgb("#de000000") + assert (0, 222, 0, 0) == ImageColor.getrgb("#00de0000") + assert (0, 0, 222, 0) == ImageColor.getrgb("#0000de00") + assert (0, 0, 0, 222) == ImageColor.getrgb("#000000de") + + # case insensitivity + assert ImageColor.getrgb("#DEF") == ImageColor.getrgb("#def") + assert ImageColor.getrgb("#CDEF") == ImageColor.getrgb("#cdef") + assert ImageColor.getrgb("#DEFDEF") == ImageColor.getrgb("#defdef") + assert ImageColor.getrgb("#CDEFCDEF") == ImageColor.getrgb("#cdefcdef") + + # not a number + with pytest.raises(ValueError): + ImageColor.getrgb("#fo0") + with pytest.raises(ValueError): + ImageColor.getrgb("#fo00") + with pytest.raises(ValueError): + ImageColor.getrgb("#fo0000") + with pytest.raises(ValueError): + ImageColor.getrgb("#fo000000") + + # wrong number of components + with pytest.raises(ValueError): + ImageColor.getrgb("#f0000") + with pytest.raises(ValueError): + ImageColor.getrgb("#f000000") + with pytest.raises(ValueError): + ImageColor.getrgb("#f00000000") + with pytest.raises(ValueError): + ImageColor.getrgb("#f000000000") + with pytest.raises(ValueError): + ImageColor.getrgb("#f00000 ") + + +def test_colormap(): + assert (0, 0, 0) == ImageColor.getrgb("black") + assert (255, 255, 255) == ImageColor.getrgb("white") + assert (255, 255, 255) == ImageColor.getrgb("WHITE") + + with pytest.raises(ValueError): + ImageColor.getrgb("black ") + + +def test_functions(): + # rgb numbers + assert (255, 0, 0) == ImageColor.getrgb("rgb(255,0,0)") + assert (0, 255, 0) == ImageColor.getrgb("rgb(0,255,0)") + assert (0, 0, 255) == ImageColor.getrgb("rgb(0,0,255)") + + # percents + assert (255, 0, 0) == ImageColor.getrgb("rgb(100%,0%,0%)") + assert (0, 255, 0) == ImageColor.getrgb("rgb(0%,100%,0%)") + assert (0, 0, 255) == ImageColor.getrgb("rgb(0%,0%,100%)") + + # rgba numbers + assert (255, 0, 0, 0) == ImageColor.getrgb("rgba(255,0,0,0)") + assert (0, 255, 0, 0) == ImageColor.getrgb("rgba(0,255,0,0)") + assert (0, 0, 255, 0) == ImageColor.getrgb("rgba(0,0,255,0)") + assert (0, 0, 0, 255) == ImageColor.getrgb("rgba(0,0,0,255)") + + assert (255, 0, 0) == ImageColor.getrgb("hsl(0,100%,50%)") + assert (255, 0, 0) == ImageColor.getrgb("hsl(360,100%,50%)") + assert (0, 255, 255) == ImageColor.getrgb("hsl(180,100%,50%)") + + assert (255, 0, 0) == ImageColor.getrgb("hsv(0,100%,100%)") + assert (255, 0, 0) == ImageColor.getrgb("hsv(360,100%,100%)") + assert (0, 255, 255) == ImageColor.getrgb("hsv(180,100%,100%)") + + # alternate format + assert ImageColor.getrgb("hsb(0,100%,50%)") == ImageColor.getrgb("hsv(0,100%,50%)") + + # floats + assert (254, 3, 3) == ImageColor.getrgb("hsl(0.1,99.2%,50.3%)") + assert (255, 0, 0) == ImageColor.getrgb("hsl(360.,100.0%,50%)") + + assert (253, 2, 2) == ImageColor.getrgb("hsv(0.1,99.2%,99.3%)") + assert (255, 0, 0) == ImageColor.getrgb("hsv(360.,100.0%,100%)") + + # case insensitivity + assert ImageColor.getrgb("RGB(255,0,0)") == ImageColor.getrgb("rgb(255,0,0)") + assert ImageColor.getrgb("RGB(100%,0%,0%)") == ImageColor.getrgb("rgb(100%,0%,0%)") + assert ImageColor.getrgb("RGBA(255,0,0,0)") == ImageColor.getrgb("rgba(255,0,0,0)") + assert ImageColor.getrgb("HSL(0,100%,50%)") == ImageColor.getrgb("hsl(0,100%,50%)") + assert ImageColor.getrgb("HSV(0,100%,50%)") == ImageColor.getrgb("hsv(0,100%,50%)") + assert ImageColor.getrgb("HSB(0,100%,50%)") == ImageColor.getrgb("hsb(0,100%,50%)") + + # space agnosticism + assert (255, 0, 0) == ImageColor.getrgb("rgb( 255 , 0 , 0 )") + assert (255, 0, 0) == ImageColor.getrgb("rgb( 100% , 0% , 0% )") + assert (255, 0, 0, 0) == ImageColor.getrgb("rgba( 255 , 0 , 0 , 0 )") + assert (255, 0, 0) == ImageColor.getrgb("hsl( 0 , 100% , 50% )") + assert (255, 0, 0) == ImageColor.getrgb("hsv( 0 , 100% , 100% )") + + # wrong number of components + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(255,0)") + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(255,0,0,0)") + + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(100%,0%)") + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(100%,0%,0)") + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(100%,0%,0 %)") + with pytest.raises(ValueError): + ImageColor.getrgb("rgb(100%,0%,0%,0%)") + + with pytest.raises(ValueError): + ImageColor.getrgb("rgba(255,0,0)") + with pytest.raises(ValueError): + ImageColor.getrgb("rgba(255,0,0,0,0)") + + with pytest.raises(ValueError): + ImageColor.getrgb("hsl(0,100%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsl(0,100%,0%,0%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsl(0%,100%,50%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsl(0,100,50%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsl(0,100%,50)") + + with pytest.raises(ValueError): + ImageColor.getrgb("hsv(0,100%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsv(0,100%,0%,0%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsv(0%,100%,50%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsv(0,100,50%)") + with pytest.raises(ValueError): + ImageColor.getrgb("hsv(0,100%,50)") + + +# look for rounding errors (based on code by Tim Hatch) +def test_rounding_errors(): + for color in ImageColor.colormap: + expected = Image.new("RGB", (1, 1), color).convert("L").getpixel((0, 0)) + actual = ImageColor.getcolor(color, "L") + assert expected == actual + + assert (0, 255, 115) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGB") + Image.new("RGB", (1, 1), "white") + + assert (0, 0, 0, 255) == ImageColor.getcolor("black", "RGBA") + assert (255, 255, 255, 255) == ImageColor.getcolor("white", "RGBA") + assert (0, 255, 115, 33) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGBA") + Image.new("RGBA", (1, 1), "white") + + assert 0 == ImageColor.getcolor("black", "L") + assert 255 == ImageColor.getcolor("white", "L") + assert 163 == ImageColor.getcolor("rgba(0, 255, 115, 33)", "L") + Image.new("L", (1, 1), "white") + + assert 0 == ImageColor.getcolor("black", "1") + assert 255 == ImageColor.getcolor("white", "1") + # The following test is wrong, but is current behavior + # The correct result should be 255 due to the mode 1 + assert 163 == ImageColor.getcolor("rgba(0, 255, 115, 33)", "1") + # Correct behavior + # assert + # 255, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1")) + Image.new("1", (1, 1), "white") + + assert (0, 255) == ImageColor.getcolor("black", "LA") + assert (255, 255) == ImageColor.getcolor("white", "LA") + assert (163, 33) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA") + Image.new("LA", (1, 1), "white") + + +def test_color_too_long(): + # Arrange + color_too_long = "hsl(" + "1" * 40 + "," + "1" * 40 + "%," + "1" * 40 + "%)" + + # Act / Assert + with pytest.raises(ValueError): + ImageColor.getrgb(color_too_long) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index a79a75ca0dd..76b7c65cc37 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,17 +1,22 @@ -from helper import unittest, PillowTestCase, hopper - -from PIL import Image -from PIL import ImageColor -from PIL import ImageDraw import os.path -import sys +import pytest + +from PIL import Image, ImageColor, ImageDraw, ImageFont + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar_tofile, + hopper, + skip_unless_feature, +) BLACK = (0, 0, 0) WHITE = (255, 255, 255) GRAY = (190, 190, 190) -DEFAULT_MODE = 'RGB' -IMAGES_PATH = os.path.join('Tests', 'images', 'imagedraw') +DEFAULT_MODE = "RGB" +IMAGES_PATH = os.path.join("Tests", "images", "imagedraw") # Image size W, H = 100, 100 @@ -33,545 +38,1414 @@ KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] -class TestImageDraw(PillowTestCase): +def test_sanity(): + im = hopper("RGB").copy() - def test_sanity(self): - im = hopper("RGB").copy() + draw = ImageDraw.ImageDraw(im) + draw = ImageDraw.Draw(im) - draw = ImageDraw.ImageDraw(im) - draw = ImageDraw.Draw(im) + draw.ellipse(list(range(4))) + draw.line(list(range(10))) + draw.polygon(list(range(100))) + draw.rectangle(list(range(4))) - draw.ellipse(list(range(4))) - draw.line(list(range(10))) - draw.polygon(list(range(100))) - draw.rectangle(list(range(4))) - def test_valueerror(self): - im = Image.open("Tests/images/chi.gif") +def test_valueerror(): + with Image.open("Tests/images/chi.gif") as im: draw = ImageDraw.Draw(im) - draw.line(((0, 0)), fill=(0, 0, 0)) + draw.line((0, 0), fill=(0, 0, 0)) - def test_mode_mismatch(self): - im = hopper("RGB").copy() - self.assertRaises(ValueError, ImageDraw.ImageDraw, im, mode="L") +def test_mode_mismatch(): + im = hopper("RGB").copy() - def helper_arc(self, bbox, start, end): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) + with pytest.raises(ValueError): + ImageDraw.ImageDraw(im, mode="L") - # Act - draw.arc(bbox, start, end) - # Assert - self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_arc.png"), 1) +@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) +def test_arc(bbox, start, end): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - def test_arc1(self): - self.helper_arc(BBOX1, 0, 180) - self.helper_arc(BBOX1, 0.5, 180.4) + # Act + draw.arc(bbox, start, end) - def test_arc2(self): - self.helper_arc(BBOX2, 0, 180) - self.helper_arc(BBOX2, 0.5, 180.4) + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1) - def test_arc_end_le_start(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - start = 270.5 - end = 0 - # Act - draw.arc(BBOX1, start=start, end=end) +def test_arc_end_le_start(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + start = 270.5 + end = 0 - # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_arc_end_le_start.png")) + # Act + draw.arc(BBOX1, start=start, end=end) - def test_arc_no_loops(self): - # No need to go in loops - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - start = 5 - end = 370 + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_end_le_start.png") - # Act - draw.arc(BBOX1, start=start, end=end) - # Assert - self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_arc_no_loops.png"), 1) +def test_arc_no_loops(): + # No need to go in loops + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + start = 5 + end = 370 + + # Act + draw.arc(BBOX1, start=start, end=end) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_no_loops.png", 1) + + +def test_arc_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.arc(BBOX1, 10, 260, width=5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width.png", 1) + + +def test_arc_width_pieslice_large(): + # Tests an arc with a large enough width that it is a pieslice + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.arc(BBOX1, 10, 260, fill="yellow", width=100) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_pieslice.png", 1) - def test_bitmap(self): - # Arrange - small = Image.open("Tests/images/pil123rgba.png").resize((50, 50)) - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) + +def test_arc_width_fill(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.arc(BBOX1, 10, 260, fill="yellow", width=5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_fill.png", 1) + + +def test_arc_width_non_whole_angle(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_arc_width_non_whole_angle.png" + + # Act + draw.arc(BBOX1, 10, 259.5, width=5) + + # Assert + assert_image_similar_tofile(im, expected, 1) + + +def test_arc_high(): + # Arrange + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + + # Act + draw.arc([10, 10, 89, 189], 20, 330, width=20, fill="white") + draw.arc([110, 10, 189, 189], 20, 150, width=20, fill="white") + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_high.png") + + +def test_bitmap(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + with Image.open("Tests/images/pil123rgba.png") as small: + small = small.resize((50, 50), Image.Resampling.NEAREST) # Act draw.bitmap((10, 10), small) - # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_bitmap.png")) + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_bitmap.png") - def helper_chord(self, mode, bbox, start, end): - # Arrange - im = Image.new(mode, (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_chord_{}.png".format(mode) - # Act - draw.chord(bbox, start, end, fill="red", outline="yellow") +@pytest.mark.parametrize("mode", ("RGB", "L")) +@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +def test_chord(mode, bbox): + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + expected = f"Tests/images/imagedraw_chord_{mode}.png" - # Assert - self.assert_image_similar(im, Image.open(expected), 1) + # Act + draw.chord(bbox, 0, 180, fill="red", outline="yellow") - def test_chord1(self): - for mode in ["RGB", "L"]: - self.helper_chord(mode, BBOX1, 0, 180) - self.helper_chord(mode, BBOX1, 0.5, 180.4) + # Assert + assert_image_similar_tofile(im, expected, 1) - def test_chord2(self): - for mode in ["RGB", "L"]: - self.helper_chord(mode, BBOX2, 0, 180) - self.helper_chord(mode, BBOX2, 0.5, 180.4) - def helper_ellipse(self, mode, bbox): - # Arrange - im = Image.new(mode, (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_ellipse_{}.png".format(mode) +def test_chord_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - # Act - draw.ellipse(bbox, fill="green", outline="blue") + # Act + draw.chord(BBOX1, 10, 260, outline="yellow", width=5) - # Assert - self.assert_image_similar(im, Image.open(expected), 1) + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width.png", 1) - def test_ellipse1(self): - for mode in ["RGB", "L"]: - self.helper_ellipse(mode, BBOX1) - def test_ellipse2(self): - for mode in ["RGB", "L"]: - self.helper_ellipse(mode, BBOX2) +def test_chord_width_fill(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - def test_ellipse_edge(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) + # Act + draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=5) - # Act - draw.ellipse(((0, 0), (W-1, H)), fill="white") + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width_fill.png", 1) - # Assert - self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1) - def helper_line(self, points): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) +def test_chord_zero_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - # Act - draw.line(points, fill="yellow", width=2) + # Act + draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=0) - # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_line.png")) + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_zero_width.png") - def test_line1(self): - self.helper_line(POINTS1) - def test_line2(self): - self.helper_line(POINTS2) +def test_chord_too_fat(): + # Arrange + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) - def test_shape1(self): - # Arrange - im = Image.new("RGB", (100, 100), "white") - draw = ImageDraw.Draw(im) - x0, y0 = 5, 5 - x1, y1 = 5, 50 - x2, y2 = 95, 50 - x3, y3 = 95, 5 + # Act + draw.chord([-150, -150, 99, 99], 15, 60, width=10, fill="white", outline="red") - # Act - s = ImageDraw.Outline() - s.move(x0, y0) - s.curve(x1, y1, x2, y2, x3, y3) - s.line(x0, y0) + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_too_fat.png") - draw.shape(s, fill=1) - # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_shape1.png")) +@pytest.mark.parametrize("mode", ("RGB", "L")) +@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +def test_ellipse(mode, bbox): + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + expected = f"Tests/images/imagedraw_ellipse_{mode}.png" - def test_shape2(self): - # Arrange - im = Image.new("RGB", (100, 100), "white") - draw = ImageDraw.Draw(im) - x0, y0 = 95, 95 - x1, y1 = 95, 50 - x2, y2 = 5, 50 - x3, y3 = 5, 95 + # Act + draw.ellipse(bbox, fill="green", outline="blue") - # Act - s = ImageDraw.Outline() - s.move(x0, y0) - s.curve(x1, y1, x2, y2, x3, y3) - s.line(x0, y0) + # Assert + assert_image_similar_tofile(im, expected, 1) - draw.shape(s, outline="blue") - # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_shape2.png")) +def test_ellipse_translucent(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") - def helper_pieslice(self, bbox, start, end): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) + # Act + draw.ellipse(BBOX1, fill=(0, 255, 0, 127)) - # Act - draw.pieslice(bbox, start, end, fill="white", outline="blue") + # Assert + expected = "Tests/images/imagedraw_ellipse_translucent.png" + assert_image_similar_tofile(im, expected, 1) - # Assert - self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_pieslice.png"), 1) - def test_pieslice1(self): - self.helper_pieslice(BBOX1, -90, 45) - self.helper_pieslice(BBOX1, -90.5, 45.4) +def test_ellipse_edge(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - def test_pieslice2(self): - self.helper_pieslice(BBOX2, -90, 45) - self.helper_pieslice(BBOX2, -90.5, 45.4) + # Act + draw.ellipse(((0, 0), (W - 1, H - 1)), fill="white") - def helper_point(self, points): - # Arrange - im = Image.new("RGB", (W, H)) + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) + + +def test_ellipse_symmetric(): + for width, bbox in ( + (100, (24, 24, 75, 75)), + (101, (25, 25, 75, 75)), + ): + im = Image.new("RGB", (width, 100)) draw = ImageDraw.Draw(im) + draw.ellipse(bbox, fill="green", outline="blue") + assert_image_equal(im, im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)) - # Act - draw.point(points, fill="yellow") - # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_point.png")) +def test_ellipse_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - def test_point1(self): - self.helper_point(POINTS1) + # Act + draw.ellipse(BBOX1, outline="blue", width=5) - def test_point2(self): - self.helper_point(POINTS2) + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1) - def helper_polygon(self, points): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - # Act - draw.polygon(points, fill="red", outline="blue") +def test_ellipse_width_large(): + # Arrange + im = Image.new("RGB", (500, 500)) + draw = ImageDraw.Draw(im) - # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_polygon.png")) + # Act + draw.ellipse((25, 25, 475, 475), outline="blue", width=75) - def test_polygon1(self): - self.helper_polygon(POINTS1) + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_large.png", 1) - def test_polygon2(self): - self.helper_polygon(POINTS2) - def test_polygon_kite(self): - # Test drawing lines of different gradients (dx>dy, dy>dx) and - # vertical (dx==0) and horizontal (dy==0) lines - for mode in ["RGB", "L"]: - # Arrange - im = Image.new(mode, (W, H)) - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_polygon_kite_{}.png".format( - mode) +def test_ellipse_width_fill(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - # Act - draw.polygon(KITE_POINTS, fill="blue", outline="yellow") + # Act + draw.ellipse(BBOX1, fill="green", outline="blue", width=5) - # Assert - self.assert_image_equal(im, Image.open(expected)) + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_fill.png", 1) - def helper_rectangle(self, bbox): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - # Act - draw.rectangle(bbox, fill="black", outline="green") +def test_ellipse_zero_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_rectangle.png")) + # Act + draw.ellipse(BBOX1, fill="green", outline="blue", width=0) - def test_rectangle1(self): - self.helper_rectangle(BBOX1) + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png") - def test_rectangle2(self): - self.helper_rectangle(BBOX2) - def test_big_rectangle(self): - # Test drawing a rectangle bigger than the image - # Arrange - im = Image.new("RGB", (W, H)) - bbox = [(-1, -1), (W+1, H+1)] - draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_big_rectangle.png" +def ellipse_various_sizes_helper(filled): + ellipse_sizes = range(32) + image_size = sum(ellipse_sizes) + len(ellipse_sizes) + 1 + im = Image.new("RGB", (image_size, image_size)) + draw = ImageDraw.Draw(im) - # Act - draw.rectangle(bbox, fill="orange") + x = 1 + for w in ellipse_sizes: + y = 1 + for h in ellipse_sizes: + border = [x, y, x + w - 1, y + h - 1] + if filled: + draw.ellipse(border, fill="white") + else: + draw.ellipse(border, outline="white") + y += h + 1 + x += w + 1 - # Assert - self.assert_image_similar(im, Image.open(expected), 1) + return im - def test_floodfill(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="yellow", fill="green") - centre_point = (int(W/2), int(H/2)) - red = ImageColor.getrgb("red") - im_floodfill = Image.open("Tests/images/imagedraw_floodfill.png") - # Act - ImageDraw.floodfill(im, centre_point, red) +def test_ellipse_various_sizes(): + im = ellipse_various_sizes_helper(False) - # Assert - self.assert_image_equal(im, im_floodfill) + assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_various_sizes.png") - # Test that using the same colour does not change the image - ImageDraw.floodfill(im, centre_point, red) - self.assert_image_equal(im, im_floodfill) - # Test that filling outside the image does not change the image - ImageDraw.floodfill(im, (W, H), red) - self.assert_image_equal(im, im_floodfill) +def test_ellipse_various_sizes_filled(): + im = ellipse_various_sizes_helper(True) - @unittest.skipIf(hasattr(sys, 'pypy_version_info'), - "Causes fatal RPython error on PyPy") - def test_floodfill_border(self): - # floodfill() is experimental + assert_image_equal_tofile( + im, "Tests/images/imagedraw_ellipse_various_sizes_filled.png" + ) - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="yellow", fill="green") - centre_point = (int(W/2), int(H/2)) - # Act - ImageDraw.floodfill( - im, centre_point, ImageColor.getrgb("red"), - border=ImageColor.getrgb("black")) +@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +def test_line(points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) - # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_floodfill2.png")) + # Act + draw.line(points, fill="yellow", width=2) - def test_floodfill_thresh(self): - # floodfill() is experimental + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="darkgreen", fill="green") - centre_point = (int(W/2), int(H/2)) - # Act - ImageDraw.floodfill( - im, centre_point, ImageColor.getrgb("red"), - thresh=30) +def test_shape1(): + # Arrange + im = Image.new("RGB", (100, 100), "white") + draw = ImageDraw.Draw(im) + x0, y0 = 5, 5 + x1, y1 = 5, 50 + x2, y2 = 95, 50 + x3, y3 = 95, 5 - # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_floodfill2.png")) - - def create_base_image_draw(self, size, - mode=DEFAULT_MODE, - background1=WHITE, - background2=GRAY): - img = Image.new(mode, size, background1) - for x in range(0, size[0]): - for y in range(0, size[1]): - if (x + y) % 2 == 0: - img.putpixel((x, y), background2) - return (img, ImageDraw.Draw(img)) - - def test_square(self): - expected = Image.open(os.path.join(IMAGES_PATH, 'square.png')) - expected.load() - img, draw = self.create_base_image_draw((10, 10)) - draw.polygon([(2, 2), (2, 7), (7, 7), (7, 2)], BLACK) - self.assert_image_equal(img, expected, - 'square as normal polygon failed') - img, draw = self.create_base_image_draw((10, 10)) - draw.polygon([(7, 7), (7, 2), (2, 2), (2, 7)], BLACK) - self.assert_image_equal(img, expected, - 'square as inverted polygon failed') - img, draw = self.create_base_image_draw((10, 10)) - draw.rectangle((2, 2, 7, 7), BLACK) - self.assert_image_equal(img, expected, - 'square as normal rectangle failed') - img, draw = self.create_base_image_draw((10, 10)) - draw.rectangle((7, 7, 2, 2), BLACK) - self.assert_image_equal( - img, expected, 'square as inverted rectangle failed') - - def test_triangle_right(self): - expected = Image.open(os.path.join(IMAGES_PATH, 'triangle_right.png')) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.polygon([(3, 5), (17, 5), (10, 12)], BLACK) - self.assert_image_equal(img, expected, 'triangle right failed') - - def test_line_horizontal(self): - expected = Image.open(os.path.join(IMAGES_PATH, - 'line_horizontal_w2px_normal.png')) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.line((5, 5, 14, 5), BLACK, 2) - self.assert_image_equal( - img, expected, 'line straight horizontal normal 2px wide failed') - expected = Image.open(os.path.join(IMAGES_PATH, - 'line_horizontal_w2px_inverted.png')) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.line((14, 5, 5, 5), BLACK, 2) - self.assert_image_equal( - img, expected, 'line straight horizontal inverted 2px wide failed') - expected = Image.open(os.path.join(IMAGES_PATH, - 'line_horizontal_w3px.png')) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.line((5, 5, 14, 5), BLACK, 3) - self.assert_image_equal( - img, expected, 'line straight horizontal normal 3px wide failed') - img, draw = self.create_base_image_draw((20, 20)) - draw.line((14, 5, 5, 5), BLACK, 3) - self.assert_image_equal( - img, expected, 'line straight horizontal inverted 3px wide failed') - expected = Image.open(os.path.join(IMAGES_PATH, - 'line_horizontal_w101px.png')) - expected.load() - img, draw = self.create_base_image_draw((200, 110)) - draw.line((5, 55, 195, 55), BLACK, 101) - self.assert_image_equal( - img, expected, 'line straight horizontal 101px wide failed') - - def test_line_h_s1_w2(self): - self.skipTest('failing') - expected = Image.open(os.path.join(IMAGES_PATH, - 'line_horizontal_slope1px_w2px.png')) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.line((5, 5, 14, 6), BLACK, 2) - self.assert_image_equal( - img, expected, 'line horizontal 1px slope 2px wide failed') - - def test_line_vertical(self): - expected = Image.open(os.path.join(IMAGES_PATH, - 'line_vertical_w2px_normal.png')) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.line((5, 5, 5, 14), BLACK, 2) - self.assert_image_equal( - img, expected, 'line straight vertical normal 2px wide failed') - expected = Image.open(os.path.join(IMAGES_PATH, - 'line_vertical_w2px_inverted.png')) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.line((5, 14, 5, 5), BLACK, 2) - self.assert_image_equal( - img, expected, 'line straight vertical inverted 2px wide failed') - expected = Image.open(os.path.join(IMAGES_PATH, - 'line_vertical_w3px.png')) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.line((5, 5, 5, 14), BLACK, 3) - self.assert_image_equal( - img, expected, 'line straight vertical normal 3px wide failed') - img, draw = self.create_base_image_draw((20, 20)) - draw.line((5, 14, 5, 5), BLACK, 3) - self.assert_image_equal( - img, expected, 'line straight vertical inverted 3px wide failed') - expected = Image.open(os.path.join(IMAGES_PATH, - 'line_vertical_w101px.png')) - expected.load() - img, draw = self.create_base_image_draw((110, 200)) - draw.line((55, 5, 55, 195), BLACK, 101) - self.assert_image_equal(img, expected, - 'line straight vertical 101px wide failed') - expected = Image.open(os.path.join(IMAGES_PATH, - 'line_vertical_slope1px_w2px.png')) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.line((5, 5, 6, 14), BLACK, 2) - self.assert_image_equal(img, expected, - 'line vertical 1px slope 2px wide failed') - - def test_line_oblique_45(self): - expected = Image.open(os.path.join(IMAGES_PATH, - 'line_oblique_45_w3px_a.png')) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.line((5, 5, 14, 14), BLACK, 3) - self.assert_image_equal(img, expected, - 'line oblique 45 normal 3px wide A failed') - img, draw = self.create_base_image_draw((20, 20)) - draw.line((14, 14, 5, 5), BLACK, 3) - self.assert_image_equal(img, expected, - 'line oblique 45 inverted 3px wide A failed') - expected = Image.open(os.path.join(IMAGES_PATH, - 'line_oblique_45_w3px_b.png')) - expected.load() - img, draw = self.create_base_image_draw((20, 20)) - draw.line((14, 5, 5, 14), BLACK, 3) - self.assert_image_equal(img, expected, - 'line oblique 45 normal 3px wide B failed') - img, draw = self.create_base_image_draw((20, 20)) - draw.line((5, 14, 14, 5), BLACK, 3) - self.assert_image_equal(img, expected, - 'line oblique 45 inverted 3px wide B failed') - - def test_wide_line_dot(self): - # Test drawing a wide "line" from one point to another just draws - # a single point + # Act + s = ImageDraw.Outline() + s.move(x0, y0) + s.curve(x1, y1, x2, y2, x3, y3) + s.line(x0, y0) + + draw.shape(s, fill=1) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_shape1.png") + + +def test_shape2(): + # Arrange + im = Image.new("RGB", (100, 100), "white") + draw = ImageDraw.Draw(im) + x0, y0 = 95, 95 + x1, y1 = 95, 50 + x2, y2 = 5, 50 + x3, y3 = 5, 95 + + # Act + s = ImageDraw.Outline() + s.move(x0, y0) + s.curve(x1, y1, x2, y2, x3, y3) + s.line(x0, y0) + + draw.shape(s, outline="blue") + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_shape2.png") + + +def test_transform(): + # Arrange + im = Image.new("RGB", (100, 100), "white") + expected = im.copy() + draw = ImageDraw.Draw(im) + + # Act + s = ImageDraw.Outline() + s.line(0, 0) + s.transform((0, 0, 0, 0, 0, 0)) + + draw.shape(s, fill=1) + + # Assert + assert_image_equal(im, expected) + + +@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) +def test_pieslice(bbox, start, end): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.pieslice(bbox, start, end, fill="white", outline="blue") + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1) + + +def test_pieslice_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.pieslice(BBOX1, 10, 260, outline="blue", width=5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice_width.png", 1) + + +def test_pieslice_width_fill(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_pieslice_width_fill.png" + + # Act + draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=5) + + # Assert + assert_image_similar_tofile(im, expected, 1) + + +def test_pieslice_zero_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=0) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_zero_width.png") + + +def test_pieslice_wide(): + # Arrange + im = Image.new("RGB", (200, 100)) + draw = ImageDraw.Draw(im) + + # Act + draw.pieslice([0, 0, 199, 99], 190, 170, width=10, fill="white", outline="red") + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_wide.png") + + +def test_pieslice_no_spikes(): + im = Image.new("RGB", (161, 161), "white") + draw = ImageDraw.Draw(im) + cxs = ( + [140] * 3 + + list(range(140, 19, -20)) + + [20] * 5 + + list(range(20, 141, 20)) + + [140] * 2 + ) + cys = ( + list(range(80, 141, 20)) + + [140] * 5 + + list(range(140, 19, -20)) + + [20] * 5 + + list(range(20, 80, 20)) + ) + + for cx, cy, angle in zip(cxs, cys, range(0, 360, 15)): + draw.pieslice( + [cx - 100, cy - 100, cx + 100, cy + 100], angle, angle + 1, fill="black" + ) + draw.point([cx, cy], fill="red") + + im_pre_erase = im.copy() + draw.rectangle([21, 21, 139, 139], fill="white") + + assert_image_equal(im, im_pre_erase) + + +@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +def test_point(points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.point(points, fill="yellow") + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") + + +@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +def test_polygon(points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.polygon(points, fill="red", outline="blue") + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") + + +@pytest.mark.parametrize("mode", ("RGB", "L")) +def test_polygon_kite(mode): + # Test drawing lines of different gradients (dx>dy, dy>dx) and + # vertical (dx==0) and horizontal (dy==0) lines + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png" + + # Act + draw.polygon(KITE_POINTS, fill="blue", outline="yellow") + + # Assert + assert_image_equal_tofile(im, expected) + + +def test_polygon_1px_high(): + # Test drawing a 1px high polygon + # Arrange + im = Image.new("RGB", (3, 3)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_polygon_1px_high.png" + + # Act + draw.polygon([(0, 1), (0, 1), (2, 1), (2, 1)], "#f00") + + # Assert + assert_image_equal_tofile(im, expected) + + +def test_polygon_1px_high_translucent(): + # Test drawing a translucent 1px high polygon + # Arrange + im = Image.new("RGB", (4, 3)) + draw = ImageDraw.Draw(im, "RGBA") + expected = "Tests/images/imagedraw_polygon_1px_high_translucent.png" + + # Act + draw.polygon([(1, 1), (1, 1), (3, 1), (3, 1)], (255, 0, 0, 127)) + + # Assert + assert_image_equal_tofile(im, expected) + + +def test_polygon_translucent(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") + + # Act + draw.polygon([(20, 80), (80, 80), (80, 20)], fill=(0, 255, 0, 127)) + + # Assert + expected = "Tests/images/imagedraw_polygon_translucent.png" + assert_image_equal_tofile(im, expected) + + +@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +def test_rectangle(bbox): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.rectangle(bbox, fill="black", outline="green") + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") + + +def test_big_rectangle(): + # Test drawing a rectangle bigger than the image + # Arrange + im = Image.new("RGB", (W, H)) + bbox = [(-1, -1), (W + 1, H + 1)] + draw = ImageDraw.Draw(im) + + # Act + draw.rectangle(bbox, fill="orange") + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_big_rectangle.png", 1) + + +def test_rectangle_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_rectangle_width.png" + + # Act + draw.rectangle(BBOX1, outline="green", width=5) + + # Assert + assert_image_equal_tofile(im, expected) + + +def test_rectangle_width_fill(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_rectangle_width_fill.png" + + # Act + draw.rectangle(BBOX1, fill="blue", outline="green", width=5) + + # Assert + assert_image_equal_tofile(im, expected) + + +def test_rectangle_zero_width(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.rectangle(BBOX1, fill="blue", outline="green", width=0) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_zero_width.png") + + +def test_rectangle_I16(): + # Arrange + im = Image.new("I;16", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.rectangle(BBOX1, fill="black", outline="green") + + # Assert + assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png") + + +def test_rectangle_translucent_outline(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") + + # Act + draw.rectangle(BBOX1, fill="black", outline=(0, 255, 0, 127), width=5) + + # Assert + assert_image_equal_tofile( + im, "Tests/images/imagedraw_rectangle_translucent_outline.png" + ) + + +@pytest.mark.parametrize( + "xy", + [(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))], +) +def test_rounded_rectangle(xy): + # Arrange + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + + # Act + draw.rounded_rectangle(xy, 30, fill="red", outline="green", width=5) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_rounded_rectangle.png") + + +@pytest.mark.parametrize( + "xy, radius, type", + [ + ((10, 20, 190, 180), 30.5, "given"), + ((10, 10, 181, 190), 90, "width"), + ((10, 20, 190, 181), 85, "height"), + ], +) +def test_rounded_rectangle_non_integer_radius(xy, radius, type): + # Arrange + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + + # Act + draw.rounded_rectangle(xy, radius, fill="red", outline="green", width=5) + + # Assert + assert_image_equal_tofile( + im, + "Tests/images/imagedraw_rounded_rectangle_non_integer_radius_" + type + ".png", + ) + + +def test_rounded_rectangle_zero_radius(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.rounded_rectangle(BBOX1, 0, fill="blue", outline="green", width=5) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_width_fill.png") + + +@pytest.mark.parametrize( + "xy, suffix", + [ + ((20, 10, 80, 90), "x"), + ((10, 20, 90, 80), "y"), + ((20, 20, 80, 80), "both"), + ], +) +def test_rounded_rectangle_translucent(xy, suffix): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") + + # Act + draw.rounded_rectangle( + xy, 30, fill=(255, 0, 0, 127), outline=(0, 255, 0, 127), width=5 + ) + + # Assert + assert_image_equal_tofile( + im, "Tests/images/imagedraw_rounded_rectangle_" + suffix + ".png" + ) + + +def test_floodfill(): + red = ImageColor.getrgb("red") + + for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: # Arrange - im = Image.new("RGB", (W, H)) + im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_wide_line_dot.png" + draw.rectangle(BBOX2, outline="yellow", fill="green") + centre_point = (int(W / 2), int(H / 2)) # Act - draw.line([(50, 50), (50, 50)], width=3) + ImageDraw.floodfill(im, centre_point, value) # Assert - self.assert_image_similar(im, Image.open(expected), 1) - - def test_textsize_empty_string(self): - # https://github.com/python-pillow/Pillow/issues/2783 + expected = "Tests/images/imagedraw_floodfill_" + mode + ".png" + with Image.open(expected) as im_floodfill: + assert_image_equal(im, im_floodfill) + + # Test that using the same colour does not change the image + ImageDraw.floodfill(im, centre_point, red) + assert_image_equal(im, im_floodfill) + + # Test that filling outside the image does not change the image + ImageDraw.floodfill(im, (W, H), red) + assert_image_equal(im, im_floodfill) + + # Test filling at the edge of an image + im = Image.new("RGB", (1, 1)) + ImageDraw.floodfill(im, (0, 0), red) + assert_image_equal(im, Image.new("RGB", (1, 1), red)) + + +def test_floodfill_border(): + # floodfill() is experimental + + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + draw.rectangle(BBOX2, outline="yellow", fill="green") + centre_point = (int(W / 2), int(H / 2)) + + # Act + ImageDraw.floodfill( + im, + centre_point, + ImageColor.getrgb("red"), + border=ImageColor.getrgb("black"), + ) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png") + + +def test_floodfill_thresh(): + # floodfill() is experimental + + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + draw.rectangle(BBOX2, outline="darkgreen", fill="green") + centre_point = (int(W / 2), int(H / 2)) + + # Act + ImageDraw.floodfill(im, centre_point, ImageColor.getrgb("red"), thresh=30) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png") + + +def test_floodfill_not_negative(): + # floodfill() is experimental + # Test that floodfill does not extend into negative coordinates + + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + draw.line((W / 2, 0, W / 2, H / 2), fill="green") + draw.line((0, H / 2, W / 2, H / 2), fill="green") + + # Act + ImageDraw.floodfill(im, (int(W / 4), int(H / 4)), ImageColor.getrgb("red")) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill_not_negative.png") + + +def create_base_image_draw( + size, mode=DEFAULT_MODE, background1=WHITE, background2=GRAY +): + img = Image.new(mode, size, background1) + for x in range(0, size[0]): + for y in range(0, size[1]): + if (x + y) % 2 == 0: + img.putpixel((x, y), background2) + return img, ImageDraw.Draw(img) + + +def test_square(): + expected = os.path.join(IMAGES_PATH, "square.png") + img, draw = create_base_image_draw((10, 10)) + draw.polygon([(2, 2), (2, 7), (7, 7), (7, 2)], BLACK) + assert_image_equal_tofile(img, expected, "square as normal polygon failed") + img, draw = create_base_image_draw((10, 10)) + draw.polygon([(7, 7), (7, 2), (2, 2), (2, 7)], BLACK) + assert_image_equal_tofile(img, expected, "square as inverted polygon failed") + img, draw = create_base_image_draw((10, 10)) + draw.rectangle((2, 2, 7, 7), BLACK) + assert_image_equal_tofile(img, expected, "square as normal rectangle failed") + img, draw = create_base_image_draw((10, 10)) + draw.rectangle((7, 7, 2, 2), BLACK) + assert_image_equal_tofile(img, expected, "square as inverted rectangle failed") + + +def test_triangle_right(): + img, draw = create_base_image_draw((20, 20)) + draw.polygon([(3, 5), (17, 5), (10, 12)], BLACK) + assert_image_equal_tofile( + img, os.path.join(IMAGES_PATH, "triangle_right.png"), "triangle right failed" + ) + + +@pytest.mark.parametrize( + "fill, suffix", + ((BLACK, "width"), (None, "width_no_fill")), +) +def test_triangle_right_width(fill, suffix): + img, draw = create_base_image_draw((100, 100)) + draw.polygon([(15, 25), (85, 25), (50, 60)], fill, WHITE, width=5) + assert_image_equal_tofile( + img, os.path.join(IMAGES_PATH, "triangle_right_" + suffix + ".png") + ) + + +def test_line_horizontal(): + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 5, 14, 5), BLACK, 2) + assert_image_equal_tofile( + img, + os.path.join(IMAGES_PATH, "line_horizontal_w2px_normal.png"), + "line straight horizontal normal 2px wide failed", + ) + + img, draw = create_base_image_draw((20, 20)) + draw.line((14, 5, 5, 5), BLACK, 2) + assert_image_equal_tofile( + img, + os.path.join(IMAGES_PATH, "line_horizontal_w2px_inverted.png"), + "line straight horizontal inverted 2px wide failed", + ) + + expected = os.path.join(IMAGES_PATH, "line_horizontal_w3px.png") + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 5, 14, 5), BLACK, 3) + assert_image_equal_tofile( + img, expected, "line straight horizontal normal 3px wide failed" + ) + img, draw = create_base_image_draw((20, 20)) + draw.line((14, 5, 5, 5), BLACK, 3) + assert_image_equal_tofile( + img, expected, "line straight horizontal inverted 3px wide failed" + ) + + img, draw = create_base_image_draw((200, 110)) + draw.line((5, 55, 195, 55), BLACK, 101) + assert_image_equal_tofile( + img, + os.path.join(IMAGES_PATH, "line_horizontal_w101px.png"), + "line straight horizontal 101px wide failed", + ) + + +def test_line_h_s1_w2(): + pytest.skip("failing") + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 5, 14, 6), BLACK, 2) + assert_image_equal_tofile( + img, + os.path.join(IMAGES_PATH, "line_horizontal_slope1px_w2px.png"), + "line horizontal 1px slope 2px wide failed", + ) + + +def test_line_vertical(): + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 5, 5, 14), BLACK, 2) + assert_image_equal_tofile( + img, + os.path.join(IMAGES_PATH, "line_vertical_w2px_normal.png"), + "line straight vertical normal 2px wide failed", + ) + + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 14, 5, 5), BLACK, 2) + assert_image_equal_tofile( + img, + os.path.join(IMAGES_PATH, "line_vertical_w2px_inverted.png"), + "line straight vertical inverted 2px wide failed", + ) + + expected = os.path.join(IMAGES_PATH, "line_vertical_w3px.png") + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 5, 5, 14), BLACK, 3) + assert_image_equal_tofile( + img, expected, "line straight vertical normal 3px wide failed" + ) + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 14, 5, 5), BLACK, 3) + assert_image_equal_tofile( + img, expected, "line straight vertical inverted 3px wide failed" + ) + + img, draw = create_base_image_draw((110, 200)) + draw.line((55, 5, 55, 195), BLACK, 101) + assert_image_equal_tofile( + img, + os.path.join(IMAGES_PATH, "line_vertical_w101px.png"), + "line straight vertical 101px wide failed", + ) + + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 5, 6, 14), BLACK, 2) + assert_image_equal_tofile( + img, + os.path.join(IMAGES_PATH, "line_vertical_slope1px_w2px.png"), + "line vertical 1px slope 2px wide failed", + ) + + +def test_line_oblique_45(): + expected = os.path.join(IMAGES_PATH, "line_oblique_45_w3px_a.png") + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 5, 14, 14), BLACK, 3) + assert_image_equal_tofile(img, expected, "line oblique 45 normal 3px wide A failed") + img, draw = create_base_image_draw((20, 20)) + draw.line((14, 14, 5, 5), BLACK, 3) + assert_image_equal_tofile( + img, expected, "line oblique 45 inverted 3px wide A failed" + ) + + expected = os.path.join(IMAGES_PATH, "line_oblique_45_w3px_b.png") + img, draw = create_base_image_draw((20, 20)) + draw.line((14, 5, 5, 14), BLACK, 3) + assert_image_equal_tofile(img, expected, "line oblique 45 normal 3px wide B failed") + img, draw = create_base_image_draw((20, 20)) + draw.line((5, 14, 14, 5), BLACK, 3) + assert_image_equal_tofile( + img, expected, "line oblique 45 inverted 3px wide B failed" + ) + + +def test_wide_line_dot(): + # Test drawing a wide "line" from one point to another just draws a single point + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.line([(50, 50), (50, 50)], width=3) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_wide_line_dot.png", 1) + + +def test_wide_line_larger_than_int(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_wide_line_larger_than_int.png" + + # Act + draw.line([(0, 0), (32768, 32768)], width=3) + + # Assert + assert_image_similar_tofile(im, expected, 1) + + +@pytest.mark.parametrize( + "xy", + [ + [ + (400, 280), + (380, 280), + (450, 280), + (440, 120), + (350, 200), + (310, 280), + (300, 280), + (250, 280), + (250, 200), + (150, 200), + (150, 260), + (50, 200), + (150, 50), + (250, 100), + ], + ( + 400, + 280, + 380, + 280, + 450, + 280, + 440, + 120, + 350, + 200, + 310, + 280, + 300, + 280, + 250, + 280, + 250, + 200, + 150, + 200, + 150, + 260, + 50, + 200, + 150, + 50, + 250, + 100, + ), + [ + 400, + 280, + 380, + 280, + 450, + 280, + 440, + 120, + 350, + 200, + 310, + 280, + 300, + 280, + 250, + 280, + 250, + 200, + 150, + 200, + 150, + 260, + 50, + 200, + 150, + 50, + 250, + 100, + ], + ], +) +def test_line_joint(xy): + im = Image.new("RGB", (500, 325)) + draw = ImageDraw.Draw(im) + + # Act + draw.line(xy, GRAY, 50, "curve") + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_line_joint_curve.png", 3) + + +def test_textsize_empty_string(): + # https://github.com/python-pillow/Pillow/issues/2783 + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + # Should not cause 'SystemError: returned NULL without setting an error' + draw.textbbox((0, 0), "") + draw.textbbox((0, 0), "\n") + draw.textbbox((0, 0), "test\n") + draw.textlength("") + + +@skip_unless_feature("freetype2") +def test_textbbox_stroke(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) + + # Act / Assert + assert draw.textbbox((2, 2), "A", font, stroke_width=2) == (0, 4, 16, 20) + assert draw.textbbox((2, 2), "A", font, stroke_width=4) == (-2, 2, 18, 22) + assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=2) == (0, 4, 52, 44) + assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=4) == (-2, 2, 54, 50) + + +def test_textsize_deprecation(): + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + with pytest.warns(DeprecationWarning) as log: + draw.textsize("Hello") + assert len(log) == 1 + with pytest.warns(DeprecationWarning) as log: + draw.textsize("Hello\nWorld") + assert len(log) == 1 + with pytest.warns(DeprecationWarning) as log: + draw.multiline_textsize("Hello\nWorld") + assert len(log) == 1 + + +@skip_unless_feature("freetype2") +def test_stroke(): + for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items(): # Arrange - im = Image.new("RGB", (W, H)) + im = Image.new("RGB", (120, 130)) draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) # Act - # Should not cause 'SystemError: returned NULL without setting an error' - draw.textsize("") - draw.textsize("\n") - draw.textsize("test\n") - + draw.text((12, 12), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill) -if __name__ == '__main__': - unittest.main() + # Assert + assert_image_similar_tofile( + im, "Tests/images/imagedraw_stroke_" + suffix + ".png", 3.1 + ) + + +@skip_unless_feature("freetype2") +def test_stroke_descender(): + # Arrange + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + draw.text((12, 2), "y", "#f00", font, stroke_width=2, stroke_fill="#0f0") + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76) + + +@skip_unless_feature("freetype2") +def test_stroke_multiline(): + # Arrange + im = Image.new("RGB", (100, 250)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + draw.multiline_text( + (12, 12), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0" + ) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3) + + +def test_setting_default_font(): + # Arrange + im = Image.new("RGB", (100, 250)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + ImageDraw.ImageDraw.font = font + + # Assert + try: + assert draw.getfont() == font + finally: + ImageDraw.ImageDraw.font = None + assert isinstance(draw.getfont(), ImageFont.ImageFont) + + +def test_same_color_outline(): + # Prepare shape + x0, y0 = 5, 5 + x1, y1 = 5, 50 + x2, y2 = 95, 50 + x3, y3 = 95, 5 + + s = ImageDraw.Outline() + s.move(x0, y0) + s.curve(x1, y1, x2, y2, x3, y3) + s.line(x0, y0) + + # Begin + for mode in ["RGB", "L"]: + for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]: + for operation, args in { + "chord": [BBOX1, 0, 180], + "ellipse": [BBOX1], + "shape": [s], + "pieslice": [BBOX1, -90, 45], + "polygon": [[(18, 30), (85, 30), (60, 72)]], + "rectangle": [BBOX1], + }.items(): + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw_method = getattr(draw, operation) + args += [fill, outline] + draw_method(*args) + + # Assert + expected = f"Tests/images/imagedraw_outline_{operation}_{mode}.png" + assert_image_similar_tofile(im, expected, 1) + + +@pytest.mark.parametrize( + "n_sides, rotation, polygon_name", + [(4, 0, "square"), (8, 0, "regular_octagon"), (4, 45, "square")], +) +def test_draw_regular_polygon(n_sides, rotation, polygon_name): + im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) + filename_base = f"Tests/images/imagedraw_{polygon_name}" + filename = ( + f"{filename_base}.png" + if rotation == 0 + else f"{filename_base}_rotate_{rotation}.png" + ) + draw = ImageDraw.Draw(im) + bounding_circle = ((W // 2, H // 2), 25) + draw.regular_polygon(bounding_circle, n_sides, rotation=rotation, fill="red") + assert_image_equal_tofile(im, filename) + + +@pytest.mark.parametrize( + "n_sides, expected_vertices", + [ + (3, [(28.35, 62.5), (71.65, 62.5), (50.0, 25.0)]), + (4, [(32.32, 67.68), (67.68, 67.68), (67.68, 32.32), (32.32, 32.32)]), + ( + 5, + [ + (35.31, 70.23), + (64.69, 70.23), + (73.78, 42.27), + (50.0, 25.0), + (26.22, 42.27), + ], + ), + ( + 6, + [ + (37.5, 71.65), + (62.5, 71.65), + (75.0, 50.0), + (62.5, 28.35), + (37.5, 28.35), + (25.0, 50.0), + ], + ), + ], +) +def test_compute_regular_polygon_vertices(n_sides, expected_vertices): + bounding_circle = (W // 2, H // 2, 25) + vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0) + assert vertices == expected_vertices + + +@pytest.mark.parametrize( + "n_sides, bounding_circle, rotation, expected_error, error_message", + [ + (None, (50, 50, 25), 0, TypeError, "n_sides should be an int"), + (1, (50, 50, 25), 0, ValueError, "n_sides should be an int > 2"), + (3, 50, 0, TypeError, "bounding_circle should be a tuple"), + ( + 3, + (50, 50, 100, 100), + 0, + ValueError, + "bounding_circle should contain 2D coordinates " + "and a radius (e.g. (x, y, r) or ((x, y), r) )", + ), + ( + 3, + (50, 50, "25"), + 0, + ValueError, + "bounding_circle should only contain numeric data", + ), + ( + 3, + ((50, 50, 50), 25), + 0, + ValueError, + "bounding_circle centre should contain 2D coordinates (e.g. (x, y))", + ), + ( + 3, + (50, 50, 0), + 0, + ValueError, + "bounding_circle radius should be > 0", + ), + ( + 3, + (50, 50, 25), + "0", + ValueError, + "rotation should be an int or float", + ), + ], +) +def test_compute_regular_polygon_vertices_input_error_handling( + n_sides, bounding_circle, rotation, expected_error, error_message +): + with pytest.raises(expected_error) as e: + ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) + assert str(e.value) == error_message + + +def test_continuous_horizontal_edges_polygon(): + xy = [ + (2, 6), + (6, 6), + (12, 6), + (12, 12), + (8, 12), + (8, 8), + (4, 8), + (2, 8), + ] + img, draw = create_base_image_draw((16, 16)) + draw.polygon(xy, BLACK) + expected = os.path.join(IMAGES_PATH, "continuous_horizontal_edges_polygon.png") + assert_image_equal_tofile( + img, expected, "continuous horizontal edges polygon failed" + ) + + +def test_discontiguous_corners_polygon(): + img, draw = create_base_image_draw((84, 68)) + draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK) + draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK) + draw.polygon( + ((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)), + BLACK, + ) + expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png") + assert_image_similar_tofile(img, expected, 1) + + +def test_polygon2(): + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red") + expected = "Tests/images/imagedraw_outline_polygon_RGB.png" + assert_image_similar_tofile(im, expected, 1) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py new file mode 100644 index 00000000000..6fc829f1a54 --- /dev/null +++ b/Tests/test_imagedraw2.py @@ -0,0 +1,217 @@ +import os.path + +import pytest + +from PIL import Image, ImageDraw, ImageDraw2 + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar_tofile, + hopper, + skip_unless_feature, +) + +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +GRAY = (190, 190, 190) +DEFAULT_MODE = "RGB" +IMAGES_PATH = os.path.join("Tests", "images", "imagedraw") + +# Image size +W, H = 100, 100 + +# Bounding box points +X0 = int(W / 4) +X1 = int(X0 * 3) +Y0 = int(H / 4) +Y1 = int(X0 * 3) + +# Two kinds of bounding box +BBOX1 = [(X0, Y0), (X1, Y1)] +BBOX2 = [X0, Y0, X1, Y1] + +# Two kinds of coordinate sequences +POINTS1 = [(10, 10), (20, 40), (30, 30)] +POINTS2 = [10, 10, 20, 40, 30, 30] + +KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] + +FONT_PATH = "Tests/fonts/FreeMono.ttf" + + +def test_sanity(): + im = hopper("RGB").copy() + + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue", width=7) + draw.line(list(range(10)), pen) + + draw, handler = ImageDraw.getdraw(im) + pen = ImageDraw2.Pen("blue", width=7) + draw.line(list(range(10)), pen) + + +@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +def test_ellipse(bbox): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue", width=2) + brush = ImageDraw2.Brush("green") + + # Act + draw.ellipse(bbox, pen, brush) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_RGB.png", 1) + + +def test_ellipse_edge(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + brush = ImageDraw2.Brush("white") + + # Act + draw.ellipse(((0, 0), (W - 1, H - 1)), brush) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) + + +@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +def test_line(points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("yellow", width=2) + + # Act + draw.line(points, pen) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") + + +def test_line_pen_as_brush(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = None + brush = ImageDraw2.Pen("yellow", width=2) + + # Act + # Pass in the pen as the brush parameter + draw.line(POINTS1, pen, brush) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") + + +@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +def test_polygon(points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue", width=2) + brush = ImageDraw2.Brush("red") + + # Act + draw.polygon(points, pen, brush) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") + + +@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +def test_rectangle(bbox): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("green", width=2) + brush = ImageDraw2.Brush("black") + + # Act + draw.rectangle(bbox, pen, brush) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") + + +def test_big_rectangle(): + # Test drawing a rectangle bigger than the image + # Arrange + im = Image.new("RGB", (W, H)) + bbox = [(-1, -1), (W + 1, H + 1)] + brush = ImageDraw2.Brush("orange") + draw = ImageDraw2.Draw(im) + expected = "Tests/images/imagedraw_big_rectangle.png" + + # Act + draw.rectangle(bbox, brush) + + # Assert + assert_image_similar_tofile(im, expected, 1) + + +@skip_unless_feature("freetype2") +def test_text(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + expected = "Tests/images/imagedraw2_text.png" + + # Act + draw.text((5, 5), "ImageDraw2", font) + + # Assert + assert_image_similar_tofile(im, expected, 13) + + +@skip_unless_feature("freetype2") +def test_textsize(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + + # Act + with pytest.warns(DeprecationWarning) as log: + size = draw.textsize("ImageDraw2", font) + assert len(log) == 1 + + # Assert + assert size[1] == 12 + + +@skip_unless_feature("freetype2") +def test_textsize_empty_string(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + + # Act + # Should not cause 'SystemError: returned NULL without setting an error' + draw.textbbox((0, 0), "", font) + draw.textbbox((0, 0), "\n", font) + draw.textbbox((0, 0), "test\n", font) + draw.textlength("", font) + + +@skip_unless_feature("freetype2") +def test_flush(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + + # Act + draw.text((5, 5), "ImageDraw2", font) + im2 = draw.flush() + + # Assert + assert_image_equal(im, im2) diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index e9727613b60..221ef8cdb26 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -1,53 +1,57 @@ -from helper import unittest, PillowTestCase, hopper +import pytest -from PIL import Image -from PIL import ImageEnhance +from PIL import Image, ImageEnhance +from .helper import assert_image_equal, hopper -class TestImageEnhance(PillowTestCase): - def test_sanity(self): +def test_sanity(): + # FIXME: assert_image + # Implicit asserts no exception: + ImageEnhance.Color(hopper()).enhance(0.5) + ImageEnhance.Contrast(hopper()).enhance(0.5) + ImageEnhance.Brightness(hopper()).enhance(0.5) + ImageEnhance.Sharpness(hopper()).enhance(0.5) - # FIXME: assert_image - # Implicit asserts no exception: - ImageEnhance.Color(hopper()).enhance(0.5) - ImageEnhance.Contrast(hopper()).enhance(0.5) - ImageEnhance.Brightness(hopper()).enhance(0.5) - ImageEnhance.Sharpness(hopper()).enhance(0.5) - def test_crash(self): +def test_crash(): + # crashes on small images + im = Image.new("RGB", (1, 1)) + ImageEnhance.Sharpness(im).enhance(0.5) - # crashes on small images - im = Image.new("RGB", (1, 1)) - ImageEnhance.Sharpness(im).enhance(0.5) - def _half_transparent_image(self): - # returns an image, half transparent, half solid - im = hopper('RGB') +def _half_transparent_image(): + # returns an image, half transparent, half solid + im = hopper("RGB") - transparent = Image.new('L', im.size, 0) - solid = Image.new('L', (im.size[0]//2, im.size[1]), 255) - transparent.paste(solid, (0, 0)) - im.putalpha(transparent) + transparent = Image.new("L", im.size, 0) + solid = Image.new("L", (im.size[0] // 2, im.size[1]), 255) + transparent.paste(solid, (0, 0)) + im.putalpha(transparent) - return im + return im - def _check_alpha(self, im, original, op, amount): - self.assertEqual(im.getbands(), original.getbands()) - self.assert_image_equal(im.getchannel('A'), original.getchannel('A'), - "Diff on %s: %s" % (op, amount)) - def test_alpha(self): - # Issue https://github.com/python-pillow/Pillow/issues/899 - # Is alpha preserved through image enhancement? +def _check_alpha(im, original, op, amount): + assert im.getbands() == original.getbands() + assert_image_equal( + im.getchannel("A"), + original.getchannel("A"), + f"Diff on {op}: {amount}", + ) - original = self._half_transparent_image() - for op in ['Color', 'Brightness', 'Contrast', 'Sharpness']: - for amount in [0, 0.5, 1.0]: - self._check_alpha(getattr(ImageEnhance, op)(original).enhance(amount), - original, op, amount) +@pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness")) +def test_alpha(op): + # Issue https://github.com/python-pillow/Pillow/issues/899 + # Is alpha preserved through image enhancement? + original = _half_transparent_image() -if __name__ == '__main__': - unittest.main() + for amount in [0, 0.5, 1.0]: + _check_alpha( + getattr(ImageEnhance, op)(original).enhance(amount), + original, + op, + amount, + ) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index b50dcc94384..fc0fbfb9bbc 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,26 +1,37 @@ -from helper import unittest, PillowTestCase, hopper, fromstring, tostring - from io import BytesIO -from PIL import Image -from PIL import ImageFile -from PIL import EpsImagePlugin - - -codecs = dir(Image.core) +import pytest + +from PIL import ( + BmpImagePlugin, + EpsImagePlugin, + Image, + ImageFile, + UnidentifiedImageError, + _binary, + features, +) + +from .helper import ( + assert_image, + assert_image_equal, + assert_image_similar, + fromstring, + hopper, + skip_unless_feature, + tostring, +) # save original block sizes MAXBLOCK = ImageFile.MAXBLOCK SAFEBLOCK = ImageFile.SAFEBLOCK -class TestImageFile(PillowTestCase): - +class TestImageFile: def test_parser(self): - def roundtrip(format): - im = hopper("L").resize((1000, 1000)) + im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): im = im.convert("1") @@ -32,27 +43,27 @@ def roundtrip(format): parser = ImageFile.Parser() parser.feed(data) - imOut = parser.close() + im_out = parser.close() - return im, imOut + return im, im_out - self.assert_image_equal(*roundtrip("BMP")) + assert_image_equal(*roundtrip("BMP")) im1, im2 = roundtrip("GIF") - self.assert_image_similar(im1.convert('P'), im2, 1) - self.assert_image_equal(*roundtrip("IM")) - self.assert_image_equal(*roundtrip("MSP")) - if "zip_encoder" in codecs: + assert_image_similar(im1.convert("P"), im2, 1) + assert_image_equal(*roundtrip("IM")) + assert_image_equal(*roundtrip("MSP")) + if features.check("zlib"): try: # force multiple blocks in PNG driver ImageFile.MAXBLOCK = 8192 - self.assert_image_equal(*roundtrip("PNG")) + assert_image_equal(*roundtrip("PNG")) finally: ImageFile.MAXBLOCK = MAXBLOCK - self.assert_image_equal(*roundtrip("PPM")) - self.assert_image_equal(*roundtrip("TIFF")) - self.assert_image_equal(*roundtrip("XBM")) - self.assert_image_equal(*roundtrip("TGA")) - self.assert_image_equal(*roundtrip("PCX")) + assert_image_equal(*roundtrip("PPM")) + assert_image_equal(*roundtrip("TIFF")) + assert_image_equal(*roundtrip("XBM")) + assert_image_equal(*roundtrip("TGA")) + assert_image_equal(*roundtrip("PCX")) if EpsImagePlugin.has_ghostscript(): im1, im2 = roundtrip("EPS") @@ -63,25 +74,37 @@ def roundtrip(format): # md5sum: ba974835ff2d6f3f2fd0053a23521d4a # EPS comes back in RGB: - self.assert_image_similar(im1, im2.convert('L'), 20) + assert_image_similar(im1, im2.convert("L"), 20) - if "jpeg_encoder" in codecs: + if features.check("jpg"): im1, im2 = roundtrip("JPEG") # lossy compression - self.assert_image(im1, im2.mode, im2.size) + assert_image(im1, im2.mode, im2.size) - self.assertRaises(IOError, roundtrip, "PDF") + with pytest.raises(OSError): + roundtrip("PDF") def test_ico(self): - with open('Tests/images/python.ico', 'rb') as f: + with open("Tests/images/python.ico", "rb") as f: data = f.read() with ImageFile.Parser() as p: p.feed(data) - self.assertEqual((48, 48), p.image.size) + assert (48, 48) == p.image.size - def test_safeblock(self): - if "zip_encoder" not in codecs: - self.skipTest("PNG (zlib) encoder not available") + @skip_unless_feature("webp") + @skip_unless_feature("webp_anim") + def test_incremental_webp(self): + with ImageFile.Parser() as p: + with open("Tests/images/hopper.webp", "rb") as f: + p.feed(f.read(1024)) + + # Check that insufficient data was given in the first feed + assert not p.image + p.feed(f.read()) + assert (128, 128) == p.image.size + + @skip_unless_feature("zlib") + def test_safeblock(self): im1 = hopper() try: @@ -90,137 +113,279 @@ def test_safeblock(self): finally: ImageFile.SAFEBLOCK = SAFEBLOCK - self.assert_image_equal(im1, im2) + assert_image_equal(im1, im2) - def test_raise_ioerror(self): - self.assertRaises(IOError, ImageFile.raise_ioerror, 1) + def test_raise_oserror(self): + with pytest.raises(OSError): + ImageFile.raise_oserror(1) def test_raise_typeerror(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): parser = ImageFile.Parser() parser.feed(1) + def test_negative_stride(self): + with open("Tests/images/raw_negative_stride.bin", "rb") as f: + input = f.read() + p = ImageFile.Parser() + p.feed(input) + with pytest.raises(OSError): + p.close() + + def test_no_format(self): + buf = BytesIO(b"\x00" * 255) + + class DummyImageFile(ImageFile.ImageFile): + def _open(self): + self.mode = "RGB" + self._size = (1, 1) + + im = DummyImageFile(buf) + assert im.format is None + assert im.get_format_mimetype() is None + + def test_oserror(self): + im = Image.new("RGB", (1, 1)) + with pytest.raises(OSError): + im.save(BytesIO(), "JPEG2000", num_resolutions=2) + + def test_truncated(self): + b = BytesIO( + b"BM000000000000" # head_data + + _binary.o32le( + ImageFile.SAFEBLOCK + 1 + 4 + ) # header_size, so BmpImagePlugin will try to read SAFEBLOCK + 1 bytes + + ( + b"0" * ImageFile.SAFEBLOCK + ) # only SAFEBLOCK bytes, so that the header is truncated + ) + with pytest.raises(OSError) as e: + BmpImagePlugin.BmpImageFile(b) + assert str(e.value) == "Truncated File Read" + + @skip_unless_feature("zlib") def test_truncated_with_errors(self): - if "zip_encoder" not in codecs: - self.skipTest("PNG (zlib) encoder not available") + with Image.open("Tests/images/truncated_image.png") as im: + with pytest.raises(OSError): + im.load() - im = Image.open("Tests/images/truncated_image.png") - with self.assertRaises(IOError): - im.load() + # Test that the error is raised if loaded a second time + with pytest.raises(OSError): + im.load() + @skip_unless_feature("zlib") def test_truncated_without_errors(self): - if "zip_encoder" not in codecs: - self.skipTest("PNG (zlib) encoder not available") - - im = Image.open("Tests/images/truncated_image.png") - - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - im.load() - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + with Image.open("Tests/images/truncated_image.png") as im: + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + im.load() + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False + @skip_unless_feature("zlib") def test_broken_datastream_with_errors(self): - if "zip_encoder" not in codecs: - self.skipTest("PNG (zlib) encoder not available") - - im = Image.open("Tests/images/broken_data_stream.png") - with self.assertRaises(IOError): - im.load() + with Image.open("Tests/images/broken_data_stream.png") as im: + with pytest.raises(OSError): + im.load() + @skip_unless_feature("zlib") def test_broken_datastream_without_errors(self): - if "zip_encoder" not in codecs: - self.skipTest("PNG (zlib) encoder not available") - - im = Image.open("Tests/images/broken_data_stream.png") - - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - im.load() - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + with Image.open("Tests/images/broken_data_stream.png") as im: + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + im.load() + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False class MockPyDecoder(ImageFile.PyDecoder): def decode(self, buffer): # eof - return (-1, 0) + return -1, 0 + + +class MockPyEncoder(ImageFile.PyEncoder): + def encode(self, buffer): + return 1, 1, b"" + + def cleanup(self): + self.cleanup_called = True + xoff, yoff, xsize, ysize = 10, 20, 100, 100 class MockImageFile(ImageFile.ImageFile): def _open(self): - self.rawmode = 'RGBA' - self.mode = 'RGBA' - self.size = (200, 200) - self.tile = [("MOCK", (xoff, yoff, xoff+xsize, yoff+ysize), 32, None)] + self.rawmode = "RGBA" + self.mode = "RGBA" + self._size = (200, 200) + self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] -class TestPyDecoder(PillowTestCase): +class CodecsTest: + @classmethod + def setup_class(cls): + cls.decoder = MockPyDecoder(None) + cls.encoder = MockPyEncoder(None) - def get_decoder(self): - decoder = MockPyDecoder(None) + def decoder_closure(mode, *args): + cls.decoder.__init__(mode, *args) + return cls.decoder - def closure(mode, *args): - decoder.__init__(mode, *args) - return decoder + def encoder_closure(mode, *args): + cls.encoder.__init__(mode, *args) + return cls.encoder - Image.register_decoder('MOCK', closure) - return decoder + Image.register_decoder("MOCK", decoder_closure) + Image.register_encoder("MOCK", encoder_closure) + +class TestPyDecoder(CodecsTest): def test_setimage(self): - buf = BytesIO(b'\x00'*255) + buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - d = self.get_decoder() im.load() - self.assertEqual(d.state.xoff, xoff) - self.assertEqual(d.state.yoff, yoff) - self.assertEqual(d.state.xsize, xsize) - self.assertEqual(d.state.ysize, ysize) + assert self.decoder.state.xoff == xoff + assert self.decoder.state.yoff == yoff + assert self.decoder.state.xsize == xsize + assert self.decoder.state.ysize == ysize - self.assertRaises(ValueError, d.set_as_raw, b'\x00') + with pytest.raises(ValueError): + self.decoder.set_as_raw(b"\x00") def test_extents_none(self): - buf = BytesIO(b'\x00'*255) + buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) im.tile = [("MOCK", None, 32, None)] - d = self.get_decoder() im.load() - self.assertEqual(d.state.xoff, 0) - self.assertEqual(d.state.yoff, 0) - self.assertEqual(d.state.xsize, 200) - self.assertEqual(d.state.ysize, 200) + assert self.decoder.state.xoff == 0 + assert self.decoder.state.yoff == 0 + assert self.decoder.state.xsize == 200 + assert self.decoder.state.ysize == 200 def test_negsize(self): - buf = BytesIO(b'\x00'*255) + buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - im.tile = [("MOCK", (xoff, yoff, -10, yoff+ysize), 32, None)] - d = self.get_decoder() + im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)] - self.assertRaises(ValueError, im.load) + with pytest.raises(ValueError): + im.load() - im.tile = [("MOCK", (xoff, yoff, xoff+xsize, -10), 32, None)] - self.assertRaises(ValueError, im.load) + im.tile = [("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)] + with pytest.raises(ValueError): + im.load() def test_oversize(self): - buf = BytesIO(b'\x00'*255) + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)] + + with pytest.raises(ValueError): + im.load() + + im.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None)] + with pytest.raises(ValueError): + im.load() + + def test_decode(self): + decoder = ImageFile.PyDecoder(None) + with pytest.raises(NotImplementedError): + decoder.decode(None) + + +class TestPyEncoder(CodecsTest): + def test_setimage(self): + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + + fp = BytesIO() + ImageFile._save( + im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] + ) + + assert self.encoder.state.xoff == xoff + assert self.encoder.state.yoff == yoff + assert self.encoder.state.xsize == xsize + assert self.encoder.state.ysize == ysize + + def test_extents_none(self): + buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - im.tile = [("MOCK", (xoff, yoff, xoff+xsize + 100, yoff+ysize), 32, None)] - d = self.get_decoder() + im.tile = [("MOCK", None, 32, None)] + + fp = BytesIO() + ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) - self.assertRaises(ValueError, im.load) + assert self.encoder.state.xoff == 0 + assert self.encoder.state.yoff == 0 + assert self.encoder.state.xsize == 200 + assert self.encoder.state.ysize == 200 + + def test_negsize(self): + buf = BytesIO(b"\x00" * 255) - im.tile = [("MOCK", (xoff, yoff, xoff+xsize, yoff+ysize + 100), 32, None)] - self.assertRaises(ValueError, im.load) + im = MockImageFile(buf) + + fp = BytesIO() + self.encoder.cleanup_called = False + with pytest.raises(ValueError): + ImageFile._save( + im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] + ) + assert self.encoder.cleanup_called + + with pytest.raises(ValueError): + ImageFile._save( + im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] + ) + + def test_oversize(self): + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) -if __name__ == '__main__': - unittest.main() + fp = BytesIO() + with pytest.raises(ValueError): + ImageFile._save( + im, + fp, + [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")], + ) + + with pytest.raises(ValueError): + ImageFile._save( + im, + fp, + [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], + ) + + def test_encode(self): + encoder = ImageFile.PyEncoder(None) + with pytest.raises(NotImplementedError): + encoder.encode(None) + + bytes_consumed, errcode = encoder.encode_to_pyfd() + assert bytes_consumed == 0 + assert ImageFile.ERRORS[errcode] == "bad configuration" + + encoder._pushes_fd = True + with pytest.raises(NotImplementedError): + encoder.encode_to_pyfd() + + with pytest.raises(NotImplementedError): + encoder.encode_to_file(None, None) + + def test_zero_height(self): + with pytest.raises(UnidentifiedImageError): + Image.open("Tests/images/zero_height.j2k") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index c437e76b29a..306a2f1bff6 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1,519 +1,1142 @@ -# -*- coding: utf-8 -*- -from helper import unittest, PillowTestCase - -from PIL import Image, ImageDraw, ImageFont, features -from io import BytesIO +import copy import os +import re +import shutil import sys -import copy +from io import BytesIO + +import pytest +from packaging.version import parse as parse_version + +from PIL import Image, ImageDraw, ImageFont, features + +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar_tofile, + is_win32, + skip_unless_feature, + skip_unless_feature_version, +) FONT_PATH = "Tests/fonts/FreeMono.ttf" FONT_SIZE = 20 TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward" -HAS_FREETYPE = features.check('freetype2') -HAS_RAQM = features.check('raqm') +pytestmark = skip_unless_feature("freetype2") -class SimplePatcher(object): - def __init__(self, parent_obj, attr_name, value): - self._parent_obj = parent_obj - self._attr_name = attr_name - self._saved = None - self._is_saved = False - self._value = value - def __enter__(self): - # Patch the attr on the object - if hasattr(self._parent_obj, self._attr_name): - self._saved = getattr(self._parent_obj, self._attr_name) - setattr(self._parent_obj, self._attr_name, self._value) - self._is_saved = True - else: - setattr(self._parent_obj, self._attr_name, self._value) - self._is_saved = False +def test_sanity(): + assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2")) - def __exit__(self, type, value, traceback): - # Restore the original value - if self._is_saved: - setattr(self._parent_obj, self._attr_name, self._saved) - else: - delattr(self._parent_obj, self._attr_name) - - -@unittest.skipUnless(HAS_FREETYPE, "ImageFont not Available") -class TestImageFont(PillowTestCase): - LAYOUT_ENGINE = ImageFont.LAYOUT_BASIC - - # Freetype has different metrics depending on the version. - # (and, other things, but first things first) - METRICS = { - ('2', '3'): {'multiline': 30, - 'textsize': 12, - 'getters': (13, 16)}, - ('2', '7'): {'multiline': 6.2, - 'textsize': 2.5, - 'getters': (12, 16)}, - ('2', '8'): {'multiline': 6.2, - 'textsize': 2.5, - 'getters': (12, 16)}, - 'Default': {'multiline': 0.5, - 'textsize': 0.5, - 'getters': (12, 16)}, - } - - def setUp(self): - freetype_version = tuple(ImageFont.core.freetype2_version.split('.'))[:2] - self.metrics = self.METRICS.get(freetype_version, self.METRICS['Default']) - - def get_font(self): - return ImageFont.truetype(FONT_PATH, FONT_SIZE, - layout_engine=self.LAYOUT_ENGINE) - - def test_sanity(self): - self.assertRegexpMatches( - ImageFont.core.freetype2_version, r"\d+\.\d+\.\d+$") - - def test_font_properties(self): - ttf = self.get_font() - self.assertEqual(ttf.path, FONT_PATH) - self.assertEqual(ttf.size, FONT_SIZE) - - ttf_copy = ttf.font_variant() - self.assertEqual(ttf_copy.path, FONT_PATH) - self.assertEqual(ttf_copy.size, FONT_SIZE) - - ttf_copy = ttf.font_variant(size=FONT_SIZE+1) - self.assertEqual(ttf_copy.size, FONT_SIZE+1) - - second_font_path = "Tests/fonts/DejaVuSans.ttf" - ttf_copy = ttf.font_variant(font=second_font_path) - self.assertEqual(ttf_copy.path, second_font_path) - - def test_font_with_name(self): - self.get_font() - self._render(FONT_PATH) - - def _font_as_bytes(self): - with open(FONT_PATH, 'rb') as f: + +@pytest.fixture( + scope="module", + params=[ + pytest.param(ImageFont.Layout.BASIC), + pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), + ], +) +def layout_engine(request): + return request.param + + +@pytest.fixture(scope="module") +def font(layout_engine): + return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine) + + +def test_font_properties(font): + assert font.path == FONT_PATH + assert font.size == FONT_SIZE + + font_copy = font.font_variant() + assert font_copy.path == FONT_PATH + assert font_copy.size == FONT_SIZE + + font_copy = font.font_variant(size=FONT_SIZE + 1) + assert font_copy.size == FONT_SIZE + 1 + + second_font_path = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" + font_copy = font.font_variant(font=second_font_path) + assert font_copy.path == second_font_path + + +def _render(font, layout_engine): + txt = "Hello World!" + ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine) + ttf.getbbox(txt) + + img = Image.new("RGB", (256, 64), "white") + d = ImageDraw.Draw(img) + d.text((10, 10), txt, font=ttf, fill="black") + + return img + + +def test_font_with_name(layout_engine): + _render(FONT_PATH, layout_engine) + + +def test_font_with_filelike(layout_engine): + def _font_as_bytes(): + with open(FONT_PATH, "rb") as f: font_bytes = BytesIO(f.read()) return font_bytes - def test_font_with_filelike(self): - ImageFont.truetype(self._font_as_bytes(), FONT_SIZE, - layout_engine=self.LAYOUT_ENGINE) - self._render(self._font_as_bytes()) - # Usage note: making two fonts from the same buffer fails. - # shared_bytes = self._font_as_bytes() - # self._render(shared_bytes) - # self.assertRaises(Exception, _render, shared_bytes) - - def test_font_with_open_file(self): - with open(FONT_PATH, 'rb') as f: - self._render(f) - - def _render(self, font): - txt = "Hello World!" - ttf = ImageFont.truetype(font, FONT_SIZE, - layout_engine=self.LAYOUT_ENGINE) - ttf.getsize(txt) - - img = Image.new("RGB", (256, 64), "white") - d = ImageDraw.Draw(img) - d.text((10, 10), txt, font=ttf, fill='black') - - return img - - def test_render_equal(self): - img_path = self._render(FONT_PATH) - with open(FONT_PATH, 'rb') as f: - font_filelike = BytesIO(f.read()) - img_filelike = self._render(font_filelike) - - self.assert_image_equal(img_path, img_filelike) - - def test_textsize_equal(self): - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - ttf = self.get_font() - - txt = "Hello World!" - size = draw.textsize(txt, ttf) - draw.text((10, 10), txt, font=ttf) - draw.rectangle((10, 10, 10 + size[0], 10 + size[1])) - - target = 'Tests/images/rectangle_surrounding_text.png' - target_img = Image.open(target) - - # Epsilon ~.5 fails with FreeType 2.7 - self.assert_image_similar(im, target_img, self.metrics['textsize']) - - def test_render_multiline(self): - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - ttf = self.get_font() - line_spacing = draw.textsize('A', font=ttf)[1] + 4 - lines = TEST_TEXT.split("\n") - y = 0 - for line in lines: - draw.text((0, y), line, font=ttf) - y += line_spacing - - target = 'Tests/images/multiline_text.png' - target_img = Image.open(target) - - # some versions of freetype have different horizontal spacing. - # setting a tight epsilon, I'm showing the original test failure - # at epsilon = ~38. - self.assert_image_similar(im, target_img, self.metrics['multiline']) - - def test_render_multiline_text(self): - ttf = self.get_font() - - # Test that text() correctly connects to multiline_text() - # and that align defaults to left - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), TEST_TEXT, font=ttf) - - target = 'Tests/images/multiline_text.png' - target_img = Image.open(target) - - # Epsilon ~.5 fails with FreeType 2.7 - self.assert_image_similar(im, target_img, self.metrics['multiline']) - - # Test that text() can pass on additional arguments - # to multiline_text() - draw.text((0, 0), TEST_TEXT, fill=None, font=ttf, anchor=None, - spacing=4, align="left") - draw.text((0, 0), TEST_TEXT, None, ttf, None, 4, "left") - - # Test align center and right - for align, ext in {"center": "_center", - "right": "_right"}.items(): - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align=align) - - target = 'Tests/images/multiline_text'+ext+'.png' - target_img = Image.open(target) - - # Epsilon ~.5 fails with FreeType 2.7 - self.assert_image_similar(im, target_img, self.metrics['multiline']) - - def test_unknown_align(self): - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - ttf = self.get_font() - - # Act/Assert - self.assertRaises(AssertionError, - draw.multiline_text, (0, 0), TEST_TEXT, - font=ttf, - align="unknown") - - def test_draw_align(self): - im = Image.new('RGB', (300, 100), 'white') - draw = ImageDraw.Draw(im) - ttf = self.get_font() - line = "some text" - draw.text((100, 40), line, (0, 0, 0), font=ttf, align='left') - - def test_multiline_size(self): - ttf = self.get_font() - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) + ttf = ImageFont.truetype(_font_as_bytes(), FONT_SIZE, layout_engine=layout_engine) + ttf_copy = ttf.font_variant() + assert ttf_copy.font_bytes == ttf.font_bytes + + _render(_font_as_bytes(), layout_engine) + # Usage note: making two fonts from the same buffer fails. + # shared_bytes = _font_as_bytes() + # _render(shared_bytes) + # with pytest.raises(Exception): + # _render(shared_bytes) + + +def test_font_with_open_file(layout_engine): + with open(FONT_PATH, "rb") as f: + _render(f, layout_engine) + + +def test_render_equal(layout_engine): + img_path = _render(FONT_PATH, layout_engine) + with open(FONT_PATH, "rb") as f: + font_filelike = BytesIO(f.read()) + img_filelike = _render(font_filelike, layout_engine) + + assert_image_equal(img_path, img_filelike) + + +def test_non_ascii_path(tmp_path, layout_engine): + tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) + try: + shutil.copy(FONT_PATH, tempfile) + except UnicodeEncodeError: + pytest.skip("Non-ASCII path could not be created") + + ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine) + +def test_transparent_background(font): + im = Image.new(mode="RGBA", size=(300, 100)) + draw = ImageDraw.Draw(im) + + txt = "Hello World!" + draw.text((10, 10), txt, font=font) + + target = "Tests/images/transparent_background_text.png" + assert_image_similar_tofile(im, target, 4.09) + + target = "Tests/images/transparent_background_text_L.png" + assert_image_similar_tofile(im.convert("L"), target, 0.01) + + +def test_I16(font): + im = Image.new(mode="I;16", size=(300, 100)) + draw = ImageDraw.Draw(im) + + txt = "Hello World!" + draw.text((10, 10), txt, font=font) + + target = "Tests/images/transparent_background_text_L.png" + assert_image_similar_tofile(im.convert("L"), target, 0.01) + + +def test_textbbox_equal(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + txt = "Hello World!" + bbox = draw.textbbox((10, 10), txt, font) + draw.text((10, 10), txt, font=font) + draw.rectangle(bbox) + + assert_image_similar_tofile(im, "Tests/images/rectangle_surrounding_text.png", 2.5) + + +@pytest.mark.parametrize( + "text, mode, fontname, size, length_basic, length_raqm", + ( + # basic test + ("text", "L", "FreeMono.ttf", 15, 36, 36), + ("text", "1", "FreeMono.ttf", 15, 36, 36), + # issue 4177 + ("rrr", "L", "DejaVuSans/DejaVuSans.ttf", 18, 21, 22.21875), + ("rrr", "1", "DejaVuSans/DejaVuSans.ttf", 18, 24, 22.21875), + # test 'l' not including extra margin + # using exact value 2047 / 64 for raqm, checked with debugger + ("ill", "L", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375), + ("ill", "1", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375), + ), +) +def test_getlength( + text, mode, fontname, size, layout_engine, length_basic, length_raqm +): + f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine) + + im = Image.new(mode, (1, 1), 0) + d = ImageDraw.Draw(im) + + if layout_engine == ImageFont.Layout.BASIC: + length = d.textlength(text, f) + assert length == length_basic + else: + # disable kerning, kerning metrics changed + length = d.textlength(text, f, features=["-kern"]) + assert length == length_raqm + + +def test_render_multiline(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + line_spacing = font.getbbox("A")[3] + 4 + lines = TEST_TEXT.split("\n") + y = 0 + for line in lines: + draw.text((0, y), line, font=font) + y += line_spacing + + # some versions of freetype have different horizontal spacing. + # setting a tight epsilon, I'm showing the original test failure + # at epsilon = ~38. + assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) + + +def test_render_multiline_text(font): + # Test that text() correctly connects to multiline_text() + # and that align defaults to left + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), TEST_TEXT, font=font) + + assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 0.01) + + # Test that text() can pass on additional arguments + # to multiline_text() + draw.text( + (0, 0), TEST_TEXT, fill=None, font=font, anchor=None, spacing=4, align="left" + ) + draw.text((0, 0), TEST_TEXT, None, font, None, 4, "left") + + +@pytest.mark.parametrize( + "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) +) +def test_render_multiline_text_align(font, align, ext): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align) + + assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) + + +def test_unknown_align(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + # Act/Assert + with pytest.raises(ValueError): + draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown") + + +def test_draw_align(font): + im = Image.new("RGB", (300, 100), "white") + draw = ImageDraw.Draw(im) + line = "some text" + draw.text((100, 40), line, (0, 0, 0), font=font, align="left") + + +def test_multiline_size(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + with pytest.warns(DeprecationWarning) as log: # Test that textsize() correctly connects to multiline_textsize() - self.assertEqual(draw.textsize(TEST_TEXT, font=ttf), - draw.multiline_textsize(TEST_TEXT, font=ttf)) + assert draw.textsize(TEST_TEXT, font=font) == draw.multiline_textsize( + TEST_TEXT, font=font + ) + + # Test that multiline_textsize corresponds to ImageFont.textsize() + # for single line text + assert font.getsize("A") == draw.multiline_textsize("A", font=font) # Test that textsize() can pass on additional arguments # to multiline_textsize() - draw.textsize(TEST_TEXT, font=ttf, spacing=4) - draw.textsize(TEST_TEXT, ttf, 4) + draw.textsize(TEST_TEXT, font=font, spacing=4) + draw.textsize(TEST_TEXT, font, 4) + assert len(log) == 6 - def test_multiline_width(self): - ttf = self.get_font() - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - self.assertEqual(draw.textsize("longest line", font=ttf)[0], - draw.multiline_textsize("longest line\nline", - font=ttf)[0]) +def test_multiline_bbox(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) - def test_multiline_spacing(self): - ttf = self.get_font() + # Test that textbbox() correctly connects to multiline_textbbox() + assert draw.textbbox((0, 0), TEST_TEXT, font=font) == draw.multiline_textbbox( + (0, 0), TEST_TEXT, font=font + ) - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.multiline_text((0, 0), TEST_TEXT, font=ttf, spacing=10) + # Test that multiline_textbbox corresponds to ImageFont.textbbox() + # for single line text + assert font.getbbox("A") == draw.multiline_textbbox((0, 0), "A", font=font) - target = 'Tests/images/multiline_text_spacing.png' - target_img = Image.open(target) + # Test that textbbox() can pass on additional arguments + # to multiline_textbbox() + draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4) - # Epsilon ~.5 fails with FreeType 2.7 - self.assert_image_similar(im, target_img, self.metrics['multiline']) - def test_rotated_transposed_font(self): - img_grey = Image.new("L", (100, 100)) - draw = ImageDraw.Draw(img_grey) - word = "testing" - font = self.get_font() +def test_multiline_width(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) - orientation = Image.ROTATE_90 - transposed_font = ImageFont.TransposedFont( - font, orientation=orientation) + assert ( + draw.textbbox((0, 0), "longest line", font=font)[2] + == draw.multiline_textbbox((0, 0), "longest line\nline", font=font)[2] + ) + with pytest.warns(DeprecationWarning) as log: + assert ( + draw.textsize("longest line", font=font)[0] + == draw.multiline_textsize("longest line\nline", font=font)[0] + ) + assert len(log) == 2 - # Original font - draw.font = font - box_size_a = draw.textsize(word) - # Rotated font - draw.font = transposed_font - box_size_b = draw.textsize(word) +def test_multiline_spacing(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10) + + assert_image_similar_tofile(im, "Tests/images/multiline_text_spacing.png", 2.5) - # Check (w,h) of box a is (h,w) of box b - self.assertEqual(box_size_a[0], box_size_b[1]) - self.assertEqual(box_size_a[1], box_size_b[0]) - def test_unrotated_transposed_font(self): - img_grey = Image.new("L", (100, 100)) - draw = ImageDraw.Draw(img_grey) - word = "testing" - font = self.get_font() +@pytest.mark.parametrize( + "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) +) +def test_rotated_transposed_font(font, orientation): + img_grey = Image.new("L", (100, 100)) + draw = ImageDraw.Draw(img_grey) + word = "testing" - orientation = None - transposed_font = ImageFont.TransposedFont( - font, orientation=orientation) + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) - # Original font - draw.font = font + # Original font + draw.font = font + with pytest.warns(DeprecationWarning) as log: box_size_a = draw.textsize(word) + assert box_size_a == font.getsize(word) + assert len(log) == 2 + bbox_a = draw.textbbox((10, 10), word) - # Rotated font - draw.font = transposed_font + # Rotated font + draw.font = transposed_font + with pytest.warns(DeprecationWarning) as log: box_size_b = draw.textsize(word) + assert box_size_b == transposed_font.getsize(word) + assert len(log) == 2 + bbox_b = draw.textbbox((20, 20), word) + + # Check (w,h) of box a is (h,w) of box b + assert box_size_a[0] == box_size_b[1] + assert box_size_a[1] == box_size_b[0] + + # Check bbox b is (20, 20, 20 + h, 20 + w) + assert bbox_b[0] == 20 + assert bbox_b[1] == 20 + assert bbox_b[2] == 20 + bbox_a[3] - bbox_a[1] + assert bbox_b[3] == 20 + bbox_a[2] - bbox_a[0] + + # text length is undefined for vertical text + pytest.raises(ValueError, draw.textlength, word) + + +@pytest.mark.parametrize( + "orientation", + ( + None, + Image.Transpose.ROTATE_180, + Image.Transpose.FLIP_LEFT_RIGHT, + Image.Transpose.FLIP_TOP_BOTTOM, + ), +) +def test_unrotated_transposed_font(font, orientation): + img_grey = Image.new("L", (100, 100)) + draw = ImageDraw.Draw(img_grey) + word = "testing" + + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) + + # Original font + draw.font = font + with pytest.warns(DeprecationWarning) as log: + box_size_a = draw.textsize(word) + assert len(log) == 1 + bbox_a = draw.textbbox((10, 10), word) + length_a = draw.textlength(word) + + # Rotated font + draw.font = transposed_font + with pytest.warns(DeprecationWarning) as log: + box_size_b = draw.textsize(word) + assert len(log) == 1 + bbox_b = draw.textbbox((20, 20), word) + length_b = draw.textlength(word) + + # Check boxes a and b are same size + assert box_size_a == box_size_b + + # Check bbox b is (20, 20, 20 + w, 20 + h) + assert bbox_b[0] == 20 + assert bbox_b[1] == 20 + assert bbox_b[2] == 20 + bbox_a[2] - bbox_a[0] + assert bbox_b[3] == 20 + bbox_a[3] - bbox_a[1] + + assert length_a == length_b - # Check boxes a and b are same size - self.assertEqual(box_size_a, box_size_b) - def test_rotated_transposed_font_get_mask(self): - # Arrange - text = "mask this" - font = self.get_font() - orientation = Image.ROTATE_90 - transposed_font = ImageFont.TransposedFont( - font, orientation=orientation) +@pytest.mark.parametrize( + "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) +) +def test_rotated_transposed_font_get_mask(font, orientation): + # Arrange + text = "mask this" + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) - # Act - mask = transposed_font.getmask(text) + # Act + mask = transposed_font.getmask(text) - # Assert - self.assertEqual(mask.size, (13, 108)) + # Assert + assert mask.size == (13, 108) - def test_unrotated_transposed_font_get_mask(self): - # Arrange - text = "mask this" - font = self.get_font() - orientation = None - transposed_font = ImageFont.TransposedFont( - font, orientation=orientation) - # Act - mask = transposed_font.getmask(text) +@pytest.mark.parametrize( + "orientation", + ( + None, + Image.Transpose.ROTATE_180, + Image.Transpose.FLIP_LEFT_RIGHT, + Image.Transpose.FLIP_TOP_BOTTOM, + ), +) +def test_unrotated_transposed_font_get_mask(font, orientation): + # Arrange + text = "mask this" + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) - # Assert - self.assertEqual(mask.size, (108, 13)) + # Act + mask = transposed_font.getmask(text) - def test_free_type_font_get_name(self): - # Arrange - font = self.get_font() + # Assert + assert mask.size == (108, 13) - # Act - name = font.getname() - # Assert - self.assertEqual(('FreeMono', 'Regular'), name) +def test_free_type_font_get_name(font): + assert ("FreeMono", "Regular") == font.getname() - def test_free_type_font_get_metrics(self): - # Arrange - font = self.get_font() - # Act - ascent, descent = font.getmetrics() +def test_free_type_font_get_metrics(font): + ascent, descent = font.getmetrics() - # Assert - self.assertIsInstance(ascent, int) - self.assertIsInstance(descent, int) - self.assertEqual((ascent, descent), (16, 4)) # too exact check? + assert isinstance(ascent, int) + assert isinstance(descent, int) + assert (ascent, descent) == (16, 4) - def test_free_type_font_get_offset(self): - # Arrange - font = self.get_font() - text = "offset this" - # Act +def test_free_type_font_get_offset(font): + # Arrange + text = "offset this" + + # Act + with pytest.warns(DeprecationWarning) as log: offset = font.getoffset(text) - # Assert - self.assertEqual(offset, (0, 3)) - - def test_free_type_font_get_mask(self): - # Arrange - font = self.get_font() - text = "mask this" - - # Act - mask = font.getmask(text) - - # Assert - self.assertEqual(mask.size, (108, 13)) - - def test_load_path_not_found(self): - # Arrange - filename = "somefilenamethatdoesntexist.ttf" - - # Act/Assert - self.assertRaises(IOError, ImageFont.load_path, filename) - - def test_default_font(self): - # Arrange - txt = 'This is a "better than nothing" default font.' - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - - target = 'Tests/images/default_font.png' - target_img = Image.open(target) - - # Act - default_font = ImageFont.load_default() - draw.text((10, 10), txt, font=default_font) - - # Assert - self.assert_image_equal(im, target_img) - - def test_getsize_empty(self): - # issue #2614 - font = self.get_font() - # should not crash. - self.assertEqual((0, 0), font.getsize('')) - - def test_render_empty(self): - # issue 2666 - font = self.get_font() - im = Image.new(mode='RGB', size=(300, 100)) - target = im.copy() - draw = ImageDraw.Draw(im) - #should not crash here. - draw.text((10, 10), '', font=font) - self.assert_image_equal(im, target) - - def test_unicode_pilfont(self): - # should not segfault, should return UnicodeDecodeError - # issue #2826 - font = ImageFont.load_default() - with self.assertRaises(UnicodeEncodeError): - font.getsize(u"’") - - - def _test_fake_loading_font(self, path_to_fake, fontname): + # Assert + assert len(log) == 1 + assert offset == (0, 3) + + +def test_free_type_font_get_mask(font): + # Arrange + text = "mask this" + + # Act + mask = font.getmask(text) + + # Assert + assert mask.size == (108, 13) + + +def test_load_path_not_found(): + # Arrange + filename = "somefilenamethatdoesntexist.ttf" + + # Act/Assert + with pytest.raises(OSError): + ImageFont.load_path(filename) + with pytest.raises(OSError): + ImageFont.truetype(filename) + + +def test_load_non_font_bytes(): + with open("Tests/images/hopper.jpg", "rb") as f: + with pytest.raises(OSError): + ImageFont.truetype(f) + + +def test_default_font(): + # Arrange + txt = 'This is a "better than nothing" default font.' + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + # Act + default_font = ImageFont.load_default() + draw.text((10, 10), txt, font=default_font) + + # Assert + assert_image_equal_tofile(im, "Tests/images/default_font.png") + + +def test_getbbox_empty(font): + # issue #2614, should not crash. + assert (0, 0, 0, 0) == font.getbbox("") + + +def test_render_empty(font): + # issue 2666 + im = Image.new(mode="RGB", size=(300, 100)) + target = im.copy() + draw = ImageDraw.Draw(im) + # should not crash here. + draw.text((10, 10), "", font=font) + assert_image_equal(im, target) + + +def test_unicode_pilfont(): + # should not segfault, should return UnicodeDecodeError + # issue #2826 + font = ImageFont.load_default() + with pytest.raises(UnicodeEncodeError): + font.getbbox("’") + + +def test_unicode_extended(layout_engine): + # issue #3777 + text = "A\u278A\U0001F12B" + target = "Tests/images/unicode_extended.png" + + ttf = ImageFont.truetype( + "Tests/fonts/NotoSansSymbols-Regular.ttf", + FONT_SIZE, + layout_engine=layout_engine, + ) + img = Image.new("RGB", (100, 60)) + d = ImageDraw.Draw(img) + d.text((10, 10), text, font=ttf) + + # fails with 14.7 + assert_image_similar_tofile(img, target, 6.2) + + +@pytest.mark.parametrize( + "platform, font_directory", + (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")), +) +@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") +def test_find_font(monkeypatch, platform, font_directory): + def _test_fake_loading_font(path_to_fake, fontname): # Make a copy of FreeTypeFont so we can patch the original free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) - with SimplePatcher(ImageFont, '_FreeTypeFont', free_type_font): + with monkeypatch.context() as m: + m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False) + def loadable_font(filepath, size, index, encoding, *args, **kwargs): if filepath == path_to_fake: - return ImageFont._FreeTypeFont(FONT_PATH, size, index, - encoding, *args, **kwargs) - return ImageFont._FreeTypeFont(filepath, size, index, - encoding, *args, **kwargs) - with SimplePatcher(ImageFont, 'FreeTypeFont', loadable_font): - font = ImageFont.truetype(fontname) - # Make sure it's loaded - name = font.getname() - self.assertEqual(('FreeMono', 'Regular'), name) - - @unittest.skipIf(sys.platform.startswith('win32'), - "requires Unix or MacOS") - def test_find_linux_font(self): - # A lot of mocking here - this is more for hitting code and - # catching syntax like errors - font_directory = '/usr/local/share/fonts' - with SimplePatcher(sys, 'platform', 'linux'): - patched_env = copy.deepcopy(os.environ) - patched_env['XDG_DATA_DIRS'] = '/usr/share/:/usr/local/share/' - with SimplePatcher(os, 'environ', patched_env): - def fake_walker(path): - if path == font_directory: - return [(path, [], [ - 'Arial.ttf', 'Single.otf', 'Duplicate.otf', - 'Duplicate.ttf'], )] - return [(path, [], ['some_random_font.ttf'], )] - with SimplePatcher(os, 'walk', fake_walker): - # Test that the font loads both with and without the - # extension - self._test_fake_loading_font( - font_directory+'/Arial.ttf', 'Arial.ttf') - self._test_fake_loading_font( - font_directory+'/Arial.ttf', 'Arial') - - # Test that non-ttf fonts can be found without the - # extension - self._test_fake_loading_font( - font_directory+'/Single.otf', 'Single') - - # Test that ttf fonts are preferred if the extension is - # not specified - self._test_fake_loading_font( - font_directory+'/Duplicate.ttf', 'Duplicate') - - @unittest.skipIf(sys.platform.startswith('win32'), - "requires Unix or MacOS") - def test_find_macos_font(self): - # Like the linux test, more cover hitting code rather than testing - # correctness. - font_directory = '/System/Library/Fonts' - with SimplePatcher(sys, 'platform', 'darwin'): - def fake_walker(path): - if path == font_directory: - return [(path, [], - ['Arial.ttf', 'Single.otf', - 'Duplicate.otf', 'Duplicate.ttf'], )] - return [(path, [], ['some_random_font.ttf'], )] - with SimplePatcher(os, 'walk', fake_walker): - self._test_fake_loading_font( - font_directory+'/Arial.ttf', 'Arial.ttf') - self._test_fake_loading_font( - font_directory+'/Arial.ttf', 'Arial') - self._test_fake_loading_font( - font_directory+'/Single.otf', 'Single') - self._test_fake_loading_font( - font_directory+'/Duplicate.ttf', 'Duplicate') - - def test_imagefont_getters(self): - # Arrange - t = self.get_font() - - # Act / Assert - self.assertEqual(t.getmetrics(), (16, 4)) - self.assertEqual(t.font.ascent, 16) - self.assertEqual(t.font.descent, 4) - self.assertEqual(t.font.height, 20) - self.assertEqual(t.font.x_ppem, 20) - self.assertEqual(t.font.y_ppem, 20) - self.assertEqual(t.font.glyphs, 4177) - self.assertEqual(t.getsize('A'), (12, 16)) - self.assertEqual(t.getsize('AB'), (24, 16)) - self.assertEqual(t.getsize('M'), self.metrics['getters']) - self.assertEqual(t.getsize('y'), (12, 20)) - self.assertEqual(t.getsize('a'), (12, 16)) - - -@unittest.skipUnless(HAS_RAQM, "Raqm not Available") -class TestImageFont_RaqmLayout(TestImageFont): - LAYOUT_ENGINE = ImageFont.LAYOUT_RAQM - - -if __name__ == '__main__': - unittest.main() + return ImageFont._FreeTypeFont( + FONT_PATH, size, index, encoding, *args, **kwargs + ) + return ImageFont._FreeTypeFont( + filepath, size, index, encoding, *args, **kwargs + ) + + m.setattr(ImageFont, "FreeTypeFont", loadable_font) + font = ImageFont.truetype(fontname) + # Make sure it's loaded + name = font.getname() + assert ("FreeMono", "Regular") == name + + # A lot of mocking here - this is more for hitting code and + # catching syntax like errors + monkeypatch.setattr(sys, "platform", platform) + if platform == "linux": + monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") + + def fake_walker(path): + if path == font_directory: + return [ + ( + path, + [], + ["Arial.ttf", "Single.otf", "Duplicate.otf", "Duplicate.ttf"], + ) + ] + return [(path, [], ["some_random_font.ttf"])] + + monkeypatch.setattr(os, "walk", fake_walker) + + # Test that the font loads both with and without the extension + _test_fake_loading_font(font_directory + "/Arial.ttf", "Arial.ttf") + _test_fake_loading_font(font_directory + "/Arial.ttf", "Arial") + + # Test that non-ttf fonts can be found without the extension + _test_fake_loading_font(font_directory + "/Single.otf", "Single") + + # Test that ttf fonts are preferred if the extension is not specified + _test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate") + + +def test_imagefont_getters(font): + assert font.getmetrics() == (16, 4) + assert font.font.ascent == 16 + assert font.font.descent == 4 + assert font.font.height == 20 + assert font.font.x_ppem == 20 + assert font.font.y_ppem == 20 + assert font.font.glyphs == 4177 + assert font.getbbox("A") == (0, 4, 12, 16) + assert font.getbbox("AB") == (0, 4, 24, 16) + assert font.getbbox("M") == (0, 4, 12, 16) + assert font.getbbox("y") == (0, 7, 12, 20) + assert font.getbbox("a") == (0, 7, 12, 16) + assert font.getlength("A") == 12 + assert font.getlength("AB") == 24 + assert font.getlength("M") == 12 + assert font.getlength("y") == 12 + assert font.getlength("a") == 12 + with pytest.warns(DeprecationWarning) as log: + assert font.getsize("A") == (12, 16) + assert font.getsize("AB") == (24, 16) + assert font.getsize("M") == (12, 16) + assert font.getsize("y") == (12, 20) + assert font.getsize("a") == (12, 16) + assert font.getsize_multiline("A") == (12, 16) + assert font.getsize_multiline("AB") == (24, 16) + assert font.getsize_multiline("a") == (12, 16) + assert font.getsize_multiline("ABC\n") == (36, 36) + assert font.getsize_multiline("ABC\nA") == (36, 36) + assert font.getsize_multiline("ABC\nAaaa") == (48, 36) + assert len(log) == 11 + + +@pytest.mark.parametrize("stroke_width", (0, 2)) +def test_getsize_stroke(font, stroke_width): + assert font.getbbox("A", stroke_width=stroke_width) == ( + 0 - stroke_width, + 4 - stroke_width, + 12 + stroke_width, + 16 + stroke_width, + ) + with pytest.warns(DeprecationWarning) as log: + assert font.getsize("A", stroke_width=stroke_width) == ( + 12 + stroke_width * 2, + 16 + stroke_width * 2, + ) + assert font.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == ( + 48 + stroke_width * 2, + 36 + stroke_width * 4, + ) + assert len(log) == 2 + + +def test_complex_font_settings(): + t = ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.BASIC) + with pytest.raises(KeyError): + t.getmask("абвг", direction="rtl") + with pytest.raises(KeyError): + t.getmask("абвг", features=["-kern"]) + with pytest.raises(KeyError): + t.getmask("абвг", language="sr") + + +def test_variation_get(font): + freetype = parse_version(features.version_module("freetype2")) + if freetype < parse_version("2.9.1"): + with pytest.raises(NotImplementedError): + font.get_variation_names() + with pytest.raises(NotImplementedError): + font.get_variation_axes() + return + + with pytest.raises(OSError): + font.get_variation_names() + with pytest.raises(OSError): + font.get_variation_axes() + + font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf") + assert font.get_variation_names(), [ + b"ExtraLight", + b"Light", + b"Regular", + b"Semibold", + b"Bold", + b"Black", + b"Black Medium Contrast", + b"Black High Contrast", + b"Default", + ] + assert font.get_variation_axes() == [ + {"name": b"Weight", "minimum": 200, "maximum": 900, "default": 389}, + {"name": b"Contrast", "minimum": 0, "maximum": 100, "default": 0}, + ] + + font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf") + assert font.get_variation_names() == [ + b"20", + b"40", + b"60", + b"80", + b"100", + b"120", + b"140", + b"160", + b"180", + b"200", + b"220", + b"240", + b"260", + b"280", + b"300", + b"Regular", + ] + assert font.get_variation_axes() == [ + {"name": b"Size", "minimum": 0, "maximum": 300, "default": 0} + ] + + +def _check_text(font, path, epsilon): + im = Image.new("RGB", (100, 75), "white") + d = ImageDraw.Draw(im) + d.text((10, 10), "Text", font=font, fill="black") + + try: + assert_image_similar_tofile(im, path, epsilon) + except AssertionError: + if "_adobe" in path: + path = path.replace("_adobe", "_adobe_older_harfbuzz") + assert_image_similar_tofile(im, path, epsilon) + else: + raise + + +def test_variation_set_by_name(font): + freetype = parse_version(features.version_module("freetype2")) + if freetype < parse_version("2.9.1"): + with pytest.raises(NotImplementedError): + font.set_variation_by_name("Bold") + return + + with pytest.raises(OSError): + font.set_variation_by_name("Bold") + + font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) + _check_text(font, "Tests/images/variation_adobe.png", 11) + for name in ["Bold", b"Bold"]: + font.set_variation_by_name(name) + 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) + + +def test_variation_set_by_axes(font): + freetype = parse_version(features.version_module("freetype2")) + if freetype < parse_version("2.9.1"): + with pytest.raises(NotImplementedError): + font.set_variation_by_axes([100]) + return + + with pytest.raises(OSError): + font.set_variation_by_axes([500, 50]) + + font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) + font.set_variation_by_axes([500, 50]) + _check_text(font, "Tests/images/variation_adobe_axes.png", 11.05) + + font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) + font.set_variation_by_axes([100]) + _check_text(font, "Tests/images/variation_tiny_axes.png", 32.5) + + +def test_textbbox_non_freetypefont(): + im = Image.new("RGB", (200, 200)) + d = ImageDraw.Draw(im) + default_font = ImageFont.load_default() + with pytest.warns(DeprecationWarning) as log: + width, height = d.textsize("test", font=default_font) + assert len(log) == 1 + assert d.textlength("test", font=default_font) == width + assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, width, height) + + +@pytest.mark.parametrize( + "anchor, left, top", + ( + # test horizontal anchors + ("ls", 0, -36), + ("ms", -64, -36), + ("rs", -128, -36), + # test vertical anchors + ("ma", -64, 16), + ("mt", -64, 0), + ("mm", -64, -17), + ("mb", -64, -44), + ("md", -64, -51), + ), + ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), +) +def test_anchor(layout_engine, anchor, left, top): + name, text = "quick", "Quick" + path = f"Tests/images/test_anchor_{name}_{anchor}.png" + + if layout_engine == ImageFont.Layout.RAQM: + width, height = (129, 44) + else: + width, height = (128, 44) + + bbox_expected = (left, top, left + width, top + height) + + f = ImageFont.truetype( + "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=layout_engine + ) + + im = Image.new("RGB", (200, 200), "white") + d = ImageDraw.Draw(im) + d.line(((0, 100), (200, 100)), "gray") + d.line(((100, 0), (100, 200)), "gray") + d.text((100, 100), text, fill="black", anchor=anchor, font=f) + + assert d.textbbox((0, 0), text, f, anchor=anchor) == bbox_expected + + assert_image_similar_tofile(im, path, 7) + + +@pytest.mark.parametrize( + "anchor, align", + ( + # test horizontal anchors + ("lm", "left"), + ("lm", "center"), + ("lm", "right"), + ("mm", "left"), + ("mm", "center"), + ("mm", "right"), + ("rm", "left"), + ("rm", "center"), + ("rm", "right"), + # test vertical anchors + ("ma", "center"), + # ("mm", "center"), # duplicate + ("md", "center"), + ), +) +def test_anchor_multiline(layout_engine, anchor, align): + target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" + text = "a\nlong\ntext sample" + + f = ImageFont.truetype( + "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=layout_engine + ) + + # test render + im = Image.new("RGB", (600, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (600, 200)), "gray") + d.line(((300, 0), (300, 400)), "gray") + d.multiline_text((300, 200), text, fill="black", anchor=anchor, font=f, align=align) + + assert_image_similar_tofile(im, target, 4) + + +def test_anchor_invalid(font): + im = Image.new("RGB", (100, 100), "white") + d = ImageDraw.Draw(im) + d.font = font + + for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]: + pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor)) + pytest.raises(ValueError, lambda: font.getbbox("hello", anchor=anchor)) + pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor)) + pytest.raises(ValueError, lambda: d.textbbox((0, 0), "hello", anchor=anchor)) + pytest.raises( + ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + ) + pytest.raises( + ValueError, + lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), + ) + for anchor in ["lt", "lb"]: + pytest.raises( + ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + ) + pytest.raises( + ValueError, + lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), + ) + + +@pytest.mark.parametrize("bpp", (1, 2, 4, 8)) +def test_bitmap_font(layout_engine, bpp): + text = "Bitmap Font" + layout_name = ["basic", "raqm"][layout_engine] + target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" + font = ImageFont.truetype( + f"Tests/fonts/DejaVuSans/DejaVuSans-24-{bpp}-stripped.ttf", + 24, + layout_engine=layout_engine, + ) + + im = Image.new("RGB", (160, 35), "white") + draw = ImageDraw.Draw(im) + draw.text((2, 2), text, "black", font) + + assert_image_equal_tofile(im, target) + + +def test_bitmap_font_stroke(layout_engine): + text = "Bitmap Font" + layout_name = ["basic", "raqm"][layout_engine] + target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" + font = ImageFont.truetype( + "Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf", + 24, + layout_engine=layout_engine, + ) + + im = Image.new("RGB", (160, 35), "white") + draw = ImageDraw.Draw(im) + draw.text((2, 2), text, "black", font, stroke_width=2, stroke_fill="red") + + assert_image_similar_tofile(im, target, 0.03) + + +def test_standard_embedded_color(layout_engine): + txt = "Hello World!" + ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) + ttf.getbbox(txt) + + im = Image.new("RGB", (300, 64), "white") + d = ImageDraw.Draw(im) + d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True) + + assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1) + + +@pytest.mark.parametrize("fontmode", ("1", "L", "RGBA")) +def test_float_coord(layout_engine, fontmode): + txt = "Hello World!" + ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) + + im = Image.new("RGB", (300, 64), "white") + d = ImageDraw.Draw(im) + if fontmode == "1": + d.fontmode = "1" + + embedded_color = fontmode == "RGBA" + d.text((9.5, 9.5), txt, font=ttf, fill="#fa6", embedded_color=embedded_color) + try: + assert_image_similar_tofile(im, "Tests/images/text_float_coord.png", 3.9) + except AssertionError: + if fontmode == "1" and layout_engine == ImageFont.Layout.BASIC: + assert_image_similar_tofile( + im, "Tests/images/text_float_coord_1_alt.png", 1 + ) + else: + raise + + +def test_cbdt(layout_engine): + try: + font = ImageFont.truetype( + "Tests/fonts/NotoColorEmoji.ttf", size=109, layout_engine=layout_engine + ) + + im = Image.new("RGB", (150, 150), "white") + d = ImageDraw.Draw(im) + + d.text((10, 10), "\U0001f469", font=font, embedded_color=True) + + assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2) + except OSError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or CBDT support") + + +def test_cbdt_mask(layout_engine): + try: + font = ImageFont.truetype( + "Tests/fonts/NotoColorEmoji.ttf", size=109, layout_engine=layout_engine + ) + + im = Image.new("RGB", (150, 150), "white") + d = ImageDraw.Draw(im) + + d.text((10, 10), "\U0001f469", "black", font=font) + + assert_image_similar_tofile( + im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2 + ) + except OSError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or CBDT support") + + +def test_sbix(layout_engine): + try: + font = ImageFont.truetype( + "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine + ) + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + + d.text((50, 50), "\uE901", font=font, embedded_color=True) + + assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1) + except OSError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or SBIX support") + + +def test_sbix_mask(layout_engine): + try: + font = ImageFont.truetype( + "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine + ) + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + + d.text((50, 50), "\uE901", (100, 0, 0), font=font) + + assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1) + except OSError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or SBIX support") + + +@skip_unless_feature_version("freetype2", "2.10.0") +def test_colr(layout_engine): + font = ImageFont.truetype( + "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", + size=64, + layout_engine=layout_engine, + ) + + im = Image.new("RGB", (300, 75), "white") + d = ImageDraw.Draw(im) + + d.text((15, 5), "Bungee", font=font, embedded_color=True) + + assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 21) + + +@skip_unless_feature_version("freetype2", "2.10.0") +def test_colr_mask(layout_engine): + font = ImageFont.truetype( + "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", + size=64, + layout_engine=layout_engine, + ) + + im = Image.new("RGB", (300, 75), "white") + d = ImageDraw.Draw(im) + + d.text((15, 5), "Bungee", "black", font=font) + + 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) + with pytest.warns(DeprecationWarning): + with pytest.raises(TypeError): + font.getmask2("Hello world", fill=None) + + +def test_render_mono_size(): + # issue 4177 + + im = Image.new("P", (100, 30), "white") + draw = ImageDraw.Draw(im) + ttf = ImageFont.truetype( + "Tests/fonts/DejaVuSans/DejaVuSans.ttf", + 18, + layout_engine=ImageFont.Layout.BASIC, + ) + + draw.text((10, 10), "r" * 10, "black", ttf) + assert_image_equal_tofile(im, "Tests/images/text_mono.gif") + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf", + ], +) +def test_oom(test_file): + with open(test_file, "rb") as f: + font = ImageFont.truetype(BytesIO(f.read())) + with pytest.raises(Image.DecompressionBombError): + font.getmask("Test Text") + + +def test_raqm_missing_warning(monkeypatch): + monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) + with pytest.warns(UserWarning) as record: + font = ImageFont.truetype( + FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.RAQM + ) + assert font.layout_engine == ImageFont.Layout.BASIC + assert str(record[-1].message) == ( + "Raqm layout was requested, but Raqm is not available. " + "Falling back to basic layout." + ) + + +def test_constants_deprecation(): + for enum, prefix in { + ImageFont.Layout: "LAYOUT_", + }.items(): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(ImageFont, prefix + name) == enum[name] diff --git a/Tests/test_imagefont_bitmap.py b/Tests/test_imagefont_bitmap.py deleted file mode 100644 index 7ee5f8a2b5e..00000000000 --- a/Tests/test_imagefont_bitmap.py +++ /dev/null @@ -1,38 +0,0 @@ -from helper import unittest, PillowTestCase -from PIL import Image, ImageFont, ImageDraw - - -image_font_installed = True -try: - ImageFont.core.getfont -except ImportError: - image_font_installed = False - - -@unittest.skipIf(not image_font_installed, "image font not installed") -class TestImageFontBitmap(PillowTestCase): - def test_similar(self): - text = 'EmbeddedBitmap' - font_outline = ImageFont.truetype( - font='Tests/fonts/DejaVuSans.ttf', size=24) - font_bitmap = ImageFont.truetype( - font='Tests/fonts/DejaVuSans-bitmap.ttf', size=24) - size_outline = font_outline.getsize(text) - size_bitmap = font_bitmap.getsize(text) - size_final = max(size_outline[0], size_bitmap[0]), max(size_outline[1], size_bitmap[1]) - im_bitmap = Image.new('RGB', size_final, (255, 255, 255)) - im_outline = im_bitmap.copy() - draw_bitmap = ImageDraw.Draw(im_bitmap) - draw_outline = ImageDraw.Draw(im_outline) - - # Metrics are different on the bitmap and ttf fonts, - # more so on some platforms and versions of freetype than others. - # Mac has a 1px difference, linux doesn't. - draw_bitmap.text((0, size_final[1] - size_bitmap[1]), - text, fill=(0, 0, 0), font=font_bitmap) - draw_outline.text((0, size_final[1] - size_outline[1]), - text, fill=(0, 0, 0), font=font_outline) - self.assert_image_similar(im_bitmap, im_outline, 20) - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 79122f6c11e..cf039e86e61 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -1,133 +1,396 @@ -# -*- coding: utf-8 -*- -from helper import unittest, PillowTestCase -from PIL import Image, ImageDraw, ImageFont, features +import pytest +from PIL import Image, ImageDraw, ImageFont + +from .helper import assert_image_similar_tofile, skip_unless_feature FONT_SIZE = 20 -FONT_PATH = "Tests/fonts/DejaVuSans.ttf" +FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" + +pytestmark = skip_unless_feature("raqm") + + +def test_english(): + # smoke test, this should not fail + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "TEST", font=ttf, fill=500, direction="ltr") + + +def test_complex_text(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "اهلا عمان", font=ttf, fill=500) + + target = "Tests/images/test_text.png" + assert_image_similar_tofile(im, target, 0.5) + + +def test_y_offset(): + ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "العالم العربي", font=ttf, fill=500) + + target = "Tests/images/test_y_offset.png" + assert_image_similar_tofile(im, target, 1.7) + -@unittest.skipUnless(features.check('raqm'), "Raqm Library is not installed.") -class TestImagecomplextext(PillowTestCase): +def test_complex_unicode_text(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - def test_english(self): - #smoke test, this should not fail - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), 'TEST', font=ttf, fill=500, direction='ltr') - - - def test_complex_text(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "السلام عليكم", font=ttf, fill=500) - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), 'اهلا عمان', font=ttf, fill=500) + target = "Tests/images/test_complex_unicode_text.png" + assert_image_similar_tofile(im, target, 0.5) - target = 'Tests/images/test_text.png' - target_img = Image.open(target) + ttf = ImageFont.truetype("Tests/fonts/KhmerOSBattambang-Regular.ttf", FONT_SIZE) - self.assert_image_similar(im, target_img, .5) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "លោកុប្បត្តិ", font=ttf, fill=500) - def test_y_offset(self): - ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) + target = "Tests/images/test_complex_unicode_text2.png" + assert_image_similar_tofile(im, target, 2.33) - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), 'العالم العربي', font=ttf, fill=500) - target = 'Tests/images/test_y_offset.png' - target_img = Image.open(target) +def test_text_direction_rtl(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - self.assert_image_similar(im, target_img, 1.7) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "English عربي", font=ttf, fill=500, direction="rtl") - def test_complex_unicode_text(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + target = "Tests/images/test_direction_rtl.png" + assert_image_similar_tofile(im, target, 0.5) - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), 'السلام عليكم', font=ttf, fill=500) - target = 'Tests/images/test_complex_unicode_text.png' - target_img = Image.open(target) +def test_text_direction_ltr(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - self.assert_image_similar(im, target_img, .5) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "سلطنة عمان Oman", font=ttf, fill=500, direction="ltr") - def test_text_direction_rtl(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + target = "Tests/images/test_direction_ltr.png" + assert_image_similar_tofile(im, target, 0.5) - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), 'English عربي', font=ttf, fill=500, direction='rtl') - target = 'Tests/images/test_direction_rtl.png' - target_img = Image.open(target) +def test_text_direction_rtl2(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - self.assert_image_similar(im, target_img, .5) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "Oman سلطنة عمان", font=ttf, fill=500, direction="rtl") - def test_text_direction_ltr(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + target = "Tests/images/test_direction_ltr.png" + assert_image_similar_tofile(im, target, 0.5) - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), 'سلطنة عمان Oman', font=ttf, fill=500, direction='ltr') - target = 'Tests/images/test_direction_ltr.png' - target_img = Image.open(target) +def test_text_direction_ttb(): + ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", FONT_SIZE) - self.assert_image_similar(im, target_img, .5) + im = Image.new(mode="RGB", size=(100, 300)) + draw = ImageDraw.Draw(im) + try: + draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb") + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + pytest.skip("libraqm 0.7 or greater not available") - def test_text_direction_rtl2(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + target = "Tests/images/test_direction_ttb.png" + assert_image_similar_tofile(im, target, 2.8) - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), 'Oman سلطنة عمان', font=ttf, fill=500, direction='rtl') - target = 'Tests/images/test_direction_ltr.png' - target_img = Image.open(target) +def test_text_direction_ttb_stroke(): + ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", 50) - self.assert_image_similar(im, target_img, .5) + im = Image.new(mode="RGB", size=(100, 300)) + draw = ImageDraw.Draw(im) + try: + draw.text( + (27, 27), + "あい", + font=ttf, + fill=500, + direction="ttb", + stroke_width=2, + stroke_fill="#0f0", + ) + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + pytest.skip("libraqm 0.7 or greater not available") - def test_ligature_features(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + target = "Tests/images/test_direction_ttb_stroke.png" + assert_image_similar_tofile(im, target, 19.4) - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), 'filling', font=ttf, fill=500, features=['-liga']) - target = 'Tests/images/test_ligature_features.png' - target_img = Image.open(target) - self.assert_image_similar(im, target_img, .5) +def test_ligature_features(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - liga_size = ttf.getsize('fi', features=['-liga']) - self.assertEqual(liga_size,(13,19)) - - def test_kerning_features(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "filling", font=ttf, fill=500, features=["-liga"]) + target = "Tests/images/test_ligature_features.png" + assert_image_similar_tofile(im, target, 0.5) - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), 'TeToAV', font=ttf, fill=500, features=['-kern']) + liga_bbox = ttf.getbbox("fi", features=["-liga"]) + assert liga_bbox == (0, 4, 13, 19) - target = 'Tests/images/test_kerning_features.png' - target_img = Image.open(target) - self.assert_image_similar(im, target_img, .5) +def test_kerning_features(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - def test_arabictext_features(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "TeToAV", font=ttf, fill=500, features=["-kern"]) - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), 'اللغة العربية', font=ttf, fill=500, features=['-fina','-init','-medi']) + target = "Tests/images/test_kerning_features.png" + assert_image_similar_tofile(im, target, 0.5) - target = 'Tests/images/test_arabictext_features.png' - target_img = Image.open(target) - self.assert_image_similar(im, target_img, .5) +def test_arabictext_features(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) -if __name__ == '__main__': - unittest.main() + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text( + (0, 0), + "اللغة العربية", + font=ttf, + fill=500, + features=["-fina", "-init", "-medi"], + ) -# End of file + target = "Tests/images/test_arabictext_features.png" + assert_image_similar_tofile(im, target, 0.5) + + +def test_x_max_and_y_offset(): + ttf = ImageFont.truetype("Tests/fonts/ArefRuqaa-Regular.ttf", 40) + + im = Image.new(mode="RGB", size=(50, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "لح", font=ttf, fill=500) + + target = "Tests/images/test_x_max_and_y_offset.png" + assert_image_similar_tofile(im, target, 0.5) + + +def test_language(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "абвг", font=ttf, fill=500, language="sr") + + target = "Tests/images/test_language.png" + assert_image_similar_tofile(im, target, 0.5) + + +@pytest.mark.parametrize("mode", ("L", "1")) +@pytest.mark.parametrize( + "text, direction, expected", + ( + ("سلطنة عمان Oman", None, 173.703125), + ("سلطنة عمان Oman", "ltr", 173.703125), + ("Oman سلطنة عمان", "rtl", 173.703125), + ("English عربي", "rtl", 123.796875), + ("test", "ttb", 80.0), + ), + ids=("None", "ltr", "rtl2", "rtl", "ttb"), +) +def test_getlength(mode, text, direction, expected): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode, (1, 1), 0) + d = ImageDraw.Draw(im) + + try: + assert d.textlength(text, ttf, direction) == expected + except ValueError as ex: + if ( + direction == "ttb" + and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction" + ): + pytest.skip("libraqm 0.7 or greater not available") + + +@pytest.mark.parametrize("mode", ("L", "1")) +@pytest.mark.parametrize("direction", ("ltr", "ttb")) +@pytest.mark.parametrize( + "text", + ("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"), + ids=("caron-above", "caron-below", "double-breve", "overline"), +) +def test_getlength_combine(mode, direction, text): + if text == "i\u0305i" and direction == "ttb": + pytest.skip("fails with this font") + + ttf = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) + + try: + target = ttf.getlength("ii", mode, direction) + actual = ttf.getlength(text, mode, direction) + + assert actual == target + except ValueError as ex: + if ( + direction == "ttb" + and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction" + ): + pytest.skip("libraqm 0.7 or greater not available") + + +@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) +def test_anchor_ttb(anchor): + text = "f" + path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png" + f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120) + + im = Image.new("RGB", (200, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (200, 200)), "gray") + d.line(((100, 0), (100, 400)), "gray") + try: + d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f) + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + pytest.skip("libraqm 0.7 or greater not available") + + assert_image_similar_tofile(im, path, 1) # fails at 5 + + +combine_tests = ( + # extends above (e.g. issue #4553) + ("caron", "a\u030C\u030C\u030C\u030C\u030Cb", None, None, 0.08), + ("caron_la", "a\u030C\u030C\u030C\u030C\u030Cb", "la", None, 0.08), + ("caron_lt", "a\u030C\u030C\u030C\u030C\u030Cb", "lt", None, 0.08), + ("caron_ls", "a\u030C\u030C\u030C\u030C\u030Cb", "ls", None, 0.08), + ("caron_ttb", "ca" + ("\u030C" * 15) + "b", None, "ttb", 0.3), + ("caron_ttb_lt", "ca" + ("\u030C" * 15) + "b", "lt", "ttb", 0.3), + # extends below + ("caron_below", "a\u032C\u032C\u032C\u032C\u032Cb", None, None, 0.02), + ("caron_below_ld", "a\u032C\u032C\u032C\u032C\u032Cb", "ld", None, 0.02), + ("caron_below_lb", "a\u032C\u032C\u032C\u032C\u032Cb", "lb", None, 0.02), + ("caron_below_ls", "a\u032C\u032C\u032C\u032C\u032Cb", "ls", None, 0.02), + ("caron_below_ttb", "a" + ("\u032C" * 15) + "b", None, "ttb", 0.03), + ("caron_below_ttb_lb", "a" + ("\u032C" * 15) + "b", "lb", "ttb", 0.03), + # extends to the right (e.g. issue #3745) + ("double_breve_below", "a\u035Ci", None, None, 0.02), + ("double_breve_below_ma", "a\u035Ci", "ma", None, 0.02), + ("double_breve_below_ra", "a\u035Ci", "ra", None, 0.02), + ("double_breve_below_ttb", "a\u035Cb", None, "ttb", 0.02), + ("double_breve_below_ttb_rt", "a\u035Cb", "rt", "ttb", 0.02), + ("double_breve_below_ttb_mt", "a\u035Cb", "mt", "ttb", 0.02), + ("double_breve_below_ttb_st", "a\u035Cb", "st", "ttb", 0.02), + # extends to the left (fail=0.064) + ("overline", "i\u0305", None, None, 0.02), + ("overline_la", "i\u0305", "la", None, 0.02), + ("overline_ra", "i\u0305", "ra", None, 0.02), + ("overline_ttb", "i\u0305", None, "ttb", 0.02), + ("overline_ttb_rt", "i\u0305", "rt", "ttb", 0.02), + ("overline_ttb_mt", "i\u0305", "mt", "ttb", 0.02), + ("overline_ttb_st", "i\u0305", "st", "ttb", 0.02), +) + + +# this tests various combining characters for anchor alignment and clipping +@pytest.mark.parametrize( + "name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests] +) +def test_combine(name, text, dir, anchor, epsilon): + path = f"Tests/images/test_combine_{name}.png" + f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (400, 200)), "gray") + d.line(((200, 0), (200, 400)), "gray") + try: + d.text((200, 200), text, fill="black", anchor=anchor, direction=dir, font=f) + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + pytest.skip("libraqm 0.7 or greater not available") + + assert_image_similar_tofile(im, path, epsilon) + + +@pytest.mark.parametrize( + "anchor, align", + ( + ("lm", "left"), # pass with getsize + ("lm", "center"), # fail at 2.12 + ("lm", "right"), # fail at 2.57 + ("mm", "left"), # fail at 2.12 + ("mm", "center"), # pass with getsize + ("mm", "right"), # fail at 2.12 + ("rm", "left"), # fail at 2.57 + ("rm", "center"), # fail at 2.12 + ("rm", "right"), # pass with getsize + ), +) +def test_combine_multiline(anchor, align): + # test that multiline text uses getlength, not getsize or getbbox + + path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png" + f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) + text = "i\u0305\u035C\ntext" # i with overline and double breve, and a word + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (400, 200)), "gray") + d.line(((200, 0), (200, 400)), "gray") + bbox = d.multiline_textbbox((200, 200), text, anchor=anchor, font=f, align=align) + d.rectangle(bbox, outline="red") + d.multiline_text((200, 200), text, fill="black", anchor=anchor, font=f, align=align) + + assert_image_similar_tofile(im, path, 0.015) + + +def test_anchor_invalid_ttb(): + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new("RGB", (100, 100), "white") + d = ImageDraw.Draw(im) + d.font = font + + for anchor in ["", "l", "a", "lax", "xa", "la", "ls", "ld", "lx"]: + pytest.raises( + ValueError, lambda: font.getmask2("hello", anchor=anchor, direction="ttb") + ) + pytest.raises( + ValueError, lambda: font.getbbox("hello", anchor=anchor, direction="ttb") + ) + pytest.raises( + ValueError, lambda: d.text((0, 0), "hello", anchor=anchor, direction="ttb") + ) + pytest.raises( + ValueError, + lambda: d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb"), + ) + pytest.raises( + ValueError, + lambda: d.multiline_text( + (0, 0), "foo\nbar", anchor=anchor, direction="ttb" + ), + ) + pytest.raises( + ValueError, + lambda: d.multiline_textbbox( + (0, 0), "foo\nbar", anchor=anchor, direction="ttb" + ), + ) + # ttb multiline text does not support anchors at all + pytest.raises( + ValueError, + lambda: d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb"), + ) + pytest.raises( + ValueError, + lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb"), + ) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index b2edffa5788..fa2291582d4 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,49 +1,93 @@ -from helper import unittest, PillowTestCase, on_appveyor - +import os +import subprocess import sys -try: - from PIL import ImageGrab +import pytest - class TestImageGrab(PillowTestCase): +from PIL import Image, ImageGrab - @unittest.skipIf(on_appveyor(), "Test fails on appveyor") - def test_grab(self): - im = ImageGrab.grab() - self.assert_image(im, im.mode, im.size) +from .helper import assert_image_equal_tofile, skip_unless_feature - @unittest.skipIf(on_appveyor(), "Test fails on appveyor") - def test_grab2(self): - im = ImageGrab.grab() - self.assert_image(im, im.mode, im.size) -except ImportError: - class TestImageGrab(PillowTestCase): - def test_skip(self): - self.skipTest("ImportError") +class TestImageGrab: + @pytest.mark.skipif( + sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS" + ) + def test_grab(self): + ImageGrab.grab() + ImageGrab.grab(include_layered_windows=True) + ImageGrab.grab(all_screens=True) + im = ImageGrab.grab(bbox=(10, 20, 50, 80)) + assert im.size == (40, 60) -class TestImageGrabImport(PillowTestCase): + @skip_unless_feature("xcb") + def test_grab_x11(self): + try: + if sys.platform not in ("win32", "darwin"): + ImageGrab.grab() - def test_import(self): - # Arrange - exception = None + ImageGrab.grab(xdisplay="") + except OSError as e: + pytest.skip(str(e)) - # Act - try: - from PIL import ImageGrab - ImageGrab.__name__ # dummy to prevent Pyflakes warning - except Exception as e: - exception = e - - # Assert - if sys.platform in ["win32", "darwin"]: - self.assertIsNone(exception) + @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") + def test_grab_no_xcb(self): + if sys.platform not in ("win32", "darwin"): + with pytest.raises(OSError) as e: + ImageGrab.grab() + assert str(e.value).startswith("Pillow was built without XCB support") + + with pytest.raises(OSError) as e: + ImageGrab.grab(xdisplay="") + assert str(e.value).startswith("Pillow was built without XCB support") + + @skip_unless_feature("xcb") + def test_grab_invalid_xdisplay(self): + with pytest.raises(OSError) as e: + ImageGrab.grab(xdisplay="error.test:0.0") + assert str(e.value).startswith("X connection failed") + + def test_grabclipboard(self): + if sys.platform == "darwin": + subprocess.call(["screencapture", "-cx"]) + elif sys.platform == "win32": + p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + p.stdin.write( + b"""[Reflection.Assembly]::LoadWithPartialName("System.Drawing") +[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") +$bmp = New-Object Drawing.Bitmap 200, 200 +[Windows.Forms.Clipboard]::SetImage($bmp)""" + ) + p.communicate() else: - self.assertIsInstance(exception, ImportError) - self.assertEqual(str(exception), - "ImageGrab is macOS and Windows only") + with pytest.raises(NotImplementedError) as e: + ImageGrab.grabclipboard() + assert str(e.value) == "ImageGrab.grabclipboard() is macOS and Windows only" + return + + ImageGrab.grabclipboard() + + @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") + def test_grabclipboard_file(self): + p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') + p.communicate() + + im = ImageGrab.grabclipboard() + assert len(im) == 1 + assert os.path.samefile(im[0], "Tests/images/hopper.gif") + @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") + def test_grabclipboard_png(self): + p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + p.stdin.write( + rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") +$ms = new-object System.IO.MemoryStream(, $bytes) +[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") +[Windows.Forms.Clipboard]::SetData("PNG", $ms)""" + ) + p.communicate() -if __name__ == '__main__': - unittest.main() + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, "Tests/images/hopper.png") diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 4f99eda9337..fe7ac9a7a93 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -1,23 +1,20 @@ -from __future__ import print_function -from helper import unittest, PillowTestCase +import pytest -from PIL import Image -from PIL import ImageMath +from PIL import Image, ImageMath def pixel(im): if hasattr(im, "im"): - return "%s %r" % (im.mode, im.getpixel((0, 0))) - else: - if isinstance(im, int): - return int(im) # hack to deal with booleans - print(im) + return f"{im.mode} {repr(im.getpixel((0, 0)))}" + if isinstance(im, int): + return int(im) # hack to deal with booleans + A = Image.new("L", (1, 1), 1) B = Image.new("L", (1, 1), 2) Z = Image.new("L", (1, 1), 0) # Z for zero F = Image.new("F", (1, 1), 3) -I = Image.new("I", (1, 1), 4) +I = Image.new("I", (1, 1), 4) # noqa: E741 A2 = A.resize((2, 2)) B2 = B.resize((2, 2)) @@ -25,163 +22,181 @@ def pixel(im): images = {"A": A, "B": B, "F": F, "I": I} -class TestImageMath(PillowTestCase): - - def test_sanity(self): - self.assertEqual(ImageMath.eval("1"), 1) - self.assertEqual(ImageMath.eval("1+A", A=2), 3) - self.assertEqual(pixel(ImageMath.eval("A+B", A=A, B=B)), "I 3") - self.assertEqual(pixel(ImageMath.eval("A+B", images)), "I 3") - self.assertEqual(pixel(ImageMath.eval("float(A)+B", images)), "F 3.0") - self.assertEqual(pixel( - ImageMath.eval("int(float(A)+B)", images)), "I 3") - - def test_ops(self): - - self.assertEqual(pixel(ImageMath.eval("-A", images)), "I -1") - self.assertEqual(pixel(ImageMath.eval("+B", images)), "L 2") - - self.assertEqual(pixel(ImageMath.eval("A+B", images)), "I 3") - self.assertEqual(pixel(ImageMath.eval("A-B", images)), "I -1") - self.assertEqual(pixel(ImageMath.eval("A*B", images)), "I 2") - self.assertEqual(pixel(ImageMath.eval("A/B", images)), "I 0") - self.assertEqual(pixel(ImageMath.eval("B**2", images)), "I 4") - self.assertEqual(pixel( - ImageMath.eval("B**33", images)), "I 2147483647") - - self.assertEqual(pixel(ImageMath.eval("float(A)+B", images)), "F 3.0") - self.assertEqual(pixel(ImageMath.eval("float(A)-B", images)), "F -1.0") - self.assertEqual(pixel(ImageMath.eval("float(A)*B", images)), "F 2.0") - self.assertEqual(pixel(ImageMath.eval("float(A)/B", images)), "F 0.5") - self.assertEqual(pixel(ImageMath.eval("float(B)**2", images)), "F 4.0") - self.assertEqual(pixel( - ImageMath.eval("float(B)**33", images)), "F 8589934592.0") - - def test_logical(self): - self.assertEqual(pixel(ImageMath.eval("not A", images)), 0) - self.assertEqual(pixel(ImageMath.eval("A and B", images)), "L 2") - self.assertEqual(pixel(ImageMath.eval("A or B", images)), "L 1") - - def test_convert(self): - self.assertEqual(pixel( - ImageMath.eval("convert(A+B, 'L')", images)), "L 3") - self.assertEqual(pixel( - ImageMath.eval("convert(A+B, '1')", images)), "1 0") - self.assertEqual(pixel( - ImageMath.eval("convert(A+B, 'RGB')", images)), "RGB (3, 3, 3)") - - def test_compare(self): - self.assertEqual(pixel(ImageMath.eval("min(A, B)", images)), "I 1") - self.assertEqual(pixel(ImageMath.eval("max(A, B)", images)), "I 2") - self.assertEqual(pixel(ImageMath.eval("A == 1", images)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A == 2", images)), "I 0") - - def test_one_image_larger(self): - self.assertEqual(pixel(ImageMath.eval("A+B", A=A2, B=B)), "I 3") - self.assertEqual(pixel(ImageMath.eval("A+B", A=A, B=B2)), "I 3") - - def test_abs(self): - self.assertEqual(pixel(ImageMath.eval("abs(A)", A=A)), "I 1") - self.assertEqual(pixel(ImageMath.eval("abs(B)", B=B)), "I 2") - - def test_binary_mod(self): - self.assertEqual(pixel(ImageMath.eval("A%A", A=A)), "I 0") - self.assertEqual(pixel(ImageMath.eval("B%B", B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("A%B", A=A, B=B)), "I 1") - self.assertEqual(pixel(ImageMath.eval("B%A", A=A, B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("Z%A", A=A, Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("Z%B", B=B, Z=Z)), "I 0") - - def test_bitwise_invert(self): - self.assertEqual(pixel(ImageMath.eval("~Z", Z=Z)), "I -1") - self.assertEqual(pixel(ImageMath.eval("~A", A=A)), "I -2") - self.assertEqual(pixel(ImageMath.eval("~B", B=B)), "I -3") - - def test_bitwise_and(self): - self.assertEqual(pixel(ImageMath.eval("Z&Z", A=A, Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("Z&A", A=A, Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("A&Z", A=A, Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("A&A", A=A, Z=Z)), "I 1") - - def test_bitwise_or(self): - self.assertEqual(pixel(ImageMath.eval("Z|Z", A=A, Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("Z|A", A=A, Z=Z)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A|Z", A=A, Z=Z)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A|A", A=A, Z=Z)), "I 1") - - def test_bitwise_xor(self): - self.assertEqual(pixel(ImageMath.eval("Z^Z", A=A, Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("Z^A", A=A, Z=Z)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A^Z", A=A, Z=Z)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A^A", A=A, Z=Z)), "I 0") - - def test_bitwise_leftshift(self): - self.assertEqual(pixel(ImageMath.eval("Z<<0", Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("Z<<1", Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("A<<0", A=A)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A<<1", A=A)), "I 2") - - def test_bitwise_rightshift(self): - self.assertEqual(pixel(ImageMath.eval("Z>>0", Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("Z>>1", Z=Z)), "I 0") - self.assertEqual(pixel(ImageMath.eval("A>>0", A=A)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A>>1", A=A)), "I 0") - - def test_logical_eq(self): - self.assertEqual(pixel(ImageMath.eval("A==A", A=A)), "I 1") - self.assertEqual(pixel(ImageMath.eval("B==B", B=B)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A==B", A=A, B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("B==A", A=A, B=B)), "I 0") - - def test_logical_ne(self): - self.assertEqual(pixel(ImageMath.eval("A!=A", A=A)), "I 0") - self.assertEqual(pixel(ImageMath.eval("B!=B", B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("A!=B", A=A, B=B)), "I 1") - self.assertEqual(pixel(ImageMath.eval("B!=A", A=A, B=B)), "I 1") - - def test_logical_lt(self): - self.assertEqual(pixel(ImageMath.eval("AA", A=A)), "I 0") - self.assertEqual(pixel(ImageMath.eval("B>B", B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("A>B", A=A, B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("B>A", A=A, B=B)), "I 1") - - def test_logical_ge(self): - self.assertEqual(pixel(ImageMath.eval("A>=A", A=A)), "I 1") - self.assertEqual(pixel(ImageMath.eval("B>=B", B=B)), "I 1") - self.assertEqual(pixel(ImageMath.eval("A>=B", A=A, B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("B>=A", A=A, B=B)), "I 1") - - def test_logical_equal(self): - self.assertEqual(pixel(ImageMath.eval("equal(A, A)", A=A)), "I 1") - self.assertEqual(pixel(ImageMath.eval("equal(B, B)", B=B)), "I 1") - self.assertEqual(pixel(ImageMath.eval("equal(Z, Z)", Z=Z)), "I 1") - self.assertEqual(pixel(ImageMath.eval("equal(A, B)", A=A, B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("equal(B, A)", A=A, B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("equal(A, Z)", A=A, Z=Z)), "I 0") - - def test_logical_not_equal(self): - self.assertEqual(pixel(ImageMath.eval("notequal(A, A)", A=A)), "I 0") - self.assertEqual(pixel(ImageMath.eval("notequal(B, B)", B=B)), "I 0") - self.assertEqual(pixel(ImageMath.eval("notequal(Z, Z)", Z=Z)), "I 0") - self.assertEqual( - pixel(ImageMath.eval("notequal(A, B)", A=A, B=B)), "I 1") - self.assertEqual( - pixel(ImageMath.eval("notequal(B, A)", A=A, B=B)), "I 1") - self.assertEqual( - pixel(ImageMath.eval("notequal(A, Z)", A=A, Z=Z)), "I 1") - - -if __name__ == '__main__': - unittest.main() +def test_sanity(): + assert ImageMath.eval("1") == 1 + assert ImageMath.eval("1+A", A=2) == 3 + assert pixel(ImageMath.eval("A+B", A=A, B=B)) == "I 3" + assert pixel(ImageMath.eval("A+B", images)) == "I 3" + assert pixel(ImageMath.eval("float(A)+B", images)) == "F 3.0" + assert pixel(ImageMath.eval("int(float(A)+B)", images)) == "I 3" + + +def test_ops(): + assert pixel(ImageMath.eval("-A", images)) == "I -1" + assert pixel(ImageMath.eval("+B", images)) == "L 2" + + assert pixel(ImageMath.eval("A+B", images)) == "I 3" + assert pixel(ImageMath.eval("A-B", images)) == "I -1" + assert pixel(ImageMath.eval("A*B", images)) == "I 2" + assert pixel(ImageMath.eval("A/B", images)) == "I 0" + assert pixel(ImageMath.eval("B**2", images)) == "I 4" + assert pixel(ImageMath.eval("B**33", images)) == "I 2147483647" + + assert pixel(ImageMath.eval("float(A)+B", images)) == "F 3.0" + assert pixel(ImageMath.eval("float(A)-B", images)) == "F -1.0" + assert pixel(ImageMath.eval("float(A)*B", images)) == "F 2.0" + assert pixel(ImageMath.eval("float(A)/B", images)) == "F 0.5" + assert pixel(ImageMath.eval("float(B)**2", images)) == "F 4.0" + assert pixel(ImageMath.eval("float(B)**33", images)) == "F 8589934592.0" + + +@pytest.mark.parametrize( + "expression", + ( + "exec('pass')", + "(lambda: exec('pass'))()", + "(lambda: (lambda: exec('pass'))())()", + ), +) +def test_prevent_exec(expression): + with pytest.raises(ValueError): + ImageMath.eval(expression) + + +def test_logical(): + assert pixel(ImageMath.eval("not A", images)) == 0 + assert pixel(ImageMath.eval("A and B", images)) == "L 2" + assert pixel(ImageMath.eval("A or B", images)) == "L 1" + + +def test_convert(): + assert pixel(ImageMath.eval("convert(A+B, 'L')", images)) == "L 3" + assert pixel(ImageMath.eval("convert(A+B, '1')", images)) == "1 0" + assert pixel(ImageMath.eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)" + + +def test_compare(): + assert pixel(ImageMath.eval("min(A, B)", images)) == "I 1" + assert pixel(ImageMath.eval("max(A, B)", images)) == "I 2" + assert pixel(ImageMath.eval("A == 1", images)) == "I 1" + assert pixel(ImageMath.eval("A == 2", images)) == "I 0" + + +def test_one_image_larger(): + assert pixel(ImageMath.eval("A+B", A=A2, B=B)) == "I 3" + assert pixel(ImageMath.eval("A+B", A=A, B=B2)) == "I 3" + + +def test_abs(): + assert pixel(ImageMath.eval("abs(A)", A=A)) == "I 1" + assert pixel(ImageMath.eval("abs(B)", B=B)) == "I 2" + + +def test_binary_mod(): + assert pixel(ImageMath.eval("A%A", A=A)) == "I 0" + assert pixel(ImageMath.eval("B%B", B=B)) == "I 0" + assert pixel(ImageMath.eval("A%B", A=A, B=B)) == "I 1" + assert pixel(ImageMath.eval("B%A", A=A, B=B)) == "I 0" + assert pixel(ImageMath.eval("Z%A", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.eval("Z%B", B=B, Z=Z)) == "I 0" + + +def test_bitwise_invert(): + assert pixel(ImageMath.eval("~Z", Z=Z)) == "I -1" + assert pixel(ImageMath.eval("~A", A=A)) == "I -2" + assert pixel(ImageMath.eval("~B", B=B)) == "I -3" + + +def test_bitwise_and(): + assert pixel(ImageMath.eval("Z&Z", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.eval("Z&A", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.eval("A&Z", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.eval("A&A", A=A, Z=Z)) == "I 1" + + +def test_bitwise_or(): + assert pixel(ImageMath.eval("Z|Z", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.eval("Z|A", A=A, Z=Z)) == "I 1" + assert pixel(ImageMath.eval("A|Z", A=A, Z=Z)) == "I 1" + assert pixel(ImageMath.eval("A|A", A=A, Z=Z)) == "I 1" + + +def test_bitwise_xor(): + assert pixel(ImageMath.eval("Z^Z", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.eval("Z^A", A=A, Z=Z)) == "I 1" + assert pixel(ImageMath.eval("A^Z", A=A, Z=Z)) == "I 1" + assert pixel(ImageMath.eval("A^A", A=A, Z=Z)) == "I 0" + + +def test_bitwise_leftshift(): + assert pixel(ImageMath.eval("Z<<0", Z=Z)) == "I 0" + assert pixel(ImageMath.eval("Z<<1", Z=Z)) == "I 0" + assert pixel(ImageMath.eval("A<<0", A=A)) == "I 1" + assert pixel(ImageMath.eval("A<<1", A=A)) == "I 2" + + +def test_bitwise_rightshift(): + assert pixel(ImageMath.eval("Z>>0", Z=Z)) == "I 0" + assert pixel(ImageMath.eval("Z>>1", Z=Z)) == "I 0" + assert pixel(ImageMath.eval("A>>0", A=A)) == "I 1" + assert pixel(ImageMath.eval("A>>1", A=A)) == "I 0" + + +def test_logical_eq(): + assert pixel(ImageMath.eval("A==A", A=A)) == "I 1" + assert pixel(ImageMath.eval("B==B", B=B)) == "I 1" + assert pixel(ImageMath.eval("A==B", A=A, B=B)) == "I 0" + assert pixel(ImageMath.eval("B==A", A=A, B=B)) == "I 0" + + +def test_logical_ne(): + assert pixel(ImageMath.eval("A!=A", A=A)) == "I 0" + assert pixel(ImageMath.eval("B!=B", B=B)) == "I 0" + assert pixel(ImageMath.eval("A!=B", A=A, B=B)) == "I 1" + assert pixel(ImageMath.eval("B!=A", A=A, B=B)) == "I 1" + + +def test_logical_lt(): + assert pixel(ImageMath.eval("AA", A=A)) == "I 0" + assert pixel(ImageMath.eval("B>B", B=B)) == "I 0" + assert pixel(ImageMath.eval("A>B", A=A, B=B)) == "I 0" + assert pixel(ImageMath.eval("B>A", A=A, B=B)) == "I 1" + + +def test_logical_ge(): + assert pixel(ImageMath.eval("A>=A", A=A)) == "I 1" + assert pixel(ImageMath.eval("B>=B", B=B)) == "I 1" + assert pixel(ImageMath.eval("A>=B", A=A, B=B)) == "I 0" + assert pixel(ImageMath.eval("B>=A", A=A, B=B)) == "I 1" + + +def test_logical_equal(): + assert pixel(ImageMath.eval("equal(A, A)", A=A)) == "I 1" + assert pixel(ImageMath.eval("equal(B, B)", B=B)) == "I 1" + assert pixel(ImageMath.eval("equal(Z, Z)", Z=Z)) == "I 1" + assert pixel(ImageMath.eval("equal(A, B)", A=A, B=B)) == "I 0" + assert pixel(ImageMath.eval("equal(B, A)", A=A, B=B)) == "I 0" + assert pixel(ImageMath.eval("equal(A, Z)", A=A, Z=Z)) == "I 0" + + +def test_logical_not_equal(): + assert pixel(ImageMath.eval("notequal(A, A)", A=A)) == "I 0" + assert pixel(ImageMath.eval("notequal(B, B)", B=B)) == "I 0" + assert pixel(ImageMath.eval("notequal(Z, Z)", Z=Z)) == "I 0" + assert pixel(ImageMath.eval("notequal(A, B)", A=A, B=B)) == "I 1" + assert pixel(ImageMath.eval("notequal(B, A)", A=A, B=B)) == "I 1" + assert pixel(ImageMath.eval("notequal(A, Z)", A=A, Z=Z)) == "I 1" diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index b51b212e0db..29c71f917c4 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -1,289 +1,339 @@ # Test the ImageMorphology functionality -from helper import unittest, PillowTestCase, hopper - -from PIL import Image -from PIL import ImageMorph - - -class MorphTests(PillowTestCase): - - def setUp(self): - self.A = self.string_to_img( - """ - ....... - ....... - ..111.. - ..111.. - ..111.. - ....... - ....... - """ - ) - - def img_to_string(self, im): - """Turn a (small) binary image into a string representation""" - chars = '.1' - width, height = im.size - return '\n'.join( - ''.join(chars[im.getpixel((c, r)) > 0] for c in range(width)) - for r in range(height)) - - def string_to_img(self, image_string): - """Turn a string image representation into a binary image""" - rows = [s for s in image_string.replace(' ', '').split('\n') - if len(s)] - height = len(rows) - width = len(rows[0]) - im = Image.new('L', (width, height)) - for i in range(width): - for j in range(height): - c = rows[j][i] - v = c in 'X1' - im.putpixel((i, j), v) - - return im - - def img_string_normalize(self, im): - return self.img_to_string(self.string_to_img(im)) - - def assert_img_equal(self, A, B): - self.assertEqual(self.img_to_string(A), self.img_to_string(B)) - - def assert_img_equal_img_string(self, A, Bstring): - self.assertEqual( - self.img_to_string(A), - self.img_string_normalize(Bstring)) - - def test_str_to_img(self): - im = Image.open('Tests/images/morph_a.png') - self.assert_image_equal(self.A, im) - - def create_lut(self): - for op in ( - 'corner', 'dilation4', 'dilation8', - 'erosion4', 'erosion8', 'edge'): - lb = ImageMorph.LutBuilder(op_name=op) - lut = lb.build_lut() - with open('Tests/images/%s.lut' % op, 'wb') as f: - f.write(lut) - - # create_lut() - def test_lut(self): - for op in ( - 'corner', 'dilation4', 'dilation8', - 'erosion4', 'erosion8', 'edge'): - lb = ImageMorph.LutBuilder(op_name=op) - self.assertIsNone(lb.get_lut()) - - lut = lb.build_lut() - with open('Tests/images/%s.lut' % op, 'rb') as f: - self.assertEqual(lut, bytearray(f.read())) - - def test_no_operator_loaded(self): - mop = ImageMorph.MorphOp() - self.assertRaises(Exception, mop.apply, None) - self.assertRaises(Exception, mop.match, None) - self.assertRaises(Exception, mop.save_lut, None) - - # Test the named patterns - def test_erosion8(self): - # erosion8 - mop = ImageMorph.MorphOp(op_name='erosion8') - count, Aout = mop.apply(self.A) - self.assertEqual(count, 8) - self.assert_img_equal_img_string(Aout, - """ - ....... - ....... - ....... - ...1... - ....... - ....... - ....... - """) - - def test_dialation8(self): - # dialation8 - mop = ImageMorph.MorphOp(op_name='dilation8') - count, Aout = mop.apply(self.A) - self.assertEqual(count, 16) - self.assert_img_equal_img_string(Aout, - """ - ....... - .11111. - .11111. - .11111. - .11111. - .11111. - ....... - """) - - def test_erosion4(self): - # erosion4 - mop = ImageMorph.MorphOp(op_name='dilation4') - count, Aout = mop.apply(self.A) - self.assertEqual(count, 12) - self.assert_img_equal_img_string(Aout, - """ - ....... - ..111.. - .11111. - .11111. - .11111. - ..111.. - ....... - """) - - def test_edge(self): - # edge - mop = ImageMorph.MorphOp(op_name='edge') - count, Aout = mop.apply(self.A) - self.assertEqual(count, 1) - self.assert_img_equal_img_string(Aout, - """ - ....... - ....... - ..111.. - ..1.1.. - ..111.. - ....... - ....... - """) - - def test_corner(self): - # Create a corner detector pattern - mop = ImageMorph.MorphOp(patterns=['1:(... ... ...)->0', - '4:(00. 01. ...)->1']) - count, Aout = mop.apply(self.A) - self.assertEqual(count, 5) - self.assert_img_equal_img_string(Aout, - """ - ....... - ....... - ..1.1.. - ....... - ..1.1.. - ....... - ....... - """) - - # Test the coordinate counting with the same operator - coords = mop.match(self.A) - self.assertEqual(len(coords), 4) - self.assertEqual(tuple(coords), ((2, 2), (4, 2), (2, 4), (4, 4))) - - coords = mop.get_on_pixels(Aout) - self.assertEqual(len(coords), 4) - self.assertEqual(tuple(coords), ((2, 2), (4, 2), (2, 4), (4, 4))) - - def test_mirroring(self): - # Test 'M' for mirroring - mop = ImageMorph.MorphOp(patterns=['1:(... ... ...)->0', - 'M:(00. 01. ...)->1']) - count, Aout = mop.apply(self.A) - self.assertEqual(count, 7) - self.assert_img_equal_img_string(Aout, - """ - ....... - ....... - ..1.1.. - ....... - ....... - ....... - ....... - """) - - def test_negate(self): - # Test 'N' for negate - mop = ImageMorph.MorphOp(patterns=['1:(... ... ...)->0', - 'N:(00. 01. ...)->1']) - count, Aout = mop.apply(self.A) - self.assertEqual(count, 8) - self.assert_img_equal_img_string(Aout, - """ - ....... - ....... - ..1.... - ....... - ....... - ....... - ....... - """) - - def test_non_binary_images(self): - im = hopper('RGB') - mop = ImageMorph.MorphOp(op_name="erosion8") - - self.assertRaises(Exception, mop.apply, im) - self.assertRaises(Exception, mop.match, im) - self.assertRaises(Exception, mop.get_on_pixels, im) - - def test_add_patterns(self): - # Arrange - lb = ImageMorph.LutBuilder(op_name='corner') - self.assertEqual(lb.patterns, ['1:(... ... ...)->0', - '4:(00. 01. ...)->1']) - new_patterns = ['M:(00. 01. ...)->1', - 'N:(00. 01. ...)->1'] - - # Act - lb.add_patterns(new_patterns) - - # Assert - self.assertEqual( - lb.patterns, - ['1:(... ... ...)->0', - '4:(00. 01. ...)->1', - 'M:(00. 01. ...)->1', - 'N:(00. 01. ...)->1']) - - def test_unknown_pattern(self): - self.assertRaises( - Exception, - ImageMorph.LutBuilder, op_name='unknown') - - def test_pattern_syntax_error(self): - # Arrange - lb = ImageMorph.LutBuilder(op_name='corner') - new_patterns = ['a pattern with a syntax error'] - lb.add_patterns(new_patterns) - - # Act / Assert - self.assertRaises(Exception, lb.build_lut) - - def test_load_invalid_mrl(self): - # Arrange - invalid_mrl = 'Tests/images/hopper.png' - mop = ImageMorph.MorphOp() - - # Act / Assert - self.assertRaises(Exception, mop.load_lut, invalid_mrl) - - def test_roundtrip_mrl(self): - # Arrange - tempfile = self.tempfile('temp.mrl') - mop = ImageMorph.MorphOp(op_name='corner') - initial_lut = mop.lut - - # Act - mop.save_lut(tempfile) - mop.load_lut(tempfile) - - # Act / Assert - self.assertEqual(mop.lut, initial_lut) - - def test_set_lut(self): - # Arrange - lb = ImageMorph.LutBuilder(op_name='corner') +import pytest + +from PIL import Image, ImageMorph, _imagingmorph + +from .helper import assert_image_equal_tofile, hopper + + +def string_to_img(image_string): + """Turn a string image representation into a binary image""" + rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)] + height = len(rows) + width = len(rows[0]) + im = Image.new("L", (width, height)) + for i in range(width): + for j in range(height): + c = rows[j][i] + v = c in "X1" + im.putpixel((i, j), v) + + return im + + +A = string_to_img( + """ + ....... + ....... + ..111.. + ..111.. + ..111.. + ....... + ....... + """ +) + + +def img_to_string(im): + """Turn a (small) binary image into a string representation""" + chars = ".1" + width, height = im.size + return "\n".join( + "".join(chars[im.getpixel((c, r)) > 0] for c in range(width)) + for r in range(height) + ) + + +def img_string_normalize(im): + return img_to_string(string_to_img(im)) + + +def assert_img_equal_img_string(a, b_string): + assert img_to_string(a) == img_string_normalize(b_string) + + +def test_str_to_img(): + assert_image_equal_tofile(A, "Tests/images/morph_a.png") + + +def create_lut(): + for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): + lb = ImageMorph.LutBuilder(op_name=op) lut = lb.build_lut() - mop = ImageMorph.MorphOp() + with open(f"Tests/images/{op}.lut", "wb") as f: + f.write(lut) + + +# create_lut() +@pytest.mark.parametrize( + "op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge") +) +def test_lut(op): + lb = ImageMorph.LutBuilder(op_name=op) + assert lb.get_lut() is None + + lut = lb.build_lut() + with open(f"Tests/images/{op}.lut", "rb") as f: + assert lut == bytearray(f.read()) + + +def test_no_operator_loaded(): + mop = ImageMorph.MorphOp() + with pytest.raises(Exception) as e: + mop.apply(None) + assert str(e.value) == "No operator loaded" + with pytest.raises(Exception) as e: + mop.match(None) + assert str(e.value) == "No operator loaded" + with pytest.raises(Exception) as e: + mop.save_lut(None) + assert str(e.value) == "No operator loaded" + + +# Test the named patterns +def test_erosion8(): + # erosion8 + mop = ImageMorph.MorphOp(op_name="erosion8") + count, Aout = mop.apply(A) + assert count == 8 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ....... + ...1... + ....... + ....... + ....... + """, + ) + + +def test_dialation8(): + # dialation8 + mop = ImageMorph.MorphOp(op_name="dilation8") + count, Aout = mop.apply(A) + assert count == 16 + assert_img_equal_img_string( + Aout, + """ + ....... + .11111. + .11111. + .11111. + .11111. + .11111. + ....... + """, + ) + + +def test_erosion4(): + # erosion4 + mop = ImageMorph.MorphOp(op_name="dilation4") + count, Aout = mop.apply(A) + assert count == 12 + assert_img_equal_img_string( + Aout, + """ + ....... + ..111.. + .11111. + .11111. + .11111. + ..111.. + ....... + """, + ) + + +def test_edge(): + # edge + mop = ImageMorph.MorphOp(op_name="edge") + count, Aout = mop.apply(A) + assert count == 1 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ..111.. + ..1.1.. + ..111.. + ....... + ....... + """, + ) + + +def test_corner(): + # Create a corner detector pattern + mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) + count, Aout = mop.apply(A) + assert count == 5 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ..1.1.. + ....... + ..1.1.. + ....... + ....... + """, + ) + + # Test the coordinate counting with the same operator + coords = mop.match(A) + assert len(coords) == 4 + assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) + + coords = mop.get_on_pixels(Aout) + assert len(coords) == 4 + assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) + + +def test_mirroring(): + # Test 'M' for mirroring + mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "M:(00. 01. ...)->1"]) + count, Aout = mop.apply(A) + assert count == 7 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ..1.1.. + ....... + ....... + ....... + ....... + """, + ) + + +def test_negate(): + # Test 'N' for negate + mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "N:(00. 01. ...)->1"]) + count, Aout = mop.apply(A) + assert count == 8 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ..1.... + ....... + ....... + ....... + ....... + """, + ) + + +def test_incorrect_mode(): + im = hopper("RGB") + mop = ImageMorph.MorphOp(op_name="erosion8") + + with pytest.raises(ValueError) as e: + mop.apply(im) + assert str(e.value) == "Image mode must be L" + with pytest.raises(ValueError) as e: + mop.match(im) + assert str(e.value) == "Image mode must be L" + with pytest.raises(ValueError) as e: + mop.get_on_pixels(im) + assert str(e.value) == "Image mode must be L" + + +def test_add_patterns(): + # Arrange + lb = ImageMorph.LutBuilder(op_name="corner") + assert lb.patterns == ["1:(... ... ...)->0", "4:(00. 01. ...)->1"] + new_patterns = ["M:(00. 01. ...)->1", "N:(00. 01. ...)->1"] + + # Act + lb.add_patterns(new_patterns) + + # Assert + assert lb.patterns == [ + "1:(... ... ...)->0", + "4:(00. 01. ...)->1", + "M:(00. 01. ...)->1", + "N:(00. 01. ...)->1", + ] + + +def test_unknown_pattern(): + with pytest.raises(Exception): + ImageMorph.LutBuilder(op_name="unknown") + + +def test_pattern_syntax_error(): + # Arrange + lb = ImageMorph.LutBuilder(op_name="corner") + new_patterns = ["a pattern with a syntax error"] + lb.add_patterns(new_patterns) + + # Act / Assert + with pytest.raises(Exception) as e: + lb.build_lut() + assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"' + + +def test_load_invalid_mrl(): + # Arrange + invalid_mrl = "Tests/images/hopper.png" + mop = ImageMorph.MorphOp() + + # Act / Assert + with pytest.raises(Exception) as e: + mop.load_lut(invalid_mrl) + assert str(e.value) == "Wrong size operator file!" + + +def test_roundtrip_mrl(tmp_path): + # Arrange + tempfile = str(tmp_path / "temp.mrl") + mop = ImageMorph.MorphOp(op_name="corner") + initial_lut = mop.lut + + # Act + mop.save_lut(tempfile) + mop.load_lut(tempfile) + + # Act / Assert + assert mop.lut == initial_lut + + +def test_set_lut(): + # Arrange + lb = ImageMorph.LutBuilder(op_name="corner") + lut = lb.build_lut() + mop = ImageMorph.MorphOp() + + # Act + mop.set_lut(lut) + + # Assert + assert mop.lut == lut + + +def test_wrong_mode(): + lut = ImageMorph.LutBuilder(op_name="corner").build_lut() + imrgb = Image.new("RGB", (10, 10)) + iml = Image.new("L", (10, 10)) - # Act - mop.set_lut(lut) + with pytest.raises(RuntimeError): + _imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) - # Assert - self.assertEqual(mop.lut, lut) + with pytest.raises(RuntimeError): + _imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id) + with pytest.raises(RuntimeError): + _imagingmorph.match(bytes(lut), imrgb.im.id) -if __name__ == '__main__': - unittest.main() + # Should not raise + _imagingmorph.match(bytes(lut), iml.im.id) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 11cf3619d3e..c9b2fd865b8 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,99 +1,513 @@ -from helper import unittest, PillowTestCase, hopper +import pytest -from PIL import ImageOps +from PIL import Image, ImageDraw, ImageOps, ImageStat, features +from .helper import ( + assert_image_equal, + assert_image_similar, + assert_image_similar_tofile, + assert_tuple_approx_equal, + hopper, +) -class TestImageOps(PillowTestCase): - class Deformer(object): - def getmesh(self, im): - x, y = im.size - return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] +class Deformer: + def getmesh(self, im): + x, y = im.size + return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] - deformer = Deformer() - def test_sanity(self): +deformer = Deformer() - ImageOps.autocontrast(hopper("L")) - ImageOps.autocontrast(hopper("RGB")) - ImageOps.autocontrast(hopper("L"), cutoff=10) - ImageOps.autocontrast(hopper("L"), ignore=[0, 255]) +def test_sanity(): - ImageOps.colorize(hopper("L"), (0, 0, 0), (255, 255, 255)) - ImageOps.colorize(hopper("L"), "black", "white") + ImageOps.autocontrast(hopper("L")) + ImageOps.autocontrast(hopper("RGB")) - ImageOps.crop(hopper("L"), 1) - ImageOps.crop(hopper("RGB"), 1) + ImageOps.autocontrast(hopper("L"), cutoff=10) + ImageOps.autocontrast(hopper("L"), cutoff=(2, 10)) + ImageOps.autocontrast(hopper("L"), ignore=[0, 255]) + ImageOps.autocontrast(hopper("L"), mask=hopper("L")) + ImageOps.autocontrast(hopper("L"), preserve_tone=True) - ImageOps.deform(hopper("L"), self.deformer) - ImageOps.deform(hopper("RGB"), self.deformer) + ImageOps.colorize(hopper("L"), (0, 0, 0), (255, 255, 255)) + ImageOps.colorize(hopper("L"), "black", "white") - ImageOps.equalize(hopper("L")) - ImageOps.equalize(hopper("RGB")) + ImageOps.pad(hopper("L"), (128, 128)) + ImageOps.pad(hopper("RGB"), (128, 128)) - ImageOps.expand(hopper("L"), 1) - ImageOps.expand(hopper("RGB"), 1) - ImageOps.expand(hopper("L"), 2, "blue") - ImageOps.expand(hopper("RGB"), 2, "blue") + ImageOps.contain(hopper("L"), (128, 128)) + ImageOps.contain(hopper("RGB"), (128, 128)) - ImageOps.fit(hopper("L"), (128, 128)) - ImageOps.fit(hopper("RGB"), (128, 128)) + ImageOps.crop(hopper("L"), 1) + ImageOps.crop(hopper("RGB"), 1) - ImageOps.flip(hopper("L")) - ImageOps.flip(hopper("RGB")) + ImageOps.deform(hopper("L"), deformer) + ImageOps.deform(hopper("RGB"), deformer) - ImageOps.grayscale(hopper("L")) - ImageOps.grayscale(hopper("RGB")) + ImageOps.equalize(hopper("L")) + ImageOps.equalize(hopper("RGB")) - ImageOps.invert(hopper("L")) - ImageOps.invert(hopper("RGB")) + ImageOps.expand(hopper("L"), 1) + ImageOps.expand(hopper("RGB"), 1) + ImageOps.expand(hopper("L"), 2, "blue") + ImageOps.expand(hopper("RGB"), 2, "blue") - ImageOps.mirror(hopper("L")) - ImageOps.mirror(hopper("RGB")) + ImageOps.fit(hopper("L"), (128, 128)) + ImageOps.fit(hopper("RGB"), (128, 128)) - ImageOps.posterize(hopper("L"), 4) - ImageOps.posterize(hopper("RGB"), 4) + ImageOps.flip(hopper("L")) + ImageOps.flip(hopper("RGB")) - ImageOps.solarize(hopper("L")) - ImageOps.solarize(hopper("RGB")) + ImageOps.grayscale(hopper("L")) + ImageOps.grayscale(hopper("RGB")) - def test_1pxfit(self): - # Division by zero in equalize if image is 1 pixel high - newimg = ImageOps.fit(hopper("RGB").resize((1, 1)), (35, 35)) - self.assertEqual(newimg.size, (35, 35)) + ImageOps.invert(hopper("1")) + ImageOps.invert(hopper("L")) + ImageOps.invert(hopper("RGB")) - newimg = ImageOps.fit(hopper("RGB").resize((1, 100)), (35, 35)) - self.assertEqual(newimg.size, (35, 35)) + ImageOps.mirror(hopper("L")) + ImageOps.mirror(hopper("RGB")) - newimg = ImageOps.fit(hopper("RGB").resize((100, 1)), (35, 35)) - self.assertEqual(newimg.size, (35, 35)) + ImageOps.posterize(hopper("L"), 4) + ImageOps.posterize(hopper("RGB"), 4) - def test_pil163(self): - # Division by zero in equalize if < 255 pixels in image (@PIL163) + ImageOps.solarize(hopper("L")) + ImageOps.solarize(hopper("RGB")) - i = hopper("RGB").resize((15, 16)) + ImageOps.exif_transpose(hopper("L")) + ImageOps.exif_transpose(hopper("RGB")) - ImageOps.equalize(i.convert("L")) - ImageOps.equalize(i.convert("P")) - ImageOps.equalize(i.convert("RGB")) - def test_scale(self): - # Test the scaling function - i = hopper("L").resize((50, 50)) +def test_1pxfit(): + # Division by zero in equalize if image is 1 pixel high + newimg = ImageOps.fit(hopper("RGB").resize((1, 1)), (35, 35)) + assert newimg.size == (35, 35) - with self.assertRaises(ValueError): - ImageOps.scale(i, -1) + newimg = ImageOps.fit(hopper("RGB").resize((1, 100)), (35, 35)) + assert newimg.size == (35, 35) - newimg = ImageOps.scale(i, 1) - self.assertEqual(newimg.size, (50, 50)) + newimg = ImageOps.fit(hopper("RGB").resize((100, 1)), (35, 35)) + assert newimg.size == (35, 35) - newimg = ImageOps.scale(i, 2) - self.assertEqual(newimg.size, (100, 100)) - newimg = ImageOps.scale(i, 0.5) - self.assertEqual(newimg.size, (25, 25)) +def test_fit_same_ratio(): + # The ratio for this image is 1000.0 / 755 = 1.3245033112582782 + # If the ratios are not acknowledged to be the same, + # and Pillow attempts to adjust the width to + # 1.3245033112582782 * 755 = 1000.0000000000001 + # then centering this greater width causes a negative x offset when cropping + with Image.new("RGB", (1000, 755)) as im: + new_im = ImageOps.fit(im, (1000, 755)) + assert new_im.size == (1000, 755) -if __name__ == '__main__': - unittest.main() +@pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512))) +def test_contain(new_size): + im = hopper() + new_im = ImageOps.contain(im, new_size) + assert new_im.size == (256, 256) + + +def test_contain_round(): + im = Image.new("1", (43, 63), 1) + new_im = ImageOps.contain(im, (5, 7)) + assert new_im.width == 5 + + im = Image.new("1", (63, 43), 1) + new_im = ImageOps.contain(im, (7, 5)) + assert new_im.height == 5 + + +def test_pad(): + # Same ratio + im = hopper() + new_size = (im.width * 2, im.height * 2) + new_im = ImageOps.pad(im, new_size) + assert new_im.size == new_size + + for label, color, new_size in [ + ("h", None, (im.width * 4, im.height * 2)), + ("v", "#f00", (im.width * 2, im.height * 4)), + ]: + for i, centering in enumerate([(0, 0), (0.5, 0.5), (1, 1)]): + new_im = ImageOps.pad(im, new_size, color=color, centering=centering) + assert new_im.size == new_size + + assert_image_similar_tofile( + new_im, "Tests/images/imageops_pad_" + label + "_" + str(i) + ".jpg", 6 + ) + + +def test_pad_round(): + im = Image.new("1", (1, 1), 1) + new_im = ImageOps.pad(im, (4, 1)) + assert new_im.load()[2, 0] == 1 + + new_im = ImageOps.pad(im, (1, 4)) + assert new_im.load()[0, 2] == 1 + + +@pytest.mark.parametrize("mode", ("P", "PA")) +def test_palette(mode): + im = hopper(mode) + + # Expand + expanded_im = ImageOps.expand(im) + assert_image_equal(im.convert("RGB"), expanded_im.convert("RGB")) + + # Pad + padded_im = ImageOps.pad(im, (256, 128), centering=(0, 0)) + assert_image_equal( + im.convert("RGB"), padded_im.convert("RGB").crop((0, 0, 128, 128)) + ) + + +def test_pil163(): + # Division by zero in equalize if < 255 pixels in image (@PIL163) + + i = hopper("RGB").resize((15, 16)) + + ImageOps.equalize(i.convert("L")) + ImageOps.equalize(i.convert("P")) + ImageOps.equalize(i.convert("RGB")) + + +def test_scale(): + # Test the scaling function + i = hopper("L").resize((50, 50)) + + with pytest.raises(ValueError): + ImageOps.scale(i, -1) + + newimg = ImageOps.scale(i, 1) + assert newimg.size == (50, 50) + + newimg = ImageOps.scale(i, 2) + assert newimg.size == (100, 100) + + newimg = ImageOps.scale(i, 0.5) + assert newimg.size == (25, 25) + + +@pytest.mark.parametrize("border", (10, (1, 2, 3, 4))) +def test_expand_palette(border): + with Image.open("Tests/images/p_16.tga") as im: + im_expanded = ImageOps.expand(im, border, (255, 0, 0)) + + if isinstance(border, int): + left = top = right = bottom = border + else: + left, top, right, bottom = border + px = im_expanded.convert("RGB").load() + for x in range(im_expanded.width): + for b in range(top): + assert px[x, b] == (255, 0, 0) + for b in range(bottom): + assert px[x, im_expanded.height - 1 - b] == (255, 0, 0) + for y in range(im_expanded.height): + for b in range(left): + assert px[b, y] == (255, 0, 0) + for b in range(right): + assert px[im_expanded.width - 1 - b, y] == (255, 0, 0) + + im_cropped = im_expanded.crop( + (left, top, im_expanded.width - right, im_expanded.height - bottom) + ) + assert_image_equal(im_cropped, im) + + +def test_colorize_2color(): + # Test the colorizing function with 2-color functionality + + # Open test image (256px by 10px, black to white) + with Image.open("Tests/images/bw_gradient.png") as im: + im = im.convert("L") + + # Create image with original 2-color functionality + im_test = ImageOps.colorize(im, "red", "green") + + # Test output image (2-color) + left = (0, 1) + middle = (127, 1) + right = (255, 1) + assert_tuple_approx_equal( + im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg="black test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(middle), + (127, 63, 0), + threshold=1, + msg="mid test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg="white test pixel incorrect", + ) + + +def test_colorize_2color_offset(): + # Test the colorizing function with 2-color functionality and offset + + # Open test image (256px by 10px, black to white) + with Image.open("Tests/images/bw_gradient.png") as im: + im = im.convert("L") + + # Create image with original 2-color functionality with offsets + im_test = ImageOps.colorize( + im, black="red", white="green", blackpoint=50, whitepoint=100 + ) + + # Test output image (2-color) with offsets + left = (25, 1) + middle = (75, 1) + right = (125, 1) + assert_tuple_approx_equal( + im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg="black test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(middle), + (127, 63, 0), + threshold=1, + msg="mid test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg="white test pixel incorrect", + ) + + +def test_colorize_3color_offset(): + # Test the colorizing function with 3-color functionality and offset + + # Open test image (256px by 10px, black to white) + with Image.open("Tests/images/bw_gradient.png") as im: + im = im.convert("L") + + # Create image with new three color functionality with offsets + im_test = ImageOps.colorize( + im, + black="red", + white="green", + mid="blue", + blackpoint=50, + whitepoint=200, + midpoint=100, + ) + + # Test output image (3-color) with offsets + left = (25, 1) + left_middle = (75, 1) + middle = (100, 1) + right_middle = (150, 1) + right = (225, 1) + assert_tuple_approx_equal( + im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg="black test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(left_middle), + (127, 0, 127), + threshold=1, + msg="low-mid test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(middle), (0, 0, 255), threshold=1, msg="mid incorrect" + ) + assert_tuple_approx_equal( + im_test.getpixel(right_middle), + (0, 63, 127), + threshold=1, + msg="high-mid test pixel incorrect", + ) + assert_tuple_approx_equal( + im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg="white test pixel incorrect", + ) + + +def test_exif_transpose(): + exts = [".jpg"] + if features.check("webp") and features.check("webp_anim"): + exts.append(".webp") + for ext in exts: + with Image.open("Tests/images/hopper" + ext) as base_im: + + def check(orientation_im): + for im in [ + orientation_im, + orientation_im.copy(), + ]: # ImageFile # Image + if orientation_im is base_im: + assert "exif" not in im.info + else: + original_exif = im.info["exif"] + transposed_im = ImageOps.exif_transpose(im) + assert_image_similar(base_im, transposed_im, 17) + if orientation_im is base_im: + assert "exif" not in im.info + else: + assert transposed_im.info["exif"] != original_exif + + assert 0x0112 in im.getexif() + assert 0x0112 not in transposed_im.getexif() + + # Repeat the operation to test that it does not keep transposing + transposed_im2 = ImageOps.exif_transpose(transposed_im) + assert_image_equal(transposed_im2, transposed_im) + + check(base_im) + for i in range(2, 9): + with Image.open( + "Tests/images/hopper_orientation_" + str(i) + ext + ) as orientation_im: + check(orientation_im) + + # Orientation from "XML:com.adobe.xmp" info key + for suffix in ("", "_exiftool"): + with Image.open("Tests/images/xmp_tags_orientation" + suffix + ".png") as im: + assert im.getexif()[0x0112] == 3 + + transposed_im = ImageOps.exif_transpose(im) + assert 0x0112 not in transposed_im.getexif() + + transposed_im._reload_exif() + assert 0x0112 not in transposed_im.getexif() + + # Orientation from "Raw profile type exif" info key + # This test image has been manually hexedited from exif_imagemagick.png + # to have a different orientation + with Image.open("Tests/images/exif_imagemagick_orientation.png") as im: + assert im.getexif()[0x0112] == 3 + + transposed_im = ImageOps.exif_transpose(im) + assert 0x0112 not in transposed_im.getexif() + + # Orientation set directly on Image.Exif + im = hopper() + im.getexif()[0x0112] = 3 + transposed_im = ImageOps.exif_transpose(im) + assert 0x0112 not in transposed_im.getexif() + + +def test_autocontrast_cutoff(): + # Test the cutoff argument of autocontrast + with Image.open("Tests/images/bw_gradient.png") as img: + + def autocontrast(cutoff): + return ImageOps.autocontrast(img, cutoff).histogram() + + assert autocontrast(10) == autocontrast((10, 10)) + assert autocontrast(10) != autocontrast((1, 10)) + + +def test_autocontrast_mask_toy_input(): + # Test the mask argument of autocontrast + with Image.open("Tests/images/bw_gradient.png") as img: + + rect_mask = Image.new("L", img.size, 0) + draw = ImageDraw.Draw(rect_mask) + x0 = img.size[0] // 4 + y0 = img.size[1] // 4 + x1 = 3 * img.size[0] // 4 + y1 = 3 * img.size[1] // 4 + draw.rectangle((x0, y0, x1, y1), fill=255) + + result = ImageOps.autocontrast(img, mask=rect_mask) + result_nomask = ImageOps.autocontrast(img) + + assert result != result_nomask + assert ImageStat.Stat(result, mask=rect_mask).median == [127] + assert ImageStat.Stat(result_nomask).median == [128] + + +def test_autocontrast_mask_real_input(): + # Test the autocontrast with a rectangular mask + with Image.open("Tests/images/iptc.jpg") as img: + + rect_mask = Image.new("L", img.size, 0) + draw = ImageDraw.Draw(rect_mask) + x0, y0 = img.size[0] // 2, img.size[1] // 2 + x1, y1 = img.size[0] - 40, img.size[1] + draw.rectangle((x0, y0, x1, y1), fill=255) + + result = ImageOps.autocontrast(img, mask=rect_mask) + result_nomask = ImageOps.autocontrast(img) + + assert result_nomask != result + assert_tuple_approx_equal( + ImageStat.Stat(result, mask=rect_mask).median, + [195, 202, 184], + threshold=2, + msg="autocontrast with mask pixel incorrect", + ) + assert_tuple_approx_equal( + ImageStat.Stat(result_nomask).median, + [119, 106, 79], + threshold=2, + msg="autocontrast without mask pixel incorrect", + ) + + +def test_autocontrast_preserve_tone(): + def autocontrast(mode, preserve_tone): + im = hopper(mode) + return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram() + + assert autocontrast("RGB", True) != autocontrast("RGB", False) + assert autocontrast("L", True) == autocontrast("L", False) + + +def test_autocontrast_preserve_gradient(): + gradient = Image.linear_gradient("L") + + # test with a grayscale gradient that extends to 0,255. + # Should be a noop. + out = ImageOps.autocontrast(gradient, cutoff=0, preserve_tone=True) + + assert_image_equal(gradient, out) + + # cutoff the top and bottom + # autocontrast should make the first and last histogram entries equal + # and, with rounding, should be 10% of the image pixels + out = ImageOps.autocontrast(gradient, cutoff=10, preserve_tone=True) + hist = out.histogram() + assert hist[0] == hist[-1] + assert hist[-1] == 256 * round(256 * 0.10) + + # in rgb + img = gradient.convert("RGB") + out = ImageOps.autocontrast(img, cutoff=0, preserve_tone=True) + assert_image_equal(img, out) + + +@pytest.mark.parametrize( + "color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0)) +) +def test_autocontrast_preserve_one_color(color): + img = Image.new("RGB", (10, 10), color) + + # single color images shouldn't change + out = ImageOps.autocontrast(img, cutoff=0, preserve_tone=True) + assert_image_equal(img, out) # single color, no cutoff + + # even if there is a cutoff + out = ImageOps.autocontrast( + img, cutoff=10, preserve_tone=True + ) # single color 10 cutoff + assert_image_equal(img, out) diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index cd7dcae5f00..8837ed2a262 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,101 +1,111 @@ -from helper import unittest, PillowTestCase - -from PIL import Image -from PIL import ImageOps -from PIL import ImageFilter - -im = Image.open("Tests/images/hopper.ppm") -snakes = Image.open("Tests/images/color_snakes.png") - - -class TestImageOpsUsm(PillowTestCase): - - def test_ops_api(self): - - i = self.assert_warning(DeprecationWarning, - ImageOps.gaussian_blur, im, 2.0) - self.assertEqual(i.mode, "RGB") - self.assertEqual(i.size, (128, 128)) - - i = self.assert_warning(DeprecationWarning, - ImageOps.gblur, im, 2.0) - self.assertEqual(i.mode, "RGB") - self.assertEqual(i.size, (128, 128)) - - i = self.assert_warning(DeprecationWarning, - ImageOps.unsharp_mask, im, 2.0, 125, 8) - self.assertEqual(i.mode, "RGB") - self.assertEqual(i.size, (128, 128)) - - i = self.assert_warning(DeprecationWarning, - ImageOps.usm, im, 2.0, 125, 8) - self.assertEqual(i.mode, "RGB") - self.assertEqual(i.size, (128, 128)) - - def test_filter_api(self): - - test_filter = ImageFilter.GaussianBlur(2.0) - i = im.filter(test_filter) - self.assertEqual(i.mode, "RGB") - self.assertEqual(i.size, (128, 128)) - - test_filter = ImageFilter.UnsharpMask(2.0, 125, 8) - i = im.filter(test_filter) - self.assertEqual(i.mode, "RGB") - self.assertEqual(i.size, (128, 128)) - - def test_usm_formats(self): - - usm = ImageFilter.UnsharpMask - self.assertRaises(ValueError, im.convert("1").filter, usm) - im.convert("L").filter(usm) - self.assertRaises(ValueError, im.convert("I").filter, usm) - self.assertRaises(ValueError, im.convert("F").filter, usm) - im.convert("RGB").filter(usm) - im.convert("RGBA").filter(usm) - im.convert("CMYK").filter(usm) - self.assertRaises(ValueError, im.convert("YCbCr").filter, usm) - - def test_blur_formats(self): - - blur = ImageFilter.GaussianBlur - self.assertRaises(ValueError, im.convert("1").filter, blur) - blur(im.convert("L")) - self.assertRaises(ValueError, im.convert("I").filter, blur) - self.assertRaises(ValueError, im.convert("F").filter, blur) - im.convert("RGB").filter(blur) - im.convert("RGBA").filter(blur) - im.convert("CMYK").filter(blur) - self.assertRaises(ValueError, im.convert("YCbCr").filter, blur) - - def test_usm_accuracy(self): - - src = snakes.convert('RGB') - i = src.filter(ImageFilter.UnsharpMask(5, 1024, 0)) - # Image should not be changed because it have only 0 and 255 levels. - self.assertEqual(i.tobytes(), src.tobytes()) - - def test_blur_accuracy(self): - - i = snakes.filter(ImageFilter.GaussianBlur(.4)) - # These pixels surrounded with pixels with 255 intensity. - # They must be very close to 255. - for x, y, c in [(1, 0, 1), (2, 0, 1), (7, 8, 1), (8, 8, 1), (2, 9, 1), - (7, 3, 0), (8, 3, 0), (5, 8, 0), (5, 9, 0), (1, 3, 0), - (4, 3, 2), (4, 2, 2)]: - self.assertGreaterEqual(i.im.getpixel((x, y))[c], 250) - # Fuzzy match. - - def gp(x, y): - return i.im.getpixel((x, y)) - self.assertTrue(236 <= gp(7, 4)[0] <= 239) - self.assertTrue(236 <= gp(7, 5)[2] <= 239) - self.assertTrue(236 <= gp(7, 6)[2] <= 239) - self.assertTrue(236 <= gp(7, 7)[1] <= 239) - self.assertTrue(236 <= gp(8, 4)[0] <= 239) - self.assertTrue(236 <= gp(8, 5)[2] <= 239) - self.assertTrue(236 <= gp(8, 6)[2] <= 239) - self.assertTrue(236 <= gp(8, 7)[1] <= 239) - -if __name__ == '__main__': - unittest.main() +import pytest + +from PIL import Image, ImageFilter + + +@pytest.fixture +def test_images(): + ims = { + "im": Image.open("Tests/images/hopper.ppm"), + "snakes": Image.open("Tests/images/color_snakes.png"), + } + try: + yield ims + finally: + for im in ims.values(): + im.close() + + +def test_filter_api(test_images): + im = test_images["im"] + + test_filter = ImageFilter.GaussianBlur(2.0) + i = im.filter(test_filter) + assert i.mode == "RGB" + assert i.size == (128, 128) + + test_filter = ImageFilter.UnsharpMask(2.0, 125, 8) + i = im.filter(test_filter) + assert i.mode == "RGB" + assert i.size == (128, 128) + + +def test_usm_formats(test_images): + im = test_images["im"] + + usm = ImageFilter.UnsharpMask + with pytest.raises(ValueError): + im.convert("1").filter(usm) + im.convert("L").filter(usm) + with pytest.raises(ValueError): + im.convert("I").filter(usm) + with pytest.raises(ValueError): + im.convert("F").filter(usm) + im.convert("RGB").filter(usm) + im.convert("RGBA").filter(usm) + im.convert("CMYK").filter(usm) + with pytest.raises(ValueError): + im.convert("YCbCr").filter(usm) + + +def test_blur_formats(test_images): + im = test_images["im"] + + blur = ImageFilter.GaussianBlur + with pytest.raises(ValueError): + im.convert("1").filter(blur) + blur(im.convert("L")) + with pytest.raises(ValueError): + im.convert("I").filter(blur) + with pytest.raises(ValueError): + im.convert("F").filter(blur) + im.convert("RGB").filter(blur) + im.convert("RGBA").filter(blur) + im.convert("CMYK").filter(blur) + with pytest.raises(ValueError): + im.convert("YCbCr").filter(blur) + + +def test_usm_accuracy(test_images): + snakes = test_images["snakes"] + + src = snakes.convert("RGB") + i = src.filter(ImageFilter.UnsharpMask(5, 1024, 0)) + # Image should not be changed because it have only 0 and 255 levels. + assert i.tobytes() == src.tobytes() + + +def test_blur_accuracy(test_images): + snakes = test_images["snakes"] + + i = snakes.filter(ImageFilter.GaussianBlur(0.4)) + # These pixels surrounded with pixels with 255 intensity. + # They must be very close to 255. + for x, y, c in [ + (1, 0, 1), + (2, 0, 1), + (7, 8, 1), + (8, 8, 1), + (2, 9, 1), + (7, 3, 0), + (8, 3, 0), + (5, 8, 0), + (5, 9, 0), + (1, 3, 0), + (4, 3, 2), + (4, 2, 2), + ]: + assert i.im.getpixel((x, y))[c] >= 250 + # Fuzzy match. + + def gp(x, y): + return i.im.getpixel((x, y)) + + assert 236 <= gp(7, 4)[0] <= 239 + assert 236 <= gp(7, 5)[2] <= 239 + assert 236 <= gp(7, 6)[2] <= 239 + assert 236 <= gp(7, 7)[1] <= 239 + assert 236 <= gp(8, 4)[0] <= 239 + assert 236 <= gp(8, 5)[2] <= 239 + assert 236 <= gp(8, 6)[2] <= 239 + assert 236 <= gp(8, 7)[1] <= 239 diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 889f022ae06..5bda2811717 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,140 +1,203 @@ -from helper import unittest, PillowTestCase +import pytest -from PIL import ImagePalette, Image +from PIL import Image, ImagePalette +from .helper import assert_image_equal, assert_image_equal_tofile -class TestImagePalette(PillowTestCase): - def test_sanity(self): +def test_sanity(): - ImagePalette.ImagePalette("RGB", list(range(256))*3) - self.assertRaises(ValueError, - ImagePalette.ImagePalette, "RGB", list(range(256))*2) + palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) + assert len(palette.colors) == 256 - def test_getcolor(self): + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError): + ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10) - palette = ImagePalette.ImagePalette() - test_map = {} - for i in range(256): - test_map[palette.getcolor((i, i, i))] = i +def test_reload(): + with Image.open("Tests/images/hopper.gif") as im: + original = im.copy() + im.palette.dirty = 1 + assert_image_equal(im.convert("RGB"), original.convert("RGB")) - self.assertEqual(len(test_map), 256) - self.assertRaises(ValueError, palette.getcolor, (1, 2, 3)) - # Test unknown color specifier - self.assertRaises(ValueError, palette.getcolor, "unknown") +def test_getcolor(): - def test_file(self): + palette = ImagePalette.ImagePalette() + assert len(palette.palette) == 0 + assert len(palette.colors) == 0 - palette = ImagePalette.ImagePalette("RGB", list(range(256))*3) + test_map = {} + for i in range(256): + test_map[palette.getcolor((i, i, i))] = i + assert len(test_map) == 256 - f = self.tempfile("temp.lut") + # Colors can be converted between RGB and RGBA + rgba_palette = ImagePalette.ImagePalette("RGBA") + assert rgba_palette.getcolor((0, 0, 0)) == rgba_palette.getcolor((0, 0, 0, 255)) + + assert palette.getcolor((0, 0, 0)) == palette.getcolor((0, 0, 0, 255)) + + # An error is raised when the palette is full + with pytest.raises(ValueError): + palette.getcolor((1, 2, 3)) + # But not if the image is not using one of the palette entries + palette.getcolor((1, 2, 3), image=Image.new("P", (1, 1))) + + # Test unknown color specifier + with pytest.raises(ValueError): + palette.getcolor("unknown") + + +def test_getcolor_rgba_color_rgb_palette(): + palette = ImagePalette.ImagePalette("RGB") + + # Opaque RGBA colors are converted + assert palette.getcolor((0, 0, 0, 255)) == palette.getcolor((0, 0, 0)) + + with pytest.raises(ValueError): + palette.getcolor((0, 0, 0, 128)) + + +@pytest.mark.parametrize( + "index, palette", + [ + # Test when the palette is not full + (0, ImagePalette.ImagePalette()), + # Test when the palette is full + (255, ImagePalette.ImagePalette("RGB", list(range(256)) * 3)), + ], +) +def test_getcolor_not_special(index, palette): + im = Image.new("P", (1, 1)) + + # Do not use transparency index as a new color + im.info["transparency"] = index + index1 = palette.getcolor((0, 0, 0), im) + assert index1 != index + + # Do not use background index as a new color + im.info["background"] = index1 + index2 = palette.getcolor((0, 0, 1), im) + assert index2 not in (index, index1) - palette.save(f) - p = ImagePalette.load(f) +def test_file(tmp_path): - # load returns raw palette information - self.assertEqual(len(p[0]), 768) - self.assertEqual(p[1], "RGB") + palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) - p = ImagePalette.raw(p[1], p[0]) - self.assertIsInstance(p, ImagePalette.ImagePalette) - self.assertEqual(p.palette, palette.tobytes()) + f = str(tmp_path / "temp.lut") - def test_make_linear_lut(self): - # Arrange - black = 0 - white = 255 + palette.save(f) - # Act - lut = ImagePalette.make_linear_lut(black, white) + p = ImagePalette.load(f) - # Assert - self.assertIsInstance(lut, list) - self.assertEqual(len(lut), 256) - # Check values - for i in range(0, len(lut)): - self.assertEqual(lut[i], i) + # load returns raw palette information + assert len(p[0]) == 768 + assert p[1] == "RGB" - def test_make_linear_lut_not_yet_implemented(self): - # Update after FIXME - # Arrange - black = 1 - white = 255 + p = ImagePalette.raw(p[1], p[0]) + assert isinstance(p, ImagePalette.ImagePalette) + assert p.palette == palette.tobytes() - # Act - self.assertRaises(NotImplementedError, - ImagePalette.make_linear_lut, black, white) - def test_make_gamma_lut(self): - # Arrange - exp = 5 +def test_make_linear_lut(): + # Arrange + black = 0 + white = 255 - # Act - lut = ImagePalette.make_gamma_lut(exp) + # Act + lut = ImagePalette.make_linear_lut(black, white) + + # Assert + assert isinstance(lut, list) + assert len(lut) == 256 + # Check values + for i in range(0, len(lut)): + assert lut[i] == i + + +def test_make_linear_lut_not_yet_implemented(): + # Update after FIXME + # Arrange + black = 1 + white = 255 + + # Act + with pytest.raises(NotImplementedError): + ImagePalette.make_linear_lut(black, white) + + +def test_make_gamma_lut(): + # Arrange + exp = 5 + + # Act + lut = ImagePalette.make_gamma_lut(exp) + + # Assert + assert isinstance(lut, list) + assert len(lut) == 256 + # Check a few values + assert lut[0] == 0 + assert lut[63] == 0 + assert lut[127] == 8 + assert lut[191] == 60 + assert lut[255] == 255 + + +def test_rawmode_valueerrors(tmp_path): + # Arrange + palette = ImagePalette.raw("RGB", list(range(256)) * 3) + + # Act / Assert + with pytest.raises(ValueError): + palette.tobytes() + with pytest.raises(ValueError): + palette.getcolor((1, 2, 3)) + f = str(tmp_path / "temp.lut") + with pytest.raises(ValueError): + palette.save(f) - # Assert - self.assertIsInstance(lut, list) - self.assertEqual(len(lut), 256) - # Check a few values - self.assertEqual(lut[0], 0) - self.assertEqual(lut[63], 0) - self.assertEqual(lut[127], 8) - self.assertEqual(lut[191], 60) - self.assertEqual(lut[255], 255) - def test_rawmode_valueerrors(self): - # Arrange - palette = ImagePalette.raw("RGB", list(range(256))*3) +def test_getdata(): + # Arrange + data_in = list(range(256)) * 3 + palette = ImagePalette.ImagePalette("RGB", data_in) - # Act / Assert - self.assertRaises(ValueError, palette.tobytes) - self.assertRaises(ValueError, palette.getcolor, (1, 2, 3)) - f = self.tempfile("temp.lut") - self.assertRaises(ValueError, palette.save, f) + # Act + mode, data_out = palette.getdata() - def test_getdata(self): - # Arrange - data_in = list(range(256))*3 - palette = ImagePalette.ImagePalette("RGB", data_in) + # Assert + assert mode == "RGB" - # Act - mode, data_out = palette.getdata() - # Assert - self.assertEqual(mode, "RGB;L") +def test_rawmode_getdata(): + # Arrange + data_in = list(range(256)) * 3 + palette = ImagePalette.raw("RGB", data_in) - def test_rawmode_getdata(self): - # Arrange - data_in = list(range(256))*3 - palette = ImagePalette.raw("RGB", data_in) + # Act + rawmode, data_out = palette.getdata() - # Act - rawmode, data_out = palette.getdata() + # Assert + assert rawmode == "RGB" + assert data_in == data_out - # Assert - self.assertEqual(rawmode, "RGB") - self.assertEqual(data_in, data_out) - def test_2bit_palette(self): - # issue #2258, 2 bit palettes are corrupted. - outfile = self.tempfile('temp.png') +def test_2bit_palette(tmp_path): + # issue #2258, 2 bit palettes are corrupted. + outfile = str(tmp_path / "temp.png") - rgb = b'\x00' * 2 + b'\x01' * 2 + b'\x02' * 2 - img = Image.frombytes('P', (6, 1), rgb) - img.putpalette(b'\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF') # RGB - img.save(outfile, format='PNG') + rgb = b"\x00" * 2 + b"\x01" * 2 + b"\x02" * 2 + img = Image.frombytes("P", (6, 1), rgb) + img.putpalette(b"\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF") # RGB + img.save(outfile, format="PNG") - reloaded = Image.open(outfile) + assert_image_equal_tofile(img, outfile) - self.assert_image_equal(img, reloaded) - def test_invalid_palette(self): - self.assertRaises(IOError, - ImagePalette.load, "Tests/images/hopper.jpg") - - -if __name__ == '__main__': - unittest.main() +def test_invalid_palette(): + with pytest.raises(OSError): + ImagePalette.load("Tests/images/hopper.jpg") diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 14cc4d14b3f..de3920cf5eb 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -1,84 +1,188 @@ -from helper import unittest, PillowTestCase - -from PIL import ImagePath, Image - import array +import math import struct +import pytest + +from PIL import Image, ImagePath + + +def test_path(): + + p = ImagePath.Path(list(range(10))) + + # sequence interface + assert len(p) == 5 + assert p[0] == (0.0, 1.0) + assert p[-1] == (8.0, 9.0) + assert list(p[:1]) == [(0.0, 1.0)] + with pytest.raises(TypeError) as cm: + p["foo"] + assert str(cm.value) == "Path indices must be integers, not str" + assert list(p) == [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)] + + # method sanity check + assert p.tolist() == [ + (0.0, 1.0), + (2.0, 3.0), + (4.0, 5.0), + (6.0, 7.0), + (8.0, 9.0), + ] + assert p.tolist(1) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] + + assert p.getbbox() == (0.0, 1.0, 8.0, 9.0) + + assert p.compact(5) == 2 + assert list(p) == [(0.0, 1.0), (4.0, 5.0), (8.0, 9.0)] + + p.transform((1, 0, 1, 0, 1, 1)) + assert list(p) == [(1.0, 2.0), (5.0, 6.0), (9.0, 10.0)] + + # alternative constructors + p = ImagePath.Path([0, 1]) + assert list(p) == [(0.0, 1.0)] + p = ImagePath.Path([0.0, 1.0]) + assert list(p) == [(0.0, 1.0)] + p = ImagePath.Path([0, 1]) + assert list(p) == [(0.0, 1.0)] + p = ImagePath.Path([(0, 1)]) + assert list(p) == [(0.0, 1.0)] + p = ImagePath.Path(p) + assert list(p) == [(0.0, 1.0)] + p = ImagePath.Path(p.tolist(0)) + assert list(p) == [(0.0, 1.0)] + p = ImagePath.Path(p.tolist(1)) + assert list(p) == [(0.0, 1.0)] + p = ImagePath.Path(array.array("f", [0, 1])) + assert list(p) == [(0.0, 1.0)] + + arr = array.array("f", [0, 1]) + if hasattr(arr, "tobytes"): + p = ImagePath.Path(arr.tobytes()) + else: + p = ImagePath.Path(arr.tostring()) + assert list(p) == [(0.0, 1.0)] + + +def test_invalid_coords(): + # Arrange + coords = ["a", "b"] + + # Act / Assert + with pytest.raises(ValueError) as e: + ImagePath.Path(coords) + + assert str(e.value) == "incorrect coordinate type" + + +def test_path_odd_number_of_coordinates(): + # Arrange + coords = [0] + + # Act / Assert + with pytest.raises(ValueError) as e: + ImagePath.Path(coords) + + assert str(e.value) == "wrong number of coordinates" + + +@pytest.mark.parametrize( + "coords, expected", + [ + ([0, 1, 2, 3], (0.0, 1.0, 2.0, 3.0)), + ([3, 2, 1, 0], (1.0, 0.0, 3.0, 2.0)), + (0, (0.0, 0.0, 0.0, 0.0)), + (1, (0.0, 0.0, 0.0, 0.0)), + ], +) +def test_getbbox(coords, expected): + # Arrange + p = ImagePath.Path(coords) + + # Act / Assert + assert p.getbbox() == expected + + +def test_getbbox_no_args(): + # Arrange + p = ImagePath.Path([0, 1, 2, 3]) + + # Act / Assert + with pytest.raises(TypeError): + p.getbbox(1) + + +@pytest.mark.parametrize( + "coords, expected", + [ + (0, []), + (list(range(6)), [(0.0, 3.0), (4.0, 9.0), (8.0, 15.0)]), + ], +) +def test_map(coords, expected): + # Arrange + p = ImagePath.Path(coords) + + # Act + # Modifies the path in-place + p.map(lambda x, y: (x * 2, y * 3)) + + # Assert + assert list(p) == expected + + +def test_transform(): + # Arrange + p = ImagePath.Path([0, 1, 2, 3]) + theta = math.pi / 15 + + # Act + # Affine transform, in-place + p.transform( + (math.cos(theta), math.sin(theta), 20, -math.sin(theta), math.cos(theta), 20), + ) + + # Assert + assert p.tolist() == [ + (20.20791169081776, 20.978147600733806), + (22.58003027392089, 22.518619420565898), + ] + + +def test_transform_with_wrap(): + # Arrange + p = ImagePath.Path([0, 1, 2, 3]) + theta = math.pi / 15 + + # Act + # Affine transform, in-place, with wrap parameter + p.transform( + (math.cos(theta), math.sin(theta), 20, -math.sin(theta), math.cos(theta), 20), + 1.0, + ) -class TestImagePath(PillowTestCase): - - def test_path(self): - - p = ImagePath.Path(list(range(10))) - - # sequence interface - self.assertEqual(len(p), 5) - self.assertEqual(p[0], (0.0, 1.0)) - self.assertEqual(p[-1], (8.0, 9.0)) - self.assertEqual(list(p[:1]), [(0.0, 1.0)]) - self.assertEqual( - list(p), - [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)]) - - # method sanity check - self.assertEqual( - p.tolist(), - [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)]) - self.assertEqual( - p.tolist(1), - [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]) - - self.assertEqual(p.getbbox(), (0.0, 1.0, 8.0, 9.0)) - - self.assertEqual(p.compact(5), 2) - self.assertEqual(list(p), [(0.0, 1.0), (4.0, 5.0), (8.0, 9.0)]) - - p.transform((1, 0, 1, 0, 1, 1)) - self.assertEqual(list(p), [(1.0, 2.0), (5.0, 6.0), (9.0, 10.0)]) - - # alternative constructors - p = ImagePath.Path([0, 1]) - self.assertEqual(list(p), [(0.0, 1.0)]) - p = ImagePath.Path([0.0, 1.0]) - self.assertEqual(list(p), [(0.0, 1.0)]) - p = ImagePath.Path([0, 1]) - self.assertEqual(list(p), [(0.0, 1.0)]) - p = ImagePath.Path([(0, 1)]) - self.assertEqual(list(p), [(0.0, 1.0)]) - p = ImagePath.Path(p) - self.assertEqual(list(p), [(0.0, 1.0)]) - p = ImagePath.Path(p.tolist(0)) - self.assertEqual(list(p), [(0.0, 1.0)]) - p = ImagePath.Path(p.tolist(1)) - self.assertEqual(list(p), [(0.0, 1.0)]) - p = ImagePath.Path(array.array("f", [0, 1])) - self.assertEqual(list(p), [(0.0, 1.0)]) - - arr = array.array("f", [0, 1]) - if hasattr(arr, 'tobytes'): - p = ImagePath.Path(arr.tobytes()) - else: - p = ImagePath.Path(arr.tostring()) - self.assertEqual(list(p), [(0.0, 1.0)]) - - def test_overflow_segfault(self): - # Some Pythons fail getting the argument as an integer, and it falls - # through to the sequence. Seeing this on 32-bit Windows. - with self.assertRaises((TypeError, MemoryError)): - # post patch, this fails with a memory error - x = evil() - - # This fails due to the invalid malloc above, - # and segfaults - for i in range(200000): - if str is bytes: - x[i] = "0"*16 - else: - x[i] = b'0'*16 - - -class evil: + # Assert + assert p.tolist() == [ + (0.20791169081775962, 20.978147600733806), + (0.5800302739208902, 22.518619420565898), + ] + + +def test_overflow_segfault(): + # Some Pythons fail getting the argument as an integer, and it falls + # through to the sequence. Seeing this on 32-bit Windows. + with pytest.raises((TypeError, MemoryError)): + # post patch, this fails with a memory error + x = Evil() + + # This fails due to the invalid malloc above, + # and segfaults + for i in range(200000): + x[i] = b"0" * 16 + + +class Evil: def __init__(self): self.corrupt = Image.core.path(0x4000000000000000) @@ -88,7 +192,3 @@ def __getitem__(self, i): def __setitem__(self, i, x): self.corrupt[i] = struct.unpack("dd", x) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index d3de5875bef..2f2b0791853 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,80 +1,65 @@ -from helper import unittest, PillowTestCase, hopper +import warnings -from PIL import ImageQt +import pytest +from .helper import assert_image_similar, hopper -if ImageQt.qt_is_installed: - from PIL.ImageQt import qRgba - - def skip_if_qt_is_not_installed(_): - pass -else: - def skip_if_qt_is_not_installed(test_case): - test_case.skipTest('Qt bindings are not installed') - - -class PillowQtTestCase(object): - - def setUp(self): - skip_if_qt_is_not_installed(self) - - def tearDown(self): - pass - - -class PillowQPixmapTestCase(PillowQtTestCase): +with warnings.catch_warnings() as w: + warnings.simplefilter("ignore", category=DeprecationWarning) + from PIL import ImageQt - def setUp(self): - PillowQtTestCase.setUp(self) - try: - if ImageQt.qt_version == '5': - from PyQt5.QtGui import QGuiApplication - elif ImageQt.qt_version == '4': - from PyQt4.QtGui import QGuiApplication - elif ImageQt.qt_version == 'side': - from PySide.QtGui import QGuiApplication - except ImportError: - self.skipTest('QGuiApplication not installed') - self.app = QGuiApplication([]) +pytestmark = pytest.mark.skipif( + not ImageQt.qt_is_installed, reason="Qt bindings are not installed" +) - def tearDown(self): - PillowQtTestCase.tearDown(self) - self.app.quit() - - -class TestImageQt(PillowQtTestCase, PillowTestCase): - - def test_rgb(self): - # from https://doc.qt.io/qt-4.8/qcolor.html - # typedef QRgb - # An ARGB quadruplet on the format #AARRGGBB, - # equivalent to an unsigned int. - if ImageQt.qt_version == '5': - from PyQt5.QtGui import qRgb - elif ImageQt.qt_version == '4': - from PyQt4.QtGui import qRgb - elif ImageQt.qt_version == 'side': - from PySide.QtGui import qRgb - - self.assertEqual(qRgb(0, 0, 0), qRgba(0, 0, 0, 255)) - - def checkrgb(r, g, b): - val = ImageQt.rgb(r, g, b) - val = val % 2**24 # drop the alpha - self.assertEqual(val >> 16, r) - self.assertEqual(((val >> 8) % 2**8), g) - self.assertEqual(val % 2**8, b) - - checkrgb(0, 0, 0) - checkrgb(255, 0, 0) - checkrgb(0, 255, 0) - checkrgb(0, 0, 255) - - def test_image(self): - for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): - ImageQt.ImageQt(hopper(mode)) +if ImageQt.qt_is_installed: + from PIL.ImageQt import qRgba -if __name__ == '__main__': - unittest.main() +def test_rgb(): + # from https://doc.qt.io/archives/qt-4.8/qcolor.html + # typedef QRgb + # An ARGB quadruplet on the format #AARRGGBB, + # equivalent to an unsigned int. + if ImageQt.qt_version == "6": + from PyQt6.QtGui import qRgb + elif ImageQt.qt_version == "side6": + from PySide6.QtGui import qRgb + elif ImageQt.qt_version == "5": + from PyQt5.QtGui import qRgb + elif ImageQt.qt_version == "side2": + from PySide2.QtGui import qRgb + + assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) + + def checkrgb(r, g, b): + val = ImageQt.rgb(r, g, b) + val = val % 2**24 # drop the alpha + assert val >> 16 == r + assert ((val >> 8) % 2**8) == g + assert val % 2**8 == b + + checkrgb(0, 0, 0) + checkrgb(255, 0, 0) + checkrgb(0, 255, 0) + checkrgb(0, 0, 255) + + +def test_image(): + modes = ["1", "RGB", "RGBA", "L", "P"] + qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage + if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+ + modes.append("I;16") + + for mode in modes: + im = hopper(mode) + roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im)) + if mode not in ("RGB", "RGBA"): + im = im.convert("RGB") + assert_image_similar(roundtripped_im, im, 1) + + +def test_closed_file(): + with warnings.catch_warnings(): + ImageQt.ImageQt("Tests/images/hopper.gif") diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 2a4a358ba8f..6af7e760204 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -1,74 +1,106 @@ -from helper import unittest, PillowTestCase, hopper +import pytest from PIL import Image, ImageSequence, TiffImagePlugin +from .helper import assert_image_equal, hopper, skip_unless_feature -class TestImageSequence(PillowTestCase): - def test_sanity(self): +def test_sanity(tmp_path): - test_file = self.tempfile("temp.im") + test_file = str(tmp_path / "temp.im") - im = hopper("RGB") - im.save(test_file) + im = hopper("RGB") + im.save(test_file) - seq = ImageSequence.Iterator(im) + seq = ImageSequence.Iterator(im) - index = 0 - for frame in seq: - self.assert_image_equal(im, frame) - self.assertEqual(im.tell(), index) - index += 1 + index = 0 + for frame in seq: + assert_image_equal(im, frame) + assert im.tell() == index + index += 1 - self.assertEqual(index, 1) + assert index == 1 - self.assertRaises(AttributeError, ImageSequence.Iterator, 0) + with pytest.raises(AttributeError): + ImageSequence.Iterator(0) - def test_iterator(self): - im = Image.open('Tests/images/multipage.tiff') + +def test_iterator(): + with Image.open("Tests/images/multipage.tiff") as im: i = ImageSequence.Iterator(im) for index in range(0, im.n_frames): - self.assertEqual(i[index], next(i)) - self.assertRaises(IndexError, lambda: i[index+1]) - self.assertRaises(StopIteration, next, i) + assert i[index] == next(i) + with pytest.raises(IndexError): + i[index + 1] + with pytest.raises(StopIteration): + next(i) + + +def test_iterator_min_frame(): + with Image.open("Tests/images/hopper.psd") as im: + i = ImageSequence.Iterator(im) + for index in range(1, im.n_frames): + assert i[index] == next(i) + - def _test_multipage_tiff(self): - im = Image.open('Tests/images/multipage.tiff') +def _test_multipage_tiff(): + with Image.open("Tests/images/multipage.tiff") as im: for index, frame in enumerate(ImageSequence.Iterator(im)): frame.load() - self.assertEqual(index, im.tell()) - frame.convert('RGB') + assert index == im.tell() + frame.convert("RGB") + - def test_tiff(self): - self._test_multipage_tiff() +def test_tiff(): + _test_multipage_tiff() - def test_libtiff(self): - codecs = dir(Image.core) - if "libtiff_encoder" not in codecs or "libtiff_decoder" not in codecs: - self.skipTest("tiff support not available") +@skip_unless_feature("libtiff") +def test_libtiff(): + TiffImagePlugin.READ_LIBTIFF = True + _test_multipage_tiff() + TiffImagePlugin.READ_LIBTIFF = False - TiffImagePlugin.READ_LIBTIFF = True - self._test_multipage_tiff() - TiffImagePlugin.READ_LIBTIFF = False - def test_consecutive(self): - im = Image.open('Tests/images/multipage.tiff') - firstFrame = None +def test_consecutive(): + with Image.open("Tests/images/multipage.tiff") as im: + first_frame = None for frame in ImageSequence.Iterator(im): - if firstFrame is None: - firstFrame = frame.copy() + if first_frame is None: + first_frame = frame.copy() for frame in ImageSequence.Iterator(im): - self.assert_image_equal(frame, firstFrame) + assert_image_equal(frame, first_frame) break - def test_palette_mmap(self): - # Using mmap in ImageFile can require to reload the palette. - im = Image.open('Tests/images/multipage-mmap.tiff') - color1 = im.getpalette()[0:3] + +def test_palette_mmap(): + # Using mmap in ImageFile can require to reload the palette. + with Image.open("Tests/images/multipage-mmap.tiff") as im: + color1 = im.getpalette()[:3] im.seek(0) - color2 = im.getpalette()[0:3] - self.assertEqual(color1, color2) + color2 = im.getpalette()[:3] + assert color1 == color2 + + +def test_all_frames(): + # Test a single image + with Image.open("Tests/images/iss634.gif") as im: + ims = ImageSequence.all_frames(im) + + assert len(ims) == 42 + for i, im_frame in enumerate(ims): + assert im_frame is not im + + im.seek(i) + assert_image_equal(im, im_frame) + + # Test a series of images + ims = ImageSequence.all_frames([im, hopper(), im]) + assert len(ims) == 85 -if __name__ == '__main__': - unittest.main() + # Test an operation + ims = ImageSequence.all_frames(im, lambda im_frame: im_frame.rotate(90)) + for i, im_frame in enumerate(ims): + im.seek(i) + assert_image_equal(im.rotate(90), im_frame) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index da91e35c77f..3e147a9efec 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,40 +1,108 @@ -from helper import unittest, PillowTestCase, hopper +import pytest -from PIL import Image -from PIL import ImageShow +from PIL import Image, ImageShow +from .helper import hopper, is_win32, on_ci -class TestImageShow(PillowTestCase): - def test_sanity(self): - dir(Image) - dir(ImageShow) +def test_sanity(): + dir(Image) + dir(ImageShow) - def test_register(self): - # Test registering a viewer that is not a class - ImageShow.register("not a class") - def test_show(self): - class TestViewer: - methodCalled = False +def test_register(): + # Test registering a viewer that is not a class + ImageShow.register("not a class") - def show(self, image, title=None, **options): - self.methodCalled = True - return True - viewer = TestViewer() - ImageShow.register(viewer, -1) + # Restore original state + ImageShow._viewers.pop() - im = hopper() - self.assertTrue(ImageShow.show(im)) - self.assertTrue(viewer.methodCalled) - def test_viewer(self): - viewer = ImageShow.Viewer() +@pytest.mark.parametrize( + "order", + [-1, 0], +) +def test_viewer_show(order): + class TestViewer(ImageShow.Viewer): + def show_image(self, image, **options): + self.methodCalled = True + return True - self.assertIsNone(viewer.get_format(None)) + viewer = TestViewer() + ImageShow.register(viewer, order) - self.assertRaises(NotImplementedError, viewer.get_command, None) + for mode in ("1", "I;16", "LA", "RGB", "RGBA"): + viewer.methodCalled = False + with hopper(mode) as im: + assert ImageShow.show(im) + assert viewer.methodCalled + # Restore original state + ImageShow._viewers.pop(0) -if __name__ == '__main__': - unittest.main() + +@pytest.mark.skipif( + not on_ci() or is_win32(), + reason="Only run on CIs; hangs on Windows CIs", +) +@pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) +def test_show(mode): + im = hopper(mode) + assert ImageShow.show(im) + + +def test_show_without_viewers(): + viewers = ImageShow._viewers + ImageShow._viewers = [] + + im = hopper() + assert not ImageShow.show(im) + + ImageShow._viewers = viewers + + +def test_viewer(): + viewer = ImageShow.Viewer() + + assert viewer.get_format(None) is None + + with pytest.raises(NotImplementedError): + viewer.get_command(None) + + +@pytest.mark.parametrize("viewer", ImageShow._viewers) +def test_viewers(viewer): + try: + viewer.get_command("test.jpg") + except NotImplementedError: + pass + + +def test_ipythonviewer(): + pytest.importorskip("IPython", reason="IPython not installed") + for viewer in ImageShow._viewers: + if isinstance(viewer, ImageShow.IPythonViewer): + test_viewer = viewer + break + else: + assert False + + im = hopper() + assert test_viewer.show(im) == 1 + + +@pytest.mark.skipif( + not on_ci() or is_win32(), + reason="Only run on CIs; hangs on Windows CIs", +) +@pytest.mark.parametrize("viewer", ImageShow._viewers) +def test_file_deprecated(tmp_path, viewer): + f = str(tmp_path / "temp.jpg") + hopper().save(f) + with pytest.warns(DeprecationWarning): + try: + viewer.show_file(file=f) + except NotImplementedError: + pass + with pytest.raises(TypeError): + viewer.show_file() diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 77eb0aac198..5717fe15036 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -1,61 +1,60 @@ -from helper import unittest, PillowTestCase, hopper +import pytest -from PIL import Image -from PIL import ImageStat +from PIL import Image, ImageStat +from .helper import hopper -class TestImageStat(PillowTestCase): - def test_sanity(self): +def test_sanity(): - im = hopper() + im = hopper() - st = ImageStat.Stat(im) - st = ImageStat.Stat(im.histogram()) - st = ImageStat.Stat(im, Image.new("1", im.size, 1)) + st = ImageStat.Stat(im) + st = ImageStat.Stat(im.histogram()) + st = ImageStat.Stat(im, Image.new("1", im.size, 1)) - # Check these run. Exceptions will cause failures. - st.extrema - st.sum - st.mean - st.median - st.rms - st.sum2 - st.var - st.stddev + # Check these run. Exceptions will cause failures. + st.extrema + st.sum + st.mean + st.median + st.rms + st.sum2 + st.var + st.stddev - self.assertRaises(AttributeError, lambda: st.spam) + with pytest.raises(AttributeError): + st.spam() - self.assertRaises(TypeError, ImageStat.Stat, 1) + with pytest.raises(TypeError): + ImageStat.Stat(1) - def test_hopper(self): - im = hopper() +def test_hopper(): - st = ImageStat.Stat(im) + im = hopper() - # verify a few values - self.assertEqual(st.extrema[0], (0, 255)) - self.assertEqual(st.median[0], 72) - self.assertEqual(st.sum[0], 1470218) - self.assertEqual(st.sum[1], 1311896) - self.assertEqual(st.sum[2], 1563008) + st = ImageStat.Stat(im) - def test_constant(self): + # verify a few values + assert st.extrema[0] == (0, 255) + assert st.median[0] == 72 + assert st.sum[0] == 1470218 + assert st.sum[1] == 1311896 + assert st.sum[2] == 1563008 - im = Image.new("L", (128, 128), 128) - st = ImageStat.Stat(im) +def test_constant(): - self.assertEqual(st.extrema[0], (128, 128)) - self.assertEqual(st.sum[0], 128**3) - self.assertEqual(st.sum2[0], 128**4) - self.assertEqual(st.mean[0], 128) - self.assertEqual(st.median[0], 128) - self.assertEqual(st.rms[0], 128) - self.assertEqual(st.var[0], 0) - self.assertEqual(st.stddev[0], 0) + im = Image.new("L", (128, 128), 128) + st = ImageStat.Stat(im) -if __name__ == '__main__': - unittest.main() + assert st.extrema[0] == (128, 128) + assert st.sum[0] == 128**3 + assert st.sum2[0] == 128**4 + assert st.mean[0] == 128 + assert st.median[0] == 128 + assert st.rms[0] == 128 + assert st.var[0] == 0 + assert st.stddev[0] == 0 diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index fbf48a1b6eb..995d0ee1f38 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -1,90 +1,109 @@ -from helper import unittest, PillowTestCase, hopper +import pytest + from PIL import Image +from .helper import assert_image_equal, hopper try: + import tkinter as tk + from PIL import ImageTk - import Tkinter as tk + dir(ImageTk) HAS_TK = True -except (OSError, ImportError) as v: - # Skipped via setUp() +except (OSError, ImportError): + # Skipped via pytestmark HAS_TK = False -TK_MODES = ('1', 'L', 'P', 'RGB', 'RGBA') +TK_MODES = ("1", "L", "P", "RGB", "RGBA") + + +pytestmark = pytest.mark.skipif(not HAS_TK, reason="Tk not installed") + + +def setup_module(): + try: + # setup tk + tk.Frame() + # root = tk.Tk() + except RuntimeError as v: + pytest.skip(f"RuntimeError: {v}") + except tk.TclError as v: + pytest.skip(f"TCL Error: {v}") + + +def test_kw(): + TEST_JPG = "Tests/images/hopper.jpg" + TEST_PNG = "Tests/images/hopper.png" + with Image.open(TEST_JPG) as im1: + with Image.open(TEST_PNG) as im2: + with open(TEST_PNG, "rb") as fp: + data = fp.read() + kw = {"file": TEST_JPG, "data": data} + + # Test "file" + im = ImageTk._get_image_from_kw(kw) + assert_image_equal(im, im1) + # Test "data" + im = ImageTk._get_image_from_kw(kw) + assert_image_equal(im, im2) -class TestImageTk(PillowTestCase): + # Test no relevant entry + im = ImageTk._get_image_from_kw(kw) + assert im is None - def setUp(self): - if not HAS_TK: - self.skipTest("Tk not installed") - try: - # setup tk - app = tk.Frame() - # root = tk.Tk() - except (tk.TclError) as v: - self.skipTest("TCL Error: %s" % v) - def test_kw(self): - TEST_JPG = "Tests/images/hopper.jpg" - TEST_PNG = "Tests/images/hopper.png" - im1 = Image.open(TEST_JPG) - im2 = Image.open(TEST_PNG) - with open(TEST_PNG, 'rb') as fp: - data = fp.read() - kw = {"file": TEST_JPG, "data": data} +@pytest.mark.parametrize("mode", TK_MODES) +def test_photoimage(mode): + # test as image: + im = hopper(mode) - # Test "file" - im = ImageTk._get_image_from_kw(kw) - self.assert_image_equal(im, im1) + # this should not crash + im_tk = ImageTk.PhotoImage(im) - # Test "data" - im = ImageTk._get_image_from_kw(kw) - self.assert_image_equal(im, im2) + assert im_tk.width() == im.width + assert im_tk.height() == im.height - # Test no relevant entry - im = ImageTk._get_image_from_kw(kw) - self.assertIsNone(im) + reloaded = ImageTk.getimage(im_tk) + assert_image_equal(reloaded, im.convert("RGBA")) - def test_photoimage(self): - for mode in TK_MODES: - # test as image: - im = hopper(mode) - # this should not crash - im_tk = ImageTk.PhotoImage(im) +def test_photoimage_apply_transparency(): + with Image.open("Tests/images/pil123p.png") as im: + im_tk = ImageTk.PhotoImage(im) + reloaded = ImageTk.getimage(im_tk) + assert_image_equal(reloaded, im.convert("RGBA")) - self.assertEqual(im_tk.width(), im.width) - self.assertEqual(im_tk.height(), im.height) - # _tkinter.TclError: this function is not yet supported - # reloaded = ImageTk.getimage(im_tk) - # self.assert_image_equal(reloaded, im) +@pytest.mark.parametrize("mode", TK_MODES) +def test_photoimage_blank(mode): + # test a image using mode/size: + im_tk = ImageTk.PhotoImage(mode, (100, 100)) - def test_photoimage_blank(self): - # test a image using mode/size: - for mode in TK_MODES: - im_tk = ImageTk.PhotoImage(mode, (100, 100)) + assert im_tk.width() == 100 + assert im_tk.height() == 100 - self.assertEqual(im_tk.width(), 100) - self.assertEqual(im_tk.height(), 100) + im = Image.new(mode, (100, 100)) + reloaded = ImageTk.getimage(im_tk) + assert_image_equal(reloaded.convert(mode), im) - # reloaded = ImageTk.getimage(im_tk) - # self.assert_image_equal(reloaded, im) - def test_bitmapimage(self): - im = hopper('1') +def test_box_deprecation(): + im = hopper() + im_tk = ImageTk.PhotoImage(im) + with pytest.warns(DeprecationWarning): + im_tk.paste(im, (0, 0, 128, 128)) - # this should not crash - im_tk = ImageTk.BitmapImage(im) - self.assertEqual(im_tk.width(), im.width) - self.assertEqual(im_tk.height(), im.height) +def test_bitmapimage(): + im = hopper("1") - # reloaded = ImageTk.getimage(im_tk) - # self.assert_image_equal(reloaded, im) + # this should not crash + im_tk = ImageTk.BitmapImage(im) + assert im_tk.width() == im.width + assert im_tk.height() == im.height -if __name__ == '__main__': - unittest.main() + # reloaded = ImageTk.getimage(im_tk) + # assert_image_equal(reloaded, im) diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 70bf28247d6..9d64d17a30f 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -1,11 +1,11 @@ -from helper import unittest, PillowTestCase, hopper +import pytest from PIL import ImageWin -import sys +from .helper import hopper, is_win32 -class TestImageWin(PillowTestCase): +class TestImageWin: def test_sanity(self): dir(ImageWin) @@ -18,7 +18,7 @@ def test_hdc(self): dc2 = int(hdc) # Assert - self.assertEqual(dc2, 50) + assert dc2 == 50 def test_hwnd(self): # Arrange @@ -29,12 +29,11 @@ def test_hwnd(self): wnd2 = int(hwnd) # Assert - self.assertEqual(wnd2, 50) + assert wnd2 == 50 -@unittest.skipUnless(sys.platform.startswith('win32'), "Windows only") -class TestImageWinDib(PillowTestCase): - +@pytest.mark.skipif(not is_win32(), reason="Windows only") +class TestImageWinDib: def test_dib_image(self): # Arrange im = hopper() @@ -43,7 +42,7 @@ def test_dib_image(self): dib = ImageWin.Dib(im) # Assert - self.assertEqual(dib.size, im.size) + assert dib.size == im.size def test_dib_mode_string(self): # Arrange @@ -54,7 +53,7 @@ def test_dib_mode_string(self): dib = ImageWin.Dib(mode, size) # Assert - self.assertEqual(dib.size, (128, 128)) + assert dib.size == (128, 128) def test_dib_paste(self): # Arrange @@ -68,7 +67,7 @@ def test_dib_paste(self): dib.paste(im) # Assert - self.assertEqual(dib.size, (128, 128)) + assert dib.size == (128, 128) def test_dib_paste_bbox(self): # Arrange @@ -83,7 +82,7 @@ def test_dib_paste_bbox(self): dib.paste(im, bbox) # Assert - self.assertEqual(dib.size, (128, 128)) + assert dib.size == (128, 128) def test_dib_frombytes_tobytes_roundtrip(self): # Arrange @@ -96,7 +95,7 @@ def test_dib_frombytes_tobytes_roundtrip(self): dib2 = ImageWin.Dib(mode, size) # Confirm they're different - self.assertNotEqual(dib1.tobytes(), dib2.tobytes()) + assert dib1.tobytes() != dib2.tobytes() # Act # Make one the same as the using tobytes()/frombytes() @@ -105,8 +104,4 @@ def test_dib_frombytes_tobytes_roundtrip(self): # Assert # Confirm they're the same - self.assertEqual(dib1.tobytes(), dib2.tobytes()) - - -if __name__ == '__main__': - unittest.main() + assert dib1.tobytes() == dib2.tobytes() diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index 7178d8cb8a0..df130565575 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -1,39 +1,39 @@ -from helper import unittest, PillowTestCase, hopper +from io import BytesIO + from PIL import Image, ImageWin -import sys -import ctypes -from io import BytesIO +from .helper import hopper, is_win32 # see https://github.com/python-pillow/Pillow/pull/1431#issuecomment-144692652 -if sys.platform.startswith('win32'): +if is_win32(): + import ctypes import ctypes.wintypes class BITMAPFILEHEADER(ctypes.Structure): _pack_ = 2 _fields_ = [ - ('bfType', ctypes.wintypes.WORD), - ('bfSize', ctypes.wintypes.DWORD), - ('bfReserved1', ctypes.wintypes.WORD), - ('bfReserved2', ctypes.wintypes.WORD), - ('bfOffBits', ctypes.wintypes.DWORD), + ("bfType", ctypes.wintypes.WORD), + ("bfSize", ctypes.wintypes.DWORD), + ("bfReserved1", ctypes.wintypes.WORD), + ("bfReserved2", ctypes.wintypes.WORD), + ("bfOffBits", ctypes.wintypes.DWORD), ] class BITMAPINFOHEADER(ctypes.Structure): _pack_ = 2 _fields_ = [ - ('biSize', ctypes.wintypes.DWORD), - ('biWidth', ctypes.wintypes.LONG), - ('biHeight', ctypes.wintypes.LONG), - ('biPlanes', ctypes.wintypes.WORD), - ('biBitCount', ctypes.wintypes.WORD), - ('biCompression', ctypes.wintypes.DWORD), - ('biSizeImage', ctypes.wintypes.DWORD), - ('biXPelsPerMeter', ctypes.wintypes.LONG), - ('biYPelsPerMeter', ctypes.wintypes.LONG), - ('biClrUsed', ctypes.wintypes.DWORD), - ('biClrImportant', ctypes.wintypes.DWORD), + ("biSize", ctypes.wintypes.DWORD), + ("biWidth", ctypes.wintypes.LONG), + ("biHeight", ctypes.wintypes.LONG), + ("biPlanes", ctypes.wintypes.WORD), + ("biBitCount", ctypes.wintypes.WORD), + ("biCompression", ctypes.wintypes.DWORD), + ("biSizeImage", ctypes.wintypes.DWORD), + ("biXPelsPerMeter", ctypes.wintypes.LONG), + ("biYPelsPerMeter", ctypes.wintypes.LONG), + ("biClrUsed", ctypes.wintypes.DWORD), + ("biClrImportant", ctypes.wintypes.DWORD), ] BI_RGB = 0 @@ -57,15 +57,19 @@ class BITMAPINFOHEADER(ctypes.Structure): DeleteObject.argtypes = [ctypes.wintypes.HGDIOBJ] CreateDIBSection = ctypes.windll.gdi32.CreateDIBSection - CreateDIBSection.argtypes = [ctypes.wintypes.HDC, ctypes.c_void_p, - ctypes.c_uint, - ctypes.POINTER(ctypes.c_void_p), - ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD] + CreateDIBSection.argtypes = [ + ctypes.wintypes.HDC, + ctypes.c_void_p, + ctypes.c_uint, + ctypes.POINTER(ctypes.c_void_p), + ctypes.wintypes.HANDLE, + ctypes.wintypes.DWORD, + ] CreateDIBSection.restype = ctypes.wintypes.HBITMAP def serialize_dib(bi, pixels): bf = BITMAPFILEHEADER() - bf.bfType = 0x4d42 + bf.bfType = 0x4D42 bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize bf.bfSize = bf.bfOffBits + bi.biSizeImage bf.bfReserved1 = bf.bfReserved2 = 0 @@ -77,37 +81,34 @@ def serialize_dib(bi, pixels): memcpy(bp + bf.bfOffBits, pixels, bi.biSizeImage) return bytearray(buf) - class TestImageWinPointers(PillowTestCase): - def test_pointer(self): - im = hopper() - (width, height) = im.size - opath = self.tempfile('temp.png') - imdib = ImageWin.Dib(im) - - hdr = BITMAPINFOHEADER() - hdr.biSize = ctypes.sizeof(hdr) - hdr.biWidth = width - hdr.biHeight = height - hdr.biPlanes = 1 - hdr.biBitCount = 32 - hdr.biCompression = BI_RGB - hdr.biSizeImage = width * height * 4 - hdr.biClrUsed = 0 - hdr.biClrImportant = 0 - - hdc = CreateCompatibleDC(None) - # print('hdc:',hex(hdc)) - pixels = ctypes.c_void_p() - dib = CreateDIBSection(hdc, ctypes.byref(hdr), DIB_RGB_COLORS, - ctypes.byref(pixels), None, 0) - SelectObject(hdc, dib) - - imdib.expose(hdc) - bitmap = serialize_dib(hdr, pixels) - DeleteObject(dib) - DeleteDC(hdc) - - Image.open(BytesIO(bitmap)).save(opath) - -if __name__ == '__main__': - unittest.main() + def test_pointer(tmp_path): + im = hopper() + (width, height) = im.size + opath = str(tmp_path / "temp.png") + imdib = ImageWin.Dib(im) + + hdr = BITMAPINFOHEADER() + hdr.biSize = ctypes.sizeof(hdr) + hdr.biWidth = width + hdr.biHeight = height + hdr.biPlanes = 1 + hdr.biBitCount = 32 + hdr.biCompression = BI_RGB + hdr.biSizeImage = width * height * 4 + hdr.biClrUsed = 0 + hdr.biClrImportant = 0 + + hdc = CreateCompatibleDC(None) + pixels = ctypes.c_void_p() + dib = CreateDIBSection( + hdc, ctypes.byref(hdr), DIB_RGB_COLORS, ctypes.byref(pixels), None, 0 + ) + SelectObject(hdc, dib) + + imdib.expose(hdc) + bitmap = serialize_dib(hdr, pixels) + DeleteObject(dib) + DeleteDC(hdc) + + with Image.open(BytesIO(bitmap)) as im: + im.save(opath) diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index aefee2e0800..37ed3659d0a 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -1,37 +1,33 @@ -from helper import unittest, PillowTestCase +import pytest from PIL import Image -class TestLibImage(PillowTestCase): +def test_setmode(): - def test_setmode(self): + im = Image.new("L", (1, 1), 255) + im.im.setmode("1") + assert im.im.getpixel((0, 0)) == 255 + im.im.setmode("L") + assert im.im.getpixel((0, 0)) == 255 - im = Image.new("L", (1, 1), 255) - im.im.setmode("1") - self.assertEqual(im.im.getpixel((0, 0)), 255) - im.im.setmode("L") - self.assertEqual(im.im.getpixel((0, 0)), 255) - - im = Image.new("1", (1, 1), 1) - im.im.setmode("L") - self.assertEqual(im.im.getpixel((0, 0)), 255) - im.im.setmode("1") - self.assertEqual(im.im.getpixel((0, 0)), 255) + im = Image.new("1", (1, 1), 1) + im.im.setmode("L") + assert im.im.getpixel((0, 0)) == 255 + im.im.setmode("1") + assert im.im.getpixel((0, 0)) == 255 - im = Image.new("RGB", (1, 1), (1, 2, 3)) - im.im.setmode("RGB") - self.assertEqual(im.im.getpixel((0, 0)), (1, 2, 3)) - im.im.setmode("RGBA") - self.assertEqual(im.im.getpixel((0, 0)), (1, 2, 3, 255)) - im.im.setmode("RGBX") - self.assertEqual(im.im.getpixel((0, 0)), (1, 2, 3, 255)) - im.im.setmode("RGB") - self.assertEqual(im.im.getpixel((0, 0)), (1, 2, 3)) + im = Image.new("RGB", (1, 1), (1, 2, 3)) + im.im.setmode("RGB") + assert im.im.getpixel((0, 0)) == (1, 2, 3) + im.im.setmode("RGBA") + assert im.im.getpixel((0, 0)) == (1, 2, 3, 255) + im.im.setmode("RGBX") + assert im.im.getpixel((0, 0)) == (1, 2, 3, 255) + im.im.setmode("RGB") + assert im.im.getpixel((0, 0)) == (1, 2, 3) - self.assertRaises(ValueError, im.im.setmode, "L") - self.assertRaises(ValueError, im.im.setmode, "RGBABCDE") - - -if __name__ == '__main__': - unittest.main() + with pytest.raises(ValueError): + im.im.setmode("L") + with pytest.raises(ValueError): + im.im.setmode("RGBABCDE") diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index c5eb2686cf5..979806cae99 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -1,14 +1,13 @@ import sys -from helper import unittest, PillowTestCase, py3 +import pytest from PIL import Image - X = 255 -class TestLibPack(PillowTestCase): +class TestLibPack: def assert_pack(self, mode, rawmode, data, *pixels): """ data - either raw bytes with data or just number of bytes in rawmode. @@ -17,226 +16,273 @@ def assert_pack(self, mode, rawmode, data, *pixels): for x, pixel in enumerate(pixels): im.putpixel((x, 0), pixel) - if isinstance(data, (int)): + if isinstance(data, int): data_len = data * len(pixels) - data = bytes(bytearray(range(1, data_len + 1))) + data = bytes(range(1, data_len + 1)) - self.assertEqual(data, im.tobytes("raw", rawmode)) + assert data == im.tobytes("raw", rawmode) def test_1(self): - self.assert_pack("1", "1", b'\x01', 0,0,0,0,0,0,0,X) - self.assert_pack("1", "1;I", b'\x01', X,X,X,X,X,X,X,0) - self.assert_pack("1", "1;R", b'\x01', X,0,0,0,0,0,0,0) - self.assert_pack("1", "1;IR", b'\x01', 0,X,X,X,X,X,X,X) + self.assert_pack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) + self.assert_pack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0) + self.assert_pack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0) + self.assert_pack("1", "1;IR", b"\x01", 0, X, X, X, X, X, X, X) - self.assert_pack("1", "1", b'\xaa', X,0,X,0,X,0,X,0) - self.assert_pack("1", "1;I", b'\xaa', 0,X,0,X,0,X,0,X) - self.assert_pack("1", "1;R", b'\xaa', 0,X,0,X,0,X,0,X) - self.assert_pack("1", "1;IR", b'\xaa', X,0,X,0,X,0,X,0) + self.assert_pack("1", "1", b"\xaa", X, 0, X, 0, X, 0, X, 0) + self.assert_pack("1", "1;I", b"\xaa", 0, X, 0, X, 0, X, 0, X) + self.assert_pack("1", "1;R", b"\xaa", 0, X, 0, X, 0, X, 0, X) + self.assert_pack("1", "1;IR", b"\xaa", X, 0, X, 0, X, 0, X, 0) - self.assert_pack("1", "L", b'\xff\x00\x00\xff\x00\x00', X,0,0,X,0,0) + self.assert_pack("1", "L", b"\xff\x00\x00\xff\x00\x00", X, 0, 0, X, 0, 0) def test_L(self): - self.assert_pack("L", "L", 1, 1,2,3,4) - self.assert_pack("L", "L;16", b'\x00\xc6\x00\xaf', 198, 175) - self.assert_pack("L", "L;16B", b'\xc6\x00\xaf\x00', 198, 175) + self.assert_pack("L", "L", 1, 1, 2, 3, 4) + self.assert_pack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175) + self.assert_pack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175) def test_LA(self): - self.assert_pack("LA", "LA", 2, (1,2), (3,4), (5,6)) - self.assert_pack("LA", "LA;L", 2, (1,4), (2,5), (3,6)) + self.assert_pack("LA", "LA", 2, (1, 2), (3, 4), (5, 6)) + self.assert_pack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6)) + + def test_La(self): + self.assert_pack("La", "La", 2, (1, 2), (3, 4), (5, 6)) def test_P(self): - self.assert_pack("P", "P;1", b'\xe4', 1, 1, 1, 0, 0, 255, 0, 0) - self.assert_pack("P", "P;2", b'\xe4', 3, 2, 1, 0) - self.assert_pack("P", "P;4", b'\x02\xef', 0, 2, 14, 15) + self.assert_pack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 255, 0, 0) + self.assert_pack("P", "P;2", b"\xe4", 3, 2, 1, 0) + self.assert_pack("P", "P;4", b"\x02\xef", 0, 2, 14, 15) self.assert_pack("P", "P", 1, 1, 2, 3, 4) def test_PA(self): - self.assert_pack("PA", "PA", 2, (1,2), (3,4), (5,6)) - self.assert_pack("PA", "PA;L", 2, (1,4), (2,5), (3,6)) + self.assert_pack("PA", "PA", 2, (1, 2), (3, 4), (5, 6)) + self.assert_pack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6)) def test_RGB(self): - self.assert_pack("RGB", "RGB", 3, (1,2,3), (4,5,6), (7,8,9)) - self.assert_pack("RGB", "RGBX", - b'\x01\x02\x03\xff\x05\x06\x07\xff', (1,2,3), (5,6,7)) - self.assert_pack("RGB", "XRGB", - b'\x00\x02\x03\x04\x00\x06\x07\x08', (2,3,4), (6,7,8)) - self.assert_pack("RGB", "BGR", 3, (3,2,1), (6,5,4), (9,8,7)) - self.assert_pack("RGB", "BGRX", - b'\x01\x02\x03\x00\x05\x06\x07\x00', (3,2,1), (7,6,5)) - self.assert_pack("RGB", "XBGR", - b'\x00\x02\x03\x04\x00\x06\x07\x08', (4,3,2), (8,7,6)) - self.assert_pack("RGB", "RGB;L", 3, (1,4,7), (2,5,8), (3,6,9)) - self.assert_pack("RGB", "R", 1, (1,9,9), (2,9,9), (3,9,9)) - self.assert_pack("RGB", "G", 1, (9,1,9), (9,2,9), (9,3,9)) - self.assert_pack("RGB", "B", 1, (9,9,1), (9,9,2), (9,9,3)) + self.assert_pack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + self.assert_pack( + "RGB", "RGBX", b"\x01\x02\x03\xff\x05\x06\x07\xff", (1, 2, 3), (5, 6, 7) + ) + self.assert_pack( + "RGB", "XRGB", b"\x00\x02\x03\x04\x00\x06\x07\x08", (2, 3, 4), (6, 7, 8) + ) + self.assert_pack("RGB", "BGR", 3, (3, 2, 1), (6, 5, 4), (9, 8, 7)) + self.assert_pack( + "RGB", "BGRX", b"\x01\x02\x03\x00\x05\x06\x07\x00", (3, 2, 1), (7, 6, 5) + ) + self.assert_pack( + "RGB", "XBGR", b"\x00\x02\x03\x04\x00\x06\x07\x08", (4, 3, 2), (8, 7, 6) + ) + self.assert_pack("RGB", "RGB;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) + self.assert_pack("RGB", "R", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) + self.assert_pack("RGB", "G", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) + self.assert_pack("RGB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) def test_RGBA(self): - self.assert_pack("RGBA", "RGBA", 4, - (1,2,3,4), (5,6,7,8), (9,10,11,12)) - self.assert_pack("RGBA", "RGBA;L", 4, - (1,4,7,10), (2,5,8,11), (3,6,9,12)) - self.assert_pack("RGBA", "RGB", 3, (1,2,3,14), (4,5,6,15), (7,8,9,16)) - self.assert_pack("RGBA", "BGR", 3, (3,2,1,14), (6,5,4,15), (9,8,7,16)) - self.assert_pack("RGBA", "BGRA", 4, - (3,2,1,4), (7,6,5,8), (11,10,9,12)) - self.assert_pack("RGBA", "ABGR", 4, - (4,3,2,1), (8,7,6,5), (12,11,10,9)) - self.assert_pack("RGBA", "BGRa", 4, - (191,127,63,4), (223,191,159,8), (233,212,191,12)) - self.assert_pack("RGBA", "R", 1, (1,0,8,9), (2,0,8,9), (3,0,8,0)) - self.assert_pack("RGBA", "G", 1, (6,1,8,9), (6,2,8,9), (6,3,8,9)) - self.assert_pack("RGBA", "B", 1, (6,7,1,9), (6,7,2,0), (6,7,3,9)) - self.assert_pack("RGBA", "A", 1, (6,7,0,1), (6,7,0,2), (0,7,0,3)) + self.assert_pack("RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_pack( + "RGBA", "RGBA;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) + self.assert_pack("RGBA", "RGB", 3, (1, 2, 3, 14), (4, 5, 6, 15), (7, 8, 9, 16)) + self.assert_pack("RGBA", "BGR", 3, (3, 2, 1, 14), (6, 5, 4, 15), (9, 8, 7, 16)) + self.assert_pack("RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) + self.assert_pack("RGBA", "ABGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) + self.assert_pack( + "RGBA", + "BGRa", + 4, + (191, 127, 63, 4), + (223, 191, 159, 8), + (233, 212, 191, 12), + ) + self.assert_pack("RGBA", "R", 1, (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) + self.assert_pack("RGBA", "G", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) + self.assert_pack("RGBA", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) + self.assert_pack("RGBA", "A", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) def test_RGBa(self): - self.assert_pack("RGBa", "RGBa", 4, - (1,2,3,4), (5,6,7,8), (9,10,11,12)) - self.assert_pack("RGBa", "BGRa", 4, - (3,2,1,4), (7,6,5,8), (11,10,9,12)) - self.assert_pack("RGBa", "aBGR", 4, - (4,3,2,1), (8,7,6,5), (12,11,10,9)) + self.assert_pack("RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_pack("RGBa", "BGRa", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) + self.assert_pack("RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) def test_RGBX(self): - self.assert_pack("RGBX", "RGBX", 4, (1,2,3,4), (5,6,7,8), (9,10,11,12)) - self.assert_pack("RGBX", "RGBX;L", 4, (1,4,7,10), (2,5,8,11), (3,6,9,12)) - self.assert_pack("RGBX", "RGB", 3, (1,2,3,X), (4,5,6,X), (7,8,9,X)) - self.assert_pack("RGBX", "BGR", 3, (3,2,1,X), (6,5,4,X), (9,8,7,X)) - self.assert_pack("RGBX", "BGRX", - b'\x01\x02\x03\x00\x05\x06\x07\x00\t\n\x0b\x00', - (3,2,1,X), (7,6,5,X), (11,10,9,X)) - self.assert_pack("RGBX", "XBGR", - b'\x00\x02\x03\x04\x00\x06\x07\x08\x00\n\x0b\x0c', - (4,3,2,X), (8,7,6,X), (12,11,10,X)) - self.assert_pack("RGBX", "R", 1, (1,0,8,9), (2,0,8,9), (3,0,8,0)) - self.assert_pack("RGBX", "G", 1, (6,1,8,9), (6,2,8,9), (6,3,8,9)) - self.assert_pack("RGBX", "B", 1, (6,7,1,9), (6,7,2,0), (6,7,3,9)) - self.assert_pack("RGBX", "X", 1, (6,7,0,1), (6,7,0,2), (0,7,0,3)) + self.assert_pack("RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_pack( + "RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) + self.assert_pack("RGBX", "RGB", 3, (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) + self.assert_pack("RGBX", "BGR", 3, (3, 2, 1, X), (6, 5, 4, X), (9, 8, 7, X)) + self.assert_pack( + "RGBX", + "BGRX", + b"\x01\x02\x03\x00\x05\x06\x07\x00\t\n\x0b\x00", + (3, 2, 1, X), + (7, 6, 5, X), + (11, 10, 9, X), + ) + self.assert_pack( + "RGBX", + "XBGR", + b"\x00\x02\x03\x04\x00\x06\x07\x08\x00\n\x0b\x0c", + (4, 3, 2, X), + (8, 7, 6, X), + (12, 11, 10, X), + ) + self.assert_pack("RGBX", "R", 1, (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) + self.assert_pack("RGBX", "G", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) + self.assert_pack("RGBX", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) + self.assert_pack("RGBX", "X", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) def test_CMYK(self): - self.assert_pack("CMYK", "CMYK", 4, (1,2,3,4), (5,6,7,8), (9,10,11,12)) - self.assert_pack("CMYK", "CMYK;I", 4, - (254,253,252,251), (250,249,248,247), (246,245,244,243)) - self.assert_pack("CMYK", "CMYK;L", 4, - (1,4,7,10), (2,5,8,11), (3,6,9,12)) - self.assert_pack("CMYK", "K", 1, (6,7,0,1), (6,7,0,2), (0,7,0,3)) + self.assert_pack("CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_pack( + "CMYK", + "CMYK;I", + 4, + (254, 253, 252, 251), + (250, 249, 248, 247), + (246, 245, 244, 243), + ) + self.assert_pack( + "CMYK", "CMYK;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) + self.assert_pack("CMYK", "K", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) def test_YCbCr(self): - self.assert_pack("YCbCr", "YCbCr", 3, (1,2,3), (4,5,6), (7,8,9)) - self.assert_pack("YCbCr", "YCbCr;L", 3, (1,4,7), (2,5,8), (3,6,9)) - self.assert_pack("YCbCr", "YCbCrX", - b'\x01\x02\x03\xff\x05\x06\x07\xff\t\n\x0b\xff', - (1,2,3), (5,6,7), (9,10,11)) - self.assert_pack("YCbCr", "YCbCrK", - b'\x01\x02\x03\xff\x05\x06\x07\xff\t\n\x0b\xff', - (1,2,3), (5,6,7), (9,10,11)) - self.assert_pack("YCbCr", "Y", 1, (1,0,8,9), (2,0,8,9), (3,0,8,0)) - self.assert_pack("YCbCr", "Cb", 1, (6,1,8,9), (6,2,8,9), (6,3,8,9)) - self.assert_pack("YCbCr", "Cr", 1, (6,7,1,9), (6,7,2,0), (6,7,3,9)) + self.assert_pack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + self.assert_pack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) + self.assert_pack( + "YCbCr", + "YCbCrX", + b"\x01\x02\x03\xff\x05\x06\x07\xff\t\n\x0b\xff", + (1, 2, 3), + (5, 6, 7), + (9, 10, 11), + ) + self.assert_pack( + "YCbCr", + "YCbCrK", + b"\x01\x02\x03\xff\x05\x06\x07\xff\t\n\x0b\xff", + (1, 2, 3), + (5, 6, 7), + (9, 10, 11), + ) + self.assert_pack("YCbCr", "Y", 1, (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) + self.assert_pack("YCbCr", "Cb", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) + self.assert_pack("YCbCr", "Cr", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) def test_LAB(self): - self.assert_pack("LAB", "LAB", 3, - (1,130,131), (4,133,134), (7,136,137)) - self.assert_pack("LAB", "L", 1, (1,9,9), (2,9,9), (3,9,9)) - self.assert_pack("LAB", "A", 1, (9,1,9), (9,2,9), (9,3,9)) - self.assert_pack("LAB", "B", 1, (9,9,1), (9,9,2), (9,9,3)) + self.assert_pack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) + self.assert_pack("LAB", "L", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) + self.assert_pack("LAB", "A", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) + self.assert_pack("LAB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) def test_HSV(self): - self.assert_pack("HSV", "HSV", 3, (1,2,3), (4,5,6), (7,8,9)) - self.assert_pack("HSV", "H", 1, (1,9,9), (2,9,9), (3,9,9)) - self.assert_pack("HSV", "S", 1, (9,1,9), (9,2,9), (9,3,9)) - self.assert_pack("HSV", "V", 1, (9,9,1), (9,9,2), (9,9,3)) + self.assert_pack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + self.assert_pack("HSV", "H", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) + self.assert_pack("HSV", "S", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) + self.assert_pack("HSV", "V", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) def test_I(self): self.assert_pack("I", "I;16B", 2, 0x0102, 0x0304) - self.assert_pack("I", "I;32S", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - 0x01000083, -2097151999) + self.assert_pack( + "I", "I;32S", b"\x83\x00\x00\x01\x01\x00\x00\x83", 0x01000083, -2097151999 + ) - if sys.byteorder == 'little': + if sys.byteorder == "little": self.assert_pack("I", "I", 4, 0x04030201, 0x08070605) - self.assert_pack("I", "I;32NS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - 0x01000083, -2097151999) + self.assert_pack( + "I", + "I;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + 0x01000083, + -2097151999, + ) else: self.assert_pack("I", "I", 4, 0x01020304, 0x05060708) - self.assert_pack("I", "I;32NS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - -2097151999, 0x01000083) + self.assert_pack( + "I", + "I;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + -2097151999, + 0x01000083, + ) def test_F_float(self): - self.assert_pack("F", "F;32F", 4, - 1.539989614439558e-36, 4.063216068939723e-34) - - if sys.byteorder == 'little': - self.assert_pack("F", "F", 4, - 1.539989614439558e-36, 4.063216068939723e-34) - self.assert_pack("F", "F;32NF", 4, - 1.539989614439558e-36, 4.063216068939723e-34) + self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) + + if sys.byteorder == "little": + self.assert_pack("F", "F", 4, 1.539989614439558e-36, 4.063216068939723e-34) + self.assert_pack( + "F", "F;32NF", 4, 1.539989614439558e-36, 4.063216068939723e-34 + ) else: - self.assert_pack("F", "F", 4, - 2.387939260590663e-38, 6.301941157072183e-36) - self.assert_pack("F", "F;32NF", 4, - 2.387939260590663e-38, 6.301941157072183e-36) + self.assert_pack("F", "F", 4, 2.387939260590663e-38, 6.301941157072183e-36) + self.assert_pack( + "F", "F;32NF", 4, 2.387939260590663e-38, 6.301941157072183e-36 + ) -class TestLibUnpack(PillowTestCase): +class TestLibUnpack: def assert_unpack(self, mode, rawmode, data, *pixels): """ data - either raw bytes with data or just number of bytes in rawmode. """ - if isinstance(data, (int)): + if isinstance(data, int): data_len = data * len(pixels) - data = bytes(bytearray(range(1, data_len + 1))) + data = bytes(range(1, data_len + 1)) im = Image.frombytes(mode, (len(pixels), 1), data, "raw", rawmode, 0, 1) for x, pixel in enumerate(pixels): - self.assertEqual(pixel, im.getpixel((x, 0))) + assert pixel == im.getpixel((x, 0)) def test_1(self): - self.assert_unpack("1", "1", b'\x01', 0, 0, 0, 0, 0, 0, 0, X) - self.assert_unpack("1", "1;I", b'\x01', X, X, X, X, X, X, X, 0) - self.assert_unpack("1", "1;R", b'\x01', X, 0, 0, 0, 0, 0, 0, 0) - self.assert_unpack("1", "1;IR", b'\x01', 0, X, X, X, X, X, X, X) + self.assert_unpack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) + self.assert_unpack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0) + self.assert_unpack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0) + self.assert_unpack("1", "1;IR", b"\x01", 0, X, X, X, X, X, X, X) - self.assert_unpack("1", "1", b'\xaa', X, 0, X, 0, X, 0, X, 0) - self.assert_unpack("1", "1;I", b'\xaa', 0, X, 0, X, 0, X, 0, X) - self.assert_unpack("1", "1;R", b'\xaa', 0, X, 0, X, 0, X, 0, X) - self.assert_unpack("1", "1;IR", b'\xaa', X, 0, X, 0, X, 0, X, 0) + self.assert_unpack("1", "1", b"\xaa", X, 0, X, 0, X, 0, X, 0) + self.assert_unpack("1", "1;I", b"\xaa", 0, X, 0, X, 0, X, 0, X) + self.assert_unpack("1", "1;R", b"\xaa", 0, X, 0, X, 0, X, 0, X) + self.assert_unpack("1", "1;IR", b"\xaa", X, 0, X, 0, X, 0, X, 0) - self.assert_unpack("1", "1;8", b'\x00\x01\x02\xff', 0, X, X, X) + self.assert_unpack("1", "1;8", b"\x00\x01\x02\xff", 0, X, X, X) def test_L(self): - self.assert_unpack("L", "L;2", b'\xe4', 255, 170, 85, 0) - self.assert_unpack("L", "L;2I", b'\xe4', 0, 85, 170, 255) - self.assert_unpack("L", "L;2R", b'\xe4', 0, 170, 85, 255) - self.assert_unpack("L", "L;2IR", b'\xe4', 255, 85, 170, 0) + self.assert_unpack("L", "L;2", b"\xe4", 255, 170, 85, 0) + self.assert_unpack("L", "L;2I", b"\xe4", 0, 85, 170, 255) + self.assert_unpack("L", "L;2R", b"\xe4", 0, 170, 85, 255) + self.assert_unpack("L", "L;2IR", b"\xe4", 255, 85, 170, 0) - self.assert_unpack("L", "L;4", b'\x02\xef', 0, 34, 238, 255) - self.assert_unpack("L", "L;4I", b'\x02\xef', 255, 221, 17, 0) - self.assert_unpack("L", "L;4R", b'\x02\xef', 68, 0, 255, 119) - self.assert_unpack("L", "L;4IR", b'\x02\xef', 187, 255, 0, 136) + self.assert_unpack("L", "L;4", b"\x02\xef", 0, 34, 238, 255) + self.assert_unpack("L", "L;4I", b"\x02\xef", 255, 221, 17, 0) + self.assert_unpack("L", "L;4R", b"\x02\xef", 68, 0, 255, 119) + self.assert_unpack("L", "L;4IR", b"\x02\xef", 187, 255, 0, 136) self.assert_unpack("L", "L", 1, 1, 2, 3, 4) self.assert_unpack("L", "L;I", 1, 254, 253, 252, 251) self.assert_unpack("L", "L;R", 1, 128, 64, 192, 32) self.assert_unpack("L", "L;16", 2, 2, 4, 6, 8) self.assert_unpack("L", "L;16B", 2, 1, 3, 5, 7) - self.assert_unpack("L", "L;16", b'\x00\xc6\x00\xaf', 198, 175) - self.assert_unpack("L", "L;16B", b'\xc6\x00\xaf\x00', 198, 175) - + self.assert_unpack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175) + self.assert_unpack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175) def test_LA(self): self.assert_unpack("LA", "LA", 2, (1, 2), (3, 4), (5, 6)) self.assert_unpack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6)) + def test_La(self): + self.assert_unpack("La", "La", 2, (1, 2), (3, 4), (5, 6)) + def test_P(self): - self.assert_unpack("P", "P;1", b'\xe4', 1, 1, 1, 0, 0, 1, 0, 0) - self.assert_unpack("P", "P;2", b'\xe4', 3, 2, 1, 0) - # self.assert_unpack("P", "P;2L", b'\xe4', 1, 1, 1, 0) # erroneous? - self.assert_unpack("P", "P;4", b'\x02\xef', 0, 2, 14, 15) - # self.assert_unpack("P", "P;4L", b'\x02\xef', 2, 10, 10, 0) # erroneous? + self.assert_unpack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 1, 0, 0) + self.assert_unpack("P", "P;2", b"\xe4", 3, 2, 1, 0) + # erroneous? + # self.assert_unpack("P", "P;2L", b'\xe4', 1, 1, 1, 0) + self.assert_unpack("P", "P;4", b"\x02\xef", 0, 2, 14, 15) + # erroneous? + # self.assert_unpack("P", "P;4L", b'\x02\xef', 2, 10, 10, 0) self.assert_unpack("P", "P", 1, 1, 2, 3, 4) self.assert_unpack("P", "P;R", 1, 128, 64, 192, 32) @@ -245,252 +291,473 @@ def test_PA(self): self.assert_unpack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6)) def test_RGB(self): - self.assert_unpack("RGB", "RGB", 3, (1,2,3), (4,5,6), (7,8,9)) - self.assert_unpack("RGB", "RGB;L", 3, (1,4,7), (2,5,8), (3,6,9)) - self.assert_unpack("RGB", "RGB;R", 3, (128,64,192), (32,160,96)) - self.assert_unpack("RGB", "RGB;16L", 6, (2,4,6), (8,10,12)) - self.assert_unpack("RGB", "RGB;16B", 6, (1,3,5), (7,9,11)) - self.assert_unpack("RGB", "BGR", 3, (3,2,1), (6,5,4), (9,8,7)) - self.assert_unpack("RGB", "RGB;15", 2, (8,131,0), (24,0,8)) - self.assert_unpack("RGB", "BGR;15", 2, (0,131,8), (8,0,24)) - self.assert_unpack("RGB", "RGB;16", 2, (8,64,0), (24,129,0)) - self.assert_unpack("RGB", "BGR;16", 2, (0,64,8), (0,129,24)) - self.assert_unpack("RGB", "RGB;4B", 2, (17,0,34), (51,0,68)) - self.assert_unpack("RGB", "RGBX", 4, (1,2,3), (5,6,7), (9,10,11)) - self.assert_unpack("RGB", "RGBX;L", 4, (1,4,7), (2,5,8), (3,6,9)) - self.assert_unpack("RGB", "BGRX", 4, (3,2,1), (7,6,5), (11,10,9)) - self.assert_unpack("RGB", "XRGB", 4, (2,3,4), (6,7,8), (10,11,12)) - self.assert_unpack("RGB", "XBGR", 4, (4,3,2), (8,7,6), (12,11,10)) - self.assert_unpack("RGB", "YCC;P", - b'D]\x9c\x82\x1a\x91\xfaOC\xe7J\x12', # random data - (127,102,0), (192,227,0), (213,255,170), (98,255,133)) - self.assert_unpack("RGB", "R", 1, (1,0,0), (2,0,0), (3,0,0)) - self.assert_unpack("RGB", "G", 1, (0,1,0), (0,2,0), (0,3,0)) - self.assert_unpack("RGB", "B", 1, (0,0,1), (0,0,2), (0,0,3)) + self.assert_unpack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + self.assert_unpack("RGB", "RGB;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) + self.assert_unpack("RGB", "RGB;R", 3, (128, 64, 192), (32, 160, 96)) + self.assert_unpack("RGB", "RGB;16L", 6, (2, 4, 6), (8, 10, 12)) + self.assert_unpack("RGB", "RGB;16B", 6, (1, 3, 5), (7, 9, 11)) + self.assert_unpack("RGB", "BGR", 3, (3, 2, 1), (6, 5, 4), (9, 8, 7)) + self.assert_unpack("RGB", "RGB;15", 2, (8, 131, 0), (24, 0, 8)) + self.assert_unpack("RGB", "BGR;15", 2, (0, 131, 8), (8, 0, 24)) + self.assert_unpack("RGB", "RGB;16", 2, (8, 64, 0), (24, 129, 0)) + self.assert_unpack("RGB", "BGR;16", 2, (0, 64, 8), (0, 129, 24)) + self.assert_unpack("RGB", "RGB;4B", 2, (17, 0, 34), (51, 0, 68)) + self.assert_unpack("RGB", "RGBX", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) + self.assert_unpack("RGB", "RGBX;L", 4, (1, 4, 7), (2, 5, 8), (3, 6, 9)) + self.assert_unpack("RGB", "BGRX", 4, (3, 2, 1), (7, 6, 5), (11, 10, 9)) + self.assert_unpack("RGB", "XRGB", 4, (2, 3, 4), (6, 7, 8), (10, 11, 12)) + self.assert_unpack("RGB", "XBGR", 4, (4, 3, 2), (8, 7, 6), (12, 11, 10)) + self.assert_unpack( + "RGB", + "YCC;P", + b"D]\x9c\x82\x1a\x91\xfaOC\xe7J\x12", # random data + (127, 102, 0), + (192, 227, 0), + (213, 255, 170), + (98, 255, 133), + ) + self.assert_unpack("RGB", "R", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) + self.assert_unpack("RGB", "G", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) + self.assert_unpack("RGB", "B", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) + + self.assert_unpack("RGB", "R;16B", 2, (1, 0, 0), (3, 0, 0), (5, 0, 0)) + self.assert_unpack("RGB", "G;16B", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0)) + self.assert_unpack("RGB", "B;16B", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5)) + + self.assert_unpack("RGB", "R;16L", 2, (2, 0, 0), (4, 0, 0), (6, 0, 0)) + self.assert_unpack("RGB", "G;16L", 2, (0, 2, 0), (0, 4, 0), (0, 6, 0)) + self.assert_unpack("RGB", "B;16L", 2, (0, 0, 2), (0, 0, 4), (0, 0, 6)) + + if sys.byteorder == "little": + self.assert_unpack("RGB", "R;16N", 2, (2, 0, 0), (4, 0, 0), (6, 0, 0)) + self.assert_unpack("RGB", "G;16N", 2, (0, 2, 0), (0, 4, 0), (0, 6, 0)) + self.assert_unpack("RGB", "B;16N", 2, (0, 0, 2), (0, 0, 4), (0, 0, 6)) + else: + self.assert_unpack("RGB", "R;16N", 2, (1, 0, 0), (3, 0, 0), (5, 0, 0)) + self.assert_unpack("RGB", "G;16N", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0)) + self.assert_unpack("RGB", "B;16N", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5)) def test_RGBA(self): - self.assert_unpack("RGBA", "LA", 2, (1,1,1,2), (3,3,3,4), (5,5,5,6)) - self.assert_unpack("RGBA", "LA;16B", 4, - (1,1,1,3), (5,5,5,7), (9,9,9,11)) - self.assert_unpack("RGBA", "RGBA", 4, - (1,2,3,4), (5,6,7,8), (9,10,11,12)) - self.assert_unpack("RGBA", "RGBa", 4, - (63,127,191,4), (159,191,223,8), (191,212,233,12)) - self.assert_unpack("RGBA", "RGBa", - b'\x01\x02\x03\x00\x10\x20\x30\xff', - (0,0,0,0), (16,32,48,255)) - self.assert_unpack("RGBA", "RGBa;16L", 8, - (63,127,191,8), (159,191,223,16), (191,212,233,24)) - self.assert_unpack("RGBA", "RGBa;16L", - b'\x88\x01\x88\x02\x88\x03\x88\x00' - b'\x88\x10\x88\x20\x88\x30\x88\xff', - (0,0,0,0), (16,32,48,255)) - self.assert_unpack("RGBA", "RGBa;16B", 8, - (36,109,182,7), (153,187,221,15), (188,210,232,23)) - self.assert_unpack("RGBA", "RGBa;16B", - b'\x01\x88\x02\x88\x03\x88\x00\x88' - b'\x10\x88\x20\x88\x30\x88\xff\x88', - (0,0,0,0), (16,32,48,255)) - self.assert_unpack("RGBA", "BGRa", 4, - (191,127,63,4), (223,191,159,8), (233,212,191,12)) - self.assert_unpack("RGBA", "BGRa", - b'\x01\x02\x03\x00\x10\x20\x30\xff', - (0,0,0,0), (48,32,16,255)) - self.assert_unpack("RGBA", "RGBA;I", 4, - (254,253,252,4), (250,249,248,8), (246,245,244,12)) - self.assert_unpack("RGBA", "RGBA;L", 4, - (1,4,7,10), (2,5,8,11), (3,6,9,12)) - self.assert_unpack("RGBA", "RGBA;15", 2, (8,131,0,0), (24,0,8,0)) - self.assert_unpack("RGBA", "BGRA;15", 2, (0,131,8,0), (8,0,24,0)) - self.assert_unpack("RGBA", "RGBA;4B", 2, (17,0,34,0), (51,0,68,0)) - self.assert_unpack("RGBA", "RGBA;16L", 8, (2,4,6,8), (10,12,14,16)) - self.assert_unpack("RGBA", "RGBA;16B", 8, (1,3,5,7), (9,11,13,15)) - self.assert_unpack("RGBA", "BGRA", 4, - (3,2,1,4), (7,6,5,8), (11,10,9,12)) - self.assert_unpack("RGBA", "ARGB", 4, - (2,3,4,1), (6,7,8,5), (10,11,12,9)) - self.assert_unpack("RGBA", "ABGR", 4, - (4,3,2,1), (8,7,6,5), (12,11,10,9)) - self.assert_unpack("RGBA", "YCCA;P", - b']bE\x04\xdd\xbej\xed57T\xce\xac\xce:\x11', # random data - (0,161,0,4), (255,255,255,237), (27,158,0,206), (0,118,0,17)) - self.assert_unpack("RGBA", "R", 1, (1,0,0,0), (2,0,0,0), (3,0,0,0)) - self.assert_unpack("RGBA", "G", 1, (0,1,0,0), (0,2,0,0), (0,3,0,0)) - self.assert_unpack("RGBA", "B", 1, (0,0,1,0), (0,0,2,0), (0,0,3,0)) - self.assert_unpack("RGBA", "A", 1, (0,0,0,1), (0,0,0,2), (0,0,0,3)) + self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) + self.assert_unpack( + "RGBA", "LA;16B", 4, (1, 1, 1, 3), (5, 5, 5, 7), (9, 9, 9, 11) + ) + self.assert_unpack( + "RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) + ) + self.assert_unpack( + "RGBA", "RGBAX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14) + ) + self.assert_unpack( + "RGBA", "RGBAXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16) + ) + self.assert_unpack( + "RGBA", + "RGBa", + 4, + (63, 127, 191, 4), + (159, 191, 223, 8), + (191, 212, 233, 12), + ) + self.assert_unpack( + "RGBA", + "RGBa", + b"\x01\x02\x03\x00\x10\x20\x30\x7f\x10\x20\x30\xff", + (0, 0, 0, 0), + (32, 64, 96, 127), + (16, 32, 48, 255), + ) + self.assert_unpack( + "RGBA", + "RGBaX", + b"\x01\x02\x03\x00-\x10\x20\x30\x7f-\x10\x20\x30\xff-", + (0, 0, 0, 0), + (32, 64, 96, 127), + (16, 32, 48, 255), + ) + self.assert_unpack( + "RGBA", + "RGBaXX", + b"\x01\x02\x03\x00==\x10\x20\x30\x7f!!\x10\x20\x30\xff??", + (0, 0, 0, 0), + (32, 64, 96, 127), + (16, 32, 48, 255), + ) + self.assert_unpack( + "RGBA", + "RGBa;16L", + 8, + (63, 127, 191, 8), + (159, 191, 223, 16), + (191, 212, 233, 24), + ) + self.assert_unpack( + "RGBA", + "RGBa;16L", + b"\x88\x01\x88\x02\x88\x03\x88\x00\x88\x10\x88\x20\x88\x30\x88\xff", + (0, 0, 0, 0), + (16, 32, 48, 255), + ) + self.assert_unpack( + "RGBA", + "RGBa;16B", + 8, + (36, 109, 182, 7), + (153, 187, 221, 15), + (188, 210, 232, 23), + ) + self.assert_unpack( + "RGBA", + "RGBa;16B", + b"\x01\x88\x02\x88\x03\x88\x00\x88\x10\x88\x20\x88\x30\x88\xff\x88", + (0, 0, 0, 0), + (16, 32, 48, 255), + ) + self.assert_unpack( + "RGBA", + "BGRa", + 4, + (191, 127, 63, 4), + (223, 191, 159, 8), + (233, 212, 191, 12), + ) + self.assert_unpack( + "RGBA", + "BGRa", + b"\x01\x02\x03\x00\x10\x20\x30\xff", + (0, 0, 0, 0), + (48, 32, 16, 255), + ) + self.assert_unpack( + "RGBA", + "RGBA;I", + 4, + (254, 253, 252, 4), + (250, 249, 248, 8), + (246, 245, 244, 12), + ) + self.assert_unpack( + "RGBA", "RGBA;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) + self.assert_unpack("RGBA", "RGBA;15", 2, (8, 131, 0, 0), (24, 0, 8, 0)) + self.assert_unpack("RGBA", "BGRA;15", 2, (0, 131, 8, 0), (8, 0, 24, 0)) + self.assert_unpack("RGBA", "RGBA;4B", 2, (17, 0, 34, 0), (51, 0, 68, 0)) + self.assert_unpack("RGBA", "RGBA;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) + self.assert_unpack("RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) + self.assert_unpack("RGBA", "BGRA;16L", 8, (6, 4, 2, 8), (14, 12, 10, 16)) + self.assert_unpack("RGBA", "BGRA;16B", 8, (5, 3, 1, 7), (13, 11, 9, 15)) + self.assert_unpack( + "RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12) + ) + self.assert_unpack( + "RGBA", "ARGB", 4, (2, 3, 4, 1), (6, 7, 8, 5), (10, 11, 12, 9) + ) + self.assert_unpack( + "RGBA", "ABGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9) + ) + self.assert_unpack( + "RGBA", + "YCCA;P", + b"]bE\x04\xdd\xbej\xed57T\xce\xac\xce:\x11", # random data + (0, 161, 0, 4), + (255, 255, 255, 237), + (27, 158, 0, 206), + (0, 118, 0, 17), + ) + self.assert_unpack("RGBA", "R", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) + self.assert_unpack("RGBA", "G", 1, (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) + self.assert_unpack("RGBA", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) + self.assert_unpack("RGBA", "A", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) + + self.assert_unpack("RGBA", "R;16B", 2, (1, 0, 0, 0), (3, 0, 0, 0), (5, 0, 0, 0)) + self.assert_unpack("RGBA", "G;16B", 2, (0, 1, 0, 0), (0, 3, 0, 0), (0, 5, 0, 0)) + self.assert_unpack("RGBA", "B;16B", 2, (0, 0, 1, 0), (0, 0, 3, 0), (0, 0, 5, 0)) + self.assert_unpack("RGBA", "A;16B", 2, (0, 0, 0, 1), (0, 0, 0, 3), (0, 0, 0, 5)) + + self.assert_unpack("RGBA", "R;16L", 2, (2, 0, 0, 0), (4, 0, 0, 0), (6, 0, 0, 0)) + self.assert_unpack("RGBA", "G;16L", 2, (0, 2, 0, 0), (0, 4, 0, 0), (0, 6, 0, 0)) + self.assert_unpack("RGBA", "B;16L", 2, (0, 0, 2, 0), (0, 0, 4, 0), (0, 0, 6, 0)) + self.assert_unpack("RGBA", "A;16L", 2, (0, 0, 0, 2), (0, 0, 0, 4), (0, 0, 0, 6)) + + if sys.byteorder == "little": + self.assert_unpack( + "RGBA", "R;16N", 2, (2, 0, 0, 0), (4, 0, 0, 0), (6, 0, 0, 0) + ) + self.assert_unpack( + "RGBA", "G;16N", 2, (0, 2, 0, 0), (0, 4, 0, 0), (0, 6, 0, 0) + ) + self.assert_unpack( + "RGBA", "B;16N", 2, (0, 0, 2, 0), (0, 0, 4, 0), (0, 0, 6, 0) + ) + self.assert_unpack( + "RGBA", "A;16N", 2, (0, 0, 0, 2), (0, 0, 0, 4), (0, 0, 0, 6) + ) + else: + self.assert_unpack( + "RGBA", "R;16N", 2, (1, 0, 0, 0), (3, 0, 0, 0), (5, 0, 0, 0) + ) + self.assert_unpack( + "RGBA", "G;16N", 2, (0, 1, 0, 0), (0, 3, 0, 0), (0, 5, 0, 0) + ) + self.assert_unpack( + "RGBA", "B;16N", 2, (0, 0, 1, 0), (0, 0, 3, 0), (0, 0, 5, 0) + ) + self.assert_unpack( + "RGBA", "A;16N", 2, (0, 0, 0, 1), (0, 0, 0, 3), (0, 0, 0, 5) + ) def test_RGBa(self): - self.assert_unpack("RGBa", "RGBa", 4, - (1,2,3,4), (5,6,7,8), (9,10,11,12)) - self.assert_unpack("RGBa", "BGRa", 4, - (3,2,1,4), (7,6,5,8), (11,10,9,12)) - self.assert_unpack("RGBa", "aRGB", 4, - (2,3,4,1), (6,7,8,5), (10,11,12,9)) - self.assert_unpack("RGBa", "aBGR", 4, - (4,3,2,1), (8,7,6,5), (12,11,10,9)) + self.assert_unpack( + "RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) + ) + self.assert_unpack( + "RGBa", "BGRa", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12) + ) + self.assert_unpack( + "RGBa", "aRGB", 4, (2, 3, 4, 1), (6, 7, 8, 5), (10, 11, 12, 9) + ) + self.assert_unpack( + "RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9) + ) def test_RGBX(self): - self.assert_unpack("RGBX", "RGB", 3, (1,2,3,X), (4,5,6,X), (7,8,9,X)) - self.assert_unpack("RGBX", "RGB;L", 3, (1,4,7,X), (2,5,8,X), (3,6,9,X)) - self.assert_unpack("RGBX", "RGB;16B", 6, (1,3,5,X), (7,9,11,X)) - self.assert_unpack("RGBX", "BGR", 3, (3,2,1,X), (6,5,4,X), (9,8,7,X)) - self.assert_unpack("RGBX", "RGB;15", 2, (8,131,0,X), (24,0,8,X)) - self.assert_unpack("RGBX", "BGR;15", 2, (0,131,8,X), (8,0,24,X)) - self.assert_unpack("RGBX", "RGB;4B", 2, (17,0,34,X), (51,0,68,X)) - self.assert_unpack("RGBX", "RGBX", 4, - (1,2,3,4), (5,6,7,8), (9,10,11,12)) - self.assert_unpack("RGBX", "RGBX;L", 4, - (1,4,7,10), (2,5,8,11), (3,6,9,12)) - self.assert_unpack("RGBX", "RGBX;16L", 8, (2,4,6,8), (10,12,14,16)) - self.assert_unpack("RGBX", "RGBX;16B", 8, (1,3,5,7), (9,11,13,15)) - self.assert_unpack("RGBX", "BGRX", 4, (3,2,1,X), (7,6,5,X), (11,10,9,X)) - self.assert_unpack("RGBX", "XRGB", 4, (2,3,4,X), (6,7,8,X), (10,11,12,X)) - self.assert_unpack("RGBX", "XBGR", 4, (4,3,2,X), (8,7,6,X), (12,11,10,X)) - self.assert_unpack("RGBX", "YCC;P", - b'D]\x9c\x82\x1a\x91\xfaOC\xe7J\x12', # random data - (127,102,0,X), (192,227,0,X), (213,255,170,X), (98,255,133,X)) - self.assert_unpack("RGBX", "R", 1, (1,0,0,0), (2,0,0,0), (3,0,0,0)) - self.assert_unpack("RGBX", "G", 1, (0,1,0,0), (0,2,0,0), (0,3,0,0)) - self.assert_unpack("RGBX", "B", 1, (0,0,1,0), (0,0,2,0), (0,0,3,0)) - self.assert_unpack("RGBX", "X", 1, (0,0,0,1), (0,0,0,2), (0,0,0,3)) + self.assert_unpack("RGBX", "RGB", 3, (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) + self.assert_unpack("RGBX", "RGB;L", 3, (1, 4, 7, X), (2, 5, 8, X), (3, 6, 9, X)) + self.assert_unpack("RGBX", "RGB;16B", 6, (1, 3, 5, X), (7, 9, 11, X)) + self.assert_unpack("RGBX", "BGR", 3, (3, 2, 1, X), (6, 5, 4, X), (9, 8, 7, X)) + self.assert_unpack("RGBX", "RGB;15", 2, (8, 131, 0, X), (24, 0, 8, X)) + self.assert_unpack("RGBX", "BGR;15", 2, (0, 131, 8, X), (8, 0, 24, X)) + self.assert_unpack("RGBX", "RGB;4B", 2, (17, 0, 34, X), (51, 0, 68, X)) + self.assert_unpack( + "RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) + ) + self.assert_unpack( + "RGBX", "RGBXX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14) + ) + self.assert_unpack( + "RGBX", "RGBXXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16) + ) + self.assert_unpack( + "RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) + self.assert_unpack("RGBX", "RGBX;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) + self.assert_unpack("RGBX", "RGBX;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) + self.assert_unpack( + "RGBX", "BGRX", 4, (3, 2, 1, X), (7, 6, 5, X), (11, 10, 9, X) + ) + self.assert_unpack( + "RGBX", "XRGB", 4, (2, 3, 4, X), (6, 7, 8, X), (10, 11, 12, X) + ) + self.assert_unpack( + "RGBX", "XBGR", 4, (4, 3, 2, X), (8, 7, 6, X), (12, 11, 10, X) + ) + self.assert_unpack( + "RGBX", + "YCC;P", + b"D]\x9c\x82\x1a\x91\xfaOC\xe7J\x12", # random data + (127, 102, 0, X), + (192, 227, 0, X), + (213, 255, 170, X), + (98, 255, 133, X), + ) + self.assert_unpack("RGBX", "R", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) + self.assert_unpack("RGBX", "G", 1, (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) + self.assert_unpack("RGBX", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) + self.assert_unpack("RGBX", "X", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) def test_CMYK(self): - self.assert_unpack("CMYK", "CMYK", 4, (1,2,3,4), (5,6,7,8), (9,10,11,12)) - self.assert_unpack("CMYK", "CMYK;I", 4, - (254,253,252,251), (250,249,248,247), (246,245,244,243)) - self.assert_unpack("CMYK", "CMYK;L", 4, - (1,4,7,10), (2,5,8,11), (3,6,9,12)) - self.assert_unpack("CMYK", "C", 1, (1,0,0,0), (2,0,0,0), (3,0,0,0)) - self.assert_unpack("CMYK", "M", 1, (0,1,0,0), (0,2,0,0), (0,3,0,0)) - self.assert_unpack("CMYK", "Y", 1, (0,0,1,0), (0,0,2,0), (0,0,3,0)) - self.assert_unpack("CMYK", "K", 1, (0,0,0,1), (0,0,0,2), (0,0,0,3)) - self.assert_unpack("CMYK", "C;I", 1, - (254,0,0,0), (253,0,0,0), (252,0,0,0)) - self.assert_unpack("CMYK", "M;I", 1, - (0,254,0,0), (0,253,0,0), (0,252,0,0)) - self.assert_unpack("CMYK", "Y;I", 1, - (0,0,254,0), (0,0,253,0), (0,0,252,0)) - self.assert_unpack("CMYK", "K;I", 1, - (0,0,0,254), (0,0,0,253), (0,0,0,252)) + self.assert_unpack( + "CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) + ) + self.assert_unpack( + "CMYK", "CMYKX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14) + ) + self.assert_unpack( + "CMYK", "CMYKXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16) + ) + self.assert_unpack( + "CMYK", + "CMYK;I", + 4, + (254, 253, 252, 251), + (250, 249, 248, 247), + (246, 245, 244, 243), + ) + self.assert_unpack( + "CMYK", "CMYK;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) + ) + self.assert_unpack("CMYK", "C", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) + self.assert_unpack("CMYK", "M", 1, (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) + self.assert_unpack("CMYK", "Y", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) + self.assert_unpack("CMYK", "K", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) + self.assert_unpack( + "CMYK", "C;I", 1, (254, 0, 0, 0), (253, 0, 0, 0), (252, 0, 0, 0) + ) + self.assert_unpack( + "CMYK", "M;I", 1, (0, 254, 0, 0), (0, 253, 0, 0), (0, 252, 0, 0) + ) + self.assert_unpack( + "CMYK", "Y;I", 1, (0, 0, 254, 0), (0, 0, 253, 0), (0, 0, 252, 0) + ) + self.assert_unpack( + "CMYK", "K;I", 1, (0, 0, 0, 254), (0, 0, 0, 253), (0, 0, 0, 252) + ) def test_YCbCr(self): - self.assert_unpack("YCbCr", "YCbCr", 3, (1,2,3), (4,5,6), (7,8,9)) - self.assert_unpack("YCbCr", "YCbCr;L", 3, (1,4,7), (2,5,8), (3,6,9)) - self.assert_unpack("YCbCr", "YCbCrX", 4, (1,2,3), (5,6,7), (9,10,11)) - self.assert_unpack("YCbCr", "YCbCrK", 4, (1,2,3), (5,6,7), (9,10,11)) + self.assert_unpack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + self.assert_unpack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) + self.assert_unpack("YCbCr", "YCbCrK", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) + self.assert_unpack("YCbCr", "YCbCrX", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) def test_LAB(self): - self.assert_unpack("LAB", "LAB", 3, - (1,130,131), (4,133,134), (7,136,137)) - self.assert_unpack("LAB", "L", 1, (1,0,0), (2,0,0), (3,0,0)) - self.assert_unpack("LAB", "A", 1, (0,1,0), (0,2,0), (0,3,0)) - self.assert_unpack("LAB", "B", 1, (0,0,1), (0,0,2), (0,0,3)) + self.assert_unpack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) + self.assert_unpack("LAB", "L", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) + self.assert_unpack("LAB", "A", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) + self.assert_unpack("LAB", "B", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) def test_HSV(self): - self.assert_unpack("HSV", "HSV", 3, (1,2,3), (4,5,6), (7,8,9)) - self.assert_unpack("HSV", "H", 1, (1,0,0), (2,0,0), (3,0,0)) - self.assert_unpack("HSV", "S", 1, (0,1,0), (0,2,0), (0,3,0)) - self.assert_unpack("HSV", "V", 1, (0,0,1), (0,0,2), (0,0,3)) + self.assert_unpack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + self.assert_unpack("HSV", "H", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) + self.assert_unpack("HSV", "S", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) + self.assert_unpack("HSV", "V", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) def test_I(self): self.assert_unpack("I", "I;8", 1, 0x01, 0x02, 0x03, 0x04) - self.assert_unpack("I", "I;8S", b'\x01\x83', 1, -125) + self.assert_unpack("I", "I;8S", b"\x01\x83", 1, -125) self.assert_unpack("I", "I;16", 2, 0x0201, 0x0403) - self.assert_unpack("I", "I;16S", b'\x83\x01\x01\x83', 0x0183, -31999) + self.assert_unpack("I", "I;16S", b"\x83\x01\x01\x83", 0x0183, -31999) self.assert_unpack("I", "I;16B", 2, 0x0102, 0x0304) - self.assert_unpack("I", "I;16BS", b'\x83\x01\x01\x83', -31999, 0x0183) + self.assert_unpack("I", "I;16BS", b"\x83\x01\x01\x83", -31999, 0x0183) self.assert_unpack("I", "I;32", 4, 0x04030201, 0x08070605) - self.assert_unpack("I", "I;32S", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - 0x01000083, -2097151999) + self.assert_unpack( + "I", "I;32S", b"\x83\x00\x00\x01\x01\x00\x00\x83", 0x01000083, -2097151999 + ) self.assert_unpack("I", "I;32B", 4, 0x01020304, 0x05060708) - self.assert_unpack("I", "I;32BS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - -2097151999, 0x01000083) + self.assert_unpack( + "I", "I;32BS", b"\x83\x00\x00\x01\x01\x00\x00\x83", -2097151999, 0x01000083 + ) - if sys.byteorder == 'little': + if sys.byteorder == "little": self.assert_unpack("I", "I", 4, 0x04030201, 0x08070605) self.assert_unpack("I", "I;16N", 2, 0x0201, 0x0403) - self.assert_unpack("I", "I;16NS", b'\x83\x01\x01\x83', 0x0183, -31999) + self.assert_unpack("I", "I;16NS", b"\x83\x01\x01\x83", 0x0183, -31999) self.assert_unpack("I", "I;32N", 4, 0x04030201, 0x08070605) - self.assert_unpack("I", "I;32NS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - 0x01000083, -2097151999) + self.assert_unpack( + "I", + "I;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + 0x01000083, + -2097151999, + ) else: self.assert_unpack("I", "I", 4, 0x01020304, 0x05060708) self.assert_unpack("I", "I;16N", 2, 0x0102, 0x0304) - self.assert_unpack("I", "I;16NS", b'\x83\x01\x01\x83', -31999, 0x0183) + self.assert_unpack("I", "I;16NS", b"\x83\x01\x01\x83", -31999, 0x0183) self.assert_unpack("I", "I;32N", 4, 0x01020304, 0x05060708) - self.assert_unpack("I", "I;32NS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - -2097151999, 0x01000083) + self.assert_unpack( + "I", + "I;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + -2097151999, + 0x01000083, + ) def test_F_int(self): self.assert_unpack("F", "F;8", 1, 0x01, 0x02, 0x03, 0x04) - self.assert_unpack("F", "F;8S", b'\x01\x83', 1, -125) + self.assert_unpack("F", "F;8S", b"\x01\x83", 1, -125) self.assert_unpack("F", "F;16", 2, 0x0201, 0x0403) - self.assert_unpack("F", "F;16S", b'\x83\x01\x01\x83', 0x0183, -31999) + self.assert_unpack("F", "F;16S", b"\x83\x01\x01\x83", 0x0183, -31999) self.assert_unpack("F", "F;16B", 2, 0x0102, 0x0304) - self.assert_unpack("F", "F;16BS", b'\x83\x01\x01\x83', -31999, 0x0183) + self.assert_unpack("F", "F;16BS", b"\x83\x01\x01\x83", -31999, 0x0183) self.assert_unpack("F", "F;32", 4, 67305984, 134678016) - self.assert_unpack("F", "F;32S", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - 16777348, -2097152000) + self.assert_unpack( + "F", "F;32S", b"\x83\x00\x00\x01\x01\x00\x00\x83", 16777348, -2097152000 + ) self.assert_unpack("F", "F;32B", 4, 0x01020304, 0x05060708) - self.assert_unpack("F", "F;32BS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - -2097152000, 16777348) + self.assert_unpack( + "F", "F;32BS", b"\x83\x00\x00\x01\x01\x00\x00\x83", -2097152000, 16777348 + ) - if sys.byteorder == 'little': + if sys.byteorder == "little": self.assert_unpack("F", "F;16N", 2, 0x0201, 0x0403) - self.assert_unpack("F", "F;16NS", b'\x83\x01\x01\x83', 0x0183, -31999) + self.assert_unpack("F", "F;16NS", b"\x83\x01\x01\x83", 0x0183, -31999) self.assert_unpack("F", "F;32N", 4, 67305984, 134678016) - self.assert_unpack("F", "F;32NS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - 16777348, -2097152000) + self.assert_unpack( + "F", + "F;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + 16777348, + -2097152000, + ) else: self.assert_unpack("F", "F;16N", 2, 0x0102, 0x0304) - self.assert_unpack("F", "F;16NS", b'\x83\x01\x01\x83', -31999, 0x0183) + self.assert_unpack("F", "F;16NS", b"\x83\x01\x01\x83", -31999, 0x0183) self.assert_unpack("F", "F;32N", 4, 0x01020304, 0x05060708) - self.assert_unpack("F", "F;32NS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - -2097152000, 16777348) + self.assert_unpack( + "F", + "F;32NS", + b"\x83\x00\x00\x01\x01\x00\x00\x83", + -2097152000, + 16777348, + ) def test_F_float(self): - self.assert_unpack("F", "F;32F", 4, - 1.539989614439558e-36, 4.063216068939723e-34) - self.assert_unpack("F", "F;32BF", 4, - 2.387939260590663e-38, 6.301941157072183e-36) - self.assert_unpack("F", "F;64F", - b'333333\xc3?\x00\x00\x00\x00\x00J\x93\xc0', # by struct.pack - 0.15000000596046448, -1234.5) - self.assert_unpack("F", "F;64BF", - b'?\xc3333333\xc0\x93J\x00\x00\x00\x00\x00', # by struct.pack - 0.15000000596046448, -1234.5) - - if sys.byteorder == 'little': - self.assert_unpack("F", "F", 4, - 1.539989614439558e-36, 4.063216068939723e-34) - self.assert_unpack("F", "F;32NF", 4, - 1.539989614439558e-36, 4.063216068939723e-34) - self.assert_unpack("F", "F;64NF", - b'333333\xc3?\x00\x00\x00\x00\x00J\x93\xc0', - 0.15000000596046448, -1234.5) + self.assert_unpack( + "F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34 + ) + self.assert_unpack( + "F", "F;32BF", 4, 2.387939260590663e-38, 6.301941157072183e-36 + ) + self.assert_unpack( + "F", + "F;64F", + b"333333\xc3?\x00\x00\x00\x00\x00J\x93\xc0", # by struct.pack + 0.15000000596046448, + -1234.5, + ) + self.assert_unpack( + "F", + "F;64BF", + b"?\xc3333333\xc0\x93J\x00\x00\x00\x00\x00", # by struct.pack + 0.15000000596046448, + -1234.5, + ) + + if sys.byteorder == "little": + self.assert_unpack( + "F", "F", 4, 1.539989614439558e-36, 4.063216068939723e-34 + ) + self.assert_unpack( + "F", "F;32NF", 4, 1.539989614439558e-36, 4.063216068939723e-34 + ) + self.assert_unpack( + "F", + "F;64NF", + b"333333\xc3?\x00\x00\x00\x00\x00J\x93\xc0", + 0.15000000596046448, + -1234.5, + ) else: - self.assert_unpack("F", "F", 4, - 2.387939260590663e-38, 6.301941157072183e-36) - self.assert_unpack("F", "F;32NF", 4, - 2.387939260590663e-38, 6.301941157072183e-36) - self.assert_unpack("F", "F;64NF", - b'?\xc3333333\xc0\x93J\x00\x00\x00\x00\x00', - 0.15000000596046448, -1234.5) + self.assert_unpack( + "F", "F", 4, 2.387939260590663e-38, 6.301941157072183e-36 + ) + self.assert_unpack( + "F", "F;32NF", 4, 2.387939260590663e-38, 6.301941157072183e-36 + ) + self.assert_unpack( + "F", + "F;64NF", + b"?\xc3333333\xc0\x93J\x00\x00\x00\x00\x00", + 0.15000000596046448, + -1234.5, + ) def test_I16(self): self.assert_unpack("I;16", "I;16", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16B", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16L", "I;16L", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16", "I;12", 2, 0x0010, 0x0203, 0x0040) - if sys.byteorder == 'little': + if sys.byteorder == "little": self.assert_unpack("I;16", "I;16N", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16B", "I;16N", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16L", "I;16N", 2, 0x0201, 0x0403, 0x0605) @@ -499,11 +766,18 @@ def test_I16(self): self.assert_unpack("I;16B", "I;16N", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16L", "I;16N", 2, 0x0102, 0x0304, 0x0506) - def test_value_error(self): - self.assertRaises(ValueError, self.assert_unpack, "L", "L", 0, 0) - self.assertRaises(ValueError, self.assert_unpack, "RGB", "RGB", 2, 0) - self.assertRaises(ValueError, self.assert_unpack, "CMYK", "CMYK", 2, 0) - + def test_CMYK16(self): + self.assert_unpack("CMYK", "CMYK;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) + self.assert_unpack("CMYK", "CMYK;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) + if sys.byteorder == "little": + self.assert_unpack("CMYK", "CMYK;16N", 8, (2, 4, 6, 8), (10, 12, 14, 16)) + else: + self.assert_unpack("CMYK", "CMYK;16N", 8, (1, 3, 5, 7), (9, 11, 13, 15)) -if __name__ == '__main__': - unittest.main() + def test_value_error(self): + with pytest.raises(ValueError): + self.assert_unpack("L", "L", 0, 0) + with pytest.raises(ValueError): + self.assert_unpack("RGB", "RGB", 2, 0) + with pytest.raises(ValueError): + self.assert_unpack("CMYK", "CMYK", 2, 0) diff --git a/Tests/test_locale.py b/Tests/test_locale.py index 14275379107..7a07fbbe0a4 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -1,9 +1,8 @@ -from __future__ import print_function -from helper import unittest, PillowTestCase +import locale -from PIL import Image +import pytest -import locale +from PIL import Image # ref https://github.com/python-pillow/Pillow/issues/272 # on windows, in polish locale: @@ -23,16 +22,16 @@ path = "Tests/images/hopper.jpg" -class TestLocale(PillowTestCase): - - def test_sanity(self): - Image.open(path) - try: - locale.setlocale(locale.LC_ALL, "polish") - except: - unittest.skip('Polish locale not available') - Image.open(path) - +def test_sanity(): + with Image.open(path): + pass + try: + locale.setlocale(locale.LC_ALL, "polish") + except locale.Error: + pytest.skip("Polish locale not available") -if __name__ == '__main__': - unittest.main() + try: + with Image.open(path): + pass + finally: + locale.setlocale(locale.LC_ALL, (None, None)) diff --git a/Tests/test_main.py b/Tests/test_main.py new file mode 100644 index 00000000000..46ff63c4e97 --- /dev/null +++ b/Tests/test_main.py @@ -0,0 +1,32 @@ +import os +import subprocess +import sys + + +def test_main(): + out = subprocess.check_output([sys.executable, "-m", "PIL"]).decode("utf-8") + lines = out.splitlines() + assert lines[0] == "-" * 68 + assert lines[1].startswith("Pillow ") + assert lines[2].startswith("Python ") + lines = lines[3:] + while lines[0].startswith(" "): + lines = lines[1:] + assert lines[0] == "-" * 68 + assert lines[1].startswith("Python modules loaded from ") + assert lines[2].startswith("Binary modules loaded from ") + assert lines[3] == "-" * 68 + jpeg = ( + os.linesep + + "-" * 68 + + os.linesep + + "JPEG image/jpeg" + + os.linesep + + "Extensions: .jfif, .jpe, .jpeg, .jpg" + + os.linesep + + "Features: open, save" + + os.linesep + + "-" * 68 + + os.linesep + ) + assert jpeg in out diff --git a/Tests/test_map.py b/Tests/test_map.py index 14bd835a209..d816bddaf3d 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -1,28 +1,45 @@ -from helper import PillowTestCase, unittest import sys +import pytest + from PIL import Image -@unittest.skipIf(sys.platform.startswith('win32'), "Win32 does not call map_buffer") -class TestMap(PillowTestCase): - def test_overflow(self): - # There is the potential to overflow comparisons in map.c - # if there are > SIZE_MAX bytes in the image or if - # the file encodes an offset that makes - # (offset + size(bytes)) > SIZE_MAX +def test_overflow(): + # There is the potential to overflow comparisons in map.c + # if there are > SIZE_MAX bytes in the image or if + # the file encodes an offset that makes + # (offset + size(bytes)) > SIZE_MAX - # Note that this image triggers the decompression bomb warning: - max_pixels = Image.MAX_IMAGE_PIXELS - Image.MAX_IMAGE_PIXELS = None + # Note that this image triggers the decompression bomb warning: + max_pixels = Image.MAX_IMAGE_PIXELS + Image.MAX_IMAGE_PIXELS = None - # This image hits the offset test. - im = Image.open('Tests/images/l2rgb_read.bmp') - with self.assertRaises((ValueError, MemoryError, IOError)): + # This image hits the offset test. + with Image.open("Tests/images/l2rgb_read.bmp") as im: + with pytest.raises((ValueError, MemoryError, OSError)): im.load() - Image.MAX_IMAGE_PIXELS = max_pixels + Image.MAX_IMAGE_PIXELS = max_pixels + + +def test_tobytes(): + # Note that this image triggers the decompression bomb warning: + max_pixels = Image.MAX_IMAGE_PIXELS + Image.MAX_IMAGE_PIXELS = None + + # Previously raised an access violation on Windows + with Image.open("Tests/images/l2rgb_read.bmp") as im: + with pytest.raises((ValueError, MemoryError, OSError)): + im.tobytes() + + Image.MAX_IMAGE_PIXELS = max_pixels + +@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") +def test_ysize(): + numpy = pytest.importorskip("numpy", reason="NumPy not installed") -if __name__ == '__main__': - unittest.main() + # Should not raise 'Integer overflow in ysize' + arr = numpy.zeros((46341, 46341), dtype=numpy.uint8) + Image.fromarray(arr) diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index d518471996b..efcdab9ec43 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,111 +1,99 @@ -from helper import unittest, PillowTestCase, hopper +import pytest from PIL import Image +from .helper import hopper -class TestModeI16(PillowTestCase): +original = hopper().resize((32, 32)).convert("I") - original = hopper().resize((32, 32)).convert('I') - def verify(self, im1): - im2 = self.original.copy() - self.assertEqual(im1.size, im2.size) - pix1 = im1.load() - pix2 = im2.load() - for y in range(im1.size[1]): - for x in range(im1.size[0]): - xy = x, y - p1 = pix1[xy] - p2 = pix2[xy] - self.assertEqual( - p1, p2, - ("got %r from mode %s at %s, expected %r" % - (p1, im1.mode, xy, p2))) +def verify(im1): + im2 = original.copy() + assert im1.size == im2.size + pix1 = im1.load() + pix2 = im2.load() + for y in range(im1.size[1]): + for x in range(im1.size[0]): + xy = x, y + p1 = pix1[xy] + p2 = pix2[xy] + assert ( + p1 == p2 + ), f"got {repr(p1)} from mode {im1.mode} at {xy}, expected {repr(p2)}" - def test_basic(self): - # PIL 1.1 has limited support for 16-bit image data. Check that - # create/copy/transform and save works as expected. - def basic(mode): +@pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I")) +def test_basic(tmp_path, mode): + # PIL 1.1 has limited support for 16-bit image data. Check that + # create/copy/transform and save works as expected. - imIn = self.original.convert(mode) - self.verify(imIn) + im_in = original.convert(mode) + verify(im_in) - w, h = imIn.size + w, h = im_in.size - imOut = imIn.copy() - self.verify(imOut) # copy + im_out = im_in.copy() + verify(im_out) # copy - imOut = imIn.transform((w, h), Image.EXTENT, (0, 0, w, h)) - self.verify(imOut) # transform + im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h)) + verify(im_out) # transform - filename = self.tempfile("temp.im") - imIn.save(filename) + filename = str(tmp_path / "temp.im") + im_in.save(filename) - imOut = Image.open(filename) + with Image.open(filename) as im_out: - self.verify(imIn) - self.verify(imOut) + verify(im_in) + verify(im_out) - imOut = imIn.crop((0, 0, w, h)) - self.verify(imOut) + im_out = im_in.crop((0, 0, w, h)) + verify(im_out) - imOut = Image.new(mode, (w, h), None) - imOut.paste(imIn.crop((0, 0, w//2, h)), (0, 0)) - imOut.paste(imIn.crop((w//2, 0, w, h)), (w//2, 0)) + im_out = Image.new(mode, (w, h), None) + im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0)) + im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0)) - self.verify(imIn) - self.verify(imOut) + verify(im_in) + verify(im_out) - imIn = Image.new(mode, (1, 1), 1) - self.assertEqual(imIn.getpixel((0, 0)), 1) + im_in = Image.new(mode, (1, 1), 1) + assert im_in.getpixel((0, 0)) == 1 - imIn.putpixel((0, 0), 2) - self.assertEqual(imIn.getpixel((0, 0)), 2) + im_in.putpixel((0, 0), 2) + assert im_in.getpixel((0, 0)) == 2 - if mode == "L": - maximum = 255 - else: - maximum = 32767 + if mode == "L": + maximum = 255 + else: + maximum = 32767 - imIn = Image.new(mode, (1, 1), 256) - self.assertEqual(imIn.getpixel((0, 0)), min(256, maximum)) + im_in = Image.new(mode, (1, 1), 256) + assert im_in.getpixel((0, 0)) == min(256, maximum) - imIn.putpixel((0, 0), 512) - self.assertEqual(imIn.getpixel((0, 0)), min(512, maximum)) + im_in.putpixel((0, 0), 512) + assert im_in.getpixel((0, 0)) == min(512, maximum) - basic("L") - basic("I;16") - basic("I;16B") - basic("I;16L") +def test_tobytes(): + def tobytes(mode): + return Image.new(mode, (1, 1), 1).tobytes() - basic("I") + order = 1 if Image._ENDIAN == "<" else -1 - def test_tobytes(self): + assert tobytes("L") == b"\x01" + assert tobytes("I;16") == b"\x01\x00" + assert tobytes("I;16B") == b"\x00\x01" + assert tobytes("I") == b"\x01\x00\x00\x00"[::order] - def tobytes(mode): - return Image.new(mode, (1, 1), 1).tobytes() - order = 1 if Image._ENDIAN == '<' else -1 +def test_convert(): - self.assertEqual(tobytes("L"), b"\x01") - self.assertEqual(tobytes("I;16"), b"\x01\x00") - self.assertEqual(tobytes("I;16B"), b"\x00\x01") - self.assertEqual(tobytes("I"), b"\x01\x00\x00\x00"[::order]) + im = original.copy() - def test_convert(self): + verify(im.convert("I;16")) + verify(im.convert("I;16").convert("L")) + verify(im.convert("I;16").convert("I")) - im = self.original.copy() - - self.verify(im.convert("I;16")) - self.verify(im.convert("I;16").convert("L")) - self.verify(im.convert("I;16").convert("I")) - - self.verify(im.convert("I;16B")) - self.verify(im.convert("I;16B").convert("L")) - self.verify(im.convert("I;16B").convert("I")) - - -if __name__ == '__main__': - unittest.main() + verify(im.convert("I;16B")) + verify(im.convert("I;16B").convert("L")) + verify(im.convert("I;16B").convert("I")) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 7eeee3a83f0..185e477ecc5 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,235 +1,241 @@ -from __future__ import print_function -import sys -from helper import unittest, PillowTestCase, hopper +import warnings + +import pytest from PIL import Image -try: - import site - import numpy - assert site # silence warning - assert numpy # silence warning -except ImportError: - # Skip via setUp() - pass +from .helper import assert_deep_equal, assert_image, hopper + +numpy = pytest.importorskip("numpy", reason="NumPy not installed") TEST_IMAGE_SIZE = (10, 10) -# Numpy on pypy as of pypy 5.3.1 is corrupting the numpy.array(Image) -# call such that it's returning a object of type numpy.ndarray, but -# the repr is that of a PIL.Image. Size and shape are 1 and (), not the -# size and shape of the array. This causes failures in several tests. -SKIP_NUMPY_ON_PYPY = hasattr(sys, 'pypy_version_info') and ( - sys.pypy_version_info <= (5, 3, 1, 'final', 0)) - - -class TestNumpy(PillowTestCase): - - def setUp(self): - try: - import site - import numpy - assert site # silence warning - assert numpy # silence warning - except ImportError: - self.skipTest("ImportError") - - def test_numpy_to_image(self): - - def to_image(dtype, bands=1, boolean=0): - if bands == 1: - if boolean: - data = [0, 255] * 50 - else: - data = list(range(100)) - a = numpy.array(data, dtype=dtype) - a.shape = TEST_IMAGE_SIZE - i = Image.fromarray(a) - if list(i.getdata()) != data: - print("data mismatch for", dtype) + +def test_numpy_to_image(): + def to_image(dtype, bands=1, boolean=0): + if bands == 1: + if boolean: + data = [0, 255] * 50 else: data = list(range(100)) - a = numpy.array([[x]*bands for x in data], dtype=dtype) - a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands - i = Image.fromarray(a) - if list(i.getchannel(0).getdata()) != list(range(100)): - print("data mismatch for", dtype) - # print(dtype, list(i.getdata())) - return i - - # Check supported 1-bit integer formats - self.assert_image(to_image(numpy.bool, 1, 1), '1', TEST_IMAGE_SIZE) - self.assert_image(to_image(numpy.bool8, 1, 1), '1', TEST_IMAGE_SIZE) - - # Check supported 8-bit integer formats - self.assert_image(to_image(numpy.uint8), "L", TEST_IMAGE_SIZE) - self.assert_image(to_image(numpy.uint8, 3), "RGB", TEST_IMAGE_SIZE) - self.assert_image(to_image(numpy.uint8, 4), "RGBA", TEST_IMAGE_SIZE) - self.assert_image(to_image(numpy.int8), "I", TEST_IMAGE_SIZE) - - # Check non-fixed-size integer types - # These may fail, depending on the platform, since we have no native - # 64 bit int image types. - # self.assert_image(to_image(numpy.uint), "I", TEST_IMAGE_SIZE) - # self.assert_image(to_image(numpy.int), "I", TEST_IMAGE_SIZE) - - # Check 16-bit integer formats - if Image._ENDIAN == '<': - self.assert_image(to_image(numpy.uint16), "I;16", TEST_IMAGE_SIZE) + a = numpy.array(data, dtype=dtype) + a.shape = TEST_IMAGE_SIZE + i = Image.fromarray(a) + if list(i.getdata()) != data: + print("data mismatch for", dtype) else: - self.assert_image(to_image(numpy.uint16), "I;16B", TEST_IMAGE_SIZE) - - self.assert_image(to_image(numpy.int16), "I", TEST_IMAGE_SIZE) - - # Check 32-bit integer formats - self.assert_image(to_image(numpy.uint32), "I", TEST_IMAGE_SIZE) - self.assert_image(to_image(numpy.int32), "I", TEST_IMAGE_SIZE) - - # Check 64-bit integer formats - self.assertRaises(TypeError, to_image, numpy.uint64) - self.assertRaises(TypeError, to_image, numpy.int64) - - # Check floating-point formats - self.assert_image(to_image(numpy.float), "F", TEST_IMAGE_SIZE) - self.assertRaises(TypeError, to_image, numpy.float16) - self.assert_image(to_image(numpy.float32), "F", TEST_IMAGE_SIZE) - self.assert_image(to_image(numpy.float64), "F", TEST_IMAGE_SIZE) - - self.assert_image(to_image(numpy.uint8, 2), "LA", (10, 10)) - self.assert_image(to_image(numpy.uint8, 3), "RGB", (10, 10)) - self.assert_image(to_image(numpy.uint8, 4), "RGBA", (10, 10)) - - # based on an erring example at - # https://stackoverflow.com/questions/10854903/what-is-causing-dimension-dependent-attributeerror-in-pil-fromarray-function - def test_3d_array(self): - size = (5, TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1]) - a = numpy.ones(size, dtype=numpy.uint8) - self.assert_image(Image.fromarray(a[1, :, :]), "L", TEST_IMAGE_SIZE) - size = (TEST_IMAGE_SIZE[0], 5, TEST_IMAGE_SIZE[1]) - a = numpy.ones(size, dtype=numpy.uint8) - self.assert_image(Image.fromarray(a[:, 1, :]), "L", TEST_IMAGE_SIZE) - size = (TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], 5) - a = numpy.ones(size, dtype=numpy.uint8) - self.assert_image(Image.fromarray(a[:, :, 1]), "L", TEST_IMAGE_SIZE) - - def _test_img_equals_nparray(self, img, np): - self.assertGreaterEqual(len(np.shape), 2) - np_size = np.shape[1], np.shape[0] - self.assertEqual(img.size, np_size) - px = img.load() - for x in range(0, img.size[0], int(img.size[0]/10)): - for y in range(0, img.size[1], int(img.size[1]/10)): - self.assert_deep_equal(px[x, y], np[y, x]) - - @unittest.skipIf(SKIP_NUMPY_ON_PYPY, "numpy.array(Image) is flaky on PyPy") - def test_16bit(self): - img = Image.open('Tests/images/16bit.cropped.tif') + data = list(range(100)) + a = numpy.array([[x] * bands for x in data], dtype=dtype) + a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands + i = Image.fromarray(a) + if list(i.getchannel(0).getdata()) != list(range(100)): + print("data mismatch for", dtype) + return i + + # 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) + + # Check supported 8-bit integer formats + assert_image(to_image(numpy.uint8), "L", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.uint8, 3), "RGB", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.uint8, 4), "RGBA", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.int8), "I", TEST_IMAGE_SIZE) + + # Check non-fixed-size integer types + # These may fail, depending on the platform, since we have no native + # 64-bit int image types. + # assert_image(to_image(numpy.uint), "I", TEST_IMAGE_SIZE) + # assert_image(to_image(numpy.int), "I", TEST_IMAGE_SIZE) + + # Check 16-bit integer formats + if Image._ENDIAN == "<": + assert_image(to_image(numpy.uint16), "I;16", TEST_IMAGE_SIZE) + else: + assert_image(to_image(numpy.uint16), "I;16B", TEST_IMAGE_SIZE) + + assert_image(to_image(numpy.int16), "I", TEST_IMAGE_SIZE) + + # Check 32-bit integer formats + assert_image(to_image(numpy.uint32), "I", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.int32), "I", TEST_IMAGE_SIZE) + + # Check 64-bit integer formats + with pytest.raises(TypeError): + to_image(numpy.uint64) + with pytest.raises(TypeError): + to_image(numpy.int64) + + # Check floating-point formats + assert_image(to_image(float), "F", TEST_IMAGE_SIZE) + with pytest.raises(TypeError): + to_image(numpy.float16) + assert_image(to_image(numpy.float32), "F", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.float64), "F", TEST_IMAGE_SIZE) + + assert_image(to_image(numpy.uint8, 2), "LA", (10, 10)) + assert_image(to_image(numpy.uint8, 3), "RGB", (10, 10)) + assert_image(to_image(numpy.uint8, 4), "RGBA", (10, 10)) + + +# Based on an erring example at +# https://stackoverflow.com/questions/10854903/what-is-causing-dimension-dependent-attributeerror-in-pil-fromarray-function +def test_3d_array(): + size = (5, TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1]) + a = numpy.ones(size, dtype=numpy.uint8) + assert_image(Image.fromarray(a[1, :, :]), "L", TEST_IMAGE_SIZE) + size = (TEST_IMAGE_SIZE[0], 5, TEST_IMAGE_SIZE[1]) + a = numpy.ones(size, dtype=numpy.uint8) + assert_image(Image.fromarray(a[:, 1, :]), "L", TEST_IMAGE_SIZE) + size = (TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], 5) + a = numpy.ones(size, dtype=numpy.uint8) + assert_image(Image.fromarray(a[:, :, 1]), "L", TEST_IMAGE_SIZE) + + +def test_1d_array(): + a = numpy.ones(5, dtype=numpy.uint8) + assert_image(Image.fromarray(a), "L", (1, 5)) + + +def _test_img_equals_nparray(img, np): + assert len(np.shape) >= 2 + np_size = np.shape[1], np.shape[0] + assert img.size == np_size + px = img.load() + for x in range(0, img.size[0], int(img.size[0] / 10)): + for y in range(0, img.size[1], int(img.size[1] / 10)): + assert_deep_equal(px[x, y], np[y, x]) + + +def test_16bit(): + with Image.open("Tests/images/16bit.cropped.tif") as img: np_img = numpy.array(img) - self._test_img_equals_nparray(img, np_img) - self.assertEqual(np_img.dtype, numpy.dtype('u2"), + ("I;16L", "u2'), - ("I;16L", '", 0) == (b"\x90\x1F\xA3", 8) + assert PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3) == (b"\x90\x1F\xA0", 17) + assert PdfParser.get_value(b"(asd)", 0) == (b"asd", 5) + assert PdfParser.get_value(b"(asd(qwe)zxc)zzz(aaa)", 0) == (b"asd(qwe)zxc", 13) + assert PdfParser.get_value(b"(Two \\\nwords.)", 0) == (b"Two words.", 14) + assert PdfParser.get_value(b"(Two\nlines.)", 0) == (b"Two\nlines.", 12) + assert PdfParser.get_value(b"(Two\r\nlines.)", 0) == (b"Two\nlines.", 13) + assert PdfParser.get_value(b"(Two\\nlines.)", 0) == (b"Two\nlines.", 13) + assert PdfParser.get_value(b"(One\\(paren).", 0) == (b"One(paren", 12) + assert PdfParser.get_value(b"(One\\)paren).", 0) == (b"One)paren", 12) + assert PdfParser.get_value(b"(\\0053)", 0) == (b"\x053", 7) + assert PdfParser.get_value(b"(\\053)", 0) == (b"\x2B", 6) + assert PdfParser.get_value(b"(\\53)", 0) == (b"\x2B", 5) + assert PdfParser.get_value(b"(\\53a)", 0) == (b"\x2Ba", 6) + assert PdfParser.get_value(b"(\\1111)", 0) == (b"\x491", 7) + assert PdfParser.get_value(b" 123 (", 0) == (123, 4) + assert round(abs(PdfParser.get_value(b" 123.4 %", 0)[0] - 123.4), 7) == 0 + assert PdfParser.get_value(b" 123.4 %", 0)[1] == 6 + with pytest.raises(PdfFormatError): + PdfParser.get_value(b"]", 0) + d = PdfParser.get_value(b"<>", 0)[0] + assert isinstance(d, PdfDict) + assert len(d) == 2 + assert d.Name == "value" + assert d[b"Name"] == b"value" + assert d.N == PdfName("V") + a = PdfParser.get_value(b"[/Name (value) /N /V]", 0)[0] + assert isinstance(a, list) + assert len(a) == 4 + assert a[0] == PdfName("Name") + s = PdfParser.get_value( + b"<>\nstream\nabcde\nendstream<<...", 0 + )[0] + assert isinstance(s, PdfStream) + assert s.dictionary.Name == "value" + assert s.decode() == b"abcde" + for name in ["CreationDate", "ModDate"]: + for date, value in { + b"20180729214124": "20180729214124", + b"D:20180729214124": "20180729214124", + b"D:2018072921": "20180729210000", + b"D:20180729214124Z": "20180729214124", + b"D:20180729214124+08'00'": "20180729134124", + b"D:20180729214124-05'00'": "20180730024124", + }.items(): + d = PdfParser.get_value(b"<>", 0)[ + 0 + ] + assert time.strftime("%Y%m%d%H%M%S", getattr(d, name)) == value + + +def test_pdf_repr(): + assert bytes(IndirectReference(1, 2)) == b"1 2 R" + assert bytes(IndirectObjectDef(*IndirectReference(1, 2))) == b"1 2 obj" + assert bytes(PdfName(b"Name#Hash")) == b"/Name#23Hash" + assert bytes(PdfName("Name#Hash")) == b"/Name#23Hash" + assert bytes(PdfDict({b"Name": IndirectReference(1, 2)})) == b"<<\n/Name 1 2 R\n>>" + assert bytes(PdfDict({"Name": IndirectReference(1, 2)})) == b"<<\n/Name 1 2 R\n>>" + assert pdf_repr(IndirectReference(1, 2)) == b"1 2 R" + assert pdf_repr(IndirectObjectDef(*IndirectReference(1, 2))) == b"1 2 obj" + assert pdf_repr(PdfName(b"Name#Hash")) == b"/Name#23Hash" + assert pdf_repr(PdfName("Name#Hash")) == b"/Name#23Hash" + assert ( + pdf_repr(PdfDict({b"Name": IndirectReference(1, 2)})) == b"<<\n/Name 1 2 R\n>>" + ) + assert ( + pdf_repr(PdfDict({"Name": IndirectReference(1, 2)})) == b"<<\n/Name 1 2 R\n>>" + ) + assert pdf_repr(123) == b"123" + assert pdf_repr(True) == b"true" + assert pdf_repr(False) == b"false" + assert pdf_repr(None) == b"null" + assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)" + assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]" + assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 69eb60949dd..23eb9e39f41 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -1,29 +1,34 @@ -from helper import unittest, PillowTestCase +import pickle -from PIL import Image +import pytest +from PIL import Image, ImageDraw, ImageFont -class TestPickle(PillowTestCase): +from .helper import assert_image_equal, skip_unless_feature - def helper_pickle_file(self, pickle, protocol=0, mode=None): - # Arrange - im = Image.open('Tests/images/hopper.jpg') - filename = self.tempfile('temp.pkl') +FONT_SIZE = 20 +FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" + + +def helper_pickle_file(tmp_path, pickle, protocol, test_file, mode): + # Arrange + with Image.open(test_file) as im: + filename = str(tmp_path / "temp.pkl") if mode: im = im.convert(mode) # Act - with open(filename, 'wb') as f: + with open(filename, "wb") as f: pickle.dump(im, f, protocol) - with open(filename, 'rb') as f: + with open(filename, "rb") as f: loaded_im = pickle.load(f) # Assert - self.assertEqual(im, loaded_im) + assert im == loaded_im + - def helper_pickle_string(self, pickle, protocol=0, - test_file='Tests/images/hopper.jpg', mode=None): - im = Image.open(test_file) +def helper_pickle_string(pickle, protocol, test_file, mode): + with Image.open(test_file) as im: if mode: im = im.convert(mode) @@ -32,65 +37,106 @@ def helper_pickle_string(self, pickle, protocol=0, loaded_im = pickle.loads(dumped_string) # Assert - self.assertEqual(im, loaded_im) - - def test_pickle_image(self): - # Arrange - import pickle - - # Act / Assert - for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): - self.helper_pickle_string(pickle, protocol) - self.helper_pickle_file(pickle, protocol) - - def test_cpickle_image(self): - # Arrange - try: - import cPickle - except ImportError: - return - - # Act / Assert - for protocol in range(0, cPickle.HIGHEST_PROTOCOL + 1): - self.helper_pickle_string(cPickle, protocol) - self.helper_pickle_file(cPickle, protocol) - - def test_pickle_p_mode(self): - # Arrange - import pickle - - # Act / Assert - for test_file in [ - "Tests/images/test-card.png", - "Tests/images/zero_bb.png", - "Tests/images/zero_bb_scale2.png", - "Tests/images/non_zero_bb.png", - "Tests/images/non_zero_bb_scale2.png", - "Tests/images/p_trns_single.png", - "Tests/images/pil123p.png" - ]: - self.helper_pickle_string(pickle, test_file=test_file) - - def test_pickle_l_mode(self): - # Arrange - import pickle - - # Act / Assert - for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): - self.helper_pickle_string(pickle, protocol, mode="L") - self.helper_pickle_file(pickle, protocol, mode="L") - - def test_cpickle_l_mode(self): - # Arrange - try: - import cPickle - except ImportError: - return - - # Act / Assert - for protocol in range(0, cPickle.HIGHEST_PROTOCOL + 1): - self.helper_pickle_string(cPickle, protocol, mode="L") - self.helper_pickle_file(cPickle, protocol, mode="L") - -if __name__ == '__main__': - unittest.main() + assert im == loaded_im + + +@pytest.mark.parametrize( + ("test_file", "test_mode"), + [ + ("Tests/images/hopper.jpg", None), + ("Tests/images/hopper.jpg", "L"), + ("Tests/images/hopper.jpg", "PA"), + pytest.param( + "Tests/images/hopper.webp", None, marks=skip_unless_feature("webp") + ), + ("Tests/images/hopper.tif", None), + ("Tests/images/test-card.png", None), + ("Tests/images/zero_bb.png", None), + ("Tests/images/zero_bb_scale2.png", None), + ("Tests/images/non_zero_bb.png", None), + ("Tests/images/non_zero_bb_scale2.png", None), + ("Tests/images/p_trns_single.png", None), + ("Tests/images/pil123p.png", None), + ("Tests/images/itxt_chunks.png", None), + ], +) +@pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1)) +def test_pickle_image(tmp_path, test_file, test_mode, protocol): + # Act / Assert + helper_pickle_string(pickle, protocol, test_file, test_mode) + helper_pickle_file(tmp_path, pickle, protocol, test_file, test_mode) + + +def test_pickle_la_mode_with_palette(tmp_path): + # Arrange + filename = str(tmp_path / "temp.pkl") + with Image.open("Tests/images/hopper.jpg") as im: + im = im.convert("PA") + + # Act / Assert + for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + im.mode = "LA" + with open(filename, "wb") as f: + pickle.dump(im, f, protocol) + with open(filename, "rb") as f: + loaded_im = pickle.load(f) + + im.mode = "PA" + assert im == loaded_im + + +@skip_unless_feature("webp") +def test_pickle_tell(): + # Arrange + with Image.open("Tests/images/hopper.webp") as image: + + # Act: roundtrip + unpickled_image = pickle.loads(pickle.dumps(image)) + + # Assert + assert unpickled_image.tell() == 0 + + +def helper_assert_pickled_font_images(font1, font2): + # Arrange + im1 = Image.new(mode="RGBA", size=(300, 100)) + im2 = Image.new(mode="RGBA", size=(300, 100)) + draw1 = ImageDraw.Draw(im1) + draw2 = ImageDraw.Draw(im2) + txt = "Hello World!" + + # Act + draw1.text((10, 10), txt, font=font1) + draw2.text((10, 10), txt, font=font2) + + # Assert + assert_image_equal(im1, im2) + + +@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) +def test_pickle_font_string(protocol): + # Arrange + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + # Act: roundtrip + pickled_font = pickle.dumps(font, protocol) + unpickled_font = pickle.loads(pickled_font) + + # Assert + helper_assert_pickled_font_images(font, unpickled_font) + + +@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) +def test_pickle_font_file(tmp_path, protocol): + # Arrange + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + filename = str(tmp_path / "temp.pkl") + + # Act: roundtrip + with open(filename, "wb") as f: + pickle.dump(font, f, protocol) + with open(filename, "rb") as f: + unpickled_font = pickle.load(f) + + # Assert + helper_assert_pickled_font_images(font, unpickled_font) diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 17fa3662b97..e74d798282a 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -1,67 +1,73 @@ -from helper import unittest, PillowTestCase - -from PIL import Image, PSDraw import os import sys +from io import BytesIO +import pytest + +from PIL import Image, PSDraw -class TestPsDraw(PillowTestCase): - def _create_document(self, ps): - im = Image.open("Tests/images/hopper.ppm") - title = "hopper" - box = (1*72, 2*72, 7*72, 10*72) # in points +def _create_document(ps): + title = "hopper" + box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points - ps.begin_document(title) + ps.begin_document(title) - # draw diagonal lines in a cross - ps.line((1*72, 2*72), (7*72, 10*72)) - ps.line((7*72, 2*72), (1*72, 10*72)) + # draw diagonal lines in a cross + ps.line((1 * 72, 2 * 72), (7 * 72, 10 * 72)) + ps.line((7 * 72, 2 * 72), (1 * 72, 10 * 72)) - # draw the image (75 dpi) + # draw the image (75 dpi) + with Image.open("Tests/images/hopper.ppm") as im: ps.image(box, im, 75) - ps.rectangle(box) + ps.rectangle(box) + + # draw title + ps.setfont("Courier", 36) + ps.text((3 * 72, 4 * 72), title) + + ps.end_document() + - # draw title - ps.setfont("Courier", 36) - ps.text((3*72, 4*72), title) +def test_draw_postscript(tmp_path): + # Based on Pillow tutorial, but there is no textsize: + # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript - ps.end_document() + # Arrange + tempfile = str(tmp_path / "temp.ps") + with open(tempfile, "wb") as fp: + # Act + ps = PSDraw.PSDraw(fp) + _create_document(ps) - def test_draw_postscript(self): + # Assert + # Check non-zero file was created + assert os.path.isfile(tempfile) + assert os.path.getsize(tempfile) > 0 - # Based on Pillow tutorial, but there is no textsize: - # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript - # Arrange - tempfile = self.tempfile('temp.ps') - with open(tempfile, "wb") as fp: - # Act - ps = PSDraw.PSDraw(fp) - self._create_document(ps) +@pytest.mark.parametrize("buffer", (True, False)) +def test_stdout(buffer): + # Temporarily redirect stdout + old_stdout = sys.stdout - # Assert - # Check non-zero file was created - self.assertTrue(os.path.isfile(tempfile)) - self.assertGreater(os.path.getsize(tempfile), 0) + if buffer: - def test_stdout(self): - # Temporarily redirect stdout - try: - from cStringIO import StringIO - except ImportError: - from io import StringIO - old_stdout = sys.stdout - sys.stdout = mystdout = StringIO() + class MyStdOut: + buffer = BytesIO() - ps = PSDraw.PSDraw() - self._create_document(ps) + mystdout = MyStdOut() + else: + mystdout = BytesIO() - # Reset stdout - sys.stdout = old_stdout + sys.stdout = mystdout - self.assertNotEqual(mystdout.getvalue(), "") + ps = PSDraw.PSDraw() + _create_document(ps) + # Reset stdout + sys.stdout = old_stdout -if __name__ == '__main__': - unittest.main() + if buffer: + mystdout = mystdout.buffer + assert mystdout.getvalue() != b"" diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index 962535f03fa..aa05c2cfdd8 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,41 +1,25 @@ -from helper import unittest, PillowTestCase +import pytest -from PIL import PILLOW_VERSION +from PIL import __version__ -try: - import pyroma -except ImportError: - # Skip via setUp() - pass +pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") -class TestPyroma(PillowTestCase): +def test_pyroma(): + # Arrange + data = pyroma.projectdata.get_data(".") - def setUp(self): - try: - import pyroma - assert pyroma # Ignore warning - except ImportError: - self.skipTest("ImportError") + # Act + rating = pyroma.ratings.rate(data) - def test_pyroma(self): - # Arrange - data = pyroma.projectdata.get_data(".") + # Assert + if "rc" in __version__: + # Pyroma needs to chill about RC versions and not kill all our tests. + assert rating == ( + 9, + ["The package's version number does not comply with PEP-386."], + ) - # Act - rating = pyroma.ratings.rate(data) - - # Assert - if 'rc' in PILLOW_VERSION: - # Pyroma needs to chill about RC versions - # and not kill all our tests. - self.assertEqual(rating, (9, [ - "The package's version number does not comply with PEP-386."])) - - else: - # Should have a perfect score - self.assertEqual(rating, (10, [])) - - -if __name__ == '__main__': - unittest.main() + else: + # Should have a perfect score + assert rating == (10, []) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py new file mode 100644 index 00000000000..1fc8161467b --- /dev/null +++ b/Tests/test_qt_image_qapplication.py @@ -0,0 +1,92 @@ +import warnings + +import pytest + +with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=DeprecationWarning) + from PIL import ImageQt + +from .helper import assert_image_equal, assert_image_equal_tofile, hopper + +if ImageQt.qt_is_installed: + from PIL.ImageQt import QPixmap + + if ImageQt.qt_version == "6": + from PyQt6.QtCore import QPoint + from PyQt6.QtGui import QImage, QPainter, QRegion + from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget + elif ImageQt.qt_version == "side6": + from PySide6.QtCore import QPoint + from PySide6.QtGui import QImage, QPainter, QRegion + from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget + elif ImageQt.qt_version == "5": + from PyQt5.QtCore import QPoint + from PyQt5.QtGui import QImage, QPainter, QRegion + from PyQt5.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget + elif ImageQt.qt_version == "side2": + from PySide2.QtCore import QPoint + from PySide2.QtGui import QImage, QPainter, QRegion + from PySide2.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget + + class Example(QWidget): + def __init__(self): + super().__init__() + + img = hopper().resize((1000, 1000)) + + qimage = ImageQt.ImageQt(img) + + pixmap1 = ImageQt.QPixmap.fromImage(qimage) + + QHBoxLayout(self) # hbox + + lbl = QLabel(self) + # Segfault in the problem + lbl.setPixmap(pixmap1.copy()) + + +def roundtrip(expected): + result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) + # Qt saves all pixmaps as rgb + assert_image_equal(result, expected.convert("RGB")) + + +@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") +def test_sanity(tmp_path): + # Segfault test + app = QApplication([]) + ex = Example() + assert app # Silence warning + assert ex # Silence warning + + for mode in ("1", "RGB", "RGBA", "L", "P"): + # to QPixmap + im = hopper(mode) + data = ImageQt.toqpixmap(im) + + assert isinstance(data, QPixmap) + assert not data.isNull() + + # Test saving the file + tempfile = str(tmp_path / f"temp_{mode}.png") + data.save(tempfile) + + # Render the image + qimage = ImageQt.ImageQt(im) + data = QPixmap.fromImage(qimage) + qt_format = QImage.Format if ImageQt.qt_version == "6" else QImage + qimage = QImage(128, 128, qt_format.Format_ARGB32) + painter = QPainter(qimage) + image_label = QLabel() + image_label.setPixmap(data) + image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) + painter.end() + rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png") + qimage.save(rendered_tempfile) + assert_image_equal_tofile(im.convert("RGBA"), rendered_tempfile) + + # from QPixmap + roundtrip(hopper(mode)) + + app.quit() + app = None diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py new file mode 100644 index 00000000000..c1983031a14 --- /dev/null +++ b/Tests/test_qt_image_toqimage.py @@ -0,0 +1,47 @@ +import warnings + +import pytest + +with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=DeprecationWarning) + from PIL import ImageQt + +from .helper import assert_image_equal, assert_image_equal_tofile, hopper + +pytestmark = pytest.mark.skipif( + not ImageQt.qt_is_installed, reason="Qt bindings are not installed" +) + +if ImageQt.qt_is_installed: + from PIL.ImageQt import QImage + + +@pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1")) +def test_sanity(mode, tmp_path): + src = hopper(mode) + data = ImageQt.toqimage(src) + + assert isinstance(data, QImage) + assert not data.isNull() + + # reload directly from the qimage + rt = ImageQt.fromqimage(data) + if mode in ("L", "P", "1"): + assert_image_equal(rt, src.convert("RGB")) + else: + assert_image_equal(rt, src) + + if mode == "1": + # BW appears to not save correctly on QT5 + # kicks out errors on console: + # libpng warning: Invalid color type/bit depth combination + # in IHDR + # libpng error: Invalid IHDR data + return + + # Test saving the file + tempfile = str(tmp_path / f"temp_{mode}.png") + data.save(tempfile) + + # Check that it actually worked. + assert_image_equal_tofile(src, tempfile) diff --git a/Tests/test_scipy.py b/Tests/test_scipy.py deleted file mode 100644 index 18c4403a080..00000000000 --- a/Tests/test_scipy.py +++ /dev/null @@ -1,53 +0,0 @@ -from helper import unittest, PillowTestCase -from distutils.version import LooseVersion -try: - import numpy as np - from numpy.testing import assert_equal - - from scipy import misc - import scipy - HAS_SCIPY = True -except ImportError: - HAS_SCIPY = False - - -class Test_scipy_resize(PillowTestCase): - """ Tests for scipy regression in Pillow 2.6.0 - - Tests from https://github.com/scipy/scipy/blob/master/scipy/misc/pilutil.py - """ - - def setUp(self): - if not HAS_SCIPY: - self.skipTest("Scipy Required") - - def test_imresize(self): - im = np.random.random((10, 20)) - for T in np.sctypes['float'] + [float]: - # 1.1 rounds to below 1.1 for float16, 1.101 works - im1 = misc.imresize(im, T(1.101)) - self.assertEqual(im1.shape, (11, 22)) - - # this test fails prior to scipy 0.14.0b1 - # https://github.com/scipy/scipy/commit/855ff1fff805fb91840cf36b7082d18565fc8352 - @unittest.skipIf(HAS_SCIPY and - (LooseVersion(scipy.__version__) < LooseVersion('0.14.0')), - "Test fails on scipy < 0.14.0") - def test_imresize4(self): - im = np.array([[1, 2], - [3, 4]]) - res = np.array([[1., 1.25, 1.75, 2.], - [1.5, 1.75, 2.25, 2.5], - [2.5, 2.75, 3.25, 3.5], - [3., 3.25, 3.75, 4.]], dtype=np.float32) - # Check that resizing by target size, float and int are the same - im2 = misc.imresize(im, (4, 4), mode='F') # output size - im3 = misc.imresize(im, 2., mode='F') # fraction - im4 = misc.imresize(im, 200, mode='F') # percentage - assert_equal(im2, res) - assert_equal(im3, res) - assert_equal(im4, res) - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py new file mode 100644 index 00000000000..b5f9d442490 --- /dev/null +++ b/Tests/test_sgi_crash.py @@ -0,0 +1,26 @@ +import pytest + +from PIL import Image + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/sgi_overrun_expandrowF04.bin", + "Tests/images/sgi_crash.bin", + "Tests/images/crash-6b7f2244da6d0ae297ee0754a424213444e92778.sgi", + "Tests/images/ossfuzz-5730089102868480.sgi", + "Tests/images/crash-754d9c7ec485ffb76a90eeaab191ef69a2a3a3cd.sgi", + "Tests/images/crash-465703f71a0f0094873a3e0e82c9f798161171b8.sgi", + "Tests/images/crash-64834657ee604b8797bf99eac6a194c124a9a8ba.sgi", + "Tests/images/crash-abcf1c97b8fe42a6c68f1fb0b978530c98d57ced.sgi", + "Tests/images/crash-b82e64d4f3f76d7465b6af535283029eda211259.sgi", + "Tests/images/crash-c1b2595b8b0b92cc5f38b6635e98e3a119ade807.sgi", + "Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi", + ], +) +def test_crashes(test_file): + with open(test_file, "rb") as f: + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index acfea3baecd..d25d42dfca3 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -1,57 +1,49 @@ -from helper import unittest, PillowTestCase -from helper import djpeg_available, cjpeg_available, netpbm_available - -import sys import shutil -from PIL import Image, JpegImagePlugin, GifImagePlugin +import pytest + +from PIL import GifImagePlugin, Image, JpegImagePlugin + +from .helper import cjpeg_available, djpeg_available, is_win32, netpbm_available TEST_JPG = "Tests/images/hopper.jpg" TEST_GIF = "Tests/images/hopper.gif" -test_filenames = ( - "temp_';", - "temp_\";", - "temp_'\"|", - "temp_'\"||", - "temp_'\"&&", -) +test_filenames = ("temp_';", 'temp_";', "temp_'\"|", "temp_'\"||", "temp_'\"&&") -@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") -class TestShellInjection(PillowTestCase): - - def assert_save_filename_check(self, src_img, save_func): +@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") +class TestShellInjection: + def assert_save_filename_check(self, tmp_path, src_img, save_func): for filename in test_filenames: - dest_file = self.tempfile(filename) + dest_file = str(tmp_path / filename) save_func(src_img, 0, dest_file) # If file can't be opened, shell injection probably occurred - Image.open(dest_file).load() + with Image.open(dest_file) as im: + im.load() - @unittest.skipUnless(djpeg_available(), "djpeg not available") - def test_load_djpeg_filename(self): + @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") + def test_load_djpeg_filename(self, tmp_path): for filename in test_filenames: - src_file = self.tempfile(filename) + src_file = str(tmp_path / filename) shutil.copy(TEST_JPG, src_file) - im = Image.open(src_file) - im.load_djpeg() - - @unittest.skipUnless(cjpeg_available(), "cjpeg not available") - def test_save_cjpeg_filename(self): - im = Image.open(TEST_JPG) - self.assert_save_filename_check(im, JpegImagePlugin._save_cjpeg) - - @unittest.skipUnless(netpbm_available(), "netpbm not available") - def test_save_netpbm_filename_bmp_mode(self): - im = Image.open(TEST_GIF).convert("RGB") - self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) - - @unittest.skipUnless(netpbm_available(), "netpbm not available") - def test_save_netpbm_filename_l_mode(self): - im = Image.open(TEST_GIF).convert("L") - self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) - - -if __name__ == '__main__': - unittest.main() + with Image.open(src_file) as im: + im.load_djpeg() + + @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") + def test_save_cjpeg_filename(self, tmp_path): + with Image.open(TEST_JPG) as im: + self.assert_save_filename_check(tmp_path, im, JpegImagePlugin._save_cjpeg) + + @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") + def test_save_netpbm_filename_bmp_mode(self, tmp_path): + with Image.open(TEST_GIF) as im: + im = im.convert("RGB") + self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) + + @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") + def test_save_netpbm_filename_l_mode(self, tmp_path): + with Image.open(TEST_GIF) as im: + im = im.convert("L") + self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py new file mode 100644 index 00000000000..143765b8eec --- /dev/null +++ b/Tests/test_tiff_crashes.py @@ -0,0 +1,54 @@ +# Reproductions/tests for crashes/read errors in TiffDecode.c + +# When run in Python, all of these images should fail for +# one reason or another, either as a buffer overrun, +# unrecognized datastream, or truncated image file. +# There shouldn't be any segfaults. +# +# if run like +# `valgrind --tool=memcheck pytest test_tiff_crashes.py 2>&1 | grep TiffDecode.c` +# the output should be empty. There may be Python issues +# in the valgrind especially if run in a debug Python +# version. + +import pytest + +from PIL import Image + +from .helper import on_ci + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/crash_1.tif", + "Tests/images/crash_2.tif", + "Tests/images/crash-2020-10-test.tif", + "Tests/images/crash-0c7e0e8e11ce787078f00b5b0ca409a167f070e0.tif", + "Tests/images/crash-0e16d3bfb83be87356d026d66919deaefca44dac.tif", + "Tests/images/crash-1152ec2d1a1a71395b6f2ce6721c38924d025bf3.tif", + "Tests/images/crash-1185209cf7655b5aed8ae5e77784dfdd18ab59e9.tif", + "Tests/images/crash-338516dbd2f0e83caddb8ce256c22db3bd6dc40f.tif", + "Tests/images/crash-4f085cc12ece8cde18758d42608bed6a2a2cfb1c.tif", + "Tests/images/crash-86214e58da443d2b80820cff9677a38a33dcbbca.tif", + "Tests/images/crash-f46f5b2f43c370fe65706c11449f567ecc345e74.tif", + "Tests/images/crash-63b1dffefc8c075ddc606c0a2f5fdc15ece78863.tif", + "Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif", + "Tests/images/crash-81154a65438ba5aaeca73fd502fa4850fbde60f8.tif", + "Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif", + ], +) +@pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") +@pytest.mark.filterwarnings("ignore:Metadata warning") +@pytest.mark.filterwarnings("ignore:Truncated File Read") +def test_tiff_crashes(test_file): + try: + with Image.open(test_file) as im: + im.load() + except FileNotFoundError: + if not on_ci(): + pytest.skip("test image not found") + return + raise + except OSError: + pass diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 0bf4503c44f..12f475df036 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,63 +1,68 @@ -from helper import unittest, PillowTestCase, hopper +from fractions import Fraction -from PIL import TiffImagePlugin, Image +from PIL import Image, TiffImagePlugin, features from PIL.TiffImagePlugin import IFDRational -from fractions import Fraction +from .helper import hopper + + +def _test_equal(num, denom, target): + + t = IFDRational(num, denom) + + assert target == t + assert t == target -class Test_IFDRational(PillowTestCase): +def test_sanity(): - def _test_equal(self, num, denom, target): + _test_equal(1, 1, 1) + _test_equal(1, 1, Fraction(1, 1)) - t = IFDRational(num, denom) + _test_equal(2, 2, 1) + _test_equal(1.0, 1, Fraction(1, 1)) - self.assertEqual(target, t) - self.assertEqual(t, target) + _test_equal(Fraction(1, 1), 1, Fraction(1, 1)) + _test_equal(IFDRational(1, 1), 1, 1) - def test_sanity(self): + _test_equal(1, 2, Fraction(1, 2)) + _test_equal(1, 2, IFDRational(1, 2)) - self._test_equal(1, 1, 1) - self._test_equal(1, 1, Fraction(1, 1)) + _test_equal(7, 5, 1.4) - self._test_equal(2, 2, 1) - self._test_equal(1.0, 1, Fraction(1, 1)) - self._test_equal(Fraction(1, 1), 1, Fraction(1, 1)) - self._test_equal(IFDRational(1, 1), 1, 1) +def test_ranges(): + for num in range(1, 10): + for denom in range(1, 10): + assert IFDRational(num, denom) == IFDRational(num, denom) - self._test_equal(1, 2, Fraction(1, 2)) - self._test_equal(1, 2, IFDRational(1, 2)) - def test_nonetype(self): - " Fails if the _delegate function doesn't return a valid function" +def test_nonetype(): + # Fails if the _delegate function doesn't return a valid function - xres = IFDRational(72) - yres = IFDRational(72) - self.assertIsNotNone(xres._val) - self.assertIsNotNone(xres.numerator) - self.assertIsNotNone(xres.denominator) - self.assertIsNotNone(yres._val) + xres = IFDRational(72) + yres = IFDRational(72) + assert xres._val is not None + assert xres.numerator is not None + assert xres.denominator is not None + assert yres._val is not None - self.assertTrue(xres and 1) - self.assertTrue(xres and yres) + assert xres and 1 + assert xres and yres - def test_ifd_rational_save(self): - methods = (True, False) - if 'libtiff_encoder' not in dir(Image.core): - methods = (False,) - for libtiff in methods: - TiffImagePlugin.WRITE_LIBTIFF = libtiff +def test_ifd_rational_save(tmp_path): + methods = (True, False) + if not features.check("libtiff"): + methods = (False,) - im = hopper() - out = self.tempfile('temp.tiff') - res = IFDRational(301, 1) - im.save(out, dpi=(res, res), compression='raw') + for libtiff in methods: + TiffImagePlugin.WRITE_LIBTIFF = libtiff - reloaded = Image.open(out) - self.assertEqual(float(IFDRational(301, 1)), - float(reloaded.tag_v2[282])) + im = hopper() + out = str(tmp_path / "temp.tiff") + res = IFDRational(301, 1) + im.save(out, dpi=(res, res), compression="raw") -if __name__ == '__main__': - unittest.main() + with Image.open(out) as reloaded: + assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) diff --git a/Tests/test_uploader.py b/Tests/test_uploader.py index b52ea10f6f3..720926e5377 100644 --- a/Tests/test_uploader.py +++ b/Tests/test_uploader.py @@ -1,18 +1,13 @@ -from helper import unittest, PillowTestCase, hopper +from .helper import assert_image_equal, assert_image_similar, hopper -from PIL import Image +def check_upload_equal(): + result = hopper("P").convert("RGB") + target = hopper("RGB") + assert_image_equal(result, target) -class TestUploader(PillowTestCase): - def check_upload_equal(self): - result = hopper('P').convert('RGB') - target = hopper('RGB') - self.assert_image_equal(result, target) - def check_upload_similar(self): - result = hopper('P').convert('RGB') - target = hopper('RGB') - self.assert_image_similar(result, target, 0) - -if __name__ == '__main__': - unittest.main() +def check_upload_similar(): + result = hopper("P").convert("RGB") + target = hopper("RGB") + assert_image_similar(result, target, 0) diff --git a/Tests/test_util.py b/Tests/test_util.py index 9901de3571d..9efbdd1f380 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,79 +1,72 @@ -from helper import unittest, PillowTestCase +import pytest from PIL import _util -class TestUtil(PillowTestCase): +def test_is_path(): + # Arrange + fp = "filename.ext" - def test_is_string_type(self): - # Arrange - color = "red" + # Act + it_is = _util.is_path(fp) - # Act - it_is = _util.isStringType(color) + # Assert + assert it_is - # Assert - self.assertTrue(it_is) - def test_is_not_string_type(self): - # Arrange - color = (255, 0, 0) +def test_path_obj_is_path(): + # Arrange + from pathlib import Path - # Act - it_is_not = _util.isStringType(color) + test_path = Path("filename.ext") - # Assert - self.assertFalse(it_is_not) + # Act + it_is = _util.is_path(test_path) - def test_is_path(self): - # Arrange - fp = "filename.ext" + # Assert + assert it_is - # Act - it_is = _util.isStringType(fp) - # Assert - self.assertTrue(it_is) +def test_is_not_path(tmp_path): + # Arrange + with (tmp_path / "temp.ext").open("w") as fp: + pass - def test_is_not_path(self): - # Arrange - filename = self.tempfile("temp.ext") - fp = open(filename, 'w').close() + # Act + it_is_not = _util.is_path(fp) - # Act - it_is_not = _util.isPath(fp) + # Assert + assert not it_is_not - # Assert - self.assertFalse(it_is_not) - def test_is_directory(self): - # Arrange - directory = "Tests" +def test_is_directory(): + # Arrange + directory = "Tests" - # Act - it_is = _util.isDirectory(directory) + # Act + it_is = _util.is_directory(directory) - # Assert - self.assertTrue(it_is) + # Assert + assert it_is - def test_is_not_directory(self): - # Arrange - text = "abc" - # Act - it_is_not = _util.isDirectory(text) +def test_is_not_directory(): + # Arrange + text = "abc" - # Assert - self.assertFalse(it_is_not) + # Act + it_is_not = _util.is_directory(text) - def test_deferred_error(self): - # Arrange + # Assert + assert not it_is_not - # Act - thing = _util.deferred_error(ValueError("Some error text")) - # Assert - self.assertRaises(ValueError, lambda: thing.some_attr) +def test_deferred_error(): + # Arrange -if __name__ == '__main__': - unittest.main() + # Act + thing = _util.DeferredError(ValueError("Some error text")) + + # Assert + with pytest.raises(ValueError): + thing.some_attr diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py new file mode 100644 index 00000000000..34197c14f80 --- /dev/null +++ b/Tests/test_webp_leaks.py @@ -0,0 +1,24 @@ +from io import BytesIO + +from PIL import Image + +from .helper import PillowLeakTestCase, skip_unless_feature + +test_file = "Tests/images/hopper.webp" + + +@skip_unless_feature("webp") +class TestWebPLeaks(PillowLeakTestCase): + + mem_limit = 3 * 1024 # kb + iterations = 100 + + def test_leak_load(self): + with open(test_file, "rb") as f: + im_data = f.read() + + def core(): + with Image.open(BytesIO(im_data)) as im: + im.load() + + self._test_leak(core) diff --git a/Tests/threaded_save.py b/Tests/threaded_save.py deleted file mode 100644 index ba8b17dbc47..00000000000 --- a/Tests/threaded_save.py +++ /dev/null @@ -1,57 +0,0 @@ -from __future__ import print_function -from PIL import Image - -import io -import queue -import sys -import threading -import time - -test_format = sys.argv[1] if len(sys.argv) > 1 else "PNG" - -im = Image.open("Tests/images/hopper.ppm") -im.load() - -queue = queue.Queue() - -result = [] - - -class Worker(threading.Thread): - def run(self): - while True: - im = queue.get() - if im is None: - queue.task_done() - sys.stdout.write("x") - break - f = io.BytesIO() - im.save(f, test_format, optimize=1) - data = f.getvalue() - result.append(len(data)) - im = Image.open(io.BytesIO(data)) - im.load() - sys.stdout.write(".") - queue.task_done() - -t0 = time.time() - -threads = 20 -jobs = 100 - -for i in range(threads): - w = Worker() - w.start() - -for i in range(jobs): - queue.put(im) - -for i in range(threads): - queue.put(None) - -queue.join() - -print() -print(time.time() - t0) -print(len(result), sum(result)) -print(result) diff --git a/Tests/versions.py b/Tests/versions.py deleted file mode 100644 index 89be1d7c82a..00000000000 --- a/Tests/versions.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import print_function -from PIL import Image - - -def version(module, version): - v = getattr(module.core, version + "_version", None) - if v: - print(version, v) - -version(Image, "jpeglib") -version(Image, "zlib") - -try: - from PIL import ImageFont -except ImportError: - pass -else: - version(ImageFont, "freetype2") - -try: - from PIL import ImageCms -except ImportError: - pass -else: - version(ImageCms, "littlecms") diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index fe128f4a4d1..00000000000 --- a/appveyor.yml +++ /dev/null @@ -1,117 +0,0 @@ -version: '{build}' -clone_folder: c:\pillow -init: -- ECHO %PYTHON% -#- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) -# Uncomment previous line to get RDP access during the build. - -environment: - X64_EXT: -x64 - EXECUTABLE: python.exe - PIP_DIR: Scripts - VENV: NO - TEST_OPTIONS: - DEPLOY: YES - matrix: - - PYTHON: C:/vp/pypy2 - EXECUTABLE: bin/pypy.exe - PIP_DIR: bin - VENV: YES - - PYTHON: C:/Python27-x64 - - PYTHON: C:/Python34 - - PYTHON: C:/Python27 - - PYTHON: C:/Python34-x64 - - PYTHON: C:/msys64/mingw32 - EXECUTABLE: bin/python3 - PIP_DIR: bin - TEST_OPTIONS: --processes=0 - DEPLOY: NO - - -install: -- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/master.zip -- 7z x pillow-depends.zip -oc:\ -- mv c:\pillow-depends-master c:\pillow-depends -- xcopy c:\pillow-depends\*.zip c:\pillow\winbuild\ -- xcopy c:\pillow-depends\*.tar.gz c:\pillow\winbuild\ -- xcopy /s c:\pillow-depends\test_images\* c:\pillow\tests\images -- cd c:\pillow\winbuild\ -- ps: | - if ($env:PYTHON -eq "c:/vp/pypy2") - { - c:\pillow\winbuild\appveyor_install_pypy.cmd - } -- ps: | - if ($env:PYTHON -eq "c:/msys64/mingw32") - { - c:\msys64\usr\bin\bash -l -c c:\\pillow\\winbuild\\appveyor_install_msys2_deps.sh - } - else - { - c:\python34\python.exe c:\pillow\winbuild\build_dep.py - c:\pillow\winbuild\build_deps.cmd - $host.SetShouldExit(0) - } - -build_script: -- ps: | - if ($env:PYTHON -eq "c:/msys64/mingw32") - { - c:\msys64\usr\bin\bash -l -c c:\\pillow\\winbuild\\appveyor_build_msys2.sh - Write-Host "through install" - $host.SetShouldExit(0) - } - else - { - & $env:PYTHON/$env:EXECUTABLE c:\pillow\winbuild\build.py - $host.SetShouldExit(0) - } -- cd c:\pillow -- '%PYTHON%\%EXECUTABLE% selftest.py --installed' - -test_script: -- cd c:\pillow -- '%PYTHON%\%PIP_DIR%\pip.exe install pytest pytest-cov' -- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov-report term --cov-report xml Tests' -#- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest? - -after_test: -- pip install codecov -- codecov --file coverage.xml --name %PYTHON% - -matrix: - fast_finish: true - -artifacts: -- path: pillow\dist\*.egg - name: egg -- path: pillow\dist\*.wheel - name: wheel - -before_deploy: - - cd c:\pillow - - '%PYTHON%\%PIP_DIR%\pip.exe install wheel' - - cd c:\pillow\winbuild\ - - '%PYTHON%\%EXECUTABLE% c:\pillow\winbuild\build.py --wheel' - - cd c:\pillow - - ps: Get-ChildItem .\dist\*.* | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } - -deploy: - provider: S3 - region: us-west-2 - access_key_id: AKIAIRAXC62ZNTVQJMOQ - secret_access_key: - secure: Hwb6klTqtBeMgxAjRoDltiiqpuH8xbwD4UooDzBSiCWXjuFj1lyl4kHgHwTCCGqi - bucket: pillow-nightly - folder: win/$(APPVEYOR_BUILD_NUMBER)/ - artifact: /.*egg|wheel/ - on: - branch: master - deploy: YES - - -# Uncomment the following lines to get RDP access after the build/test and block for -# up to the timeout limit (~1hr) -# -#on_finish: -#- ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/build_children.sh b/build_children.sh deleted file mode 100755 index c4ed4ebfa8e..00000000000 --- a/build_children.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -# Get last child project build number from branch named "latest" -BUILD_NUM=$(curl -s 'https://api.travis-ci.org/repos/python-pillow/pillow-wheels/branches/latest' | grep -o '^{"branch":{"id":[0-9]*,' | grep -o '[0-9]' | tr -d '\n') - -# Restart last child project build -curl -X POST https://api.travis-ci.org/builds/$BUILD_NUM/restart --header "Authorization: token "$AUTH_TOKEN diff --git a/codecov.yml b/codecov.yml index db2472009c6..f3afccc1caf 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1 +1,22 @@ -comment: off +# Documentation: https://docs.codecov.io/docs/codecov-yaml + +codecov: + # Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" + # https://github.com/codecov/support/issues/363 + # https://docs.codecov.io/docs/comparing-commits + allow_coverage_offsets: true + +comment: false + +coverage: + status: + project: + default: + threshold: 0.01% + +# Matches 'omit:' in .coveragerc +ignore: + - "Tests/32bit_segfault_check.py" + - "Tests/bench_cffi_access.py" + - "Tests/check_*.py" + - "Tests/createfontdatachunk.py" diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000000..e123cca80fe --- /dev/null +++ b/conftest.py @@ -0,0 +1 @@ +pytest_plugins = ["Tests.helper"] diff --git a/depends/README.rst b/depends/README.rst index 779e956f496..b69c9dcbf8f 100644 --- a/depends/README.rst +++ b/depends/README.rst @@ -1,27 +1,9 @@ Depends ======= -``install_openjpeg.sh``, ``install_webp.sh`` and ``install_imagequant.sh`` can -be used to download, build & install non-packaged dependencies; useful for -testing with Travis CI. - -The other scripts can be used to install all of the dependencies for -the listed operating systems/distros. The ``ubuntu_14.04.sh`` and -``debian_8.2.sh`` scripts have been tested on bare AWS images and will -install all required dependencies for the system Python 2.7 and 3.4 -for all of the optional dependencies. Git may also be required prior -to running the script to actually download Pillow. - -e.g.:: - - $ sudo apt-get install git - $ git clone https://github.com/python-pillow/Pillow.git - $ cd Pillow/depends - $ ./debian_8.2.sh - $ cd .. - $ git checkout [branch or tag] - $ virtualenv -p /usr/bin/python2.7 ~/vpy27 - $ source ~/vpy27/bin/activate - $ make install - $ make test +``install_openjpeg.sh``, ``install_webp.sh``, ``install_imagequant.sh``, +``install_raqm.sh`` and ``install_raqm_cmake.sh`` can be used to download, +build & install non-packaged dependencies; useful for testing on CI. +``install_extra_test_images.sh`` can be used to install additional test images +that are used by CI. diff --git a/depends/alpine_Dockerfile b/depends/alpine_Dockerfile deleted file mode 100644 index 69bdf84f6cc..00000000000 --- a/depends/alpine_Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -# This is a sample Dockerfile to build Pillow on Alpine Linux -# with all/most of the dependencies working. -# -# Tcl/Tk isn't detecting -# Freetype has different metrics so tests are failing. -# sudo and bash are required for the webp build script. - -FROM alpine -USER root - -RUN apk --no-cache add python \ - build-base \ - python-dev \ - py-pip \ - # Pillow dependencies - jpeg-dev \ - zlib-dev \ - freetype-dev \ - lcms2-dev \ - openjpeg-dev \ - tiff-dev \ - tk-dev \ - tcl-dev - -# install from pip, without webp -#RUN LIBRARY_PATH=/lib:/usr/lib /bin/sh -c "pip install Pillow" - -# install from git, run tests, including webp -RUN apk --no-cache add git \ - bash \ - sudo - -RUN git clone https://github.com/python-pillow/Pillow.git /Pillow -RUN pip install virtualenv && virtualenv /vpy && source /vpy/bin/activate && pip install nose - -RUN echo "#!/bin/bash" >> /test && \ - echo "source /vpy/bin/activate && cd /Pillow " >> test && \ - echo "pushd depends && ./install_webp.sh && ./install_imagequant.sh && popd" >> test && \ - echo "LIBRARY_PATH=/lib:/usr/lib make install && make test" >> test - -RUN chmod +x /test - -CMD ["/test"] diff --git a/depends/debian_8.2.sh b/depends/debian_8.2.sh deleted file mode 100755 index c4f72bf8ee5..00000000000 --- a/depends/debian_8.2.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -# -# Installs all of the dependencies for Pillow for Debian 8.2 -# for both system Pythons 2.7 and 3.4 -# -# Also works for Raspbian Jessie -# - -sudo apt-get -y install python-dev python-setuptools \ - python3-dev python-virtualenv cmake -sudo apt-get -y install libtiff5-dev libjpeg62-turbo-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \ - python-tk python3-tk libharfbuzz-dev libfribidi-dev - -./install_openjpeg.sh -./install_imagequant.sh -./install_raqm.sh diff --git a/depends/diffcover-install.sh b/depends/diffcover-install.sh deleted file mode 100755 index 850d368f8f6..00000000000 --- a/depends/diffcover-install.sh +++ /dev/null @@ -1,7 +0,0 @@ -# Fetch the remote master branch before running diff-cover on Travis CI. -# https://github.com/Bachmann1234/diff-cover#troubleshooting -git fetch origin master:refs/remotes/origin/master - -# CFLAGS=-O0 means build with no optimisation. -# Makes build much quicker for lxml and other dependencies. -time CFLAGS=-O0 pip install --use-wheel diff_cover diff --git a/depends/diffcover-run.sh b/depends/diffcover-run.sh deleted file mode 100755 index 02efab6aea5..00000000000 --- a/depends/diffcover-run.sh +++ /dev/null @@ -1,4 +0,0 @@ -coverage xml -diff-cover coverage.xml -diff-quality --violation=pyflakes -diff-quality --violation=pep8 diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh index 7cc905e8543..d9608e7827a 100755 --- a/depends/download-and-extract.sh +++ b/depends/download-and-extract.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Usage: ./download-and-extract.sh something.tar.gz https://example.com/something.tar.gz +# Usage: ./download-and-extract.sh something https://example.com/something.tar.gz archive=$1 url=$2 diff --git a/depends/fedora_23.sh b/depends/fedora_23.sh deleted file mode 100755 index 5bdcf7f174a..00000000000 --- a/depends/fedora_23.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -# -# Installs all of the dependencies for Pillow for Fedora 23 -# for both system Pythons 2.7 and 3.4 -# -# note that Fedora does ship packages for Pillow as python-pillow - -# this is a workaround for -# "gcc: error: /usr/lib/rpm/redhat/redhat-hardened-cc1: No such file or directory" -# errors when compiling. -sudo dnf install redhat-rpm-config - -sudo dnf install python-devel python3-devel python-virtualenv make gcc - -sudo dnf install libtiff-devel libjpeg-devel zlib-devel freetype-devel \ - lcms2-devel libwebp-devel openjpeg2-devel tkinter python3-tkinter \ - tcl-devel tk-devel harfbuzz-devel fribidi-devel libraqm-devel \ No newline at end of file diff --git a/depends/freebsd_10.sh b/depends/freebsd_10.sh deleted file mode 100755 index 36d9c106913..00000000000 --- a/depends/freebsd_10.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -# -# Installs all of the dependencies for Pillow for Freebsd 10.x -# for both system Pythons 2.7 and 3.4 -# -sudo pkg install python2 python3 py27-pip py27-virtualenv wget cmake - -# Openjpeg fails badly using the openjpeg package. -# I can't find a python3.4 version of tkinter -sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 harfbuzz fribidi py27-tkinter - -./install_raqm_cmake.sh diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 667c74e6d72..02da12d61a4 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -1,9 +1,15 @@ #!/bin/bash # install extra test images -rm -r test_images +# Use SVN to just fetch a single Git subdirectory +svn_export() +{ + if [ ! -z $1 ]; then + echo "" + echo "Retrying svn export..." + echo "" + fi -# Use SVN to just fetch a single git subdirectory -svn checkout https://github.com/python-pillow/pillow-depends/trunk/test_images - -cp -r test_images/* ../Tests/images + svn export --force https://github.com/python-pillow/pillow-depends/trunk/test_images ../Tests/images +} +svn_export || svn_export retry || svn_export retry || svn_export retry diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 1ab3a6981e5..64dd024bd7f 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,14 +1,15 @@ #!/bin/bash # install libimagequant -archive=libimagequant-2.11.4 +archive=libimagequant-4.0.4 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz -pushd $archive +pushd $archive/imagequant-sys -make shared -sudo cp libimagequant.so* /usr/lib/ -sudo cp libimagequant.h /usr/include/ +cargo install cargo-c +cargo cinstall --prefix=/usr --destdir=. +sudo cp usr/lib/libimagequant.so* /usr/lib/ +sudo cp usr/include/libimagequant.h /usr/include/ popd diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index ed7f0d6b521..4f4b81a628b 100755 --- a/depends/install_openjpeg.sh +++ b/depends/install_openjpeg.sh @@ -1,9 +1,9 @@ #!/bin/bash # install openjpeg -archive=openjpeg-2.3.0 +archive=openjpeg-2.5.0 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz pushd $archive diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index 7b10df7d434..99250365065 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -2,13 +2,13 @@ # install raqm -archive=raqm-0.3.0 +archive=libraqm-0.9.0 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz pushd $archive -./configure --prefix=/usr && make -j4 && sudo make -j4 install +meson build --prefix=/usr && sudo ninja -C build install popd diff --git a/depends/install_raqm_cmake.sh b/depends/install_raqm_cmake.sh index 0c5ed8b697f..7d2c399df5d 100755 --- a/depends/install_raqm_cmake.sh +++ b/depends/install_raqm_cmake.sh @@ -2,9 +2,9 @@ # install raqm -archive=raqm-cmake-b517ba80 +archive=raqm-cmake-99300ff3 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz pushd $archive diff --git a/depends/install_webp.sh b/depends/install_webp.sh index 37a77243603..05867b7d448 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,9 +1,9 @@ #!/bin/bash # install webp -archive=libwebp-0.6.1 +archive=libwebp-1.2.4 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz pushd $archive diff --git a/depends/termux.sh b/depends/termux.sh index f117790c5a0..1acc09c4463 100755 --- a/depends/termux.sh +++ b/depends/termux.sh @@ -1,5 +1,5 @@ #!/bin/sh -pkg -y install python python-dev ndk-sysroot clang make \ - libjpeg-turbo-dev +pkg install -y python ndk-sysroot clang make \ + libjpeg-turbo diff --git a/depends/ubuntu_12.04.sh b/depends/ubuntu_12.04.sh deleted file mode 100755 index 9bfae43b0bc..00000000000 --- a/depends/ubuntu_12.04.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -# -# Installs all of the dependencies for Pillow for Ubuntu 12.04 -# for both system Pythons 2.7 and 3.2 -# - -sudo apt-get -y install python-dev python-setuptools \ - python3-dev python-virtualenv cmake -sudo apt-get install libtiff4-dev libjpeg8-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev tcl8.5-dev \ - tk8.5-dev python-tk python3-tk - - -./install_openjpeg.sh -./install_webp.sh -./install_imagequant.sh diff --git a/depends/ubuntu_14.04.sh b/depends/ubuntu_14.04.sh deleted file mode 100755 index f7d28fba71b..00000000000 --- a/depends/ubuntu_14.04.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh - -# -# Installs all of the dependencies for Pillow for Ubuntu 14.04 -# for both system Pythons 2.7 and 3.4 -# -sudo apt-get update -sudo apt-get -y install python-dev python-setuptools \ - python3-dev python-virtualenv cmake -sudo apt-get -y install libtiff5-dev libjpeg8-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \ - python-tk python3-tk libharfbuzz-dev libfribidi-dev - -./install_openjpeg.sh -./install_imagequant.sh -./install_raqm.sh diff --git a/docs/COPYING b/docs/COPYING index 75452788585..25f03b34312 100644 --- a/docs/COPYING +++ b/docs/COPYING @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2018 by Alex Clark and contributors + Copyright © 2010-2022 by Alex Clark and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/docs/Guardfile b/docs/Guardfile index f8f3051ed9e..b689b079aea 100755 --- a/docs/Guardfile +++ b/docs/Guardfile @@ -1,10 +1,8 @@ -#!/usr/bin/env python -from livereload.task import Task +#!/usr/bin/env python3 from livereload.compiler import shell +from livereload.task import Task Task.add('*.rst', shell('make html')) Task.add('*/*.rst', shell('make html')) -Task.add('_static/*.css', shell('make clean html')) -Task.add('_templates/*', shell('make clean html')) Task.add('Makefile', shell('make html')) Task.add('conf.py', shell('make html')) diff --git a/docs/Makefile b/docs/Makefile index 1a912039ec9..458299aac7b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,8 +2,9 @@ # # You can set these variables from the command line. +PYTHON = python3 SPHINXOPTS = -SPHINXBUILD = sphinx-build +SPHINXBUILD = $(PYTHON) -m sphinx.cmd.build PAPER = BUILDDIR = _build @@ -41,38 +42,48 @@ help: clean: -rm -rf $(BUILDDIR)/* +install-sphinx: + $(PYTHON) -m pip install --quiet sphinx sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph furo olefile + html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + $(MAKE) install-sphinx + $(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: + $(MAKE) install-sphinx $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: + $(MAKE) install-sphinx $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: + $(MAKE) install-sphinx $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: + $(MAKE) install-sphinx $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: + $(MAKE) install-sphinx $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: + $(MAKE) install-sphinx $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ @@ -82,6 +93,7 @@ qthelp: @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc" devhelp: + $(MAKE) install-sphinx $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @@ -91,11 +103,13 @@ devhelp: @echo "# devhelp" epub: + $(MAKE) install-sphinx $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: + $(MAKE) install-sphinx $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @@ -103,22 +117,26 @@ latex: "(use \`make latexpdf' here to do that automatically)." latexpdf: + $(MAKE) install-sphinx $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: + $(MAKE) install-sphinx $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: + $(MAKE) install-sphinx $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: + $(MAKE) install-sphinx $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @@ -126,28 +144,33 @@ texinfo: "(use \`make info' here to do that automatically)." info: + $(MAKE) install-sphinx $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: + $(MAKE) install-sphinx $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: + $(MAKE) install-sphinx $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + $(MAKE) install-sphinx + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." 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." @@ -156,4 +179,4 @@ livehtml: html livereload $(BUILDDIR)/html -p 33233 serve: - cd $(BUILDDIR)/html; python -m SimpleHTTPServer + cd $(BUILDDIR)/html; $(PYTHON) -m http.server diff --git a/docs/PIL.rst b/docs/PIL.rst index 67edb990192..fa036b9ccfe 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -4,108 +4,97 @@ PIL Package (autodoc of remaining modules) Reference for modules whose documentation has not yet been ported or written can be found here. -:mod:`BdfFontFile` Module -------------------------- +:mod:`PIL` Module +----------------- + +.. py:module:: PIL + +.. autoexception:: UnidentifiedImageError + :show-inheritance: + +:mod:`~PIL.BdfFontFile` Module +------------------------------ .. automodule:: PIL.BdfFontFile :members: :undoc-members: :show-inheritance: -:mod:`ContainerIO` Module -------------------------- +:mod:`~PIL.ContainerIO` Module +------------------------------ .. automodule:: PIL.ContainerIO :members: :undoc-members: :show-inheritance: -:mod:`FontFile` Module ----------------------- +:mod:`~PIL.FontFile` Module +--------------------------- .. automodule:: PIL.FontFile :members: :undoc-members: :show-inheritance: -:mod:`GdImageFile` Module -------------------------- +:mod:`~PIL.GdImageFile` Module +------------------------------ .. automodule:: PIL.GdImageFile :members: :undoc-members: :show-inheritance: -:mod:`GimpGradientFile` Module ------------------------------- +:mod:`~PIL.GimpGradientFile` Module +----------------------------------- .. automodule:: PIL.GimpGradientFile :members: :undoc-members: :show-inheritance: -:mod:`GimpPaletteFile` Module ------------------------------ +:mod:`~PIL.GimpPaletteFile` Module +---------------------------------- .. automodule:: PIL.GimpPaletteFile :members: :undoc-members: :show-inheritance: -.. intentionally skipped documenting this because it's not documented anywhere - -:mod:`ImageDraw2` Module ------------------------- +:mod:`~PIL.ImageDraw2` Module +----------------------------- .. automodule:: PIL.ImageDraw2 :members: + :member-order: bysource :undoc-members: :show-inheritance: -.. intentionally skipped documenting this because it's deprecated - -:mod:`ImageShow` Module ------------------------ - -.. automodule:: PIL.ImageShow - :members: - :undoc-members: - :show-inheritance: - -:mod:`ImageTransform` Module ----------------------------- +:mod:`~PIL.ImageTransform` Module +--------------------------------- .. automodule:: PIL.ImageTransform :members: :undoc-members: :show-inheritance: -:mod:`JpegPresets` Module -------------------------- - -.. automodule:: PIL.JpegPresets - :members: - :undoc-members: - :show-inheritance: - -:mod:`PaletteFile` Module -------------------------- +:mod:`~PIL.PaletteFile` Module +------------------------------ .. automodule:: PIL.PaletteFile :members: :undoc-members: :show-inheritance: -:mod:`PcfFontFile` Module -------------------------- +:mod:`~PIL.PcfFontFile` Module +------------------------------ .. automodule:: PIL.PcfFontFile :members: :undoc-members: :show-inheritance: -:class:`PngImagePlugin.iTXt` Class ----------------------------------- +:class:`.PngImagePlugin.iTXt` Class +----------------------------------- .. autoclass:: PIL.PngImagePlugin.iTXt :members: @@ -118,8 +107,8 @@ can be found here. :param lang: language code :param tkey: UTF-8 version of the key name -:class:`PngImagePlugin.PngInfo` Class -------------------------------------- +:class:`.PngImagePlugin.PngInfo` Class +-------------------------------------- .. autoclass:: PIL.PngImagePlugin.PngInfo :members: @@ -127,27 +116,18 @@ can be found here. :show-inheritance: -:mod:`TarIO` Module -------------------- +:mod:`~PIL.TarIO` Module +------------------------ .. automodule:: PIL.TarIO :members: :undoc-members: :show-inheritance: -:mod:`WalImageFile` Module --------------------------- +:mod:`~PIL.WalImageFile` Module +------------------------------- .. automodule:: PIL.WalImageFile :members: :undoc-members: :show-inheritance: - -:mod:`_binary` Module ---------------------- - -.. automodule:: PIL._binary - :members: - :undoc-members: - :show-inheritance: - diff --git a/docs/_static/.gitignore b/docs/_static/.gitignore deleted file mode 100644 index b1f9a2ade2a..00000000000 --- a/docs/_static/.gitignore +++ /dev/null @@ -1 +0,0 @@ -# Empty file, to make the directory available in the repository diff --git a/docs/_templates/.gitignore b/docs/_templates/.gitignore deleted file mode 100644 index b1f9a2ade2a..00000000000 --- a/docs/_templates/.gitignore +++ /dev/null @@ -1 +0,0 @@ -# Empty file, to make the directory available in the repository diff --git a/docs/_templates/sidebarhelp.html b/docs/_templates/sidebarhelp.html deleted file mode 100644 index da3882e8d09..00000000000 --- a/docs/_templates/sidebarhelp.html +++ /dev/null @@ -1,4 +0,0 @@ -

Need help?

-

- You can get help via IRC at irc://irc.freenode.net#pil or Stack Overflow here and here. Please report issues on GitHub. -

diff --git a/docs/about.rst b/docs/about.rst index dd6ca9a9882..03829c133f5 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -6,19 +6,20 @@ Goals The fork author's goal is to foster and support active development of PIL through: -- Continuous integration testing via `Travis CI`_ and `AppVeyor`_ +- Continuous integration testing via `GitHub Actions`_, `AppVeyor`_ and `Travis CI`_ - Publicized development activity on `GitHub`_ - Regular releases to the `Python Package Index`_ -.. _Travis CI: https://travis-ci.org/python-pillow/Pillow +.. _GitHub Actions: https://github.com/python-pillow/Pillow/actions .. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow +.. _Travis CI: https://app.travis-ci.com/github/python-pillow/pillow-wheels .. _GitHub: https://github.com/python-pillow/Pillow -.. _Python Package Index: https://pypi.python.org/pypi/Pillow +.. _Python Package Index: https://pypi.org/project/Pillow/ License ------- -Like PIL, Pillow is `licensed under the open source PIL Software License `_ +Like PIL, Pillow is `licensed under the open source HPND License `_ Why a fork? ----------- @@ -35,10 +36,4 @@ What about PIL? Prior to Pillow 2.0.0, very few image code changes were made. Pillow 2.0.0 added Python 3 support and includes many bug fixes from many contributors. -As more time passes since the last PIL release, the likelihood of a new PIL release decreases. However, we've yet to hear an official "PIL is dead" announcement. So if you still want to support PIL, please `report issues here first`_, then `open corresponding Pillow tickets here`_. - -.. _report issues here first: https://bitbucket.org/effbot/pil-2009-raclette/issues - -.. _open corresponding Pillow tickets here: https://github.com/python-pillow/Pillow/issues - -Please provide a link to the first ticket so we can track the issue(s) upstream. +As more time passes since the last PIL release (1.1.7 in 2009), the likelihood of a new PIL release decreases. However, we've yet to hear an official "PIL is dead" announcement. diff --git a/docs/conf.py b/docs/conf.py index 4053e24e6ef..bc67d936893 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Pillow (PIL Fork) documentation build configuration file, created by # sphinx-quickstart on Sat Apr 4 07:54:11 2015. @@ -15,237 +14,258 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) + +import PIL # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +needs_sphinx = "2.4" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', - 'sphinx.ext.intersphinx'] +extensions = [ + "sphinx_copybutton", + "sphinx_issues", + "sphinx_removed_in", + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", + "sphinxext.opengraph", +] -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'Pillow (PIL Fork)' -copyright = u'1995-2011 Fredrik Lundh, 2010-2018 Alex Clark and Contributors' -author = u'Fredrik Lundh, Alex Clark and Contributors' +project = "Pillow (PIL Fork)" +copyright = "1995-2011 Fredrik Lundh, 2010-2022 Alex Clark and Contributors" +author = "Fredrik Lundh, Alex Clark and Contributors" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -import PIL -version = PIL.PILLOW_VERSION +version = PIL.__version__ # The full version, including alpha/beta/rc tags. -release = PIL.PILLOW_VERSION +release = PIL.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build", "releasenotes/template.rst"] # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False +# If true, Sphinx will warn about all references where the target cannot be found. +# Default is False. You can activate this mode temporarily using the -n command-line +# switch. +nitpicky = True + +# A list of (type, target) tuples (by default empty) that should be ignored when +# generating warnings in “nitpicky mode”. Note that type should include the domain name +# if present. Example entries would be ('py:func', 'int') or +# ('envvar', 'LD_LIBRARY_PATH'). +# nitpick_ignore = [] + # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -import sphinx_rtd_theme -html_theme = "sphinx_rtd_theme" -html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +html_theme = "furo" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +html_theme_options = { + "light_logo": "pillow-logo-dark-text.png", + "dark_logo": "pillow-logo.png", +} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = "resources/pillow-logo.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +html_favicon = "resources/favicon.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["resources"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -#html_search_language = 'en' +# html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} +# html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'PillowPILForkdoc' +htmlhelp_basename = "PillowPILForkdoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', + # Latex figure (float) alignment + # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'PillowPILFork.tex', u'Pillow (PIL Fork) Documentation', - u'Alex Clark', 'manual'), + ( + master_doc, + "PillowPILFork.tex", + "Pillow (PIL Fork) Documentation", + "Alex Clark", + "manual", + ) ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- @@ -253,12 +273,11 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'pillowpilfork', u'Pillow (PIL Fork) Documentation', - [author], 1) + (master_doc, "pillowpilfork", "Pillow (PIL Fork) Documentation", [author], 1) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -267,19 +286,40 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'PillowPILFork', u'Pillow (PIL Fork) Documentation', - author, 'PillowPILFork', 'Pillow is the friendly PIL fork by Alex Clark and Contributors.', - 'Miscellaneous'), + ( + master_doc, + "PillowPILFork", + "Pillow (PIL Fork) Documentation", + author, + "PillowPILFork", + "Pillow is the friendly PIL fork by Alex Clark and Contributors.", + "Miscellaneous", + ) ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False + + +def setup(app): + app.add_css_file("css/dark.css") + + +# GitHub repo for sphinx-issues +issues_github_path = "python-pillow/Pillow" + +# sphinxext.opengraph +ogp_image = ( + "https://raw.githubusercontent.com/python-pillow/pillow-logo/main/" + "pillow-logo-dark-text-1280x640.png" +) +ogp_image_alt = "Pillow" diff --git a/docs/deprecations.rst b/docs/deprecations.rst new file mode 100644 index 00000000000..dec652df88f --- /dev/null +++ b/docs/deprecations.rst @@ -0,0 +1,459 @@ +.. _deprecations: + +Deprecations and removals +========================= + +This page lists Pillow features that are deprecated, or have been removed in +past major releases, and gives the alternatives to use instead. + +Deprecated features +------------------- + +Below are features which are considered deprecated. Where appropriate, +a ``DeprecationWarning`` is issued. + +Tk/Tcl 8.4 +~~~~~~~~~~ + +.. deprecated:: 8.2.0 + +Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), +when Tk/Tcl 8.5 will be the minimum supported. + +Categories +~~~~~~~~~~ + +.. deprecated:: 8.2.0 + +``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), +along with the related ``Image.NORMAL``, ``Image.SEQUENCE`` and +``Image.CONTAINER`` attributes. + +To determine if an image has multiple frames or not, +``getattr(im, "is_animated", False)`` can be used instead. + +JpegImagePlugin.convert_dict_qtables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 8.3.0 + +JPEG ``quantization`` is now automatically converted, but still returned as a +dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no longer +performs any operations on the data given to it, has been deprecated and will be +removed in Pillow 10.0.0 (2023-07-01). + +ImagePalette size parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 8.4.0 + +The ``size`` parameter will be removed in Pillow 10.0.0 (2023-07-01). + +Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by +default, and the size parameter could be used to override that. Pillow 8.3.0 removed +the default required length, also removing the need for the size parameter. + +ImageShow.Viewer.show_file file argument +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.1.0 + +The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been +deprecated and will be removed in Pillow 10.0.0 (2023-07-01). It has been replaced by +``path``. + +In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. +``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest +``viewer.show_file(path="test.jpg")`` instead. + +Constants +~~~~~~~~~ + +.. deprecated:: 9.1.0 + +A number of constants have been deprecated and will be removed in Pillow 10.0.0 +(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. + +===================================================== ============================================================ +Deprecated Use instead +===================================================== ============================================================ +``Image.NONE`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` +``Image.NEAREST`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` +``Image.ORDERED`` ``Image.Dither.ORDERED`` +``Image.RASTERIZE`` ``Image.Dither.RASTERIZE`` +``Image.FLOYDSTEINBERG`` ``Image.Dither.FLOYDSTEINBERG`` +``Image.WEB`` ``Image.Palette.WEB`` +``Image.ADAPTIVE`` ``Image.Palette.ADAPTIVE`` +``Image.AFFINE`` ``Image.Transform.AFFINE`` +``Image.EXTENT`` ``Image.Transform.EXTENT`` +``Image.PERSPECTIVE`` ``Image.Transform.PERSPECTIVE`` +``Image.QUAD`` ``Image.Transform.QUAD`` +``Image.MESH`` ``Image.Transform.MESH`` +``Image.FLIP_LEFT_RIGHT`` ``Image.Transpose.FLIP_LEFT_RIGHT`` +``Image.FLIP_TOP_BOTTOM`` ``Image.Transpose.FLIP_TOP_BOTTOM`` +``Image.ROTATE_90`` ``Image.Transpose.ROTATE_90`` +``Image.ROTATE_180`` ``Image.Transpose.ROTATE_180`` +``Image.ROTATE_270`` ``Image.Transpose.ROTATE_270`` +``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE`` +``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE`` +``Image.BOX`` ``Image.Resampling.BOX`` +``Image.BILINEAR`` ``Image.Resampling.BILINEAR`` +``Image.LINEAR`` ``Image.Resampling.BILINEAR`` +``Image.HAMMING`` ``Image.Resampling.HAMMING`` +``Image.BICUBIC`` ``Image.Resampling.BICUBIC`` +``Image.CUBIC`` ``Image.Resampling.BICUBIC`` +``Image.LANCZOS`` ``Image.Resampling.LANCZOS`` +``Image.ANTIALIAS`` ``Image.Resampling.LANCZOS`` +``Image.MEDIANCUT`` ``Image.Quantize.MEDIANCUT`` +``Image.MAXCOVERAGE`` ``Image.Quantize.MAXCOVERAGE`` +``Image.FASTOCTREE`` ``Image.Quantize.FASTOCTREE`` +``Image.LIBIMAGEQUANT`` ``Image.Quantize.LIBIMAGEQUANT`` +``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` +``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` +``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` +``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` +``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` +``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` +``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` +``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` +``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` +``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` +``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` +``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` +``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` +``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` +``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` +``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` +``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` +``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` +===================================================== ============================================================ + +FitsStubImagePlugin +~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.1.0 + +The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in +Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through +:mod:`~PIL.FitsImagePlugin` instead. + +FreeTypeFont.getmask2 fill parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 + +The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been +deprecated and will be removed in Pillow 10 (2023-07-01). + +PhotoImage.paste box parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 + +The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01). + +PyQt5 and PySide2 +~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 + +`Qt 5 reached end-of-life `_ on 2020-12-08 for +open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). + +Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed +in Pillow 10 (2023-07-01). Upgrade to +`PyQt6 `_ or +`PySide6 `_ instead. + +Image.coerce_e +~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 + +This undocumented method has been deprecated and will be removed in Pillow 10 +(2023-07-01). + +.. _Font size and offset methods: + +Font size and offset methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 + +Several functions for computing the size and offset of rendered text +have been deprecated and will be removed in Pillow 10 (2023-07-01): + +=========================================================================== ============================================================================================================= +Deprecated Use instead +=========================================================================== ============================================================================================================= +:py:meth:`.FreeTypeFont.getsize` and :py:meth:`.FreeTypeFont.getoffset` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` +:py:meth:`.FreeTypeFont.getsize_multiline` :py:meth:`.ImageDraw.multiline_textbbox` +:py:meth:`.ImageFont.getsize` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` +:py:meth:`.TransposedFont.getsize` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` +:py:meth:`.ImageDraw.textsize` and :py:meth:`.ImageDraw.multiline_textsize` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` +:py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` +=========================================================================== ============================================================================================================= + +Previous code: + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + width, height = font.getsize("Hello world") + left, top = font.getoffset("Hello world") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width, height = draw.textsize("Hello world") + + width, height = font.getsize_multiline("Hello\nworld") + width, height = draw.multiline_textsize("Hello\nworld") + +Use instead: + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + left, top, right, bottom = font.getbbox("Hello world") + width, height = right - left, bottom - top + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width = draw.textlength("Hello world") + + left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld") + width, height = right - left, bottom - top + +Removed features +---------------- + +Deprecated features are only removed in major releases after an appropriate +period of deprecation has passed. + +PILLOW_VERSION constant +~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.2.0 +.. versionremoved:: 9.0.0 + +Use ``__version__`` instead. + +It was initially removed in Pillow 7.0.0, but temporarily brought back in 7.1.0 +to give projects more time to upgrade. + +Image.show command parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 7.2.0 +.. versionremoved:: 9.0.0 + +The ``command`` parameter has been removed. Use a subclass of +:py:class:`.ImageShow.Viewer` instead. + +Image._showxv +~~~~~~~~~~~~~ + +.. deprecated:: 7.2.0 +.. versionremoved:: 9.0.0 + +Use :py:meth:`.Image.Image.show` instead. If custom behaviour is required, use +:py:func:`.ImageShow.register` to add a custom :py:class:`.ImageShow.Viewer` class. + +ImageFile.raise_ioerror +~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 7.2.0 +.. versionremoved:: 9.0.0 + +``IOError`` was merged into ``OSError`` in Python 3.3. +So, ``ImageFile.raise_ioerror`` has been removed. +Use ``ImageFile.raise_oserror`` instead. + +FreeType 2.7 +~~~~~~~~~~~~ + +.. deprecated:: 8.1.0 +.. versionremoved:: 9.0.0 + +Support for FreeType 2.7 has been removed. + +We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). + +.. _FreeType: https://freetype.org/ + +im.offset +~~~~~~~~~ + +.. deprecated:: 1.1.2 +.. versionremoved:: 8.0.0 + +``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead. + +It was documented as deprecated in PIL 1.1.2, +raised a ``DeprecationWarning`` since 1.1.5, +an ``Exception`` since Pillow 3.0.0 +and ``NotImplementedError`` since 3.3.0. + +Image.fromstring, im.fromstring and im.tostring +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 2.0.0 +.. versionremoved:: 8.0.0 + +* ``Image.fromstring()`` has been removed, call :py:func:`.Image.frombytes()` instead. +* ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead. +* ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead. + +They issued a ``DeprecationWarning`` since 2.0.0, +an ``Exception`` since 3.0.0 +and ``NotImplementedError`` since 3.3.0. + +ImageCms.CmsProfile attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 3.2.0 +.. versionremoved:: 8.0.0 + +Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0, +they issued a ``DeprecationWarning``: + +======================== =================================================== +Removed Use instead +======================== =================================================== +``color_space`` Padded :py:attr:`~.CmsProfile.xcolor_space` +``pcs`` Padded :py:attr:`~.CmsProfile.connection_space` +``product_copyright`` Unicode :py:attr:`~.CmsProfile.copyright` +``product_desc`` Unicode :py:attr:`~.CmsProfile.profile_description` +``product_description`` Unicode :py:attr:`~.CmsProfile.profile_description` +``product_manufacturer`` Unicode :py:attr:`~.CmsProfile.manufacturer` +``product_model`` Unicode :py:attr:`~.CmsProfile.model` +======================== =================================================== + +Python 2.7 +~~~~~~~~~~ + +.. deprecated:: 6.0.0 +.. versionremoved:: 7.0.0 + +Python 2.7 reached end-of-life on 2020-01-01. Pillow 6.x was the last series to +support Python 2. + +Image.__del__ +~~~~~~~~~~~~~ + +.. deprecated:: 6.1.0 +.. versionremoved:: 7.0.0 + +Implicitly closing the image's underlying file in ``Image.__del__`` has been removed. +Use a context manager or call ``Image.close()`` instead to close the file in a +deterministic way. + +Previous method: + +.. code-block:: python + + im = Image.open("hopper.png") + im.save("out.jpg") + +Use instead: + +.. code-block:: python + + with Image.open("hopper.png") as im: + im.save("out.jpg") + +PIL.*ImagePlugin.__version__ attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 6.0.0 +.. versionremoved:: 7.0.0 + +The version constants of individual plugins have been removed. Use ``PIL.__version__`` +instead. + +=============================== ================================= ================================== +Removed Removed Removed +=============================== ================================= ================================== +``BmpImagePlugin.__version__`` ``Jpeg2KImagePlugin.__version__`` ``PngImagePlugin.__version__`` +``CurImagePlugin.__version__`` ``JpegImagePlugin.__version__`` ``PpmImagePlugin.__version__`` +``DcxImagePlugin.__version__`` ``McIdasImagePlugin.__version__`` ``PsdImagePlugin.__version__`` +``EpsImagePlugin.__version__`` ``MicImagePlugin.__version__`` ``SgiImagePlugin.__version__`` +``FliImagePlugin.__version__`` ``MpegImagePlugin.__version__`` ``SunImagePlugin.__version__`` +``FpxImagePlugin.__version__`` ``MpoImagePlugin.__version__`` ``TgaImagePlugin.__version__`` +``GdImageFile.__version__`` ``MspImagePlugin.__version__`` ``TiffImagePlugin.__version__`` +``GifImagePlugin.__version__`` ``PalmImagePlugin.__version__`` ``WmfImagePlugin.__version__`` +``IcoImagePlugin.__version__`` ``PcdImagePlugin.__version__`` ``XbmImagePlugin.__version__`` +``ImImagePlugin.__version__`` ``PcxImagePlugin.__version__`` ``XpmImagePlugin.__version__`` +``ImtImagePlugin.__version__`` ``PdfImagePlugin.__version__`` ``XVThumbImagePlugin.__version__`` +``IptcImagePlugin.__version__`` ``PixarImagePlugin.__version__`` +=============================== ================================= ================================== + +PyQt4 and PySide +~~~~~~~~~~~~~~~~ + +.. deprecated:: 6.0.0 +.. versionremoved:: 7.0.0 + +Qt 4 reached end-of-life on 2015-12-19. Its Python bindings are also EOL: PyQt4 since +2018-08-31 and PySide since 2015-10-14. + +Support for PyQt4 and PySide has been removed from ``ImageQt``. Please upgrade to PyQt5 +or PySide2. + +Setting the size of TIFF images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.3.0 +.. versionremoved:: 7.0.0 + +Setting the size of a TIFF image directly (eg. ``im.size = (256, 256)``) throws +an error. Use ``Image.resize`` instead. + +VERSION constant +~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.2.0 +.. versionremoved:: 6.0.0 + +``VERSION`` (the old PIL version, always 1.1.7) has been removed. Use +``__version__`` instead. + +Undocumented ImageOps functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 4.3.0 +.. versionremoved:: 6.0.0 + +Several undocumented functions in ``ImageOps`` have been removed. Use the equivalents +in ``ImageFilter`` instead: + +========================== ============================ +Removed Use instead +========================== ============================ +``ImageOps.box_blur`` ``ImageFilter.BoxBlur`` +``ImageOps.gaussian_blur`` ``ImageFilter.GaussianBlur`` +``ImageOps.gblur`` ``ImageFilter.GaussianBlur`` +``ImageOps.usm`` ``ImageFilter.UnsharpMask`` +``ImageOps.unsharp_mask`` ``ImageFilter.UnsharpMask`` +========================== ============================ + +PIL.OleFileIO +~~~~~~~~~~~~~ + +.. deprecated:: 4.0.0 +.. versionremoved:: 6.0.0 + +PIL.OleFileIO was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of +the upstream olefile Python package, and replaced with an ``ImportError`` in 5.0.0 +(2018-01). The deprecated file has now been removed from Pillow. If needed, install from +PyPI (eg. ``python3 -m pip install olefile``). diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 29e13b9207a..ec3938b367f 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -3,7 +3,7 @@ Jerome Leclanche Documentation: - http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt + https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt The contents of this file are hereby released in the public domain (CC0) Full text of the CC0 license: @@ -12,8 +12,8 @@ import struct from io import BytesIO -from PIL import Image, ImageFile +from PIL import Image, ImageFile # Magic ("DDS ") DDS_MAGIC = 0x20534444 @@ -61,8 +61,7 @@ DDS_ALPHA = DDPF_ALPHA DDS_PAL8 = DDPF_PALETTEINDEXED8 -DDS_HEADER_FLAGS_TEXTURE = (DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | - DDSD_PIXELFORMAT) +DDS_HEADER_FLAGS_TEXTURE = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT DDS_HEADER_FLAGS_MIPMAP = DDSD_MIPMAPCOUNT DDS_HEADER_FLAGS_VOLUME = DDSD_DEPTH DDS_HEADER_FLAGS_PITCH = DDSD_PITCH @@ -94,9 +93,9 @@ def _decode565(bits): - a = ((bits >> 11) & 0x1f) << 3 - b = ((bits >> 5) & 0x3f) << 2 - c = (bits & 0x1f) << 3 + a = ((bits >> 11) & 0x1F) << 3 + b = ((bits >> 5) & 0x3F) << 2 + c = (bits & 0x1F) << 3 return a, b, c @@ -145,7 +144,7 @@ def _dxt1(data, width, height): r, g, b = 0, 0, 0 idx = 4 * ((y + j) * width + x + i) - ret[idx:idx+4] = struct.pack('4B', r, g, b, 255) + ret[idx : idx + 4] = struct.pack("4B", r, g, b, 255) return bytes(ret) @@ -167,7 +166,7 @@ def _dxtc_alpha(a0, a1, ac0, ac1, ai): elif ac == 6: alpha = 0 elif ac == 7: - alpha = 0xff + alpha = 0xFF else: alpha = ((6 - ac) * a0 + (ac - 1) * a1) // 5 @@ -180,8 +179,7 @@ def _dxt5(data, width, height): for y in range(0, height, 4): for x in range(0, width, 4): - a0, a1, ac0, ac1, c0, c1, code = struct.unpack("<2BHI2HI", - data.read(16)) + a0, a1, ac0, ac1, c0, c1, code = struct.unpack("<2BHI2HI", data.read(16)) r0, g0, b0 = _decode565(c0) r1, g1, b1 = _decode565(c1) @@ -202,7 +200,7 @@ def _dxt5(data, width, height): r, g, b = _c3(r0, r1), _c3(g0, g1), _c3(b0, b1) idx = 4 * ((y + j) * width + x + i) - ret[idx:idx+4] = struct.pack('4B', r, g, b, alpha) + ret[idx : idx + 4] = struct.pack("4B", r, g, b, alpha) return bytes(ret) @@ -212,39 +210,36 @@ class DdsImageFile(ImageFile.ImageFile): format_description = "DirectDraw Surface" def _open(self): - magic, header_size = struct.unpack("` for details. + +.. _apng-sequences: + +APNG sequences +~~~~~~~~~~~~~~ + +The PNG loader includes limited support for reading and writing Animated Portable +Network Graphics (APNG) files. +When an APNG file is loaded, :py:meth:`~PIL.ImageFile.ImageFile.get_format_mimetype` +will return ``"image/apng"``. The value of the :py:attr:`~PIL.Image.Image.is_animated` +property will be ``True`` when the :py:attr:`~PIL.Image.Image.n_frames` property is +greater than 1. For APNG files, the ``n_frames`` property depends on both the animation +frame count as well as the presence or absence of a default image. See the +``default_image`` property documentation below for more details. +The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods +are supported. + +``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the last frame. + +These :py:attr:`~PIL.Image.Image.info` properties will be set for APNG frames, +where applicable: + +**default_image** + Specifies whether or not this APNG file contains a separate default image, + which is not a part of the actual APNG animation. + + When an APNG file contains a default image, the initially loaded image (i.e. + the result of ``seek(0)``) will be the default image. + To account for the presence of the default image, the + :py:attr:`~PIL.Image.Image.n_frames` property will be set to ``frame_count + 1``, + where ``frame_count`` is the actual APNG animation frame count. + To load the first APNG animation frame, ``seek(1)`` must be called. + + * ``True`` - The APNG contains default image, which is not an animation frame. + * ``False`` - The APNG does not contain a default image. The ``n_frames`` property + will be set to the actual APNG animation frame count. + The initially loaded image (i.e. ``seek(0)``) will be the first APNG animation + frame. + +**loop** + The number of times to loop this APNG, 0 indicates infinite looping. + +**duration** + The time to display this APNG frame (in milliseconds). + +.. note:: + + The APNG loader returns images the same size as the APNG file's logical screen size. + The returned image contains the pixel data for a given frame, after applying + any APNG frame disposal and frame blend operations (i.e. it contains what a web + browser would render for this frame - the composite of all previous frames and this + frame). + + Any APNG file containing sequence errors is treated as an invalid image. The APNG + loader will not attempt to repair and reorder files containing sequence errors. + +.. _apng-saving: + +Saving +~~~~~~ + +When calling :py:meth:`~PIL.Image.Image.save`, by default only a single frame PNG file +will be saved. To save an APNG file (including a single frame APNG), the ``save_all`` +parameter must be set to ``True``. The following parameters can also be set: + +**default_image** + Boolean value, specifying whether or not the base image is a default image. + If ``True``, the base image will be used as the default image, and the first image + from the ``append_images`` sequence will be the first APNG animation frame. + If ``False``, the base image will be used as the first APNG animation frame. + Defaults to ``False``. + +**append_images** + A list or tuple of images to append as additional frames. Each of the + images in the list can be single or multiframe images. The size of each frame + should match the size of the base image. Also note that if a frame's mode does + not match that of the base image, the frame will be converted to the base image + mode. + +**loop** + Integer number of times to loop this APNG, 0 indicates infinite looping. + Defaults to 0. + +**duration** + Integer (or list or tuple of integers) length of time to display this APNG frame + (in milliseconds). + Defaults to 0. + +**disposal** + An integer (or list or tuple of integers) specifying the APNG disposal + operation to be used for this frame before rendering the next frame. + Defaults to 0. + + * 0 (:py:data:`~PIL.PngImagePlugin.Disposal.OP_NONE`, default) - + No disposal is done on this frame before rendering the next frame. + * 1 (:py:data:`PIL.PngImagePlugin.Disposal.OP_BACKGROUND`) - + This frame's modified region is cleared to fully transparent black before + rendering the next frame. + * 2 (:py:data:`~PIL.PngImagePlugin.Disposal.OP_PREVIOUS`) - + This frame's modified region is reverted to the previous frame's contents before + rendering the next frame. + +**blend** + An integer (or list or tuple of integers) specifying the APNG blend + operation to be used for this frame before rendering the next frame. + Defaults to 0. + + * 0 (:py:data:`~PIL.PngImagePlugin.Blend.OP_SOURCE`) - + All color components of this frame, including alpha, overwrite the previous output + image contents. + * 1 (:py:data:`~PIL.PngImagePlugin.Blend.OP_OVER`) - + This frame should be alpha composited with the previous output image contents. + +.. note:: + + The ``duration``, ``disposal`` and ``blend`` parameters can be set to lists or tuples to + specify values for each individual frame in the animation. The length of the list or tuple + must be identical to the total number of actual frames in the APNG animation. + If the APNG contains a default image (i.e. ``default_image`` is set to ``True``), + these list or tuple parameters should not include an entry for the default image. + PPM ^^^ -PIL reads and writes PBM, PGM and PPM files containing ``1``, ``L`` or ``RGB`` -data. +Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or +``RGB`` data. SGI ^^^ @@ -509,14 +847,19 @@ Pillow reads and writes uncompressed ``L``, ``RGB``, and ``RGBA`` files. SPIDER ^^^^^^ -PIL reads and writes SPIDER image files of 32-bit floating point data +Pillow reads and writes SPIDER image files of 32-bit floating point data ("F;32F"). -PIL also reads SPIDER stack files containing sequences of SPIDER images. The -:py:meth:`~file.seek` and :py:meth:`~file.tell` methods are supported, and +Pillow also reads SPIDER stack files containing sequences of SPIDER images. The +:py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods are supported, and random access is allowed. -The :py:meth:`~PIL.Image.Image.open` method sets the following attributes: +.. _spider-opening: + +Opening +~~~~~~~ + +The :py:meth:`~PIL.Image.open` method sets the following attributes: **format** Set to ``SPIDER`` @@ -524,35 +867,64 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following attributes: **istack** Set to 1 if the file is an image stack, else 0. -**nimages** +**n_frames** Set to the number of images in the stack. -A convenience method, :py:meth:`~PIL.Image.Image.convert2byte`, is provided for -converting floating point data to byte data (mode ``L``):: +A convenience method, :py:meth:`~PIL.SpiderImagePlugin.SpiderImageFile.convert2byte`, +is provided for converting floating point data to byte data (mode ``L``):: + + im = Image.open("image001.spi").convert2byte() - im = Image.open('image001.spi').convert2byte() +.. _spider-saving: -Writing files in SPIDER format -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Saving +~~~~~~ The extension of SPIDER files may be any 3 alphanumeric characters. Therefore the output format must be specified explicitly:: im.save('newimage.spi', format='SPIDER') -For more information about the SPIDER image processing package, see the -`SPIDER homepage`_ at `Wadsworth Center`_. +For more information about the SPIDER image processing package, see +https://github.com/spider-em/SPIDER + +TGA +^^^ -.. _SPIDER homepage: https://spider.wadsworth.org/spider_doc/spider/docs/spider.html -.. _Wadsworth Center: https://www.wadsworth.org/ +Pillow reads and writes TGA images containing ``L``, ``LA``, ``P``, +``RGB``, and ``RGBA`` data. Pillow can read and write both uncompressed and +run-length encoded TGAs. + +.. _tga-saving: + +Saving +~~~~~~ + +The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: + +**compression** + If set to "tga_rle", the file will be run-length encoded. + + .. versionadded:: 5.3.0 + +**id_section** + The identification field. + + .. versionadded:: 5.3.0 + +**orientation** + If present and a positive number, the first pixel is for the top left corner, + rather than the bottom left corner. + + .. versionadded:: 5.3.0 TIFF ^^^^ Pillow reads and writes TIFF files. It can read both striped and tiled images, pixel and plane interleaved multi-band images. If you have -libtiff and its headers installed, PIL can read and write many kinds -of compressed TIFF files. If not, PIL will only read and write +libtiff and its headers installed, Pillow can read and write many kinds +of compressed TIFF files. If not, Pillow will only read and write uncompressed files. .. note:: @@ -562,7 +934,12 @@ uncompressed files. support for reading Packbits, LZW and JPEG compressed TIFFs without using libtiff. -The :py:meth:`~PIL.Image.Image.open` method sets the following +.. _tiff-opening: + +Opening +~~~~~~~ + +The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **compression** @@ -572,8 +949,8 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following **dpi** Image resolution as an ``(xdpi, ydpi)`` tuple, where applicable. You can use - the :py:attr:`~PIL.Image.Image.tag` attribute to get more detailed - information about the image resolution. + the :py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag` attribute to get more + detailed information about the image resolution. .. versionadded:: 1.1.5 @@ -584,9 +961,9 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following .. versionadded:: 1.1.5 -The :py:attr:`~PIL.Image.Image.tag_v2` attribute contains a dictionary -of TIFF metadata. The keys are numerical indexes from -:py:attr:`~PIL.TiffTags.TAGS_V2`. Values are strings or numbers for single +The :py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag_v2` attribute contains a +dictionary of TIFF metadata. The keys are numerical indexes from +:py:data:`.TiffTags.TAGS_V2`. Values are strings or numbers for single items, multiple values are returned in a tuple of values. Rational numbers are returned as a :py:class:`~PIL.TiffImagePlugin.IFDRational` object. @@ -594,16 +971,29 @@ object. .. versionadded:: 3.0.0 For compatibility with legacy code, the -:py:attr:`~PIL.Image.Image.tag` attribute contains a dictionary of -decoded TIFF fields as returned prior to version 3.0.0. Values are +:py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag` attribute contains a dictionary +of decoded TIFF fields as returned prior to version 3.0.0. Values are returned as either strings or tuples of numeric values. Rational numbers are returned as a tuple of ``(numerator, denominator)``. .. deprecated:: 3.0.0 +Reading Multi-frame TIFF Images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Saving Tiff Images -~~~~~~~~~~~~~~~~~~ +The TIFF loader supports the :py:meth:`~PIL.Image.Image.seek` and +:py:meth:`~PIL.Image.Image.tell` methods, taking and returning frame numbers +within the image file. You can combine these methods to seek to the next frame +(``im.seek(im.tell() + 1)``). Frames are numbered from 0 to ``im.n_frames - 1``, +and can be accessed in any order. + +``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the +last frame. + +.. _tiff-saving: + +Saving +~~~~~~ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: @@ -612,6 +1002,14 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 3.4.0 +**append_images** + A list of images to append as additional frames. Each of the + images in the list can be single or multiframe images. Note however, that for + correct results, all the appended images should have the same + ``encoderinfo`` and ``encoderconfig`` properties. + + .. versionadded:: 4.2.0 + **tiffinfo** A :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` object or dict object containing tiff tags and values. The TIFF field type is @@ -620,7 +1018,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum object and setting the type in :py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype` with the appropriate numerical value from - ``TiffTags.TYPES``. + :py:data:`.TiffTags.TYPES`. .. versionadded:: 2.3.0 @@ -633,21 +1031,38 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` object may be passed in this field. However, this is deprecated. - .. versionadded:: 3.0.0 - - .. note:: + .. versionadded:: 5.4.0 - Only some tags are currently supported when writing using + Previous versions only supported some tags when writing using libtiff. The supported list is found in - :py:attr:`~PIL:TiffTags.LIBTIFF_CORE`. + :py:data:`.TiffTags.LIBTIFF_CORE`. + + .. versionadded:: 6.1.0 + + Added support for signed types (e.g. ``TIFF_SIGNED_LONG``) and multiple values. + Multiple values for a single tag must be to + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` as a tuple and + require a matching type in + :py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype` tagtype. + +**exif** + Alternate keyword to "tiffinfo", for consistency with other formats. + + .. versionadded:: 8.4.0 **compression** A string containing the desired compression method for the file. (valid only with libtiff installed) Valid compression - methods are: ``None``, ``"tiff_ccitt"``, ``"group3"``, - ``"group4"``, ``"tiff_jpeg"``, ``"tiff_adobe_deflate"``, - ``"tiff_thunderscan"``, ``"tiff_deflate"``, ``"tiff_sgilog"``, - ``"tiff_sgilog24"``, ``"tiff_raw_16"`` + methods are: :data:`None`, ``"group3"``, ``"group4"``, ``"jpeg"``, ``"lzma"``, + ``"packbits"``, ``"tiff_adobe_deflate"``, ``"tiff_ccitt"``, ``"tiff_lzw"``, + ``"tiff_raw_16"``, ``"tiff_sgilog"``, ``"tiff_sgilog24"``, ``"tiff_thunderscan"``, + ``"webp"``, ``"zstd"`` + +**quality** + The image quality for JPEG compression, on a scale from 0 (worst) to 100 + (best). The default is 75. + + .. versionadded:: 6.1.0 These arguments to set the tiff header fields are an alternative to using the general tags available through tiffinfo. @@ -663,26 +1078,37 @@ using the general tags available through tiffinfo. **copyright** Strings +**icc_profile** + The ICC Profile to include in the saved file. + **resolution_unit** - A string of "inch", "centimeter" or "cm" + An integer. 1 for no unit, 2 for inches and 3 for centimeters. **resolution** + Either an integer or a float, used for both the x and y resolution. **x_resolution** + Either an integer or a float. **y_resolution** + Either an integer or a float. **dpi** - Either a Float, 2 tuple of (numerator, denominator) or a - :py:class:`~PIL.TiffImagePlugin.IFDRational`. Resolution implies - an equal x and y resolution, dpi also implies a unit of inches. + A tuple of (x_resolution, y_resolution), with inches as the resolution + unit. For consistency with other image formats, the x and y resolutions + of the dpi will be rounded to the nearest integer. WebP ^^^^ -PIL reads and writes WebP files. The specifics of PIL's capabilities with this -format are currently undocumented. +Pillow reads and writes WebP files. The specifics of Pillow's capabilities with +this format are currently undocumented. + +.. _webp-saving: + +Saving +~~~~~~ The :py:meth:`~PIL.Image.Image.save` method supports the following options: @@ -696,9 +1122,9 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: files compared to the slowest, but best, 100. **method** - Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 0. + Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4. -**icc_procfile** +**icc_profile** The ICC Profile to include in the saved file. Only supported if the system WebP library was built with webpmux support. @@ -707,16 +1133,18 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: the system WebP library was built with webpmux support. Saving sequences -~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~ .. note:: Support for animated WebP files will only be enabled if the system WebP library is v0.5.0 or later. You can check webp animation support at - runtime by calling `features.check("webp_anim")`. + runtime by calling ``features.check("webp_anim")``. -When calling :py:meth:`~PIL.Image.Image.save`, the following options -are available when the `save_all` argument is present and true. +When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default +only the first frame of a multiframe image will be saved. If the ``save_all`` +argument is present and true, then all frames will be saved, and the following +options will also be available. **append_images** A list of images to append as additional frames. Each of the @@ -754,7 +1182,7 @@ are available when the `save_all` argument is present and true. XBM ^^^ -PIL reads and writes X bitmap files (mode ``1``). +Pillow reads and writes X bitmap files (mode ``1``). Read-only formats ----------------- @@ -773,25 +1201,21 @@ is commonly used in fax applications. The DCX decoder can read files containing ``1``, ``L``, ``P``, or ``RGB`` data. When the file is opened, only the first image is read. You can use -:py:meth:`~file.seek` or :py:mod:`~PIL.ImageSequence` to read other images. +:py:meth:`~PIL.Image.Image.seek` or :py:mod:`~PIL.ImageSequence` to read other images. +FITS +^^^^ -DDS -^^^ - -DDS is a popular container texture format used in video games and natively -supported by DirectX. -Currently, DXT1, DXT3, and DXT5 pixel formats are supported and only in ``RGBA`` -mode. +.. versionadded:: 9.1.0 -.. versionadded:: 3.4.0 DXT3 +Pillow identifies and reads FITS files, commonly used for astronomy. FLI, FLC ^^^^^^^^ -PIL reads Autodesk FLI and FLC animations. +Pillow reads Autodesk FLI and FLC animations. -The :py:meth:`~PIL.Image.Image.open` method sets the following +The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **duration** @@ -800,7 +1224,7 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following FPX ^^^ -PIL reads Kodak FlashPix files. In the current version, only the highest +Pillow reads Kodak FlashPix files. In the current version, only the highest resolution image is read from the file, and the viewing transform is not taken into account. @@ -824,7 +1248,12 @@ GBR The GBR decoder reads GIMP brush files, version 1 and 2. -The :py:meth:`~PIL.Image.Image.open` method sets the following +.. _gbr-opening: + +Opening +~~~~~~~ + +The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **comment** @@ -836,11 +1265,15 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following GD ^^ -PIL reads uncompressed GD files. Note that this file format cannot be -automatically identified, so you must use :py:func:`PIL.GdImageFile.open` to -read such a file. +Pillow reads uncompressed GD2 files. Note that you must use +:py:func:`PIL.GdImageFile.open` to read such a file. -The :py:meth:`~PIL.Image.Image.open` method sets the following +.. _gd-opening: + +Opening +~~~~~~~ + +The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **transparency** @@ -850,24 +1283,24 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following IMT ^^^ -PIL reads Image Tools images containing ``L`` data. +Pillow reads Image Tools images containing ``L`` data. IPTC/NAA ^^^^^^^^ -PIL provides limited read support for IPTC/NAA newsphoto files. +Pillow provides limited read support for IPTC/NAA newsphoto files. MCIDAS ^^^^^^ -PIL identifies and reads 8-bit McIdas area files. +Pillow identifies and reads 8-bit McIdas area files. MIC ^^^ -PIL identifies and reads Microsoft Image Composer (MIC) files. When opened, the -first sprite in the file is loaded. You can use :py:meth:`~file.seek` and -:py:meth:`~file.tell` to read other sprites from the file. +Pillow identifies and reads Microsoft Image Composer (MIC) files. When opened, +the first sprite in the file is loaded. You can use :py:meth:`~PIL.Image.Image.seek` and +:py:meth:`~PIL.Image.Image.tell` to read other sprites from the file. Note that there may be an embedded gamma of 2.2 in MIC files. @@ -875,42 +1308,58 @@ MPO ^^^ Pillow identifies and reads Multi Picture Object (MPO) files, loading the primary -image when first opened. The :py:meth:`~file.seek` and :py:meth:`~file.tell` +image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods may be used to read other pictures from the file. The pictures are zero-indexed and random access is supported. +.. _mpo-saving: + +Saving +~~~~~~ + +When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default +only the first frame of a multiframe image will be saved. If the ``save_all`` +argument is present and true, then all frames will be saved, and the following +option will also be available. + +**append_images** + A list of images to append as additional pictures. Each of the + images in the list can be single or multiframe images. + + .. versionadded:: 9.3.0 + PCD ^^^ -PIL reads PhotoCD files containing ``RGB`` data. This only reads the 768x512 +Pillow reads PhotoCD files containing ``RGB`` data. This only reads the 768x512 resolution image from the file. Higher resolutions are encoded in a proprietary encoding. PIXAR ^^^^^ -PIL provides limited support for PIXAR raster files. The library can identify -and read “dumped” RGB files. +Pillow provides limited support for PIXAR raster files. The library can +identify and read “dumped” RGB files. The format code is ``PIXAR``. PSD ^^^ -PIL identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0. +Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0. -TGA +SUN ^^^ -PIL reads 24- and 32-bit uncompressed and run-length encoded TGA files. +Pillow identifies and reads Sun raster files. WAL ^^^ .. versionadded:: 1.1.4 -PIL reads Quake2 WAL texture files. +Pillow reads Quake2 WAL texture files. Note that this file format cannot be automatically identified, so you must use the open function in the :py:mod:`~PIL.WalImageFile` module to read files in @@ -919,12 +1368,60 @@ this format. By default, a Quake2 standard palette is attached to the texture. To override the palette, use the putpalette method. +WMF, EMF +^^^^^^^^ + +Pillow can identify WMF and EMF files. + +On Windows, it can read WMF and EMF files. By default, it will load the image +at 72 dpi. To load it at another resolution: + +.. code-block:: python + + from PIL import Image + + with Image.open("drawing.wmf") as im: + im.load(dpi=144) + +To add other read or write support, use +:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF +handler. + +.. code-block:: python + + from PIL import Image + from PIL import WmfImagePlugin + + + class WmfHandler: + def open(self, im): + ... + + def load(self, im): + ... + return image + + def save(self, im, fp, filename): + ... + + + wmf_handler = WmfHandler() + + WmfImagePlugin.register_handler(wmf_handler) + + im = Image.open("sample.wmf") + XPM ^^^ -PIL reads X pixmap files (mode ``P``) with 256 colors or less. +Pillow reads X pixmap files (mode ``P``) with 256 colors or less. + +.. _xpm-opening: -The :py:meth:`~PIL.Image.Image.open` method sets the following +Opening +~~~~~~~ + +The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **transparency** @@ -937,26 +1434,100 @@ Write-only formats PALM ^^^^ -PIL provides write-only support for PALM pixmap files. +Pillow provides write-only support for PALM pixmap files. The format code is ``Palm``, the extension is ``.palm``. PDF ^^^ -PIL can write PDF (Acrobat) images. Such images are written as binary PDF 1.1 +Pillow can write PDF (Acrobat) images. Such images are written as binary PDF 1.4 files, using either JPEG or HEX encoding depending on the image mode (and whether JPEG support is available or not). -When calling :py:meth:`~PIL.Image.Image.save`, if a multiframe image is used, -by default, only the first image will be saved. To save all frames, each frame -to a separate page of the PDF, the ``save_all`` parameter must be present and -set to ``True``. +.. _pdf-saving: + +Saving +~~~~~~ + +The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: + +**save_all** + If a multiframe image is used, by default, only the first image will be saved. + To save all frames, each frame to a separate page of the PDF, the ``save_all`` + parameter must be present and set to ``True``. + + .. versionadded:: 3.0.0 + +**append_images** + A list of :py:class:`PIL.Image.Image` objects to append as additional pages. Each + of the images in the list can be single or multiframe images. The ``save_all`` + parameter must be present and set to ``True`` in conjunction with + ``append_images``. + + .. versionadded:: 4.2.0 + +**append** + Set to True to append pages to an existing PDF file. If the file doesn't + exist, an :py:exc:`OSError` will be raised. + + .. versionadded:: 5.1.0 + +**resolution** + Image resolution in DPI. This, together with the number of pixels in the + image, will determine the physical dimensions of the page that will be + saved in the PDF. + +**title** + The document’s title. If not appending to an existing PDF file, this will + default to the filename. + + .. versionadded:: 5.1.0 + +**author** + The name of the person who created the document. + + .. versionadded:: 5.1.0 + +**subject** + The subject of the document. + + .. versionadded:: 5.1.0 + +**keywords** + Keywords associated with the document. + + .. versionadded:: 5.1.0 + +**creator** + If the document was converted to PDF from another format, the name of the + conforming product that created the original document from which it was + converted. + + .. versionadded:: 5.1.0 + +**producer** + If the document was converted to PDF from another format, the name of the + conforming product that converted it to PDF. + + .. versionadded:: 5.1.0 + +**creationDate** + The creation date of the document. If not appending to an existing PDF + file, this will default to the current time. + + .. versionadded:: 5.3.0 + +**modDate** + The modification date of the document. If not appending to an existing PDF + file, this will default to the current time. + + .. versionadded:: 5.3.0 XV Thumbnails ^^^^^^^^^^^^^ -PIL can read XV thumbnail files. +Pillow can read XV thumbnail files. Identify-only formats --------------------- @@ -966,31 +1537,21 @@ BUFR .. versionadded:: 1.1.3 -PIL provides a stub driver for BUFR files. +Pillow provides a stub driver for BUFR files. To add read or write support to your application, use :py:func:`PIL.BufrStubImagePlugin.register_handler`. -FITS -^^^^ - -.. versionadded:: 1.1.5 - -PIL provides a stub driver for FITS files. - -To add read or write support to your application, use -:py:func:`PIL.FitsStubImagePlugin.register_handler`. - GRIB ^^^^ .. versionadded:: 1.1.5 -PIL provides a stub driver for GRIB files. +Pillow provides a stub driver for GRIB files. The driver requires the file to start with a GRIB header. If you have files with embedded GRIB data, or files with multiple GRIB fields, your application -has to seek to the header before passing the file handle to PIL. +has to seek to the header before passing the file handle to Pillow. To add read or write support to your application, use :py:func:`PIL.GribStubImagePlugin.register_handler`. @@ -1000,7 +1561,7 @@ HDF5 .. versionadded:: 1.1.5 -PIL provides a stub driver for HDF5 files. +Pillow provides a stub driver for HDF5 files. To add read or write support to your application, use :py:func:`PIL.Hdf5StubImagePlugin.register_handler`. @@ -1008,36 +1569,4 @@ To add read or write support to your application, use MPEG ^^^^ -PIL identifies MPEG files. - -WMF -^^^ - -PIL can identify playable WMF files. - -In PIL 1.1.4 and earlier, the WMF driver provides some limited rendering -support, but not enough to be useful for any real application. - -In PIL 1.1.5 and later, the WMF driver is a stub driver. To add WMF read or -write support to your application, use -:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF handler. - -:: - - from PIL import Image - from PIL import WmfImagePlugin - - class WmfHandler: - def open(self, im): - ... - def load(self, im): - ... - return image - def save(self, im, fp, filename): - ... - - wmf_handler = WmfHandler() - - WmfImagePlugin.register_handler(wmf_handler) - - im = Image.open("sample.wmf") +Pillow identifies MPEG files. diff --git a/docs/handbook/overview.rst b/docs/handbook/overview.rst index b52939b89c6..17964d1c5f3 100644 --- a/docs/handbook/overview.rst +++ b/docs/handbook/overview.rst @@ -33,7 +33,7 @@ DIB interface ` that can be used with PythonWin and other Windows-based toolkits. Many other GUI toolkits come with some kind of PIL support. -For debugging, there’s also a :py:meth:`show` method which saves an image to +For debugging, there’s also a :py:meth:`~PIL.Image.Image.show` method which saves an image to disk, and calls an external display utility. Image Processing diff --git a/docs/handbook/text-anchors.rst b/docs/handbook/text-anchors.rst new file mode 100644 index 00000000000..0aecd348366 --- /dev/null +++ b/docs/handbook/text-anchors.rst @@ -0,0 +1,140 @@ + +.. _text-anchors: + +Text anchors +============ + +The ``anchor`` parameter determines the alignment of drawn text relative to the ``xy`` parameter. +The default alignment is top left, specifically ``la`` (left-ascender) for horizontal text +and ``lt`` (left-top) for vertical text. + +This parameter is only supported by OpenType/TrueType fonts. +Other fonts may ignore the parameter and use the default (top left) alignment. + +Specifying an anchor +^^^^^^^^^^^^^^^^^^^^ + +An anchor is specified with a two-character string. The first character is the +horizontal alignment, the second character is the vertical alignment. +For example, the default value of ``la`` for horizontal text means left-ascender +aligned text. + +When drawing text with :py:meth:`PIL.ImageDraw.ImageDraw.text` with a specific anchor, +the text will be placed such that the specified anchor point is at the ``xy`` coordinates. + +For example, in the following image, the text is ``ms`` (middle-baseline) aligned, with +``xy`` at the intersection of the two lines: + +.. image:: ../../Tests/images/test_anchor_quick_ms.png + :alt: ms (middle-baseline) aligned text. + :align: left + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) + im = Image.new("RGB", (200, 200), "white") + d = ImageDraw.Draw(im) + d.line(((0, 100), (200, 100)), "gray") + d.line(((100, 0), (100, 200)), "gray") + d.text((100, 100), "Quick", fill="black", anchor="ms", font=font) + +.. container:: clearer + + | + +.. only: comment + The container above prevents the image alignment from affecting the following text. + +Quick reference +^^^^^^^^^^^^^^^ + +.. image:: ../resources/anchor_horizontal.svg + :alt: Horizontal text + :align: center + +.. image:: ../resources/anchor_vertical.svg + :alt: Vertical text + :align: center + +Horizontal anchor alignment +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``l`` --- left + Anchor is to the left of the text. + + For *horizontal* text this is the origin of the first glyph, as shown in the `FreeType tutorial`_. + +``m`` --- middle + Anchor is horizontally centered with the text. + + For *vertical* text it is recommended to use ``s`` (baseline) alignment instead, + as it does not change based on the specific glyphs of the given text. + +``r`` --- right + Anchor is to the right of the text. + + For *horizontal* text this is the advanced origin of the last glyph, as shown in the `FreeType tutorial`_. + +``s`` --- baseline *(vertical text only)* + Anchor is at the baseline (middle) of the text. The exact alignment depends on the font. + + For *vertical* text this is the recommended alignment, + as it does not change based on the specific glyphs of the given text + (see image for vertical text above). + +Vertical anchor alignment +^^^^^^^^^^^^^^^^^^^^^^^^^ + +``a`` --- ascender / top *(horizontal text only)* + Anchor is at the ascender line (top) of the first line of text, as defined by the font. + + See `Font metrics on Wikipedia`_ for more information. + +``t`` --- top *(single-line text only)* + Anchor is at the top of the text. + + For *vertical* text this is the origin of the first glyph, as shown in the `FreeType tutorial`_. + + For *horizontal* text it is recommended to use ``a`` (ascender) alignment instead, + as it does not change based on the specific glyphs of the given text. + +``m`` --- middle + Anchor is vertically centered with the text. + + For *horizontal* text this is the midpoint of the first ascender line and the last descender line. + +``s`` --- baseline *(horizontal text only)* + Anchor is at the baseline (bottom) of the first line of text, only descenders extend below the anchor. + + See `Font metrics on Wikipedia`_ for more information. + +``b`` --- bottom *(single-line text only)* + Anchor is at the bottom of the text. + + For *vertical* text this is the advanced origin of the last glyph, as shown in the `FreeType tutorial`_. + + For *horizontal* text it is recommended to use ``d`` (descender) alignment instead, + as it does not change based on the specific glyphs of the given text. + +``d`` --- descender / bottom *(horizontal text only)* + Anchor is at the descender line (bottom) of the last line of text, as defined by the font. + + See `Font metrics on Wikipedia`_ for more information. + +Examples +^^^^^^^^ + +The following image shows several examples of anchors for horizontal text. +In each section the ``xy`` parameter was set to the center shown by the intersection +of the two lines. + +.. comment: Image generated with ../example/anchors.py + +.. image:: ../example/anchors.png + :alt: Text anchor examples + :align: center + +.. _Font metrics on Wikipedia: https://en.wikipedia.org/wiki/Typeface#Font_metrics +.. _FreeType tutorial: https://freetype.org/freetype2/docs/tutorial/step2.html diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index e822f5a084b..50133f15ec2 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -18,7 +18,6 @@ in the :py:mod:`~PIL.Image` module:: If successful, this function returns an :py:class:`~PIL.Image.Image` object. You can now use instance attributes to examine the file contents:: - >>> from __future__ import print_function >>> print(im.format, im.size, im.mode) PPM (512, 512) RGB @@ -30,7 +29,7 @@ bands in the image, and also the pixel type and depth. Common modes are “L” (luminance) for greyscale images, “RGB” for true color images, and “CMYK” for pre-press images. -If the file cannot be opened, an :py:exc:`IOError` exception is raised. +If the file cannot be opened, an :py:exc:`OSError` exception is raised. Once you have an instance of the :py:class:`~PIL.Image.Image` class, you can use the methods defined by this class to process and manipulate the image. For @@ -41,10 +40,10 @@ example, let’s display the image we just loaded:: .. note:: The standard version of :py:meth:`~PIL.Image.Image.show` is not very - efficient, since it saves the image to a temporary file and calls the - :command:`xv` utility to display the image. If you don’t have :command:`xv` - installed, it won’t even work. When it does work though, it is very handy - for debugging and tests. + efficient, since it saves the image to a temporary file and calls a utility + to display the image. If you don’t have an appropriate utility installed, + it won’t even work. When it does work though, it is very handy for + debugging and tests. The following sections provide an overview of the different functions provided in this library. @@ -67,7 +66,6 @@ Convert files to JPEG :: - from __future__ import print_function import os, sys from PIL import Image @@ -76,8 +74,9 @@ Convert files to JPEG outfile = f + ".jpg" if infile != outfile: try: - Image.open(infile).save(outfile) - except IOError: + with Image.open(infile) as im: + im.save(outfile) + except OSError: print("cannot convert", infile) A second argument can be supplied to the :py:meth:`~PIL.Image.Image.save` @@ -89,7 +88,6 @@ Create JPEG thumbnails :: - from __future__ import print_function import os, sys from PIL import Image @@ -99,10 +97,10 @@ Create JPEG thumbnails outfile = os.path.splitext(infile)[0] + ".thumbnail" if infile != outfile: try: - im = Image.open(infile) - im.thumbnail(size) - im.save(outfile, "JPEG") - except IOError: + with Image.open(infile) as im: + im.thumbnail(size) + im.save(outfile, "JPEG") + except OSError: print("cannot create thumbnail for", infile) It is important to note that the library doesn’t decode or load the raster data @@ -120,15 +118,14 @@ Identify Image Files :: - from __future__ import print_function import sys from PIL import Image for infile in sys.argv[1:]: try: with Image.open(infile) as im: - print(infile, im.format, "%dx%d" % im.size, im.mode) - except IOError: + print(infile, im.format, f"{im.size}x{im.mode}") + except OSError: pass Cutting, pasting, and merging images @@ -158,7 +155,7 @@ Processing a subrectangle, and pasting it back :: - region = region.transpose(Image.ROTATE_180) + region = region.transpose(Image.Transpose.ROTATE_180) im.paste(region, box) When pasting regions back, the size of the region must match the given region @@ -174,29 +171,37 @@ Rolling an image :: - def roll(image, delta): - "Roll an image sideways" - - xsize, ysize = image.size + def roll(im, delta): + """Roll an image sideways.""" + xsize, ysize = im.size delta = delta % xsize - if delta == 0: return image + if delta == 0: + return im + + part1 = im.crop((0, 0, delta, ysize)) + part2 = im.crop((delta, 0, xsize, ysize)) + im.paste(part1, (xsize - delta, 0, xsize, ysize)) + im.paste(part2, (0, 0, xsize - delta, ysize)) + + return im + +Or if you would like to merge two images into a wider image: + +Merging images +^^^^^^^^^^^^^^ - part1 = image.crop((0, 0, delta, ysize)) - part2 = image.crop((delta, 0, xsize, ysize)) - part1.load() - part2.load() - image.paste(part2, (0, 0, xsize-delta, ysize)) - image.paste(part1, (xsize-delta, 0, xsize, ysize)) +:: + + def merge(im1, im2): + w = im1.size[0] + im2.size[0] + h = max(im1.size[1], im2.size[1]) + im = Image.new("RGBA", (w, h)) - return image + im.paste(im1) + im.paste(im2, (im1.size[0], 0)) -Note that when pasting it back from the :py:meth:`~PIL.Image.Image.crop` -operation, :py:meth:`~PIL.Image.Image.load` is called first. This is because -cropping is a lazy operation. If :py:meth:`~PIL.Image.Image.load` was not -called, then the crop operation would not be performed until the images were -used in the paste commands. This would mean that ``part1`` would be cropped from -the version of ``image`` already modified by the first paste. + return im For more advanced tricks, the paste method can also take a transparency mask as an optional argument. In this mask, the value 255 indicates that the pasted @@ -250,14 +255,14 @@ Transposing an image :: - out = im.transpose(Image.FLIP_LEFT_RIGHT) - out = im.transpose(Image.FLIP_TOP_BOTTOM) - out = im.transpose(Image.ROTATE_90) - out = im.transpose(Image.ROTATE_180) - out = im.transpose(Image.ROTATE_270) + out = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + out = im.transpose(Image.Transpose.FLIP_TOP_BOTTOM) + out = im.transpose(Image.Transpose.ROTATE_90) + out = im.transpose(Image.Transpose.ROTATE_180) + out = im.transpose(Image.Transpose.ROTATE_270) ``transpose(ROTATE)`` operations can also be performed identically with -:py:meth:`~PIL.Image.Image.rotate` operations, provided the `expand` flag is +:py:meth:`~PIL.Image.Image.rotate` operations, provided the ``expand`` flag is true, to provide for the same changes to the image's size. A more general form of image transformations can be carried out via the @@ -277,7 +282,9 @@ Converting between modes :: from PIL import Image - im = Image.open("hopper.ppm").convert("L") + + with Image.open("hopper.ppm") as im: + im = im.convert("L") The library supports transformations between each supported mode and the “L” and “RGB” modes. To convert between other modes, you may have to use an @@ -393,23 +400,19 @@ Reading sequences from PIL import Image - im = Image.open("animation.gif") - im.seek(1) # skip to the second frame + with Image.open("animation.gif") as im: + im.seek(1) # skip to the second frame - try: - while 1: - im.seek(im.tell()+1) - # do something to im - except EOFError: - pass # end of sequence + try: + while 1: + im.seek(im.tell() + 1) + # do something to im + except EOFError: + pass # end of sequence As seen in this example, you’ll get an :py:exc:`EOFError` exception when the sequence ends. -Note that most drivers in the current version of the library only allow you to -seek to the next frame (as in the above example). To rewind the file, you may -have to reopen it. - The following class lets you use the for-statement to loop over the sequence: Using the ImageSequence Iterator class @@ -422,13 +425,13 @@ Using the ImageSequence Iterator class # ...do something to frame... -Postscript printing +PostScript printing ------------------- The Python Imaging Library includes functions to print images, text and -graphics on Postscript printers. Here’s a simple example: +graphics on PostScript printers. Here’s a simple example: -Drawing Postscript +Drawing PostScript ^^^^^^^^^^^^^^^^^^ :: @@ -436,39 +439,41 @@ Drawing Postscript from PIL import Image from PIL import PSDraw - im = Image.open("hopper.ppm") - title = "hopper" - box = (1*72, 2*72, 7*72, 10*72) # in points + with Image.open("hopper.ppm") as im: + title = "hopper" + box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points - ps = PSDraw.PSDraw() # default is sys.stdout - ps.begin_document(title) + ps = PSDraw.PSDraw() # default is sys.stdout or sys.stdout.buffer + ps.begin_document(title) - # draw the image (75 dpi) - ps.image(box, im, 75) - ps.rectangle(box) + # draw the image (75 dpi) + ps.image(box, im, 75) + ps.rectangle(box) - # draw title - ps.setfont("HelveticaNarrow-Bold", 36) - ps.text((3*72, 4*72), title) + # draw title + ps.setfont("HelveticaNarrow-Bold", 36) + ps.text((3 * 72, 4 * 72), title) - ps.end_document() + ps.end_document() More on reading images ---------------------- As described earlier, the :py:func:`~PIL.Image.open` function of the :py:mod:`~PIL.Image` module is used to open an image file. In most cases, you -simply pass it the filename as an argument:: +simply pass it the filename as an argument. ``Image.open()`` can be used as a +context manager:: from PIL import Image - im = Image.open("hopper.ppm") + with Image.open("hopper.ppm") as im: + ... If everything goes well, the result is an :py:class:`PIL.Image.Image` object. -Otherwise, an :exc:`IOError` exception is raised. +Otherwise, an :exc:`OSError` exception is raised. You can use a file-like object instead of the filename. The object must -implement :py:meth:`~file.read`, :py:meth:`~file.seek` and -:py:meth:`~file.tell` methods, and be opened in binary mode. +implement ``file.read``, ``file.seek`` and ``file.tell`` methods, +and be opened in binary mode. Reading from an open file ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -476,20 +481,22 @@ Reading from an open file :: from PIL import Image + with open("hopper.ppm", "rb") as fp: im = Image.open(fp) -To read an image from string data, use the :py:class:`~StringIO.StringIO` +To read an image from binary data, use the :py:class:`~io.BytesIO` class: -Reading from a string -^^^^^^^^^^^^^^^^^^^^^ +Reading from binary data +^^^^^^^^^^^^^^^^^^^^^^^^ :: - import StringIO + from PIL import Image + import io - im = Image.open(StringIO.StringIO(buffer)) + im = Image.open(io.BytesIO(buffer)) Note that the library rewinds the file (using ``seek(0)``) before reading the image header. In addition, seek will also be used when the image data is read @@ -497,6 +504,17 @@ image header. In addition, seek will also be used when the image data is read tar file, you can use the :py:class:`~PIL.ContainerIO` or :py:class:`~PIL.TarIO` modules to access it. +Reading from URL +^^^^^^^^^^^^^^^^ + +:: + + from PIL import Image + from urllib.request import urlopen + url = "https://python-pillow.org/images/pillow-logo.png" + img = Image.open(urlopen(url)) + + Reading from a tar archive ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -507,6 +525,43 @@ Reading from a tar archive fp = TarIO.TarIO("Tests/images/hopper.tar", "hopper.jpg") im = Image.open(fp) + +Batch processing +^^^^^^^^^^^^^^^^ + +Operations can be applied to multiple image files. For example, all PNG images +in the current directory can be saved as JPEGs at reduced quality. + +:: + + import glob + from PIL import Image + + + def compress_image(source_path, dest_path): + with Image.open(source_path) as img: + if img.mode != "RGB": + img = img.convert("RGB") + img.save(dest_path, "JPEG", optimize=True, quality=80) + + + paths = glob.glob("*.png") + for path in paths: + compress_image(path, path[:-4] + ".jpg") + +Since images can also be opened from a ``Path`` from the ``pathlib`` module, +the example could be modified to use ``pathlib`` instead of the ``glob`` +module. + +:: + + from pathlib import Path + + paths = Path(".").glob("*.png") + for path in paths: + compress_image(path, path.stem + ".jpg") + + Controlling the decoder ----------------------- @@ -527,12 +582,12 @@ This is only available for JPEG and MPO files. :: from PIL import Image - from __future__ import print_function - im = Image.open(file) - print("original =", im.mode, im.size) - im.draft("L", (100, 100)) - print("draft =", im.mode, im.size) + with Image.open(file) as im: + print("original =", im.mode, im.size) + + im.draft("L", (100, 100)) + print("draft =", im.mode, im.size) This prints something like:: diff --git a/docs/handbook/writing-your-own-file-decoder.rst b/docs/handbook/writing-your-own-image-plugin.rst similarity index 52% rename from docs/handbook/writing-your-own-file-decoder.rst rename to docs/handbook/writing-your-own-image-plugin.rst index aa2463bd1d3..323127e5b04 100644 --- a/docs/handbook/writing-your-own-file-decoder.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -3,22 +3,21 @@ Writing Your Own Image Plugin ============================= -The Pillow uses a plug-in model which allows you to add your own -decoders to the library, without any changes to the library -itself. Such plug-ins usually have names like -:file:`XxxImagePlugin.py`, where ``Xxx`` is a unique format name -(usually an abbreviation). +Pillow uses a plugin model which allows you to add your own +decoders and encoders to the library, without any changes to the library +itself. Such plugins usually have names like :file:`XxxImagePlugin.py`, +where ``Xxx`` is a unique format name (usually an abbreviation). .. warning:: Pillow >= 2.1.0 no longer automatically imports any file in the Python path with a name ending in :file:`ImagePlugin.py`. You will need to import your image plugin manually. -Pillow decodes files in 2 stages: +Pillow decodes files in two stages: 1. It loops over the available image plugins in the loaded order, and - calls the plugin's ``accept`` function with the first 16 bytes of - the file. If the ``accept`` function returns true, the plugin's + calls the plugin's ``_accept`` function with the first 16 bytes of + the file. If the ``_accept`` function returns true, the plugin's ``_open`` method is called to set up the image metadata and image tiles. The ``_open`` method is not for decoding the actual image data. @@ -26,24 +25,24 @@ Pillow decodes files in 2 stages: called, which sets up a decoder for each tile and feeds the data to it. -An image plug-in should contain a format handler derived from the +An image plugin should contain a format handler derived from the :py:class:`PIL.ImageFile.ImageFile` base class. This class should -provide an :py:meth:`_open` method, which reads the file header and +provide an ``_open`` method, which reads the file header and sets up at least the :py:attr:`~PIL.Image.Image.mode` and :py:attr:`~PIL.Image.Image.size` attributes. To be able to load the -file, the method must also create a list of :py:attr:`tile` -descriptors, which contain a decoder name, extents of the tile, and +file, the method must also create a list of ``tile`` descriptors, +which contain a decoder name, extents of the tile, and any decoder-specific data. The format handler class must be explicitly registered, via a call to the :py:mod:`~PIL.Image` module. .. note:: For performance reasons, it is important that the - :py:meth:`_open` method quickly rejects files that do not have the + ``_open`` method quickly rejects files that do not have the appropriate contents. Example ------- -The following plug-in supports a simple format, which has a 128-byte header +The following plugin supports a simple format, which has a 128-byte header consisting of the words “SPAM” followed by the width, height, and pixel size in bits. The header fields are separated by spaces. The image data follows directly after the header, and can be either bi-level, greyscale, or 24-bit @@ -52,7 +51,11 @@ true color. **SpamImagePlugin.py**:: from PIL import Image, ImageFile - import string + + + def _accept(prefix): + return prefix[:4] == b"SPAM" + class SpamImageFile(ImageFile.ImageFile): @@ -61,15 +64,10 @@ true color. def _open(self): - # check header - header = self.fp.read(128) - if header[:4] != "SPAM": - raise SyntaxError, "not a SPAM file" - - header = string.split(header) + header = self.fp.read(128).split() # size in pixels (width, height) - self.size = int(header[1]), int(header[2]) + self._size = int(header[1]), int(header[2]) # mode setting bits = int(header[3]) @@ -80,17 +78,22 @@ true color. elif bits == 24: self.mode = "RGB" else: - raise SyntaxError, "unknown number of bits" + raise SyntaxError("unknown number of bits") # data descriptor - self.tile = [ - ("raw", (0, 0) + self.size, 128, (self.mode, 0, 1)) - ] + self.tile = [("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] + - Image.register_open(SpamImageFile.format, SpamImageFile) + Image.register_open(SpamImageFile.format, SpamImageFile, _accept) + + Image.register_extensions( + SpamImageFile.format, + [ + ".spam", + ".spa", # DOS version + ], + ) - Image.register_extension(SpamImageFile.format, ".spam") - Image.register_extension(SpamImageFile.format, ".spa") # dos version The format handler must always set the :py:attr:`~PIL.Image.Image.size` and :py:attr:`~PIL.Image.Image.mode` @@ -104,14 +107,28 @@ Note that the image plugin must be explicitly registered using :py:func:`PIL.Image.register_open`. Although not required, it is also a good idea to register any extensions used by this format. -The :py:attr:`tile` attribute ------------------------------ +Once the plugin has been imported, it can be used: + +.. code-block:: python + + from PIL import Image + import SpamImagePlugin + + with Image.open("hopper.spam") as im: + pass -To be able to read the file as well as just identifying it, the :py:attr:`tile` +The ``tile`` attribute +---------------------- + +To be able to read the file as well as just identifying it, the ``tile`` attribute must also be set. This attribute consists of a list of tile descriptors, where each descriptor specifies how data should be loaded to a -given region in the image. In most cases, only a single descriptor is used, -covering the full image. +given region in the image. + +In most cases, only a single descriptor is used, covering the full image. +:py:class:`.PsdImagePlugin.PsdImageFile` uses multiple tiles to combine +channels within a single layer, given that the channels are stored separately, +one after the other. The tile descriptor is a 4-tuple with the following contents:: @@ -124,6 +141,10 @@ The fields are used as follows: uncompressed data, in a variety of pixel formats. For more information on this decoder, see the description below. + A list of C decoders can be seen under codecs section of the function array + in :file:`_imaging.c`. Python decoders are registered within the relevant + plugins. + **region** A 4-tuple specifying where to store data in the image. @@ -133,9 +154,9 @@ The fields are used as follows: **parameters** Parameters to the decoder. The contents of this field depends on the decoder specified by the first field in the tile descriptor tuple. If the - decoder doesn’t need any parameters, use None for this field. + decoder doesn’t need any parameters, use :data:`None` for this field. -Note that the :py:attr:`tile` attribute contains a list of tile descriptors, +Note that the ``tile`` attribute contains a list of tile descriptors, not just a single descriptor. Decoders @@ -147,20 +168,22 @@ The raw decoder The ``raw`` decoder is used to read uncompressed data from an image file. It can be used with most uncompressed file formats, such as PPM, BMP, uncompressed TIFF, and many others. To use the raw decoder with the -:py:func:`PIL.Image.frombytes` function, use the following syntax:: +:py:func:`PIL.Image.frombytes` function, use the following syntax: + +.. code-block:: python image = Image.frombytes( mode, size, data, "raw", - raw mode, stride, orientation + raw_mode, stride, orientation ) When used in a tile descriptor, the parameter field should look like:: - (raw mode, stride, orientation) + (raw_mode, stride, orientation) The fields are used as follows: -**raw mode** +**raw_mode** The pixel layout used in the file, and is used to properly convert data to PIL’s internal layout. For a summary of the available formats, see the table below. @@ -171,48 +194,48 @@ The fields are used as follows: stride defaults to 0. **orientation** - Whether the first line in the image is the top line on the screen (1), or the bottom line (-1). If omitted, the orientation defaults to 1. The **raw mode** field is used to determine how the data should be unpacked to match PIL’s internal pixel layout. PIL supports a large set of raw modes; for a -complete list, see the table in the :py:mod:`Unpack.c` module. The following +complete list, see the table in the :file:`Unpack.c` module. The following table describes some commonly used **raw modes**: -+-----------+-----------------------------------------------------------------+ -| mode | description | -+===========+=================================================================+ -| ``1`` | 1-bit bilevel, stored with the leftmost pixel in the most | -| | significant bit. 0 means black, 1 means white. | -+-----------+-----------------------------------------------------------------+ -| ``1;I`` | 1-bit inverted bilevel, stored with the leftmost pixel in the | -| | most significant bit. 0 means white, 1 means black. | -+-----------+-----------------------------------------------------------------+ -| ``1;R`` | 1-bit reversed bilevel, stored with the leftmost pixel in the | -| | least significant bit. 0 means black, 1 means white. | -+-----------+-----------------------------------------------------------------+ -| ``L`` | 8-bit greyscale. 0 means black, 255 means white. | -+-----------+-----------------------------------------------------------------+ -| ``L;I`` | 8-bit inverted greyscale. 0 means white, 255 means black. | -+-----------+-----------------------------------------------------------------+ -| ``P`` | 8-bit palette-mapped image. | -+-----------+-----------------------------------------------------------------+ -| ``RGB`` | 24-bit true colour, stored as (red, green, blue). | -+-----------+-----------------------------------------------------------------+ -| ``BGR`` | 24-bit true colour, stored as (blue, green, red). | -+-----------+-----------------------------------------------------------------+ -| ``RGBX`` | 24-bit true colour, stored as (red, green, blue, pad). | -+-----------+-----------------------------------------------------------------+ -| ``RGB;L`` | 24-bit true colour, line interleaved (first all red pixels, the | -| | all green pixels, finally all blue pixels). | -+-----------+-----------------------------------------------------------------+ ++-----------+-------------------------------------------------------------------+ +| mode | description | ++===========+===================================================================+ +| ``1`` | | 1-bit bilevel, stored with the leftmost pixel in the most | +| | | significant bit. 0 means black, 1 means white. | ++-----------+-------------------------------------------------------------------+ +| ``1;I`` | | 1-bit inverted bilevel, stored with the leftmost pixel in the | +| | | most significant bit. 0 means white, 1 means black. | ++-----------+-------------------------------------------------------------------+ +| ``1;R`` | | 1-bit reversed bilevel, stored with the leftmost pixel in the | +| | | least significant bit. 0 means black, 1 means white. | ++-----------+-------------------------------------------------------------------+ +| ``L`` | 8-bit greyscale. 0 means black, 255 means white. | ++-----------+-------------------------------------------------------------------+ +| ``L;I`` | 8-bit inverted greyscale. 0 means white, 255 means black. | ++-----------+-------------------------------------------------------------------+ +| ``P`` | 8-bit palette-mapped image. | ++-----------+-------------------------------------------------------------------+ +| ``RGB`` | 24-bit true colour, stored as (red, green, blue). | ++-----------+-------------------------------------------------------------------+ +| ``BGR`` | 24-bit true colour, stored as (blue, green, red). | ++-----------+-------------------------------------------------------------------+ +| ``RGBX`` | | 24-bit true colour, stored as (red, green, blue, pad). The pad | +| | | pixels may vary. | ++-----------+-------------------------------------------------------------------+ +| ``RGB;L`` | | 24-bit true colour, line interleaved (first all red pixels, then| +| | | all green pixels, finally all blue pixels). | ++-----------+-------------------------------------------------------------------+ Note that for the most common cases, the raw mode is simply the same as the mode. The Python Imaging Library supports many other decoders, including JPEG, PNG, and PackBits. For details, see the :file:`decode.c` source file, and the -standard plug-in implementations provided with the library. +standard plugin implementations provided with the library. Decoding floating point data ---------------------------- @@ -224,7 +247,7 @@ You can use the ``raw`` decoder to read images where data is packed in any standard machine data type, using one of the following raw modes: ============ ======================================= -mode description +mode description ============ ======================================= ``F`` 32-bit native floating point. ``F;8`` 8-bit unsigned integer. @@ -256,9 +279,12 @@ If the raw decoder cannot handle your format, PIL also provides a special “bit decoder that can be used to read various packed formats into a floating point image memory. -To use the bit decoder with the frombytes function, use the following syntax:: +To use the bit decoder with the :py:func:`PIL.Image.frombytes` function, use +the following syntax: - image = frombytes( +.. code-block:: python + + image = Image.frombytes( mode, size, data, "bit", bits, pad, fill, sign, orientation ) @@ -306,42 +332,42 @@ The fields are used as follows: Whether the first line in the image is the top line on the screen (1), or the bottom line (-1). If omitted, the orientation defaults to 1. -.. _file-decoders: +.. _file-codecs: -Writing Your Own File Decoder in C -================================== +Writing Your Own File Codec in C +================================ -There are 3 stages in a file decoder's lifetime: +There are 3 stages in a file codec's lifetime: -1. Setup: Pillow looks for a function in the decoder registry, falling - back to a function named ``[decodername]_decoder`` on the internal - core image object. That function is called with the ``args`` tuple - from the ``tile`` setup in the ``_open`` method. +1. Setup: Pillow looks for a function in the decoder or encoder registry, + falling back to a function named ``[codecname]_decoder`` or + ``[codecname]_encoder`` on the internal core image object. That function is + called with the ``args`` tuple from the ``tile``. -2. Decoding: The decoder's decode function is repeatedly called with - chunks of image data. +2. Transforming: The codec's ``decode`` or ``encode`` function is repeatedly + called with chunks of image data. -3. Cleanup: If the decoder has registered a cleanup function, it will - be called at the end of the decoding process, even if there was an +3. Cleanup: If the codec has registered a cleanup function, it will + be called at the end of the transformation process, even if there was an exception raised. Setup ----- -The current conventions are that the decoder setup function is named -``PyImaging_[Decodername]DecoderNew`` and defined in ``decode.c``. The -python binding for it is named ``[decodername]_decoder`` and is setup -from within the ``_imaging.c`` file in the codecs section of the -function array. +The current conventions are that the codec setup function is named +``PyImaging_[codecname]DecoderNew`` or ``PyImaging_[codecname]EncoderNew`` +and defined in ``decode.c`` or ``encode.c``. The Python binding for it is +named ``[codecname]_decoder`` or ``[codecname]_encoder`` and is set up from +within the ``_imaging.c`` file in the codecs section of the function array. -The setup function needs to call ``PyImaging_DecoderNew`` and at the -very least, set the ``decode`` function pointer. The fields of -interest in this object are: +The setup function needs to call ``PyImaging_DecoderNew`` or +``PyImaging_EncoderNew`` and at the very least, set the ``decode`` or +``encode`` function pointer. The fields of interest in this object are: -**decode** - Function pointer to the decode function, which has access to - ``im``, ``state``, and the buffer of data to be added to the image. +**decode**/**encode** + Function pointer to the decode or encode function, which has access to + ``im``, ``state``, and the buffer of data to be transformed. **cleanup** Function pointer to the cleanup function, has access to ``state``. @@ -350,37 +376,35 @@ interest in this object are: The target image, will be set by Pillow. **state** - An ImagingCodecStateInstance, will be set by Pillow. The **context** - member is an opaque struct that can be used by the decoder to store + An ImagingCodecStateInstance, will be set by Pillow. The ``context`` + member is an opaque struct that can be used by the codec to store any format specific state or options. -**pulls_fd** - **EXPERIMENTAL** -- **WARNING**, interface may change. If set to 1, - ``state->fd`` will be a pointer to the Python file like object. The - decoder may use the functions in ``codec_fd.c`` to read directly - from the file like object rather than have the data pushed through a - buffer. Note that this implementation may be refactored until this - warning is removed. +**pulls_fd**/**pushes_fd** + If the decoder has ``pulls_fd`` or the encoder has ``pushes_fd`` set to 1, + ``state->fd`` will be a pointer to the Python file like object. The codec may + use the functions in ``codec_fd.c`` to read or write directly with the file + like object rather than have the data pushed through a buffer. .. versionadded:: 3.3.0 -Decoding --------- +Transforming +------------ -The decode function is called with the target (core) image, the -decoder state structure, and a buffer of data to be decoded. +The decode or encode function is called with the target (core) image, the codec +state structure, and a buffer of data to be transformed. -**Experimental** -- If ``pulls_fd`` is set, then the decode function -is called once, with an empty buffer. It is the decoder's -responsibility to decode the entire tile in that one call. The rest of -this section only applies if ``pulls_fd`` is not set. +It is the codec's responsibility to pull as much data as possible out of the +buffer and return the number of bytes consumed. The next call to the codec will +include the previous unconsumed tail. The codec function will be called +multiple times as the data processed. -It is the decoder's responsibility to pull as much data as possible -out of the buffer and return the number of bytes consumed. The next -call to the decoder will include the previous unconsumed tail. The -decoder function will be called multiple times as the data is read -from the file like object. +Alternatively, if ``pulls_fd`` or ``pushes_fd`` is set, then the decode or +encode function is called once, with an empty buffer. It is the codec's +responsibility to transform the entire tile in that one call. Using this will +provide a codec with more freedom, but that freedom may mean increased memory +usage if the entire tile is held in memory at once by the codec. If an error occurs, set ``state->errcode`` and return -1. @@ -389,29 +413,49 @@ Return -1 on success, without setting the errcode. Cleanup ------- -The cleanup function is called after the decoder returns a negative -value, or if there is a read error from the file. This function should -free any allocated memory and release any resources from external -libraries. +The cleanup function is called after the codec returns a negative +value, or if there is an error. This function should free any allocated +memory and release any resources from external libraries. -.. _file-decoders-py: +.. _file-codecs-py: -Writing Your Own File Decoder in Python -======================================= +Writing Your Own File Codec in Python +===================================== -Python file decoders should derive from -:py:class:`PIL.ImageFile.PyDecoder` and should at least override the -decode method. File decoders should be registered using -:py:meth:`PIL.Image.register_decoder`. As in the C implementation of -the file decoders, there are three stages in the lifetime of a -Python-based file decoder: +Python file decoders and encoders should derive from +:py:class:`PIL.ImageFile.PyDecoder` and :py:class:`PIL.ImageFile.PyEncoder` +respectively, and should at least override the decode or encode method. +They should be registered using :py:meth:`PIL.Image.register_decoder` and +:py:meth:`PIL.Image.register_encoder`. As in the C implementation of +the file codecs, there are three stages in the lifetime of a +Python-based file codec: -1. Setup: Pillow looks for the decoder in the registry, then +1. Setup: Pillow looks for the codec in the decoder or encoder registry, then instantiates the class. -2. Decoding: The decoder instance's ``decode`` method is repeatedly - called with a buffer of data to be interpreted. +2. Transforming: The instance's ``decode`` method is repeatedly called with + a buffer of data to be interpreted, or the ``encode`` method is repeatedly + called with the size of data to be output. + + Alternatively, if the decoder's ``_pulls_fd`` property (or the encoder's + ``_pushes_fd`` property) is set to ``True``, then ``decode`` and ``encode`` + will only be called once. In the decoder, ``self.fd`` can be used to access + the file-like object. Using this will provide a codec with more freedom, but + that freedom may mean increased memory usage if entire file is held in + memory at once by the codec. + + In ``decode``, once the data has been interpreted, ``set_as_raw`` can be + used to populate the image. -3. Cleanup: The decoder instance's ``cleanup`` method is called. +3. Cleanup: The instance's ``cleanup`` method is called once the transformation + is complete. This can be used to clean up any resources used by the codec. + If you set ``_pulls_fd`` or ``_pushes_fd`` to ``True`` however, then you + probably chose to perform any cleanup tasks at the end of ``decode`` or + ``encode``. +For an example :py:class:`PIL.ImageFile.PyDecoder`, see `DdsImagePlugin +`_. +For a plugin that uses both :py:class:`PIL.ImageFile.PyDecoder` and +:py:class:`PIL.ImageFile.PyEncoder`, see `BlpImagePlugin +`_ diff --git a/docs/index.rst b/docs/index.rst index 55ba13bb712..45af4c5714c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,32 +3,84 @@ Pillow Pillow is the friendly PIL fork by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. -.. image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg - :target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow +Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_. .. image:: https://readthedocs.org/projects/pillow/badge/?version=latest :target: https://pillow.readthedocs.io/?badge=latest :alt: Documentation Status -.. image:: https://travis-ci.org/python-pillow/Pillow.svg?branch=master - :target: https://travis-ci.org/python-pillow/Pillow - :alt: Travis CI build status (Linux) +.. image:: https://github.com/python-pillow/Pillow/workflows/Lint/badge.svg + :target: https://github.com/python-pillow/Pillow/actions/workflows/lint.yml + :alt: GitHub Actions build status (Lint) + +.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg + :target: https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml + :alt: GitHub Actions build status (Test Docker) + +.. image:: https://github.com/python-pillow/Pillow/workflows/Test/badge.svg + :target: https://github.com/python-pillow/Pillow/actions/workflows/test.yml + :alt: GitHub Actions build status (Test Linux and macOS) + +.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20Windows/badge.svg + :target: https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml + :alt: GitHub Actions build status (Test Windows) + +.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg + :target: https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml + :alt: GitHub Actions build status (Test MinGW) -.. image:: https://travis-ci.org/python-pillow/pillow-wheels.svg?branch=latest - :target: https://travis-ci.org/python-pillow/pillow-wheels - :alt: Travis CI build status (macOS) +.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20Cygwin/badge.svg + :target: https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml + :alt: GitHub Actions build status (Test Cygwin) -.. image:: https://img.shields.io/appveyor/ci/python-pillow/Pillow/master.svg?label=Windows%20build +.. image:: https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build :target: https://ci.appveyor.com/project/python-pillow/Pillow :alt: AppVeyor CI build status (Windows) +.. image:: https://github.com/python-pillow/pillow-wheels/workflows/Wheels/badge.svg + :target: https://github.com/python-pillow/pillow-wheels/actions + :alt: GitHub Actions wheels build status (Wheels) + +.. image:: https://img.shields.io/travis/com/python-pillow/pillow-wheels/main.svg?label=aarch64%20wheels + :target: https://app.travis-ci.com/github/python-pillow/pillow-wheels + :alt: Travis CI wheels build status (aarch64) + +.. image:: https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg + :target: https://app.codecov.io/gh/python-pillow/Pillow + :alt: Code coverage + +.. image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg + :target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow + :alt: Zenodo + +.. image:: https://tidelift.com/badges/package/pypi/Pillow?style=flat + :target: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge + :alt: Tidelift + +.. image:: https://github.com/python-pillow/Pillow/actions/workflows/tidelift.yml/badge.svg + :target: https://github.com/python-pillow/Pillow/actions/workflows/tidelift.yml + :alt: Tidelift Align + .. image:: https://img.shields.io/pypi/v/pillow.svg - :target: https://pypi.python.org/pypi/Pillow/ + :target: https://pypi.org/project/Pillow/ :alt: Latest PyPI version -.. image:: https://coveralls.io/repos/python-pillow/Pillow/badge.svg?branch=master - :target: https://coveralls.io/github/python-pillow/Pillow?branch=master - :alt: Code coverage +.. image:: https://img.shields.io/pypi/dm/pillow.svg + :target: https://pypi.org/project/Pillow/ + :alt: Number of PyPI downloads + +.. image:: https://bestpractices.coreinfrastructure.org/projects/6331/badge + :target: https://bestpractices.coreinfrastructure.org/projects/6331 + :alt: OpenSSF Best Practices + +Overview +======== + +The Python Imaging Library adds image processing capabilities to your Python interpreter. + +This library provides extensive file format support, an efficient internal representation, and fairly powerful image processing capabilities. + +The core image library is designed for fast access to data stored in a few basic pixel formats. It should provide a solid foundation for a general image processing tool. .. toctree:: :maxdepth: 2 @@ -39,16 +91,10 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors - Support via Gratipay - + deprecations.rst Indices and tables ================== * :ref:`genindex` * :ref:`modindex` -* :ref:`search` diff --git a/docs/installation.rst b/docs/installation.rst index 893ed3556d1..eb69d580567 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -6,20 +6,22 @@ Warnings .. warning:: Pillow and PIL cannot co-exist in the same environment. Before installing Pillow, please uninstall PIL. -.. warning:: Pillow >= 1.0 no longer supports "import Image". Please use "from PIL import Image" instead. +.. warning:: Pillow >= 1.0 no longer supports ``import Image``. Please use ``from PIL import Image`` instead. -.. warning:: Pillow >= 2.1.0 no longer supports "import _imaging". Please use "from PIL.Image import core as _imaging" instead. +.. warning:: Pillow >= 2.1.0 no longer supports ``import _imaging``. Please use ``from PIL.Image import core as _imaging`` instead. -Notes ------ +Python Support +-------------- -.. note:: Pillow < 2.0.0 supports Python versions 2.4, 2.5, 2.6, 2.7. +Pillow supports these Python versions. -.. note:: Pillow >= 2.0.0 < 4.0.0 supports Python versions 2.6, 2.7, 3.2, 3.3, 3.4, 3.5 +.. csv-table:: Newer versions + :file: newer-versions.csv + :header-rows: 1 -.. note:: Pillow >= 4.0.0 < 5.0.0 supports Python versions 2.7, 3.3, 3.4, 3.5, 3.6 - -.. note:: Pillow >= 5.0.0 supports Python versions 2.7, 3.4, 3.5, 3.6 +.. csv-table:: Older versions + :file: older-versions.csv + :header-rows: 1 Basic Installation ------------------ @@ -32,18 +34,23 @@ Basic Installation Install Pillow with :command:`pip`:: - $ pip install Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow Windows Installation ^^^^^^^^^^^^^^^^^^^^ We provide Pillow binaries for Windows compiled for the matrix of -supported Pythons in both 32 and 64-bit versions in wheel, egg, and -executable installers. These binaries have all of the optional -libraries included except for raqm and libimagequant:: +supported Pythons in both 32 and 64-bit versions in the wheel format. +These binaries include support for all optional libraries except +libimagequant and libxcb. Raqm support requires +FriBiDi to be installed separately:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow - > pip install Pillow +To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. macOS Installation @@ -51,10 +58,11 @@ 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 libraqm, -fribidi, and harfbuzz to be installed separately:: +libraries except libimagequant. Raqm support requires +FriBiDi to be installed separately:: - $ pip install Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow Linux Installation ^^^^^^^^^^^^^^^^^^ @@ -62,13 +70,15 @@ Linux Installation We provide binaries for Linux for each of the supported Python versions in the manylinux wheel format. These include support for all optional libraries except libimagequant. Raqm support requires -libraqm, fribidi, and harfbuzz to be installed separately:: +FriBiDi to be installed separately:: - $ pip install Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow -Most major Linux distributions, including Fedora, Debian/Ubuntu and -ArchLinux also include Pillow in packages that previously contained -PIL e.g. ``python-imaging``. +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 ^^^^^^^^^^^^^^^^^^^^ @@ -77,18 +87,17 @@ Pillow can be installed on FreeBSD via the official Ports or Packages systems: **Ports**:: - $ cd /usr/ports/graphics/py-pillow && make install clean + cd /usr/ports/graphics/py-pillow && make install clean **Packages**:: - $ pkg install py27-pillow + pkg install py38-pillow .. note:: The `Pillow FreeBSD port `_ and packages - are tested by the ports team with all supported FreeBSD versions - and against Python 2.x and 3.x. + are tested by the ports team with all supported FreeBSD versions. Building From Source @@ -96,7 +105,7 @@ Building From Source Download and extract the `compressed archive from PyPI`_. -.. _compressed archive from PyPI: https://pypi.python.org/pypi/Pillow +.. _compressed archive from PyPI: https://pypi.org/project/Pillow/ .. _external-libraries: @@ -111,17 +120,16 @@ External Libraries .. note:: - There are scripts to install the dependencies for some operating - systems included in the ``depends`` directory. Also see the - Dockerfiles in our `docker images repo - `_. + There are Dockerfiles in our `Docker images repo + `_ to install the + dependencies for some operating systems. Many of Pillow's features require external libraries: * **libjpeg** provides JPEG functionality. - * Pillow has been tested with libjpeg versions **6b**, **8**, **9**, **9a**, - and **9b** and libjpeg-turbo version **8**. + * Pillow has been tested with libjpeg versions **6b**, **8**, **9-9d** and + libjpeg-turbo version **8**. * Starting with Pillow 3.0.0, libjpeg is required by default, but may be disabled with the ``--disable-jpeg`` flag. @@ -132,14 +140,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** + * Pillow has been tested with libtiff versions **3.x** and **4.0-4.4** * **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**. + above uses liblcms2. Tested with **1.19** and **2.7-2.13.1**. * **libwebp** provides the WebP format. @@ -151,18 +159,17 @@ Many of Pillow's features require external libraries: * **openjpeg** provides JPEG 2000 functionality. - * Pillow has been tested with openjpeg **2.0.0** and **2.1.0**. + * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, + **2.4.0** and **2.5.0**. * Pillow does **not** support the earlier **1.5** series which ships - with Ubuntu <= 14.04 and Debian Jessie. + with Debian Jessie. * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-2.11** + * Pillow has been tested with libimagequant **2.6-4.0.4** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. - * Windows support: Libimagequant requires VS2013/MSVC 18 to compile, - so it is unlikely to work with any Python prior to 3.5 on Windows. * **libraqm** provides complex text layout support. @@ -170,17 +177,24 @@ Many of Pillow's features require external libraries: shaping (using HarfBuzz), and proper script itemization. As a result, Raqm can support most writing systems covered by Unicode. * libraqm depends on the following libraries: FreeType, HarfBuzz, - FriBiDi, make sure that you install them before install libraqm + FriBiDi, make sure that you install them before installing libraqm if not available as package in your system. - * setting text direction or font features is not supported without - libraqm. - * libraqm is dynamically loaded in Pillow 5.0.0 and above, so support - is available if all the libraries are installed. - * Windows support: Raqm support is currently unsupported on Windows. + * Setting text direction or font features is not supported without libraqm. + * Pillow wheels since version 8.2.0 include a modified version of libraqm that + loads libfribidi at runtime if it is installed. + On Windows this requires compiling FriBiDi and installing ``fribidi.dll`` + into a directory listed in the `Dynamic-Link Library Search Order (Microsoft Docs) + `_ + (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected). + See `Build Options`_ to see how to build this version. + * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime. + +* **libxcb** provides X11 screengrab support. Once you have installed the prerequisites, run:: - $ pip install Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow --no-binary :all: If the prerequisites are installed in the standard library locations for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no @@ -190,7 +204,7 @@ those locations by editing :file:`setup.py` or :file:`setup.cfg`, or by adding environment variables on the command line:: - $ CFLAGS="-I/usr/pkg/include" pip install pillow + CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all: If Pillow has been previously built without the required prerequisites, it may be necessary to manually clear the pip cache or @@ -201,28 +215,33 @@ build with newly installed external libraries. Build Options ^^^^^^^^^^^^^ -* Environment variable: ``MAX_CONCURRENCY=n``. By default, Pillow will - use multiprocessing to build the extension on all available CPUs, - but not more than 4. Setting ``MAX_CONCURRENCY`` to 1 will disable - parallel building. +* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use + multiprocessing to build the extension. Setting ``MAX_CONCURRENCY`` + sets the number of CPUs to use, or can disable parallel building by + using a setting of 1. By default, it uses 4 CPUs, or if 4 are not + available, as many as are present. * Build flags: ``--disable-zlib``, ``--disable-jpeg``, - ``--disable-tiff``, ``--disable-freetype``, ``--disable-tcl``, - ``--disable-tk``, ``--disable-lcms``, ``--disable-webp``, - ``--disable-webpmux``, ``--disable-jpeg2000``, - ``--disable-imagequant``. + ``--disable-tiff``, ``--disable-freetype``, ``--disable-lcms``, + ``--disable-webp``, ``--disable-webpmux``, ``--disable-jpeg2000``, + ``--disable-imagequant``, ``--disable-xcb``. Disable building the corresponding feature even if the development libraries are present on the building machine. * Build flags: ``--enable-zlib``, ``--enable-jpeg``, - ``--enable-tiff``, ``--enable-freetype``, ``--enable-tcl``, - ``--enable-tk``, ``--enable-lcms``, ``--enable-webp``, - ``--enable-webpmux``, ``--enable-jpeg2000``, - ``--enable-imagequant``. + ``--enable-tiff``, ``--enable-freetype``, ``--enable-lcms``, + ``--enable-webp``, ``--enable-webpmux``, ``--enable-jpeg2000``, + ``--enable-imagequant``, ``--enable-xcb``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Webpmux (WebP metadata) relies on WebP support. Tcl and Tk also must be used together. +* Build flags: ``--vendor-raqm --vendor-fribidi`` + These flags are used to compile a modified version of libraqm and + a shim that dynamically loads libfribidi at runtime. These are + used to compile the standard Pillow wheels. Compiling libraqm requires + a C99-compliant compiler. + * Build flag: ``--disable-platform-guessing``. Skips all of the platform dependent guessing of include and library directories for automated build systems that configure the proper paths in the @@ -235,11 +254,7 @@ Build Options Sample usage:: - $ MAX_CONCURRENCY=1 python setup.py build_ext --enable-[feature] install - -or using pip:: - - $ pip install pillow --global-option="build_ext" --global-option="--enable-[feature]" + python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]" Building on macOS @@ -255,45 +270,79 @@ tools. The easiest way to install external libraries is via `Homebrew `_. After you install Homebrew, run:: - $ brew install libtiff libjpeg webp little-cms2 + brew install libjpeg libtiff little-cms2 openjpeg webp To install libraqm on macOS use Homebrew to install its dependencies:: - $ brew install freetype harfbuzz fribidi + brew install freetype harfbuzz fribidi Then see ``depends/install_raqm_cmake.sh`` to install libraqm. Now install Pillow with:: - $ pip install Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow --no-binary :all: or from within the uncompressed source directory:: - $ python setup.py install + python3 -m pip install . Building on Windows ^^^^^^^^^^^^^^^^^^^ -We don't recommend trying to build on Windows. It is a maze of twisty -passages, mostly dead ends. There are build scripts and notes for the -Windows build in the ``winbuild`` directory. +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 python2 - -Or for Python 3:: +Make sure you have Python's development libraries installed:: - $ sudo pkg install python3 + 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 + sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb Then see ``depends/install_raqm_cmake.sh`` to install libraqm. @@ -306,39 +355,42 @@ development libraries installed. In Debian or Ubuntu:: - $ sudo apt-get install python-dev python-setuptools - -Or for Python 3:: - - $ sudo apt-get install python3-dev python3-setuptools + sudo apt-get install python3-dev python3-setuptools In Fedora, the command is:: - $ sudo dnf install python-devel redhat-rpm-config + sudo dnf install python3-devel redhat-rpm-config -Or for Python 3:: +In Alpine, the command is:: - $ sudo dnf install python3-devel redhat-rpm-config + sudo apk add python3-dev py3-setuptools .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. -Prerequisites are installed on **Ubuntu 14.04 LTS** with:: +Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: - $ sudo apt-get install libtiff5-dev libjpeg8-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev libharfbuzz-dev libfribidi-dev \ - tcl8.6-dev tk8.6-dev python-tk + 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 -Then see ``depends/install_raqm.sh`` to install libraqm. +To install libraqm, ``sudo apt-get install meson`` and then see +``depends/install_raqm.sh``. -Prerequisites are installed on recent **RedHat** **Centos** or **Fedora** with:: +Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: - $ sudo dnf install libtiff-devel libjpeg-devel zlib-devel freetype-devel \ - lcms2-devel libwebp-devel tcl-devel tk-devel libraqm-devel \ - libimagequant-devel + 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 +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. @@ -349,8 +401,8 @@ Building on Android Basic Android support has been added for compilation within the Termux environment. The dependencies can be installed by:: - $ pkg -y install python python-dev ndk-sysroot clang make \ - libjpeg-turbo-dev + pkg install -y python ndk-sysroot clang make \ + libjpeg-turbo This has been tested within the Termux app on ChromeOS, on x86. @@ -369,40 +421,52 @@ Continuous Integration Targets These platforms are built and tested for every change. -+----------------------------------+-------------------------------+-----------------------+ -|**Operating system** |**Tested Python versions** |**Tested Architecture**| -+----------------------------------+-------------------------------+-----------------------+ -| Alpine | 2.7 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Arch | 2.7 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Amazon | 2.7 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Centos 6 | 2.7 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Centos 7 | 2.7 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Debian Stretch | 2.7 |x86 | -+----------------------------------+-------------------------------+-----------------------+ -| Fedora 25 | 2.7 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Fedora 26 | 2.7 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Mac OS X 10.10 Yosemite* | 2.7, 3.4, 3.5, 3.6 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Ubuntu Linux 16.04 LTS | 2.7 |x86-64 | -+----------------------------------+-------------------------------+-----------------------+ -| Ubuntu Linux 14.04 LTS | 2.7, 3.4, 3.5, 3.6, |x86-64 | -| | pypy, pypy3 | | -| | | | -| | 2.7 |x86 | -+----------------------------------+-------------------------------+-----------------------+ -| Windows Server 2012 R2 | 2.7, 3.4 |x86, x86-64 | -| | | | -| | pypy, 3.5/mingw |x86 | -+----------------------------------+-------------------------------+-----------------------+ - -\* Mac OS X CI is not run for every commit, but is run for every release. ++----------------------------------+----------------------------+---------------------+ +| Operating system | Tested Python versions | Tested architecture | ++==================================+============================+=====================+ +| Alpine | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Amazon Linux 2 | 3.7 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Arch | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| CentOS 7 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| CentOS Stream 8 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| CentOS Stream 9 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Debian 10 Buster | 3.7 | x86 | ++----------------------------------+----------------------------+---------------------+ +| Debian 11 Bullseye | 3.9 | x86 | ++----------------------------------+----------------------------+---------------------+ +| Fedora 35 | 3.10 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Fedora 36 | 3.10 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Gentoo | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | PyPy3 | | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | PyPy3 | | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | arm64v8, ppc64le, | +| | | s390x, x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Windows Server 2016 | 3.7 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Windows Server 2022 | 3.7, 3.8, 3.9, 3.10, 3.11, | x86, x86-64 | +| | PyPy3 | | +| +----------------------------+---------------------+ +| | 3.9 (MinGW) | x86, x86-64 | +| +----------------------------+---------------------+ +| | 3.7, 3.8, 3.9 (Cygwin) | x86-64 | ++----------------------------------+----------------------------+---------------------+ + Other Platforms ^^^^^^^^^^^^^^^ @@ -414,60 +478,91 @@ These platforms have been reported to work at the versions mentioned. Contributors please test Pillow on your platform then update this document and send a pull request. -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -|**Operating system** |**Tested Python versions** |**Latest tested Pillow version**|**Tested processors** | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Mac OS X 10.11 El Capitan | 2.7, 3.3, 3.4, 3.5 | 4.1.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Redhat Linux 6 | 2.6 | |x86 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| CentOS 6.3 | 2.7, 3.3 | |x86 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Ubuntu Linux 12.04 LTS | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | -| | PyPy5.3.1, PyPy3 v2.4.0 | | | -| | | | | -| | 2.7 | 4.3.0 |x86-64 | -| | | | | -| | 2.7, 3.2 | 3.4.1 |ppc | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Ubuntu Linux 10.04 LTS | 2.6 | 2.3.0 |x86,x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Raspian Jessie | 2.7, 3.4 | 3.1.0 |arm | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Windows 7 Pro | 2.7, 3.2, 3.3 | 3.4.1 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ ++----------------------------------+---------------------------+------------------+--------------+ +| 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 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.6 | 8.4.0 | | ++----------------------------------+---------------------------+------------------+--------------+ +| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 | +| +---------------------------+------------------+ | +| | 3.5 | 7.2.0 | | ++----------------------------------+---------------------------+------------------+--------------+ +| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 | +| +---------------------------+------------------+ | +| | 2.7 | 6.0.0 | | +| +---------------------------+------------------+ | +| | 3.4 | 5.4.1 | | ++----------------------------------+---------------------------+------------------+--------------+ +| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 | +| +---------------------------+------------------+ | +| | 3.3 | 4.1.0 | | ++----------------------------------+---------------------------+------------------+--------------+ +| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Redhat Linux 6 | 2.6 | |x86 | ++----------------------------------+---------------------------+------------------+--------------+ +| CentOS 6.3 | 2.7, 3.3 | |x86 | ++----------------------------------+---------------------------+------------------+--------------+ +| CentOS 8 | 3.9 | 9.0.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | +| | | PyPy5.3.1, PyPy3 v2.4.0 | | | +| +---------------------------+------------------+--------------+ +| | 2.7 | 4.3.0 |x86-64 | +| +---------------------------+------------------+--------------+ +| | 2.7, 3.2 | 3.4.1 |ppc | ++----------------------------------+---------------------------+------------------+--------------+ +| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm | ++----------------------------------+---------------------------+------------------+--------------+ +| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm | ++----------------------------------+---------------------------+------------------+--------------+ +| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm | +| +---------------------------+------------------+ | +| | 2.7 | 6.2.2 | | ++----------------------------------+---------------------------+------------------+--------------+ +| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Windows 10 | 3.7 | 7.1.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ Old Versions ------------ -You can download old distributions from `PyPI -`_. Only the latest major -releases for Python 2.x and 3.x are visible, but all releases are -available by direct URL access -e.g. https://pypi.python.org/pypi/Pillow/1.0. +You can download old distributions from the `release history at PyPI +`_ and by direct URL access +eg. https://pypi.org/project/Pillow/1.0/. diff --git a/docs/newer-versions.csv b/docs/newer-versions.csv new file mode 100644 index 00000000000..ed2369259d4 --- /dev/null +++ b/docs/newer-versions.csv @@ -0,0 +1,6 @@ +Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5 +Pillow >= 9.3,Yes,Yes,Yes,Yes,Yes,, +Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,, +Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes, +Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes, +Pillow 7.0 - 7.2,,,,Yes,Yes,Yes,Yes diff --git a/docs/older-versions.csv b/docs/older-versions.csv new file mode 100644 index 00000000000..6058f0524ad --- /dev/null +++ b/docs/older-versions.csv @@ -0,0 +1,8 @@ +Python,3.8,3.7,3.6,3.5,3.4,3.3,3.2,2.7,2.6,2.5,2.4 +Pillow 6.2.1 - 6.2.2,Yes,Yes,Yes,Yes,,,,Yes,,, +Pillow 6.0 - 6.2.0,,Yes,Yes,Yes,,,,Yes,,, +Pillow 5.2 - 5.4,,Yes,Yes,Yes,Yes,,,Yes,,, +Pillow 5.0 - 5.1,,,Yes,Yes,Yes,,,Yes,,, +Pillow 4,,,Yes,Yes,Yes,Yes,,Yes,,, +Pillow 2 - 3,,,,Yes,Yes,Yes,Yes,Yes,Yes,, +Pillow < 2,,,,,,,,Yes,Yes,Yes,Yes \ No newline at end of file diff --git a/docs/porting.rst b/docs/porting.rst index 50b713fac3f..2943d72fd69 100644 --- a/docs/porting.rst +++ b/docs/porting.rst @@ -3,9 +3,14 @@ Porting **Porting existing PIL-based code to Pillow** -Pillow is a functional drop-in replacement for the Python Imaging Library. To -run your existing PIL-compatible code with Pillow, it needs to be modified to -import the ``Image`` module from the ``PIL`` namespace *instead* of the +Pillow is a functional drop-in replacement for the Python Imaging Library. + +PIL is Python 2 only. Pillow dropped support for Python 2 in Pillow +7.0. So if you would like to run the latest version of Pillow, you will first +and foremost need to port your code from Python 2 to 3. + +To run your existing PIL-compatible code with Pillow, it needs to be modified +to import the ``Image`` module from the ``PIL`` namespace *instead* of the global namespace. Change this:: import Image @@ -14,7 +19,8 @@ to this:: from PIL import Image -The :py:mod:`_imaging` module has been moved. You can now import it like this:: +The :py:mod:`PIL._imaging` module has been moved to :py:mod:`PIL.Image.core`. +You can now import it like this:: from PIL.Image import core as _imaging diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 9fc7cd13ba3..ff57885240e 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -1,26 +1,47 @@ .. py:module:: PIL.ExifTags .. py:currentmodule:: PIL.ExifTags -:py:mod:`ExifTags` Module -========================== +:py:mod:`~PIL.ExifTags` Module +============================== -The :py:mod:`ExifTags` module exposes two dictionaries which +The :py:mod:`~PIL.ExifTags` module exposes two dictionaries which provide constants and clear-text names for various well-known EXIF tags. -.. py:class:: PIL.ExifTags.TAGS +.. py:data:: TAGS + :type: dict - The TAG dictionary maps 16-bit integer EXIF tag enumerations to - descriptive string names. For instance: + The TAGS dictionary maps 16-bit integer EXIF tag enumerations to + descriptive string names. For instance: >>> from PIL.ExifTags import TAGS >>> TAGS[0x010e] 'ImageDescription' -.. py:class:: PIL.ExifTags.GPSTAGS +.. 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] 'GPSDestLatitude' + + +These values are also exposed as ``enum.IntEnum`` classes. + +.. 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' diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 915f61c0467..7f6f666c33c 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -1,8 +1,8 @@ .. py:module:: PIL.Image .. py:currentmodule:: PIL.Image -:py:mod:`Image` Module -====================== +:py:mod:`~PIL.Image` Module +=========================== The :py:mod:`~PIL.Image` module provides a class with the same name which is used to represent a PIL image. The module also provides a number of factory @@ -12,25 +12,25 @@ images. Examples -------- -The following script loads an image, rotates it 45 degrees, and displays it -using an external viewer (usually xv on Unix, and the paint program on -Windows). - Open, rotate, and display an image (using the default viewer) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The following script loads an image, rotates it 45 degrees, and displays it +using an external viewer (usually xv on Unix, and the Paint program on +Windows). + .. code-block:: python from PIL import Image - im = Image.open("bride.jpg") - im.rotate(45).show() - -The following script creates nice thumbnails of all JPEG images in the -current directory preserving aspect ratios with 128x128 max resolution. + with Image.open("hopper.jpg") as im: + im.rotate(45).show() Create thumbnails ^^^^^^^^^^^^^^^^^ +The following script creates nice thumbnails of all JPEG images in the +current directory preserving aspect ratios with 128x128 max resolution. + .. code-block:: python from PIL import Image @@ -40,9 +40,9 @@ Create thumbnails for infile in glob.glob("*.jpg"): file, ext = os.path.splitext(infile) - im = Image.open(infile) - im.thumbnail(size) - im.save(file + ".thumbnail", "JPEG") + with Image.open(infile) as im: + im.thumbnail(size) + im.save(file + ".thumbnail", "JPEG") Functions --------- @@ -52,14 +52,22 @@ Functions .. warning:: To protect against potential DOS attacks caused by "`decompression bombs`_" (i.e. malicious files which decompress into a huge amount of data and are designed to crash or cause disruption by using up - a lot of memory), Pillow will issue a `DecompressionBombWarning` if the image is over a certain - limit. If desired, the warning can be turned into an error with + a lot of memory), Pillow will issue a ``DecompressionBombWarning`` if the number of pixels in an + image is over a certain limit, :py:data:`MAX_IMAGE_PIXELS`. + + This threshold can be changed by setting :py:data:`MAX_IMAGE_PIXELS`. It can be disabled + by setting ``Image.MAX_IMAGE_PIXELS = None``. + + If desired, the warning can be turned into an error with ``warnings.simplefilter('error', Image.DecompressionBombWarning)`` or suppressed entirely with - ``warnings.simplefilter('ignore', Image.DecompressionBombWarning)``. See also `the logging - documentation`_ to have warnings output to the logging facility instead of stderr. + ``warnings.simplefilter('ignore', Image.DecompressionBombWarning)``. See also + `the logging documentation`_ to have warnings output to the logging facility instead of stderr. + + If the number of pixels is greater than twice :py:data:`MAX_IMAGE_PIXELS`, then a + ``DecompressionBombError`` will be raised instead. - .. _decompression bombs: https://en.wikipedia.org/wiki/Zip_bomb - .. _the logging documentation: https://docs.python.org/2/library/logging.html?highlight=logging#integration-with-the-warnings-module + .. _decompression bombs: https://en.wikipedia.org/wiki/Zip_bomb + .. _the logging documentation: https://docs.python.org/3/library/logging.html#integration-with-the-warnings-module Image processing ^^^^^^^^^^^^^^^^ @@ -76,9 +84,16 @@ Constructing images .. autofunction:: new .. autofunction:: fromarray .. autofunction:: frombytes -.. autofunction:: fromstring .. autofunction:: frombuffer +Generating images +^^^^^^^^^^^^^^^^^ + +.. autofunction:: effect_mandelbrot +.. autofunction:: effect_noise +.. autofunction:: linear_gradient +.. autofunction:: radial_gradient + Registering plugins ^^^^^^^^^^^^^^^^^^^ @@ -88,12 +103,14 @@ Registering plugins ignore them. .. autofunction:: register_open -.. autofunction:: register_decoder .. autofunction:: register_mime .. autofunction:: register_save -.. autofunction:: register_encoder +.. autofunction:: register_save_all .. autofunction:: register_extension - +.. autofunction:: register_extensions +.. autofunction:: registered_extensions +.. autofunction:: register_decoder +.. autofunction:: register_encoder The Image Class --------------- @@ -106,6 +123,7 @@ methods. Unless otherwise stated, all methods return a new instance of the .. automethod:: PIL.Image.Image.alpha_composite +.. automethod:: PIL.Image.Image.apply_transparency .. automethod:: PIL.Image.Image.convert The following example converts an RGB image (linearly calibrated according to @@ -116,22 +134,78 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: rgb2xyz = ( 0.412453, 0.357580, 0.180423, 0, 0.212671, 0.715160, 0.072169, 0, - 0.019334, 0.119193, 0.950227, 0 ) + 0.019334, 0.119193, 0.950227, 0) out = im.convert("RGB", rgb2xyz) .. automethod:: PIL.Image.Image.copy .. automethod:: PIL.Image.Image.crop + +This crops the input image with the provided coordinates: + +.. code-block:: python + + from PIL import Image + + with Image.open("hopper.jpg") as im: + + # The crop method from the Image module takes four coordinates as input. + # The right can also be represented as (left+width) + # and lower can be represented as (upper+height). + (left, upper, right, lower) = (20, 20, 100, 100) + + # Here the image "im" is cropped and assigned to new variable im_crop + im_crop = im.crop((left, upper, right, lower)) + + .. automethod:: PIL.Image.Image.draft +.. automethod:: PIL.Image.Image.effect_spread +.. automethod:: PIL.Image.Image.entropy .. automethod:: PIL.Image.Image.filter + +This blurs the input image using a filter from the ``ImageFilter`` module: + +.. code-block:: python + + from PIL import Image, ImageFilter + + with Image.open("hopper.jpg") as im: + + # Blur the input image using the filter ImageFilter.BLUR + im_blurred = im.filter(filter=ImageFilter.BLUR) + +.. automethod:: PIL.Image.Image.frombytes .. automethod:: PIL.Image.Image.getbands + +This helps to get the bands of the input image: + +.. code-block:: python + + from PIL import Image + + with Image.open("hopper.jpg") as im: + print(im.getbands()) # Returns ('R', 'G', 'B') + .. automethod:: PIL.Image.Image.getbbox + +This helps to get the bounding box coordinates of the input image: + +.. code-block:: python + + from PIL import Image + + with Image.open("hopper.jpg") as im: + print(im.getbbox()) + # Returns four coordinates in the format (left, upper, right, lower) + +.. automethod:: PIL.Image.Image.getchannel .. automethod:: PIL.Image.Image.getcolors .. automethod:: PIL.Image.Image.getdata +.. automethod:: PIL.Image.Image.getexif .. automethod:: PIL.Image.Image.getextrema .. automethod:: PIL.Image.Image.getpalette .. automethod:: PIL.Image.Image.getpixel +.. automethod:: PIL.Image.Image.getprojection .. automethod:: PIL.Image.Image.histogram -.. automethod:: PIL.Image.Image.offset .. automethod:: PIL.Image.Image.paste .. automethod:: PIL.Image.Image.point .. automethod:: PIL.Image.Image.putalpha @@ -139,84 +213,118 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.putpalette .. automethod:: PIL.Image.Image.putpixel .. automethod:: PIL.Image.Image.quantize -.. automethod:: PIL.Image.Image.resize +.. automethod:: PIL.Image.Image.reduce .. automethod:: PIL.Image.Image.remap_palette +.. automethod:: PIL.Image.Image.resize + +This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``: + +.. code-block:: python + + from PIL import Image + + with Image.open("hopper.jpg") as im: + + # Provide the target width and height of the image + (width, height) = (im.width // 2, im.height // 2) + im_resized = im.resize((width, height)) + .. automethod:: PIL.Image.Image.rotate + +This rotates the input image by ``theta`` degrees counter clockwise: + +.. code-block:: python + + from PIL import Image + + with Image.open("hopper.jpg") as im: + + # Rotate the image by 60 degrees counter clockwise + theta = 60 + # Angle is in degrees counter clockwise + im_rotated = im.rotate(angle=theta) + .. automethod:: PIL.Image.Image.save .. automethod:: PIL.Image.Image.seek .. automethod:: PIL.Image.Image.show .. automethod:: PIL.Image.Image.split -.. automethod:: PIL.Image.Image.getchannel .. automethod:: PIL.Image.Image.tell .. automethod:: PIL.Image.Image.thumbnail .. automethod:: PIL.Image.Image.tobitmap .. automethod:: PIL.Image.Image.tobytes -.. automethod:: PIL.Image.Image.tostring .. automethod:: PIL.Image.Image.transform .. automethod:: PIL.Image.Image.transpose -.. automethod:: PIL.Image.Image.verify -.. automethod:: PIL.Image.Image.fromstring +This flips the input image by using the :data:`Transpose.FLIP_LEFT_RIGHT` +method. + +.. code-block:: python + + from PIL import Image + + with Image.open("hopper.jpg") as im: + + # Flip the image from left to right + im_flipped = im.transpose(method=Image.Transpose.FLIP_LEFT_RIGHT) + # To flip the image from top to bottom, + # use the method "Image.Transpose.FLIP_TOP_BOTTOM" + + +.. automethod:: PIL.Image.Image.verify .. automethod:: PIL.Image.Image.load .. automethod:: PIL.Image.Image.close -Attributes ----------- +Image Attributes +---------------- Instances of the :py:class:`Image` class have the following attributes: -.. py:attribute:: filename +.. py:attribute:: Image.filename + :type: str - The filename or path of the source file. Only images created with the - factory function `open` have a filename attribute. If the input is a + The filename or path of the source file. Only images created with the + factory function ``open`` have a filename attribute. If the input is a file like object, the filename attribute is set to an empty string. - - :type: :py:class: `string` -.. py:attribute:: format +.. py:attribute:: Image.format + :type: Optional[str] The file format of the source file. For images created by the library itself (via a factory function, or by running a method on an existing - image), this attribute is set to ``None``. - - :type: :py:class:`string` or ``None`` + image), this attribute is set to :data:`None`. -.. py:attribute:: mode +.. py:attribute:: Image.mode + :type: str Image mode. This is a string specifying the pixel format used by the image. Typical values are “1”, “L”, “RGB”, or “CMYK.” See :ref:`concept-modes` for a full list. - :type: :py:class:`string` - -.. py:attribute:: size +.. py:attribute:: Image.size + :type: tuple[int] Image size, in pixels. The size is given as a 2-tuple (width, height). - :type: ``(width, height)`` - -.. py:attribute:: width +.. py:attribute:: Image.width + :type: int Image width, in pixels. - :type: :py:class:`int` - -.. py:attribute:: height +.. py:attribute:: Image.height + :type: int Image height, in pixels. - :type: :py:class:`int` - -.. py:attribute:: palette - - Colour palette table, if any. If mode is “P”, this should be an instance of - the :py:class:`~PIL.ImagePalette.ImagePalette` class. Otherwise, it should - be set to ``None``. +.. py:attribute:: Image.palette + :type: Optional[PIL.ImagePalette.ImagePalette] - :type: :py:class:`~PIL.ImagePalette.ImagePalette` or ``None`` + Colour palette table, if any. If mode is "P" or "PA", this should be an + instance of the :py:class:`~PIL.ImagePalette.ImagePalette` class. + Otherwise, it should be set to :data:`None`. -.. py:attribute:: info +.. py:attribute:: Image.info + :type: dict A dictionary holding data associated with the image. This dictionary is used by file handlers to pass on various non-image information read from @@ -229,4 +337,168 @@ Instances of the :py:class:`Image` class have the following attributes: Unless noted elsewhere, this dictionary does not affect saving files. - :type: :py:class:`dict` +.. py:attribute:: Image.is_animated + :type: bool + + ``True`` if this image has more than one frame, or ``False`` otherwise. + + This attribute is only defined by image plugins that support animated images. + Plugins may leave this attribute undefined if they don't support loading + animated images, even if the given format supports animated images. + + Given that this attribute is not present for all images use + ``getattr(image, "is_animated", False)`` to check if Pillow is aware of multiple + frames in an image regardless of its format. + + .. seealso:: :attr:`~Image.n_frames`, :func:`~Image.seek` and :func:`~Image.tell` + +.. py:attribute:: Image.n_frames + :type: int + + The number of frames in this image. + + This attribute is only defined by image plugins that support animated images. + Plugins may leave this attribute undefined if they don't support loading + animated images, even if the given format supports animated images. + + Given that this attribute is not present for all images use + ``getattr(image, "n_frames", 1)`` to check the number of frames that Pillow is + aware of in an image regardless of its format. + + .. seealso:: :attr:`~Image.is_animated`, :func:`~Image.seek` and :func:`~Image.tell` + +Classes +------- + +.. autoclass:: PIL.Image.Exif + :members: + :undoc-members: + :show-inheritance: +.. autoclass:: PIL.Image.ImagePointHandler +.. autoclass:: PIL.Image.ImageTransformHandler + +Constants +--------- + +.. data:: NONE +.. data:: MAX_IMAGE_PIXELS + + Set to 89,478,485, approximately 0.25GB for a 24-bit (3 bpp) image. + See :py:meth:`~PIL.Image.open` for more information about how this is used. + +Transpose methods +^^^^^^^^^^^^^^^^^ + +Used to specify the :meth:`Image.transpose` method to use. + +.. autoclass:: Transpose + :members: + :undoc-members: + +Transform methods +^^^^^^^^^^^^^^^^^ + +Used to specify the :meth:`Image.transform` method to use. + +.. py:class:: Transform + + .. py:attribute:: AFFINE + + Affine transform + + .. py:attribute:: EXTENT + + Cut out a rectangular subregion + + .. py:attribute:: PERSPECTIVE + + Perspective transform + + .. py:attribute:: QUAD + + Map a quadrilateral to a rectangle + + .. py:attribute:: MESH + + Map a number of source quadrilaterals in one operation + +Resampling filters +^^^^^^^^^^^^^^^^^^ + +See :ref:`concept-filters` for details. + +.. autoclass:: Resampling + :members: + :undoc-members: + +Some deprecated filters are also available under the following names: + +.. data:: NONE + :noindex: + :value: Resampling.NEAREST +.. data:: LINEAR + :value: Resampling.BILINEAR +.. data:: CUBIC + :value: Resampling.BICUBIC +.. data:: ANTIALIAS + :value: Resampling.LANCZOS + +Dither modes +^^^^^^^^^^^^ + +Used to specify the dithering method to use for the +:meth:`~Image.convert` and :meth:`~Image.quantize` methods. + +.. py:class:: Dither + + .. py:attribute:: NONE + + No dither + + .. py:attribute:: ORDERED + + Not implemented + + .. py:attribute:: RASTERIZE + + Not implemented + + .. py:attribute:: FLOYDSTEINBERG + + Floyd-Steinberg dither + +Palettes +^^^^^^^^ + +Used to specify the pallete to use for the :meth:`~Image.convert` method. + +.. autoclass:: Palette + :members: + :undoc-members: + +Quantization methods +^^^^^^^^^^^^^^^^^^^^ + +Used to specify the quantization method to use for the :meth:`~Image.quantize` method. + +.. py:class:: Quantize + + .. py:attribute:: MEDIANCUT + + Median cut. Default method, except for RGBA images. This method does not support + RGBA images. + + .. py:attribute:: MAXCOVERAGE + + Maximum coverage. This method does not support RGBA images. + + .. py:attribute:: FASTOCTREE + + Fast octree. Default method for RGBA images. + + .. py:attribute:: LIBIMAGEQUANT + + libimagequant + + Check support using :py:func:`PIL.features.check_feature` with + ``feature="libimagequant"``. diff --git a/docs/reference/ImageChops.rst b/docs/reference/ImageChops.rst index 2e4e21f1987..9519361a7e6 100644 --- a/docs/reference/ImageChops.rst +++ b/docs/reference/ImageChops.rst @@ -1,15 +1,15 @@ .. py:module:: PIL.ImageChops .. py:currentmodule:: PIL.ImageChops -:py:mod:`ImageChops` ("Channel Operations") Module -================================================== +:py:mod:`~PIL.ImageChops` ("Channel Operations") Module +======================================================= -The :py:mod:`ImageChops` module contains a number of arithmetical image +The :py:mod:`~PIL.ImageChops` module contains a number of arithmetical image operations, called channel operations (“chops”). These can be used for various purposes, including special effects, image compositions, algorithmic painting, and more. -For more pre-made operations, see :py:mod:`ImageOps`. +For more pre-made operations, see :py:mod:`~PIL.ImageOps`. At this time, most channel operations are only implemented for 8-bit images (e.g. “L” and “RGB”). @@ -34,13 +34,12 @@ operations in this module). .. autofunction:: PIL.ImageChops.lighter .. autofunction:: PIL.ImageChops.logical_and .. autofunction:: PIL.ImageChops.logical_or +.. autofunction:: PIL.ImageChops.logical_xor .. autofunction:: PIL.ImageChops.multiply -.. py:method:: PIL.ImageChops.offset(image, xoffset, yoffset=None) - - Returns a copy of the image where data has been offset by the given - distances. Data wraps around the edges. If **yoffset** is omitted, it - is assumed to be equal to **xoffset**. - +.. autofunction:: PIL.ImageChops.soft_light +.. autofunction:: PIL.ImageChops.hard_light +.. autofunction:: PIL.ImageChops.overlay +.. autofunction:: PIL.ImageChops.offset .. autofunction:: PIL.ImageChops.screen .. autofunction:: PIL.ImageChops.subtract .. autofunction:: PIL.ImageChops.subtract_modulo diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 35f4acee64c..9b9b5e7b29e 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -1,16 +1,37 @@ .. py:module:: PIL.ImageCms .. py:currentmodule:: PIL.ImageCms -:py:mod:`ImageCms` Module -========================= +:py:mod:`~PIL.ImageCms` Module +============================== -The :py:mod:`ImageCms` module provides color profile management +The :py:mod:`~PIL.ImageCms` module provides color profile management support using the LittleCMS2 color management engine, based on Kevin Cazabon's PyCMS library. -.. automodule:: PIL.ImageCms - :members: - :noindex: +.. autoclass:: ImageCmsTransform +.. autoexception:: PyCMSError + +Functions +--------- + +.. autofunction:: applyTransform +.. autofunction:: buildProofTransform +.. autofunction:: buildProofTransformFromOpenProfiles +.. autofunction:: buildTransform +.. autofunction:: buildTransformFromOpenProfiles +.. autofunction:: createProfile +.. autofunction:: getDefaultIntent +.. autofunction:: getOpenProfile +.. autofunction:: getProfileCopyright +.. autofunction:: getProfileDescription +.. autofunction:: getProfileInfo +.. autofunction:: getProfileManufacturer +.. autofunction:: getProfileModel +.. autofunction:: getProfileName +.. autofunction:: get_display_profile +.. autofunction:: isIntentSupported +.. autofunction:: profileToProfile +.. autofunction:: versions CmsProfile ---------- @@ -25,232 +46,210 @@ can be easily displayed in a chromaticity diagram, for example). .. py:class:: CmsProfile .. py:attribute:: creation_date + :type: Optional[datetime.datetime] Date and time this profile was first created (see 7.2.1 of ICC.1:2010). - :type: :py:class:`datetime.datetime` or ``None`` - .. py:attribute:: version + :type: float The version number of the ICC standard that this profile follows - (e.g. `2.0`). - - :type: :py:class:`float` + (e.g. ``2.0``). .. py:attribute:: icc_version + :type: int - Same as `version`, but in encoded format (see 7.2.4 of ICC.1:2010). + Same as ``version``, but in encoded format (see 7.2.4 of ICC.1:2010). .. py:attribute:: device_class + :type: str 4-character string identifying the profile class. One of ``scnr``, ``mntr``, ``prtr``, ``link``, ``spac``, ``abst``, ``nmcl`` (see 7.2.5 of ICC.1:2010 for details). - :type: :py:class:`string` - .. py:attribute:: xcolor_space + :type: str 4-character string (padded with whitespace) identifying the color space, e.g. ``XYZ␣``, ``RGB␣`` or ``CMYK`` (see 7.2.6 of ICC.1:2010 for details). - Note that the deprecated attribute ``color_space`` contains an - interpreted (non-padded) variant of this (but can be empty on - unknown input). - - :type: :py:class:`string` - .. py:attribute:: connection_space + :type: str 4-character string (padded with whitespace) identifying the color space on the B-side of the transform (see 7.2.7 of ICC.1:2010 for details). - Note that the deprecated attribute ``pcs`` contains an interpreted - (non-padded) variant of this (but can be empty on unknown input). - - :type: :py:class:`string` - .. py:attribute:: header_flags + :type: int The encoded header flags of the profile (see 7.2.11 of ICC.1:2010 for details). - :type: :py:class:`int` - .. py:attribute:: header_manufacturer + :type: str 4-character string (padded with whitespace) identifying the device manufacturer, which shall match the signature contained in the appropriate section of the ICC signature registry found at www.color.org (see 7.2.12 of ICC.1:2010). - :type: :py:class:`string` - .. py:attribute:: header_model + :type: str 4-character string (padded with whitespace) identifying the device model, which shall match the signature contained in the appropriate section of the ICC signature registry found at www.color.org (see 7.2.13 of ICC.1:2010). - :type: :py:class:`string` - .. py:attribute:: attributes + :type: int Flags used to identify attributes unique to the particular device setup for which the profile is applicable (see 7.2.14 of ICC.1:2010 for details). - :type: :py:class:`int` - .. py:attribute:: rendering_intent + :type: int The rendering intent to use when combining this profile with another profile (usually overridden at run-time, but provided here for DeviceLink and embedded source profiles, see 7.2.15 of ICC.1:2010). - One of ``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, ``ImageCms.INTENT_PERCEPTUAL``, - ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and ``ImageCms.INTENT_SATURATION``. - - :type: :py:class:`int` + One of ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, ``ImageCms.Intent.PERCEPTUAL``, + ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and ``ImageCms.Intent.SATURATION``. .. py:attribute:: profile_id + :type: bytes A sequence of 16 bytes identifying the profile (via a specially constructed MD5 sum), or 16 binary zeroes if the profile ID has not been calculated (see 7.2.18 of ICC.1:2010). - :type: :py:class:`bytes` - .. py:attribute:: copyright + :type: Optional[str] The text copyright information for the profile (see 9.2.21 of ICC.1:2010). - :type: :py:class:`unicode` or ``None`` - .. py:attribute:: manufacturer + :type: Optional[str] - The (english) display string for the device manufacturer (see + The (English) display string for the device manufacturer (see 9.2.22 of ICC.1:2010). - :type: :py:class:`unicode` or ``None`` - .. py:attribute:: model + :type: Optional[str] - The (english) display string for the device model of the device + The (English) display string for the device model of the device for which this profile is created (see 9.2.23 of ICC.1:2010). - :type: :py:class:`unicode` or ``None`` - .. py:attribute:: profile_description + :type: Optional[str] - The (english) display string for the profile description (see + The (English) display string for the profile description (see 9.2.41 of ICC.1:2010). - :type: :py:class:`unicode` or ``None`` - .. py:attribute:: target + :type: Optional[str] The name of the registered characterization data set, or the measurement data for a characterization target (see 9.2.14 of ICC.1:2010). - :type: :py:class:`unicode` or ``None`` - .. py:attribute:: red_colorant + :type: Optional[tuple[tuple[float]]] The first column in the matrix used in matrix/TRC transforms (see 9.2.44 of ICC.1:2010). - :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. .. py:attribute:: green_colorant + :type: Optional[tuple[tuple[float]]] The second column in the matrix used in matrix/TRC transforms (see 9.2.30 of ICC.1:2010). - :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. .. py:attribute:: blue_colorant + :type: Optional[tuple[tuple[float]]] The third column in the matrix used in matrix/TRC transforms (see 9.2.4 of ICC.1:2010). - :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. .. py:attribute:: luminance + :type: Optional[tuple[tuple[float]]] The absolute luminance of emissive devices in candelas per square metre as described by the Y channel (see 9.2.32 of ICC.1:2010). - :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. .. py:attribute:: chromaticity + :type: Optional[tuple[tuple[float]]] The data of the phosphor/colorant chromaticity set used (red, green and blue channels, see 9.2.16 of ICC.1:2010). - :type: ``((x, y, Y), (x, y, Y), (x, y, Y))`` or ``None`` + The value is in the format ``((x, y, Y), (x, y, Y), (x, y, Y))``, if available. .. py:attribute:: chromatic_adaption + :type: tuple[tuple[float]] The chromatic adaption matrix converts a color measured using the actual illumination conditions and relative to the actual adopted - white, to an color relative to the PCS adopted white, with + white, to a color relative to the PCS adopted white, with complete adaptation from the actual adopted white chromaticity to the PCS adopted white chromaticity (see 9.2.15 of ICC.1:2010). - Two matrices are returned, one in (X, Y, Z) space and one in (x, y, Y) space. - - :type: 2-tuple of 3-tuple, the first with (X, Y, Z) and the second with (x, y, Y) values + Two 3-tuples of floats are returned in a 2-tuple, + one in (X, Y, Z) space and one in (x, y, Y) space. .. py:attribute:: colorant_table + :type: list[str] This tag identifies the colorants used in the profile by a unique name and set of PCSXYZ or PCSLAB values (see 9.2.19 of ICC.1:2010). - :type: list of strings - .. py:attribute:: colorant_table_out + :type: list[str] This tag identifies the colorants used in the profile by a unique name and set of PCSLAB values (for DeviceLink profiles only, see 9.2.19 of ICC.1:2010). - :type: list of strings - .. py:attribute:: colorimetric_intent + :type: Optional[str] 4-character string (padded with whitespace) identifying the image state of PCS colorimetry produced using the colorimetric intent transforms (see 9.2.20 of ICC.1:2010 for details). - :type: :py:class:`string` or ``None`` - .. py:attribute:: perceptual_rendering_intent_gamut + :type: Optional[str] 4-character string (padded with whitespace) identifying the (one) standard reference medium gamut (see 9.2.37 of ICC.1:2010 for details). - :type: :py:class:`string` or ``None`` - .. py:attribute:: saturation_rendering_intent_gamut + :type: Optional[str] 4-character string (padded with whitespace) identifying the (one) standard reference medium gamut (see 9.2.37 of ICC.1:2010 for details). - :type: :py:class:`string` or ``None`` - .. py:attribute:: technology + :type: Optional[str] 4-character string (padded with whitespace) identifying the device technology (see 9.2.47 of ICC.1:2010 for details). - :type: :py:class:`string` or ``None`` - .. py:attribute:: media_black_point + :type: Optional[tuple[tuple[float]]] This tag specifies the media black point and is used for generating absolute colorimetry. @@ -258,151 +257,92 @@ can be easily displayed in a chromaticity diagram, for example). This tag was available in ICC 3.2, but it is removed from version 4. - :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. .. py:attribute:: media_white_point_temperature + :type: Optional[float] Calculates the white point temperature (see the LCMS documentation for more information). - :type: :py:class:`float` or ``None`` - .. py:attribute:: viewing_condition + :type: Optional[str] - The (english) display string for the viewing conditions (see + The (English) display string for the viewing conditions (see 9.2.48 of ICC.1:2010). - :type: :py:class:`unicode` or ``None`` - .. py:attribute:: screening_description + :type: Optional[str] - The (english) display string for the screening conditions. + The (English) display string for the screening conditions. This tag was available in ICC 3.2, but it is removed from version 4. - :type: :py:class:`unicode` or ``None`` - .. py:attribute:: red_primary + :type: Optional[tuple[tuple[float]]] The XYZ-transformed of the RGB primary color red (1, 0, 0). - :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. .. py:attribute:: green_primary + :type: Optional[tuple[tuple[float]]] The XYZ-transformed of the RGB primary color green (0, 1, 0). - :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. .. py:attribute:: blue_primary + :type: Optional[tuple[tuple[float]]] The XYZ-transformed of the RGB primary color blue (0, 0, 1). - :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. .. py:attribute:: is_matrix_shaper + :type: bool True if this profile is implemented as a matrix shaper (see documentation on LCMS). - :type: :py:class:`bool` - .. py:attribute:: clut + :type: dict[tuple[bool]] Returns a dictionary of all supported intents and directions for the CLUT model. The dictionary is indexed by intents - (``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, - ``ImageCms.INTENT_PERCEPTUAL``, - ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and - ``ImageCms.INTENT_SATURATION``). + (``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, + ``ImageCms.Intent.PERCEPTUAL``, + ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and + ``ImageCms.Intent.SATURATION``). The values are 3-tuples indexed by directions - (``ImageCms.DIRECTION_INPUT``, ``ImageCms.DIRECTION_OUTPUT``, - ``ImageCms.DIRECTION_PROOF``). + (``ImageCms.Direction.INPUT``, ``ImageCms.Direction.OUTPUT``, + ``ImageCms.Direction.PROOF``). The elements of the tuple are booleans. If the value is ``True``, that intent is supported for that direction. - :type: :py:class:`dict` of boolean 3-tuples - .. py:attribute:: intent_supported + :type: dict[tuple[bool]] Returns a dictionary of all supported intents and directions. The dictionary is indexed by intents - (``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, - ``ImageCms.INTENT_PERCEPTUAL``, - ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and - ``ImageCms.INTENT_SATURATION``). + (``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, + ``ImageCms.Intent.PERCEPTUAL``, + ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and + ``ImageCms.Intent.SATURATION``). The values are 3-tuples indexed by directions - (``ImageCms.DIRECTION_INPUT``, ``ImageCms.DIRECTION_OUTPUT``, - ``ImageCms.DIRECTION_PROOF``). + (``ImageCms.Direction.INPUT``, ``ImageCms.Direction.OUTPUT``, + ``ImageCms.Direction.PROOF``). The elements of the tuple are booleans. If the value is ``True``, that intent is supported for that direction. - :type: :py:class:`dict` of boolean 3-tuples - - .. py:attribute:: color_space - - Deprecated but retained for backwards compatibility. - Interpreted value of :py:attr:`.xcolor_space`. May be the - empty string if value could not be decoded. - - :type: :py:class:`string` - - .. py:attribute:: pcs - - Deprecated but retained for backwards compatibility. - Interpreted value of :py:attr:`.connection_space`. May be - the empty string if value could not be decoded. - - :type: :py:class:`string` - - .. py:attribute:: product_model - - Deprecated but retained for backwards compatibility. - ASCII-encoded value of :py:attr:`.model`. - - :type: :py:class:`string` - - .. py:attribute:: product_manufacturer - - Deprecated but retained for backwards compatibility. - ASCII-encoded value of :py:attr:`.manufacturer`. - - :type: :py:class:`string` - - .. py:attribute:: product_copyright - - Deprecated but retained for backwards compatibility. - ASCII-encoded value of :py:attr:`.copyright`. - - :type: :py:class:`string` - - .. py:attribute:: product_description - - Deprecated but retained for backwards compatibility. - ASCII-encoded value of :py:attr:`.profile_description`. - - :type: :py:class:`string` - - .. py:attribute:: product_desc - - Deprecated but retained for backwards compatibility. - ASCII-encoded value of :py:attr:`.profile_description`. - - This alias of :py:attr:`.product_description` used to - contain a derived informative string about the profile, - depending on the value of the description, copyright, - manufacturer and model fields). - - :type: :py:class:`string` - There is one function defined on the class: .. py:method:: is_intent_supported(intent, direction) @@ -412,11 +352,11 @@ can be easily displayed in a chromaticity diagram, for example). Note that you can also get this information for all intents and directions with :py:attr:`.intent_supported`. - :param intent: One of ``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, - ``ImageCms.INTENT_PERCEPTUAL``, - ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` - and ``ImageCms.INTENT_SATURATION``. - :param direction: One of ``ImageCms.DIRECTION_INPUT``, - ``ImageCms.DIRECTION_OUTPUT`` - and ``ImageCms.DIRECTION_PROOF`` + :param intent: One of ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, + ``ImageCms.Intent.PERCEPTUAL``, + ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` + and ``ImageCms.Intent.SATURATION``. + :param direction: One of ``ImageCms.Direction.INPUT``, + ``ImageCms.Direction.OUTPUT`` + and ``ImageCms.Direction.PROOF`` :return: Boolean if the intent and direction is supported. diff --git a/docs/reference/ImageColor.rst b/docs/reference/ImageColor.rst index f4bd9bd0ebc..20237eccf78 100644 --- a/docs/reference/ImageColor.rst +++ b/docs/reference/ImageColor.rst @@ -1,12 +1,12 @@ .. py:module:: PIL.ImageColor .. py:currentmodule:: PIL.ImageColor -:py:mod:`ImageColor` Module -=========================== +:py:mod:`~PIL.ImageColor` Module +================================ -The :py:mod:`ImageColor` module contains color tables and converters from +The :py:mod:`~PIL.ImageColor` module contains color tables and converters from CSS3-style color specifiers to RGB tuples. This module is used by -:py:meth:`PIL.Image.Image.new` and the :py:mod:`~PIL.ImageDraw` module, among +:py:meth:`PIL.Image.new` and the :py:mod:`~PIL.ImageDraw` module, among others. .. _color-names: @@ -16,8 +16,11 @@ Color Names The ImageColor module supports the following string formats: -* Hexadecimal color specifiers, given as ``#rgb`` or ``#rrggbb``. For example, - ``#ff0000`` specifies pure red. +* Hexadecimal color specifiers, given as ``#rgb``, ``#rgba``, ``#rrggbb`` or + ``#rrggbbaa``, where ``r`` is red, ``g`` is green, ``b`` is blue and ``a`` is + alpha (also called 'opacity'). For example, ``#ff0000`` specifies pure red, + and ``#ff0000cc`` specifies red with 80% opacity (``cc`` is 204 in decimal + form, and 204 / 255 = 0.8). * RGB functions, given as ``rgb(red, green, blue)`` where the color values are integers in the range 0 to 255. Alternatively, the color values can be given @@ -31,6 +34,13 @@ The ImageColor module supports the following string formats: (black=0%, normal=50%, white=100%). For example, ``hsl(0,100%,50%)`` is pure red. +* Hue-Saturation-Value (HSV) functions, given as ``hsv(hue, saturation%, + value%)`` where hue and saturation are the same as HSL, and value is between + 0% and 100% (black=0%, normal=100%). For example, ``hsv(0,100%,100%)`` is + pure red. This format is also known as Hue-Saturation-Brightness (HSB), and + can be given as ``hsb(hue, saturation%, brightness%)``, where each of the + values are used as they are in HSV. + * Common HTML color names. The :py:mod:`~PIL.ImageColor` module provides some 140 standard color names, based on the colors supported by the X Window system and most web browsers. color names are case insensitive. For example, diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 7e498797156..9aa26916a90 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageDraw .. py:currentmodule:: PIL.ImageDraw -:py:mod:`ImageDraw` Module -========================== +:py:mod:`~PIL.ImageDraw` Module +=============================== -The :py:mod:`ImageDraw` module provides simple 2D graphics for +The :py:mod:`~PIL.ImageDraw` module provides simple 2D graphics for :py:class:`~PIL.Image.Image` objects. You can use this module to create new images, annotate or retouch existing images, and to generate graphics on the fly for web use. @@ -18,17 +18,17 @@ Example: Draw a gray cross over an image .. code-block:: python + import sys from PIL import Image, ImageDraw - im = Image.open("hopper.jpg") + with Image.open("hopper.jpg") as im: - draw = ImageDraw.Draw(im) - draw.line((0, 0) + im.size, fill=128) - draw.line((0, im.size[1], im.size[0], 0), fill=128) - del draw + draw = ImageDraw.Draw(im) + draw.line((0, 0) + im.size, fill=128) + draw.line((0, im.size[1], im.size[0], 0), fill=128) - # write to stdout - im.save(sys.stdout, "PNG") + # write to stdout + im.save(sys.stdout, "PNG") Concepts @@ -38,13 +38,14 @@ Coordinates ^^^^^^^^^^^ The graphics interface uses the same coordinate system as PIL itself, with (0, -0) in the upper left corner. +0) in the upper left corner. Any pixels drawn outside of the image bounds will +be discarded. Colors ^^^^^^ To specify colors, you can use numbers or tuples just as you would use with -:py:meth:`PIL.Image.Image.new` or :py:meth:`PIL.Image.Image.putpixel`. For “1”, +:py:meth:`PIL.Image.new` or :py:meth:`PIL.Image.Image.putpixel`. For “1”, “L”, and “I” images, use integers. For “RGB” images, use a 3-tuple containing integer values. For “F” images, use integer or floating point values. @@ -63,7 +64,7 @@ Fonts PIL can use bitmap fonts or OpenType/TrueType fonts. -Bitmap fonts are stored in PIL’s own format, where each font typically consists +Bitmap fonts are stored in PIL's own format, where each font typically consists of two files, one named .pil and the other usually named .pbm. The former contains font metrics, the latter raster data. @@ -80,32 +81,52 @@ Example: Draw Partial Opacity Text .. code-block:: python from PIL import Image, ImageDraw, ImageFont + # get an image - base = Image.open('Pillow/Tests/images/hopper.png').convert('RGBA') + with Image.open("Pillow/Tests/images/hopper.png").convert("RGBA") as base: + + # make a blank image for the text, initialized to transparent text color + txt = Image.new("RGBA", base.size, (255, 255, 255, 0)) + + # get a font + fnt = ImageFont.truetype("Pillow/Tests/fonts/FreeMono.ttf", 40) + # get a drawing context + d = ImageDraw.Draw(txt) + + # draw text, half opacity + d.text((10, 10), "Hello", font=fnt, fill=(255, 255, 255, 128)) + # draw text, full opacity + d.text((10, 60), "World", font=fnt, fill=(255, 255, 255, 255)) + + out = Image.alpha_composite(base, txt) + + out.show() + +Example: Draw Multiline Text +---------------------------- + +.. code-block:: python - # make a blank image for the text, initialized to transparent text color - txt = Image.new('RGBA', base.size, (255,255,255,0)) + from PIL import Image, ImageDraw, ImageFont + + # create an image + out = Image.new("RGB", (150, 100), (255, 255, 255)) # get a font - fnt = ImageFont.truetype('Pillow/Tests/fonts/FreeMono.ttf', 40) + fnt = ImageFont.truetype("Pillow/Tests/fonts/FreeMono.ttf", 40) # get a drawing context - d = ImageDraw.Draw(txt) - - # draw text, half opacity - d.text((10,10), "Hello", font=fnt, fill=(255,255,255,128)) - # draw text, full opacity - d.text((10,60), "World", font=fnt, fill=(255,255,255,255)) + d = ImageDraw.Draw(out) - out = Image.alpha_composite(base, txt) + # draw multiline text + d.multiline_text((10, 10), "Hello\nWorld", font=fnt, fill=(0, 0, 0)) out.show() - Functions --------- -.. py:class:: PIL.ImageDraw.Draw(im, mode=None) +.. py:method:: Draw(im, mode=None) Creates an object that can be used to draw in the given image. @@ -118,28 +139,70 @@ 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:: PIL.ImageDraw.ImageDraw.getfont() +.. py:method:: ImageDraw.getfont() - Get the current default font. + Get the current default font, :py:attr:`ImageDraw.font`. + + If the current default font is ``None``, + it is initialized with :py:func:`.ImageFont.load_default`. :returns: An image font. -.. py:method:: PIL.ImageDraw.ImageDraw.arc(xy, start, end, fill=None) +.. py:method:: ImageDraw.arc(xy, start, end, fill=None, width=0) Draws an arc (a portion of a circle outline) between the start and end angles, inside the given bounding box. - :param xy: Two points to define the bounding box. Sequence of - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. - :param start: Starting angle, in degrees. Angles are measured from - 3 o'clock, increasing clockwise. + :param xy: Two points to define the bounding box. Sequence of ``[(x0, y0), + (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= + y0``. + :param start: Starting angle, in degrees. Angles are measured from 3 + o'clock, increasing clockwise. :param end: Ending angle, in degrees. :param fill: Color to use for the arc. + :param width: The line width, in pixels. + + .. versionadded:: 5.3.0 -.. py:method:: PIL.ImageDraw.ImageDraw.bitmap(xy, bitmap, fill=None) +.. py:method:: ImageDraw.bitmap(xy, bitmap, fill=None) Draws a bitmap (mask) at the given position, using the current fill color for the non-zero portions. The bitmap should be a valid transparency mask @@ -150,53 +213,67 @@ Methods To paste pixel data into an image, use the :py:meth:`~PIL.Image.Image.paste` method on the image itself. -.. py:method:: PIL.ImageDraw.ImageDraw.chord(xy, start, end, fill=None, outline=None) +.. py:method:: ImageDraw.chord(xy, start, end, fill=None, outline=None, width=1) Same as :py:meth:`~PIL.ImageDraw.ImageDraw.arc`, but connects the end points with a straight line. - :param xy: Two points to define the bounding box. Sequence of - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. + :param xy: Two points to define the bounding box. Sequence of ``[(x0, y0), + (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= + y0``. :param outline: Color to use for the outline. :param fill: Color to use for the fill. + :param width: The line width, in pixels. + + .. versionadded:: 5.3.0 -.. py:method:: PIL.ImageDraw.ImageDraw.ellipse(xy, fill=None, outline=None) +.. py:method:: ImageDraw.ellipse(xy, fill=None, outline=None, width=1) Draws an ellipse inside the given bounding box. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` + and ``y1 >= y0``. :param outline: Color to use for the outline. :param fill: Color to use for the fill. + :param width: The line width, in pixels. -.. py:method:: PIL.ImageDraw.ImageDraw.line(xy, fill=None, width=0) + .. versionadded:: 5.3.0 - Draws a line between the coordinates in the **xy** list. +.. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None) + + Draws a line between the coordinates in the ``xy`` list. :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. :param fill: Color to use for the line. - :param width: The line width, in pixels. Note that line - joins are not handled well, so wide polylines will not look good. + :param width: The line width, in pixels. .. versionadded:: 1.1.5 .. note:: This option was broken until version 1.1.6. + :param joint: Joint type between a sequence of lines. It can be ``"curve"``, for rounded edges, or :data:`None`. + + .. versionadded:: 5.3.0 -.. py:method:: PIL.ImageDraw.ImageDraw.pieslice(xy, start, end, fill=None, outline=None) +.. py:method:: ImageDraw.pieslice(xy, start, end, fill=None, outline=None, width=1) Same as arc, but also draws straight lines between the end points and the center of the bounding box. - :param xy: Two points to define the bounding box. Sequence of - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. - :param start: Starting angle, in degrees. Angles are measured from - 3 o'clock, increasing clockwise. + :param xy: Two points to define the bounding box. Sequence of ``[(x0, y0), + (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= + y0``. + :param start: Starting angle, in degrees. Angles are measured from 3 + o'clock, increasing clockwise. :param end: Ending angle, in degrees. :param fill: Color to use for the fill. :param outline: Color to use for the outline. + :param width: The line width, in pixels. -.. py:method:: PIL.ImageDraw.ImageDraw.point(xy, fill=None) + .. versionadded:: 5.3.0 + +.. py:method:: ImageDraw.point(xy, fill=None) Draws points (individual pixels) at the given coordinates. @@ -204,7 +281,7 @@ Methods numeric values like ``[x, y, x, y, ...]``. :param fill: Color to use for the point. -.. py:method:: PIL.ImageDraw.ImageDraw.polygon(xy, fill=None, outline=None) +.. py:method:: ImageDraw.polygon(xy, fill=None, outline=None, width=1) Draws a polygon. @@ -214,145 +291,461 @@ Methods :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. + :param fill: Color to use for the fill. :param outline: Color to use for the outline. + :param width: The line width, in pixels. + + +.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None) + + Draws a regular polygon inscribed in ``bounding_circle``, + with ``n_sides``, and rotation of ``rotation`` degrees. + + :param bounding_circle: The bounding circle is a tuple defined + by a point and radius. + (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``). + The polygon is inscribed in this circle. + :param n_sides: Number of sides + (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon). + :param rotation: Apply an arbitrary rotation to the polygon + (e.g. ``rotation=90``, applies a 90 degree rotation). :param fill: Color to use for the fill. + :param outline: Color to use for the outline. -.. py:method:: PIL.ImageDraw.ImageDraw.rectangle(xy, fill=None, outline=None) + +.. py:method:: ImageDraw.rectangle(xy, fill=None, outline=None, width=1) Draws a rectangle. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The second point - is just outside the drawn rectangle. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box + is inclusive of both endpoints. + :param outline: Color to use for the outline. + :param fill: Color to use for the fill. + :param width: The line width, in pixels. + + .. versionadded:: 5.3.0 + +.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1) + + Draws a rounded rectangle. + + :param xy: Two points to define the bounding box. Sequence of either + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box + is inclusive of both endpoints. + :param radius: Radius of the corners. :param outline: Color to use for the outline. :param fill: Color to use for the fill. + :param width: The line width, in pixels. + + .. versionadded:: 8.2.0 -.. py:method:: PIL.ImageDraw.ImageDraw.shape(shape, fill=None, outline=None) +.. py:method:: ImageDraw.shape(shape, fill=None, outline=None) .. warning:: This method is experimental. Draw a shape. -.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None) +.. py:method:: ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False) Draws the string at the given position. - :param xy: Top left corner of the text. - :param text: Text to be drawn. If it contains any newline characters, - the text is passed on to multiline_text() + :param xy: The anchor coordinates of the text. + :param text: String to be drawn. If it contains any newline characters, + the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`. :param fill: Color to use for the text. :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. - :param spacing: If the text is passed on to multiline_text(), + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. This parameter is + ignored for non-TrueType fonts. + + .. note:: This parameter was present in earlier versions + of Pillow, but implemented only in version 8.0.0. + + :param spacing: If the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`, the number of pixels between lines. - :param align: If the text is passed on to multiline_text(), - "left", "center" or "right". - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right), 'ttb' (top to - bottom) or 'btt' (bottom to top). Requires - libraqm. + :param align: If the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`, + ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. + Use the ``anchor`` parameter to specify the alignment to ``xy``. + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). + Requires libraqm. .. versionadded:: 4.2.0 :param features: A list of OpenType font features to be used during text layout. This is usually used to turn on optional font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` to disable kerning. To get all supported - features, see - https://www.microsoft.com/typography/otspec/featurelist.htm + features, see `OpenType docs`_. Requires libraqm. .. versionadded:: 4.2.0 -.. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None) + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code`_. + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :param stroke_fill: Color to use for the text stroke. If not given, will default to + the ``fill`` parameter. + + .. versionadded:: 6.2.0 + + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). + + .. versionadded:: 8.0.0 + + +.. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False) Draws the string at the given position. - :param xy: Top left corner of the text. - :param text: Text to be drawn. + :param xy: The anchor coordinates of the text. + :param text: String to be drawn. :param fill: Color to use for the text. :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. + + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. This parameter is + ignored for non-TrueType fonts. + + .. note:: This parameter was present in earlier versions + of Pillow, but implemented only in version 8.0.0. + :param spacing: The number of pixels between lines. - :param align: "left", "center" or "right". - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right), 'ttb' (top to - bottom) or 'btt' (bottom to top). Requires - libraqm. + :param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. + Use the ``anchor`` parameter to specify the alignment to ``xy``. + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). + Requires libraqm. .. versionadded:: 4.2.0 :param features: A list of OpenType font features to be used during text layout. This is usually used to turn on optional font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` to disable kerning. To get all supported - features, see - https://www.microsoft.com/typography/otspec/featurelist.htm + features, see `OpenType docs`_. Requires libraqm. .. versionadded:: 4.2.0 -.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None) + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code`_. + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :param stroke_fill: Color to use for the text stroke. If not given, will default to + the ``fill`` parameter. + + .. versionadded:: 6.2.0 + + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). + + .. versionadded:: 8.0.0 + +.. py:method:: ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) + + .. deprecated:: 9.2.0 + + See :ref:`deprecations ` for more information. + + Use :py:meth:`textlength()` to measure the offset of following text with + 1/64 pixel precision. + Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. Return the size of the given string, in pixels. + .. note:: For historical reasons this function measures text height from + the ascender line instead of the top, see :ref:`text-anchors`. + If you wish to measure text height from the top, it is recommended + to use :meth:`textbbox` with ``anchor='lt'`` instead. + :param text: Text to be measured. If it contains any newline characters, - the text is passed on to multiline_textsize() + the text is passed on to :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textsize`. :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. - :param spacing: If the text is passed on to multiline_textsize(), + :param spacing: If the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textsize`, the number of pixels between lines. - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right), 'ttb' (top to - bottom) or 'btt' (bottom to top). Requires - libraqm. + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). + Requires libraqm. .. versionadded:: 4.2.0 - :param features: A list of OpenType font features to be used during text layout. This is usually used to turn on optional font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` to disable kerning. To get all supported - features, see - https://www.microsoft.com/typography/otspec/featurelist.htm + features, see `OpenType docs`_. Requires libraqm. .. versionadded:: 4.2.0 + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code`_. + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 -.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None) + :return: (width, height) + +.. py:method:: ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) + + .. deprecated:: 9.2.0 + + See :ref:`deprecations ` for more information. + + Use :py:meth:`.multiline_textbbox` instead. Return the size of the given string, in pixels. + Use :py:meth:`textlength()` to measure the offset of following text with + 1/64 pixel precision. + Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. + + .. note:: For historical reasons this function measures text height as the + distance between the top ascender line and bottom descender line, + not the top and bottom of the text, see :ref:`text-anchors`. + If you wish to measure text height from the top to the bottom of text, + it is recommended to use :meth:`multiline_textbbox` instead. + :param text: Text to be measured. :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param spacing: The number of pixels between lines. - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right), 'ttb' (top to - bottom) or 'btt' (bottom to top). Requires - libraqm. + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). + Requires libraqm. .. versionadded:: 4.2.0 :param features: A list of OpenType font features to be used during text layout. This is usually used to turn on optional font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` to disable kerning. To get all supported - features, see - https://www.microsoft.com/typography/otspec/featurelist.htm + features, see `OpenType docs`_. Requires libraqm. .. versionadded:: 4.2.0 -.. py:method:: PIL.ImageDraw.getdraw(im=None, hints=None) + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code`_. + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :return: (width, height) + +.. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False) + + Returns length (in pixels with 1/64 precision) of given text when rendered + in font with provided direction, features, and language. + + This is the amount by which following text should be offset. + Text bounding box may extend past the length in some fonts, + e.g. when using italics or accents. + + The result is returned as a float; it is a whole number if using basic layout. + + Note that the sum of two lengths may not equal the length of a concatenated + string due to kerning. If you need to adjust for kerning, include the following + character and subtract its length. + + For example, instead of + + .. code-block:: python + + hello = draw.textlength("Hello", font) + world = draw.textlength("World", font) + hello_world = hello + world # not adjusted for kerning + assert hello_world == draw.textlength("HelloWorld", font) # may fail + + use + + .. code-block:: python + + hello = draw.textlength("HelloW", font) - draw.textlength( + "W", font + ) # adjusted for kerning + world = draw.textlength("World", font) + hello_world = hello + world # adjusted for kerning + assert hello_world == draw.textlength("HelloWorld", font) # True + + or disable kerning with (requires libraqm) + + .. code-block:: python + + hello = draw.textlength("Hello", font, features=["-kern"]) + world = draw.textlength("World", font, features=["-kern"]) + hello_world = hello + world # kerning is disabled, no need to adjust + assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"]) # True + + .. versionadded:: 8.0.0 + + :param text: Text to be measured. May not contain any newline characters. + :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). + Requires libraqm. + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` + to disable kerning. To get all supported + features, see `OpenType docs`_. + Requires libraqm. + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code`_. + Requires libraqm. + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). + :return: Width for horizontal, height for vertical text. + +.. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False) + + Returns bounding box (in pixels) of given text relative to given anchor + when rendered in font with provided direction, features, and language. + Only supported for TrueType fonts. + + Use :py:meth:`textlength` to get the offset of following text with + 1/64 pixel precision. The bounding box includes extra margins for + some fonts, e.g. italics or accents. + + .. versionadded:: 8.0.0 + + :param xy: The anchor coordinates of the text. + :param text: Text to be measured. If it contains any newline characters, + the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`. + :param font: A :py:class:`~PIL.ImageFont.FreeTypeFont` instance. + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. This parameter is + ignored for non-TrueType fonts. + :param spacing: If the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`, + the number of pixels between lines. + :param align: If the text is passed on to + :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`, + ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. + Use the ``anchor`` parameter to specify the alignment to ``xy``. + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). + Requires libraqm. + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` + to disable kerning. To get all supported + features, see `OpenType docs`_. + Requires libraqm. + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code`_. + Requires libraqm. + :param stroke_width: The width of the text stroke. + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). + :return: ``(left, top, right, bottom)`` bounding box + +.. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False) + + Returns bounding box (in pixels) of given text relative to given anchor + when rendered in font with provided direction, features, and language. + Only supported for TrueType fonts. + + Use :py:meth:`textlength` to get the offset of following text with + 1/64 pixel precision. The bounding box includes extra margins for + some fonts, e.g. italics or accents. + + .. versionadded:: 8.0.0 + + :param xy: The anchor coordinates of the text. + :param text: Text to be measured. + :param font: A :py:class:`~PIL.ImageFont.FreeTypeFont` instance. + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. This parameter is + ignored for non-TrueType fonts. + :param spacing: The number of pixels between lines. + :param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. + Use the ``anchor`` parameter to specify the alignment to ``xy``. + :param direction: Direction of the text. It can be ``"rtl"`` (right to + left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). + Requires libraqm. + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example ``"dlig"`` or ``"ss01"``, but can be also + used to turn off default font features, for + example ``"-liga"`` to disable ligatures or ``"-kern"`` + to disable kerning. To get all supported + features, see `OpenType docs`_. + Requires libraqm. + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code`_. + Requires libraqm. + :param stroke_width: The width of the text stroke. + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). + :return: ``(left, top, right, bottom)`` bounding box + +.. py:method:: getdraw(im=None, hints=None) .. warning:: This method is experimental. @@ -363,7 +756,7 @@ Methods :param hints: An optional list of hints. :returns: A (drawing context, drawing resource factory) tuple. -.. py:method:: PIL.ImageDraw.floodfill(image, xy, value, border=None, thresh=0) +.. py:method:: floodfill(image, xy, value, border=None, thresh=0) .. warning:: This method is experimental. @@ -380,3 +773,6 @@ Methods tolerable difference of a pixel value from the 'background' in order for it to be replaced. Useful for filling regions of non- homogeneous, but similar, colors. + +.. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/ +.. _OpenType docs: https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index b172054b2e3..29ceee314cc 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageEnhance .. py:currentmodule:: PIL.ImageEnhance -:py:mod:`ImageEnhance` Module -============================= +:py:mod:`~PIL.ImageEnhance` Module +================================== -The :py:mod:`ImageEnhance` module contains a number of classes that can be used +The :py:mod:`~PIL.ImageEnhance` module contains a number of classes that can be used for image enhancement. Example: Vary the sharpness of an image @@ -18,7 +18,7 @@ Example: Vary the sharpness of an image for i in range(8): factor = i / 4.0 - enhancer.enhance(factor).show("Sharpness %f" % factor) + enhancer.enhance(factor).show(f"Sharpness {factor:f}") Also see the :file:`enhancer.py` demo program in the :file:`Scripts/` directory. @@ -29,7 +29,8 @@ Classes All enhancement classes implement a common interface, containing a single method: -.. py:class:: PIL.ImageEnhance._Enhance +.. py:class:: _Enhance + .. py:method:: enhance(factor) Returns an enhanced image. @@ -40,7 +41,7 @@ method: etc), and higher values more. There are no restrictions on this value. -.. py:class:: PIL.ImageEnhance.Color(image) +.. py:class:: Color(image) Adjust image color balance. @@ -49,7 +50,7 @@ method: factor of 0.0 gives a black and white image. A factor of 1.0 gives the original image. -.. py:class:: PIL.ImageEnhance.Contrast(image) +.. py:class:: Contrast(image) Adjust image contrast. @@ -57,7 +58,7 @@ method: to the contrast control on a TV set. An enhancement factor of 0.0 gives a solid grey image. A factor of 1.0 gives the original image. -.. py:class:: PIL.ImageEnhance.Brightness(image) +.. py:class:: Brightness(image) Adjust image brightness. @@ -65,7 +66,7 @@ method: enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the original image. -.. py:class:: PIL.ImageEnhance.Sharpness(image) +.. py:class:: Sharpness(image) Adjust image sharpness. diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index d93dfb3a3df..3cf59c610c8 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageFile .. py:currentmodule:: PIL.ImageFile -:py:mod:`ImageFile` Module -========================== +:py:mod:`~PIL.ImageFile` Module +=============================== -The :py:mod:`ImageFile` module provides support functions for the image open +The :py:mod:`~PIL.ImageFile` module provides support functions for the image open and save functions. In addition, it provides a :py:class:`Parser` class which can be used to decode @@ -34,14 +34,36 @@ Example: Parse an image im.save("copy.jpg") -:py:class:`~PIL.ImageFile.Parser` ---------------------------------- +Classes +------- .. autoclass:: PIL.ImageFile.Parser() :members: -:py:class:`~PIL.ImageFile.PyDecoder` ------------------------------------- +.. autoclass:: PIL.ImageFile.PyCodec() + :members: .. autoclass:: PIL.ImageFile.PyDecoder() :members: + :show-inheritance: + +.. autoclass:: PIL.ImageFile.PyEncoder() + :members: + :show-inheritance: + +.. autoclass:: PIL.ImageFile.ImageFile() + :member-order: bysource + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: PIL.ImageFile.StubImageFile() + :members: + :show-inheritance: + +Constants +--------- + +.. autodata:: PIL.ImageFile.LOAD_TRUNCATED_IMAGES +.. autodata:: PIL.ImageFile.ERRORS + :annotation: diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index bc1868667f1..c85da4fb57a 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageFilter .. py:currentmodule:: PIL.ImageFilter -:py:mod:`ImageFilter` Module -============================ +:py:mod:`~PIL.ImageFilter` Module +================================= -The :py:mod:`ImageFilter` module contains definitions for a pre-defined set of +The :py:mod:`~PIL.ImageFilter` module contains definitions for a pre-defined set of filters, which can be be used with the :py:meth:`Image.filter() ` method. @@ -33,16 +33,62 @@ image enhancement filters: * **EDGE_ENHANCE_MORE** * **EMBOSS** * **FIND_EDGES** +* **SHARPEN** * **SMOOTH** * **SMOOTH_MORE** -* **SHARPEN** -.. autoclass:: PIL.ImageFilter.GaussianBlur +.. autoclass:: PIL.ImageFilter.Color3DLUT + :members: + .. autoclass:: PIL.ImageFilter.BoxBlur + :members: + +.. autoclass:: PIL.ImageFilter.GaussianBlur + :members: + .. autoclass:: PIL.ImageFilter.UnsharpMask + :members: + .. autoclass:: PIL.ImageFilter.Kernel + :members: + .. autoclass:: PIL.ImageFilter.RankFilter + :members: + .. autoclass:: PIL.ImageFilter.MedianFilter + :members: + .. autoclass:: PIL.ImageFilter.MinFilter + :members: + .. autoclass:: PIL.ImageFilter.MaxFilter + :members: + .. autoclass:: PIL.ImageFilter.ModeFilter + :members: + +.. class:: Filter + + An abstract mixin used for filtering images + (for use with :py:meth:`~PIL.Image.Image.filter`). + + Implementors must provide the following method: + + .. method:: filter(self, image) + + Applies a filter to a single-band image, or a single band of an image. + + :returns: A filtered copy of the image. + +.. class:: MultibandFilter + + An abstract mixin used for filtering multi-band images + (for use with :py:meth:`~PIL.Image.Image.filter`). + + Implementors must provide the following method: + + .. method:: filter(self, image) + + Applies a filter to a multi-band image. + + :returns: A filtered copy of the image. diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 76fde44ff4a..516fa63a783 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -1,21 +1,22 @@ .. py:module:: PIL.ImageFont .. py:currentmodule:: PIL.ImageFont -:py:mod:`ImageFont` Module -========================== +:py:mod:`~PIL.ImageFont` Module +=============================== -The :py:mod:`ImageFont` module defines a class with the same name. Instances of +The :py:mod:`~PIL.ImageFont` module defines a class with the same name. Instances of this class store bitmap fonts, and are used with the -:py:meth:`PIL.ImageDraw.Draw.text` method. +:py:meth:`PIL.ImageDraw.ImageDraw.text` method. -PIL uses its own font file format to store bitmap fonts. You can use the -:command:`pilfont` utility to convert BDF and PCF font descriptors (X window -font formats) to this format. +PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You can use +`pilfont.py `_ +from `pillow-scripts `_ to convert BDF and +PCF font descriptors (X window font formats) to this format. Starting with version 1.1.4, PIL can be configured to support TrueType and OpenType fonts (as well as other font formats supported by the FreeType library). For earlier versions, TrueType support is only available as part of -the imToolkit package +the imToolkit package. Example ------- @@ -47,44 +48,28 @@ Functions Methods ------- -.. py:method:: PIL.ImageFont.ImageFont.getsize(text) +.. autoclass:: PIL.ImageFont.ImageFont + :members: - :return: (width, height) +.. autoclass:: PIL.ImageFont.FreeTypeFont + :members: -.. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='', direction=None, features=[]) +.. autoclass:: PIL.ImageFont.TransposedFont + :members: + :undoc-members: - Create a bitmap for the text. - - If the font uses antialiasing, the bitmap should have mode “L” and use a - maximum value of 255. Otherwise, it should have mode “1”. - - :param text: Text to render. - :param mode: Used by some graphics drivers to indicate what mode the - driver prefers; if empty, the renderer may return either - mode. Note that the mode is always a string, to simplify - C-level implementations. - - .. versionadded:: 1.1.5 +Constants +--------- - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right), 'ttb' (top to - bottom) or 'btt' (bottom to top). Requires - libraqm. +.. data:: PIL.ImageFont.Layout.BASIC - .. versionadded:: 4.2.0 + Use basic text layout for TrueType font. + Advanced features such as text direction are not supported. - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://www.microsoft.com/typography/otspec/featurelist.htm - Requires libraqm. +.. data:: PIL.ImageFont.Layout.RAQM - .. versionadded:: 4.2.0 + Use Raqm text layout for TrueType font. + Advanced features are supported. - :return: An internal PIL storage memory instance as defined by the - :py:mod:`PIL.Image.core` interface module. + Requires Raqm, you can check support using + :py:func:`PIL.features.check_feature` with ``feature="raqm"``. diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 39aaef6bc12..3086ba8c311 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -1,30 +1,45 @@ .. py:module:: PIL.ImageGrab .. py:currentmodule:: PIL.ImageGrab -:py:mod:`ImageGrab` Module (macOS and Windows only) -=================================================== +:py:mod:`~PIL.ImageGrab` Module +=============================== -The :py:mod:`ImageGrab` module can be used to copy the contents of the screen +The :py:mod:`~PIL.ImageGrab` module can be used to copy the contents of the screen or the clipboard to a PIL image memory. -.. note:: The current version works on macOS and Windows only. - .. versionadded:: 1.1.3 -.. py:function:: PIL.ImageGrab.grab(bbox=None) +.. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None) Take a snapshot of the screen. The pixels inside the bounding box are - returned as an "RGB" image on Windows or "RGBA" on macOS. + returned as an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted, the entire screen is copied. - .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS) + On Linux, if ``xdisplay`` is ``None`` then ``gnome-screenshot`` will be used if it + is installed. To capture the default X11 display instead, pass ``xdisplay=""``. + + .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) :param bbox: What region to copy. Default is the entire screen. + Note that on Windows OS, the top-left point may be negative if ``all_screens=True`` is used. + :param include_layered_windows: Includes layered windows. Windows OS only. + + .. versionadded:: 6.1.0 + :param all_screens: Capture all monitors. Windows OS only. + + .. versionadded:: 6.2.0 + + :param xdisplay: + X11 Display address. Pass :data:`None` to grab the default system screen. Pass ``""`` to grab the default X11 screen on Windows or macOS. + + You can check X11 support using :py:func:`PIL.features.check_feature` with ``feature="xcb"``. + + .. versionadded:: 7.1.0 :return: An image -.. py:function:: PIL.ImageGrab.grabclipboard() +.. py:function:: grabclipboard() - Take a snapshot of the clipboard image, if any. + Take a snapshot of the clipboard image, if any. Only macOS and Windows are currently supported. .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS) diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index 445a7e277b6..63f88fddd21 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -1,12 +1,12 @@ .. py:module:: PIL.ImageMath .. py:currentmodule:: PIL.ImageMath -:py:mod:`ImageMath` Module -========================== +:py:mod:`~PIL.ImageMath` Module +=============================== -The :py:mod:`ImageMath` module can be used to evaluate “image expressions”. The -module provides a single eval function, which takes an expression string and -one or more images. +The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressions”. The +module provides a single :py:meth:`~PIL.ImageMath.eval` function, which takes +an expression string and one or more images. Example: Using the :py:mod:`~PIL.ImageMath` module -------------------------------------------------- @@ -15,11 +15,11 @@ Example: Using the :py:mod:`~PIL.ImageMath` module from PIL import Image, ImageMath - im1 = Image.open("image1.jpg") - im2 = Image.open("image2.jpg") + with Image.open("image1.jpg") as im1: + with Image.open("image2.jpg") as im2: - out = ImageMath.eval("convert(min(a, b), 'L')", a=im1, b=im2) - out.save("result.png") + out = ImageMath.eval("convert(min(a, b), 'L')", a=im1, b=im2) + out.save("result.png") .. py:function:: eval(expression, environment) @@ -60,9 +60,8 @@ point values, as necessary. For example, if you add two 8-bit images, the result will be a 32-bit integer image. If you add a floating point constant to an 8-bit image, the result will be a 32-bit floating point image. -You can force conversion using the :py:func:`~PIL.ImageMath.convert`, -:py:func:`~PIL.ImageMath.float`, and :py:func:`~PIL.ImageMath.int` functions -described below. +You can force conversion using the ``convert()``, ``float()``, and ``int()`` +functions described below. Bitwise Operators ^^^^^^^^^^^^^^^^^ @@ -98,20 +97,24 @@ These functions are applied to each individual pixel. .. py:currentmodule:: None .. py:function:: abs(image) + :noindex: Absolute value. .. py:function:: convert(image, mode) + :noindex: Convert image to the given mode. The mode must be given as a string constant. .. py:function:: float(image) + :noindex: Convert image to 32-bit floating point. This is equivalent to convert(image, “F”). .. py:function:: int(image) + :noindex: Convert image to 32-bit integer. This is equivalent to convert(image, “I”). @@ -119,9 +122,11 @@ These functions are applied to each individual pixel. integers if necessary to get a correct result. .. py:function:: max(image1, image2) + :noindex: Maximum value. .. py:function:: min(image1, image2) + :noindex: Minimum value. diff --git a/docs/reference/ImageMorph.rst b/docs/reference/ImageMorph.rst index be9d59348a8..d4522a06ae3 100644 --- a/docs/reference/ImageMorph.rst +++ b/docs/reference/ImageMorph.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageMorph .. py:currentmodule:: PIL.ImageMorph -:py:mod:`ImageMorph` Module -=========================== +:py:mod:`~PIL.ImageMorph` Module +================================ -The :py:mod:`ImageMorph` module provides morphology operations on images. +The :py:mod:`~PIL.ImageMorph` module provides morphology operations on images. .. automodule:: PIL.ImageMorph :members: diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst index 50cea90ca98..d1c43cf6092 100644 --- a/docs/reference/ImageOps.rst +++ b/docs/reference/ImageOps.rst @@ -1,20 +1,21 @@ .. py:module:: PIL.ImageOps .. py:currentmodule:: PIL.ImageOps -:py:mod:`ImageOps` Module -========================== +:py:mod:`~PIL.ImageOps` Module +============================== -The :py:mod:`ImageOps` module contains a number of ‘ready-made’ image +The :py:mod:`~PIL.ImageOps` module contains a number of ‘ready-made’ image processing operations. This module is somewhat experimental, and most operators only work on L and RGB images. -Only bug fixes have been added since the Pillow fork. - .. versionadded:: 1.1.3 .. autofunction:: autocontrast .. autofunction:: colorize +.. autofunction:: contain +.. autofunction:: pad .. autofunction:: crop +.. autofunction:: scale .. autofunction:: deform .. autofunction:: equalize .. autofunction:: expand @@ -25,3 +26,4 @@ Only bug fixes have been added since the Pillow fork. .. autofunction:: mirror .. autofunction:: posterize .. autofunction:: solarize +.. autofunction:: exif_transpose diff --git a/docs/reference/ImagePalette.rst b/docs/reference/ImagePalette.rst index 15b8aed8f05..72ccfac7d83 100644 --- a/docs/reference/ImagePalette.rst +++ b/docs/reference/ImagePalette.rst @@ -1,18 +1,14 @@ .. py:module:: PIL.ImagePalette .. py:currentmodule:: PIL.ImagePalette -:py:mod:`ImagePalette` Module -============================= +:py:mod:`~PIL.ImagePalette` Module +================================== -The :py:mod:`ImagePalette` module contains a class of the same name to +The :py:mod:`~PIL.ImagePalette` module contains a class of the same name to represent the color palette of palette mapped images. .. note:: - This module was never well-documented. It hasn't changed since 2001, - though, so it's probably safe for you to read the source code and puzzle - out the internals if you need to. - The :py:class:`~PIL.ImagePalette.ImagePalette` class has several methods, but they are all marked as "experimental." Read that as you will. The ``[source]`` link is there for a reason. diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index 978db4caff7..b9bdfc50772 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImagePath .. py:currentmodule:: PIL.ImagePath -:py:mod:`ImagePath` Module -========================== +:py:mod:`~PIL.ImagePath` Module +=============================== -The :py:mod:`ImagePath` module is used to store and manipulate 2-dimensional +The :py:mod:`~PIL.ImagePath` module is used to store and manipulate 2-dimensional vector data. Path objects can be passed to the methods on the :py:mod:`~PIL.ImageDraw` module. @@ -33,7 +33,7 @@ vector data. Path objects can be passed to the methods on the method modifies the path in place, and returns the number of points left in the path. - **distance** is measured as `Manhattan distance`_ and defaults to two + ``distance`` is measured as `Manhattan distance`_ and defaults to two pixels. .. _Manhattan distance: https://en.wikipedia.org/wiki/Manhattan_distance @@ -53,9 +53,9 @@ vector data. Path objects can be passed to the methods on the Converts the path to a Python list [(x, y), …]. :param flat: By default, this function returns a list of 2-tuples - [(x, y), ...]. If this argument is `True`, it + [(x, y), ...]. If this argument is ``True``, it returns a flat list [x, y, ...] instead. - :return: A list of coordinates. See **flat**. + :return: A list of coordinates. See ``flat``. .. py:method:: PIL.ImagePath.Path.transform(matrix) diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index 7bc426eec95..15d052d1c4e 100644 --- a/docs/reference/ImageQt.rst +++ b/docs/reference/ImageQt.rst @@ -1,20 +1,28 @@ .. py:module:: PIL.ImageQt .. py:currentmodule:: PIL.ImageQt -:py:mod:`ImageQt` Module -======================== +:py:mod:`~PIL.ImageQt` Module +============================= -The :py:mod:`ImageQt` module contains support for creating PyQt4, PyQt5 or -PySide QImage objects from PIL images. +The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6, PySide6, PyQt5 +or PySide2 QImage objects from PIL images. + +`Qt 5 reached end-of-life `_ on 2020-12-08 for +open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). + +Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed +in Pillow 10 (2023-07-01). Upgrade to +`PyQt6 `_ or +`PySide6 `_ instead. .. versionadded:: 1.1.6 -.. py:class:: ImageQt.ImageQt(image) +.. py:class:: ImageQt(image) Creates an :py:class:`~PIL.ImageQt.ImageQt` object from a PIL :py:class:`~PIL.Image.Image` object. This class is a subclass of QtGui.QImage, which means that you can pass the resulting objects directly - to PyQt4/PyQt5/PySide API functions and methods. + to PyQt6/PySide6/PyQt5/PySide2 API functions and methods. This operation is currently supported for mode 1, L, P, RGB, and RGBA images. To handle other modes, you need to convert the image first. diff --git a/docs/reference/ImageSequence.rst b/docs/reference/ImageSequence.rst index f8ea9ee92c2..f2e7d9edd28 100644 --- a/docs/reference/ImageSequence.rst +++ b/docs/reference/ImageSequence.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageSequence .. py:currentmodule:: PIL.ImageSequence -:py:mod:`ImageSequence` Module -============================== +:py:mod:`~PIL.ImageSequence` Module +=================================== -The :py:mod:`ImageSequence` module contains a wrapper class that lets you +The :py:mod:`~PIL.ImageSequence` module contains a wrapper class that lets you iterate over the frames of an image sequence. Extracting frames from an animation @@ -14,14 +14,19 @@ Extracting frames from an animation from PIL import Image, ImageSequence - im = Image.open("animation.fli") - - index = 1 - for frame in ImageSequence.Iterator(im): - frame.save("frame%d.png" % index) - index += 1 + with Image.open("animation.fli") as im: + index = 1 + for frame in ImageSequence.Iterator(im): + frame.save(f"frame{index}.png") + index += 1 The :py:class:`~PIL.ImageSequence.Iterator` class ------------------------------------------------- .. autoclass:: PIL.ImageSequence.Iterator + :members: + +Functions +--------- + +.. autofunction:: PIL.ImageSequence.all_frames diff --git a/docs/reference/ImageShow.rst b/docs/reference/ImageShow.rst new file mode 100644 index 00000000000..5cedede69e6 --- /dev/null +++ b/docs/reference/ImageShow.rst @@ -0,0 +1,33 @@ +.. py:module:: PIL.ImageShow +.. py:currentmodule:: PIL.ImageShow + +:py:mod:`~PIL.ImageShow` Module +=============================== + +The :py:mod:`~PIL.ImageShow` Module is used to display images. +All default viewers convert the image to be shown to PNG format. + +.. autofunction:: PIL.ImageShow.show + +.. autoclass:: IPythonViewer +.. autoclass:: WindowsViewer +.. autoclass:: MacViewer + +.. class:: UnixViewer + + The following viewers may be registered on Unix-based systems, if the given command is found: + + .. autoclass:: PIL.ImageShow.XDGViewer + .. autoclass:: PIL.ImageShow.DisplayViewer + .. autoclass:: PIL.ImageShow.GmDisplayViewer + .. autoclass:: PIL.ImageShow.EogViewer + .. autoclass:: PIL.ImageShow.XVViewer + + To provide maximum functionality on Unix-based systems, temporary files created + from images will not be automatically removed by Pillow. + +.. autofunction:: PIL.ImageShow.register +.. autoclass:: PIL.ImageShow.Viewer + :member-order: bysource + :members: + :undoc-members: diff --git a/docs/reference/ImageStat.rst b/docs/reference/ImageStat.rst index b8925bf8cd5..f61d123131a 100644 --- a/docs/reference/ImageStat.rst +++ b/docs/reference/ImageStat.rst @@ -1,25 +1,45 @@ .. py:module:: PIL.ImageStat .. py:currentmodule:: PIL.ImageStat -:py:mod:`ImageStat` Module -========================== +:py:mod:`~PIL.ImageStat` Module +=============================== -The :py:mod:`ImageStat` module calculates global statistics for an image, or +The :py:mod:`~PIL.ImageStat` module calculates global statistics for an image, or for a region of an image. -.. py:class:: PIL.ImageStat.Stat(image_or_list, mask=None) +.. py:class:: Stat(image_or_list, mask=None) Calculate statistics for the given image. If a mask is included, only the regions covered by that mask are included in the statistics. You can also pass in a previously calculated histogram. :param image: A PIL image, or a precalculated histogram. + + .. note:: + + For a PIL image, calculations rely on the + :py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are + grouped into 256 bins, even if the image has more than 8 bits per + channel. So ``I`` and ``F`` mode images have a maximum ``mean``, + ``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum + of more than 255. + :param mask: An optional mask. .. py:attribute:: extrema Min/max values for each band in the image. + .. note:: + + This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and + simply returns the low and high bins used. This is correct for + images with 8 bits per channel, but fails for other modes such as + ``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to + return per-band extrema for the image. This is more correct and + efficient because, for non-8-bit modes, the histogram method uses + :py:meth:`~PIL.Image.Image.getextrema` to determine the bins used. + .. py:attribute:: count Total number of pixels for each band in the image. diff --git a/docs/reference/ImageTk.rst b/docs/reference/ImageTk.rst index 7ee4af02980..134ef565188 100644 --- a/docs/reference/ImageTk.rst +++ b/docs/reference/ImageTk.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageTk .. py:currentmodule:: PIL.ImageTk -:py:mod:`ImageTk` Module -======================== +:py:mod:`~PIL.ImageTk` Module +============================= -The :py:mod:`ImageTk` module contains support to create and modify Tkinter +The :py:mod:`~PIL.ImageTk` module contains support to create and modify Tkinter BitmapImage and PhotoImage objects from PIL images. For examples, see the demo programs in the Scripts directory. diff --git a/docs/reference/ImageWin.rst b/docs/reference/ImageWin.rst index 2696e7e991a..2ee3cadb70b 100644 --- a/docs/reference/ImageWin.rst +++ b/docs/reference/ImageWin.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageWin .. py:currentmodule:: PIL.ImageWin -:py:mod:`ImageWin` Module (Windows-only) -======================================== +:py:mod:`~PIL.ImageWin` Module (Windows-only) +============================================= -The :py:mod:`ImageWin` module contains support to create and display images on +The :py:mod:`~PIL.ImageWin` module contains support to create and display images on Windows. ImageWin can be used with PythonWin and other user interface toolkits that @@ -24,6 +24,8 @@ Tkinter makes the window handle available via the winfo_id method: .. autoclass:: PIL.ImageWin.Dib :members: - .. autoclass:: PIL.ImageWin.HDC + :members: + .. autoclass:: PIL.ImageWin.HWND + :members: diff --git a/docs/reference/JpegPresets.rst b/docs/reference/JpegPresets.rst new file mode 100644 index 00000000000..aafae44cf4a --- /dev/null +++ b/docs/reference/JpegPresets.rst @@ -0,0 +1,11 @@ +.. py:currentmodule:: PIL.JpegPresets + +:py:mod:`~PIL.JpegPresets` Module +================================= + +.. automodule:: PIL.JpegPresets + + .. data:: presets + :type: dict + + A dictionary of all supported presets. diff --git a/docs/reference/PSDraw.rst b/docs/reference/PSDraw.rst index 2b5b9b340b3..3e8512e7aa8 100644 --- a/docs/reference/PSDraw.rst +++ b/docs/reference/PSDraw.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.PSDraw .. py:currentmodule:: PIL.PSDraw -:py:mod:`PSDraw` Module -======================= +:py:mod:`~PIL.PSDraw` Module +============================ -The :py:mod:`PSDraw` module provides simple print support for Postscript +The :py:mod:`~PIL.PSDraw` module provides simple print support for PostScript printers. You can print text, graphics and images through this module. .. autoclass:: PIL.PSDraw.PSDraw diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index 5389dab33a8..b234b7b4efb 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -6,7 +6,13 @@ The PixelAccess class provides read and write access to :py:class:`PIL.Image` data at a pixel level. -.. note:: Accessing individual pixels is fairly slow. If you are looping over all of the pixels in an image, there is likely a faster way using other parts of the Pillow API. +.. note:: Accessing individual pixels is fairly slow. If you are + looping over all of the pixels in an image, there is likely + a faster way using other parts of the Pillow API. + + :mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps` + have methods for many standard operations. If you wish to perform + a custom mapping, check out :py:meth:`~PIL.Image.Image.point`. Example ------- @@ -17,21 +23,29 @@ changes it. .. code-block:: python from PIL import Image - im = Image.open('hopper.jpg') - px = im.load() - print (px[4,4]) - px[4,4] = (0,0,0) - print (px[4,4]) + + with Image.open("hopper.jpg") as im: + px = im.load() + print(px[4, 4]) + px[4, 4] = (0, 0, 0) + print(px[4, 4]) Results in the following:: (23, 24, 68) (0, 0, 0) +Access using negative indexes is also possible. + +.. code-block:: python + + px[-1, -1] = (0, 0, 0) + print(px[-1, -1]) + :py:class:`PixelAccess` Class ------------------------------------ +----------------------------- .. class:: PixelAccess @@ -58,7 +72,8 @@ Results in the following:: Modifies the pixel at x,y. The color is given as a single numerical value for single band images, and a tuple for - multi-band images + multi-band images. In addition to this, RGB and RGBA tuples + are accepted for P and PA images. :param xy: The pixel coordinate, given as (x, y). :param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode) diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst index 8bd8af9ff67..f9eb9b52418 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -1,14 +1,18 @@ .. py:module:: PIL.PyAccess .. py:currentmodule:: PIL.PyAccess -:py:mod:`PyAccess` Module -========================= +:py:mod:`~PIL.PyAccess` Module +============================== -The :py:mod:`PyAccess` module provides a CFFI/Python implementation of the :ref:`PixelAccess`. This implementation is far faster on PyPy than the PixelAccess version. +The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the :ref:`PixelAccess`. This implementation is far faster on PyPy than the PixelAccess version. .. note:: Accessing individual pixels is fairly slow. If you are - looping over all of the pixels in an image, there is likely - a faster way using other parts of the Pillow API. + looping over all of the pixels in an image, there is likely + a faster way using other parts of the Pillow API. + + :mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps` + have methods for many standard operations. If you wish to perform + a custom mapping, check out :py:meth:`~PIL.Image.Image.point`. Example ------- @@ -18,17 +22,25 @@ The following script loads an image, accesses one pixel from it, then changes it .. code-block:: python from PIL import Image - im = Image.open('hopper.jpg') - px = im.load() - print (px[4,4]) - px[4,4] = (0,0,0) - print (px[4,4]) + + with Image.open("hopper.jpg") as im: + px = im.load() + print(px[4, 4]) + px[4, 4] = (0, 0, 0) + print(px[4, 4]) Results in the following:: (23, 24, 68) (0, 0, 0) +Access using negative indexes is also possible. + +.. code-block:: python + + px[-1, -1] = (0, 0, 0) + print(px[-1, -1]) + :py:class:`PyAccess` Class diff --git a/docs/reference/TiffTags.rst b/docs/reference/TiffTags.rst index 3b261625a02..7cb7d16ae47 100644 --- a/docs/reference/TiffTags.rst +++ b/docs/reference/TiffTags.rst @@ -1,17 +1,21 @@ .. py:module:: PIL.TiffTags .. py:currentmodule:: PIL.TiffTags -:py:mod:`TiffTags` Module -========================= +:py:mod:`~PIL.TiffTags` Module +============================== -The :py:mod:`TiffTags` module exposes many of the standard TIFF +The :py:mod:`~PIL.TiffTags` module exposes many of the standard TIFF metadata tag numbers, names, and type information. .. method:: lookup(tag) :param tag: Integer tag number - :returns: Taginfo namedtuple, From the ``TAGS_V2`` info if possible, - otherwise just populating the value and name from ``TAGS``. + :param group: Which :py:data:`~PIL.TiffTags.TAGS_V2_GROUPS` to look in + + .. versionadded:: 8.3.0 + + :returns: Taginfo namedtuple, From the :py:data:`~PIL.TiffTags.TAGS_V2` info if possible, + otherwise just populating the value and name from :py:data:`~PIL.TiffTags.TAGS`. If the tag is not recognized, "unknown" is returned for the name .. versionadded:: 3.1.0 @@ -22,7 +26,7 @@ metadata tag numbers, names, and type information. :param value: Integer Tag Number :param name: Tag Name - :param type: Integer type from :py:attr:`PIL.TiffTags.TYPES` + :param type: Integer type from :py:data:`PIL.TiffTags.TYPES` :param length: Array length: 0 == variable, 1 == single value, n = fixed :param enum: Dict of name:integer value options for an enumeration @@ -33,15 +37,27 @@ metadata tag numbers, names, and type information. .. versionadded:: 3.0.0 -.. py:attribute:: PIL.TiffTags.TAGS_V2 +.. py:data:: PIL.TiffTags.TAGS_V2 + :type: dict The ``TAGS_V2`` dictionary maps 16-bit integer tag numbers to - :py:class:`PIL.TagTypes.TagInfo` tuples for metadata fields defined in the TIFF + :py:class:`PIL.TiffTags.TagInfo` tuples for metadata fields defined in the TIFF spec. .. versionadded:: 3.0.0 -.. py:attribute:: PIL.TiffTags.TAGS +.. py:data:: PIL.TiffTags.TAGS_V2_GROUPS + :type: dict + + :py:data:`~PIL.TiffTags.TAGS_V2` is one dimensional and + doesn't account for the fact that tags actually exist in + `different groups `_. + This dictionary is used when the tag in question is part of a group. + +.. versionadded:: 8.3.0 + +.. py:data:: PIL.TiffTags.TAGS + :type: dict The ``TAGS`` dictionary maps 16-bit integer TIFF tag number to descriptive string names. For instance: @@ -50,10 +66,16 @@ metadata tag numbers, names, and type information. >>> TAGS[0x010e] 'ImageDescription' - This dictionary contains a superset of the tags in TAGS_V2, common + This dictionary contains a superset of the tags in :py:data:`~PIL.TiffTags.TAGS_V2`, common EXIF tags, and other well known metadata tags. -.. py:attribute:: PIL.TiffTags.TYPES +.. py:data:: PIL.TiffTags.TYPES + :type: dict The ``TYPES`` dictionary maps the TIFF type short integer to a human readable type name. + +.. py:data:: PIL.TiffTags.LIBTIFF_CORE + :type: list + + A list of supported tag IDs when writing using LibTIFF. diff --git a/docs/reference/block_allocator.rst b/docs/reference/block_allocator.rst index da5b3b8d837..1abe5280fbf 100644 --- a/docs/reference/block_allocator.rst +++ b/docs/reference/block_allocator.rst @@ -8,7 +8,7 @@ Historically there have been two image allocators in Pillow: ``ImagingAllocateBlock`` and ``ImagingAllocateArray``. The first works for images smaller than 16MB of data and allocates one large chunk of memory of ``im->linesize * im->ysize`` bytes. The second works for -large images and make one allocation for each scan line of size +large images and makes one allocation for each scan line of size ``im->linesize`` bytes. This makes for a very sharp transition between one allocation and potentially thousands of small allocations, leading to unpredictable performance penalties around the transition. @@ -40,8 +40,8 @@ variables: * ``PILLOW_BLOCK_SIZE``, in bytes, K, or M. Specifies the maximum block size for ``ImagingAllocateArray``. Valid values are - integers, with an optional `k` or `m` suffix. Defaults to 16M. + integers, with an optional ``k`` or ``m`` suffix. Defaults to 16M. * ``PILLOW_BLOCKS_MAX`` Specifies the number of freed blocks to retain to fill future memory requests. Any freed blocks over this - threshold will be returned to the OS immediately. Defaults to 0. + threshold will be returned to the OS immediately. Defaults to 0. diff --git a/docs/reference/c_extension_debugging.rst b/docs/reference/c_extension_debugging.rst new file mode 100644 index 00000000000..dc4c2bf94d0 --- /dev/null +++ b/docs/reference/c_extension_debugging.rst @@ -0,0 +1,470 @@ +C Extension debugging on Linux, with gbd/valgrind. +================================================== + +Install the tools +----------------- + +You need some basics in addition to the basic tools to build +pillow. These are what's required on Ubuntu, YMMV for other +distributions. + +- ``python3-dbg`` package for the gdb extensions and python symbols +- ``gdb`` and ``valgrind`` +- Potentially debug symbols for libraries. On ubuntu they're shipped + in package-dbgsym packages, from a different repo. + +:: + + deb http://ddebs.ubuntu.com focal main restricted universe multiverse + deb http://ddebs.ubuntu.com focal-updates main restricted universe multiverse + deb http://ddebs.ubuntu.com focal-proposed main restricted universe multiverse + +Then ``sudo apt-get update && sudo apt-get install libtiff5-dbgsym`` + +- There's a bug with the dbg package for at least python 3.8 on ubuntu + 20.04, and you need to add a new link or two to make it autoload when + running python: + +:: + + cd /usr/share/gdb/auto-load/usr/bin + ln -s python3.8m-gdb.py python3.8d-gdb.py + +- In Ubuntu 18.04, it's actually including the path to the virtualenv + in the search for the ``python3.*-gdb.py`` file, but you can + helpfully put in the same directory as the binary. + +- I also find that history is really useful for gdb, so I added this to + my ``~/.gdbinit`` file: + +:: + + set history filename ~/.gdb_history + set history save on + +- If the python stack isn't working in gdb, then + ``set debug auto-load`` can also be helpful in ``.gdbinit``. + +- Make a virtualenv with the debug python and activate it, then install + whatever dependencies are required and build. You want to build with + the debug python so you get symbols for your extension. + +:: + + virtualenv -p python3.8-dbg ~/vpy38-dbg + source ~/vpy38-dbg/bin/activate + cd ~/Pillow && make install + +Test Case +--------- + +Take your test image, and make a really simple harness. + +:: + + from PIL import Image + + with Image.open(path) as im: + im.load() + +- Run this through valgrind, but note that python triggers some issues + on its own, so you're looking for items within the Pillow hierarchy + that don't look like they're solely in the python call chain. In this + example, the ones we're interested are after the warnings, and have + ``decode.c`` and ``TiffDecode.c`` in the call stack: + +:: + + (vpy38-dbg) ubuntu@primary:~/Home/tests$ valgrind python test_tiff.py + ==51890== Memcheck, a memory error detector + ==51890== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. + ==51890== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info + ==51890== Command: python test_tiff.py + ==51890== + ==51890== Invalid read of size 4 + ==51890== at 0x472E3D: address_in_range (obmalloc.c:1401) + ==51890== by 0x472EEA: pymalloc_free (obmalloc.c:1677) + ==51890== by 0x474960: _PyObject_Free (obmalloc.c:1896) + ==51890== by 0x473BAC: _PyMem_DebugRawFree (obmalloc.c:2187) + ==51890== by 0x473BD4: _PyMem_DebugFree (obmalloc.c:2318) + ==51890== by 0x474C08: PyObject_Free (obmalloc.c:709) + ==51890== by 0x45DD60: dictresize (dictobject.c:1259) + ==51890== by 0x45DD76: insertion_resize (dictobject.c:1019) + ==51890== by 0x464F30: PyDict_SetDefault (dictobject.c:2924) + ==51890== by 0x4D03BE: PyUnicode_InternInPlace (unicodeobject.c:15289) + ==51890== by 0x4D0700: PyUnicode_InternFromString (unicodeobject.c:15322) + ==51890== by 0x64D2FC: descr_new (descrobject.c:857) + ==51890== Address 0x4c1b020 is 384 bytes inside a block of size 1,160 free'd + ==51890== at 0x483CA3F: free (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) + ==51890== by 0x4735D3: _PyMem_RawFree (obmalloc.c:127) + ==51890== by 0x473BAC: _PyMem_DebugRawFree (obmalloc.c:2187) + ==51890== by 0x474941: PyMem_RawFree (obmalloc.c:595) + ==51890== by 0x47496E: _PyObject_Free (obmalloc.c:1898) + ==51890== by 0x473BAC: _PyMem_DebugRawFree (obmalloc.c:2187) + ==51890== by 0x473BD4: _PyMem_DebugFree (obmalloc.c:2318) + ==51890== by 0x474C08: PyObject_Free (obmalloc.c:709) + ==51890== by 0x45DD60: dictresize (dictobject.c:1259) + ==51890== by 0x45DD76: insertion_resize (dictobject.c:1019) + ==51890== by 0x464F30: PyDict_SetDefault (dictobject.c:2924) + ==51890== by 0x4D03BE: PyUnicode_InternInPlace (unicodeobject.c:15289) + ==51890== Block was alloc'd at + ==51890== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) + ==51890== by 0x473646: _PyMem_RawMalloc (obmalloc.c:99) + ==51890== by 0x473529: _PyMem_DebugRawAlloc (obmalloc.c:2120) + ==51890== by 0x473565: _PyMem_DebugRawMalloc (obmalloc.c:2153) + ==51890== by 0x4748B1: PyMem_RawMalloc (obmalloc.c:572) + ==51890== by 0x475909: _PyObject_Malloc (obmalloc.c:1628) + ==51890== by 0x473529: _PyMem_DebugRawAlloc (obmalloc.c:2120) + ==51890== by 0x473565: _PyMem_DebugRawMalloc (obmalloc.c:2153) + ==51890== by 0x4736B0: _PyMem_DebugMalloc (obmalloc.c:2303) + ==51890== by 0x474B78: PyObject_Malloc (obmalloc.c:685) + ==51890== by 0x45C435: new_keys_object (dictobject.c:558) + ==51890== by 0x45DA95: dictresize (dictobject.c:1202) + ==51890== + ==51890== Invalid read of size 4 + ==51890== at 0x472E3D: address_in_range (obmalloc.c:1401) + ==51890== by 0x47594A: pymalloc_realloc (obmalloc.c:1929) + ==51890== by 0x475A02: _PyObject_Realloc (obmalloc.c:1982) + ==51890== by 0x473DCA: _PyMem_DebugRawRealloc (obmalloc.c:2240) + ==51890== by 0x473FF8: _PyMem_DebugRealloc (obmalloc.c:2326) + ==51890== by 0x4749FB: PyMem_Realloc (obmalloc.c:623) + ==51890== by 0x44A6FC: list_resize (listobject.c:70) + ==51890== by 0x44A872: app1 (listobject.c:340) + ==51890== by 0x44FD65: PyList_Append (listobject.c:352) + ==51890== by 0x514315: r_ref (marshal.c:945) + ==51890== by 0x516034: r_object (marshal.c:1139) + ==51890== by 0x516C70: r_object (marshal.c:1389) + ==51890== Address 0x4c41020 is 32 bytes before a block of size 1,600 in arena "client" + ==51890== + ==51890== Conditional jump or move depends on uninitialised value(s) + ==51890== at 0x472E46: address_in_range (obmalloc.c:1403) + ==51890== by 0x47594A: pymalloc_realloc (obmalloc.c:1929) + ==51890== by 0x475A02: _PyObject_Realloc (obmalloc.c:1982) + ==51890== by 0x473DCA: _PyMem_DebugRawRealloc (obmalloc.c:2240) + ==51890== by 0x473FF8: _PyMem_DebugRealloc (obmalloc.c:2326) + ==51890== by 0x4749FB: PyMem_Realloc (obmalloc.c:623) + ==51890== by 0x44A6FC: list_resize (listobject.c:70) + ==51890== by 0x44A872: app1 (listobject.c:340) + ==51890== by 0x44FD65: PyList_Append (listobject.c:352) + ==51890== by 0x5E3321: _posix_listdir (posixmodule.c:3823) + ==51890== by 0x5E33A8: os_listdir_impl (posixmodule.c:3879) + ==51890== by 0x5E4D77: os_listdir (posixmodule.c.h:1197) + ==51890== + ==51890== Use of uninitialised value of size 8 + ==51890== at 0x472E59: address_in_range (obmalloc.c:1403) + ==51890== by 0x47594A: pymalloc_realloc (obmalloc.c:1929) + ==51890== by 0x475A02: _PyObject_Realloc (obmalloc.c:1982) + ==51890== by 0x473DCA: _PyMem_DebugRawRealloc (obmalloc.c:2240) + ==51890== by 0x473FF8: _PyMem_DebugRealloc (obmalloc.c:2326) + ==51890== by 0x4749FB: PyMem_Realloc (obmalloc.c:623) + ==51890== by 0x44A6FC: list_resize (listobject.c:70) + ==51890== by 0x44A872: app1 (listobject.c:340) + ==51890== by 0x44FD65: PyList_Append (listobject.c:352) + ==51890== by 0x5E3321: _posix_listdir (posixmodule.c:3823) + ==51890== by 0x5E33A8: os_listdir_impl (posixmodule.c:3879) + ==51890== by 0x5E4D77: os_listdir (posixmodule.c.h:1197) + ==51890== + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 16908288 bytes but only got 0. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67895296 bytes but only got 0. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 1572864 bytes but only got 0. Skipping tag 42 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 116647 bytes but only got 4867. Skipping tag 42738 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 3468830728 bytes but only got 4851. Skipping tag 279 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 2198732800 bytes but only got 0. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67239937 bytes but only got 4125. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33947764 bytes but only got 0. Skipping tag 139 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 17170432 bytes but only got 0. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 80478208 bytes but only got 0. Skipping tag 1 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 787460 bytes but only got 4882. Skipping tag 20 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 1075 bytes but only got 0. Skipping tag 256 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 120586240 bytes but only got 0. Skipping tag 194 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 65536 bytes but only got 0. Skipping tag 3 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 198656 bytes but only got 0. Skipping tag 279 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 206848 bytes but only got 0. Skipping tag 64512 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 130968 bytes but only got 4882. Skipping tag 256 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 77848 bytes but only got 4689. Skipping tag 64270 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 262156 bytes but only got 0. Skipping tag 257 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33624064 bytes but only got 0. Skipping tag 49152 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67178752 bytes but only got 4627. Skipping tag 50688 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33632768 bytes but only got 0. Skipping tag 56320 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 134386688 bytes but only got 4115. Skipping tag 2048 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33912832 bytes but only got 0. Skipping tag 7168 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 151966208 bytes but only got 4627. Skipping tag 10240 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 119032832 bytes but only got 3859. Skipping tag 256 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 46535680 bytes but only got 0. Skipping tag 256 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 35651584 bytes but only got 0. Skipping tag 42 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 524288 bytes but only got 0. Skipping tag 0 + warnings.warn( + _TIFFVSetField: tempfile.tif: Null count for "Tag 769" (type 1, writecount -3, passcount 1). + _TIFFVSetField: tempfile.tif: Null count for "Tag 42754" (type 1, writecount -3, passcount 1). + _TIFFVSetField: tempfile.tif: Null count for "Tag 769" (type 1, writecount -3, passcount 1). + _TIFFVSetField: tempfile.tif: Null count for "Tag 42754" (type 1, writecount -3, passcount 1). + ZIPDecode: Decoding error at scanline 0, incorrect header check. + ==51890== Invalid write of size 4 + ==51890== at 0x61C39E6: putcontig8bitYCbCr22tile (tif_getimage.c:2146) + ==51890== by 0x61C5865: gtStripContig (tif_getimage.c:977) + ==51890== by 0x6094317: ReadStrip (TiffDecode.c:269) + ==51890== by 0x6094749: ImagingLibTiffDecode (TiffDecode.c:479) + ==51890== by 0x60615D1: _decode (decode.c:136) + ==51890== by 0x64BF47: method_vectorcall_VARARGS (descrobject.c:300) + ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) + ==51890== by 0x4EB73C: call_function (ceval.c:4963) + ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) + ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) + ==51890== by 0x43627B: function_code_fastcall (call.c:283) + ==51890== by 0x436D21: _PyFunction_Vectorcall (call.c:410) + ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) + ==51890== by 0x4EB73C: call_function (ceval.c:4963) + ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) + ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) + ==51890== Address 0x6f456d4 is 0 bytes after a block of size 68 alloc'd + ==51890== at 0x483DFAF: realloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) + ==51890== by 0x60946D0: ImagingLibTiffDecode (TiffDecode.c:469) + ==51890== by 0x60615D1: _decode (decode.c:136) + ==51890== by 0x64BF47: method_vectorcall_VARARGS (descrobject.c:300) + ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) + ==51890== by 0x4EB73C: call_function (ceval.c:4963) + ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) + ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) + ==51890== by 0x43627B: function_code_fastcall (call.c:283) + ==51890== by 0x436D21: _PyFunction_Vectorcall (call.c:410) + ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) + ==51890== by 0x4EB73C: call_function (ceval.c:4963) + ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) + ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) + ==51890== by 0x4DFDFB: _PyEval_EvalCodeWithName (ceval.c:4298) + ==51890== by 0x436C40: _PyFunction_Vectorcall (call.c:435) + ==51890== + ==51890== Invalid write of size 4 + ==51890== at 0x61C39B5: putcontig8bitYCbCr22tile (tif_getimage.c:2145) + ==51890== by 0x61C5865: gtStripContig (tif_getimage.c:977) + ==51890== by 0x6094317: ReadStrip (TiffDecode.c:269) + ==51890== by 0x6094749: ImagingLibTiffDecode (TiffDecode.c:479) + ==51890== by 0x60615D1: _decode (decode.c:136) + ==51890== by 0x64BF47: method_vectorcall_VARARGS (descrobject.c:300) + ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) + ==51890== by 0x4EB73C: call_function (ceval.c:4963) + ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) + ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) + ==51890== by 0x43627B: function_code_fastcall (call.c:283) + ==51890== by 0x436D21: _PyFunction_Vectorcall (call.c:410) + ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) + ==51890== by 0x4EB73C: call_function (ceval.c:4963) + ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) + ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) + ==51890== Address 0x6f456d8 is 4 bytes after a block of size 68 alloc'd + ==51890== at 0x483DFAF: realloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) + ==51890== by 0x60946D0: ImagingLibTiffDecode (TiffDecode.c:469) + ==51890== by 0x60615D1: _decode (decode.c:136) + ==51890== by 0x64BF47: method_vectorcall_VARARGS (descrobject.c:300) + ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) + ==51890== by 0x4EB73C: call_function (ceval.c:4963) + ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) + ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) + ==51890== by 0x43627B: function_code_fastcall (call.c:283) + ==51890== by 0x436D21: _PyFunction_Vectorcall (call.c:410) + ==51890== by 0x4EB73C: _PyObject_Vectorcall (abstract.h:127) + ==51890== by 0x4EB73C: call_function (ceval.c:4963) + ==51890== by 0x4EB73C: _PyEval_EvalFrameDefault (ceval.c:3486) + ==51890== by 0x4DF2EE: PyEval_EvalFrameEx (ceval.c:741) + ==51890== by 0x4DFDFB: _PyEval_EvalCodeWithName (ceval.c:4298) + ==51890== by 0x436C40: _PyFunction_Vectorcall (call.c:435) + ==51890== + TIFFFillStrip: Invalid strip byte count 0, strip 1. + Traceback (most recent call last): + File "test_tiff.py", line 8, in + im.load() + File "/home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py", line 1087, in load + return self._load_libtiff() + File "/home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py", line 1191, in _load_libtiff + raise OSError(err) + OSError: -2 + sys:1: ResourceWarning: unclosed file <_io.BufferedReader name='crash-2020-10-test.tiff'> + ==51890== + ==51890== HEAP SUMMARY: + ==51890== in use at exit: 748,734 bytes in 444 blocks + ==51890== total heap usage: 6,320 allocs, 5,876 frees, 69,142,969 bytes allocated + ==51890== + ==51890== LEAK SUMMARY: + ==51890== definitely lost: 0 bytes in 0 blocks + ==51890== indirectly lost: 0 bytes in 0 blocks + ==51890== possibly lost: 721,538 bytes in 372 blocks + ==51890== still reachable: 27,196 bytes in 72 blocks + ==51890== suppressed: 0 bytes in 0 blocks + ==51890== Rerun with --leak-check=full to see details of leaked memory + ==51890== + ==51890== Use --track-origins=yes to see where uninitialised values come from + ==51890== For lists of detected and suppressed errors, rerun with: -s + ==51890== ERROR SUMMARY: 2556 errors from 6 contexts (suppressed: 0 from 0) + (vpy38-dbg) ubuntu@primary:~/Home/tests$ + +- Now that we've confirmed that there's something odd/bad going on, + it's time to gdb. +- Start with ``gdb python`` +- Set a break point starting with the valgrind stack trace. + ``b TiffDecode.c:269`` +- Run the script with ``r test_tiff.py`` +- When the break point is hit, explore the state with ``info locals``, + ``bt``, ``py-bt``, or ``p [variable]``. For pointers, + ``p *[variable]`` is useful. + +:: + + (vpy38-dbg) ubuntu@primary:~/Home/tests$ gdb python + GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2 + Copyright (C) 2020 Free Software Foundation, Inc. + License GPLv3+: GNU GPL version 3 or later + This is free software: you are free to change and redistribute it. + There is NO WARRANTY, to the extent permitted by law. + Type "show copying" and "show warranty" for details. + This GDB was configured as "x86_64-linux-gnu". + Type "show configuration" for configuration details. + For bug reporting instructions, please see: + . + Find the GDB manual and other documentation resources online at: + . + + For help, type "help". + Type "apropos word" to search for commands related to "word"... + Reading symbols from python... + (gdb) b TiffDecode.c:269 + No source file named TiffDecode.c. + Make breakpoint pending on future shared library load? (y or [n]) y + Breakpoint 1 (TiffDecode.c:269) pending. + (gdb) r test_tiff.py + Starting program: /home/ubuntu/vpy38-dbg/bin/python test_tiff.py + [Thread debugging using libthread_db enabled] + Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 16908288 bytes but only got 0. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67895296 bytes but only got 0. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 1572864 bytes but only got 0. Skipping tag 42 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 116647 bytes but only got 4867. Skipping tag 42738 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 3468830728 bytes but only got 4851. Skipping tag 279 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 2198732800 bytes but only got 0. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67239937 bytes but only got 4125. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33947764 bytes but only got 0. Skipping tag 139 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 17170432 bytes but only got 0. Skipping tag 0 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 80478208 bytes but only got 0. Skipping tag 1 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 787460 bytes but only got 4882. Skipping tag 20 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 1075 bytes but only got 0. Skipping tag 256 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 120586240 bytes but only got 0. Skipping tag 194 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 65536 bytes but only got 0. Skipping tag 3 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 198656 bytes but only got 0. Skipping tag 279 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 206848 bytes but only got 0. Skipping tag 64512 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 130968 bytes but only got 4882. Skipping tag 256 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 77848 bytes but only got 4689. Skipping tag 64270 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 262156 bytes but only got 0. Skipping tag 257 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33624064 bytes but only got 0. Skipping tag 49152 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 67178752 bytes but only got 4627. Skipping tag 50688 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33632768 bytes but only got 0. Skipping tag 56320 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 134386688 bytes but only got 4115. Skipping tag 2048 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 33912832 bytes but only got 0. Skipping tag 7168 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 151966208 bytes but only got 4627. Skipping tag 10240 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 119032832 bytes but only got 3859. Skipping tag 256 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 46535680 bytes but only got 0. Skipping tag 256 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 35651584 bytes but only got 0. Skipping tag 42 + warnings.warn( + /home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py:770: UserWarning: Possibly corrupt EXIF data. Expecting to read 524288 bytes but only got 0. Skipping tag 0 + warnings.warn( + _TIFFVSetField: tempfile.tif: Null count for "Tag 769" (type 1, writecount -3, passcount 1). + _TIFFVSetField: tempfile.tif: Null count for "Tag 42754" (type 1, writecount -3, passcount 1). + _TIFFVSetField: tempfile.tif: Null count for "Tag 769" (type 1, writecount -3, passcount 1). + _TIFFVSetField: tempfile.tif: Null count for "Tag 42754" (type 1, writecount -3, passcount 1). + + Breakpoint 1, ReadStrip (tiff=tiff@entry=0xae9b90, row=0, buffer=0xac2eb0) at src/libImaging/TiffDecode.c:269 + 269 ok = TIFFRGBAImageGet(&img, buffer, img.width, rows_to_read); + (gdb) p img + $1 = {tif = 0xae9b90, stoponerr = 0, isContig = 1, alpha = 0, width = 20, height = 1536, bitspersample = 8, samplesperpixel = 3, + orientation = 1, req_orientation = 1, photometric = 6, redcmap = 0x0, greencmap = 0x0, bluecmap = 0x0, get = + 0x7ffff71d0710 , put = {any = 0x7ffff71ce550 , + contig = 0x7ffff71ce550 , separate = 0x7ffff71ce550 }, Map = 0x0, + BWmap = 0x0, PALmap = 0x0, ycbcr = 0xaf24b0, cielab = 0x0, UaToAa = 0x0, Bitdepth16To8 = 0x0, row_offset = 0, col_offset = 0} + (gdb) up + #1 0x00007ffff736174a in ImagingLibTiffDecode (im=0xac1f90, state=0x7ffff76767e0, buffer=, bytes=) + at src/libImaging/TiffDecode.c:479 + 479 if (ReadStrip(tiff, state->y, (UINT32 *)state->buffer) == -1) { + (gdb) p *state + $2 = {count = 0, state = 0, errcode = 0, x = 0, y = 0, ystep = 0, xsize = 17, ysize = 108, xoff = 0, yoff = 0, + shuffle = 0x7ffff735f411 , bits = 32, bytes = 68, buffer = 0xac2eb0 "P\354\336\367\377\177", context = 0xa75440, fd = 0x0} + (gdb) py-bt + Traceback (most recent call first): + File "/home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py", line 1428, in _load_libtiff + + File "/home/ubuntu/vpy38-dbg/lib/python3.8/site-packages/Pillow-8.0.1-py3.8-linux-x86_64.egg/PIL/TiffImagePlugin.py", line 1087, in load + return self._load_libtiff() + File "test_tiff.py", line 8, in + im.load() + +- Poke around till you understand what's going on. In this case, + state->xsize and img.width are different, which led to an out of + bounds write, as the receiving buffer was sized for the smaller of + the two. + +Caveats +------- + +- If your program is running/hung in a docker container and your host + has the appropriate tools, you can run gdb as the superuser in the + host and you may be able to get a trace of where the process is hung. + You probably won't have the capability to do that from within the + docker container, as the trace capacity isn't allowed by default. + +- Variations of this are possible on the mac/windows, but the details + are going to be different. + +- IIRC, Fedora has the gdb bits working by default. Ubuntu has always + been a bit of a battle to make it work. diff --git a/docs/reference/features.rst b/docs/reference/features.rst new file mode 100644 index 00000000000..c6619306186 --- /dev/null +++ b/docs/reference/features.rst @@ -0,0 +1,66 @@ +.. py:module:: PIL.features +.. py:currentmodule:: PIL.features + +:py:mod:`~PIL.features` Module +============================== + +The :py:mod:`PIL.features` module can be used to detect which Pillow features are available on your system. + +.. autofunction:: PIL.features.pilinfo +.. autofunction:: PIL.features.check +.. autofunction:: PIL.features.version +.. autofunction:: PIL.features.get_supported + +Modules +------- + +Support for the following modules can be checked: + +* ``pil``: The Pillow core module, required for all functionality. +* ``tkinter``: Tkinter support. +* ``freetype2``: FreeType font support via :py:func:`PIL.ImageFont.truetype`. +* ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`. +* ``webp``: WebP image support. + +.. autofunction:: PIL.features.check_module +.. autofunction:: PIL.features.version_module +.. autofunction:: PIL.features.get_supported_modules + +Codecs +------ + +Support for these is only checked during Pillow compilation. +If the required library was uninstalled from the system, the ``pil`` core module may fail to load instead. +Except for ``jpg``, the version number is checked at run-time. + +Support for the following codecs can be checked: + +* ``jpg``: (compile time) Libjpeg support, required for JPEG based image formats. Only compile time version number is available. +* ``jpg_2000``: (compile time) OpenJPEG support, required for JPEG 2000 image formats. +* ``zlib``: (compile time) Zlib support, required for zlib compressed formats, such as PNG. +* ``libtiff``: (compile time) LibTIFF support, required for TIFF based image formats. + +.. autofunction:: PIL.features.check_codec +.. autofunction:: PIL.features.version_codec +.. autofunction:: PIL.features.get_supported_codecs + +Features +-------- + +Some of these are only checked during Pillow compilation. +If the required library was uninstalled from the system, the relevant module may fail to load instead. +Feature version numbers are available only where stated. + +Support for the following features can be checked: + +* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. +* ``transp_webp``: Support for transparency in WebP images. +* ``webp_mux``: (compile time) Support for EXIF data in WebP images. +* ``webp_anim``: (compile time) Support for animated WebP images. +* ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. +* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. +* ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. + +.. autofunction:: PIL.features.check_feature +.. autofunction:: PIL.features.version_feature +.. autofunction:: PIL.features.get_supported_features diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 8c09e7b67f7..5d6affa94ad 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -7,8 +7,8 @@ Reference Image ImageChops - ImageColor ImageCms + ImageColor ImageDraw ImageEnhance ImageFile @@ -22,14 +22,17 @@ Reference ImagePath ImageQt ImageSequence + ImageShow ImageStat ImageTk ImageWin ExifTags TiffTags + JpegPresets PSDraw PixelAccess PyAccess + features ../PIL plugins internal_design diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst index 4c0fbb85d50..2e2d3322f75 100644 --- a/docs/reference/internal_design.rst +++ b/docs/reference/internal_design.rst @@ -3,8 +3,9 @@ Internal Reference Docs .. toctree:: :maxdepth: 2 - + open_files limits block_allocator - + internal_modules + c_extension_debugging diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst new file mode 100644 index 00000000000..363a67d9b02 --- /dev/null +++ b/docs/reference/internal_modules.rst @@ -0,0 +1,55 @@ +Internal Modules +================ + +:mod:`~PIL._binary` Module +-------------------------- + +.. automodule:: PIL._binary + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL._deprecate` Module +----------------------------- + +.. automodule:: PIL._deprecate + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL._tkinter_finder` Module +---------------------------------- + +.. automodule:: PIL._tkinter_finder + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL._util` Module +------------------------ + +.. automodule:: PIL._util + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL._version` Module +--------------------------- + +.. module:: PIL._version + +.. data:: __version__ + :annotation: + :type: str + + This is the master version number for Pillow, + all other uses reference this module. + +:mod:`PIL.Image.core` Module +---------------------------- + +.. module:: PIL._imaging +.. module:: PIL.Image.core + +An internal interface module previously known as :mod:`~PIL._imaging`, +implemented in :file:`_imaging.c`. diff --git a/docs/reference/limits.rst b/docs/reference/limits.rst index 79dc66e6779..a71b514b5aa 100644 --- a/docs/reference/limits.rst +++ b/docs/reference/limits.rst @@ -25,13 +25,6 @@ Internal Limits is smaller than 2GB, as calculated by ``y*stride`` (so 2Gpx for 'L' images, and .5Gpx for 'RGB' -* Any call to internal python size functions for buffers or strings - are currently returned as int32, not py_ssize_t. This limits the - maximum buffer to 2GB for operations like frombytes and frombuffer. - -* This also limits the size of buffers converted using a - decoder. (decode.c:127) - Format Size Limits ================== diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index 143eb7209fd..6bfd50588ab 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -1,115 +1,102 @@ +.. _file-handling: + File Handling in Pillow ======================= -When opening a file as an image, Pillow requires a filename, -pathlib.Path object, or a file-like object. Pillow uses the filename -or Path to open a file, so for the rest of this article, they will all -be treated as a file-like object. +When opening a file as an image, Pillow requires a filename, ``pathlib.Path`` +object, or a file-like object. Pillow uses the filename or ``Path`` to open a +file, so for the rest of this article, they will all be treated as a file-like +object. -The first four of these items are equivalent, the last is dangerous -and may fail:: +The following are all equivalent:: from PIL import Image import io import pathlib - - im = Image.open('test.jpg') - - im2 = Image.open(pathlib.Path('test.jpg')) - f = open('test.jpg', 'rb') - im3 = Image.open(f) - - with open('test.jpg', 'rb') as f: - im4 = Image.open(io.BytesIO(f.read())) + with Image.open("test.jpg") as im: + ... - # Dangerous FAIL: - with open('test.jpg', 'rb') as f: - im5 = Image.open(f) - im5.load() # FAILS, closed file + with Image.open(pathlib.Path("test.jpg")) as im2: + ... -The documentation specifies that the file will be closed after the -``Image.Image.load()`` method is called. This is an aspirational -specification rather than an accurate reflection of the state of the -code. + with open("test.jpg", "rb") as f: + im3 = Image.open(f) + ... -Pillow cannot in general close and reopen a file, so any access to -that file needs to be prior to the close. - -Issues ------- + with open("test.jpg", "rb") as f: + im4 = Image.open(io.BytesIO(f.read())) + ... -The current open file handling is inconsistent at best: +If a filename or a path-like object is passed to Pillow, then the resulting +file object opened by Pillow may also be closed by Pillow after the +``Image.Image.load()`` method is called, provided the associated image does not +have multiple frames. -* Most of the image plugins do not close the input file. -* Multi-frame images behave badly when seeking through the file, as - it's legal to seek backward in the file until the last image is - read, and then it's not. -* Using the file context manager to provide a file-like object to - Pillow is dangerous unless the context of the image is limited to - the context of the file. +Pillow cannot in general close and reopen a file, so any access to +that file needs to be prior to the close. Image Lifecycle --------------- -* ``Image.open()`` called. Path-like objects are opened as a - file. Metadata is read from the open file. The file is left open for - further usage. +* ``Image.open()`` Filenames and ``Path`` objects are opened as a file. + Metadata is read from the open file. The file is left open for further usage. -* ``Image.Image.load()`` when the pixel data from the image is +* ``Image.Image.load()`` When the pixel data from the image is required, ``load()`` is called. The current frame is read into memory. The image can now be used independently of the underlying - image file. + image file. + + Any Pillow method that creates a new image instance based on another will + internally call ``load()`` on the original image and then read the data. + The new image instance will not be associated with the original image file. + + If a filename or a ``Path`` object was passed to ``Image.open()``, then the + file object was opened by Pillow and is considered to be used exclusively by + Pillow. So if the image is a single-frame image, the file will be closed in + this method after the frame is read. If the image is a multi-frame image, + (e.g. multipage TIFF and animated GIF) the image file is left open so that + ``Image.Image.seek()`` can load the appropriate frame. + +* ``Image.Image.close()`` Closes the file and destroys the core image object. -* ``Image.Image.seek()`` in the case of multi-frame images - (e.g. multipage TIFF and animated GIF) the image file left open so - that seek can load the appropriate frame. When the last frame is - read, the image file is closed (at least in some image plugins), and - no more seeks can occur. + The Pillow context manager will also close the file, but will not destroy + the core image object. e.g.: -* ``Image.Image.close()`` Closes the file pointer and destroys the - core image object. This is used in the Pillow context manager - support. e.g.:: +.. code-block:: python - with Image.open('test.jpg') as img: - ... # image operations here. + with Image.open("test.jpg") as img: + img.load() + assert img.fp is None + img.save("test.png") -The lifecycle of a single frame image is relatively simple. The file -must remain open until the ``load()`` or ``close()`` function is -called. +The lifecycle of a single-frame image is relatively simple. The file must +remain open until the ``load()`` or ``close()`` function is called or the +context manager exits. Multi-frame images are more complicated. The ``load()`` method is not -a terminal method, so it should not close the underlying file. The -current behavior of ``seek()`` closing the underlying file on -accessing the last frame is presumably a heuristic for closing the -file after iterating through the entire sequence. In general, Pillow -does not know if there are going to be any requests for additional -data until the caller has explicitly closed the image. +a terminal method, so it should not close the underlying file. In general, +Pillow does not know if there are going to be any requests for additional +data until the caller has explicitly closed the image. Complications ------------- -* TiffImagePlugin has some code to pass the underlying file descriptor - into libtiff (if working on an actual file). Since libtiff closes - the file descriptor internally, it is duplicated prior to passing it - into libtiff. +* ``TiffImagePlugin`` has some code to pass the underlying file descriptor into + libtiff (if working on an actual file). Since libtiff closes the file + descriptor internally, it is duplicated prior to passing it into libtiff. -* ``decoder.handles_eof`` This slightly misnamed flag indicates that - the decoder wants to be called with a 0 length buffer when reads are - done. Despite the comments in ``ImageFile.load()``, the only decoder - that actually uses this flag is the Jpeg2K decoder. The use of this - flag in Jpeg2K predated the change to the decoder that added the - pulls_fd flag, and is therefore not used. +* After a file has been closed, operations that require file access will fail:: -* I don't think that there's any way to make this safe without - changing the lazy loading:: - - # Dangerous FAIL: - with open('test.jpg', 'rb') as f: + with open("test.jpg", "rb") as f: im5 = Image.open(f) - im5.load() # FAILS, closed file + im5.load() # FAILS, closed file + + with Image.open("test.jpg") as im6: + pass + im6.load() # FAILS, closed file Proposed File Handling @@ -118,8 +105,8 @@ Proposed File Handling * ``Image.Image.load()`` should close the image file, unless there are multiple frames. -* ``Image.Image.seek()`` should never close the image file. - -* Users of the library should call ``Image.Image.close()`` on any - multi-frame image to ensure that the underlying file is closed. +* ``Image.Image.seek()`` should never close the image file. +* Users of the library should use a context manager or call + ``Image.Image.close()`` on any image opened with a filename or ``Path`` + object to ensure that the underlying file is closed. diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index 46f657fce57..fcf4514a84d 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -1,340 +1,331 @@ Plugin reference ================ -:mod:`BmpImagePlugin` Module ----------------------------- +:mod:`~PIL.BmpImagePlugin` Module +--------------------------------- .. automodule:: PIL.BmpImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`BufrStubImagePlugin` Module ---------------------------------- +:mod:`~PIL.BufrStubImagePlugin` Module +-------------------------------------- .. automodule:: PIL.BufrStubImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`CurImagePlugin` Module ----------------------------- +:mod:`~PIL.CurImagePlugin` Module +--------------------------------- .. automodule:: PIL.CurImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`DcxImagePlugin` Module ----------------------------- +:mod:`~PIL.DcxImagePlugin` Module +--------------------------------- .. automodule:: PIL.DcxImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`EpsImagePlugin` Module ----------------------------- +:mod:`~PIL.EpsImagePlugin` Module +--------------------------------- .. automodule:: PIL.EpsImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`FitsStubImagePlugin` Module ---------------------------------- +:mod:`~PIL.FitsImagePlugin` Module +-------------------------------------- -.. automodule:: PIL.FitsStubImagePlugin +.. automodule:: PIL.FitsImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`FliImagePlugin` Module ----------------------------- +:mod:`~PIL.FliImagePlugin` Module +--------------------------------- .. automodule:: PIL.FliImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`FpxImagePlugin` Module ----------------------------- +:mod:`~PIL.FpxImagePlugin` Module +--------------------------------- .. automodule:: PIL.FpxImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`GbrImagePlugin` Module ----------------------------- +:mod:`~PIL.GbrImagePlugin` Module +--------------------------------- .. automodule:: PIL.GbrImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`GifImagePlugin` Module ----------------------------- +:mod:`~PIL.GifImagePlugin` Module +--------------------------------- .. automodule:: PIL.GifImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`GribStubImagePlugin` Module ---------------------------------- +:mod:`~PIL.GribStubImagePlugin` Module +-------------------------------------- .. automodule:: PIL.GribStubImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`Hdf5StubImagePlugin` Module ---------------------------------- +:mod:`~PIL.Hdf5StubImagePlugin` Module +-------------------------------------- .. automodule:: PIL.Hdf5StubImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`IcnsImagePlugin` Module ------------------------------ +:mod:`~PIL.IcnsImagePlugin` Module +---------------------------------- .. automodule:: PIL.IcnsImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`IcoImagePlugin` Module ----------------------------- +:mod:`~PIL.IcoImagePlugin` Module +--------------------------------- .. automodule:: PIL.IcoImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`ImImagePlugin` Module ---------------------------- +:mod:`~PIL.ImImagePlugin` Module +-------------------------------- .. automodule:: PIL.ImImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`ImtImagePlugin` Module ----------------------------- +:mod:`~PIL.ImtImagePlugin` Module +--------------------------------- .. automodule:: PIL.ImtImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`IptcImagePlugin` Module ------------------------------ +:mod:`~PIL.IptcImagePlugin` Module +---------------------------------- .. automodule:: PIL.IptcImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`JpegImagePlugin` Module ------------------------------ +:mod:`~PIL.JpegImagePlugin` Module +---------------------------------- .. automodule:: PIL.JpegImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`Jpeg2KImagePlugin` Module -------------------------------- +:mod:`~PIL.Jpeg2KImagePlugin` Module +------------------------------------ .. automodule:: PIL.Jpeg2KImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`McIdasImagePlugin` Module -------------------------------- +:mod:`~PIL.McIdasImagePlugin` Module +------------------------------------ .. automodule:: PIL.McIdasImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`MicImagePlugin` Module ----------------------------- +:mod:`~PIL.MicImagePlugin` Module +--------------------------------- .. automodule:: PIL.MicImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`MpegImagePlugin` Module ------------------------------ +:mod:`~PIL.MpegImagePlugin` Module +---------------------------------- .. automodule:: PIL.MpegImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`MspImagePlugin` Module ----------------------------- +:mod:`~PIL.MspImagePlugin` Module +--------------------------------- .. automodule:: PIL.MspImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`PalmImagePlugin` Module ------------------------------ +:mod:`~PIL.PalmImagePlugin` Module +---------------------------------- .. automodule:: PIL.PalmImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`PcdImagePlugin` Module ----------------------------- +:mod:`~PIL.PcdImagePlugin` Module +--------------------------------- .. automodule:: PIL.PcdImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`PcxImagePlugin` Module ----------------------------- +:mod:`~PIL.PcxImagePlugin` Module +--------------------------------- .. automodule:: PIL.PcxImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`PdfImagePlugin` Module ----------------------------- +:mod:`~PIL.PdfImagePlugin` Module +--------------------------------- .. automodule:: PIL.PdfImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`PixarImagePlugin` Module ------------------------------- +:mod:`~PIL.PixarImagePlugin` Module +----------------------------------- .. automodule:: PIL.PixarImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`PngImagePlugin` Module ----------------------------- +:mod:`~PIL.PngImagePlugin` Module +--------------------------------- .. automodule:: PIL.PngImagePlugin - :members: ChunkStream, PngImageFile, PngStream, getchunks, is_cid, putchunk - :show-inheritance: -.. autoclass:: PIL.PngImagePlugin.ChunkStream - :members: - :undoc-members: - :show-inheritance: -.. autoclass:: PIL.PngImagePlugin.PngImageFile - :members: - :undoc-members: - :show-inheritance: -.. autoclass:: PIL.PngImagePlugin.PngStream - :members: + :members: ChunkStream, PngImageFile, PngStream, getchunks, is_cid, putchunk, + Blend, Disposal, MAX_TEXT_CHUNK, MAX_TEXT_MEMORY :undoc-members: :show-inheritance: + :member-order: groupwise -:mod:`PpmImagePlugin` Module ----------------------------- +:mod:`~PIL.PpmImagePlugin` Module +--------------------------------- .. automodule:: PIL.PpmImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`PsdImagePlugin` Module ----------------------------- +:mod:`~PIL.PsdImagePlugin` Module +--------------------------------- .. automodule:: PIL.PsdImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`SgiImagePlugin` Module ----------------------------- +:mod:`~PIL.SgiImagePlugin` Module +--------------------------------- .. automodule:: PIL.SgiImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`SpiderImagePlugin` Module -------------------------------- +:mod:`~PIL.SpiderImagePlugin` Module +------------------------------------ .. automodule:: PIL.SpiderImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`SunImagePlugin` Module ----------------------------- +:mod:`~PIL.SunImagePlugin` Module +--------------------------------- .. automodule:: PIL.SunImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`TgaImagePlugin` Module ----------------------------- +:mod:`~PIL.TgaImagePlugin` Module +--------------------------------- .. automodule:: PIL.TgaImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`TiffImagePlugin` Module ------------------------------ +:mod:`~PIL.TiffImagePlugin` Module +---------------------------------- .. automodule:: PIL.TiffImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`WebPImagePlugin` Module ------------------------------ +:mod:`~PIL.WebPImagePlugin` Module +---------------------------------- .. automodule:: PIL.WebPImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`WmfImagePlugin` Module ----------------------------- +:mod:`~PIL.WmfImagePlugin` Module +--------------------------------- .. automodule:: PIL.WmfImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`XVThumbImagePlugin` Module --------------------------------- +:mod:`~PIL.XVThumbImagePlugin` Module +------------------------------------- .. automodule:: PIL.XVThumbImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`XbmImagePlugin` Module ----------------------------- +:mod:`~PIL.XbmImagePlugin` Module +--------------------------------- .. automodule:: PIL.XbmImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`XpmImagePlugin` Module ----------------------------- +:mod:`~PIL.XpmImagePlugin` Module +--------------------------------- .. automodule:: PIL.XpmImagePlugin :members: diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst index 4bb25e37104..dda814c1f7d 100644 --- a/docs/releasenotes/2.7.0.rst +++ b/docs/releasenotes/2.7.0.rst @@ -14,7 +14,7 @@ Png text chunk size limits To prevent potential denial of service attacks using compressed text chunks, there are now limits to the decompressed size of text chunks decoded from PNG images. If the limits are exceeded when opening a PNG -image a ``ValueError`` will be raised. +image a :py:exc:`ValueError` will be raised. Individual text chunks are limited to :py:attr:`PIL.PngImagePlugin.MAX_TEXT_CHUNK`, set to 1MB by @@ -27,55 +27,55 @@ Image resizing filters ---------------------- Image resizing methods :py:meth:`~PIL.Image.Image.resize` and -:py:meth:`~PIL.Image.Image.thumbnail` take a `resample` argument, which tells +:py:meth:`~PIL.Image.Image.thumbnail` take a ``resample`` argument, which tells which filter should be used for resampling. Possible values are: -:py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BILINEAR`, -:py:attr:`PIL.Image.BICUBIC` and :py:attr:`PIL.Image.ANTIALIAS`. +:py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BILINEAR`, +:py:data:`PIL.Image.BICUBIC` and :py:data:`PIL.Image.ANTIALIAS`. Almost all of them were changed in this version. Bicubic and bilinear downscaling ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -From the beginning :py:attr:`~PIL.Image.BILINEAR` and -:py:attr:`~PIL.Image.BICUBIC` filters were based on affine transformations +From the beginning :py:data:`~PIL.Image.BILINEAR` and +:py:data:`~PIL.Image.BICUBIC` filters were based on affine transformations and used a fixed number of pixels from the source image for every destination -pixel (2x2 pixels for :py:attr:`~PIL.Image.BILINEAR` and 4x4 for -:py:attr:`~PIL.Image.BICUBIC`). This gave an unsatisfactory result for +pixel (2x2 pixels for :py:data:`~PIL.Image.BILINEAR` and 4x4 for +:py:data:`~PIL.Image.BICUBIC`). This gave an unsatisfactory result for downscaling. At the same time, a high quality convolutions-based algorithm with -flexible kernel was used for :py:attr:`~PIL.Image.ANTIALIAS` filter. +flexible kernel was used for :py:data:`~PIL.Image.ANTIALIAS` filter. Starting from Pillow 2.7.0, a high quality convolutions-based algorithm is used for all of these three filters. If you have previously used any tricks to maintain quality when downscaling with -:py:attr:`~PIL.Image.BILINEAR` and :py:attr:`~PIL.Image.BICUBIC` filters +:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` filters (for example, reducing within several steps), they are unnecessary now. Antialias renamed to Lanczos ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -A new :py:attr:`PIL.Image.LANCZOS` constant was added instead of -:py:attr:`~PIL.Image.ANTIALIAS`. +A new :py:data:`PIL.Image.LANCZOS` constant was added instead of +:py:data:`~PIL.Image.ANTIALIAS`. -When :py:attr:`~PIL.Image.ANTIALIAS` was initially added, it was the only +When :py:data:`~PIL.Image.ANTIALIAS` was initially added, it was the only high-quality filter based on convolutions. It's name was supposed to reflect this. Starting from Pillow 2.7.0 all resize method are based on convolutions. All of them are antialias from now on. And the real name of the -:py:attr:`~PIL.Image.ANTIALIAS` filter is Lanczos filter. +:py:data:`~PIL.Image.ANTIALIAS` filter is Lanczos filter. -The :py:attr:`~PIL.Image.ANTIALIAS` constant is left for backward compatibility -and is an alias for :py:attr:`~PIL.Image.LANCZOS`. +The :py:data:`~PIL.Image.ANTIALIAS` constant is left for backward compatibility +and is an alias for :py:data:`~PIL.Image.LANCZOS`. Lanczos upscaling quality ^^^^^^^^^^^^^^^^^^^^^^^^^ -The image upscaling quality with :py:attr:`~PIL.Image.LANCZOS` filter was -almost the same as :py:attr:`~PIL.Image.BILINEAR` due to bug. This has been fixed. +The image upscaling quality with :py:data:`~PIL.Image.LANCZOS` filter was +almost the same as :py:data:`~PIL.Image.BILINEAR` due to bug. This has been fixed. Bicubic upscaling quality ^^^^^^^^^^^^^^^^^^^^^^^^^ -The :py:attr:`~PIL.Image.BICUBIC` filter for affine transformations produced +The :py:data:`~PIL.Image.BICUBIC` filter for affine transformations produced sharp, slightly pixelated image for upscaling. Bicubic for convolutions is more soft. @@ -84,43 +84,41 @@ Resize performance In most cases, convolution is more a expensive algorithm for downscaling because it takes into account all the pixels of source image. Therefore -:py:attr:`~PIL.Image.BILINEAR` and :py:attr:`~PIL.Image.BICUBIC` filters' +:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` filters' performance can be lower than before. On the other hand the quality of -:py:attr:`~PIL.Image.BILINEAR` and :py:attr:`~PIL.Image.BICUBIC` was close to -:py:attr:`~PIL.Image.NEAREST`. So if such quality is suitable for your tasks -you can switch to :py:attr:`~PIL.Image.NEAREST` filter for downscaling, +:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` was close to +:py:data:`~PIL.Image.NEAREST`. So if such quality is suitable for your tasks +you can switch to :py:data:`~PIL.Image.NEAREST` filter for downscaling, which will give a huge improvement in performance. At the same time performance of convolution resampling for downscaling has been improved by around a factor of two compared to the previous version. -The upscaling performance of the :py:attr:`~PIL.Image.LANCZOS` filter has -remained the same. For :py:attr:`~PIL.Image.BILINEAR` filter it has improved by -1.5 times and for :py:attr:`~PIL.Image.BICUBIC` by four times. +The upscaling performance of the :py:data:`~PIL.Image.LANCZOS` filter has +remained the same. For :py:data:`~PIL.Image.BILINEAR` filter it has improved by +1.5 times and for :py:data:`~PIL.Image.BICUBIC` by four times. Default filter for thumbnails ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In Pillow 2.5 the default filter for :py:meth:`~PIL.Image.Image.thumbnail` was -changed from :py:attr:`~PIL.Image.NEAREST` to :py:attr:`~PIL.Image.ANTIALIAS`. +changed from :py:data:`~PIL.Image.NEAREST` to :py:data:`~PIL.Image.ANTIALIAS`. Antialias was chosen because all the other filters gave poor quality for -reduction. Starting from Pillow 2.7.0, :py:attr:`~PIL.Image.ANTIALIAS` has been -replaced with :py:attr:`~PIL.Image.BICUBIC`, because it's faster and -:py:attr:`~PIL.Image.ANTIALIAS` doesn't give any advantages after +reduction. Starting from Pillow 2.7.0, :py:data:`~PIL.Image.ANTIALIAS` has been +replaced with :py:data:`~PIL.Image.BICUBIC`, because it's faster and +:py:data:`~PIL.Image.ANTIALIAS` doesn't give any advantages after downscaling with libjpeg, which uses supersampling internally, not convolutions. Image transposition ------------------- -A new method :py:attr:`PIL.Image.TRANSPOSE` has been added for the +A new method ``TRANSPOSE`` has been added for the :py:meth:`~PIL.Image.Image.transpose` operation in addition to -:py:attr:`~PIL.Image.FLIP_LEFT_RIGHT`, :py:attr:`~PIL.Image.FLIP_TOP_BOTTOM`, -:py:attr:`~PIL.Image.ROTATE_90`, :py:attr:`~PIL.Image.ROTATE_180`, -:py:attr:`~PIL.Image.ROTATE_270`. :py:attr:`~PIL.Image.TRANSPOSE` is an algebra -transpose, with an image reflected across its main diagonal. - -The speed of :py:attr:`~PIL.Image.ROTATE_90`, :py:attr:`~PIL.Image.ROTATE_270` -and :py:attr:`~PIL.Image.TRANSPOSE` has been significantly improved for large -images which don't fit in the processor cache. +``FLIP_LEFT_RIGHT``, ``FLIP_TOP_BOTTOM``, ``ROTATE_90``, ``ROTATE_180``, +``ROTATE_270``. ``TRANSPOSE`` is an algebra transpose, with an image reflected +across its main diagonal. + +The speed of ``ROTATE_90``, ``ROTATE_270`` and ``TRANSPOSE`` has been significantly +improved for large images which don't fit in the processor cache. Gaussian blur and unsharp mask ------------------------------ diff --git a/docs/releasenotes/2.8.0.rst b/docs/releasenotes/2.8.0.rst index 85235d72aa9..c522fe8b0a3 100644 --- a/docs/releasenotes/2.8.0.rst +++ b/docs/releasenotes/2.8.0.rst @@ -4,18 +4,28 @@ Open HTTP response objects with Image.open ------------------------------------------ -HTTP response objects returned from `urllib2.urlopen(url)` or `requests.get(url, stream=True).raw` are 'file-like' but do not support `.seek()` operations. As a result PIL was unable to open them as images, requiring a wrap in `cStringIO` or `BytesIO`. +HTTP response objects returned from ``urllib2.urlopen(url)`` or +``requests.get(url, stream=True).raw`` are 'file-like' but do not support ``.seek()`` +operations. As a result PIL was unable to open them as images, requiring a wrap in +``cStringIO`` or ``BytesIO``. -Now new functionality has been added to `Image.open()` by way of an `.seek(0)` check and catch on exception `AttributeError` or `io.UnsupportedOperation`. If this is caught we attempt to wrap the object using `io.BytesIO` (which will only work on buffer-file-like objects). +Now new functionality has been added to ``Image.open()`` by way of an ``.seek(0)`` check and +catch on exception ``AttributeError`` or ``io.UnsupportedOperation``. If this is caught we +attempt to wrap the object using ``io.BytesIO`` (which will only work on buffer-file-like +objects). -This allows opening of files using both `urllib2` and `requests`, e.g.:: +This allows opening of files using both ``urllib2`` and ``requests``, e.g.:: Image.open(urllib2.urlopen(url)) Image.open(requests.get(url, stream=True).raw) -If the response uses content-encoding (compression, either gzip or deflate) then this will fail as both the urllib2 and requests raw file object will produce compressed data in that case. Using Content-Encoding on images is rather non-sensical as most images are already compressed, but it can still happen. +If the response uses content-encoding (compression, either gzip or deflate) then this +will fail as both the urllib2 and requests raw file object will produce compressed data +in that case. Using Content-Encoding on images is rather non-sensical as most images are +already compressed, but it can still happen. -For requests the work-around is to set the decode_content attribute on the raw object to True:: +For requests the work-around is to set the decode_content attribute on the raw object to +True:: response = requests.get(url, stream=True) response.raw.decode_content = True diff --git a/docs/releasenotes/3.0.0.rst b/docs/releasenotes/3.0.0.rst index 9cc1de98c49..67569d3378b 100644 --- a/docs/releasenotes/3.0.0.rst +++ b/docs/releasenotes/3.0.0.rst @@ -5,8 +5,8 @@ Saving Multipage Images ----------------------- -There is now support for saving multipage images in the `GIF` and -`PDF` formats. To enable this functionality, pass in `save_all=True` +There is now support for saving multipage images in the ``GIF`` and +``PDF`` formats. To enable this functionality, pass in ``save_all=True`` as a keyword argument to the save:: im.save('test.pdf', save_all=True) @@ -37,7 +37,7 @@ have been removed in this release:: ImageDraw.setink() ImageDraw.setfill() The ImageFileIO module - The ImageFont.FreeTypeFont and ImageFont.truetype `file` keyword arg + The ImageFont.FreeTypeFont and ImageFont.truetype ``file`` keyword arg The ImagePalette private _make functions ImageWin.fromstring() ImageWin.tostring() diff --git a/docs/releasenotes/3.1.0.rst b/docs/releasenotes/3.1.0.rst index 388af03acb9..3cdb6939d49 100644 --- a/docs/releasenotes/3.1.0.rst +++ b/docs/releasenotes/3.1.0.rst @@ -5,8 +5,8 @@ ImageDraw arc, chord and pieslice can now use floats ---------------------------------------------------- -There is no longer a need to ensure that the start and end arguments for `arc`, -`chord` and `pieslice` are integers. +There is no longer a need to ensure that the start and end arguments for ``arc``, +``chord`` and ``pieslice`` are integers. Note that these numbers are not simply rounded internally, but are actually utilised in the drawing process. diff --git a/docs/releasenotes/3.1.1.rst b/docs/releasenotes/3.1.1.rst index 8c32a43e74e..38118ea39c4 100644 --- a/docs/releasenotes/3.1.1.rst +++ b/docs/releasenotes/3.1.1.rst @@ -6,7 +6,7 @@ CVE-2016-0740 -- Buffer overflow in TiffDecode.c ------------------------------------------------ Pillow 3.1.0 and earlier when linked against libtiff >= 4.0.0 on x64 -may overflow a buffer when reading a specially crafted tiff file. +may overflow a buffer when reading a specially crafted tiff file (:cve:`CVE-2016-0740`). Specifically, libtiff >= 4.0.0 changed the return type of ``TIFFScanlineSize`` from ``int32`` to machine dependent @@ -24,9 +24,11 @@ CVE-2016-0775 -- Buffer overflow in FliDecode.c ----------------------------------------------- In all versions of Pillow, dating back at least to the last PIL 1.1.7 -release, FliDecode.c has a buffer overflow error. +release, FliDecode.c has a buffer overflow error (:cve:`CVE-2016-0775`). -Around line 192:: +Around line 192: + +.. code-block:: c case 16: /* COPY chunk */ @@ -45,13 +47,13 @@ is a set of row pointers to segments of memory that are the size of the row. At the max ``y``, this will write the contents of the line off the end of the memory buffer, causing a segfault. -This issue was found by Alyssa Besseling at Atlassian +This issue was found by Alyssa Besseling at Atlassian. CVE-2016-2533 -- Buffer overflow in PcdDecode.c ----------------------------------------------- In all versions of Pillow, dating back at least to the last PIL 1.1.7 -release, ``PcdDecode.c`` has a buffer overflow error. +release, ``PcdDecode.c`` has a buffer overflow error (:cve:`CVE-2016-2533`). The ``state.buffer`` for ``PcdDecode.c`` is allocated based on a 3 bytes per pixel sizing, where ``PcdDecode.c`` wrote into the buffer @@ -63,14 +65,16 @@ Integer overflow in Resample.c ------------------------------ If a large value was passed into the new size for an image, it is -possible to overflow an int32 value passed into malloc. +possible to overflow an ``int32`` value passed into malloc. + +.. code-block:: c - kk = malloc(xsize * kmax * sizeof(float)); - ... - xbounds = malloc(xsize * 2 * sizeof(int)); + kk = malloc(xsize * kmax * sizeof(float)); + ... + xbounds = malloc(xsize * 2 * sizeof(int)); ``xsize`` is trusted user input. These multiplications can overflow, -leading the malloc'd buffer to be undersized. These allocations are +leading the ``malloc``'d buffer to be undersized. These allocations are followed by a loop that writes out of bounds. This can lead to corruption on the heap of the Python process with attacker controlled float data. diff --git a/docs/releasenotes/3.1.2.rst b/docs/releasenotes/3.1.2.rst index ddb6a2adacf..b5f7cfe9963 100644 --- a/docs/releasenotes/3.1.2.rst +++ b/docs/releasenotes/3.1.2.rst @@ -7,9 +7,11 @@ CVE-2016-3076 -- Buffer overflow in Jpeg2KEncode.c Pillow between 2.5.0 and 3.1.1 may overflow a buffer when writing large Jpeg2000 files, allowing for code execution or other memory -corruption. +corruption (:cve:`CVE-2016-3076`). -This occurs specifically in the function ``j2k_encode_entry``, at the line:: +This occurs specifically in the function ``j2k_encode_entry``, at the line: + +.. code-block:: c state->buffer = malloc (tile_width * tile_height * components * prec / 8); diff --git a/docs/releasenotes/4.0.0.rst b/docs/releasenotes/4.0.0.rst index 4d21a2e54fe..cbf131c9311 100644 --- a/docs/releasenotes/4.0.0.rst +++ b/docs/releasenotes/4.0.0.rst @@ -23,17 +23,17 @@ redirected to the olefile package. Direct accesses to ``PIL.OlefileIO`` raises a deprecation warning, then patches the upstream olefile into ``sys.modules`` in its place. -SGI image save +SGI image save ============== It is now possible to save images in modes ``L``, ``RGB``, and -``RGBA`` to the uncompressed SGI image format. +``RGBA`` to the uncompressed SGI image format. Zero sized images ================= Pillow 3.4.0 removed support for creating images with (0,0) size. This -has been reenabled, restoring pre 3.4 behavior. +has been reenabled, restoring pre 3.4 behavior. Internal handles_eof flag ========================= @@ -41,11 +41,11 @@ Internal handles_eof flag The ``handles_eof flag`` for decoding images has been removed, as there were no internal users of the flag. Anyone maintaining image decoders outside of the Pillow source tree should consider using the cleanup -function pointers instead. +function pointers instead. Image.core.stretch removed ========================== The stretch function on the core image object has been removed. This used to be for enlarging the image, but has been aliased to resize -recently. +recently. diff --git a/docs/releasenotes/4.1.0.rst b/docs/releasenotes/4.1.0.rst index a6fb9d2afec..4d6598d8efa 100644 --- a/docs/releasenotes/4.1.0.rst +++ b/docs/releasenotes/4.1.0.rst @@ -10,9 +10,9 @@ Several deprecated items have been removed. resolution', 'resolution unit', and 'date time' has been removed. Underscores should be used instead. -* The methods :py:meth:`PIL.ImageDraw.ImageDraw.setink`, - :py:meth:`PIL.ImageDraw.ImageDraw.setfill`, and - :py:meth:`PIL.ImageDraw.ImageDraw.setfont` have been removed. +* The methods ``PIL.ImageDraw.ImageDraw.setink``, + ``PIL.ImageDraw.ImageDraw.setfill``, and + ``PIL.ImageDraw.ImageDraw.setfont`` have been removed. Closing Files When Opening Images @@ -27,7 +27,7 @@ is specified: responsibility of the calling code to close the file. * For images where Pillow opens the file and the file is known to have - only one frame, the file is closed after loading. + only one frame, the file is closed after loading. * If the file has more than one frame, or if it can't be determined, then the file is left open to permit seeking to subsequent @@ -36,7 +36,7 @@ is specified: * If the image is memory mapped, then we can't close the mapping to the underlying file until we are done with the image. The mapping - will be closed in the ``close`` or ``__del__`` method. + will be closed in the ``close`` or ``__del__`` method. Changes to GIF Handling When Saving @@ -50,7 +50,7 @@ saving images. There are two external changes that arise from this: * The image to be saved is no longer modified in place by any of the operations of the save function. Previously it was modified when - optimizing the image palette. + optimizing the image palette. This refactor fixed some bugs with palette handling when saving multiple frame GIFs. @@ -60,7 +60,7 @@ New Method: Image.remap_palette The method :py:meth:`PIL.Image.Image.remap_palette()` has been added. This method was hoisted from the GifImagePlugin code used to -optimize the palette. +optimize the palette. Added Decoder Registry and Support for Python Based Decoders ============================================================ @@ -76,7 +76,7 @@ Tests ===== Many tests have been added, including correctness tests for image -formats that have been previously untested. +formats that have been previously untested. We are now running automated tests in Docker containers against more Linux versions than are provided on Travis CI, which is currently diff --git a/docs/releasenotes/4.2.0.rst b/docs/releasenotes/4.2.0.rst index 1b41580a71c..1e9637f1e32 100644 --- a/docs/releasenotes/4.2.0.rst +++ b/docs/releasenotes/4.2.0.rst @@ -6,8 +6,8 @@ Added Complex Text Rendering Pillow now supports complex text rendering for scripts requiring glyph composition and bidirectional flow. This optional feature adds three -dependencies: harfbuzz, fribidi, and raqm. See the install -documentation for further details. This feature is tested and works on +dependencies: harfbuzz, fribidi, and raqm. See the :doc:`install documentation +<../installation>` for further details. This feature is tested and works on Unix and Mac, but has not yet been built on Windows platforms. New Optional Parameters @@ -26,16 +26,16 @@ New DecompressionBomb Warning :py:meth:`PIL.Image.Image.crop` now may raise a DecompressionBomb warning if the crop region enlarges the image over the threshold -specified by :py:attr:`PIL.Image.MAX_PIXELS`. +specified by :py:data:`PIL.Image.MAX_IMAGE_PIXELS`. Removed Deprecated Items ======================== Several deprecated items have been removed. -* The methods :py:meth:`PIL.ImageWin.Dib.fromstring`, - :py:meth:`PIL.ImageWin.Dib.tostring` and - :py:meth:`PIL.TiffImagePlugin.ImageFileDirectory_v2.as_dict` have +* The methods ``PIL.ImageWin.Dib.fromstring``, + ``PIL.ImageWin.Dib.tostring`` and + ``PIL.TiffImagePlugin.ImageFileDirectory_v2.as_dict`` have been removed. * Before Pillow 4.2.0, attempting to save an RGBA image as JPEG would diff --git a/docs/releasenotes/4.3.0.rst b/docs/releasenotes/4.3.0.rst index 64913592220..ea81fc45ea0 100644 --- a/docs/releasenotes/4.3.0.rst +++ b/docs/releasenotes/4.3.0.rst @@ -9,7 +9,7 @@ Deprecations Several undocumented functions in ImageOps have been deprecated: ``gaussian_blur``, ``gblur``, ``unsharp_mask``, ``usm`` and -``box_blur``. Use the equivalent operations in ImageFilter +``box_blur``. Use the equivalent operations in ``ImageFilter`` instead. These functions will be removed in a future release. TIFF Metadata Changes @@ -20,7 +20,7 @@ TIFF Metadata Changes single element tuple. This is only with the new api, not the legacy api. This normalizes the handling of fields, so that the metadata with inferred or image specified counts are handled the same as - metadata with count specified in the TIFF spec. + metadata with count specified in the TIFF spec. * The ``PhotoshopInfo``, ``XMP``, and ``JPEGTables`` tags now have a defined type (bytes) and a count of 1. * The ``ImageJMetaDataByteCounts`` tag now has an arbitrary number of @@ -85,7 +85,7 @@ There is a new :py:class:`PIL.ImageFilter.MultibandFilter` base class for image filters that can run on all channels of an image in one operation. The original :py:class:`PIL.ImageFilter.Filter` class remains for image filters that can process only single band images, or -require splitting of channels prior to filtering. +require splitting of channels prior to filtering. Other Changes ============= @@ -109,7 +109,7 @@ images to and from RGB and RGBA formats. The image data is truncated to 8-bit precision. Pillow can now read RLE encoded SGI images in both 8 and 16-bit -precision. +precision. Performance ^^^^^^^^^^^ @@ -124,7 +124,7 @@ This release contains several performance improvements: * ``Image.transpose`` has been accelerated 15% or more by using a cache friendly algorithm. * ImageFilters based on Kernel convolution are significantly faster - due to the new MultibandFilter feature. + due to the new :py:class:`~PIL.ImageFilter.MultibandFilter` feature. * All memory allocation for images is now done in blocks, rather than falling back to an allocation for each scan line for images larger than the block size. diff --git a/docs/releasenotes/5.0.0.rst b/docs/releasenotes/5.0.0.rst index 2bbeed11f99..509edbe6df8 100644 --- a/docs/releasenotes/5.0.0.rst +++ b/docs/releasenotes/5.0.0.rst @@ -17,7 +17,7 @@ Decompression Bombs now raise Exceptions Pillow has previously emitted warnings for images that are unexpectedly large and may be a denial of service. These warnings are -now upgraded to ``DecompressionBombError``s for images that are twice +now upgraded to ``DecompressionBombError``\s for images that are twice the size of images that trigger the ``DecompressionBombWarning``. The default threshold is 128Mpx, or 0.5GB for an ``RGB`` or ``RGBA`` image. This can be disabled or changed by setting @@ -69,7 +69,7 @@ GIF Disposal ^^^^^^^^^^^^ Multiframe GIF images now take an optional disposal parameter to -specify the disposal option for changed pixels. +specify the disposal option for changed pixels. Other Changes ============= @@ -88,7 +88,7 @@ Libraqm is now Dynamically Linked The libraqm dependency for complex text scripts is now linked dynamically at runtime rather than at packaging time. This allows us to release binaries with support for libraqm if it is installed on the -user's machine. +user's machine. Source Layout Changes ^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/releasenotes/5.1.0.rst b/docs/releasenotes/5.1.0.rst new file mode 100644 index 00000000000..2a4c64ac52e --- /dev/null +++ b/docs/releasenotes/5.1.0.rst @@ -0,0 +1,36 @@ +5.1.0 +----- + +New File Format +=============== + +BLP File Format +^^^^^^^^^^^^^^^ + +Pillow now supports reading the BLP "Blizzard Mipmap" file format used +for tiles in Blizzard's engine. + +API Changes +=========== + +Optional channels for TIFF files +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow can now open TIFF files with base modes of ``RGB``, ``YCbCr``, +and ``CMYK`` with up to 6 8-bit channels, discarding any extra +channels if the content is tagged as UNSPECIFIED. Pillow still does +not store more than 4 8-bit channels of image data. + +Append to PDF Files +^^^^^^^^^^^^^^^^^^^ + +Images can now be appended to PDF files in place by passing in +``append=True`` when saving the image. + +Other Changes +============= + +WebP memory leak +^^^^^^^^^^^^^^^^ + +A memory leak when opening ``WebP`` files has been fixed. diff --git a/docs/releasenotes/5.2.0.rst b/docs/releasenotes/5.2.0.rst new file mode 100644 index 00000000000..d9b8f0fb7c8 --- /dev/null +++ b/docs/releasenotes/5.2.0.rst @@ -0,0 +1,113 @@ +5.2.0 +----- + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +These version constants have been deprecated. ``VERSION`` will be removed in +Pillow 6.0.0, and ``PILLOW_VERSION`` will be removed after that. + +* ``PIL.VERSION`` (old PIL version 1.1.7) +* ``PIL.PILLOW_VERSION`` +* ``PIL.Image.VERSION`` +* ``PIL.Image.PILLOW_VERSION`` + +Use ``PIL.__version__`` instead. + +API Additions +============= + +3D color lookup tables +^^^^^^^^^^^^^^^^^^^^^^ + +Support for 3D color lookup table transformations has been added. + +* https://en.wikipedia.org/wiki/3D_lookup_table + +``Color3DLUT.generate`` transforms 3-channel pixels using the values of the +channels as coordinates in the 3D lookup table and interpolating the nearest +elements. + +It allows you to apply almost any color transformation in constant time by +using pre-calculated decimated tables. + +``Color3DLUT.transform()`` allows altering table values with a callback. + +If NumPy is installed, the performance of argument conversion is dramatically +improved when a source table supports buffer interface (NumPy && arrays in +Python >= 3). + +ImageColor.getrgb +^^^^^^^^^^^^^^^^^ + +Previously ``Image.rotate`` only supported HSL color strings. Now HSB and HSV +strings are also supported, as well as float values. For example, +``ImageColor.getrgb("hsv(180,100%,99.5%)")``. + +ImageFile.get_format_mimetype +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``ImageFile.get_format_mimetype`` has been added to return the MIME type of an +image file, where available. For example, +``Image.open("hopper.jpg").get_format_mimetype()`` returns ``"image/jpeg"``. + +ImageFont.getsize_multiline +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A new method to return the size of multiline text, for example +``font.getsize_multiline("ABC\nAaaa")`` + +Image.rotate +^^^^^^^^^^^^ + +A new named parameter, ``fillcolor``, has been added to ``Image.rotate``. This +color specifies the background color to use in the area outside the rotated +image. This parameter takes the same color specifications as used in +``Image.new``. + + +TGA file format +^^^^^^^^^^^^^^^ + +Pillow can now read and write LA data (in addition to L, P, RGB and RGBA), and +write RLE data (in addition to uncompressed). + +Other Changes +============= + +Support added for Python 3.7 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 5.2 supports Python 3.7. + +Build macOS wheels with Xcode 6.4, supporting older macOS versions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The macOS wheels for Pillow 5.1.0 were built with Xcode 9.2, meaning 10.12 +Sierra was the lowest supported version. + +Prior to Pillow 5.1.0, Xcode 8 was used, supporting El Capitan 10.11. + +Instead, Pillow 5.2.0 is built with the oldest available Xcode 6.4 to support +at least 10.10 Yosemite. + +Fix _i2f compilation with some GCC versions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For example, this allows compilation with GCC 4.8 on NetBSD. + +Resolve confusion getting PIL / Pillow version string +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Re: "version constants deprecated" listed above, as user gnbl notes in #3082: + +- it's confusing that PIL.VERSION returns the version string of the former PIL instead of Pillow's +- ReadTheDocs documentation is missing for some version branches (why is this, will it ever change, ...) +- it's confusing that PIL.version is a module and does not return the version information directly or hints on how to get it +- the package information header is essentially useless (placeholder, does not even mention Pillow, nor the version) +- PIL._version module documentation comment could explain how to access the version information + +We have attempted to resolve these issues in #3083, #3090 and #3218. diff --git a/docs/releasenotes/5.3.0.rst b/docs/releasenotes/5.3.0.rst new file mode 100644 index 00000000000..bff56566b66 --- /dev/null +++ b/docs/releasenotes/5.3.0.rst @@ -0,0 +1,67 @@ +5.3.0 +----- + +API Changes +=========== + +Image size +^^^^^^^^^^ + +If you attempt to set the size of an image directly, e.g. +``im.size = (100, 100)``, you will now receive an ``AttributeError``. This is +not about removing existing functionality, but instead about raising an +explicit error to prevent later consequences. The ``resize`` method is the +correct way to change an image's size. + +The exceptions to this are: + +* The ICO and ICNS image formats, which use ``im.size = (100, 100)`` to select a subimage. +* The TIFF image format, which now has a ``DeprecationWarning`` for this action, as direct image size setting was previously necessary to work around an issue with tile extents. + + +API Additions +============= + +Added line width parameter to rectangle and ellipse-based shapes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An optional line ``width`` parameter has been added to ``ImageDraw.Draw.arc``, +``chord``, ``ellipse``, ``pieslice`` and ``rectangle``. + +Curved joints for line sequences +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``ImageDraw.Draw.line`` draws a line, or lines, between points. Previously, +when multiple points are given, for a larger ``width``, the joints between +these lines looked unsightly. There is now an additional optional argument, +``joint``, defaulting to :data:`None`. When it is set to ``curved``, the joints +between the lines will become rounded. + +ImageOps.colorize +^^^^^^^^^^^^^^^^^ + +Previously ``ImageOps.colorize`` only supported two-color mapping with +``black`` and ``white`` arguments being mapped to 0 and 255 respectively. +Now it supports three-color mapping with the optional ``mid`` parameter, and +the positions for all three color arguments can each be optionally specified +(``blackpoint``, ``whitepoint`` and ``midpoint``). +For example, with all optional arguments:: + + ImageOps.colorize(im, black=(32, 37, 79), white='white', mid=(59, 101, 175), + blackpoint=15, whitepoint=240, midpoint=100) + +ImageOps.pad +^^^^^^^^^^^^ + +While ``ImageOps.fit`` allows users to crop images to a requested aspect ratio +and size, new method ``ImageOps.pad`` pads images to fill a requested aspect +ratio and size, filling new space with a provided ``color`` and positioning the +image within the new area through a ``centering`` argument. + +Other Changes +============= + +Added support for reading tiled TIFF images through LibTIFF. Compressed TIFF +images are now read through LibTIFF. + +RGB WebP images are now read as RGB mode, rather than RGBX. diff --git a/docs/releasenotes/5.4.0.rst b/docs/releasenotes/5.4.0.rst new file mode 100644 index 00000000000..6d7277c70ea --- /dev/null +++ b/docs/releasenotes/5.4.0.rst @@ -0,0 +1,67 @@ +5.4.0 +----- + +API Changes +=========== + +APNG extension to PNG plugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Animated Portable Network Graphics (APNG) images are not fully supported but +can be opened via the PNG plugin to get some basic info:: + + im = Image.open("image.apng") + print(im.mode) # "RGBA" + print(im.size) # (245, 245) + im.show() # Shows a single frame + +Check for libjpeg-turbo +^^^^^^^^^^^^^^^^^^^^^^^ + +You can check if Pillow has been built against the libjpeg-turbo version of the +libjpeg library:: + + from PIL import features + features.check_feature("libjpeg_turbo") # True or False + +Negative indexes in pixel access +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When accessing individual image pixels, negative indexes are now also accepted. +For example, to get or set the farthest pixel in the lower right of an image:: + + px = im.load() + print(px[-1, -1]) + px[-1, -1] = (0, 0, 0) + + +New custom TIFF tags +^^^^^^^^^^^^^^^^^^^^ + +TIFF images can now be saved with custom integer, float and string TIFF tags:: + + im = Image.new("RGB", (200, 100)) + custom = { + 37000: 4, + 37001: 4.2, + 37002: "custom tag value", + 37003: u"custom tag value", + 37004: b"custom tag value", + } + im.save("output.tif", tiffinfo=custom) + + im2 = Image.open("output.tif") + print(im2.tag_v2[37000]) # 4 + print(im2.tag_v2[37002]) # "custom tag value" + print(im2.tag_v2[37004]) # b"custom tag value" + +Other Changes +============= + +ImageOps.fit +^^^^^^^^^^^^ + +Now uses one resize operation with ``box`` parameter internally +instead of a crop and scale operations sequence. +This improves the performance and accuracy of cropping since +the ``box`` parameter accepts float values. diff --git a/docs/releasenotes/5.4.1.rst b/docs/releasenotes/5.4.1.rst new file mode 100644 index 00000000000..78f483db658 --- /dev/null +++ b/docs/releasenotes/5.4.1.rst @@ -0,0 +1,36 @@ +5.4.1 +----- + +This release fixes regressions in 5.4.0. + +Installation on Termux +^^^^^^^^^^^^^^^^^^^^^^ + +A change to the way Pillow detects libraries during installed prevented +installation on Termux, which does not have ``/sbin/ldconfig``. This is now +fixed. + +PNG: Handle IDAT chunks after image end +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Some PNG images have multiple IDAT chunks. In some cases, Pillow will stop +reading image data before the IDAT chunks finish. A regression caused an +``EOFError`` exception when previously there was none. This is now fixed, and +file reading continues in case there are subsequent text chunks. + +PNG: MIME type +^^^^^^^^^^^^^^ + +The addition of limited APNG support to the PNG plugin also overwrote the MIME +type for PNG files, causing "image/apng" to be returned as the MIME type of +both APNG and PNG files. This has been fixed so the MIME type of PNG files is +"image/png". + +File closing +^^^^^^^^^^^^ + +A regression caused an unsupported image file to report a +``ValueError: seek of closed file`` exception instead of an ``OSError``. This +has been fixed by ensuring that image plugins only close their internal ``__fp`` +if they are not the same as ``ImageFile``'s ``fp``, allowing each to manage their own +file pointers. diff --git a/docs/releasenotes/6.0.0.rst b/docs/releasenotes/6.0.0.rst new file mode 100644 index 00000000000..3e3b945a0a9 --- /dev/null +++ b/docs/releasenotes/6.0.0.rst @@ -0,0 +1,212 @@ +6.0.0 +----- + +Backwards Incompatible Changes +============================== + +Python 3.4 dropped +^^^^^^^^^^^^^^^^^^ + +Python 3.4 is EOL since 2019-03-16 and no longer supported. We will not be creating +binaries, testing, or retaining compatibility with this version. The final version of +Pillow for Python 3.4 is 5.4.1. + +Removed deprecated PIL.OleFileIO +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +PIL.OleFileIO was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of +the upstream olefile Python package, and replaced with an ``ImportError``. The +deprecated file has now been removed from Pillow. If needed, install from PyPI (eg. +``python3 -m pip install olefile``). + +Removed deprecated ImageOps functions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Several undocumented functions in ``ImageOps`` were deprecated in Pillow 4.3.0 (2017-10) +and have now been removed: ``gaussian_blur``, ``gblur``, ``unsharp_mask``, ``usm`` and +``box_blur``. Use the equivalent operations in ``ImageFilter`` instead. + +Removed deprecated VERSION +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``VERSION`` (the old PIL version, always 1.1.7) has been removed. Use ``__version__`` +instead. + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +Python 2.7 +~~~~~~~~~~ + +Python 2.7 reaches end-of-life on 2020-01-01. + +Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python 2.7, making +Pillow 6.x the last series to support Python 2. + +PyQt4 and PySide +~~~~~~~~~~~~~~~~ + +Qt 4 reached end-of-life on 2015-12-19. Its Python bindings are also EOL: PyQt4 since +2018-08-31 and PySide since 2015-10-14. + +Support for PyQt4 and PySide has been deprecated from ``ImageQt`` and will be removed in +a future version. Please upgrade to PyQt5 or PySide2. + +PIL.*ImagePlugin.__version__ attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These version constants have been deprecated and will be removed in a future +version. + +* ``BmpImagePlugin.__version__`` +* ``CurImagePlugin.__version__`` +* ``DcxImagePlugin.__version__`` +* ``EpsImagePlugin.__version__`` +* ``FliImagePlugin.__version__`` +* ``FpxImagePlugin.__version__`` +* ``GdImageFile.__version__`` +* ``GifImagePlugin.__version__`` +* ``IcoImagePlugin.__version__`` +* ``ImImagePlugin.__version__`` +* ``ImtImagePlugin.__version__`` +* ``IptcImagePlugin.__version__`` +* ``Jpeg2KImagePlugin.__version__`` +* ``JpegImagePlugin.__version__`` +* ``McIdasImagePlugin.__version__`` +* ``MicImagePlugin.__version__`` +* ``MpegImagePlugin.__version__`` +* ``MpoImagePlugin.__version__`` +* ``MspImagePlugin.__version__`` +* ``PalmImagePlugin.__version__`` +* ``PcdImagePlugin.__version__`` +* ``PcxImagePlugin.__version__`` +* ``PdfImagePlugin.__version__`` +* ``PixarImagePlugin.__version__`` +* ``PngImagePlugin.__version__`` +* ``PpmImagePlugin.__version__`` +* ``PsdImagePlugin.__version__`` +* ``SgiImagePlugin.__version__`` +* ``SunImagePlugin.__version__`` +* ``TgaImagePlugin.__version__`` +* ``TiffImagePlugin.__version__`` +* ``WmfImagePlugin.__version__`` +* ``XbmImagePlugin.__version__`` +* ``XpmImagePlugin.__version__`` +* ``XVThumbImagePlugin.__version__`` + +Use ``PIL.__version__`` instead. + +ImageCms.CmsProfile attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some attributes in ``ImageCms.CmsProfile`` have been deprecated since Pillow 3.2.0. From +6.0.0, they issue a ``DeprecationWarning``: + +======================== =============================== +Deprecated Use instead +======================== =============================== +``color_space`` Padded ``xcolor_space`` +``pcs`` Padded ``connection_space`` +``product_copyright`` Unicode ``copyright`` +``product_desc`` Unicode ``profile_description`` +``product_description`` Unicode ``profile_description`` +``product_manufacturer`` Unicode ``manufacturer`` +``product_model`` Unicode ``model`` +======================== =============================== + +MIME type improvements +^^^^^^^^^^^^^^^^^^^^^^ + +Previously, all JPEG2000 images had the MIME type "image/jpx". This has now been +corrected. After the file format drivers have been loaded, ``Image.MIME["JPEG2000"]`` +will return "image/jp2". ``ImageFile.get_format_mimetype`` will return "image/jpx" if +a JPX profile is present, or "image/jp2" otherwise. + +Previously, all SGI images had the MIME type "image/rgb". This has now been +corrected. After the file format drivers have been loaded, ``Image.MIME["SGI"]`` +will return "image/sgi". ``ImageFile.get_format_mimetype`` will return "image/rgb" if +RGB image data is present, or "image/sgi" otherwise. + +MIME types have been added to the PPM format. After the file format drivers have been +loaded, ``Image.MIME["PPM"]`` will now return the generic "image/x-portable-anymap". +``ImageFile.get_format_mimetype`` will return a MIME type specific to the color type. + +The TGA, PCX and ICO formats also now have MIME types: "image/x-tga", "image/x-pcx" and +"image/x-icon" respectively. + +API Additions +============= + +DIB file format +^^^^^^^^^^^^^^^ + +Pillow now supports reading and writing the Device Independent Bitmap file format. + +Image.quantize +^^^^^^^^^^^^^^ + +The ``dither`` option is now a customisable parameter (was previously hardcoded to ``1``). +This parameter takes the same values used in :py:meth:`~PIL.Image.Image.convert`. + +New language parameter +^^^^^^^^^^^^^^^^^^^^^^ + +These text-rendering functions now accept a ``language`` parameter to request +language-specific glyphs and ligatures from the font: + +* ``ImageDraw.ImageDraw.multiline_text()`` +* ``ImageDraw.ImageDraw.multiline_textsize()`` +* ``ImageDraw.ImageDraw.text()`` +* ``ImageDraw.ImageDraw.textsize()`` +* ``ImageFont.ImageFont.getmask()`` +* ``ImageFont.ImageFont.getsize_multiline()`` +* ``ImageFont.ImageFont.getsize()`` + +Added EXIF class +^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.Image.getexif` has been added, which returns an +:py:class:`~PIL.Image.Exif` instance. Values can be retrieved and set like a +dictionary. When saving JPEG, PNG or WEBP, the instance can be passed as an +``exif`` argument to include any changes in the output image. + +Added ImageOps.exif_transpose +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.ImageOps.exif_transpose` returns a copy of an image, transposed +according to its EXIF Orientation tag. + +PNG EXIF data +^^^^^^^^^^^^^ + +EXIF data can now be read from and saved to PNG images. However, unlike other image +formats, EXIF data is not guaranteed to be present in :py:attr:`~PIL.Image.Image.info` +until :py:meth:`~PIL.Image.Image.load` has been called. + +Other Changes +============= + +Reading new DDS image format +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow can now read uncompressed RGB data from DDS images. + +Reading TIFF with old-style JPEG compression +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Added support reading TIFF files with old-style JPEG compression through LibTIFF. All +YCbCr TIFF images are now always read as RGB. + +TIFF compression codecs +^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for the LZMA, Zstd and WebP TIFF compression codecs. + +Improved support for transposing I;16 images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +I;16, I;16L and I;16B are now supported image modes for all +:py:meth:`~PIL.Image.Image.transpose` operations. diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst new file mode 100644 index 00000000000..eb4304843e1 --- /dev/null +++ b/docs/releasenotes/6.1.0.rst @@ -0,0 +1,111 @@ +6.1.0 +----- + +Deprecations +============ + +Image.__del__ +^^^^^^^^^^^^^ + +.. deprecated:: 6.1.0 + +Implicitly closing the image's underlying file in ``Image.__del__`` has been deprecated. +Use a context manager or call ``Image.close()`` instead to close the file in a +deterministic way. + +Deprecated: + +.. code-block:: python + + im = Image.open("hopper.png") + im.save("out.jpg") + +Use instead: + +.. code-block:: python + + with Image.open("hopper.png") as im: + im.save("out.jpg") + +API Additions +============= + +Image.entropy +^^^^^^^^^^^^^ +Calculates and returns the entropy for the image. A bilevel image (mode "1") is treated +as a greyscale ("L") image by this method. If a mask is provided, the method employs +the histogram for those parts of the image where the mask image is non-zero. The mask +image must have the same size as the image, and be either a bi-level image (mode "1") or +a greyscale image ("L"). + +ImageGrab.grab +^^^^^^^^^^^^^^ + +An optional ``include_layered_windows`` parameter has been added to ``ImageGrab.grab``, +defaulting to ``False``. If true, layered windows will be included in the resulting +image on Windows. + +ImageSequence.all_frames +^^^^^^^^^^^^^^^^^^^^^^^^ + +A new method to facilitate applying a given function to all frames in an image, or to +all frames in a list of images. The frames are returned as a list of separate images. +For example, ``ImageSequence.all_frames(im, lambda im_frame: im_frame.rotate(90))`` +could be used to return all frames from an image, each rotated 90 degrees. + +Variation fonts +^^^^^^^^^^^^^^^ + +Variation fonts are now supported, allowing for different styles from the same font +file. ``ImageFont.FreeTypeFont`` has four new methods, +:py:meth:`PIL.ImageFont.FreeTypeFont.get_variation_names` and +:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_name` for using named styles, and +:py:meth:`PIL.ImageFont.FreeTypeFont.get_variation_axes` and +:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_axes` for using font axes +instead. An ``IOError`` will be raised if the font is not a variation font. FreeType +2.9.1 or greater is required. + +Other Changes +============= + +ImageTk.getimage +^^^^^^^^^^^^^^^^ + +This function is now supported. It returns the contents of an ``ImageTk.PhotoImage`` as +an RGBA ``Image.Image`` instance. + +Image quality for JPEG compressed TIFF +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The TIFF encoder accepts a ``quality`` parameter for ``jpeg`` compressed TIFF files. A +value from 0 (worst) to 100 (best) controls the image quality, similar to the JPEG +encoder. The default is 75. For example: + +.. code-block:: python + + im.save("out.tif", compression="jpeg", quality=85) + +Improve encoding of TIFF tags +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The TIFF encoder supports more types, especially arrays. This is required for the +GeoTIFF format which encodes geospatial information. + +* Pass ``tagtype`` from v2 directory to libtiff encoder, instead of autodetecting type. +* Use explicit types eg. ``uint32_t`` for ``TIFF_LONG`` to fix issues on platforms with + 64-bit longs. +* Add support for multiple values (arrays). Requires type in v2 directory and values + must be passed as a tuple. +* Add support for signed types eg. ``TIFFTypes.TIFF_SIGNED_SHORT``. + +Respect PKG_CONFIG environment variable when building +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This variable is commonly used by other build systems and using it can help with +cross-compiling. Falls back to ``pkg-config`` as before. + +Top-to-bottom complex text rendering +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Drawing text in the 'ttb' direction with ``ImageFont`` has been significantly improved +and requires Raqm 0.7 or greater. diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst new file mode 100644 index 00000000000..20a009cc177 --- /dev/null +++ b/docs/releasenotes/6.2.0.rst @@ -0,0 +1,109 @@ +6.2.0 +----- + +API Additions +============= + +Text stroking +^^^^^^^^^^^^^ + +``stroke_width`` and ``stroke_fill`` arguments have been added to text drawing +operations. They allow text to be outlined, setting the width of the stroke and +and the color respectively. If not provided, ``stroke_fill`` will default to +the ``fill`` parameter. + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 40) + font.getsize_multiline("A", stroke_width=2) + font.getsize("ABC\nAaaa", stroke_width=2) + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + draw.textsize("A", font, stroke_width=2) + draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2) + draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill="#0f0") + draw.multiline_text((10, 10), "A\nB", "#f00", font, + stroke_width=2, stroke_fill="#0f0") + +For example, + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill="#0f0") + + +creates the following image: + +.. image:: ../../Tests/images/imagedraw_stroke_different.png + +ImageGrab on multi-monitor Windows +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An ``all_screens`` argument has been added to ``ImageGrab.grab``. If ``True``, +all monitors will be included in the created image. + +API Changes +=========== + +Image.getexif +^^^^^^^^^^^^^ + +To allow for lazy loading of Exif data, ``Image.getexif()`` now returns a +shared instance of ``Image.Exif``. + +Deprecations +^^^^^^^^^^^^ + +Image.frombuffer +~~~~~~~~~~~~~~~~ + +There has been a longstanding warning that the defaults of ``Image.frombuffer`` +may change in the future for the "raw" decoder. The change will now take place +in Pillow 7.0. + +Security +======== + +This release catches several buffer overruns, as well as addressing +:cve:`CVE-2019-16865`. The CVE is regarding DOS problems, such as consuming large +amounts of memory, or taking a large amount of time to process an image. + +In RawDecode.c, an error is now thrown if skip is calculated to be less than +zero. It is intended to skip padding between lines, not to go backwards. + +In PsdImagePlugin, if the combined sizes of the individual parts is larger than +the declared size of the extra data field, then it looked for the next layer by +seeking backwards. This is now corrected by seeking to (the start of the layer ++ the size of the extra data field) instead of (the read parts of the layer + +the rest of the layer). + +Decompression bomb checks have been added to GIF and ICO formats. + +An error is now raised if a TIFF dimension is a string, rather than trying to +perform operations on it. + +Other Changes +============= + +Removed bdist_wininst .exe installers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.exe installers fell out of favour with :pep:`527`, and will be deprecated in +Python 3.8. Pillow will no longer be distributing them. Wheels should be used +instead. + +Flags for libwebp in wheels +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When building libwebp for inclusion in wheels, Pillow now adds the ``-O3`` and +``-DNDEBUG`` CFLAGS. These flags would be used by default if building libwebp +without debugging, and using them fixes a significant decrease in speed when +a wheel-installed copy of Pillow performs libwebp operations. diff --git a/docs/releasenotes/6.2.1.rst b/docs/releasenotes/6.2.1.rst new file mode 100644 index 00000000000..ca298fa702c --- /dev/null +++ b/docs/releasenotes/6.2.1.rst @@ -0,0 +1,26 @@ +6.2.1 +----- + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +Python 2.7 +~~~~~~~~~~ + +Python 2.7 reaches end-of-life on 2020-01-01. + +Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python +2.7, making Pillow 6.2.x the last release series to support Python 2. + +Other Changes +============= + + + +Support added for Python 3.8 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 6.2.1 supports Python 3.8. diff --git a/docs/releasenotes/6.2.2.rst b/docs/releasenotes/6.2.2.rst new file mode 100644 index 00000000000..79d4b88aac4 --- /dev/null +++ b/docs/releasenotes/6.2.2.rst @@ -0,0 +1,18 @@ +6.2.2 +----- + +Security +======== + +This release addresses several security problems. + +:cve:`CVE-2019-19911` is regarding FPX images. If an image reports that it has a large +number of bands, a large amount of resources will be used when trying to process the +image. This is fixed by limiting the number of bands to those usable by Pillow. + +Buffer overruns were found when processing an SGI (:cve:`CVE-2020-5311`), +PCX (:cve:`CVE-2020-5312`) or FLI image (:cve:`CVE-2020-5313`). Checks have been added +to prevent this. + +:cve:`CVE-2020-5310`: Overflow checks have been added when calculating the size of a +memory block to be reallocated in the processing of a TIFF image. diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst new file mode 100644 index 00000000000..80002b0ce71 --- /dev/null +++ b/docs/releasenotes/7.0.0.rst @@ -0,0 +1,161 @@ +7.0.0 +----- + +Backwards Incompatible Changes +============================== + +Python 2.7 +^^^^^^^^^^ + +Pillow has dropped support for Python 2.7, which reached end-of-life on 2020-01-01. + +PILLOW_VERSION constant +^^^^^^^^^^^^^^^^^^^^^^^ + +``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. + +PIL.*ImagePlugin.__version__ attributes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The version constants of individual plugins have been removed. Use ``PIL.__version__`` +instead. + +=============================== ================================= ================================== +Removed Removed Removed +=============================== ================================= ================================== +``BmpImagePlugin.__version__`` ``Jpeg2KImagePlugin.__version__`` ``PngImagePlugin.__version__`` +``CurImagePlugin.__version__`` ``JpegImagePlugin.__version__`` ``PpmImagePlugin.__version__`` +``DcxImagePlugin.__version__`` ``McIdasImagePlugin.__version__`` ``PsdImagePlugin.__version__`` +``EpsImagePlugin.__version__`` ``MicImagePlugin.__version__`` ``SgiImagePlugin.__version__`` +``FliImagePlugin.__version__`` ``MpegImagePlugin.__version__`` ``SunImagePlugin.__version__`` +``FpxImagePlugin.__version__`` ``MpoImagePlugin.__version__`` ``TgaImagePlugin.__version__`` +``GdImageFile.__version__`` ``MspImagePlugin.__version__`` ``TiffImagePlugin.__version__`` +``GifImagePlugin.__version__`` ``PalmImagePlugin.__version__`` ``WmfImagePlugin.__version__`` +``IcoImagePlugin.__version__`` ``PcdImagePlugin.__version__`` ``XbmImagePlugin.__version__`` +``ImImagePlugin.__version__`` ``PcxImagePlugin.__version__`` ``XpmImagePlugin.__version__`` +``ImtImagePlugin.__version__`` ``PdfImagePlugin.__version__`` ``XVThumbImagePlugin.__version__`` +``IptcImagePlugin.__version__`` ``PixarImagePlugin.__version__`` +=============================== ================================= ================================== + +PyQt4 and PySide +^^^^^^^^^^^^^^^^ + +Qt 4 reached end-of-life on 2015-12-19. Its Python bindings are also EOL: PyQt4 since +2018-08-31 and PySide since 2015-10-14. + +Support for PyQt4 and PySide has been removed from ``ImageQt``. Please upgrade to PyQt5 +or PySide2. + +Setting the size of TIFF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Setting the size of a TIFF image directly (eg. ``im.size = (256, 256)``) throws +an error. Use ``Image.resize`` instead. + +Default resampling filter +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The default resampling filter has been changed to the high-quality convolution +``Image.BICUBIC`` instead of ``Image.NEAREST``, for the :py:meth:`~PIL.Image.Image.resize` +method and the :py:meth:`~PIL.ImageOps.pad`, :py:meth:`~PIL.ImageOps.scale` +and :py:meth:`~PIL.ImageOps.fit` functions. +``Image.NEAREST`` is still always used for images in "P" and "1" modes. +See :ref:`concept-filters` to learn the difference. In short, +``Image.NEAREST`` is a very fast filter, but simple and low-quality. + +Image.draft() return value +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If the :py:meth:`~PIL.Image.Image.draft` method has no effect, it returns :data:`None`. +If it does have an effect, then it previously returned the image itself. +However, unlike other `chain methods`_, :py:meth:`~PIL.Image.Image.draft` does not +return a modified version of the image, but modifies it in-place. So instead, if +:py:meth:`~PIL.Image.Image.draft` has an effect, Pillow will now return a tuple +of the image mode and a co-ordinate box. The box is the original coordinates in the +bounds of resulting image. This may be useful in a subsequent +:py:meth:`~PIL.Image.Image.resize` call. + +.. _chain methods: https://en.wikipedia.org/wiki/Method_chaining + + +API Additions +============= + +Custom unidentified image error +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow will now throw a custom ``UnidentifiedImageError`` when an image cannot be +identified. For backwards compatibility, this will inherit from ``OSError``. + +New argument ``reducing_gap`` for Image.resize() and Image.thumbnail() methods +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Speeds up resizing by resizing the image in two steps. The bigger ``reducing_gap``, +the closer the result to the fair resampling. The smaller ``reducing_gap``, +the faster resizing. With ``reducing_gap`` greater or equal to 3.0, +the result is indistinguishable from fair resampling. + +The default value for :py:meth:`~PIL.Image.Image.resize` is :data:`None`, +which means that the optimization is turned off by default. + +The default value for :py:meth:`~PIL.Image.Image.thumbnail` is 2.0, +which is very close to fair resampling while still being faster in many cases. +In addition, the same gap is applied when :py:meth:`~PIL.Image.Image.thumbnail` +calls :py:meth:`~PIL.Image.Image.draft`, which may greatly improve the quality +of JPEG thumbnails. As a result, :py:meth:`~PIL.Image.Image.thumbnail` +in the new version provides equally high speed and high quality from any +source (JPEG or arbitrary images). + +New Image.reduce() method +^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.Image.reduce` is a highly efficient operation +to reduce an image by integer times. Normally, it shouldn't be used directly. +Used internally by :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail` +methods to speed up resize when a new argument ``reducing_gap`` is set. + +Loading WMF images at a given DPI +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +On Windows, Pillow can read WMF files, with a default DPI of 72. An image can +now also be loaded at another resolution: + +.. code-block:: python + + from PIL import Image + with Image.open("drawing.wmf") as im: + im.load(dpi=144) + +Other Changes +============= + +Image.__del__ +^^^^^^^^^^^^^ + +Implicitly closing the image's underlying file in ``Image.__del__`` has been removed. +Use a context manager or call :py:meth:`~PIL.Image.Image.close` instead to close +the file in a deterministic way. + +Previous method: + +.. code-block:: python + + im = Image.open("hopper.png") + im.save("out.jpg") + +Use instead: + +.. code-block:: python + + with Image.open("hopper.png") as im: + im.save("out.jpg") + +Better thumbnail geometry +^^^^^^^^^^^^^^^^^^^^^^^^^ + +When calculating the new dimensions in :py:meth:`~PIL.Image.Image.thumbnail`, +round to the nearest integer, instead of always rounding down. +This better preserves the original aspect ratio. + +When the image width or height is not divisible by 8 the last row and column +in the image get the correct weight after JPEG DCT scaling. diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst new file mode 100644 index 00000000000..0024a537d12 --- /dev/null +++ b/docs/releasenotes/7.1.0.rst @@ -0,0 +1,102 @@ +7.1.0 +----- + +API Changes +=========== + +Allow saving of zero quality JPEG images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If no quality was specified when saving a JPEG, Pillow internally used a value +of zero to indicate that the default quality should be used. However, this +removed the ability to actually save a JPEG with zero quality. This has now +been resolved. + +.. code-block:: python + + from PIL import Image + im = Image.open("hopper.jpg") + im.save("out.jpg", quality=0) + +API Additions +============= + +New channel operations +^^^^^^^^^^^^^^^^^^^^^^ + +Three new channel operations have been added: :py:meth:`~PIL.ImageChops.soft_light`, +:py:meth:`~PIL.ImageChops.hard_light` and :py:meth:`~PIL.ImageChops.overlay`. + +PILLOW_VERSION constant +^^^^^^^^^^^^^^^^^^^^^^^ + +``PILLOW_VERSION`` has been re-added but is deprecated and will be removed in a future +release. Use ``__version__`` instead. + +It was initially removed in Pillow 7.0.0, but brought back in 7.1.0 to give projects +more time to upgrade. + +Reading JPEG comments +^^^^^^^^^^^^^^^^^^^^^ + +When opening a JPEG image, the comment may now be read into +:py:attr:`~PIL.Image.Image.info`. + +Support for different charset encodings in PcfFontFile +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously ``PcfFontFile`` output only bitmap PIL fonts with ISO 8859-1 encoding, even +though the PCF format supports Unicode, making it hard to work with Pillow with bitmap +fonts in languages which use different character sets. + +Now it's possible to set a different charset encoding in ``PcfFontFile``'s class +constructor. By default, it generates a PIL font file with ISO 8859-1 as before. The +generated PIL font file still contains up to 256 characters, but the character set is +different depending on the selected encoding. + +To use such a font with ``ImageDraw.text``, call it with a bytes object with the same +encoding as the font file. + +X11 ImageGrab.grab() +^^^^^^^^^^^^^^^^^^^^ +Support has been added for ``ImageGrab.grab()`` on Linux using the X server +with the XCB library. + +An optional ``xdisplay`` parameter has been added to select the X server, +with the default value of :data:`None` using the default X server. + +Passing a different value on Windows or macOS will force taking a snapshot +using the selected X server; pass an empty string to use the default X server. +XCB support is not included in pre-compiled wheels for Windows and macOS. + +Security +======== + +This release includes security fixes. + +* :cve:`CVE-2020-10177` Fix multiple out-of-bounds reads in FLI decoding +* :cve:`CVE-2020-10378` Fix bounds overflow in PCX decoding +* :cve:`CVE-2020-10379` Fix two buffer overflows in TIFF decoding +* :cve:`CVE-2020-10994` Fix bounds overflow in JPEG 2000 decoding +* :cve:`CVE-2020-11538` Fix buffer overflow in SGI-RLE decoding + +Other Changes +============= + +If present, only use alpha channel for bounding box +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When the :py:meth:`~PIL.Image.Image.getbbox` method calculates the bounding +box, for an RGB image it trims black pixels. Similarly, for an RGBA image it +would trim black transparent pixels. This is now changed so that if an image +has an alpha channel (RGBA, RGBa, PA, LA, La), any transparent pixels are +trimmed. + +Improved APNG support +^^^^^^^^^^^^^^^^^^^^^ + +Added support for reading and writing Animated Portable Network Graphics (APNG) images. +The PNG plugin now supports using the :py:meth:`~PIL.Image.Image.seek` method and the +:py:class:`~PIL.ImageSequence.Iterator` class to read APNG frame sequences. +The PNG plugin also now supports using the ``append_images`` argument to write APNG frame +sequences. See :ref:`apng-sequences` for further details. diff --git a/docs/releasenotes/7.1.1.rst b/docs/releasenotes/7.1.1.rst new file mode 100644 index 00000000000..2169e6a05b8 --- /dev/null +++ b/docs/releasenotes/7.1.1.rst @@ -0,0 +1,25 @@ +7.1.1 +----- + +Fix regression seeking PNG files +================================ + +This fixes a regression introduced in 7.1.0 when adding support for APNG files when calling +``seek`` and ``tell``: + +.. code-block:: pycon + + >>> from PIL import Image + >>> with Image.open("Tests/images/hopper.png") as im: + ... im.seek(0) + ... + Traceback (most recent call last): + File "", line 2, in + File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/PIL/PngImagePlugin.py", line 739, in seek + if not self._seek_check(frame): + File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/PIL/ImageFile.py", line 306, in _seek_check + return self.tell() != frame + File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/PIL/PngImagePlugin.py", line 827, in tell + return self.__frame + AttributeError: 'PngImageFile' object has no attribute '_PngImageFile__frame' + >>> diff --git a/docs/releasenotes/7.1.2.rst b/docs/releasenotes/7.1.2.rst new file mode 100644 index 00000000000..b12d84e33bd --- /dev/null +++ b/docs/releasenotes/7.1.2.rst @@ -0,0 +1,16 @@ +7.1.2 +----- + +Fix another regression seeking PNG files +======================================== + +This fixes a regression introduced in 7.1.0 when adding support for APNG files. + +When calling ``seek(n)`` on a regular PNG where ``n > 0``, it failed to raise an +``EOFError`` as it should have done, resulting in: + +.. code-block:: pycon + + AttributeError: 'NoneType' object has no attribute 'read' + +Pillow 7.1.2 now raises the correct exception. diff --git a/docs/releasenotes/7.2.0.rst b/docs/releasenotes/7.2.0.rst new file mode 100644 index 00000000000..ff1b7c9e764 --- /dev/null +++ b/docs/releasenotes/7.2.0.rst @@ -0,0 +1,58 @@ +7.2.0 +----- + +API Changes +=========== + +Replaced TiffImagePlugin DEBUG with logging +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``TiffImagePlugin.DEBUG = True`` has been a way to print various debugging +information when interacting with TIFF images. This has now been removed +in favour of Python's ``logging`` module, already used in other places in the +Pillow source code. + +Corrected default offset when writing EXIF data +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, the default ``offset`` argument for +:py:meth:`~PIL.Image.Exif.tobytes` was 0, which did not include the magic +header. It is now 8. + +Moved to ImageFileDirectory_v2 in Image.Exif +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Moved from the legacy :py:class:`PIL.TiffImagePlugin.ImageFileDirectory_v1` to +:py:class:`PIL.TiffImagePlugin.ImageFileDirectory_v2` in +:py:class:`PIL.Image.Exif`. This means that Exif RATIONALs and SIGNED_RATIONALs +are now read as :py:class:`PIL.TiffImagePlugin.IFDRational`, instead of as a +tuple with a numerator and a denominator. + +TIFF BYTE tags format +^^^^^^^^^^^^^^^^^^^^^ + +TIFF BYTE tags were previously read as a tuple containing a bytestring. They +are now read as just a single bytestring. + +Deprecations +^^^^^^^^^^^^ + +Image.show command parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``command`` parameter was deprecated and will be removed in a future release. +Use a subclass of :py:class:`PIL.ImageShow.Viewer` instead. + +Image._showxv +~~~~~~~~~~~~~ + +``Image._showxv`` has been deprecated. Use :py:meth:`~PIL.Image.Image.show` +instead. If custom behaviour is required, use :py:meth:`~PIL.ImageShow.register` to add +a custom :py:class:`~PIL.ImageShow.Viewer` class. + +ImageFile.raise_ioerror +~~~~~~~~~~~~~~~~~~~~~~~ + +``IOError`` was merged into ``OSError`` in Python 3.3. So, ``ImageFile.raise_ioerror`` +is now deprecated and will be removed in a future release. Use +``ImageFile.raise_oserror`` instead. diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst new file mode 100644 index 00000000000..fe26580474d --- /dev/null +++ b/docs/releasenotes/8.0.0.rst @@ -0,0 +1,180 @@ +8.0.0 +----- + +Backwards Incompatible Changes +============================== + +Python 3.5 +^^^^^^^^^^ + +Pillow has dropped support for Python 3.5, which reached end-of-life on 2020-09-13. + +PyPy 7.1.x +^^^^^^^^^^ + +Pillow has dropped support for PyPy3 7.1.1. +PyPy3 7.2.0, released on 2019-10-14, is now the minimum compatible version. + +im.offset +^^^^^^^^^ + +``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead. + +Image.fromstring, im.fromstring and im.tostring +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* ``Image.fromstring()`` has been removed, call :py:func:`.Image.frombytes()` instead. +* ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead. +* ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead. + +ImageCms.CmsProfile attributes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed: + +======================== =================================================== +Removed Use instead +======================== =================================================== +``color_space`` Padded :py:attr:`~.CmsProfile.xcolor_space` +``pcs`` Padded :py:attr:`~.CmsProfile.connection_space` +``product_copyright`` Unicode :py:attr:`~.CmsProfile.copyright` +``product_desc`` Unicode :py:attr:`~.CmsProfile.profile_description` +``product_description`` Unicode :py:attr:`~.CmsProfile.profile_description` +``product_manufacturer`` Unicode :py:attr:`~.CmsProfile.manufacturer` +``product_model`` Unicode :py:attr:`~.CmsProfile.model` +======================== =================================================== + +API Changes +=========== + +ImageDraw.text: stroke_width +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Fixed issue where passing ``stroke_width`` with a non-zero value +to :py:meth:`.ImageDraw.text` would cause the text to be offset by that amount. + +ImageDraw.text: anchor +^^^^^^^^^^^^^^^^^^^^^^ + +The ``anchor`` parameter of :py:meth:`.ImageDraw.text` has been implemented. + +Use this parameter to change the position of text relative to the +specified ``xy`` point. See :ref:`text-anchors` for details. + +Add MIME type to PsdImagePlugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +"image/vnd.adobe.photoshop" is now registered as the +:py:class:`.PsdImagePlugin.PsdImageFile` MIME type. + +API Additions +============= + +Image.open: add formats parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Added a new ``formats`` parameter to :py:func:`.Image.open`: + +* A list or tuple of formats to attempt to load the file in. + This can be used to restrict the set of formats checked. + Pass ``None`` to try all supported formats. You can print the set of + available formats by running ``python3 -m PIL`` or using + the :py:func:`PIL.features.pilinfo` function. + +ImageOps.autocontrast: add mask parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:func:`.ImageOps.autocontrast` can now take a ``mask`` parameter: + +* Histogram used in contrast operation is computed using pixels within the mask. + If no mask is given the entire image is used for histogram computation. + +ImageOps.autocontrast cutoffs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, the ``cutoff`` parameter of :py:func:`.ImageOps.autocontrast` could only +be a single number, used as the percent to cut off from the histogram on the low and +high ends. + +Now, it can also be a tuple ``(low, high)``. + +ImageDraw.regular_polygon +^^^^^^^^^^^^^^^^^^^^^^^^^ + +A new method :py:meth:`.ImageDraw.regular_polygon`, draws a regular polygon of ``n_sides``, inscribed in a ``bounding_circle``. + +For example ``draw.regular_polygon(((100, 100), 50), 5)`` +draws a pentagon centered at the point ``(100, 100)`` with a polygon radius of ``50``. + +ImageDraw.text: embedded_color +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The methods :py:meth:`.ImageDraw.text` and :py:meth:`.ImageDraw.multiline_text` +now support fonts with embedded color data. + +To render text with embedded color data, use the parameter ``embedded_color=True``. + +Support for CBDT fonts requires FreeType 2.5 compiled with libpng. +Support for SBIX fonts requires FreeType 2.5.1 compiled with libpng. +Support for COLR fonts requires FreeType 2.10. +SVG fonts are not yet supported. + +ImageDraw.textlength +^^^^^^^^^^^^^^^^^^^^ + +Two new methods :py:meth:`.ImageDraw.textlength` and :py:meth:`.FreeTypeFont.getlength` +were added, returning the exact advance length of text with 1/64 pixel precision. + +These can be used for word-wrapping or rendering text in parts. + +ImageDraw.textbbox +^^^^^^^^^^^^^^^^^^ + +Three new methods :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.multiline_textbbox`, +and :py:meth:`.FreeTypeFont.getbbox` return the bounding box of rendered text. + +These functions accept an ``anchor`` parameter, see :ref:`text-anchors` for details. + +Other Changes +============= + +Improved ellipse-drawing algorithm +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ellipse-drawing algorithm has been changed from drawing a 360-sided polygon to one +which resembles Bresenham's algorithm for circles. It should be faster and produce +smoother curves, especially for smaller ellipses. + +ImageDraw.text and ImageDraw.multiline_text +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Fixed multiple issues in methods :py:meth:`.ImageDraw.text` and :py:meth:`.ImageDraw.multiline_text` +sometimes causing unexpected text alignment issues. + +The ``align`` parameter of :py:meth:`.ImageDraw.multiline_text` now gives better results in some cases. + +TrueType fonts with embedded bitmaps are now supported. + +Added writing of subIFDs +^^^^^^^^^^^^^^^^^^^^^^^^ + +When saving EXIF data, Pillow is now able to write subIFDs, such as the GPS IFD. This +should happen automatically when saving an image using the EXIF data that it was opened +with, such as in :py:meth:`~PIL.ImageOps.exif_transpose`. + +Previously, the code of the first tag of the subIFD was incorrectly written as the +offset. + +Error for large BMP files +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, if a BMP file was too large, an ``OSError`` would be raised. Now, +``DecompressionBombError`` is used instead, as Pillow already uses for other formats. + +Dark theme for docs +^^^^^^^^^^^^^^^^^^^ + +The https://pillow.readthedocs.io documentation will use a dark theme if the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query. + + + diff --git a/docs/releasenotes/8.0.1.rst b/docs/releasenotes/8.0.1.rst new file mode 100644 index 00000000000..3584a5d72e9 --- /dev/null +++ b/docs/releasenotes/8.0.1.rst @@ -0,0 +1,22 @@ +8.0.1 +----- + +Security +======== + +Update FreeType used in binary wheels to `2.10.4`_ to fix :cve:`CVE-2020-15999`: + + - A heap buffer overflow has been found in the handling of embedded PNG bitmaps, + introduced in FreeType version 2.6. + + If you use option ``FT_CONFIG_OPTION_USE_PNG`` you should upgrade immediately. + +We strongly recommend updating to Pillow 8.0.1 if you are using Pillow 8.0.0, which improved support for bitmap fonts. + +In Pillow 7.2.0 and earlier bitmap fonts were disabled with ``FT_LOAD_NO_BITMAP``, but it is not +clear if this prevents the exploit and we recommend updating to Pillow 8.0.1. + +Pillow 8.0.0 and earlier are potentially vulnerable releases, including the last release +to support Python 2.7, namely Pillow 6.2.2. + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ diff --git a/docs/releasenotes/8.1.0.rst b/docs/releasenotes/8.1.0.rst new file mode 100644 index 00000000000..8ed1d9d85cc --- /dev/null +++ b/docs/releasenotes/8.1.0.rst @@ -0,0 +1,93 @@ +8.1.0 +----- + +Deprecations +============ + +FreeType 2.7 +^^^^^^^^^^^^ + +Support for FreeType 2.7 is deprecated and will be removed in Pillow 9.0.0 (2022-01-02), +when FreeType 2.8 will be the minimum supported. + +We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ + +Makefile +^^^^^^^^ + +The ``install-venv`` target has been deprecated. + +API Additions +============= + +Append images to ICO +^^^^^^^^^^^^^^^^^^^^ + +When saving an ICO image, the file may contain versions of the image at different +sizes. By default, Pillow will scale down the main image to create these copies. + +With this release, a list of images can be provided to the ``append_images`` parameter +when saving, to replace the scaled down versions. This is the same functionality that +already exists for the ICNS format. + +Security +======== + +This release includes security fixes. + +* An out-of-bounds read when saving TIFFs with custom metadata through LibTIFF +* An out-of-bounds read when saving a GIF of 1px width +* :cve:`CVE-2020-35653` Buffer read overrun in PCX decoding + +The PCX image decoder used the reported image stride to calculate the row buffer, +rather than calculating it from the image size. This issue dates back to the PIL fork. +Thanks to Google's `OSS-Fuzz`_ project for finding this. + +* :cve:`CVE-2020-35654` Fix TIFF out-of-bounds write error + +Out-of-bounds write in ``TiffDecode.c`` when reading corrupt YCbCr files in some +LibTIFF versions (4.1.0/Ubuntu 20.04, but not 4.0.9/Ubuntu 18.04). In some cases +LibTIFF's interpretation of the file is different when reading in RGBA mode, leading to +an out-of-bounds write in ``TiffDecode.c``. This potentially affects Pillow versions +from 6.0.0 to 8.0.1, depending on the version of LibTIFF. This was reported through +`Tidelift`_. + +* :cve:`CVE-2020-35655` Fix for SGI Decode buffer overrun + +4 byte read overflow in ``SgiRleDecode.c``, where the code was not correctly checking the +offsets and length tables. Independently reported through `Tidelift`_ and Google's +`OSS-Fuzz`_. This vulnerability covers Pillow versions 4.3.0->8.0.1. + +.. _Tidelift: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pillow&utm_medium=referral&utm_campaign=docs +.. _OSS-Fuzz: https://github.com/google/oss-fuzz + +Dependencies +^^^^^^^^^^^^ + +OpenJPEG in the macOS and Linux wheels has been updated from 2.3.1 to 2.4.0, including +security fixes. + +LibTIFF in the macOS and Linux wheels has been updated from 4.1.0 to 4.2.0, including +security fixes discovered by fuzzers. + +Other Changes +============= + +Makefile +^^^^^^^^ + +The ``co`` target has been removed. + +PyPy wheels +^^^^^^^^^^^ + +Wheels have been added for PyPy 3.7. + +PySide6 +^^^^^^^ + +Support has been added for PySide6. If it is installed, it will be used instead of +PyQt5 or PySide2, since it is based on a newer Qt. diff --git a/docs/releasenotes/8.1.1.rst b/docs/releasenotes/8.1.1.rst new file mode 100644 index 00000000000..4081c49ca5c --- /dev/null +++ b/docs/releasenotes/8.1.1.rst @@ -0,0 +1,27 @@ +8.1.1 +----- + +Security +======== + +:cve:`CVE-2021-25289`: The previous fix for :cve:`CVE-2020-35654` was insufficient +due to incorrect error checking in ``TiffDecode.c``. + +:cve:`CVE-2021-25290`: In ``TiffDecode.c``, there is a negative-offset ``memcpy`` +with an invalid size. + +:cve:`CVE-2021-25291`: In ``TiffDecode.c``, invalid tile boundaries could lead to +an out-of-bounds read in ``TIFFReadRGBATile``. + +:cve:`CVE-2021-25292`: The PDF parser has a catastrophic backtracking regex +that could be used as a DOS attack. + +:cve:`CVE-2021-25293`: There is an out-of-bounds read in ``SgiRleDecode.c``, +since Pillow 4.3.0. + + +Other Changes +============= + +A crash with the feature flags for libimagequant, libjpeg-turbo, WebP and XCB on +unreleased Python 3.10 has been fixed (:issue:`5193`). diff --git a/docs/releasenotes/8.1.2.rst b/docs/releasenotes/8.1.2.rst new file mode 100644 index 00000000000..50d132f3337 --- /dev/null +++ b/docs/releasenotes/8.1.2.rst @@ -0,0 +1,12 @@ +8.1.2 +----- + +Security +======== + +There is an exhaustion of memory DOS in the BLP (:cve:`CVE-2021-27921`), +ICNS (:cve:`CVE-2021-27922`) and ICO (:cve:`CVE-2021-27923`) container formats +where Pillow did not properly check the reported size of the contained image. +These images could cause arbitrarily large memory allocations. This was reported +by Jiayi Lin, Luke Shaffer, Xinran Xie, and Akshay Ajayan of +`Arizona State University `_. diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst new file mode 100644 index 00000000000..c902ccf71fb --- /dev/null +++ b/docs/releasenotes/8.2.0.rst @@ -0,0 +1,230 @@ +8.2.0 +----- + +Deprecations +============ + +Categories +^^^^^^^^^^ + +``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), +along with the related ``Image.NORMAL``, ``Image.SEQUENCE`` and +``Image.CONTAINER`` attributes. + +To determine if an image has multiple frames or not, +``getattr(im, "is_animated", False)`` can be used instead. + +Tk/Tcl 8.4 +^^^^^^^^^^ + +Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), +when Tk/Tcl 8.5 will be the minimum supported. + +API Changes +=========== + +Image.alpha_composite: dest +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When calling :py:meth:`~PIL.Image.Image.alpha_composite`, the ``dest`` argument now +accepts negative co-ordinates, like the upper left corner of the ``box`` argument of +:py:meth:`~PIL.Image.Image.paste` can be negative. Naturally, this has effect of +cropping the overlaid image. + +Image.getexif: EXIF and GPS IFD +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, :py:meth:`~PIL.Image.Image.getexif` flattened the EXIF IFD into the rest of +the data, losing information. This information is now kept separate, moved under +``im.getexif().get_ifd(0x8769)``. + +Direct access to the GPS IFD dictionary was possible through ``im.getexif()[0x8825]``. +This is now consistent with other IFDs, and must be accessed through +``im.getexif().get_ifd(0x8825)``. + +These changes only affect :py:meth:`~PIL.Image.Image.getexif`, introduced in Pillow +6.0. The older ``_getexif()`` methods are unaffected. + +Image._MODEINFO +^^^^^^^^^^^^^^^ + +This internal dictionary had been deprecated by a comment since PIL, and is now +removed. Instead, ``Image.getmodebase()``, ``Image.getmodetype()``, +``Image.getmodebandnames()``, ``Image.getmodebands()`` or ``ImageMode.getmode()`` +can be used. + +API Additions +============= + +getxmp() for JPEG images +^^^^^^^^^^^^^^^^^^^^^^^^ + +A new method has been added to return +`XMP data `_ for JPEG +images. It reads the XML data into a dictionary of names and values. + +For example:: + + >>> from PIL import Image + >>> with Image.open("Tests/images/xmp_test.jpg") as im: + >>> print(im.getxmp()) + {'RDF': {}, 'Description': {'Version': '10.4', 'ProcessVersion': '10.0', ...}, ...} + +ImageDraw.rounded_rectangle +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Added :py:meth:`~PIL.ImageDraw.ImageDraw.rounded_rectangle`. It works the same as +:py:meth:`~PIL.ImageDraw.ImageDraw.rectangle`, except with an additional ``radius`` +argument. ``radius`` is limited to half of the width or the height, so that users can +create a circle, but not any other ellipse. + +.. code-block:: python + + from PIL import Image, ImageDraw + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + draw.rounded_rectangle(xy=(10, 20, 190, 180), radius=30, fill="red") + +ImageOps.autocontrast: preserve_tone +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The default behaviour of :py:meth:`~PIL.ImageOps.autocontrast` is to normalize +separate histograms for each color channel, changing the tone of the image. The new +``preserve_tone`` argument keeps the tone unchanged by using one luminance histogram +for all channels. + +ImageShow.GmDisplayViewer +^^^^^^^^^^^^^^^^^^^^^^^^^ + +If GraphicsMagick is present, this new :py:class:`PIL.ImageShow.Viewer` subclass will +be registered. It uses GraphicsMagick_, an ImageMagick_ fork, to display images. + +The GraphicsMagick based viewer has a lower priority than its ImageMagick +counterpart. Thus, if both ImageMagick and GraphicsMagick are installed, +``im.show()`` and :py:func:`.ImageShow.show()` prefer the viewer based on +ImageMagick, i.e the behaviour stays the same for Pillow users having +ImageMagick installed. + +ImageShow.IPythonViewer +^^^^^^^^^^^^^^^^^^^^^^^ + +If IPython is present, this new :py:class:`PIL.ImageShow.Viewer` subclass will be +registered. It displays images on all IPython frontends. This will be helpful +to users of Google Colab, allowing ``im.show()`` to display images. + +It is lower in priority than the other default :py:class:`PIL.ImageShow.Viewer` +instances, so it will only be used by ``im.show()`` or :py:func:`.ImageShow.show()` +if none of the other viewers are available. This means that the behaviour of +:py:class:`PIL.ImageShow` will stay the same for most Pillow users. + +Saving TIFF with ICC profile +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +As is already possible for JPEG, PNG and WebP, the ICC profile for TIFF files can now +be specified through a keyword argument:: + + im.save("out.tif", icc_profile=...) + + +Security +======== + +These were all found with `OSS-Fuzz`_. + +:cve:`CVE-2021-25287`, :cve:`CVE-2021-25288`: Fix OOB read in Jpeg2KDecode +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* For J2k images with multiple bands, it's legal to have different widths for each band, + e.g. 1 byte for ``L``, 4 bytes for ``A``. +* This dates to Pillow 2.4.0. + +:cve:`CVE-2021-28675`: Fix DOS in PsdImagePlugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* :py:class:`.PsdImagePlugin.PsdImageFile` did not sanity check the number of input + layers with regard to the size of the data block, this could lead to a + denial-of-service on :py:meth:`~PIL.Image.open` prior to + :py:meth:`~PIL.Image.Image.load`. +* This dates to the PIL fork. + +:cve:`CVE-2021-28676`: Fix FLI DOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* ``FliDecode.c`` did not properly check that the block advance was non-zero, + potentially leading to an infinite loop on load. +* This dates to the PIL fork. + +:cve:`CVE-2021-28677`: Fix EPS DOS on _open +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* The readline used in EPS has to deal with any combination of ``\r`` and ``\n`` as line + endings. It accidentally used a quadratic method of accumulating lines while looking + for a line ending. +* A malicious EPS file could use this to perform a denial-of-service of Pillow in the + open phase, before an image was accepted for opening. +* This dates to the PIL fork. + +:cve:`CVE-2021-28678`: Fix BLP DOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* ``BlpImagePlugin`` did not properly check that reads after jumping to file offsets + returned data. This could lead to a denial-of-service where the decoder could be run a + large number of times on empty data. +* This dates to Pillow 5.1.0. + +Fix memory DOS in ImageFont +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* A corrupt or specially crafted TTF font could have font metrics that lead to + unreasonably large sizes when rendering text in font. ``ImageFont.py`` did not check + the image size before allocating memory for it. +* This dates to the PIL fork. + +Other Changes +============= + +GIF writer uses LZW encoding +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +GIF files are now written using LZW encoding, which will generate smaller files, +typically about 70% of the size generated by the older encoder. + +The pixel data is encoded using the format specified in the `CompuServe GIF standard +`_. + +The older encoder used a variant of run-length encoding that was compatible but less +efficient. + +GraphicsMagick +^^^^^^^^^^^^^^ + +The test suite can now be run on systems which have GraphicsMagick_ but not +ImageMagick_ installed. If both are installed, the tests prefer ImageMagick. + +Libraqm and FriBiDi linking +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The way the libraqm dependency for complex text scripts is linked has been changed: + +Source builds will now link against the system version of libraqm at build time +rather than at runtime by default. + +Binary wheels now include a statically linked modified version of libraqm that +links against FriBiDi at runtime instead. This change is intended to address +issues with the previous implementation on some platforms. These are created +by building Pillow with the new build flags ``--vendor-raqm --vendor-fribidi``. + +Windows users will now need to install ``fribidi.dll`` (or ``fribidi-0.dll``) only, +``libraqm.dll`` is no longer used. + +See :doc:`installation documentation<../installation>` for more information. + +PyQt6 +^^^^^ + +Support has been added for PyQt6. If it is installed, it will be used instead of +PySide6, PyQt5 or PySide2. + +.. _GraphicsMagick: http://www.graphicsmagick.org/ +.. _ImageMagick: https://imagemagick.org/ +.. _OSS-Fuzz: https://github.com/google/oss-fuzz diff --git a/docs/releasenotes/8.3.0.rst b/docs/releasenotes/8.3.0.rst new file mode 100644 index 00000000000..0bfead14470 --- /dev/null +++ b/docs/releasenotes/8.3.0.rst @@ -0,0 +1,113 @@ +8.3.0 +----- + +Deprecations +============ + +JpegImagePlugin.convert_dict_qtables +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +JPEG ``quantization`` is now automatically converted, but still returned as a +dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no longer +performs any operations on the data given to it, has been deprecated and will be +removed in Pillow 10.0.0 (2023-07-01). + +API Changes +=========== + +Changed WebP default "method" value when saving +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, it was 0, for the best speed. The default has now been changed to 4, to +match WebP's default, for higher quality with still some speed optimisation. + +Default resampling filter for special image modes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 7.0 changed the default resampling filter to ``Image.BICUBIC``. However, as this +is not supported yet for images with a custom number of bits, the default filter for +those modes has been reverted to ``Image.NEAREST``. + +ImageMorph incorrect mode errors +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For ``apply()``, ``match()`` and ``get_on_pixels()``, if the image mode is not L, an +:py:exc:`Exception` was thrown. This has now been changed to a :py:exc:`ValueError`. + +getxmp() +^^^^^^^^ + +`XMP data `_ can now be +returned for PNG and TIFF images, through ``getxmp()`` for each format. + +The returned dictionary will start from the base of the XML, meaning that the top level +should contain an "xmpmeta" key. JPEG's ``getxmp()`` method has also been updated to +this structure. + +TIFF getexif() +^^^^^^^^^^^^^^ + +TIFF :py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag_v2` data can now be accessed +through :py:meth:`~PIL.Image.Image.getexif`. This also provides access to the GPS and +EXIF IFDs, through ``im.getexif().get_ifd(0x8825)`` and +``im.getexif().get_ifd(0x8769)`` respectively. + +API Additions +============= + +ImageOps.contain +^^^^^^^^^^^^^^^^ + +Returns a resized version of the image, set to the maximum width and height within +``size``, while maintaining the original aspect ratio. + +To compare it to other ImageOps methods: + +- :py:meth:`~PIL.ImageOps.fit` expands an image until is fills ``size``, cropping the + parts of the image that do not fit. +- :py:meth:`~PIL.ImageOps.pad` expands an image to fill ``size``, without cropping, but + instead filling the extra space with ``color``. +- :py:meth:`~PIL.ImageOps.contain` is similar to :py:meth:`~PIL.ImageOps.pad`, but it + does not fill the extra space. Instead, the original aspect ratio is maintained. So + unlike the other two methods, it is not guaranteed to return an image of ``size``. + +ICO saving: bitmap_format argument +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +By default, Pillow saves ICO files in the PNG format. They can now also be saved in BMP +format, through the new ``bitmap_format`` argument:: + + im.save("out.ico", bitmap_format="bmp") + +Security +======== + +Buffer overflow +^^^^^^^^^^^^^^^ + +This release addresses :cve:`CVE-2021-34552`. PIL since 1.1.4 and Pillow since 1.0 +allowed parameters passed into a convert function to trigger buffer overflow in +Convert.c. + +Parsing XML +^^^^^^^^^^^ + +Pillow previously parsed XMP data using Python's ``xml`` module. However, this module +is not secure. + +- :py:meth:`~PIL.Image.Image.getexif` has used ``xml`` to potentially retrieve + orientation data since Pillow 7.2.0. It has been refactored to use ``re`` instead. +- :py:meth:`~PIL.JpegImagePlugin.JpegImageFile.getxmp` was added in Pillow 8.2.0. It + will now use ``defusedxml`` instead. If the dependency is not present, an empty + dictionary will be returned and a warning raised. + +Other Changes +============= + +Added DDS BC5 reading and uncompressed saving +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added to read the BC5 format of DDS images, whether UNORM, SNORM or +TYPELESS. + +Support has also been added to write the uncompressed format of DDS images. diff --git a/docs/releasenotes/8.3.1.rst b/docs/releasenotes/8.3.1.rst new file mode 100644 index 00000000000..e97070c111c --- /dev/null +++ b/docs/releasenotes/8.3.1.rst @@ -0,0 +1,40 @@ +8.3.1 +----- + +Fixed regression converting to NumPy arrays +=========================================== + +This fixes a regression introduced in 8.3.0 when converting an image to a NumPy array +with a ``dtype`` argument. + +.. code-block:: pycon + + >>> from PIL import Image + >>> import numpy + >>> im = Image.new("RGB", (100, 100)) + >>> numpy.array(im, dtype=numpy.float64) + Traceback (most recent call last): + File "", line 1, in + TypeError: __array__() takes 1 positional argument but 2 were given + >>> + +Catch OSError when checking if destination is sys.stdout +======================================================== + +In 8.3.0, a check to see if the destination was ``sys.stdout`` when saving an image was +updated. This lead to an OSError being raised if the environment restricted access. + +The OSError is now silently caught. + +Fixed removing orientation in ImageOps.exif_transpose +===================================================== + +In 8.3.0, :py:meth:`~PIL.ImageOps.exif_transpose` was changed to ensure that the +original image EXIF data was not modified, and the orientation was only removed from +the modified copy. + +However, for certain images the orientation was already missing from the modified +image, leading to a KeyError. + +This error has been resolved, and the copying of metadata to the modified image +improved. diff --git a/docs/releasenotes/8.3.2.rst b/docs/releasenotes/8.3.2.rst new file mode 100644 index 00000000000..6b5c759fc0a --- /dev/null +++ b/docs/releasenotes/8.3.2.rst @@ -0,0 +1,41 @@ +8.3.2 +----- + +Security +======== + +* :cve:`CVE-2021-23437`: Avoid a potential ReDoS (regular expression denial of service) + in :py:class:`~PIL.ImageColor`'s :py:meth:`~PIL.ImageColor.getrgb` by raising + :py:exc:`ValueError` if the color specifier is too long. Present since Pillow 5.2.0. + +* Fix 6-byte out-of-bounds (OOB) read. The previous bounds check in ``FliDecode.c`` + incorrectly calculated the required read buffer size when copying a chunk, potentially + reading six extra bytes off the end of the allocated buffer from the heap. Present + since Pillow 7.1.0. This bug was found by Google's `OSS-Fuzz`_ `CIFuzz`_ runs. + +Other Changes +============= + +Python 3.10 wheels +^^^^^^^^^^^^^^^^^^ + +Pillow now includes binary wheels for Python 3.10. + +The Python 3.10 release candidate was released on 2021-08-03 with the final release due +2021-10-04 (:pep:`619`). The CPython core team strongly encourages maintainers of +third-party Python projects to prepare for 3.10 compatibility. And as there are `no ABI +changes`_ planned we are releasing wheels to help others prepare for 3.10, and ensure +Pillow can be used immediately on release day of 3.10.0 final. + +Fixed regressions +^^^^^^^^^^^^^^^^^ + +* Ensure TIFF ``RowsPerStrip`` is multiple of 8 for JPEG compression (:pr:`5588`). + +* Updates for :py:class:`~PIL.ImagePalette` channel order (:pr:`5599`). + +* Hide FriBiDi shim symbols to avoid conflict with real FriBiDi library (:pr:`5651`). + +.. _OSS-Fuzz: https://github.com/google/oss-fuzz +.. _CIFuzz: https://google.github.io/oss-fuzz/getting-started/continuous-integration/ +.. _no ABI changes: https://www.python.org/downloads/release/python-3100rc1/ diff --git a/docs/releasenotes/8.4.0.rst b/docs/releasenotes/8.4.0.rst new file mode 100644 index 00000000000..9becf91465e --- /dev/null +++ b/docs/releasenotes/8.4.0.rst @@ -0,0 +1,53 @@ +8.4.0 +----- + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +ImagePalette size parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``size`` parameter will be removed in Pillow 10.0.0 (2023-07-01). + +Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by +default, and the size parameter could be used to override that. Pillow 8.3.0 removed +the default required length, also removing the need for the size parameter. + +API Additions +============= + +Added "transparency" argument for loading EPS images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This new argument switches the Ghostscript device from "ppmraw" to "pngalpha", +generating an RGBA image with a transparent background instead of an RGB image with a +white background. + +.. code-block:: python + + with Image.open("sample.eps") as im: + im.load(transparency=True) + +Added WalImageFile class +^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:func:`PIL.WalImageFile.open()` previously returned a generic +:py:class:`PIL.Image.Image` instance. It now returns a dedicated +:py:class:`PIL.WalImageFile.WalImageFile` class. + +Other Changes +============= + +Speed improvement when rotating square images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Starting with Pillow 3.3.0, the speed of rotating images by 90 or 270 degrees was +improved by quickly returning :py:meth:`~PIL.Image.Image.transpose` instead, if the +rotate operation allowed for expansion and did not specify a center or post-rotate +translation. + +Since the ``expand`` flag makes no difference for square images though, Pillow now +uses this faster method for square images without the ``expand`` flag as well. diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst new file mode 100644 index 00000000000..a19da361aaa --- /dev/null +++ b/docs/releasenotes/9.0.0.rst @@ -0,0 +1,174 @@ +9.0.0 +----- + +Fredrik Lundh +============= + +This release is dedicated to the memory of Fredrik Lundh, aka Effbot, who died in +November 2021. Fredrik created PIL in 1995 and he was instrumental in the early +success of Python. + +`Guido wrote `_: + + Fredrik was an early Python contributor (e.g. Elementtree and the 're' + module) and his enthusiasm for the language and community were inspiring + for all who encountered him or his work. He spent countless hours on + comp.lang.python answering questions from newbies and advanced users alike. + + He also co-founded an early Python startup, Secret Labs AB, which among + other software released an IDE named PythonWorks. Fredrik also created the + Python Imaging Library (PIL) which is still THE way to interact with images + in Python, now most often through its Pillow fork. His effbot.org site was + a valuable resource for generations of Python users, especially its Tkinter + documentation. + +Thank you, Fredrik. + +Backwards Incompatible Changes +============================== + +Python 3.6 +^^^^^^^^^^ + +Pillow has dropped support for Python 3.6, which reached end-of-life on 2021-12-23. + +PILLOW_VERSION constant +^^^^^^^^^^^^^^^^^^^^^^^ + +``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. + +FreeType 2.7 +^^^^^^^^^^^^ + +Support for FreeType 2.7 has been removed; FreeType 2.8 is the minimum supported. + +We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). + +.. _FreeType: https://freetype.org/ + +Image.show command parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``command`` parameter has been removed. Use a subclass of +:py:class:`PIL.ImageShow.Viewer` instead. + +Image._showxv +^^^^^^^^^^^^^ + +``Image._showxv`` has been removed. Use :py:meth:`~PIL.Image.Image.show` +instead. If custom behaviour is required, use :py:meth:`~PIL.ImageShow.register` to add +a custom :py:class:`~PIL.ImageShow.Viewer` class. + +ImageFile.raise_ioerror +^^^^^^^^^^^^^^^^^^^^^^^ + +``IOError`` was merged into ``OSError`` in Python 3.3. So, ``ImageFile.raise_ioerror`` +has been removed. Use ``ImageFile.raise_oserror`` instead. + + +API Changes +=========== + +Added line width parameter to ImageDraw polygon +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An optional line ``width`` parameter has been added to ``ImageDraw.Draw.polygon``. + + +API Additions +============= + +ImageShow.XDGViewer +^^^^^^^^^^^^^^^^^^^ + +If ``xdg-open`` is present on Linux, this new :py:class:`PIL.ImageShow.Viewer` subclass +will be registered. It displays images using the application selected by the system. + +It is higher in priority than the other default :py:class:`PIL.ImageShow.Viewer` +instances, so it will be preferred by ``im.show()`` or :py:func:`.ImageShow.show()`. + +Added support for "title" argument to DisplayViewer +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for the "title" argument in +:py:class:`~PIL.ImageShow.UnixViewer.DisplayViewer`, so that when ``im.show()`` or +:py:func:`.ImageShow.show()` use the ``display`` command line tool, the "title" +argument will also now be supported, e.g. ``im.show(title="My Image")`` and +``ImageShow.show(im, title="My Image")``. + +Security +======== + +Ensure JpegImagePlugin stops at the end of a truncated file +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``JpegImagePlugin`` may append an EOF marker to the end of a truncated file, so that +the last segment of the data will still be processed by the decoder. + +If the EOF marker is not detected as such however, this could lead to an infinite +loop where ``JpegImagePlugin`` keeps trying to end the file. + +Remove consecutive duplicate tiles that only differ by their offset +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To prevent attempts to slow down loading times for images, if an image has consecutive +duplicate tiles that only differ by their offset, only load the last tile. Credit to +Google's `OSS-Fuzz`_ project for finding this issue. + +Restrict builtins available to ImageMath.eval +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cve:`CVE-2022-22817`: To limit :py:class:`PIL.ImageMath` to working with images, Pillow +will now restrict the builtins available to :py:meth:`PIL.ImageMath.eval`. This will +help prevent problems arising if users evaluate arbitrary expressions, such as +``ImageMath.eval("exec(exit())")``. + +Fixed ImagePath.Path array handling +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cve:`CVE-2022-22815` (:cwe:`CWE-126`) and :cve:`CVE-2022-22816` (:cwe:`CWE-665`) were +found when initializing ``ImagePath.Path``. + +.. _OSS-Fuzz: https://github.com/google/oss-fuzz + +Other Changes +============= + +Convert subsequent GIF frames to RGB or RGBA +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Since each frame of a GIF can have up to 256 colors, after the first frame it is +possible for there to be too many colors to fit in a P mode image. To allow for this, +seeking to any subsequent GIF frame will now convert the image to RGB or RGBA, +depending on whether or not the first frame had transparency. + +Switched to libjpeg-turbo in macOS and Linux wheels +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The Pillow wheels from PyPI for macOS and Linux have switched from libjpeg to +libjpeg-turbo. It is a fork of libjpeg, popular for its speed. + +Because different JPEG decoders load images differently, JPEG pixels may be +altered slightly with this change. + +Added support for pickling TrueType fonts +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +TrueType fonts may now be pickled and unpickled. For example: + +.. code-block:: python + + import pickle + from PIL import ImageFont + + font = ImageFont.truetype("arial.ttf", size=30) + pickled_font = pickle.dumps(font, protocol=pickle.HIGHEST_PROTOCOL) + + # Later... + unpickled_font = pickle.loads(pickled_font) + +Added support for additional TGA orientations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +TGA images with top right or bottom right orientations are now supported. diff --git a/docs/releasenotes/9.0.1.rst b/docs/releasenotes/9.0.1.rst new file mode 100644 index 00000000000..c1feee088b6 --- /dev/null +++ b/docs/releasenotes/9.0.1.rst @@ -0,0 +1,23 @@ +9.0.1 +----- + +Security +======== + +This release addresses several security problems. + +:cve:`CVE-2022-24303`: If the path to the temporary directory on Linux or macOS +contained a space, this would break removal of the temporary image file after +``im.show()`` (and related actions), and potentially remove an unrelated file. This +has been present since PIL. + +:cve:`CVE-2022-22817`: While Pillow 9.0 restricted top-level builtins available to +:py:meth:`PIL.ImageMath.eval`, it did not prevent builtins available to lambda +expressions. These are now also restricted. + +Other Changes +============= + +Pillow 9.0 added support for ``xdg-open`` as an image viewer, but there have been +reports that the temporary image file was removed too quickly to be loaded into the +final application. A delay has been added. diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst new file mode 100644 index 00000000000..48ce6fef70c --- /dev/null +++ b/docs/releasenotes/9.1.0.rst @@ -0,0 +1,228 @@ +9.1.0 +----- + +API Changes +=========== + +Raise an error when performing a negative crop +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Performing a negative crop on an image previously just returned a ``(0, 0)`` image. Now +it will raise a ``ValueError``, to help reduce confusion if a user has unintentionally +provided the wrong arguments. + +Added specific error if path coordinate type is incorrect +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Rather than returning a ``SystemError``, passing the incorrect types of coordinates into +a path will now raise a more specific ``ValueError``, with the message "incorrect +coordinate type". + +Replace requirements.txt with extras +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Rather than installing all dependencies for docs and tests via ``requirements.txt``, +``extras_require`` is used instead. This installs only those needed and at the same +time as installing Pillow. + +For example: + +.. code-block:: bash + + # Install with dependencies for tests: + python3 -m pip install .[tests] + + # Or for building docs: + python3 -m pip install .[docs] + + # Or for all: + python3 -m pip install .[docs,tests] + +On macOS, the last argument may need to be wrapped in quotes, e.g. +``python3 -m pip install ".[tests]"`` + +Therefore ``requirements.txt`` has been removed along with the ``make install-req`` +command for installing its contents. + +Deprecations +============ + +Constants +^^^^^^^^^ + +A number of constants have been deprecated and will be removed in Pillow 10.0.0 +(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. + +===================================================== ============================================================ +Deprecated Use instead +===================================================== ============================================================ +``Image.NONE`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` +``Image.NEAREST`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` +``Image.ORDERED`` ``Image.Dither.ORDERED`` +``Image.RASTERIZE`` ``Image.Dither.RASTERIZE`` +``Image.FLOYDSTEINBERG`` ``Image.Dither.FLOYDSTEINBERG`` +``Image.WEB`` ``Image.Palette.WEB`` +``Image.ADAPTIVE`` ``Image.Palette.ADAPTIVE`` +``Image.AFFINE`` ``Image.Transform.AFFINE`` +``Image.EXTENT`` ``Image.Transform.EXTENT`` +``Image.PERSPECTIVE`` ``Image.Transform.PERSPECTIVE`` +``Image.QUAD`` ``Image.Transform.QUAD`` +``Image.MESH`` ``Image.Transform.MESH`` +``Image.FLIP_LEFT_RIGHT`` ``Image.Transpose.FLIP_LEFT_RIGHT`` +``Image.FLIP_TOP_BOTTOM`` ``Image.Transpose.FLIP_TOP_BOTTOM`` +``Image.ROTATE_90`` ``Image.Transpose.ROTATE_90`` +``Image.ROTATE_180`` ``Image.Transpose.ROTATE_180`` +``Image.ROTATE_270`` ``Image.Transpose.ROTATE_270`` +``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE`` +``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE`` +``Image.BOX`` ``Image.Resampling.BOX`` +``Image.BILINEAR`` ``Image.Resampling.BILINEAR`` +``Image.LINEAR`` ``Image.Resampling.BILINEAR`` +``Image.HAMMING`` ``Image.Resampling.HAMMING`` +``Image.BICUBIC`` ``Image.Resampling.BICUBIC`` +``Image.CUBIC`` ``Image.Resampling.BICUBIC`` +``Image.LANCZOS`` ``Image.Resampling.LANCZOS`` +``Image.ANTIALIAS`` ``Image.Resampling.LANCZOS`` +``Image.MEDIANCUT`` ``Image.Quantize.MEDIANCUT`` +``Image.MAXCOVERAGE`` ``Image.Quantize.MAXCOVERAGE`` +``Image.FASTOCTREE`` ``Image.Quantize.FASTOCTREE`` +``Image.LIBIMAGEQUANT`` ``Image.Quantize.LIBIMAGEQUANT`` +``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` +``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` +``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` +``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` +``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` +``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` +``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` +``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` +``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` +``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` +``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` +``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` +``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` +``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` +``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` +``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` +``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` +``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` +===================================================== ============================================================ + +ImageShow.Viewer.show_file file argument +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been +deprecated and will be removed in Pillow 10.0.0 (2023-07-01). It has been replaced by +``path``. + +In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. +``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest +``viewer.show_file(path="test.jpg")`` instead. + +FitsStubImagePlugin +^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 9.1.0 + +The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in +Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through +:mod:`~PIL.FitsImagePlugin` instead. + +API Additions +============= + +Added get_photoshop_blocks() to parse Photoshop TIFF tag +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.TiffImagePlugin.TiffImageFile.get_photoshop_blocks` has been added, to +allow users to determine what Photoshop "Image Resource Blocks" are contained within an +image. The keys of the returned dictionary are the image resource IDs. + +At present, the information within each block is merely returned as a dictionary with a +"data" entry. This will allow more useful information to be added in the future without +breaking backwards compatibility. + +Added mct and no_jp2 options for saving JPEG 2000 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :py:meth:`PIL.Image.Image.save` method now supports the following options for +JPEG 2000: + +**mct** + If ``1`` then enable multiple component transformation when encoding, + otherwise use ``0`` for no component transformation (default). If MCT is + enabled and ``irreversible`` is ``True`` then the Irreversible Color + Transformation will be applied, otherwise encoding will use the + Reversible Color Transformation. MCT works best with a ``mode`` of + ``RGB`` and is only applicable when the image data has 3 components. + +**no_jp2** + If ``True`` then don't wrap the raw codestream in the JP2 file format when + saving, otherwise the extension of the filename will be used to determine + the format (default). + +Added PyEncoder +^^^^^^^^^^^^^^^ + +:py:class:`~PIL.ImageFile.PyEncoder` has been added, allowing for file encoders to be +written in Python. See :ref:`Writing Your Own File Codec in Python` for +more information. + +GifImagePlugin loading strategy +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 9.0.0 introduced the conversion of subsequent GIF frames to ``RGB`` or ``RGBA``. This +behaviour can now be changed so that the first ``P`` frame is converted to ``RGB`` as +well. + +.. code-block:: python + + from PIL import GifImagePlugin + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS + +Or subsequent frames can be kept in ``P`` mode as long as there is only a single +palette. + +.. code-block:: python + + from PIL import GifImagePlugin + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY + +Other Changes +============= + +musllinux wheels +^^^^^^^^^^^^^^^^ + +Pillow now builds binary wheels for musllinux, suitable for Linux distributions based on the musl C standard library such as Alpine +(rather than the glibc library used by manylinux wheels). See :pep:`656`. + +ImageShow temporary files on Unix +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When calling :py:meth:`~PIL.Image.Image.show` or using :py:mod:`~PIL.ImageShow`, +a temporary file is created from the image. On Unix, Pillow will no longer delete these +files, and instead leave it to the operating system to do so. + +Image._repr_pretty_ +^^^^^^^^^^^^^^^^^^^ + +``im._repr_pretty_`` has been added to provide a representation of an image without the +identity of the object. This allows Jupyter to describe an image and have that +description stay the same on subsequent executions of the same code. + +Added BigTIFF reading +^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for reading BigTIFF images. + +Added BLP saving +^^^^^^^^^^^^^^^^ + +Support has been added for saving BLP images. ``blp_version`` can be used to specify +whether the image should be saved as BLP1 or BLP2, e.g. +``im.save("out.blp", blp_version="BLP1")``. By default, BLP2 will be used. diff --git a/docs/releasenotes/9.1.1.rst b/docs/releasenotes/9.1.1.rst new file mode 100644 index 00000000000..f8b155f3d6a --- /dev/null +++ b/docs/releasenotes/9.1.1.rst @@ -0,0 +1,16 @@ +9.1.1 +----- + +Security +======== + +This release addresses several security problems. + +:cve:`CVE-2022-30595`: When reading a TGA file with RLE packets that cross scan lines, +Pillow reads the information past the end of the first line without deducting that +from the length of the remaining file data. This vulnerability was introduced in Pillow +9.1.0, and can cause a heap buffer overflow. + +Opening an image with a zero or negative height has been found to bypass a +decompression bomb check. This will now raise a :py:exc:`SyntaxError` instead, in turn +raising a ``PIL.UnidentifiedImageError``. diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst new file mode 100644 index 00000000000..6dbfa2702eb --- /dev/null +++ b/docs/releasenotes/9.2.0.rst @@ -0,0 +1,120 @@ +9.2.0 +----- + +Deprecations +============ + +PyQt5 and PySide2 +^^^^^^^^^^^^^^^^^ + +.. deprecated:: 9.2.0 + +`Qt 5 reached end-of-life `_ on 2020-12-08 for +open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). + +Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed +in Pillow 10 (2023-07-01). Upgrade to +`PyQt6 `_ or +`PySide6 `_ instead. + +FreeTypeFont.getmask2 fill parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 9.2.0 + +The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` +has been deprecated and will be removed in Pillow 10 (2023-07-01). + +PhotoImage.paste box parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 9.2.0 + +The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01). + +Image.coerce_e +^^^^^^^^^^^^^^ + +.. deprecated:: 9.2.0 + +This undocumented method has been deprecated and will be removed in Pillow 10 +(2023-07-01). + +Font size and offset methods +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 9.2.0 + +Several functions for computing the size and offset of rendered text +have been deprecated and will be removed in Pillow 10 (2023-07-01): + +=========================================================================== ============================================================================================================= +Deprecated Use instead +=========================================================================== ============================================================================================================= +:py:meth:`.FreeTypeFont.getsize` and :py:meth:`.FreeTypeFont.getoffset` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` +:py:meth:`.FreeTypeFont.getsize_multiline` :py:meth:`.ImageDraw.multiline_textbbox` +:py:meth:`.ImageFont.getsize` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` +:py:meth:`.TransposedFont.getsize` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` +:py:meth:`.ImageDraw.textsize` and :py:meth:`.ImageDraw.multiline_textsize` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` +:py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` +=========================================================================== ============================================================================================================= + +Previous code: + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + width, height = font.getsize("Hello world") + left, top = font.getoffset("Hello world") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width, height = draw.textsize("Hello world") + + width, height = font.getsize_multiline("Hello\nworld") + width, height = draw.multiline_textsize("Hello\nworld") + +Use instead: + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + left, top, right, bottom = font.getbbox("Hello world") + width, height = right - left, bottom - top + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width = draw.textlength("Hello world") + + left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld") + width, height = right - left, bottom - top + +API Additions +============= + +Image.apply_transparency +^^^^^^^^^^^^^^^^^^^^^^^^ + +Added :py:meth:`~PIL.Image.Image.apply_transparency`, a method to take a P mode image +with "transparency" in ``im.info``, and apply the transparency to the palette instead. +The image's palette mode will become "RGBA", and "transparency" will be removed from +``im.info``. + +Security +======== + +An additional decompression bomb check has been added for the GIF format. + +Other Changes +============= + +Using gnome-screenshot on Linux +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In :py:meth:`~PIL.ImageGrab.grab` on Linux, if ``xdisplay`` is ``None`` then +``gnome-screenshot`` will be used to capture the display if it is installed. To capture +the default X11 display instead, pass ``xdisplay=""``. diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst new file mode 100644 index 00000000000..fde2faae3a7 --- /dev/null +++ b/docs/releasenotes/9.3.0.rst @@ -0,0 +1,107 @@ +9.3.0 +----- + +API Additions +============= + +Allow default ImageDraw font to be set +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Rather than specifying a font when calling text-related ImageDraw methods, or +setting a font on each ImageDraw instance, the default font can now be set for +all future ImageDraw operations:: + + from PIL import ImageDraw, ImageFont + ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + +Saving multiple MPO frames +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Multiple MPO frames can now be saved. Using the ``save_all`` argument, all of +an image's frames will be saved to file:: + + from PIL import Image + im = Image.open("frozenpond.mpo") + im.save(out, save_all=True) + +Additional images can also be appended when saving, by combining the +``save_all`` argument with the ``append_images`` argument:: + + 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 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Within the BLP image format, BLP1 data may use JPEG compression. Instead of +telling the JPEG library that this data is in BGRX mode, Pillow will now +decode the data in its natural CMYK mode, then convert it to RGB and rearrange +the channels afterwards. Trying to load the data in an incorrect mode could +result in a segmentation fault. This issue was introduced in Pillow 9.1.0. + +Limit SAMPLESPERPIXEL to avoid runtime DOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A large value in the ``SAMPLESPERPIXEL`` tag could lead to a memory and runtime DOS in +``TiffImagePlugin.py`` when setting up the context for image decoding. +This was introduced in Pillow 9.2.0, found with `OSS-Fuzz`_ and fixed by limiting +``SAMPLESPERPIXEL`` to the number of planes that we can decode. + + +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/index.rst b/docs/releasenotes/index.rst index 0ee853fca9d..8c436be3bd8 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -1,11 +1,50 @@ Release Notes ============= +Pillow is released quarterly on January 2nd, April 1st, July 1st and October 15th. +Patch releases are created if the latest release contains severe bugs, or if security +fixes are put together before a scheduled release. See :ref:`versioning` for more +information. + +Please use the latest version of Pillow. Functionality and security fixes should not be +expected to be backported to earlier versions. + .. note:: Contributors please include release notes as needed or appropriate with your bug fixes, feature additions and tests. .. toctree:: :maxdepth: 2 + 9.3.0 + 9.2.0 + 9.1.1 + 9.1.0 + 9.0.1 + 9.0.0 + 8.4.0 + 8.3.2 + 8.3.1 + 8.3.0 + 8.2.0 + 8.1.2 + 8.1.1 + 8.1.0 + 8.0.1 + 8.0.0 + 7.2.0 + 7.1.2 + 7.1.1 + 7.1.0 + 7.0.0 + 6.2.2 + 6.2.1 + 6.2.0 + 6.1.0 + 6.0.0 + 5.4.1 + 5.4.0 + 5.3.0 + 5.2.0 + 5.1.0 5.0.0 4.3.0 4.2.1 @@ -23,3 +62,4 @@ Release Notes 3.0.0 2.8.0 2.7.0 + versioning diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst new file mode 100644 index 00000000000..f7271ae2bf8 --- /dev/null +++ b/docs/releasenotes/template.rst @@ -0,0 +1,48 @@ +x.y.z +----- + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/versioning.rst b/docs/releasenotes/versioning.rst new file mode 100644 index 00000000000..2a0af9e59ec --- /dev/null +++ b/docs/releasenotes/versioning.rst @@ -0,0 +1,30 @@ +.. _versioning: + +Versioning +========== + +Pillow follows `Semantic Versioning `_: + + Given a version number MAJOR.MINOR.PATCH, increment the: + + 1. MAJOR version when you make incompatible API changes, + 2. MINOR version when you add functionality in a backwards compatible manner, and + 3. PATCH version when you make backwards compatible bug fixes. + +Quarterly releases ("`Main Release `_") +bump at least the MINOR version, as new functionality has likely been added in the +prior three months. + +A quarterly release bumps the MAJOR version when incompatible API changes are +made, such as removing deprecated APIs or dropping an EOL Python version. In practice, +these occur every 12-18 months, guided by +`Python's EOL schedule `_, and +any APIs that have been deprecated for at least a year are removed at the same time. + +PATCH versions ("`Point Release `_" +or "`Embargoed Release `_") +are for security, installation or critical bug fixes. These are less common as it is +preferred to stick to quarterly releases. + +Between quarterly releases, ``.dev0`` is appended to the ``main`` branch, indicating that +this is not a formally released copy. diff --git a/docs/resources/anchor_horizontal.svg b/docs/resources/anchor_horizontal.svg new file mode 100644 index 00000000000..a0648a10cb8 --- /dev/null +++ b/docs/resources/anchor_horizontal.svg @@ -0,0 +1,467 @@ + + + + + Pillow horizontal text anchors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Pillow horizontal text anchors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (d) descender + (s) baseline + (a) ascender + (m) middle + (t) top + (b) bottom + (l) left + (r) right + (m) middle + + + Horizontal text + + diff --git a/docs/resources/anchor_vertical.svg b/docs/resources/anchor_vertical.svg new file mode 100644 index 00000000000..95da30ffde2 --- /dev/null +++ b/docs/resources/anchor_vertical.svg @@ -0,0 +1,841 @@ + + + + + Pillow vertical text anchors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Pillow vertical text anchors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (l)left + (s) baseline + (r)right + (t) top + (m) middle + (b) bottom + (m)middle + (l)left + (s) baseline + (r)right + (t) top + (m) middle + (b) bottom + (m)middle + + + Verticaltext + + diff --git a/docs/resources/css/dark.css b/docs/resources/css/dark.css new file mode 100644 index 00000000000..1571cbc4e91 --- /dev/null +++ b/docs/resources/css/dark.css @@ -0,0 +1,9 @@ +/* For black-on-white/transparent images at handbook/text-anchors.html */ +body[data-theme="dark"] #text-anchors img { + filter: invert(1) brightness(0.85) hue-rotate(-60deg); +} +@media (prefers-color-scheme: dark) { + body[data-theme="auto"] #text-anchors img { + filter: invert(1) brightness(0.85) hue-rotate(-60deg); + } +} diff --git a/docs/resources/favicon.ico b/docs/resources/favicon.ico new file mode 100644 index 00000000000..78eef9ae3e0 Binary files /dev/null and b/docs/resources/favicon.ico differ diff --git a/docs/resources/pillow-logo-dark-text.png b/docs/resources/pillow-logo-dark-text.png new file mode 100644 index 00000000000..545104e6b87 Binary files /dev/null and b/docs/resources/pillow-logo-dark-text.png differ diff --git a/docs/resources/pillow-logo.png b/docs/resources/pillow-logo.png new file mode 100644 index 00000000000..1cc2006a6f7 Binary files /dev/null and b/docs/resources/pillow-logo.png differ diff --git a/mp_compile.py b/mp_compile.py deleted file mode 100644 index 5fac2399f1a..00000000000 --- a/mp_compile.py +++ /dev/null @@ -1,80 +0,0 @@ -# A monkey patch of the base distutils.ccompiler to use parallel builds -# Tested on 2.7, looks to be identical to 3.3. - -from __future__ import print_function -from multiprocessing import Pool, cpu_count -from distutils.ccompiler import CCompiler -import os -import sys - -try: - MAX_PROCS = int(os.environ.get('MAX_CONCURRENCY', min(4, cpu_count()))) -except NotImplementedError: - MAX_PROCS = None - - -# hideous monkeypatching. but. but. but. -def _mp_compile_one(tp): - (self, obj, build, cc_args, extra_postargs, pp_opts) = tp - try: - src, ext = build[obj] - except KeyError: - return - self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts) - return - - -def _mp_compile(self, sources, output_dir=None, macros=None, - include_dirs=None, debug=0, extra_preargs=None, - extra_postargs=None, depends=None): - """Compile one or more source files. - - see distutils.ccompiler.CCompiler.compile for comments. - """ - # A concrete compiler class can either override this method - # entirely or implement _compile(). - - macros, objects, extra_postargs, pp_opts, build = self._setup_compile( - output_dir, macros, include_dirs, sources, depends, extra_postargs) - cc_args = self._get_cc_args(pp_opts, debug, extra_preargs) - - pool = Pool(MAX_PROCS) - try: - print("Building using %d processes" % pool._processes) - except: - pass - arr = [(self, obj, build, cc_args, extra_postargs, pp_opts) - for obj in objects] - pool.map_async(_mp_compile_one, arr) - pool.close() - pool.join() - # Return *all* object filenames, not just the ones we just built. - return objects - - -def install(): - - fl_win = sys.platform.startswith('win') - fl_cygwin = sys.platform.startswith('cygwin') - - if fl_win or fl_cygwin: - # Windows barfs on multiprocessing installs - print("Single threaded build for Windows") - return - - if MAX_PROCS != 1: - # explicitly don't enable if environment says 1 processor - try: - # bug, only enable if we can make a Pool. see issue #790 and - # https://stackoverflow.com/questions/6033599/oserror-38-errno-38-with-multiprocessing - Pool(2) - CCompiler.compile = _mp_compile - except Exception as msg: - print("Exception installing mp_compile, proceeding without:" - "%s" % msg) - else: - print("Single threaded build, not installing mp_compile:" - "%s processes" % MAX_PROCS) - - -install() diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 4cb3c090704..00000000000 --- a/requirements.txt +++ /dev/null @@ -1,26 +0,0 @@ -# Development, documentation & testing requirements. --e . -alabaster -Babel -check-manifest -cov-core -coverage -coveralls -docopt -docutils -jarn.viewdoc -Jinja2 -MarkupSafe -olefile -pycodestyle -pytest -pytest-cov -pyflakes -Pygments -pyroma -pytz -requests -six -snowballstemmer -Sphinx -sphinx-rtd-theme diff --git a/selftest.py b/selftest.py index 3f358c583d4..6eeadd1dbc6 100755 --- a/selftest.py +++ b/selftest.py @@ -1,12 +1,9 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # minimal sanity check -from __future__ import print_function import sys -import os -from PIL import Image, ImageDraw, ImageFilter, ImageMath -from PIL import features +from PIL import Image, features try: Image.core.ping @@ -17,16 +14,13 @@ pass -def _info(im): - im.load() - return im.format, im.mode, im.size - - def testimage(): """ PIL lets you create in-memory images with various pixel types: + >>> from PIL import Image, ImageDraw, ImageFilter, ImageMath >>> im = Image.new("1", (128, 128)) # monochrome + >>> def _info(im): return im.format, im.mode, im.size >>> _info(im) (None, '1', (128, 128)) >>> _info(Image.new("L", (128, 128))) # grayscale (luminance) @@ -42,14 +36,14 @@ def testimage(): Or open existing files: - >>> im = Image.open("Tests/images/hopper.gif") - >>> _info(im) + >>> with Image.open("Tests/images/hopper.gif") as im: + ... _info(im) ('GIF', 'P', (128, 128)) >>> _info(Image.open("Tests/images/hopper.ppm")) ('PPM', 'RGB', (128, 128)) >>> try: ... _info(Image.open("Tests/images/hopper.jpg")) - ... except IOError as v: + ... except OSError as v: ... print(v) ('JPEG', 'RGB', (128, 128)) @@ -89,6 +83,8 @@ def testimage(): 2 >>> len(im.histogram()) 768 + >>> '%.7f' % im.entropy() + '8.8212866' >>> _info(im.point(list(range(256))*3)) (None, 'RGB', (128, 128)) >>> _info(im.resize((64, 64))) @@ -101,9 +97,9 @@ def testimage(): 10456 >>> len(im.tobytes()) 49152 - >>> _info(im.transform((512, 512), Image.AFFINE, (1,0,0,0,1,0))) + >>> _info(im.transform((512, 512), Image.Transform.AFFINE, (1,0,0,0,1,0))) (None, 'RGB', (512, 512)) - >>> _info(im.transform((512, 512), Image.EXTENT, (32,32,96,96))) + >>> _info(im.transform((512, 512), Image.Transform.EXTENT, (32,32,96,96))) (None, 'RGB', (512, 512)) The ImageDraw module lets you draw stuff in raster images: @@ -147,9 +143,7 @@ def testimage(): ('F', (128, 128)) PIL can do many other things, but I'll leave that for another - day. If you're curious, check the handbook, available from: - - http://www.pythonware.com + day. Cheers /F """ @@ -160,35 +154,11 @@ def testimage(): exit_status = 0 - print("-"*68) - print("Pillow", Image.PILLOW_VERSION, "TEST SUMMARY ") - print("-"*68) - print("Python modules loaded from", os.path.dirname(Image.__file__)) - print("Binary modules loaded from", os.path.dirname(Image.core.__file__)) - print("-"*68) - for name, feature in [ - ("pil", "PIL CORE"), - ("tkinter", "TKINTER"), - ("freetype2", "FREETYPE2"), - ("littlecms2", "LITTLECMS2"), - ("webp", "WEBP"), - ("transp_webp", "WEBP Transparency"), - ("webp_mux", "WEBPMUX"), - ("webp_anim", "WEBP Animation"), - ("jpg", "JPEG"), - ("jpg_2000", "OPENJPEG (JPEG2000)"), - ("zlib", "ZLIB (PNG/ZIP)"), - ("libtiff", "LIBTIFF"), - ("raqm", "RAQM (Bidirectional Text)") - ]: - if features.check(name): - print("---", feature, "support ok") - else: - print("***", feature, "support not installed") - print("-"*68) + features.pilinfo(sys.stdout, False) # use doctest to make sure the test program behaves as documented! import doctest + print("Running selftest:") status = doctest.testmod(sys.modules[__name__]) if status[0]: diff --git a/setup.cfg b/setup.cfg index 8032517f61d..44feb25ff7f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,73 @@ -[aliases] -test=pytest [metadata] -license_file = LICENSE +name = Pillow +description = Python Imaging Library (Fork) +long_description = file: README.md +long_description_content_type = text/markdown +url = https://python-pillow.org +author = Alex Clark (PIL Fork Author) +author_email = aclark@python-pillow.org +license = HPND +classifiers = + Development Status :: 6 - Mature + License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND) + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + Topic :: Multimedia :: Graphics + Topic :: Multimedia :: Graphics :: Capture :: Digital Camera + Topic :: Multimedia :: Graphics :: Capture :: Screen Capture + Topic :: Multimedia :: Graphics :: Graphics Conversion + Topic :: Multimedia :: Graphics :: Viewers +keywords = Imaging +project_urls = + Documentation=https://pillow.readthedocs.io + Source=https://github.com/python-pillow/Pillow + Funding=https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi + Release notes=https://pillow.readthedocs.io/en/stable/releasenotes/index.html + Changelog=https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst + Twitter=https://twitter.com/PythonPillow + +[options] +packages = PIL +python_requires = >=3.7 +include_package_data = True +package_dir = + = src + +[options.extras_require] +docs = + furo + olefile + sphinx>=2.4 + sphinx-copybutton + sphinx-issues>=3.0.1 + sphinx-removed-in + sphinxext-opengraph +tests = + check-manifest + coverage + defusedxml + markdown2 + olefile + packaging + pyroma + pytest + pytest-cov + pytest-timeout + +[flake8] +extend-ignore = E203 +max-line-length = 88 + +[isort] +profile = black + [tool:pytest] -addopts = -vx Tests +addopts = -ra --color=yes +testpaths = Tests diff --git a/setup.py b/setup.py index 98d482738fa..7a1fabe23f6 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # > pyroma . # ------------------------------ # Checking . @@ -7,49 +7,122 @@ # Final rating: 10/10 # Your cheese is so fresh most people think it's a cream: Mascarpone # ------------------------------ -from __future__ import print_function import os -import platform as plat import re import struct import subprocess import sys import warnings -from distutils import ccompiler, sysconfig -from distutils.command.build_ext import build_ext from setuptools import Extension, setup +from setuptools.command.build_ext import build_ext -# monkey patch import hook. Even though flake8 says it's not used, it is. -# comment this out to disable multi threaded builds. -import mp_compile + +def get_version(): + version_file = "src/PIL/_version.py" + with open(version_file, encoding="utf-8") as f: + exec(compile(f.read(), version_file, "exec")) + return locals()["__version__"] -if sys.platform == "win32" and sys.version_info >= (3, 7): - warnings.warn( - "Pillow does not yet support Python {}.{} and does not yet provide " - "prebuilt Windows binaries. We do not recommend building from " - "source on Windows.".format(sys.version_info.major, - sys.version_info.minor), - RuntimeWarning) +PILLOW_VERSION = get_version() +FREETYPE_ROOT = None +HARFBUZZ_ROOT = None +FRIBIDI_ROOT = None +IMAGEQUANT_ROOT = None +JPEG2K_ROOT = None +JPEG_ROOT = None +LCMS_ROOT = None +TIFF_ROOT = None +ZLIB_ROOT = None +FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ + +if sys.platform == "win32" and sys.version_info >= (3, 12): + import atexit + + atexit.register( + lambda: warnings.warn( + f"Pillow {PILLOW_VERSION} does not support Python " + f"{sys.version_info.major}.{sys.version_info.minor} and does not provide " + "prebuilt Windows binaries. We do not recommend building from source on " + "Windows.", + RuntimeWarning, + ) + ) _IMAGING = ("decode", "encode", "map", "display", "outline", "path") _LIB_IMAGING = ( - "Access", "AlphaComposite", "Resample", "Bands", "BcnDecode", "BitDecode", - "Blend", "Chops", "Convert", "ConvertYCbCr", "Copy", "Crc32", "Crop", - "Dib", "Draw", "Effects", "EpsEncode", "File", "Fill", "Filter", - "FliDecode", "Geometry", "GetBBox", "GifDecode", "GifEncode", "HexDecode", - "Histo", "JpegDecode", "JpegEncode", "Matrix", "ModeFilter", - "Negative", "Offset", "Pack", "PackDecode", "Palette", "Paste", "Quant", - "QuantOctree", "QuantHash", "QuantHeap", "PcdDecode", "PcxDecode", - "PcxEncode", "Point", "RankFilter", "RawDecode", "RawEncode", "Storage", - "SgiRleDecode", "SunRleDecode", "TgaRleDecode", "Unpack", "UnpackYCC", - "UnsharpMask", "XbmDecode", "XbmEncode", "ZipDecode", "ZipEncode", - "TiffDecode", "Jpeg2KDecode", "Jpeg2KEncode", "BoxBlur", "QuantPngQuant", - "codec_fd") + "Access", + "AlphaComposite", + "Resample", + "Reduce", + "Bands", + "BcnDecode", + "BitDecode", + "Blend", + "Chops", + "ColorLUT", + "Convert", + "ConvertYCbCr", + "Copy", + "Crop", + "Dib", + "Draw", + "Effects", + "EpsEncode", + "File", + "Fill", + "Filter", + "FliDecode", + "Geometry", + "GetBBox", + "GifDecode", + "GifEncode", + "HexDecode", + "Histo", + "JpegDecode", + "JpegEncode", + "Matrix", + "ModeFilter", + "Negative", + "Offset", + "Pack", + "PackDecode", + "Palette", + "Paste", + "Quant", + "QuantOctree", + "QuantHash", + "QuantHeap", + "PcdDecode", + "PcxDecode", + "PcxEncode", + "Point", + "RankFilter", + "RawDecode", + "RawEncode", + "Storage", + "SgiRleDecode", + "SunRleDecode", + "TgaRleDecode", + "TgaRleEncode", + "Unpack", + "UnpackYCC", + "UnsharpMask", + "XbmDecode", + "XbmEncode", + "ZipDecode", + "ZipEncode", + "TiffDecode", + "Jpeg2KDecode", + "Jpeg2KEncode", + "BoxBlur", + "QuantPngQuant", + "codec_fd", +) DEBUG = False @@ -62,8 +135,8 @@ class RequiredDependencyException(Exception): pass -PLATFORM_MINGW = 'mingw' in ccompiler.get_default_compiler() -PLATFORM_PYPY = hasattr(sys, 'pypy_version_info') +PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version +PLATFORM_PYPY = hasattr(sys, "pypy_version_info") def _dbg(s, tp=None): @@ -74,16 +147,64 @@ def _dbg(s, tp=None): print(s) +def _find_library_dirs_ldconfig(): + # Based on ctypes.util from Python 2 + + if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): + if struct.calcsize("l") == 4: + machine = os.uname()[4] + "-32" + else: + machine = os.uname()[4] + "-64" + mach_map = { + "x86_64-64": "libc6,x86-64", + "ppc64-64": "libc6,64bit", + "sparc64-64": "libc6,64bit", + "s390x-64": "libc6,64bit", + "ia64-64": "libc6,IA-64", + } + abi_type = mach_map.get(machine, "libc6") + + # Assuming GLIBC's ldconfig (with option -p) + # Alpine Linux uses musl that can't print cache + args = ["/sbin/ldconfig", "-p"] + expr = rf".*\({abi_type}.*\) => (.*)" + env = dict(os.environ) + env["LC_ALL"] = "C" + env["LANG"] = "C" + + elif sys.platform.startswith("freebsd"): + args = ["/sbin/ldconfig", "-r"] + expr = r".* => (.*)" + env = {} + + try: + p = subprocess.Popen( + args, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, env=env + ) + except OSError: # E.g. command not found + return [] + [data, _] = p.communicate() + if isinstance(data, bytes): + data = data.decode("latin1") + + dirs = [] + for dll in re.findall(expr, data): + dir = os.path.dirname(dll) + if dir not in dirs: + dirs.append(dir) + return dirs + + def _add_directory(path, subdir, where=None): if subdir is None: return subdir = os.path.realpath(subdir) if os.path.isdir(subdir) and subdir not in path: if where is None: - _dbg('Appending path %s', subdir) + _dbg("Appending path %s", subdir) path.append(subdir) else: - _dbg('Inserting path %s', subdir) + _dbg("Inserting path %s", subdir) path.insert(where, subdir) elif subdir in path and where is not None: path.remove(subdir) @@ -92,9 +213,9 @@ def _add_directory(path, subdir, where=None): def _find_include_file(self, include): for directory in self.compiler.include_dirs: - _dbg('Checking for include file %s in %s', (include, directory)) + _dbg("Checking for include file %s in %s", (include, directory)) if os.path.isfile(os.path.join(directory, include)): - _dbg('Found %s', include) + _dbg("Found %s", include) return 1 return 0 @@ -102,13 +223,25 @@ def _find_include_file(self, include): def _find_library_file(self, library): ret = self.compiler.find_library_file(self.compiler.library_dirs, library) if ret: - _dbg('Found library %s at %s', (library, ret)) + _dbg("Found library %s at %s", (library, ret)) else: - _dbg("Couldn't find library %s in %s", - (library, self.compiler.library_dirs)) + _dbg("Couldn't find library %s in %s", (library, self.compiler.library_dirs)) return ret +def _find_include_dir(self, dirname, include): + for directory in self.compiler.include_dirs: + _dbg("Checking for include file %s in %s", (include, directory)) + if os.path.isfile(os.path.join(directory, include)): + _dbg("Found %s in %s", (include, directory)) + return True + subdir = os.path.join(directory, dirname) + _dbg("Checking for include file %s in %s", (include, subdir)) + if os.path.isfile(os.path.join(subdir, include)): + _dbg("Found %s in %s", (include, subdir)) + return subdir + + def _cmd_exists(cmd): return any( os.access(os.path.join(path, cmd), os.X_OK) @@ -116,56 +249,55 @@ def _cmd_exists(cmd): ) -def _read(file): - with open(file, 'rb') as fp: - return fp.read() - - -def get_version(): - version_file = 'src/PIL/version.py' - with open(version_file, 'r') as f: - exec(compile(f.read(), version_file, 'exec')) - return locals()['__version__'] - - -try: - import _tkinter -except (ImportError, OSError): - # pypy emits an oserror - _tkinter = None - -NAME = 'Pillow' -PILLOW_VERSION = get_version() -JPEG_ROOT = None -JPEG2K_ROOT = None -ZLIB_ROOT = None -IMAGEQUANT_ROOT = None -TIFF_ROOT = None -FREETYPE_ROOT = None -LCMS_ROOT = None - - def _pkg_config(name): - try: - command = [ - 'pkg-config', - '--libs-only-L', name, - '--cflags-only-I', name, - ] - if not DEBUG: - command.append('--silence-errors') - libs = subprocess.check_output(command).decode('utf8').split(' ') - return libs[1][2:].strip(), libs[0][2:].strip() - except Exception: - pass + command = os.environ.get("PKG_CONFIG", "pkg-config") + for keep_system in (True, False): + try: + command_libs = [command, "--libs-only-L", name] + command_cflags = [command, "--cflags-only-I", name] + stderr = None + if keep_system: + command_libs.append("--keep-system-libs") + command_cflags.append("--keep-system-cflags") + stderr = subprocess.DEVNULL + if not DEBUG: + command_libs.append("--silence-errors") + command_cflags.append("--silence-errors") + libs = ( + subprocess.check_output(command_libs, stderr=stderr) + .decode("utf8") + .strip() + .replace("-L", "") + ) + cflags = ( + subprocess.check_output(command_cflags) + .decode("utf8") + .strip() + .replace("-I", "") + ) + return libs, cflags + except Exception: + pass class pil_build_ext(build_ext): class feature: - features = ['zlib', 'jpeg', 'tiff', 'freetype', 'lcms', 'webp', - 'webpmux', 'jpeg2000', 'imagequant'] + features = [ + "zlib", + "jpeg", + "tiff", + "freetype", + "raqm", + "lcms", + "webp", + "webpmux", + "jpeg2000", + "imagequant", + "xcb", + ] - required = {'jpeg', 'zlib'} + required = {"jpeg", "zlib"} + vendor = set() def __init__(self): for f in self.features: @@ -177,78 +309,165 @@ def require(self, feat): def want(self, feat): return getattr(self, feat) is None + def want_vendor(self, feat): + return feat in self.vendor + def __iter__(self): - for x in self.features: - yield x + yield from self.features feature = feature() - user_options = build_ext.user_options + [ - ('disable-%s' % x, None, 'Disable support for %s' % x) for x in feature - ] + [ - ('enable-%s' % x, None, 'Enable support for %s' % x) for x in feature - ] + [ - ('disable-platform-guessing', None, - 'Disable platform guessing on Linux'), - ('debug', None, 'Debug logging') - ] + user_options = ( + build_ext.user_options + + [(f"disable-{x}", None, f"Disable support for {x}") for x in feature] + + [(f"enable-{x}", None, f"Enable support for {x}") for x in feature] + + [ + (f"vendor-{x}", None, f"Use vendored version of {x}") + for x in ("raqm", "fribidi") + ] + + [ + ("disable-platform-guessing", None, "Disable platform guessing on Linux"), + ("debug", None, "Debug logging"), + ] + + [("add-imaging-libs=", None, "Add libs to _imaging build")] + ) def initialize_options(self): self.disable_platform_guessing = None + self.add_imaging_libs = "" build_ext.initialize_options(self) for x in self.feature: - setattr(self, 'disable_%s' % x, None) - setattr(self, 'enable_%s' % x, None) + setattr(self, f"disable_{x}", None) + setattr(self, f"enable_{x}", None) + for x in ("raqm", "fribidi"): + setattr(self, f"vendor_{x}", None) def finalize_options(self): build_ext.finalize_options(self) if self.debug: global DEBUG DEBUG = True + if not self.parallel: + # If --parallel (or -j) wasn't specified, we want to reproduce the same + # behavior as before, that is, auto-detect the number of jobs. + try: + self.parallel = int( + os.environ.get("MAX_CONCURRENCY", min(4, os.cpu_count())) + ) + except TypeError: + self.parallel = None for x in self.feature: - if getattr(self, 'disable_%s' % x): + if getattr(self, f"disable_{x}"): setattr(self.feature, x, False) self.feature.required.discard(x) - _dbg('Disabling %s', x) - if getattr(self, 'enable_%s' % x): + _dbg("Disabling %s", x) + if getattr(self, f"enable_{x}"): raise ValueError( - 'Conflicting options: --enable-%s and --disable-%s' % - (x, x)) - if getattr(self, 'enable_%s' % x): - _dbg('Requiring %s', x) + f"Conflicting options: --enable-{x} and --disable-{x}" + ) + if x == "freetype": + _dbg("--disable-freetype implies --disable-raqm") + if getattr(self, "enable_raqm"): + raise ValueError( + "Conflicting options: --enable-raqm and --disable-freetype" + ) + setattr(self, "disable_raqm", True) + if getattr(self, f"enable_{x}"): + _dbg("Requiring %s", x) self.feature.required.add(x) + if x == "raqm": + _dbg("--enable-raqm implies --enable-freetype") + self.feature.required.add("freetype") + 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" + ) + if x == "fribidi" and not getattr(self, "vendor_raqm"): + raise ValueError( + f"Conflicting options: --vendor-{x} and not --vendor-raqm" + ) + _dbg("Using vendored version of %s", x) + self.feature.vendor.add(x) + + def _update_extension(self, name, libraries, define_macros=None, sources=None): + for extension in self.extensions: + if extension.name == name: + extension.libraries += libraries + if define_macros is not None: + extension.define_macros += define_macros + if sources is not None: + extension.sources += sources + if FUZZING_BUILD: + extension.language = "c++" + extension.extra_link_args = ["--stdlib=libc++"] + break + + def _remove_extension(self, name): + for extension in self.extensions: + if extension.name == name: + self.extensions.remove(extension) + break + + def get_macos_sdk_path(self): + try: + sdk_path = ( + subprocess.check_output(["xcrun", "--show-sdk-path"]) + .strip() + .decode("latin1") + ) + except Exception: + sdk_path = None + if ( + not sdk_path + or sdk_path == "/Applications/Xcode.app/Contents/Developer" + "/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk" + ): + commandlinetools_sdk_path = ( + "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk" + ) + if os.path.exists(commandlinetools_sdk_path): + sdk_path = commandlinetools_sdk_path + return sdk_path def build_extensions(self): library_dirs = [] include_dirs = [] - _add_directory(include_dirs, "src/libImaging") - pkg_config = None - if _cmd_exists('pkg-config'): + if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")): pkg_config = _pkg_config # # add configured kits - for root_name, lib_name in dict(JPEG_ROOT="libjpeg", - JPEG2K_ROOT="libopenjp2", - TIFF_ROOT=("libtiff-5", "libtiff-4"), - ZLIB_ROOT="zlib", - FREETYPE_ROOT="freetype2", - LCMS_ROOT="lcms2", - IMAGEQUANT_ROOT="libimagequant" - ).items(): + for root_name, lib_name in dict( + JPEG_ROOT="libjpeg", + JPEG2K_ROOT="libopenjp2", + TIFF_ROOT=("libtiff-5", "libtiff-4"), + ZLIB_ROOT="zlib", + FREETYPE_ROOT="freetype2", + HARFBUZZ_ROOT="harfbuzz", + FRIBIDI_ROOT="fribidi", + LCMS_ROOT="lcms2", + IMAGEQUANT_ROOT="libimagequant", + ).items(): root = globals()[root_name] + + if root is None and root_name in os.environ: + prefix = os.environ[root_name] + root = (os.path.join(prefix, "lib"), os.path.join(prefix, "include")) + if root is None and pkg_config: if isinstance(lib_name, tuple): for lib_name2 in lib_name: - _dbg('Looking for `%s` using pkg-config.' % lib_name2) + _dbg(f"Looking for `{lib_name2}` using pkg-config.") root = pkg_config(lib_name2) if root: break else: - _dbg('Looking for `%s` using pkg-config.' % lib_name) + _dbg(f"Looking for `{lib_name}` using pkg-config.") root = pkg_config(lib_name) if isinstance(root, tuple): @@ -259,29 +478,27 @@ def build_extensions(self): _add_directory(library_dirs, lib_root) _add_directory(include_dirs, include_root) - # respect CFLAGS/LDFLAGS - for k in ('CFLAGS', 'LDFLAGS'): + # respect CFLAGS/CPPFLAGS/LDFLAGS + for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"): if k in os.environ: - for match in re.finditer(r'-I([^\s]+)', os.environ[k]): + for match in re.finditer(r"-I([^\s]+)", os.environ[k]): _add_directory(include_dirs, match.group(1)) - for match in re.finditer(r'-L([^\s]+)', os.environ[k]): + for match in re.finditer(r"-L([^\s]+)", os.environ[k]): _add_directory(library_dirs, match.group(1)) # include, rpath, if set as environment variables: - for k in ('C_INCLUDE_PATH', 'CPATH', 'INCLUDE'): + for k in ("C_INCLUDE_PATH", "CPATH", "INCLUDE"): if k in os.environ: for d in os.environ[k].split(os.path.pathsep): _add_directory(include_dirs, d) - for k in ('LD_RUN_PATH', 'LIBRARY_PATH', 'LIB'): + for k in ("LD_RUN_PATH", "LIBRARY_PATH", "LIB"): if k in os.environ: for d in os.environ[k].split(os.path.pathsep): _add_directory(library_dirs, d) - prefix = sysconfig.get_config_var("prefix") - if prefix: - _add_directory(library_dirs, os.path.join(prefix, "lib")) - _add_directory(include_dirs, os.path.join(prefix, "include")) + _add_directory(library_dirs, os.path.join(sys.prefix, "lib")) + _add_directory(include_dirs, os.path.join(sys.prefix, "include")) # # add platform directories @@ -291,9 +508,12 @@ def build_extensions(self): elif sys.platform == "cygwin": # pythonX.Y.dll.a is in the /usr/lib/pythonX.Y/config directory - _add_directory(library_dirs, - os.path.join("/usr/lib", "python%s" % - sys.version[:3], "config")) + _add_directory( + library_dirs, + os.path.join( + "/usr/lib", "python{}.{}".format(*sys.version_info), "config" + ), + ) elif sys.platform == "darwin": # attempt to make sure we pick freetype2 over other versions @@ -308,8 +528,11 @@ def build_extensions(self): # if Homebrew is installed, use its lib and include directories try: - prefix = subprocess.check_output(['brew', '--prefix']).strip( - ).decode('latin1') + prefix = ( + subprocess.check_output(["brew", "--prefix"]) + .strip() + .decode("latin1") + ) except Exception: # Homebrew not installed prefix = None @@ -318,81 +541,48 @@ def build_extensions(self): if prefix: # add Homebrew's include and lib directories - _add_directory(library_dirs, os.path.join(prefix, 'lib')) - _add_directory(include_dirs, os.path.join(prefix, 'include')) - ft_prefix = os.path.join(prefix, 'opt', 'freetype') + _add_directory(library_dirs, os.path.join(prefix, "lib")) + _add_directory(include_dirs, os.path.join(prefix, "include")) + _add_directory( + include_dirs, os.path.join(prefix, "opt", "zlib", "include") + ) + ft_prefix = os.path.join(prefix, "opt", "freetype") if ft_prefix and os.path.isdir(ft_prefix): # freetype might not be linked into Homebrew's prefix - _add_directory(library_dirs, os.path.join(ft_prefix, 'lib')) - _add_directory(include_dirs, - os.path.join(ft_prefix, 'include')) + _add_directory(library_dirs, os.path.join(ft_prefix, "lib")) + _add_directory(include_dirs, os.path.join(ft_prefix, "include")) else: # fall back to freetype from XQuartz if # Homebrew's freetype is missing _add_directory(library_dirs, "/usr/X11/lib") _add_directory(include_dirs, "/usr/X11/include") - elif sys.platform.startswith("linux"): - arch_tp = (plat.processor(), plat.architecture()[0]) - # This should be correct on debian derivatives. - if os.path.exists('/etc/debian_version'): - # If this doesn't work, don't just silently patch - # downstream because it's going to break when people - # try to build pillow from source instead of - # installing from the system packages. - self.add_multiarch_paths() - - elif arch_tp == ("x86_64", "32bit"): - # Special Case: 32-bit build on 64-bit machine. - _add_directory(library_dirs, "/usr/lib/i386-linux-gnu") - else: - libdirs = { - 'x86_64': ["/lib64", "/usr/lib64", - "/usr/lib/x86_64-linux-gnu"], - '64bit': ["/lib64", "/usr/lib64", - "/usr/lib/x86_64-linux-gnu"], - 'i386': ["/usr/lib/i386-linux-gnu"], - 'i686': ["/usr/lib/i386-linux-gnu"], - '32bit': ["/usr/lib/i386-linux-gnu"], - 'aarch64': ["/usr/lib64", "/usr/lib/aarch64-linux-gnu"], - 'arm': ["/usr/lib/arm-linux-gnueabi"], - 'armv71': ["/usr/lib/arm-linux-gnueabi"], - 'ppc64': ["/usr/lib64", "/usr/lib/ppc64-linux-gnu", - "/usr/lib/powerpc64-linux-gnu"], - 'ppc': ["/usr/lib/ppc-linux-gnu", - "/usr/lib/powerpc-linux-gnu"], - 's390x': ["/usr/lib64", "/usr/lib/s390x-linux-gnu"], - 's390': ["/usr/lib/s390-linux-gnu"], - } - - for platform_ in arch_tp: - dirs = libdirs.get(platform_, None) - if not platform_: - continue - for path in dirs: - _add_directory(library_dirs, path) - break - - else: - raise ValueError( - "Unable to identify Linux platform: `%s`" % platform_) - + sdk_path = self.get_macos_sdk_path() + if sdk_path: + _add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib")) + _add_directory(include_dirs, os.path.join(sdk_path, "usr", "include")) + elif ( + sys.platform.startswith("linux") + or sys.platform.startswith("gnu") + or sys.platform.startswith("freebsd") + ): + for dirname in _find_library_dirs_ldconfig(): + _add_directory(library_dirs, dirname) + if sys.platform.startswith("linux") and os.environ.get( + "ANDROID_ROOT", None + ): # termux support for android. # system libraries (zlib) are installed in /system/lib # headers are at $PREFIX/include # user libs are at $PREFIX/lib - if os.environ.get('ANDROID_ROOT', None): - _add_directory(library_dirs, - os.path.join(os.environ['ANDROID_ROOT'], - 'lib')) - - elif sys.platform.startswith("gnu"): - self.add_multiarch_paths() - - elif sys.platform.startswith("freebsd"): - _add_directory(library_dirs, "/usr/local/lib") - _add_directory(include_dirs, "/usr/local/include") + _add_directory( + library_dirs, + os.path.join( + os.environ["ANDROID_ROOT"], + "lib" if struct.calcsize("l") == 4 else "lib64", + ), + ) elif sys.platform.startswith("netbsd"): _add_directory(library_dirs, "/usr/pkg/lib") @@ -414,24 +604,23 @@ def build_extensions(self): # alpine, at least _add_directory(library_dirs, "/lib") - # on Windows, look for the OpenJPEG libraries in the location that - # the official installer puts them if sys.platform == "win32": - program_files = os.environ.get('ProgramFiles', '') + # on Windows, look for the OpenJPEG libraries in the location that + # the official installer puts them + program_files = os.environ.get("ProgramFiles", "") best_version = (0, 0) best_path = None for name in os.listdir(program_files): - if name.startswith('OpenJPEG '): - version = tuple(int(x) for x in name[9:].strip().split('.')) + if name.startswith("OpenJPEG "): + version = tuple(int(x) for x in name[9:].strip().split(".")) if version > best_version: best_version = version best_path = os.path.join(program_files, name) if best_path: - _dbg('Adding %s to search list', best_path) - _add_directory(library_dirs, os.path.join(best_path, 'lib')) - _add_directory(include_dirs, - os.path.join(best_path, 'include')) + _dbg("Adding %s to search list", best_path) + _add_directory(library_dirs, os.path.join(best_path, "lib")) + _add_directory(include_dirs, os.path.join(best_path, "include")) # # insert new dirs *before* default libs, to avoid conflicts @@ -445,51 +634,51 @@ def build_extensions(self): feature = self.feature - if feature.want('zlib'): - _dbg('Looking for zlib') + if feature.want("zlib"): + _dbg("Looking for zlib") if _find_include_file(self, "zlib.h"): if _find_library_file(self, "z"): feature.zlib = "z" - elif (sys.platform == "win32" and - _find_library_file(self, "zlib")): + elif sys.platform == "win32" and _find_library_file(self, "zlib"): feature.zlib = "zlib" # alternative name - if feature.want('jpeg'): - _dbg('Looking for jpeg') + if feature.want("jpeg"): + _dbg("Looking for jpeg") if _find_include_file(self, "jpeglib.h"): if _find_library_file(self, "jpeg"): feature.jpeg = "jpeg" - elif (sys.platform == "win32" and - _find_library_file(self, "libjpeg")): + elif sys.platform == "win32" and _find_library_file(self, "libjpeg"): feature.jpeg = "libjpeg" # alternative name feature.openjpeg_version = None - if feature.want('jpeg2000'): - _dbg('Looking for jpeg2000') + if feature.want("jpeg2000"): + _dbg("Looking for jpeg2000") best_version = None best_path = None # Find the best version for directory in self.compiler.include_dirs: - _dbg('Checking for openjpeg-#.# in %s', directory) + _dbg("Checking for openjpeg-#.# in %s", directory) try: listdir = os.listdir(directory) except Exception: - # WindowsError, FileNotFoundError + # OSError, FileNotFoundError continue for name in listdir: - if name.startswith('openjpeg-') and \ - os.path.isfile(os.path.join(directory, name, - 'openjpeg.h')): - _dbg('Found openjpeg.h in %s/%s', (directory, name)) - version = tuple(int(x) for x in name[9:].split('.')) + if name.startswith("openjpeg-") and os.path.isfile( + os.path.join(directory, name, "openjpeg.h") + ): + _dbg("Found openjpeg.h in %s/%s", (directory, name)) + version = tuple(int(x) for x in name[9:].split(".")) if best_version is None or version > best_version: best_version = version best_path = os.path.join(directory, name) - _dbg('Best openjpeg version %s so far in %s', - (best_version, best_path)) + _dbg( + "Best openjpeg version %s so far in %s", + (best_version, best_path), + ) - if best_version and _find_library_file(self, 'openjp2'): + if best_version and _find_library_file(self, "openjp2"): # Add the directory to the include path so we can include # rather than having to cope with the versioned # include path @@ -498,54 +687,85 @@ def build_extensions(self): # self.compiler.include_dirs. Should investigate how that is # possible. _add_directory(self.compiler.include_dirs, best_path, 0) - feature.jpeg2000 = 'openjp2' - feature.openjpeg_version = '.'.join(str(x) for x in best_version) + feature.jpeg2000 = "openjp2" + feature.openjpeg_version = ".".join(str(x) for x in best_version) - if feature.want('imagequant'): - _dbg('Looking for imagequant') - if _find_include_file(self, 'libimagequant.h'): + if feature.want("imagequant"): + _dbg("Looking for imagequant") + if _find_include_file(self, "libimagequant.h"): if _find_library_file(self, "imagequant"): feature.imagequant = "imagequant" elif _find_library_file(self, "libimagequant"): feature.imagequant = "libimagequant" - if feature.want('tiff'): - _dbg('Looking for tiff') - if _find_include_file(self, 'tiff.h'): + if feature.want("tiff"): + _dbg("Looking for tiff") + if _find_include_file(self, "tiff.h"): if _find_library_file(self, "tiff"): feature.tiff = "tiff" - if sys.platform == "win32" and _find_library_file(self, "libtiff"): - feature.tiff = "libtiff" - if (sys.platform == "darwin" and - _find_library_file(self, "libtiff")): + if sys.platform in ["win32", "darwin"] and _find_library_file( + self, "libtiff" + ): feature.tiff = "libtiff" - if feature.want('freetype'): - _dbg('Looking for freetype') + if feature.want("freetype"): + _dbg("Looking for freetype") if _find_library_file(self, "freetype"): # look for freetype2 include files freetype_version = 0 for subdir in self.compiler.include_dirs: - _dbg('Checking for include file %s in %s', ("ft2build.h", subdir)) + _dbg("Checking for include file %s in %s", ("ft2build.h", subdir)) if os.path.isfile(os.path.join(subdir, "ft2build.h")): - _dbg('Found %s in %s', ("ft2build.h", subdir)) + _dbg("Found %s in %s", ("ft2build.h", subdir)) freetype_version = 21 subdir = os.path.join(subdir, "freetype2") break subdir = os.path.join(subdir, "freetype2") - _dbg('Checking for include file %s in %s', ("ft2build.h", subdir)) + _dbg("Checking for include file %s in %s", ("ft2build.h", subdir)) if os.path.isfile(os.path.join(subdir, "ft2build.h")): - _dbg('Found %s in %s', ("ft2build.h", subdir)) + _dbg("Found %s in %s", ("ft2build.h", subdir)) freetype_version = 21 break if freetype_version: feature.freetype = "freetype" - feature.freetype_version = freetype_version if subdir: _add_directory(self.compiler.include_dirs, subdir, 0) - if feature.want('lcms'): - _dbg('Looking for lcms') + if feature.freetype and feature.want("raqm"): + if not feature.want_vendor("raqm"): # want system Raqm + _dbg("Looking for Raqm") + if _find_include_file(self, "raqm.h"): + if _find_library_file(self, "raqm"): + feature.raqm = "raqm" + elif _find_library_file(self, "libraqm"): + feature.raqm = "libraqm" + else: # want to build Raqm from src/thirdparty + _dbg("Looking for HarfBuzz") + feature.harfbuzz = None + hb_dir = _find_include_dir(self, "harfbuzz", "hb.h") + if hb_dir: + if isinstance(hb_dir, str): + _add_directory(self.compiler.include_dirs, hb_dir, 0) + if _find_library_file(self, "harfbuzz"): + feature.harfbuzz = "harfbuzz" + if feature.harfbuzz: + if not feature.want_vendor("fribidi"): # want system FriBiDi + _dbg("Looking for FriBiDi") + feature.fribidi = None + fribidi_dir = _find_include_dir(self, "fribidi", "fribidi.h") + if fribidi_dir: + if isinstance(fribidi_dir, str): + _add_directory( + self.compiler.include_dirs, fribidi_dir, 0 + ) + if _find_library_file(self, "fribidi"): + feature.fribidi = "fribidi" + feature.raqm = True + else: # want to build FriBiDi shim from src/thirdparty + feature.raqm = True + + if feature.want("lcms"): + _dbg("Looking for lcms") if _find_include_file(self, "lcms2.h"): if _find_library_file(self, "lcms2"): feature.lcms = "lcms2" @@ -553,43 +773,47 @@ def build_extensions(self): # alternate Windows name. feature.lcms = "lcms2_static" - if feature.want('webp'): - _dbg('Looking for webp') - if (_find_include_file(self, "webp/encode.h") and - _find_include_file(self, "webp/decode.h")): + if feature.want("webp"): + _dbg("Looking for webp") + if _find_include_file(self, "webp/encode.h") and _find_include_file( + self, "webp/decode.h" + ): # In Google's precompiled zip it is call "libwebp": if _find_library_file(self, "webp"): feature.webp = "webp" elif _find_library_file(self, "libwebp"): feature.webp = "libwebp" - if feature.want('webpmux'): - _dbg('Looking for webpmux') - if (_find_include_file(self, "webp/mux.h") and - _find_include_file(self, "webp/demux.h")): - if (_find_library_file(self, "webpmux") and - _find_library_file(self, "webpdemux")): + if feature.want("webpmux"): + _dbg("Looking for webpmux") + if _find_include_file(self, "webp/mux.h") and _find_include_file( + self, "webp/demux.h" + ): + if _find_library_file(self, "webpmux") and _find_library_file( + self, "webpdemux" + ): feature.webpmux = "webpmux" - if (_find_library_file(self, "libwebpmux") and - _find_library_file(self, "libwebpdemux")): + if _find_library_file(self, "libwebpmux") and _find_library_file( + self, "libwebpdemux" + ): feature.webpmux = "libwebpmux" + if feature.want("xcb"): + _dbg("Looking for xcb") + if _find_include_file(self, "xcb/xcb.h"): + if _find_library_file(self, "xcb"): + feature.xcb = "xcb" + for f in feature: if not getattr(feature, f) and feature.require(f): - if f in ('jpeg', 'zlib'): + if f in ("jpeg", "zlib"): raise RequiredDependencyException(f) raise DependencyException(f) # # core library - files = ["src/_imaging.c"] - for src_file in _IMAGING: - files.append("src/" + src_file + ".c") - for src_file in _LIB_IMAGING: - files.append(os.path.join("src/libImaging", src_file + ".c")) - - libs = [] + libs = self.add_imaging_libs.split() defs = [] if feature.jpeg: libs.append(feature.jpeg) @@ -597,7 +821,7 @@ def build_extensions(self): if feature.jpeg2000: libs.append(feature.jpeg2000) defs.append(("HAVE_OPENJPEG", None)) - if sys.platform == "win32": + if sys.platform == "win32" and not PLATFORM_MINGW: defs.append(("OPJ_STATIC", None)) if feature.zlib: libs.append(feature.zlib) @@ -608,38 +832,64 @@ def build_extensions(self): if feature.tiff: libs.append(feature.tiff) defs.append(("HAVE_LIBTIFF", None)) + if sys.platform == "win32": + # This define needs to be defined if-and-only-if it was defined + # when compiling LibTIFF. LibTIFF doesn't expose it in `tiffconf.h`, + # so we have to guess; by default it is defined in all Windows builds. + # See #4237, #5243, #5359 for more information. + defs.append(("USE_WIN32_FILEIO", None)) + if feature.xcb: + libs.append(feature.xcb) + defs.append(("HAVE_XCB", None)) if sys.platform == "win32": libs.extend(["kernel32", "user32", "gdi32"]) - if struct.unpack("h", "\0\1".encode('ascii'))[0] == 1: + if struct.unpack("h", b"\0\1")[0] == 1: defs.append(("WORDS_BIGENDIAN", None)) - if sys.platform == "win32" and not (PLATFORM_PYPY or PLATFORM_MINGW): - defs.append(("PILLOW_VERSION", '"\\"%s\\""' % PILLOW_VERSION)) + if ( + sys.platform == "win32" + and sys.version_info < (3, 9) + and not (PLATFORM_PYPY or PLATFORM_MINGW) + ): + defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""')) else: - defs.append(("PILLOW_VERSION", '"%s"' % PILLOW_VERSION)) + defs.append(("PILLOW_VERSION", f'"{PILLOW_VERSION}"')) - exts = [(Extension("PIL._imaging", - files, - libraries=libs, - define_macros=defs))] + self._update_extension("PIL._imaging", libs, defs) # # additional libraries if feature.freetype: + srcs = [] libs = ["freetype"] defs = [] - exts.append(Extension( - "PIL._imagingft", ["src/_imagingft.c"], libraries=libs, - define_macros=defs)) + if feature.raqm: + if not feature.want_vendor("raqm"): # using system Raqm + defs.append(("HAVE_RAQM", None)) + defs.append(("HAVE_RAQM_SYSTEM", None)) + libs.append(feature.raqm) + else: # building Raqm from src/thirdparty + defs.append(("HAVE_RAQM", None)) + srcs.append("src/thirdparty/raqm/raqm.c") + libs.append(feature.harfbuzz) + if not feature.want_vendor("fribidi"): # using system FriBiDi + defs.append(("HAVE_FRIBIDI_SYSTEM", None)) + libs.append(feature.fribidi) + else: # building FriBiDi shim from src/thirdparty + srcs.append("src/thirdparty/fribidi-shim/fribidi.c") + self._update_extension("PIL._imagingft", libs, defs, srcs) + + else: + self._remove_extension("PIL._imagingft") if feature.lcms: extra = [] if sys.platform == "win32": extra.extend(["user32", "gdi32"]) - exts.append(Extension("PIL._imagingcms", - ["src/_imagingcms.c"], - libraries=[feature.lcms] + extra)) + self._update_extension("PIL._imagingcms", [feature.lcms] + extra) + else: + self._remove_extension("PIL._imagingcms") if feature.webp: libs = [feature.webp] @@ -648,23 +898,14 @@ def build_extensions(self): if feature.webpmux: defs.append(("HAVE_WEBPMUX", None)) libs.append(feature.webpmux) - libs.append(feature.webpmux.replace('pmux', 'pdemux')) - - exts.append(Extension("PIL._webp", - ["src/_webp.c"], - libraries=libs, - define_macros=defs)) + libs.append(feature.webpmux.replace("pmux", "pdemux")) - tk_libs = ['psapi'] if sys.platform == 'win32' else [] - exts.append(Extension("PIL._imagingtk", - ["src/_imagingtk.c", "Tk/tkImaging.c"], - include_dirs=['Tk'], - libraries=tk_libs)) - - exts.append(Extension("PIL._imagingmath", ["src/_imagingmath.c"])) - exts.append(Extension("PIL._imagingmorph", ["src/_imagingmorph.c"])) + self._update_extension("PIL._webp", libs, defs) + else: + self._remove_extension("PIL._webp") - self.extensions[:] = exts + tk_libs = ["psapi"] if sys.platform in ("win32", "cygwin") else [] + self._update_extension("PIL._imagingtk", tk_libs) build_ext.build_extensions(self) @@ -678,35 +919,42 @@ def summary_report(self, feature): print("-" * 68) print("PIL SETUP SUMMARY") print("-" * 68) - print("version Pillow %s" % PILLOW_VERSION) + print(f"version Pillow {PILLOW_VERSION}") v = sys.version.split("[") - print("platform %s %s" % (sys.platform, v[0].strip())) + print(f"platform {sys.platform} {v[0].strip()}") for v in v[1:]: - print(" [%s" % v.strip()) + print(f" [{v.strip()}") print("-" * 68) + raqm_extra_info = "" + if feature.want_vendor("raqm"): + raqm_extra_info += "bundled" + if feature.want_vendor("fribidi"): + raqm_extra_info += ", FriBiDi shim" + options = [ (feature.jpeg, "JPEG"), - (feature.jpeg2000, "OPENJPEG (JPEG2000)", - feature.openjpeg_version), + (feature.jpeg2000, "OPENJPEG (JPEG2000)", feature.openjpeg_version), (feature.zlib, "ZLIB (PNG/ZIP)"), (feature.imagequant, "LIBIMAGEQUANT"), (feature.tiff, "LIBTIFF"), (feature.freetype, "FREETYPE2"), + (feature.raqm, "RAQM (Text shaping)", raqm_extra_info), (feature.lcms, "LITTLECMS2"), (feature.webp, "WEBP"), (feature.webpmux, "WEBPMUX"), + (feature.xcb, "XCB (X protocol)"), ] all = 1 for option in options: if option[0]: - version = '' + extra_info = "" if len(option) >= 3 and option[2]: - version = ' (%s)' % option[2] - print("--- %s support available%s" % (option[1], version)) + extra_info = f" ({option[2]})" + print(f"--- {option[1]} support available{extra_info}") else: - print("*** %s support not available" % option[1]) + print(f"*** {option[1]} support not available") all = 0 print("-" * 68) @@ -714,100 +962,60 @@ def summary_report(self, feature): if not all: print("To add a missing option, make sure you have the required") print("library and headers.") - print("See https://pillow.readthedocs.io/en/latest/installation." - "html#building-from-source") + print( + "See https://pillow.readthedocs.io/en/latest/installation." + "html#building-from-source" + ) print("") print("To check the build, run the selftest.py script.") print("") - # https://hg.python.org/users/barry/rev/7e8deab93d5a - def add_multiarch_paths(self): - # Debian/Ubuntu multiarch support. - # https://wiki.ubuntu.com/MultiarchSpec - # self.build_temp - tmpfile = os.path.join(self.build_temp, 'multiarch') - if not os.path.exists(self.build_temp): - os.makedirs(self.build_temp) - with open(tmpfile, 'wb') as fp: - try: - ret = subprocess.call(['dpkg-architecture', - '-qDEB_HOST_MULTIARCH'], stdout=fp) - except Exception: - return - try: - if ret >> 8 == 0: - with open(tmpfile, 'r') as fp: - multiarch_path_component = fp.readline().strip() - _add_directory(self.compiler.library_dirs, - '/usr/lib/' + multiarch_path_component) - _add_directory(self.compiler.include_dirs, - '/usr/include/' + multiarch_path_component) - finally: - os.unlink(tmpfile) - def debug_build(): - return hasattr(sys, 'gettotalrefcount') - - -needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv) -pytest_runner = ['pytest-runner'] if needs_pytest else [] + return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD + + +files = ["src/_imaging.c"] +for src_file in _IMAGING: + files.append("src/" + src_file + ".c") +for src_file in _LIB_IMAGING: + files.append(os.path.join("src/libImaging", src_file + ".c")) +ext_modules = [ + Extension("PIL._imaging", files), + Extension("PIL._imagingft", ["src/_imagingft.c"]), + Extension("PIL._imagingcms", ["src/_imagingcms.c"]), + Extension("PIL._webp", ["src/_webp.c"]), + Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]), + Extension("PIL._imagingmath", ["src/_imagingmath.c"]), + Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), +] try: - setup(name=NAME, - version=PILLOW_VERSION, - description='Python Imaging Library (Fork)', - long_description=_read('README.rst').decode('utf-8'), - author='Alex Clark (Fork Author)', - author_email='aclark@aclark.net', - url='https://python-pillow.org', - classifiers=[ - "Development Status :: 6 - Mature", - "Topic :: Multimedia :: Graphics", - "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera", - "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", - "Topic :: Multimedia :: Graphics :: Graphics Conversion", - "Topic :: Multimedia :: Graphics :: Viewers", - "License :: Other/Proprietary License", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - ], - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", - cmdclass={"build_ext": pil_build_ext}, - ext_modules=[Extension("PIL._imaging", ["_imaging.c"])], - include_package_data=True, - setup_requires=pytest_runner, - tests_require=['pytest'], - packages=["PIL"], - package_dir={'':'src'}, - keywords=["Imaging", ], - license='Standard PIL License', - zip_safe=not (debug_build() or PLATFORM_MINGW), ) + setup( + version=PILLOW_VERSION, + cmdclass={"build_ext": pil_build_ext}, + ext_modules=ext_modules, + zip_safe=not (debug_build() or PLATFORM_MINGW), + ) except RequiredDependencyException as err: - msg = """ + msg = f""" -The headers or library files could not be found for %s, +The headers or library files could not be found for {str(err)}, a required dependency when compiling Pillow from source. Please see the install instructions at: https://pillow.readthedocs.io/en/latest/installation.html -""" % (str(err)) +""" sys.stderr.write(msg) raise RequiredDependencyException(msg) except DependencyException as err: - msg = """ + msg = f""" -The headers or library files could not be found for %s, -which was requested by the option flag --enable-%s +The headers or library files could not be found for {str(err)}, +which was requested by the option flag --enable-{str(err)} -""" % (str(err), str(err)) +""" sys.stderr.write(msg) raise DependencyException(msg) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index c8bc604610a..102b72e1d5a 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -17,14 +17,12 @@ # See the README file for information on usage and redistribution. # -from __future__ import print_function +""" +Parse X Bitmap Distribution Format (BDF) +""" -from . import Image, FontFile - -# -------------------------------------------------------------------- -# parse X Bitmap Distribution Format (BDF) -# -------------------------------------------------------------------- +from . import FontFile, Image bdf_slant = { "R": "Roman", @@ -32,14 +30,10 @@ "O": "Oblique", "RI": "Reverse Italic", "RO": "Reverse Oblique", - "OT": "Other" + "OT": "Other", } -bdf_spacing = { - "P": "Proportional", - "M": "Monospaced", - "C": "Cell" -} +bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"} def bdf_char(f): @@ -50,7 +44,7 @@ def bdf_char(f): return None if s[:9] == b"STARTCHAR": break - id = s[9:].strip().decode('ascii') + id = s[9:].strip().decode("ascii") # load symbol properties props = {} @@ -59,7 +53,7 @@ def bdf_char(f): if not s or s[:6] == b"BITMAP": break i = s.find(b" ") - props[s[:i].decode('ascii')] = s[i+1:-1].decode('ascii') + props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") # load bitmap bitmap = [] @@ -73,7 +67,7 @@ def bdf_char(f): [x, y, l, d] = [int(p) for p in props["BBX"].split()] [dx, dy] = [int(p) for p in props["DWIDTH"].split()] - bbox = (dx, dy), (l, -d-y, x+l, -d), (0, 0, x, y) + bbox = (dx, dy), (l, -d - y, x + l, -d), (0, 0, x, y) try: im = Image.frombytes("1", (x, y), bitmap, "hex", "1") @@ -84,14 +78,11 @@ def bdf_char(f): return id, int(props["ENCODING"]), bbox, im -## -# Font file plugin for the X11 BDF format. - class BdfFontFile(FontFile.FontFile): + """Font file plugin for the X11 BDF format.""" def __init__(self, fp): - - FontFile.FontFile.__init__(self) + super().__init__() s = fp.readline() if s[:13] != b"STARTFONT 2.1": @@ -105,24 +96,10 @@ def __init__(self, fp): if not s or s[:13] == b"ENDPROPERTIES": break i = s.find(b" ") - props[s[:i].decode('ascii')] = s[i+1:-1].decode('ascii') + props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") if s[:i] in [b"COMMENT", b"COPYRIGHT"]: if s.find(b"LogicalFontDescription") < 0: - comments.append(s[i+1:-1].decode('ascii')) - - # font = props["FONT"].split("-") - - # font[4] = bdf_slant[font[4].upper()] - # font[11] = bdf_spacing[font[11].upper()] - - # ascent = int(props["FONT_ASCENT"]) - # descent = int(props["FONT_DESCENT"]) - - # fontname = ";".join(font[1:]) - - # print("#", fontname) - # for i in comments: - # print("#", i) + comments.append(s[i + 1 : -1].decode("ascii")) while True: c = bdf_char(fp) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py new file mode 100644 index 00000000000..53399773716 --- /dev/null +++ b/src/PIL/BlpImagePlugin.py @@ -0,0 +1,484 @@ +""" +Blizzard Mipmap Format (.blp) +Jerome Leclanche + +The contents of this file are hereby released in the public domain (CC0) +Full text of the CC0 license: + https://creativecommons.org/publicdomain/zero/1.0/ + +BLP1 files, used mostly in Warcraft III, are not fully supported. +All types of BLP2 files used in World of Warcraft are supported. + +The BLP file structure consists of a header, up to 16 mipmaps of the +texture + +Texture sizes must be powers of two, though the two dimensions do +not have to be equal; 512x256 is valid, but 512x200 is not. +The first mipmap (mipmap #0) is the full size image; each subsequent +mipmap halves both dimensions. The final mipmap should be 1x1. + +BLP files come in many different flavours: +* JPEG-compressed (type == 0) - only supported for BLP1. +* RAW images (type == 1, encoding == 1). Each mipmap is stored as an + array of 8-bit values, one per pixel, left to right, top to bottom. + Each value is an index to the palette. +* DXT-compressed (type == 1, encoding == 2): +- DXT1 compression is used if alpha_encoding == 0. + - An additional alpha bit is used if alpha_depth == 1. + - DXT3 compression is used if alpha_encoding == 1. + - DXT5 compression is used if alpha_encoding == 7. +""" + +import os +import struct +from enum import IntEnum +from io import BytesIO + +from . import Image, ImageFile +from ._deprecate import deprecate + + +class Format(IntEnum): + JPEG = 0 + + +class Encoding(IntEnum): + UNCOMPRESSED = 1 + DXT = 2 + UNCOMPRESSED_RAW_BGRA = 3 + + +class AlphaEncoding(IntEnum): + DXT1 = 0 + DXT3 = 1 + DXT5 = 7 + + +def __getattr__(name): + for enum, prefix in { + Format: "BLP_FORMAT_", + Encoding: "BLP_ENCODING_", + AlphaEncoding: "BLP_ALPHA_ENCODING_", + }.items(): + if name.startswith(prefix): + name = name[len(prefix) :] + 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}'") + + +def unpack_565(i): + return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3 + + +def decode_dxt1(data, alpha=False): + """ + input: one "row" of data (i.e. will produce 4*width pixels) + """ + + blocks = len(data) // 8 # number of blocks in row + ret = (bytearray(), bytearray(), bytearray(), bytearray()) + + for block in range(blocks): + # Decode next 8-byte block. + idx = block * 8 + color0, color1, bits = struct.unpack_from("> 2 + + a = 0xFF + if control == 0: + r, g, b = r0, g0, b0 + elif control == 1: + r, g, b = r1, g1, b1 + elif control == 2: + if color0 > color1: + r = (2 * r0 + r1) // 3 + g = (2 * g0 + g1) // 3 + b = (2 * b0 + b1) // 3 + else: + r = (r0 + r1) // 2 + g = (g0 + g1) // 2 + b = (b0 + b1) // 2 + elif control == 3: + if color0 > color1: + r = (2 * r1 + r0) // 3 + g = (2 * g1 + g0) // 3 + b = (2 * b1 + b0) // 3 + else: + r, g, b, a = 0, 0, 0, 0 + + if alpha: + ret[j].extend([r, g, b, a]) + else: + ret[j].extend([r, g, b]) + + return ret + + +def decode_dxt3(data): + """ + input: one "row" of data (i.e. will produce 4*width pixels) + """ + + blocks = len(data) // 16 # number of blocks in row + ret = (bytearray(), bytearray(), bytearray(), bytearray()) + + for block in range(blocks): + idx = block * 16 + block = data[idx : idx + 16] + # Decode next 16-byte block. + bits = struct.unpack_from("<8B", block) + color0, color1 = struct.unpack_from(">= 4 + else: + high = True + a &= 0xF + a *= 17 # We get a value between 0 and 15 + + color_code = (code >> 2 * (4 * j + i)) & 0x03 + + if color_code == 0: + r, g, b = r0, g0, b0 + elif color_code == 1: + r, g, b = r1, g1, b1 + elif color_code == 2: + r = (2 * r0 + r1) // 3 + g = (2 * g0 + g1) // 3 + b = (2 * b0 + b1) // 3 + elif color_code == 3: + r = (2 * r1 + r0) // 3 + g = (2 * g1 + g0) // 3 + b = (2 * b1 + b0) // 3 + + ret[j].extend([r, g, b, a]) + + return ret + + +def decode_dxt5(data): + """ + input: one "row" of data (i.e. will produce 4 * width pixels) + """ + + blocks = len(data) // 16 # number of blocks in row + ret = (bytearray(), bytearray(), bytearray(), bytearray()) + + for block in range(blocks): + idx = block * 16 + block = data[idx : idx + 16] + # Decode next 16-byte block. + a0, a1 = struct.unpack_from("> alphacode_index) & 0x07 + elif alphacode_index == 15: + alphacode = (alphacode2 >> 15) | ((alphacode1 << 1) & 0x06) + else: # alphacode_index >= 18 and alphacode_index <= 45 + alphacode = (alphacode1 >> (alphacode_index - 16)) & 0x07 + + if alphacode == 0: + a = a0 + elif alphacode == 1: + a = a1 + elif a0 > a1: + a = ((8 - alphacode) * a0 + (alphacode - 1) * a1) // 7 + elif alphacode == 6: + a = 0 + elif alphacode == 7: + a = 255 + else: + a = ((6 - alphacode) * a0 + (alphacode - 1) * a1) // 5 + + color_code = (code >> 2 * (4 * j + i)) & 0x03 + + if color_code == 0: + r, g, b = r0, g0, b0 + elif color_code == 1: + r, g, b = r1, g1, b1 + elif color_code == 2: + r = (2 * r0 + r1) // 3 + g = (2 * g0 + g1) // 3 + b = (2 * b0 + b1) // 3 + elif color_code == 3: + r = (2 * r1 + r0) // 3 + g = (2 * g1 + g0) // 3 + b = (2 * b1 + b0) // 3 + + ret[j].extend([r, g, b, a]) + + return ret + + +class BLPFormatError(NotImplementedError): + pass + + +def _accept(prefix): + return prefix[:4] in (b"BLP1", b"BLP2") + + +class BlpImageFile(ImageFile.ImageFile): + """ + Blizzard Mipmap Format + """ + + format = "BLP" + format_description = "Blizzard Mipmap Format" + + def _open(self): + self.magic = self.fp.read(4) + + self.fp.seek(5, os.SEEK_CUR) + (self._blp_alpha_depth,) = struct.unpack("= 40: # v3 and OS/2 - file_info['y_flip'] = i8(header_data[7]) == 0xff - file_info['direction'] = 1 if file_info['y_flip'] else -1 - file_info['width'] = i32(header_data[0:4]) - file_info['height'] = i32(header_data[4:8]) if not file_info['y_flip'] else 2**32 - i32(header_data[4:8]) - file_info['planes'] = i16(header_data[8:10]) - file_info['bits'] = i16(header_data[10:12]) - file_info['compression'] = i32(header_data[12:16]) - file_info['data_size'] = i32(header_data[16:20]) # byte size of pixel data - file_info['pixels_per_meter'] = (i32(header_data[20:24]), i32(header_data[24:28])) - file_info['colors'] = i32(header_data[28:32]) - file_info['palette_padding'] = 4 - self.info["dpi"] = tuple( - map(lambda x: int(math.ceil(x / 39.3701)), - file_info['pixels_per_meter'])) - if file_info['compression'] == self.BITFIELDS: - if len(header_data) >= 52: - for idx, mask in enumerate(['r_mask', 'g_mask', 'b_mask', 'a_mask']): - file_info[mask] = i32(header_data[36+idx*4:40+idx*4]) - else: - # 40 byte headers only have the three components in the bitfields masks, - # ref: https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx - # See also https://github.com/python-pillow/Pillow/issues/1293 - # There is a 4th component in the RGBQuad, in the alpha location, but it - # is listed as a reserved component, and it is not generally an alpha channel - file_info['a_mask'] = 0x0 - for mask in ['r_mask', 'g_mask', 'b_mask']: - file_info[mask] = i32(read(4)) - file_info['rgb_mask'] = (file_info['r_mask'], file_info['g_mask'], file_info['b_mask']) - file_info['rgba_mask'] = (file_info['r_mask'], file_info['g_mask'], file_info['b_mask'], file_info['a_mask']) + # read bmp header size @offset 14 (this is part of the header size) + file_info = {"header_size": i32(read(4)), "direction": -1} + + # -------------------- If requested, read header at a specific position + # read the rest of the bmp header, without its size + header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4) + + # -------------------------------------------------- IBM OS/2 Bitmap v1 + # ----- This format has different offsets because of width/height types + if file_info["header_size"] == 12: + file_info["width"] = i16(header_data, 0) + file_info["height"] = i16(header_data, 2) + file_info["planes"] = i16(header_data, 4) + file_info["bits"] = i16(header_data, 6) + file_info["compression"] = self.RAW + file_info["palette_padding"] = 3 + + # --------------------------------------------- Windows Bitmap v2 to v5 + # v3, OS/2 v2, v4, v5 + elif file_info["header_size"] in (40, 64, 108, 124): + file_info["y_flip"] = header_data[7] == 0xFF + file_info["direction"] = 1 if file_info["y_flip"] else -1 + file_info["width"] = i32(header_data, 0) + file_info["height"] = ( + i32(header_data, 4) + if not file_info["y_flip"] + else 2**32 - i32(header_data, 4) + ) + file_info["planes"] = i16(header_data, 8) + file_info["bits"] = i16(header_data, 10) + file_info["compression"] = i32(header_data, 12) + # byte size of pixel data + file_info["data_size"] = i32(header_data, 16) + file_info["pixels_per_meter"] = ( + i32(header_data, 20), + i32(header_data, 24), + ) + file_info["colors"] = i32(header_data, 28) + file_info["palette_padding"] = 4 + self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"]) + if file_info["compression"] == self.BITFIELDS: + if len(header_data) >= 52: + for idx, mask in enumerate( + ["r_mask", "g_mask", "b_mask", "a_mask"] + ): + file_info[mask] = i32(header_data, 36 + idx * 4) + else: + # 40 byte headers only have the three components in the + # bitfields masks, ref: + # https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx + # See also + # https://github.com/python-pillow/Pillow/issues/1293 + # There is a 4th component in the RGBQuad, in the alpha + # location, but it is listed as a reserved component, + # and it is not generally an alpha channel + file_info["a_mask"] = 0x0 + for mask in ["r_mask", "g_mask", "b_mask"]: + file_info[mask] = i32(read(4)) + file_info["rgb_mask"] = ( + file_info["r_mask"], + file_info["g_mask"], + file_info["b_mask"], + ) + file_info["rgba_mask"] = ( + file_info["r_mask"], + file_info["g_mask"], + file_info["b_mask"], + file_info["a_mask"], + ) else: - raise IOError("Unsupported BMP header type (%d)" % file_info['header_size']) + raise OSError(f"Unsupported BMP header type ({file_info['header_size']})") + # ------------------ Special case : header is reported 40, which # ---------------------- is shorter than real size for bpp >= 16 - self.size = file_info['width'], file_info['height'] - # -------- If color count was not found in the header, compute from bits - file_info['colors'] = file_info['colors'] if file_info.get('colors', 0) else (1 << file_info['bits']) - # -------------------------------- Check abnormal values for DOS attacks - if file_info['width'] * file_info['height'] > 2**31: - raise IOError("Unsupported BMP Size: (%dx%d)" % self.size) - # ----------------------- Check bit depth for unusual unsupported values - self.mode, raw_mode = BIT2MODE.get(file_info['bits'], (None, None)) + self._size = file_info["width"], file_info["height"] + + # ------- If color count was not found in the header, compute from bits + file_info["colors"] = ( + file_info["colors"] + if file_info.get("colors", 0) + else (1 << file_info["bits"]) + ) + if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8: + offset += 4 * file_info["colors"] + + # ---------------------- Check bit depth for unusual unsupported values + self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) if self.mode is None: - raise IOError("Unsupported BMP pixel depth (%d)" % file_info['bits']) - # ----------------- Process BMP with Bitfields compression (not palette) - if file_info['compression'] == self.BITFIELDS: + raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})") + + # ---------------- Process BMP with Bitfields compression (not palette) + decoder_name = "raw" + if file_info["compression"] == self.BITFIELDS: SUPPORTED = { - 32: [(0xff0000, 0xff00, 0xff, 0x0), (0xff0000, 0xff00, 0xff, 0xff000000), (0x0, 0x0, 0x0, 0x0), (0xff000000, 0xff0000, 0xff00, 0x0)], - 24: [(0xff0000, 0xff00, 0xff)], - 16: [(0xf800, 0x7e0, 0x1f), (0x7c00, 0x3e0, 0x1f)] + 32: [ + (0xFF0000, 0xFF00, 0xFF, 0x0), + (0xFF000000, 0xFF0000, 0xFF00, 0x0), + (0xFF000000, 0xFF0000, 0xFF00, 0xFF), + (0xFF, 0xFF00, 0xFF0000, 0xFF000000), + (0xFF0000, 0xFF00, 0xFF, 0xFF000000), + (0x0, 0x0, 0x0, 0x0), + ], + 24: [(0xFF0000, 0xFF00, 0xFF)], + 16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)], } MASK_MODES = { - (32, (0xff0000, 0xff00, 0xff, 0x0)): "BGRX", - (32, (0xff000000, 0xff0000, 0xff00, 0x0)): "XBGR", - (32, (0xff0000, 0xff00, 0xff, 0xff000000)): "BGRA", + (32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX", + (32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR", + (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR", + (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA", + (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA", (32, (0x0, 0x0, 0x0, 0x0)): "BGRA", - (24, (0xff0000, 0xff00, 0xff)): "BGR", - (16, (0xf800, 0x7e0, 0x1f)): "BGR;16", - (16, (0x7c00, 0x3e0, 0x1f)): "BGR;15" + (24, (0xFF0000, 0xFF00, 0xFF)): "BGR", + (16, (0xF800, 0x7E0, 0x1F)): "BGR;16", + (16, (0x7C00, 0x3E0, 0x1F)): "BGR;15", } - if file_info['bits'] in SUPPORTED: - if file_info['bits'] == 32 and file_info['rgba_mask'] in SUPPORTED[file_info['bits']]: - raw_mode = MASK_MODES[(file_info['bits'], file_info['rgba_mask'])] - self.mode = "RGBA" if raw_mode in ("BGRA",) else self.mode - elif file_info['bits'] in (24, 16) and file_info['rgb_mask'] in SUPPORTED[file_info['bits']]: - raw_mode = MASK_MODES[(file_info['bits'], file_info['rgb_mask'])] + if file_info["bits"] in SUPPORTED: + if ( + file_info["bits"] == 32 + and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]] + ): + raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])] + self.mode = "RGBA" if "A" in raw_mode else self.mode + elif ( + file_info["bits"] in (24, 16) + and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]] + ): + raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])] else: - raise IOError("Unsupported BMP bitfields layout") + raise OSError("Unsupported BMP bitfields layout") else: - raise IOError("Unsupported BMP bitfields layout") - elif file_info['compression'] == self.RAW: - if file_info['bits'] == 32 and header == 22: # 32-bit .cur offset + raise OSError("Unsupported BMP bitfields layout") + elif file_info["compression"] == self.RAW: + if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset raw_mode, self.mode = "BGRA", "RGBA" + elif file_info["compression"] in (self.RLE8, self.RLE4): + decoder_name = "bmp_rle" else: - raise IOError("Unsupported BMP compression (%d)" % file_info['compression']) - # ---------------- Once the header is processed, process the palette/LUT + raise OSError(f"Unsupported BMP compression ({file_info['compression']})") + + # --------------- 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 IOError("Unsupported BMP Palette size (%d)" % file_info['colors']) + + # ---------------------------------------------------- 1-bit images + if not (0 < file_info["colors"] <= 65536): + raise OSError(f"Unsupported BMP Palette size ({file_info['colors']})") else: - padding = file_info['palette_padding'] - palette = read(padding * file_info['colors']) + padding = file_info["palette_padding"] + palette = read(padding * file_info["colors"]) greyscale = True - indices = (0, 255) if file_info['colors'] == 2 else list(range(file_info['colors'])) - # ------------------ Check if greyscale and ignore palette if so + indices = ( + (0, 255) + if file_info["colors"] == 2 + else list(range(file_info["colors"])) + ) + + # ----------------- Check if greyscale and ignore palette if so for ind, val in enumerate(indices): - rgb = palette[ind*padding:ind*padding + 3] + rgb = palette[ind * padding : ind * padding + 3] if rgb != o8(val) * 3: greyscale = False - # -------- If all colors are grey, white or black, ditch palette + + # ------- If all colors are grey, white or black, ditch palette if greyscale: - self.mode = "1" if file_info['colors'] == 2 else "L" + self.mode = "1" if file_info["colors"] == 2 else "L" raw_mode = self.mode else: self.mode = "P" - self.palette = ImagePalette.raw("BGRX" if padding == 4 else "BGR", palette) - - # ----------------------------- Finally set the tile data for the plugin - self.info['compression'] = file_info['compression'] - self.tile = [('raw', (0, 0, file_info['width'], file_info['height']), offset or self.fp.tell(), - (raw_mode, ((file_info['width'] * file_info['bits'] + 31) >> 3) & (~3), file_info['direction']) - )] + self.palette = ImagePalette.raw( + "BGRX" if padding == 4 else "BGR", palette + ) + + # ---------------------------- Finally set the tile data for the plugin + self.info["compression"] = file_info["compression"] + args = [raw_mode] + if decoder_name == "bmp_rle": + args.append(file_info["compression"] == self.RLE4) + else: + args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3)) + args.append(file_info["direction"]) + self.tile = [ + ( + decoder_name, + (0, 0, file_info["width"], file_info["height"]), + offset or self.fp.tell(), + tuple(args), + ) + ] def _open(self): - """ Open file, check magic number and read header """ + """Open file, check magic number and read header""" # read 14 bytes: magic number, filesize, reserved, header final offset head_data = self.fp.read(14) # choke if the file does not have the required magic bytes - if head_data[0:2] != b"BM": + if not _accept(head_data): raise SyntaxError("Not a BMP file") # read the start position of the BMP image data (u32) - offset = i32(head_data[10:14]) + offset = i32(head_data, 10) # load bitmap information (offset=raster info) self._bitmap(offset=offset) -# ============================================================================== +class BmpRleDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer): + rle4 = self.args[1] + data = bytearray() + x = 0 + while len(data) < self.state.xsize * self.state.ysize: + pixels = self.fd.read(1) + byte = self.fd.read(1) + if not pixels or not byte: + break + num_pixels = pixels[0] + if num_pixels: + # encoded mode + if x + num_pixels > self.state.xsize: + # Too much data for row + num_pixels = max(0, self.state.xsize - x) + if rle4: + first_pixel = o8(byte[0] >> 4) + second_pixel = o8(byte[0] & 0x0F) + for index in range(num_pixels): + if index % 2 == 0: + data += first_pixel + else: + data += second_pixel + else: + data += byte * num_pixels + x += num_pixels + else: + if byte[0] == 0: + # end of line + while len(data) % self.state.xsize != 0: + data += b"\x00" + x = 0 + elif byte[0] == 1: + # end of bitmap + break + elif byte[0] == 2: + # delta + bytes_read = self.fd.read(2) + if len(bytes_read) < 2: + break + right, up = self.fd.read(2) + data += b"\x00" * (right + up * self.state.xsize) + x = len(data) % self.state.xsize + else: + # absolute mode + if rle4: + # 2 pixels per byte + byte_count = byte[0] // 2 + bytes_read = self.fd.read(byte_count) + for byte_read in bytes_read: + data += o8(byte_read >> 4) + data += o8(byte_read & 0x0F) + else: + byte_count = byte[0] + bytes_read = self.fd.read(byte_count) + data += bytes_read + if len(bytes_read) < byte_count: + break + x += byte[0] + + # align to 16-bit word boundary + if self.fd.tell() % 2 != 0: + self.fd.seek(1, os.SEEK_CUR) + rawmode = "L" if self.mode == "L" else "P" + self.set_as_raw(bytes(data), (rawmode, 0, self.args[-1])) + return -1, 0 + + +# ============================================================================= # Image plugin for the DIB format (BMP alias) -# ============================================================================== +# ============================================================================= class DibImageFile(BmpImageFile): format = "DIB" @@ -212,10 +360,12 @@ class DibImageFile(BmpImageFile): def _open(self): self._bitmap() + # # -------------------------------------------------------------------- # Write BMP file + SAVE = { "1": ("1", 1, 2), "L": ("L", 8, 256), @@ -225,63 +375,90 @@ def _open(self): } -def _save(im, fp, filename): +def _dib_save(im, fp, filename): + _save(im, fp, filename, False) + + +def _save(im, fp, filename, bitmap_header=True): try: rawmode, bits, colors = SAVE[im.mode] - except KeyError: - raise IOError("cannot write mode %s as BMP" % im.mode) + except KeyError as e: + raise OSError(f"cannot write mode {im.mode} as BMP") from e info = im.encoderinfo dpi = info.get("dpi", (96, 96)) # 1 meter == 39.3701 inches - ppm = tuple(map(lambda x: int(x * 39.3701), dpi)) + ppm = tuple(map(lambda x: int(x * 39.3701 + 0.5), dpi)) - stride = ((im.size[0]*bits+7)//8+3) & (~3) + stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3) header = 40 # or 64 for OS/2 version 2 - offset = 14 + header + colors * 4 image = stride * im.size[1] + if im.mode == "1": + palette = b"".join(o8(i) * 4 for i in (0, 255)) + elif im.mode == "L": + palette = b"".join(o8(i) * 4 for i in range(256)) + elif im.mode == "P": + palette = im.im.getpalette("RGB", "BGRX") + colors = len(palette) // 4 + else: + palette = None + # bitmap header - fp.write(b"BM" + # file type (magic) - o32(offset+image) + # file size - o32(0) + # reserved - o32(offset)) # image data offset + if bitmap_header: + 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") + fp.write( + b"BM" # file type (magic) + + o32(file_size) # file size + + o32(0) # reserved + + o32(offset) # image data offset + ) # bitmap info header - fp.write(o32(header) + # info header size - o32(im.size[0]) + # width - o32(im.size[1]) + # height - o16(1) + # planes - o16(bits) + # depth - o32(0) + # compression (0=uncompressed) - o32(image) + # size of bitmap - o32(ppm[0]) + o32(ppm[1]) + # resolution - o32(colors) + # colors used - o32(colors)) # colors important - - fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) + fp.write( + o32(header) # info header size + + o32(im.size[0]) # width + + o32(im.size[1]) # height + + o16(1) # planes + + o16(bits) # depth + + o32(0) # compression (0=uncompressed) + + o32(image) # size of bitmap + + o32(ppm[0]) # resolution + + o32(ppm[1]) # resolution + + o32(colors) # colors used + + o32(colors) # colors important + ) - if im.mode == "1": - for i in (0, 255): - fp.write(o8(i) * 4) - elif im.mode == "L": - for i in range(256): - fp.write(o8(i) * 4) - elif im.mode == "P": - fp.write(im.im.getpalette("RGB", "BGRX")) + fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) + + if palette: + fp.write(palette) + + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]) - ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0, - (rawmode, stride, -1))]) # # -------------------------------------------------------------------- # Registry + Image.register_open(BmpImageFile.format, BmpImageFile, _accept) Image.register_save(BmpImageFile.format, _save) Image.register_extension(BmpImageFile.format, ".bmp") Image.register_mime(BmpImageFile.format, "image/bmp") + +Image.register_decoder("bmp_rle", BmpRleDecoder) + +Image.register_open(DibImageFile.format, DibImageFile, _dib_accept) +Image.register_save(DibImageFile.format, _dib_save) + +Image.register_extension(DibImageFile.format, ".dib") + +Image.register_mime(DibImageFile.format, "image/bmp") diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 16d83c74def..9510f733e1a 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -27,6 +27,7 @@ def register_handler(handler): # -------------------------------------------------------------------- # Image adapter + def _accept(prefix): return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC" @@ -47,7 +48,7 @@ def _open(self): # make something up self.mode = "F" - self.size = 1, 1 + self._size = 1, 1 loader = self._load() if loader: @@ -58,8 +59,8 @@ def _load(self): def _save(im, fp, filename): - if _handler is None or not hasattr("_handler", "save"): - raise IOError("BUFR save handler not installed") + if _handler is None or not hasattr(_handler, "save"): + raise OSError("BUFR save handler not installed") _handler.save(im, fp, filename) diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py index 496ed68263d..45e80b39af7 100644 --- a/src/PIL/ContainerIO.py +++ b/src/PIL/ContainerIO.py @@ -14,12 +14,15 @@ # See the README file for information on usage and redistribution. # -## -# A file object that provides read access to a part of an existing -# file (for example a TAR file). +import io -class ContainerIO(object): + +class ContainerIO: + """ + A file object that provides read access to a part of an existing + file (for example a TAR file). + """ def __init__(self, file, offset, length): """ @@ -39,9 +42,9 @@ def __init__(self, file, offset, length): # Always false. def isatty(self): - return 0 + return False - def seek(self, offset, mode=0): + def seek(self, offset, mode=io.SEEK_SET): """ Move file pointer. @@ -81,7 +84,7 @@ def read(self, n=0): else: n = self.length - self.pos if not n: # EOF - return "" + return b"" if "b" in self.fh.mode else "" self.pos = self.pos + n return self.fh.read(n) @@ -91,13 +94,14 @@ def readline(self): :returns: An 8-bit string. """ - s = "" + s = b"" if "b" in self.fh.mode else "" + newline_character = b"\n" if "b" in self.fh.mode else "\n" while True: c = self.read(1) if not c: break s = s + c - if c == "\n": + if c == newline_character: break return s @@ -107,10 +111,10 @@ def readlines(self): :returns: A list of 8-bit strings. """ - l = [] + lines = [] while True: s = self.readline() if not s: break - l.append(s) - return l + lines.append(s) + return lines diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index e4257cd5a47..42af5cafcef 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -15,13 +15,9 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function - -from . import Image, BmpImagePlugin -from ._binary import i8, i16le as i16, i32le as i32 - -__version__ = "0.1" +from . import BmpImagePlugin, Image +from ._binary import i16le as i16 +from ._binary import i32le as i32 # # -------------------------------------------------------------------- @@ -34,6 +30,7 @@ def _accept(prefix): ## # Image plugin for Windows Cursor files. + class CurImageFile(BmpImagePlugin.BmpImageFile): format = "CUR" @@ -50,30 +47,22 @@ def _open(self): # pick the largest cursor in the file m = b"" - for i in range(i16(s[4:])): + for i in range(i16(s, 4)): s = self.fp.read(16) if not m: m = s - elif i8(s[0]) > i8(m[0]) and i8(s[1]) > i8(m[1]): + elif s[0] > m[0] and s[1] > m[1]: m = s - # print("width", i8(s[0])) - # print("height", i8(s[1])) - # print("colors", i8(s[2])) - # print("reserved", i8(s[3])) - # print("hotspot x", i16(s[4:])) - # print("hotspot y", i16(s[6:])) - # print("bytes", i32(s[8:])) - # print("offset", i32(s[12:])) if not m: raise TypeError("No cursors were found") # load as bitmap - self._bitmap(i32(m[12:]) + offset) + self._bitmap(i32(m, 12) + offset) # patch up the bitmap height - self.size = self.size[0], self.size[1]//2 + self._size = self.size[0], self.size[1] // 2 d, e, o, a = self.tile[0] - self.tile[0] = d, (0, 0)+self.size, o, a + self.tile[0] = d, (0, 0) + self.size, o, a return diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index 0d920ad3c37..aeed1e7c7ba 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -25,8 +25,6 @@ from ._binary import i32le as i32 from .PcxImagePlugin import PcxImageFile -__version__ = "0.2" - MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? @@ -37,17 +35,18 @@ def _accept(prefix): ## # Image plugin for the Intel DCX format. + class DcxImageFile(PcxImageFile): format = "DCX" format_description = "Intel DCX" _close_exclusive_fp_after_loading = False - + def _open(self): # Header s = self.fp.read(4) - if i32(s) != MAGIC: + if not _accept(s): raise SyntaxError("not a DCX file") # Component directory @@ -58,23 +57,17 @@ def _open(self): break self._offset.append(offset) - self.__fp = self.fp + self._fp = self.fp self.frame = None + self.n_frames = len(self._offset) + self.is_animated = self.n_frames > 1 self.seek(0) - @property - def n_frames(self): - return len(self._offset) - - @property - def is_animated(self): - return len(self._offset) > 1 - def seek(self, frame): if not self._seek_check(frame): return self.frame = frame - self.fp = self.__fp + self.fp = self._fp self.fp.seek(self._offset[frame]) PcxImageFile._open(self) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 9508e61c872..eea6e31534c 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -3,7 +3,7 @@ Jerome Leclanche Documentation: - http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt + https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt The contents of this file are hereby released in the public domain (CC0) Full text of the CC0 license: @@ -12,8 +12,9 @@ import struct from io import BytesIO -from . import Image, ImageFile +from . import Image, ImageFile +from ._binary import o32le as o32 # Magic ("DDS ") DDS_MAGIC = 0x20534444 @@ -61,8 +62,7 @@ DDS_ALPHA = DDPF_ALPHA DDS_PAL8 = DDPF_PALETTEINDEXED8 -DDS_HEADER_FLAGS_TEXTURE = (DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | - DDSD_PIXELFORMAT) +DDS_HEADER_FLAGS_TEXTURE = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT DDS_HEADER_FLAGS_MIPMAP = DDSD_MIPMAPCOUNT DDS_HEADER_FLAGS_VOLUME = DDSD_DEPTH DDS_HEADER_FLAGS_PITCH = DDSD_PITCH @@ -95,6 +95,14 @@ # dxgiformat.h +DXGI_FORMAT_R8G8B8A8_TYPELESS = 27 +DXGI_FORMAT_R8G8B8A8_UNORM = 28 +DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29 +DXGI_FORMAT_BC5_TYPELESS = 82 +DXGI_FORMAT_BC5_UNORM = 83 +DXGI_FORMAT_BC5_SNORM = 84 +DXGI_FORMAT_BC6H_UF16 = 95 +DXGI_FORMAT_BC6H_SF16 = 96 DXGI_FORMAT_BC7_TYPELESS = 97 DXGI_FORMAT_BC7_UNORM = 98 DXGI_FORMAT_BC7_UNORM_SRGB = 99 @@ -105,68 +113,155 @@ class DdsImageFile(ImageFile.ImageFile): format_description = "DirectDraw Surface" def _open(self): - magic, header_size = struct.unpack(" 0: - s = fp.read(min(lengthfile, 100*1024)) + s = fp.read(min(lengthfile, 100 * 1024)) if not s: break lengthfile -= len(s) f.write(s) - # Build ghostscript command - command = ["gs", - "-q", # quiet mode - "-g%dx%d" % size, # set output geometry (pixels) - "-r%fx%f" % res, # set input DPI (dots per inch) - "-dBATCH", # exit after processing - "-dNOPAUSE", # don't pause between pages, - "-dSAFER", # safe mode - "-sDEVICE=ppmraw", # ppm driver - "-sOutputFile=%s" % outfile, # output file - "-c", "%d %d translate" % (-bbox[0], -bbox[1]), - # adjust for image origin - "-f", infile, # input file - "-c", "showpage", # showpage (see: https://bugs.ghostscript.com/show_bug.cgi?id=698272) - ] + device = "pngalpha" if transparency else "ppmraw" + + # Build Ghostscript command + command = [ + "gs", + "-q", # quiet mode + "-g%dx%d" % size, # set output geometry (pixels) + "-r%fx%f" % res, # set input DPI (dots per inch) + "-dBATCH", # exit after processing + "-dNOPAUSE", # don't pause between pages + "-dSAFER", # safe mode + f"-sDEVICE={device}", + f"-sOutputFile={outfile}", # output file + # adjust for image origin + "-c", + f"{-bbox[0]} {-bbox[1]} translate", + "-f", + infile, # input file + # showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272) + "-c", + "showpage", + ] if gs_windows_binary is not None: if not gs_windows_binary: - raise WindowsError('Unable to locate Ghostscript on paths') + raise OSError("Unable to locate Ghostscript on paths") command[0] = gs_windows_binary - # push data through ghostscript + # push data through Ghostscript try: - with open(os.devnull, 'w+b') as devnull: - subprocess.check_call(command, stdin=devnull, stdout=devnull) - im = Image.open(outfile) - im.load() + startupinfo = None + if sys.platform.startswith("win"): + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + subprocess.check_call(command, startupinfo=startupinfo) + out_im = Image.open(outfile) + out_im.load() finally: try: os.unlink(outfile) @@ -151,28 +153,31 @@ def Ghostscript(tile, size, fp, scale=1): except OSError: pass - return im.im.copy() + im = out_im.im.copy() + out_im.close() + return im -class PSFile(object): +class PSFile: """ Wrapper for bytesio object that treats either CR or LF as end of line. """ + def __init__(self, fp): self.fp = fp self.char = None - def seek(self, offset, whence=0): + def seek(self, offset, whence=io.SEEK_SET): self.char = None self.fp.seek(offset, whence) def readline(self): - s = self.char or b"" + s = [self.char or b""] self.char = None c = self.fp.read(1) - while c not in b"\r\n": - s = s + c + while (c not in b"\r\n") and len(c): + s.append(c) c = self.fp.read(1) self.char = self.fp.read(1) @@ -180,15 +185,15 @@ def readline(self): if self.char in b"\r\n": self.char = None - return s.decode('latin-1') + return b"".join(s).decode("latin-1") def _accept(prefix): - return prefix[:4] == b"%!PS" or \ - (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) + return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) + ## -# Image plugin for Encapsulated Postscript. This plugin supports only +# Image plugin for Encapsulated PostScript. This plugin supports only # a few variants of this format. @@ -205,16 +210,7 @@ def _open(self): # Rewrap the open file pointer in something that will # convert line endings and decode to latin-1. - try: - if bytes is str: - # Python2, no encoding conversion necessary - fp = open(self.fp.name, "Ur") - else: - # Python3, can use bare open command. - fp = open(self.fp.name, "Ur", encoding='latin-1') - except: - # Expect this for bytesio/stringio - fp = PSFile(self.fp) + fp = PSFile(self.fp) # go to offset - start of "%!PS" fp.seek(offset) @@ -222,13 +218,13 @@ def _open(self): box = None self.mode = "RGB" - self.size = 1, 1 # FIXME: huh? + self._size = 1, 1 # FIXME: huh? # # Load EPS header s_raw = fp.readline() - s = s_raw.strip('\r\n') + s = s_raw.strip("\r\n") while s_raw: if s: @@ -237,8 +233,8 @@ def _open(self): try: m = split.match(s) - except re.error as v: - raise SyntaxError("not an EPS file") + except re.error as e: + raise SyntaxError("not an EPS file") from e if m: k, v = m.group(1, 2) @@ -249,10 +245,11 @@ def _open(self): # fields should be integers, but some drivers # put floating point values there anyway. box = [int(float(i)) for i in v.split()] - self.size = box[2] - box[0], box[3] - box[1] - self.tile = [("eps", (0, 0) + self.size, offset, - (length, box))] - except: + self._size = box[2] - box[0], box[3] - box[1] + self.tile = [ + ("eps", (0, 0) + self.size, offset, (length, box)) + ] + except Exception: pass else: @@ -266,15 +263,15 @@ def _open(self): self.info[k[:8]] = k[9:] else: self.info[k] = "" - elif s[0] == '%': - # handle non-DSC Postscript comments that some + elif s[0] == "%": + # handle non-DSC PostScript comments that some # tools mistakenly put in the Comments section pass else: - raise IOError("bad EPS header") + raise OSError("bad EPS header") s_raw = fp.readline() - s = s_raw.strip('\r\n') + s = s_raw.strip("\r\n") if s and s[:1] != "%": break @@ -291,22 +288,25 @@ def _open(self): # Encoded bitmapped image. x, y, bi, mo = s[11:].split(None, 7)[:4] - if int(bi) != 8: - break - try: - self.mode = self.mode_map[int(mo)] - except ValueError: + if int(bi) == 1: + self.mode = "1" + elif int(bi) == 8: + try: + self.mode = self.mode_map[int(mo)] + except ValueError: + break + else: break - self.size = int(x), int(y) + self._size = int(x), int(y) return - s = fp.readline().strip('\r\n') + s = fp.readline().strip("\r\n") if not s: break if not box: - raise IOError("cannot determine EPS bounding box") + raise OSError("cannot determine EPS bounding box") def _find_offset(self, fp): @@ -314,30 +314,30 @@ def _find_offset(self, fp): if s[:4] == b"%!PS": # for HEAD without binary preview - fp.seek(0, 2) + fp.seek(0, io.SEEK_END) length = fp.tell() offset = 0 - elif i32(s[0:4]) == 0xC6D3D0C5: + elif i32(s, 0) == 0xC6D3D0C5: # FIX for: Some EPS file not handled correctly / issue #302 # EPS can contain binary data # or start directly with latin coding # more info see: # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf - offset = i32(s[4:8]) - length = i32(s[8:12]) + offset = i32(s, 4) + length = i32(s, 8) else: raise SyntaxError("not an EPS file") - return (length, offset) + return length, offset - def load(self, scale=1): + def load(self, scale=1, transparency=False): # Load EPS via Ghostscript - if not self.tile: - return - self.im = Ghostscript(self.tile, self.size, self.fp, scale) - self.mode = self.im.mode - self.size = self.im.size - self.tile = [] + if self.tile: + self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency) + self.mode = self.im.mode + self._size = self.im.size + self.tile = [] + return Image.Image.load(self) def load_seek(self, *args, **kwargs): # we can't incrementally load, so force ImageFile.parser to @@ -348,6 +348,7 @@ def load_seek(self, *args, **kwargs): # # -------------------------------------------------------------------- + def _save(im, fp, filename, eps=1): """EPS Writer for the Python Imaging Library.""" @@ -356,68 +357,54 @@ def _save(im, fp, filename, eps=1): im.load() # - # determine postscript image mode + # determine PostScript image mode if im.mode == "L": - operator = (8, 1, "image") + operator = (8, 1, b"image") elif im.mode == "RGB": - operator = (8, 3, "false 3 colorimage") + operator = (8, 3, b"false 3 colorimage") elif im.mode == "CMYK": - operator = (8, 4, "false 4 colorimage") + operator = (8, 4, b"false 4 colorimage") else: raise ValueError("image mode is not supported") - class NoCloseStream(object): - def __init__(self, fp): - self.fp = fp - - def __getattr__(self, name): - return getattr(self.fp, name) - - def close(self): - pass - - base_fp = fp - if fp != sys.stdout: - fp = NoCloseStream(fp) - if sys.version_info[0] > 2: - fp = io.TextIOWrapper(fp, encoding='latin-1') - if eps: # # write EPS header - fp.write("%!PS-Adobe-3.0 EPSF-3.0\n") - fp.write("%%Creator: PIL 0.1 EpsEncode\n") + fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n") + fp.write(b"%%Creator: PIL 0.1 EpsEncode\n") # fp.write("%%CreationDate: %s"...) - fp.write("%%%%BoundingBox: 0 0 %d %d\n" % im.size) - fp.write("%%Pages: 1\n") - fp.write("%%EndComments\n") - fp.write("%%Page: 1 1\n") - fp.write("%%ImageData: %d %d " % im.size) - fp.write("%d %d 0 1 1 \"%s\"\n" % operator) + fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size) + fp.write(b"%%Pages: 1\n") + fp.write(b"%%EndComments\n") + fp.write(b"%%Page: 1 1\n") + fp.write(b"%%ImageData: %d %d " % im.size) + fp.write(b'%d %d 0 1 1 "%s"\n' % operator) # # image header - fp.write("gsave\n") - fp.write("10 dict begin\n") - fp.write("/buf %d string def\n" % (im.size[0] * operator[1])) - fp.write("%d %d scale\n" % im.size) - fp.write("%d %d 8\n" % im.size) # <= bits - fp.write("[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1])) - fp.write("{ currentfile buf readhexstring pop } bind\n") - fp.write(operator[2] + "\n") + fp.write(b"gsave\n") + fp.write(b"10 dict begin\n") + fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1])) + fp.write(b"%d %d scale\n" % im.size) + fp.write(b"%d %d 8\n" % im.size) # <= bits + fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1])) + fp.write(b"{ currentfile buf readhexstring pop } bind\n") + fp.write(operator[2] + b"\n") if hasattr(fp, "flush"): fp.flush() - ImageFile._save(im, base_fp, [("eps", (0, 0)+im.size, 0, None)]) + ImageFile._save(im, fp, [("eps", (0, 0) + im.size, 0, None)]) - fp.write("\n%%%%EndBinary\n") - fp.write("grestore end\n") + fp.write(b"\n%%%%EndBinary\n") + fp.write(b"grestore end\n") if hasattr(fp, "flush"): fp.flush() + # # -------------------------------------------------------------------- + Image.register_open(EpsImageFile.format, EpsImageFile, _accept) Image.register_save(EpsImageFile.format, _save) diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index a8ad26bcc1d..f3a73bf1a52 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -9,307 +9,332 @@ # See the README file for information on usage and redistribution. # -## -# This module provides constants and clear-text names for various -# well-known EXIF tags. -## +""" +This module provides constants and clear-text names for various +well-known EXIF tags. +""" -## -# Maps EXIF tags to tag names. +from enum import IntEnum -TAGS = { +class Base(IntEnum): # possibly incomplete - 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", - 0x9000: "ExifVersion", - 0x9003: "DateTimeOriginal", - 0x9004: "DateTimeDigitized", - 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", - 0x920c: "SpatialFrequencyResponse", - 0x920d: "Noise", - 0x9211: "ImageNumber", - 0x9212: "SecurityClassification", - 0x9213: "ImageHistory", + 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", 0x9214: "SubjectLocation", 0x9215: "ExposureIndex", + 0x828E: "CFAPattern", + 0x920B: "FlashEnergy", 0x9216: "TIFF/EPStandardID", - 0x927c: "MakerNote", - 0x9286: "UserComment", - 0x9290: "SubsecTime", - 0x9291: "SubsecTimeOriginal", - 0x9292: "SubsecTimeDigitized", - 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", - 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 GPS 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} diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py new file mode 100644 index 00000000000..c16300efa89 --- /dev/null +++ b/src/PIL/FitsImagePlugin.py @@ -0,0 +1,71 @@ +# +# The Python Imaging Library +# $Id$ +# +# FITS file handling +# +# Copyright (c) 1998-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import math + +from . import Image, ImageFile + + +def _accept(prefix): + return prefix[:6] == b"SIMPLE" + + +class FitsImageFile(ImageFile.ImageFile): + + format = "FITS" + format_description = "FITS" + + def _open(self): + headers = {} + while True: + header = self.fp.read(80) + if not header: + raise OSError("Truncated FITS file") + keyword = header[:8].strip() + if keyword == b"END": + break + value = header[8:].strip() + 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") + headers[keyword] = value + + naxis = int(headers[b"NAXIS"]) + if naxis == 0: + raise ValueError("No image data") + elif naxis == 1: + self._size = 1, int(headers[b"NAXIS1"]) + else: + self._size = int(headers[b"NAXIS1"]), int(headers[b"NAXIS2"]) + + number_of_bits = int(headers[b"BITPIX"]) + if number_of_bits == 8: + self.mode = "L" + elif number_of_bits == 16: + self.mode = "I" + # rawmode = "I;16S" + elif number_of_bits == 32: + self.mode = "I" + elif number_of_bits in (-32, -64): + self.mode = "F" + # rawmode = "F" if number_of_bits == -32 else "F;64F" + + offset = math.ceil(self.fp.tell() / 2880) * 2880 + self.tile = [("raw", (0, 0) + self.size, offset, (self.mode, 0, -1))] + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(FitsImageFile.format, FitsImageFile, _accept) + +Image.register_extensions(FitsImageFile.format, [".fit", ".fits"]) diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py index be926cadb84..440240a9958 100644 --- a/src/PIL/FitsStubImagePlugin.py +++ b/src/PIL/FitsStubImagePlugin.py @@ -9,7 +9,8 @@ # See the README file for information on usage and redistribution. # -from . import Image, ImageFile +from . import FitsImagePlugin, Image, ImageFile +from ._deprecate import deprecate _handler = None @@ -23,35 +24,40 @@ def register_handler(handler): global _handler _handler = handler -# -------------------------------------------------------------------- -# Image adapter + deprecate( + "FitsStubImagePlugin", + 10, + action="FITS images can now be read without " + "a handler through FitsImagePlugin instead", + ) + # Override FitsImagePlugin with this handler + # for backwards compatibility + try: + Image.ID.remove(FITSStubImageFile.format) + except ValueError: + pass -def _accept(prefix): - return prefix[:6] == b"SIMPLE" + Image.register_open( + FITSStubImageFile.format, FITSStubImageFile, FitsImagePlugin._accept + ) class FITSStubImageFile(ImageFile.StubImageFile): - format = "FITS" - format_description = "FITS" + format = FitsImagePlugin.FitsImageFile.format + format_description = FitsImagePlugin.FitsImageFile.format_description def _open(self): - offset = self.fp.tell() - if not _accept(self.fp.read(6)): - raise SyntaxError("Not a FITS file") - - # FIXME: add more sanity checks here; mandatory header items - # include SIMPLE, BITPIX, NAXIS, etc. + im = FitsImagePlugin.FitsImageFile(self.fp) + self._size = im.size + self.mode = im.mode + self.tile = [] self.fp.seek(offset) - # make something up - self.mode = "F" - self.size = 1, 1 - loader = self._load() if loader: loader.open(self) @@ -61,15 +67,10 @@ def _load(self): def _save(im, fp, filename): - if _handler is None or not hasattr("_handler", "save"): - raise IOError("FITS save handler not installed") - _handler.save(im, fp, filename) + raise OSError("FITS save handler not installed") # -------------------------------------------------------------------- # Registry -Image.register_open(FITSStubImageFile.format, FITSStubImageFile, _accept) Image.register_save(FITSStubImageFile.format, _save) - -Image.register_extensions(FITSStubImageFile.format, [".fit", ".fits"]) diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 2c190b6354f..908bed9f427 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -15,24 +15,30 @@ # See the README file for information on usage and redistribution. # +import os from . import Image, ImageFile, ImagePalette -from ._binary import i8, i16le as i16, i32le as i32, o8 - -__version__ = "0.2" - +from ._binary import i16le as i16 +from ._binary import i32le as i32 +from ._binary import o8 # # decoder + def _accept(prefix): - return len(prefix) >= 6 and i16(prefix[4:6]) in [0xAF11, 0xAF12] + return ( + len(prefix) >= 6 + and i16(prefix, 4) in [0xAF11, 0xAF12] + and i16(prefix, 14) in [0, 3] # flags + ) ## # Image plugin for the FLI/FLC animation format. Use the seek # method to load individual frames. + class FliImageFile(ImageFile.ImageFile): format = "FLI" @@ -43,21 +49,20 @@ def _open(self): # HEAD s = self.fp.read(128) - magic = i16(s[4:6]) - if not (magic in [0xAF11, 0xAF12] and - i16(s[14:16]) in [0, 3] and # flags - s[20:22] == b"\x00\x00"): # reserved + if not (_accept(s) and s[20:22] == b"\x00\x00"): raise SyntaxError("not an FLI/FLC file") # frames - self.__framecount = i16(s[6:8]) + self.n_frames = i16(s, 6) + self.is_animated = self.n_frames > 1 # image characteristics self.mode = "P" - self.size = i16(s[8:10]), i16(s[10:12]) + self._size = i16(s, 8), i16(s, 10) # animation speed - duration = i32(s[16:20]) + duration = i32(s, 16) + magic = i16(s, 4) if magic == 0xAF11: duration = (duration * 1000) // 70 self.info["duration"] = duration @@ -69,25 +74,33 @@ def _open(self): self.__offset = 128 - if i16(s[4:6]) == 0xF100: + if i16(s, 4) == 0xF100: # prefix chunk; ignore it self.__offset = self.__offset + i32(s) s = self.fp.read(16) - if i16(s[4:6]) == 0xF1FA: + if i16(s, 4) == 0xF1FA: # look for palette chunk - s = self.fp.read(6) - if i16(s[4:6]) == 11: - self._palette(palette, 2) - elif i16(s[4:6]) == 4: - self._palette(palette, 0) - - palette = [o8(r)+o8(g)+o8(b) for (r, g, b) in palette] + number_of_subchunks = i16(s, 6) + chunk_size = None + for _ in range(number_of_subchunks): + if chunk_size is not None: + self.fp.seek(chunk_size - 6, os.SEEK_CUR) + s = self.fp.read(6) + chunk_type = i16(s, 4) + if chunk_type in (4, 11): + self._palette(palette, 2 if chunk_type == 11 else 0) + break + chunk_size = i32(s) + if not chunk_size: + break + + palette = [o8(r) + o8(g) + o8(b) for (r, g, b) in palette] self.palette = ImagePalette.raw("RGB", b"".join(palette)) # set things up to decode first frame self.__frame = -1 - self.__fp = self.fp + self._fp = self.fp self.__rewind = self.fp.tell() self.seek(0) @@ -97,26 +110,18 @@ def _palette(self, palette, shift): i = 0 for e in range(i16(self.fp.read(2))): s = self.fp.read(2) - i = i + i8(s[0]) - n = i8(s[1]) + i = i + s[0] + n = s[1] if n == 0: n = 256 s = self.fp.read(n * 3) for n in range(0, len(s), 3): - r = i8(s[n]) << shift - g = i8(s[n+1]) << shift - b = i8(s[n+2]) << shift + r = s[n] << shift + g = s[n + 1] << shift + b = s[n + 2] << shift palette[i] = (r, g, b) i += 1 - @property - def n_frames(self): - return self.__framecount - - @property - def is_animated(self): - return self.__framecount > 1 - def seek(self, frame): if not self._seek_check(frame): return @@ -129,15 +134,18 @@ def seek(self, frame): def _seek(self, frame): if frame == 0: self.__frame = -1 - self.__fp.seek(self.__rewind) + self._fp.seek(self.__rewind) self.__offset = 128 + else: + # ensure that the previous frame was loaded + self.load() if frame != self.__frame + 1: - raise ValueError("cannot seek to frame %d" % frame) + raise ValueError(f"cannot seek to frame {frame}") self.__frame = frame # move to next frame - self.fp = self.__fp + self.fp = self._fp self.fp.seek(self.__offset) s = self.fp.read(4) @@ -147,7 +155,7 @@ def _seek(self, frame): framesize = i32(s) self.decodermaxblock = framesize - self.tile = [("fli", (0, 0)+self.size, self.__offset, None)] + self.tile = [("fli", (0, 0) + self.size, self.__offset, None)] self.__offset += framesize diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 46e49bc4ee1..c5fc80b37ba 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -14,26 +14,24 @@ # See the README file for information on usage and redistribution. # -from __future__ import print_function import os + from . import Image, _binary WIDTH = 800 def puti16(fp, values): - # write network order (big-endian) 16-bit sequence + """Write network order (big-endian) 16-bit sequence""" for v in values: if v < 0: v += 65536 fp.write(_binary.o16be(v)) -## -# Base class for raster font file handlers. - -class FontFile(object): +class FontFile: + """Base class for raster font file handlers.""" bitmap = None @@ -46,7 +44,7 @@ def __getitem__(self, ix): return self.glyph[ix] def compile(self): - "Create metrics and bitmap" + """Create metrics and bitmap""" if self.bitmap: return @@ -61,7 +59,7 @@ def compile(self): w = w + (src[2] - src[0]) if w > WIDTH: lines += 1 - w = (src[2] - src[0]) + w = src[2] - src[0] maxwidth = max(maxwidth, w) xsize = maxwidth @@ -90,11 +88,10 @@ def compile(self): x = xx s = src[0] + x0, src[1] + y0, src[2] + x0, src[3] + y0 self.bitmap.paste(im.crop(src), s) - # print(chr(i), dst, s) self.metrics[i] = d, dst, s def save(self, filename): - "Save font" + """Save font""" self.compile() @@ -104,7 +101,7 @@ def save(self, filename): # font metrics with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp: fp.write(b"PILfont\n") - fp.write((";;;;;;%d;\n" % self.ysize).encode('ascii')) # HACK!!! + fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!! fp.write(b"DATA\n") for id in range(256): m = self.metrics[id] diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 23f15f45936..a55376d0e08 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -14,35 +14,31 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function - -from . import Image, ImageFile -from ._binary import i32le as i32, i8 - import olefile -__version__ = "0.1" +from . import Image, ImageFile +from ._binary import i32le as i32 # we map from colour field tuples to (mode, rawmode) descriptors MODES = { # opacity - (0x00007ffe): ("A", "L"), + (0x00007FFE,): ("A", "L"), # monochrome (0x00010000,): ("L", "L"), - (0x00018000, 0x00017ffe): ("RGBA", "LA"), + (0x00018000, 0x00017FFE): ("RGBA", "LA"), # photo YCC (0x00020000, 0x00020001, 0x00020002): ("RGB", "YCC;P"), - (0x00028000, 0x00028001, 0x00028002, 0x00027ffe): ("RGBA", "YCCA;P"), + (0x00028000, 0x00028001, 0x00028002, 0x00027FFE): ("RGBA", "YCCA;P"), # standard RGB (NIFRGB) (0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"), - (0x00038000, 0x00038001, 0x00038002, 0x00037ffe): ("RGBA", "RGBA"), + (0x00038000, 0x00038001, 0x00038002, 0x00037FFE): ("RGBA", "RGBA"), } # # -------------------------------------------------------------------- + def _accept(prefix): return prefix[:8] == olefile.MAGIC @@ -50,6 +46,7 @@ def _accept(prefix): ## # Image plugin for the FlashPix images. + class FpxImageFile(ImageFile.ImageFile): format = "FPX" @@ -62,8 +59,8 @@ def _open(self): try: self.ole = olefile.OleFileIO(self.fp) - except IOError: - raise SyntaxError("not an FPX file; invalid OLE file") + except OSError as e: + raise SyntaxError("not an FPX file; invalid OLE file") from e if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B": raise SyntaxError("not an FPX file; bad root CLSID") @@ -74,14 +71,13 @@ def _open_index(self, index=1): # # get the Image Contents Property Set - prop = self.ole.getproperties([ - "Data Object Store %06d" % index, - "\005Image Contents" - ]) + prop = self.ole.getproperties( + [f"Data Object Store {index:06d}", "\005Image Contents"] + ) # size (highest resolution) - self.size = prop[0x1000002], prop[0x1000003] + self._size = prop[0x1000002], prop[0x1000003] size = max(self.size) i = 1 @@ -101,9 +97,12 @@ def _open_index(self, index=1): s = prop[0x2000002 | id] colors = [] - for i in range(i32(s, 4)): + bands = i32(s, 4) + if bands > 4: + raise OSError("Invalid number of bands") + for i in range(bands): # note: for now, we ignore the "uncalibrated" flag - colors.append(i32(s, 8+i*4) & 0x7fffffff) + colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF) self.mode, self.rawmode = MODES[tuple(colors)] @@ -114,8 +113,6 @@ def _open_index(self, index=1): if id in prop: self.jpeg[i] = prop[id] - # print(len(self.jpeg), "tables loaded") - self._open_subimage(1, self.maxid) def _open_subimage(self, index=1, subimage=0): @@ -123,9 +120,9 @@ def _open_subimage(self, index=1, subimage=0): # setup tile descriptors for a given subimage stream = [ - "Data Object Store %06d" % index, - "Resolution %04d" % subimage, - "Subimage 0000 Header" + f"Data Object Store {index:06d}", + f"Resolution {subimage:04d}", + "Subimage 0000 Header", ] fp = self.ole.openstream(stream) @@ -143,10 +140,8 @@ def _open_subimage(self, index=1, subimage=0): offset = i32(s, 28) length = i32(s, 32) - # print(size, self.mode, self.rawmode) - if size != self.size: - raise IOError("subimage mismatch") + raise OSError("subimage mismatch") # get tile descriptors fp.seek(28 + offset) @@ -159,22 +154,37 @@ def _open_subimage(self, index=1, subimage=0): for i in range(0, len(s), length): - compression = i32(s, i+8) + x1 = min(xsize, x + xtile) + y1 = min(ysize, y + ytile) + + compression = i32(s, i + 8) if compression == 0: - self.tile.append(("raw", (x, y, x+xtile, y+ytile), - i32(s, i) + 28, (self.rawmode))) + self.tile.append( + ( + "raw", + (x, y, x1, y1), + i32(s, i) + 28, + (self.rawmode,), + ) + ) elif compression == 1: # FIXME: the fill decoder is not implemented - self.tile.append(("fill", (x, y, x+xtile, y+ytile), - i32(s, i) + 28, (self.rawmode, s[12:16]))) + self.tile.append( + ( + "fill", + (x, y, x1, y1), + i32(s, i) + 28, + (self.rawmode, s[12:16]), + ) + ) elif compression == 2: - internal_color_conversion = i8(s[14]) - jpeg_tables = i8(s[15]) + internal_color_conversion = s[14] + jpeg_tables = s[15] rawmode = self.rawmode if internal_color_conversion: @@ -191,8 +201,14 @@ def _open_subimage(self, index=1, subimage=0): # The image is stored as defined by rawmode jpegmode = rawmode - self.tile.append(("jpeg", (x, y, x+xtile, y+ytile), - i32(s, i) + 28, (rawmode, jpegmode))) + self.tile.append( + ( + "jpeg", + (x, y, x1, y1), + i32(s, i) + 28, + (rawmode, jpegmode), + ) + ) # FIXME: jpeg tables are tile dependent; the prefix # data must be placed in the tile descriptor itself! @@ -201,7 +217,7 @@ def _open_subimage(self, index=1, subimage=0): self.tile_prefix = self.jpeg[jpeg_tables] else: - raise IOError("unknown/invalid compression") + raise OSError("unknown/invalid compression") x = x + xtile if x >= xsize: @@ -215,14 +231,15 @@ def _open_subimage(self, index=1, subimage=0): def load(self): if not self.fp: - self.fp = self.ole.openstream(self.stream[:2] + - ["Subimage 0000 Data"]) + self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"]) return ImageFile.ImageFile.load(self) + # # -------------------------------------------------------------------- + Image.register_open(FpxImageFile.format, FpxImageFile, _accept) Image.register_extension(FpxImageFile.format, ".fpx") diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index 9b9809062d2..1b714eb4f65 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -9,7 +9,8 @@ Independence War 2: Edge Of Chaos - Texture File Format - 16 October 2001 The textures used for 3D objects in Independence War 2: Edge Of Chaos are in a -packed custom format called FTEX. This file format uses file extensions FTC and FTU. +packed custom format called FTEX. This file format uses file extensions FTC +and FTU. * FTC files are compressed textures (using standard texture compression). * FTU files are not compressed. Texture File Format @@ -19,35 +20,60 @@ {format_directory} {data} Where: -{header} = { u32:magic, u32:version, u32:width, u32:height, u32:mipmap_count, u32:format_count } +{header} = { + u32:magic, + u32:version, + u32:width, + u32:height, + u32:mipmap_count, + u32:format_count +} * The "magic" number is "FTEX". * "width" and "height" are the dimensions of the texture. * "mipmap_count" is the number of mipmaps in the texture. -* "format_count" is the number of texture formats (different versions of the same texture) in this file. +* "format_count" is the number of texture formats (different versions of the +same texture) in this file. {format_directory} = format_count * { u32:format, u32:where } -The format value is 0 for DXT1 compressed textures and 1 for 24-bit RGB uncompressed textures. +The format value is 0 for DXT1 compressed textures and 1 for 24-bit RGB +uncompressed textures. The texture data for a format starts at the position "where" in the file. Each set of texture data in the file has the following structure: {data} = format_count * { u32:mipmap_size, mipmap_size * { u8 } } -* "mipmap_size" is the number of bytes in that mip level. For compressed textures this is the -size of the texture data compressed with DXT1. For 24 bit uncompressed textures, this is 3 * width * height. -Following this are the image bytes for that mipmap level. +* "mipmap_size" is the number of bytes in that mip level. For compressed +textures this is the size of the texture data compressed with DXT1. For 24 bit +uncompressed textures, this is 3 * width * height. Following this are the image +bytes for that mipmap level. Note: All data is stored in little-Endian (Intel) byte order. """ import struct +from enum import IntEnum from io import BytesIO -from . import Image, ImageFile +from . import Image, ImageFile +from ._deprecate import deprecate MAGIC = b"FTEX" -FORMAT_DXT1 = 0 -FORMAT_UNCOMPRESSED = 1 + + +class Format(IntEnum): + DXT1 = 0 + UNCOMPRESSED = 1 + + +def __getattr__(name): + for enum, prefix in {Format: "FORMAT_"}.items(): + if name.startswith(prefix): + name = name[len(prefix) :] + 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}'") class FtexImageFile(ImageFile.ImageFile): @@ -55,29 +81,31 @@ class FtexImageFile(ImageFile.ImageFile): format_description = "Texture File Format (IW2:EOC)" def _open(self): - magic = struct.unpack("= 8 and i32(prefix[:4]) >= 20 and i32(prefix[4:8]) in (1, 2) + return len(prefix) >= 8 and i32(prefix, 0) >= 20 and i32(prefix, 4) in (1, 2) ## # Image plugin for the GIMP brush format. + class GbrImageFile(ImageFile.ImageFile): format = "GBR" @@ -42,11 +43,11 @@ class GbrImageFile(ImageFile.ImageFile): def _open(self): header_size = i32(self.fp.read(4)) - version = i32(self.fp.read(4)) if header_size < 20: raise SyntaxError("not a GIMP brush") + version = i32(self.fp.read(4)) if version not in (1, 2): - raise SyntaxError("Unsupported GIMP brush version: %s" % version) + raise SyntaxError(f"Unsupported GIMP brush version: {version}") width = i32(self.fp.read(4)) height = i32(self.fp.read(4)) @@ -54,25 +55,25 @@ def _open(self): if width <= 0 or height <= 0: raise SyntaxError("not a GIMP brush") if color_depth not in (1, 4): - raise SyntaxError("Unsupported GIMP brush color depth: %s" % color_depth) + raise SyntaxError(f"Unsupported GIMP brush color depth: {color_depth}") if version == 1: - comment_length = header_size-20 + comment_length = header_size - 20 else: - comment_length = header_size-28 + comment_length = header_size - 28 magic_number = self.fp.read(4) - if magic_number != b'GIMP': + if magic_number != b"GIMP": raise SyntaxError("not a GIMP brush, bad magic number") - self.info['spacing'] = i32(self.fp.read(4)) + self.info["spacing"] = i32(self.fp.read(4)) comment = self.fp.read(comment_length)[:-1] if color_depth == 1: self.mode = "L" else: - self.mode = 'RGBA' + self.mode = "RGBA" - self.size = width, height + self._size = width, height self.info["comment"] = comment @@ -83,11 +84,15 @@ def _open(self): self._data_size = width * height * color_depth def load(self): - self.im = Image.core.new(self.mode, self.size) - self.frombytes(self.fp.read(self._data_size)) + if not self.im: + self.im = Image.core.new(self.mode, self.size) + self.frombytes(self.fp.read(self._data_size)) + return Image.Image.load(self) + # # registry + Image.register_open(GbrImageFile.format, GbrImageFile, _accept) Image.register_extension(GbrImageFile.format, ".gbr") diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 09ab5ec6964..1ac3b67b791 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -14,35 +14,31 @@ # -# NOTE: This format cannot be automatically recognized, so the -# class is not registered for use with Image.open(). To open a -# gd file, use the GdImageFile.open() function instead. +""" +.. note:: + This format cannot be automatically recognized, so the + class is not registered for use with :py:func:`PIL.Image.open()`. To open a + gd file, use the :py:func:`PIL.GdImageFile.open()` function instead. -# THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This -# implementation is provided for convenience and demonstrational -# purposes only. +.. warning:: + THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This + implementation is provided for convenience and demonstrational + purposes only. +""" -from . import ImageFile, ImagePalette +from . import ImageFile, ImagePalette, UnidentifiedImageError from ._binary import i16be as i16 -from ._util import isPath +from ._binary import i32be as i32 -__version__ = "0.1" - -try: - import builtins -except ImportError: - import __builtin__ - builtins = __builtin__ - - -## -# Image plugin for the GD uncompressed format. Note that this format -# is not supported by the standard Image.open function. To use -# this plugin, you have to import the GdImageFile module and -# use the GdImageFile.open function. class GdImageFile(ImageFile.ImageFile): + """ + Image plugin for the GD uncompressed format. Note that this format + is not supported by the standard :py:func:`PIL.Image.open()` function. To use + this plugin, you have to import the :py:mod:`PIL.GdImageFile` module and + use the :py:func:`PIL.GdImageFile.open()` function. + """ format = "GD" format_description = "GD uncompressed images" @@ -50,41 +46,50 @@ class GdImageFile(ImageFile.ImageFile): def _open(self): # Header - s = self.fp.read(775) + s = self.fp.read(1037) + + if not i16(s) in [65534, 65535]: + raise SyntaxError("Not a valid GD 2.x .gd file") self.mode = "L" # FIXME: "P" - self.size = i16(s[0:2]), i16(s[2:4]) + self._size = i16(s, 2), i16(s, 4) + + true_color = s[6] + true_color_offset = 2 if true_color else 0 # transparency index - tindex = i16(s[5:7]) + tindex = i32(s, 7 + true_color_offset) if tindex < 256: - self.info["transparent"] = tindex + self.info["transparency"] = tindex - self.palette = ImagePalette.raw("RGB", s[7:]) + self.palette = ImagePalette.raw( + "XBGR", s[7 + true_color_offset + 4 : 7 + true_color_offset + 4 + 256 * 4] + ) - self.tile = [("raw", (0, 0)+self.size, 775, ("L", 0, -1))] + self.tile = [ + ( + "raw", + (0, 0) + self.size, + 7 + true_color_offset + 4 + 256 * 4, + ("L", 0, 1), + ) + ] def open(fp, mode="r"): """ Load texture from a GD image file. - :param filename: GD file name, or an opened file handle. + :param fp: GD file name, or an opened file handle. :param mode: Optional mode. In this version, if the mode argument is given, it must be "r". :returns: An image instance. - :raises IOError: If the image could not be read. + :raises OSError: If the image could not be read. """ if mode != "r": raise ValueError("bad mode") - if isPath(fp): - filename = fp - fp = builtins.open(fp, "rb") - else: - filename = "" - try: - return GdImageFile(fp, filename) - except SyntaxError: - raise IOError("cannot identify this image file") + return GdImageFile(fp) + except SyntaxError as e: + raise UnidentifiedImageError("cannot identify this image file") from e diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 71d8ce59a5d..dd1b21f2e63 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -24,17 +24,33 @@ # See the README file for information on usage and redistribution. # -from . import Image, ImageFile, ImagePalette, ImageChops, ImageSequence -from ._binary import i8, i16le as i16, o8, o16le as o16 - import itertools +import math +import os +import subprocess +from enum import IntEnum + +from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence +from ._binary import i16le as i16 +from ._binary import o8 +from ._binary import o16le as o16 + + +class LoadingStrategy(IntEnum): + """.. versionadded:: 9.1.0""" -__version__ = "0.9" + RGB_AFTER_FIRST = 0 + RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1 + RGB_ALWAYS = 2 +#: .. versionadded:: 9.1.0 +LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST + # -------------------------------------------------------------------- # Identify/read GIF files + def _accept(prefix): return prefix[:6] in [b"GIF87a", b"GIF89a"] @@ -43,6 +59,7 @@ def _accept(prefix): # Image plugin for GIF images. This plugin supports both GIF87 and # GIF89 images. + class GifImageFile(ImageFile.ImageFile): format = "GIF" @@ -53,35 +70,39 @@ class GifImageFile(ImageFile.ImageFile): def data(self): s = self.fp.read(1) - if s and i8(s): - return self.fp.read(i8(s)) + if s and s[0]: + return self.fp.read(s[0]) return None + def _is_palette_needed(self, p): + for i in range(0, len(p), 3): + if not (i // 3 == p[i] == p[i + 1] == p[i + 2]): + return True + return False + def _open(self): # Screen s = self.fp.read(13) - if s[:6] not in [b"GIF87a", b"GIF89a"]: + if not _accept(s): raise SyntaxError("not a GIF file") self.info["version"] = s[:6] - self.size = i16(s[6:]), i16(s[8:]) + self._size = i16(s, 6), i16(s, 8) self.tile = [] - flags = i8(s[10]) + flags = s[10] bits = (flags & 7) + 1 if flags & 128: # get global palette - self.info["background"] = i8(s[11]) + self.info["background"] = s[11] # check if palette contains colour indices p = self.fp.read(3 << bits) - for i in range(0, len(p), 3): - if not (i//3 == i8(p[i]) == i8(p[i+1]) == i8(p[i+2])): - p = ImagePalette.raw("RGB", p) - self.global_palette = self.palette = p - break + if self._is_palette_needed(p): + p = ImagePalette.raw("RGB", p) + self.global_palette = self.palette = p - self.__fp = self.fp # FIXME: hack + self._fp = self.fp # FIXME: hack self.__rewind = self.fp.tell() self._n_frames = None self._is_animated = None @@ -93,7 +114,7 @@ def n_frames(self): current = self.tell() try: while True: - self.seek(self.tell() + 1) + self._seek(self.tell() + 1, False) except EOFError: self._n_frames = self.tell() + 1 self.seek(current) @@ -106,53 +127,53 @@ def is_animated(self): self._is_animated = self._n_frames != 1 else: current = self.tell() - - try: - self.seek(1) + if current: self._is_animated = True - except EOFError: - self._is_animated = False + else: + try: + self._seek(1, False) + self._is_animated = True + except EOFError: + self._is_animated = False - self.seek(current) + self.seek(current) return self._is_animated def seek(self, frame): if not self._seek_check(frame): return if frame < self.__frame: + self.im = None self._seek(0) last_frame = self.__frame for f in range(self.__frame + 1, frame + 1): try: self._seek(f) - except EOFError: + except EOFError as e: self.seek(last_frame) - raise EOFError("no more images in GIF file") + raise EOFError("no more images in GIF file") from e - def _seek(self, frame): + def _seek(self, frame, update_image=True): if frame == 0: # rewind self.__offset = 0 self.dispose = None - self.dispose_extent = [0, 0, 0, 0] # x0, y0, x1, y1 self.__frame = -1 - self.__fp.seek(self.__rewind) - self._prev_im = None + self._fp.seek(self.__rewind) self.disposal_method = 0 + if "comment" in self.info: + del self.info["comment"] else: # ensure that the previous frame was loaded - if not self.im: + if self.tile and update_image: self.load() if frame != self.__frame + 1: - raise ValueError("cannot seek to frame %d" % frame) - self.__frame = frame + raise ValueError(f"cannot seek to frame {frame}") - self.tile = [] - - self.fp = self.__fp + self.fp = self._fp if self.__offset: # backup to last frame self.fp.seek(self.__offset) @@ -160,15 +181,20 @@ def _seek(self, frame): pass self.__offset = 0 - if self.dispose: - self.im.paste(self.dispose, self.dispose_extent) + s = self.fp.read(1) + if not s or s == b";": + raise EOFError - from copy import copy - self.palette = copy(self.global_palette) + palette = None + info = {} + frame_transparency = None + interlace = None + frame_dispose_extent = None while True: - s = self.fp.read(1) + if not s: + s = self.fp.read(1) if not s or s == b";": break @@ -178,14 +204,14 @@ def _seek(self, frame): # s = self.fp.read(1) block = self.data() - if i8(s) == 249: + if s[0] == 249: # # graphic control extension # - flags = i8(block[0]) + flags = block[0] if flags & 1: - self.info["transparency"] = i8(block[3]) - self.info["duration"] = i16(block[1:3]) * 10 + frame_transparency = block[3] + info["duration"] = i16(block, 1) * 10 # disposal method - find the value of bits 4 - 6 dispose_bits = 0b00011100 & flags @@ -196,20 +222,33 @@ def _seek(self, frame): # correct, but it seems to prevent the last # frame from looking odd for some animations self.disposal_method = dispose_bits - elif i8(s) == 254: + elif s[0] == 254: # # comment extension # - self.info["comment"] = block - elif i8(s) == 255: + comment = b"" + + # Read this comment block + while block: + comment += block + block = self.data() + + if "comment" in info: + # If multiple comment blocks in frame, separate with \n + info["comment"] += b"\n" + comment + else: + info["comment"] = comment + s = None + continue + elif s[0] == 255 and frame == 0: # # application extension # - self.info["extension"] = block, self.fp.tell() + info["extension"] = block, self.fp.tell() if block[:11] == b"NETSCAPE2.0": block = self.data() - if len(block) >= 3 and i8(block[0]) == 1: - self.info["loop"] = i16(block[1:3]) + if len(block) >= 3 and block[0] == 1: + self.info["loop"] = i16(block, 1) while self.data(): pass @@ -220,86 +259,215 @@ def _seek(self, frame): s = self.fp.read(9) # extent - x0, y0 = i16(s[0:]), i16(s[2:]) - x1, y1 = x0 + i16(s[4:]), y0 + i16(s[6:]) - self.dispose_extent = x0, y0, x1, y1 - flags = i8(s[8]) + x0, y0 = i16(s, 0), i16(s, 2) + x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6) + if (x1 > self.size[0] or y1 > self.size[1]) and update_image: + self._size = max(x1, self.size[0]), max(y1, self.size[1]) + Image._decompression_bomb_check(self._size) + frame_dispose_extent = x0, y0, x1, y1 + flags = s[8] interlace = (flags & 64) != 0 if flags & 128: bits = (flags & 7) + 1 - self.palette =\ - ImagePalette.raw("RGB", self.fp.read(3 << bits)) + p = self.fp.read(3 << bits) + if self._is_palette_needed(p): + palette = ImagePalette.raw("RGB", p) + else: + palette = False # image data - bits = i8(self.fp.read(1)) + bits = self.fp.read(1)[0] self.__offset = self.fp.tell() - self.tile = [("gif", - (x0, y0, x1, y1), - self.__offset, - (bits, interlace))] break else: pass - # raise IOError, "illegal GIF tag `%x`" % i8(s) + # raise OSError, "illegal GIF tag `%x`" % s[0] + s = None + + if interlace is None: + # self._fp = None + raise EOFError + + self.__frame = frame + if not update_image: + return + + self.tile = [] + + if self.dispose: + self.im.paste(self.dispose, self.dispose_extent) + + self._frame_palette = palette if palette is not None else self.global_palette + self._frame_transparency = frame_transparency + if frame == 0: + if self._frame_palette: + if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: + self.mode = "RGBA" if frame_transparency is not None else "RGB" + else: + self.mode = "P" + else: + self.mode = "L" + + if not palette and self.global_palette: + from copy import copy + + palette = copy(self.global_palette) + self.palette = palette + else: + if self.mode == "P": + if ( + LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY + or palette + ): + self.pyaccess = None + if "transparency" in self.info: + self.im.putpalettealpha(self.info["transparency"], 0) + self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) + self.mode = "RGBA" + del self.info["transparency"] + else: + self.mode = "RGB" + self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) + + def _rgb(color): + if self._frame_palette: + color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]) + else: + color = (color, color, color) + return color + self.dispose_extent = frame_dispose_extent try: if self.disposal_method < 2: # do not dispose or none specified self.dispose = None elif self.disposal_method == 2: # replace with background colour - self.dispose = Image.core.fill("P", self.size, - self.info["background"]) + + # only dispose the extent in this frame + x0, y0, x1, y1 = self.dispose_extent + dispose_size = (x1 - x0, y1 - y0) + + Image._decompression_bomb_check(dispose_size) + + # by convention, attempt to use transparency first + dispose_mode = "P" + color = self.info.get("transparency", frame_transparency) + if color is not None: + if self.mode in ("RGB", "RGBA"): + dispose_mode = "RGBA" + color = _rgb(color) + (0,) + else: + color = self.info.get("background", 0) + if self.mode in ("RGB", "RGBA"): + dispose_mode = "RGB" + color = _rgb(color) + self.dispose = Image.core.fill(dispose_mode, dispose_size, color) else: # replace with previous contents - if self.im: - self.dispose = self.im.copy() - - # only dispose the extent in this frame - if self.dispose: - self.dispose = self._crop(self.dispose, self.dispose_extent) - except (AttributeError, KeyError): + if self.im is not None: + # only dispose the extent in this frame + self.dispose = self._crop(self.im, self.dispose_extent) + elif frame_transparency is not None: + x0, y0, x1, y1 = self.dispose_extent + dispose_size = (x1 - x0, y1 - y0) + + Image._decompression_bomb_check(dispose_size) + dispose_mode = "P" + color = frame_transparency + if self.mode in ("RGB", "RGBA"): + dispose_mode = "RGBA" + color = _rgb(frame_transparency) + (0,) + self.dispose = Image.core.fill(dispose_mode, dispose_size, color) + except AttributeError: pass - if not self.tile: - # self.__fp = None - raise EOFError + if interlace is not None: + transparency = -1 + if frame_transparency is not None: + if frame == 0: + if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS: + self.info["transparency"] = frame_transparency + elif self.mode not in ("RGB", "RGBA"): + transparency = frame_transparency + self.tile = [ + ( + "gif", + (x0, y0, x1, y1), + self.__offset, + (bits, interlace, transparency), + ) + ] + + if info.get("comment"): + self.info["comment"] = info["comment"] + for k in ["duration", "extension"]: + if k in info: + self.info[k] = info[k] + elif k in self.info: + del self.info[k] + + def load_prepare(self): + temp_mode = "P" if self._frame_palette else "L" + self._prev_im = None + if self.__frame == 0: + if self._frame_transparency is not None: + self.im = Image.core.fill( + temp_mode, self.size, self._frame_transparency + ) + elif self.mode in ("RGB", "RGBA"): + self._prev_im = self.im + if self._frame_palette: + self.im = Image.core.fill("P", self.size, self._frame_transparency or 0) + self.im.putpalette(*self._frame_palette.getdata()) + else: + self.im = None + self.mode = temp_mode + self._frame_palette = None + + super().load_prepare() + + def load_end(self): + if self.__frame == 0: + if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: + if self._frame_transparency is not None: + self.im.putpalettealpha(self._frame_transparency, 0) + self.mode = "RGBA" + else: + self.mode = "RGB" + self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG) + return + if not self._prev_im: + return + if self._frame_transparency is not None: + self.im.putpalettealpha(self._frame_transparency, 0) + frame_im = self.im.convert("RGBA") + else: + frame_im = self.im.convert("RGB") + frame_im = self._crop(frame_im, self.dispose_extent) - self.mode = "L" - if self.palette: - self.mode = "P" + self.im = self._prev_im + self.mode = self.im.mode + if frame_im.mode == "RGBA": + self.im.paste(frame_im, self.dispose_extent, frame_im) + else: + self.im.paste(frame_im, self.dispose_extent) def tell(self): return self.__frame - def load_end(self): - ImageFile.ImageFile.load_end(self) - - # if the disposal method is 'do not dispose', transparent - # pixels should show the content of the previous frame - if self._prev_im and self.disposal_method == 1: - # we do this by pasting the updated area onto the previous - # frame which we then use as the current image content - updated = self._crop(self.im, self.dispose_extent) - self._prev_im.paste(updated, self.dispose_extent, - updated.convert('RGBA')) - self.im = self._prev_im - self._prev_im = self.im.copy() # -------------------------------------------------------------------- # Write GIF files -RAWMODE = { - "1": "L", - "L": "L", - "P": "P" -} + +RAWMODE = {"1": "L", "L": "L", "P": "P"} -def _normalize_mode(im, initial_call=False): +def _normalize_mode(im): """ Takes an image (or frame), returns an image in a mode that is appropriate for saving in a Gif. @@ -307,25 +475,20 @@ def _normalize_mode(im, initial_call=False): It may return the original image, or it may return an image converted to palette or 'L' mode. - UNDONE: What is the point of mucking with the initial call palette, for - an image that shouldn't have a palette, or it would be a mode 'P' and - get returned in the RAWMODE clause. - :param im: Image object - :param initial_call: Default false, set to true for a single frame. :returns: Image object """ if im.mode in RAWMODE: im.load() return im if Image.getmodebase(im.mode) == "RGB": - if initial_call: - palette_size = 256 - if im.palette: - palette_size = len(im.palette.getdata()[1]) // 3 - return im.convert("P", palette=Image.ADAPTIVE, colors=palette_size) - else: - return im.convert("P") + im = im.convert("P", palette=Image.Palette.ADAPTIVE) + if im.palette.mode == "RGBA": + for rgba in im.palette.colors.keys(): + if rgba[3] == 0: + im.info["transparency"] = im.palette.colors[rgba] + break + return im return im.convert("L") @@ -347,30 +510,44 @@ def _normalize_palette(im, palette, info): if isinstance(palette, (bytes, bytearray, list)): source_palette = bytearray(palette[:768]) if isinstance(palette, ImagePalette.ImagePalette): - source_palette = bytearray(itertools.chain.from_iterable( - zip(palette.palette[:256], - palette.palette[256:512], - palette.palette[512:768]))) + source_palette = bytearray(palette.palette) if im.mode == "P": if not source_palette: source_palette = im.im.getpalette("RGB")[:768] else: # L-mode if not source_palette: - source_palette = bytearray(i//3 for i in range(768)) - im.palette = ImagePalette.ImagePalette("RGB", - palette=source_palette) + source_palette = bytearray(i // 3 for i in range(768)) + im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette) - used_palette_colors = _get_optimize(im, info) - if used_palette_colors is not None: - return im.remap_palette(used_palette_colors, source_palette) + if palette: + used_palette_colors = [] + for i in range(0, len(source_palette), 3): + source_color = tuple(source_palette[i : i + 3]) + index = im.palette.colors.get(source_color) + if index in used_palette_colors: + index = None + used_palette_colors.append(index) + for i, index in enumerate(used_palette_colors): + if index is None: + for j in range(len(used_palette_colors)): + if j not in used_palette_colors: + used_palette_colors[i] = j + break + im = im.remap_palette(used_palette_colors) + else: + used_palette_colors = _get_optimize(im, info) + if used_palette_colors is not None: + return im.remap_palette(used_palette_colors, source_palette) im.palette.palette = source_palette return im def _write_single_frame(im, fp, palette): - im_out = _normalize_mode(im, True) + im_out = _normalize_mode(im) + for k, v in im_out.info.items(): + im.encoderinfo.setdefault(k, v) im_out = _normalize_palette(im_out, palette, im.encoderinfo) for s in _get_global_header(im_out, im.encoderinfo): @@ -383,28 +560,37 @@ def _write_single_frame(im, fp, palette): _write_local_header(fp, im, (0, 0), flags) im_out.encoderconfig = (8, get_interlace(im)) - ImageFile._save(im_out, fp, [("gif", (0, 0)+im.size, 0, - RAWMODE[im_out.mode])]) + ImageFile._save(im_out, fp, [("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])]) fp.write(b"\0") # end of image data def _write_multiple_frames(im, fp, palette): - duration = im.encoderinfo.get("duration", None) - disposal = im.encoderinfo.get('disposal', None) + duration = im.encoderinfo.get("duration") + disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) im_frames = [] frame_count = 0 + background_im = None for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): for im_frame in ImageSequence.Iterator(imSequence): # a copy is required here since seek can still mutate the image im_frame = _normalize_mode(im_frame.copy()) - im_frame = _normalize_palette(im_frame, palette, im.encoderinfo) + if frame_count == 0: + for k, v in im_frame.info.items(): + if k == "transparency": + continue + im.encoderinfo.setdefault(k, v) encoderinfo = im.encoderinfo.copy() + im_frame = _normalize_palette(im_frame, palette, encoderinfo) + if "transparency" in im_frame.info: + encoderinfo.setdefault("transparency", im_frame.info["transparency"]) if isinstance(duration, (list, tuple)): - encoderinfo['duration'] = duration[frame_count] + encoderinfo["duration"] = duration[frame_count] + elif duration is None and "duration" in im_frame.info: + encoderinfo["duration"] = im_frame.info["duration"] if isinstance(disposal, (list, tuple)): encoderinfo["disposal"] = disposal[frame_count] frame_count += 1 @@ -412,43 +598,55 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if _get_palette_bytes(im_frame) == _get_palette_bytes(previous['im']): - delta = ImageChops.subtract_modulo(im_frame, - previous['im']) + if encoderinfo.get("disposal") == 2: + if background_im is None: + color = im.encoderinfo.get( + "transparency", im.info.get("transparency", (0, 0, 0)) + ) + background = _get_background(im_frame, color) + background_im = Image.new("P", im_frame.size, background) + background_im.putpalette(im_frames[0]["im"].palette) + base_im = background_im + else: + base_im = previous["im"] + if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im): + delta = ImageChops.subtract_modulo(im_frame, base_im) else: - delta = ImageChops.subtract_modulo(im_frame.convert('RGB'), - previous['im'].convert('RGB')) + delta = ImageChops.subtract_modulo( + im_frame.convert("RGB"), base_im.convert("RGB") + ) bbox = delta.getbbox() if not bbox: # This frame is identical to the previous frame if duration: - previous['encoderinfo']['duration'] += encoderinfo['duration'] + previous["encoderinfo"]["duration"] += encoderinfo["duration"] continue else: bbox = None - im_frames.append({ - 'im': im_frame, - 'bbox': bbox, - 'encoderinfo': encoderinfo - }) + im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) if len(im_frames) > 1: for frame_data in im_frames: - im_frame = frame_data['im'] - if not frame_data['bbox']: + im_frame = frame_data["im"] + if not frame_data["bbox"]: # global header - for s in _get_global_header(im_frame, - frame_data['encoderinfo']): + for s in _get_global_header(im_frame, frame_data["encoderinfo"]): fp.write(s) offset = (0, 0) else: # compress difference - frame_data['encoderinfo']['include_color_table'] = True + if not palette: + frame_data["encoderinfo"]["include_color_table"] = True - im_frame = im_frame.crop(frame_data['bbox']) - offset = frame_data['bbox'][:2] - _write_frame_data(fp, im_frame, offset, frame_data['encoderinfo']) + im_frame = im_frame.crop(frame_data["bbox"]) + offset = frame_data["bbox"][:2] + _write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"]) return True + elif "duration" in im.encoderinfo and isinstance( + im.encoderinfo["duration"], (list, tuple) + ): + # Since multiple frames will not be written, add together the frame durations + im.encoderinfo["duration"] = sum(im.encoderinfo["duration"]) def _save_all(im, fp, filename): @@ -456,11 +654,10 @@ def _save_all(im, fp, filename): def _save(im, fp, filename, save_all=False): - im.encoderinfo.update(im.info) # header - try: - palette = im.encoderinfo["palette"] - except KeyError: + if "palette" in im.encoderinfo or "palette" in im.info: + palette = im.encoderinfo.get("palette", im.info.get("palette")) + else: palette = None im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True) @@ -486,11 +683,14 @@ def get_interlace(im): def _write_local_header(fp, im, offset, flags): transparent_color_exists = False try: - transparency = im.encoderinfo["transparency"] - except KeyError: + if "transparency" in im.encoderinfo: + transparency = im.encoderinfo["transparency"] + else: + transparency = im.info["transparency"] + transparency = int(transparency) + except (KeyError, ValueError): pass else: - transparency = int(transparency) # optimize the block away if transparent color is not used transparent_color_exists = True @@ -507,7 +707,7 @@ def _write_local_header(fp, im, offset, flags): else: duration = 0 - disposal = int(im.encoderinfo.get('disposal', 0)) + disposal = int(im.encoderinfo.get("disposal", 0)) if transparent_color_exists or duration != 0 or disposal: packed_flag = 1 if transparent_color_exists else 0 @@ -515,48 +715,35 @@ def _write_local_header(fp, im, offset, flags): if not transparent_color_exists: transparency = 0 - fp.write(b"!" + - o8(249) + # extension intro - o8(4) + # length - o8(packed_flag) + # packed fields - o16(duration) + # duration - o8(transparency) + # transparency index - o8(0)) - - if "comment" in im.encoderinfo and 1 <= len(im.encoderinfo["comment"]) <= 255: - fp.write(b"!" + - o8(254) + # extension intro - o8(len(im.encoderinfo["comment"])) + - im.encoderinfo["comment"] + - o8(0)) - if "loop" in im.encoderinfo: - number_of_loops = im.encoderinfo["loop"] - fp.write(b"!" + - o8(255) + # extension intro - o8(11) + - b"NETSCAPE2.0" + - o8(3) + - o8(1) + - o16(number_of_loops) + # number of loops - o8(0)) - include_color_table = im.encoderinfo.get('include_color_table') + fp.write( + b"!" + + o8(249) # extension intro + + o8(4) # length + + o8(packed_flag) # packed fields + + o16(duration) # duration + + o8(transparency) # transparency index + + o8(0) + ) + + include_color_table = im.encoderinfo.get("include_color_table") if include_color_table: - palette = im.encoderinfo.get("palette", None) palette_bytes = _get_palette_bytes(im) color_table_size = _get_color_table_size(palette_bytes) if color_table_size: - flags = flags | 128 # local color table flag + flags = flags | 128 # local color table flag flags = flags | color_table_size - fp.write(b"," + - o16(offset[0]) + # offset - o16(offset[1]) + - o16(im.size[0]) + # size - o16(im.size[1]) + - o8(flags)) # flags + fp.write( + b"," + + o16(offset[0]) # offset + + o16(offset[1]) + + o16(im.size[0]) # size + + o16(im.size[1]) + + o8(flags) # flags + ) if include_color_table and color_table_size: fp.write(_get_header_palette(palette_bytes)) - fp.write(o8(8)) # bits + fp.write(o8(8)) # bits def _save_netpbm(im, fp, filename): @@ -567,40 +754,44 @@ def _save_netpbm(im, fp, filename): # If you need real GIF compression and/or RGB quantization, you # can use the external NETPBM/PBMPLUS utilities. See comments # below for information on how to enable this. - - import os - from subprocess import Popen, check_call, PIPE, CalledProcessError - file = im._dump() - - with open(filename, 'wb') as f: - if im.mode != "RGB": - with open(os.devnull, 'wb') as devnull: - check_call(["ppmtogif", file], stdout=f, stderr=devnull) - else: - # Pipe ppmquant output into ppmtogif - # "ppmquant 256 %s | ppmtogif > %s" % (file, filename) - quant_cmd = ["ppmquant", "256", file] - togif_cmd = ["ppmtogif"] - with open(os.devnull, 'wb') as devnull: - quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=devnull) - togif_proc = Popen(togif_cmd, stdin=quant_proc.stdout, - stdout=f, stderr=devnull) - - # Allow ppmquant to receive SIGPIPE if ppmtogif exits - quant_proc.stdout.close() - - retcode = quant_proc.wait() - if retcode: - raise CalledProcessError(retcode, quant_cmd) - - retcode = togif_proc.wait() - if retcode: - raise CalledProcessError(retcode, togif_cmd) + tempfile = im._dump() try: - os.unlink(file) - except OSError: - pass + with open(filename, "wb") as f: + if im.mode != "RGB": + subprocess.check_call( + ["ppmtogif", tempfile], stdout=f, stderr=subprocess.DEVNULL + ) + else: + # Pipe ppmquant output into ppmtogif + # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename) + quant_cmd = ["ppmquant", "256", tempfile] + togif_cmd = ["ppmtogif"] + quant_proc = subprocess.Popen( + quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL + ) + togif_proc = subprocess.Popen( + togif_cmd, + stdin=quant_proc.stdout, + stdout=f, + stderr=subprocess.DEVNULL, + ) + + # Allow ppmquant to receive SIGPIPE if ppmtogif exits + quant_proc.stdout.close() + + retcode = quant_proc.wait() + if retcode: + raise subprocess.CalledProcessError(retcode, quant_cmd) + + retcode = togif_proc.wait() + if retcode: + raise subprocess.CalledProcessError(retcode, togif_cmd) + finally: + try: + os.unlink(tempfile) + except OSError: + pass # Force optimization so that we can test performance against @@ -630,7 +821,7 @@ def _get_optimize(im, info): # * If we have a 'large' image, the palette is in the noise. # create the new palette if not every color is used - optimise = _FORCE_OPTIMIZE or im.mode == 'L' + optimise = _FORCE_OPTIMIZE or im.mode == "L" if optimise or im.width * im.height < 512 * 512: # check which colors are used used_palette_colors = [] @@ -638,18 +829,30 @@ def _get_optimize(im, info): if count: used_palette_colors.append(i) - if optimise or (len(used_palette_colors) <= 128 and - max(used_palette_colors) > len(used_palette_colors)): + if optimise or max(used_palette_colors) >= len(used_palette_colors): + return used_palette_colors + + num_palette_colors = len(im.palette.palette) // Image.getmodebands( + im.palette.mode + ) + current_palette_size = 1 << (num_palette_colors - 1).bit_length() + if ( + # check that the palette would become smaller when saved + len(used_palette_colors) <= current_palette_size // 2 + # check that the palette is not already the smallest possible size + and current_palette_size > 2 + ): return used_palette_colors def _get_color_table_size(palette_bytes): # calculate the palette size for the header - import math - color_table_size = int(math.ceil(math.log(len(palette_bytes)//3, 2)))-1 - if color_table_size < 0: - color_table_size = 0 - return color_table_size + if not palette_bytes: + return 0 + elif len(palette_bytes) < 9: + return 1 + else: + return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1 def _get_header_palette(palette_bytes): @@ -664,7 +867,7 @@ def _get_header_palette(palette_bytes): # add the missing amount of bytes # the palette has to be 2< 0: palette_bytes += o8(0) * 3 * actual_target_size_diff return palette_bytes @@ -680,43 +883,86 @@ def _get_palette_bytes(im): return im.palette.palette +def _get_background(im, info_background): + background = 0 + if info_background: + background = info_background + if isinstance(background, tuple): + # WebPImagePlugin stores an RGBA value in info["background"] + # So it must be converted to the same format as GifImagePlugin's + # info["background"] - a global color table index + try: + background = im.palette.getcolor(background, im) + except ValueError as e: + if str(e) == "cannot allocate more than 256 colors": + # If all 256 colors are in use, + # then there is no need for the background color + return 0 + else: + raise + return background + + def _get_global_header(im, info): """Return a list of strings representing a GIF header""" # Header Block - # http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp + # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp version = b"87a" - for extensionKey in ["transparency", "duration", "loop", "comment"]: - if info and extensionKey in info: - if ((extensionKey == "duration" and info[extensionKey] == 0) or - (extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255))): - continue - version = b"89a" - break - else: - if im.info.get("version") == b"89a": - version = b"89a" + if im.info.get("version") == b"89a" or ( + info + and ( + "transparency" in info + or "loop" in info + or info.get("duration") + or info.get("comment") + ) + ): + version = b"89a" + + background = _get_background(im, info.get("background")) palette_bytes = _get_palette_bytes(im) color_table_size = _get_color_table_size(palette_bytes) - background = info["background"] if "background" in info else 0 - - return [ - b"GIF"+version + # signature + version - o16(im.size[0]) + # canvas width - o16(im.size[1]), # canvas height - + header = [ + b"GIF" # signature + + version # version + + o16(im.size[0]) # canvas width + + o16(im.size[1]), # canvas height # Logical Screen Descriptor # size of global color table + global color table flag - o8(color_table_size + 128), # packed fields + o8(color_table_size + 128), # packed fields # background + reserved/aspect o8(background) + o8(0), - # Global Color Table - _get_header_palette(palette_bytes) + _get_header_palette(palette_bytes), ] + if "loop" in info: + header.append( + b"!" + + o8(255) # extension intro + + o8(11) + + b"NETSCAPE2.0" + + o8(3) + + o8(1) + + o16(info["loop"]) # number of loops + + o8(0) + ) + if info.get("comment"): + comment_block = b"!" + o8(254) # extension intro + + comment = info["comment"] + if isinstance(comment, str): + comment = comment.encode() + for i in range(0, len(comment), 255): + subblock = comment[i : i + 255] + comment_block += o8(len(subblock)) + subblock + + comment_block += o8(0) + header.append(comment_block) + return header def _write_frame_data(fp, im_frame, offset, params): @@ -726,13 +972,15 @@ def _write_frame_data(fp, im_frame, offset, params): # local image header _write_local_header(fp, im_frame, offset, 0) - ImageFile._save(im_frame, fp, [("gif", (0, 0)+im_frame.size, 0, - RAWMODE[im_frame.mode])]) + ImageFile._save( + im_frame, fp, [("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])] + ) fp.write(b"\0") # end of image data finally: del im_frame.encoderinfo + # -------------------------------------------------------------------- # Legacy GIF utilities @@ -765,8 +1013,6 @@ def getheader(im, palette=None, info=None): return header, used_palette_colors -# To specify duration, add the time in milliseconds to getdata(), -# e.g. getdata(im_frame, duration=1000) def getdata(im, offset=(0, 0), **params): """ Legacy Method @@ -775,13 +1021,17 @@ def getdata(im, offset=(0, 0), **params): The first string is a local image header, the rest contains encoded image data. + To specify duration, add the time in milliseconds, + e.g. ``getdata(im_frame, duration=1000)`` + :param im: Image object - :param offset: Tuple of (x, y) pixels. Defaults to (0,0) - :param \**params: E.g. duration or other encoder info parameters - :returns: List of Bytes containing gif encoded frame data + :param offset: Tuple of (x, y) pixels. Defaults to (0, 0) + :param \\**params: e.g. duration or other encoder info parameters + :returns: List of bytes containing GIF encoded frame data """ - class Collector(object): + + class Collector: data = [] def write(self, data): diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index 43cd7264933..7ab7f9990ac 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -13,16 +13,19 @@ # See the README file for information on usage and redistribution. # -from math import pi, log, sin, sqrt -from ._binary import o8 +""" +Stuff to translate curve segments to palette values (derived from +the corresponding code in GIMP, written by Federico Mena Quintero. +See the GIMP distribution for more information.) +""" -# -------------------------------------------------------------------- -# Stuff to translate curve segments to palette values (derived from -# the corresponding code in GIMP, written by Federico Mena Quintero. -# See the GIMP distribution for more information.) -# + +from math import log, pi, sin, sqrt + +from ._binary import o8 EPSILON = 1e-10 +"""""" # Enable auto-doc for data member def linear(middle, pos): @@ -55,10 +58,12 @@ def sphere_increasing(middle, pos): def sphere_decreasing(middle, pos): return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2) + SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] +"""""" # Enable auto-doc for data member -class GradientFile(object): +class GradientFile: gradient = None @@ -71,7 +76,7 @@ def getpalette(self, entries=256): for i in range(entries): - x = i / float(entries-1) + x = i / (entries - 1) while x1 < x: ix += 1 @@ -96,10 +101,8 @@ def getpalette(self, entries=256): return b"".join(palette), "RGBA" -## -# File handler for GIMP's gradient format. - class GimpGradientFile(GradientFile): + """File handler for GIMP's gradient format.""" def __init__(self, fp): @@ -130,7 +133,7 @@ def __init__(self, fp): cspace = int(s[12]) if cspace != 0: - raise IOError("cannot handle HSV colour space") + raise OSError("cannot handle HSV colour space") gradient.append((x0, x1, xm, rgb0, rgb1, segment)) diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 6eef6a2ddfb..4d7cfbabab5 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -15,33 +15,30 @@ # import re -from ._binary import o8 +from ._binary import o8 -## -# File handler for GIMP's palette format. -class GimpPaletteFile(object): +class GimpPaletteFile: + """File handler for GIMP's palette format.""" rawmode = "RGB" def __init__(self, fp): - self.palette = [o8(i)*3 for i in range(256)] + self.palette = [o8(i) * 3 for i in range(256)] if fp.readline()[:12] != b"GIMP Palette": raise SyntaxError("not a GIMP palette file") - i = 0 - - while i <= 255: + for i in range(256): s = fp.readline() - if not s: break + # skip fields and comment lines - if re.match(br"\w+:|#", s): + if re.match(rb"\w+:|#", s): continue if len(s) > 100: raise SyntaxError("bad palette file") @@ -50,10 +47,7 @@ def __init__(self, fp): if len(v) != 3: raise ValueError("bad palette entry") - if 0 <= i <= 255: - self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) - - i += 1 + self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) self.palette = b"".join(self.palette) diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 33c8291ea22..4575f8237dc 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -10,7 +10,6 @@ # from . import Image, ImageFile -from ._binary import i8 _handler = None @@ -28,8 +27,9 @@ def register_handler(handler): # -------------------------------------------------------------------- # Image adapter + def _accept(prefix): - return prefix[0:4] == b"GRIB" and i8(prefix[7]) == 1 + return prefix[:4] == b"GRIB" and prefix[7] == 1 class GribStubImageFile(ImageFile.StubImageFile): @@ -48,7 +48,7 @@ def _open(self): # make something up self.mode = "F" - self.size = 1, 1 + self._size = 1, 1 loader = self._load() if loader: @@ -59,8 +59,8 @@ def _load(self): def _save(im, fp, filename): - if _handler is None or not hasattr("_handler", "save"): - raise IOError("GRIB save handler not installed") + if _handler is None or not hasattr(_handler, "save"): + raise OSError("GRIB save handler not installed") _handler.save(im, fp, filename) diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index de4d5bb0c97..df11cf2a6db 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -27,6 +27,7 @@ def register_handler(handler): # -------------------------------------------------------------------- # Image adapter + def _accept(prefix): return prefix[:8] == b"\x89HDF\r\n\x1a\n" @@ -47,7 +48,7 @@ def _open(self): # make something up self.mode = "F" - self.size = 1, 1 + self._size = 1, 1 loader = self._load() if loader: @@ -58,8 +59,8 @@ def _load(self): def _save(im, fp, filename): - if _handler is None or not hasattr("_handler", "save"): - raise IOError("HDF5 save handler not installed") + if _handler is None or not hasattr(_handler, "save"): + raise OSError("HDF5 save handler not installed") _handler.save(im, fp, filename) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 5c5bd7cf9e7..fa192f053f9 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -6,33 +6,34 @@ # # history: # 2004-10-09 fl Turned into a PIL plugin; removed 2.3 dependencies. +# 2020-04-04 Allow saving on all operating systems. # # Copyright (c) 2004 by Bob Ippolito. # Copyright (c) 2004 by Secret Labs. # Copyright (c) 2004 by Fredrik Lundh. # Copyright (c) 2014 by Alastair Houghton. +# Copyright (c) 2020 by Pan Jing. # # See the README file for information on usage and redistribution. # -from PIL import Image, ImageFile, PngImagePlugin -from PIL._binary import i8 import io import os -import shutil import struct import sys -import tempfile -enable_jpeg2k = hasattr(Image.core, 'jp2klib_version') +from PIL import Image, ImageFile, PngImagePlugin, features + +enable_jpeg2k = features.check_codec("jpg_2000") if enable_jpeg2k: from PIL import Jpeg2KImagePlugin +MAGIC = b"icns" HEADERSIZE = 8 def nextheader(fobj): - return struct.unpack('>4sI', fobj.read(HEADERSIZE)) + return struct.unpack(">4sI", fobj.read(HEADERSIZE)) def read_32t(fobj, start_length, size): @@ -40,8 +41,8 @@ def read_32t(fobj, start_length, size): (start, length) = start_length fobj.seek(start) sig = fobj.read(4) - if sig != b'\x00\x00\x00\x00': - raise SyntaxError('Unknown signature, expecting 0x00000000') + if sig != b"\x00\x00\x00\x00": + raise SyntaxError("Unknown signature, expecting 0x00000000") return read_32(fobj, (start + 4, length - 4), size) @@ -68,7 +69,7 @@ def read_32(fobj, start_length, size): byte = fobj.read(1) if not byte: break - byte = i8(byte) + byte = byte[0] if byte & 0x80: blocksize = byte - 125 byte = fobj.read(1) @@ -81,12 +82,8 @@ def read_32(fobj, start_length, size): if bytesleft <= 0: break if bytesleft != 0: - raise SyntaxError( - "Error reading channel [%r left]" % bytesleft - ) - band = Image.frombuffer( - "L", pixel_size, b"".join(data), "raw", "L", 0, 1 - ) + raise SyntaxError(f"Error reading channel [{repr(bytesleft)} left]") + band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1) im.im.putband(band.im, band_ix) return {"RGB": im} @@ -97,9 +94,7 @@ def read_mk(fobj, start_length, size): fobj.seek(start) pixel_size = (size[0] * size[2], size[1] * size[2]) sizesq = pixel_size[0] * pixel_size[1] - band = Image.frombuffer( - "L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1 - ) + band = Image.frombuffer("L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1) return {"A": band} @@ -107,73 +102,60 @@ def read_png_or_jpeg2000(fobj, start_length, size): (start, length) = start_length fobj.seek(start) sig = fobj.read(12) - if sig[:8] == b'\x89PNG\x0d\x0a\x1a\x0a': + if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a": fobj.seek(start) im = PngImagePlugin.PngImageFile(fobj) + Image._decompression_bomb_check(im.size) return {"RGBA": im} - elif sig[:4] == b'\xff\x4f\xff\x51' \ - or sig[:4] == b'\x0d\x0a\x87\x0a' \ - or sig == b'\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a': + elif ( + sig[:4] == b"\xff\x4f\xff\x51" + or sig[:4] == b"\x0d\x0a\x87\x0a" + or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" + ): if not enable_jpeg2k: - raise ValueError('Unsupported icon subimage format (rebuild PIL ' - 'with JPEG 2000 support to fix this)') + raise ValueError( + "Unsupported icon subimage format (rebuild PIL " + "with JPEG 2000 support to fix this)" + ) # j2k, jpc or j2c fobj.seek(start) jp2kstream = fobj.read(length) f = io.BytesIO(jp2kstream) im = Jpeg2KImagePlugin.Jpeg2KImageFile(f) - if im.mode != 'RGBA': - im = im.convert('RGBA') + Image._decompression_bomb_check(im.size) + if im.mode != "RGBA": + im = im.convert("RGBA") return {"RGBA": im} else: - raise ValueError('Unsupported icon subimage format') + raise ValueError("Unsupported icon subimage format") -class IcnsFile(object): +class IcnsFile: SIZES = { - (512, 512, 2): [ - (b'ic10', read_png_or_jpeg2000), - ], - (512, 512, 1): [ - (b'ic09', read_png_or_jpeg2000), - ], - (256, 256, 2): [ - (b'ic14', read_png_or_jpeg2000), - ], - (256, 256, 1): [ - (b'ic08', read_png_or_jpeg2000), - ], - (128, 128, 2): [ - (b'ic13', read_png_or_jpeg2000), - ], + (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)], + (512, 512, 1): [(b"ic09", read_png_or_jpeg2000)], + (256, 256, 2): [(b"ic14", read_png_or_jpeg2000)], + (256, 256, 1): [(b"ic08", read_png_or_jpeg2000)], + (128, 128, 2): [(b"ic13", read_png_or_jpeg2000)], (128, 128, 1): [ - (b'ic07', read_png_or_jpeg2000), - (b'it32', read_32t), - (b't8mk', read_mk), - ], - (64, 64, 1): [ - (b'icp6', read_png_or_jpeg2000), - ], - (32, 32, 2): [ - (b'ic12', read_png_or_jpeg2000), - ], - (48, 48, 1): [ - (b'ih32', read_32), - (b'h8mk', read_mk), + (b"ic07", read_png_or_jpeg2000), + (b"it32", read_32t), + (b"t8mk", read_mk), ], + (64, 64, 1): [(b"icp6", read_png_or_jpeg2000)], + (32, 32, 2): [(b"ic12", read_png_or_jpeg2000)], + (48, 48, 1): [(b"ih32", read_32), (b"h8mk", read_mk)], (32, 32, 1): [ - (b'icp5', read_png_or_jpeg2000), - (b'il32', read_32), - (b'l8mk', read_mk), - ], - (16, 16, 2): [ - (b'ic11', read_png_or_jpeg2000), + (b"icp5", read_png_or_jpeg2000), + (b"il32", read_32), + (b"l8mk", read_mk), ], + (16, 16, 2): [(b"ic11", read_png_or_jpeg2000)], (16, 16, 1): [ - (b'icp4', read_png_or_jpeg2000), - (b'is32', read_32), - (b's8mk', read_mk), + (b"icp4", read_png_or_jpeg2000), + (b"is32", read_32), + (b"s8mk", read_mk), ], } @@ -185,17 +167,17 @@ def __init__(self, fobj): self.dct = dct = {} self.fobj = fobj sig, filesize = nextheader(fobj) - if sig != b'icns': - raise SyntaxError('not an icns file') + if not _accept(sig): + raise SyntaxError("not an icns file") i = HEADERSIZE while i < filesize: sig, blocksize = nextheader(fobj) if blocksize <= 0: - raise SyntaxError('invalid block header') + raise SyntaxError("invalid block header") i += HEADERSIZE blocksize -= HEADERSIZE dct[sig] = (i, blocksize) - fobj.seek(blocksize, 1) + fobj.seek(blocksize, io.SEEK_CUR) i += blocksize def itersizes(self): @@ -233,7 +215,7 @@ def getimage(self, size=None): size = (size[0], size[1], 1) channels = self.dataforsize(size) - im = channels.get('RGBA', None) + im = channels.get("RGBA", None) if im: return im @@ -248,6 +230,7 @@ def getimage(self, size=None): ## # Image plugin for Mac OS icons. + class IcnsImageFile(ImageFile.ImageFile): """ PIL image support for Mac OS .icns files. @@ -264,102 +247,146 @@ class IcnsImageFile(ImageFile.ImageFile): def _open(self): self.icns = IcnsFile(self.fp) - self.mode = 'RGBA' + self.mode = "RGBA" + self.info["sizes"] = self.icns.itersizes() self.best_size = self.icns.bestsize() - self.size = (self.best_size[0] * self.best_size[2], - self.best_size[1] * self.best_size[2]) - self.info['sizes'] = self.icns.itersizes() - # Just use this to see if it's loaded or not yet. - self.tile = ('',) + self.size = ( + self.best_size[0] * self.best_size[2], + self.best_size[1] * self.best_size[2], + ) + + @property + def size(self): + return self._size + + @size.setter + def size(self, value): + info_size = value + if info_size not in self.info["sizes"] and len(info_size) == 2: + info_size = (info_size[0], info_size[1], 1) + if ( + info_size not in self.info["sizes"] + and len(info_size) == 3 + and info_size[2] == 1 + ): + simple_sizes = [ + (size[0] * size[2], size[1] * size[2]) for size in self.info["sizes"] + ] + 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") + self._size = value def load(self): if len(self.size) == 3: self.best_size = self.size - self.size = (self.best_size[0] * self.best_size[2], - self.best_size[1] * self.best_size[2]) - - Image.Image.load(self) - if not self.tile: - return + self.size = ( + self.best_size[0] * self.best_size[2], + self.best_size[1] * self.best_size[2], + ) + + px = Image.Image.load(self) + if self.im is not None and self.im.size == self.size: + # Already loaded + return px self.load_prepare() # This is likely NOT the best way to do it, but whatever. im = self.icns.getimage(self.best_size) # If this is a PNG or JPEG 2000, it won't be loaded yet - im.load() + px = im.load() self.im = im.im self.mode = im.mode self.size = im.size - self.fp = None - self.icns = None - self.tile = () - self.load_end() + + return px def _save(im, fp, filename): """ Saves the image as a series of PNG files, - that are then converted to a .icns file - using the macOS command line utility 'iconutil'. - - macOS only. + that are then combined into a .icns file. """ if hasattr(fp, "flush"): fp.flush() - # create the temporary set of pngs - iconset = tempfile.mkdtemp('.iconset') - last_w = None - last_im = None - for w in [16, 32, 128, 256, 512]: - prefix = 'icon_{}x{}'.format(w, w) - - if last_w == w: - im_scaled = last_im - else: - im_scaled = im.resize((w, w), Image.LANCZOS) - im_scaled.save(os.path.join(iconset, prefix+'.png')) - - im_scaled = im.resize((w*2, w*2), Image.LANCZOS) - im_scaled.save(os.path.join(iconset, prefix+'@2x.png')) - last_im = im_scaled + sizes = { + b"ic07": 128, + b"ic08": 256, + b"ic09": 512, + b"ic10": 1024, + b"ic11": 32, + b"ic12": 64, + b"ic13": 256, + b"ic14": 512, + } + provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])} + size_streams = {} + for size in set(sizes.values()): + image = ( + provided_images[size] + if size in provided_images + else im.resize((size, size)) + ) - # iconutil -c icns -o {} {} - from subprocess import Popen, PIPE, CalledProcessError + temp = io.BytesIO() + image.save(temp, "png") + size_streams[size] = temp.getvalue() - convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset] - with open(os.devnull, 'wb') as devnull: - convert_proc = Popen(convert_cmd, stdout=PIPE, stderr=devnull) + entries = [] + for type, size in sizes.items(): + stream = size_streams[size] + entries.append( + {"type": type, "size": HEADERSIZE + len(stream), "stream": stream} + ) - convert_proc.stdout.close() + # Header + fp.write(MAGIC) + file_length = HEADERSIZE # Header + file_length += HEADERSIZE + 8 * len(entries) # TOC + file_length += sum(entry["size"] for entry in entries) + fp.write(struct.pack(">i", file_length)) + + # TOC + fp.write(b"TOC ") + fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE)) + for entry in entries: + fp.write(entry["type"]) + fp.write(struct.pack(">i", entry["size"])) + + # Data + for entry in entries: + fp.write(entry["type"]) + fp.write(struct.pack(">i", entry["size"])) + fp.write(entry["stream"]) - retcode = convert_proc.wait() + if hasattr(fp, "flush"): + fp.flush() - # remove the temporary files - shutil.rmtree(iconset) - if retcode: - raise CalledProcessError(retcode, convert_cmd) +def _accept(prefix): + return prefix[:4] == MAGIC -Image.register_open(IcnsImageFile.format, IcnsImageFile, - lambda x: x[:4] == b'icns') -Image.register_extension(IcnsImageFile.format, '.icns') -if sys.platform == 'darwin': - Image.register_save(IcnsImageFile.format, _save) +Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept) +Image.register_extension(IcnsImageFile.format, ".icns") - Image.register_mime(IcnsImageFile.format, "image/icns") +Image.register_save(IcnsImageFile.format, _save) +Image.register_mime(IcnsImageFile.format, "image/icns") +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Syntax: python3 IcnsImagePlugin.py [file]") + sys.exit() -if __name__ == '__main__': - imf = IcnsImageFile(open(sys.argv[1], 'rb')) - for size in imf.info['sizes']: - imf.size = size - imf.load() - im = imf.im - im.save('out-%s-%s-%s.png' % size) - im = Image.open(open(sys.argv[1], "rb")) - im.save("out.png") - if sys.platform == 'windows': - os.startfile("out.png") + with open(sys.argv[1], "rb") as fp: + imf = IcnsImageFile(fp) + for size in imf.info["sizes"]: + imf.size = size + imf.save("out-%s-%s-%s.png" % size) + with Image.open(sys.argv[1]) as im: + im.save("out.png") + if sys.platform == "windows": + os.startfile("out.png") diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 428fdd41a3c..17b9855a0a5 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -22,14 +22,16 @@ # * https://msdn.microsoft.com/en-us/library/ms997538.aspx -import struct +import warnings from io import BytesIO +from math import ceil, log -from . import Image, ImageFile, BmpImagePlugin, PngImagePlugin -from ._binary import i8, i16le as i16, i32le as i32 -from math import log, ceil - -__version__ = "0.1" +from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin +from ._binary import i16le as i16 +from ._binary import i32le as i32 +from ._binary import o8 +from ._binary import o16le as o16 +from ._binary import o32le as o32 # # -------------------------------------------------------------------- @@ -39,35 +41,72 @@ def _save(im, fp, filename): fp.write(_MAGIC) # (2+2) - sizes = im.encoderinfo.get("sizes", - [(16, 16), (24, 24), (32, 32), (48, 48), - (64, 64), (128, 128), (256, 256)]) + bmp = im.encoderinfo.get("bitmap_format") == "bmp" + sizes = im.encoderinfo.get( + "sizes", + [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)], + ) + frames = [] + provided_ims = [im] + im.encoderinfo.get("append_images", []) width, height = im.size - sizes = filter(lambda x: False if (x[0] > width or x[1] > height or - x[0] > 256 or x[1] > 256) else True, - sizes) - sizes = list(sizes) - fp.write(struct.pack(" width or size[1] > height or size[0] > 256 or size[1] > 256: + continue + + for provided_im in provided_ims: + if provided_im.size != size: + continue + frames.append(provided_im) + if bmp: + bits = BmpImagePlugin.SAVE[provided_im.mode][1] + bits_used = [bits] + for other_im in provided_ims: + if other_im.size != size: + continue + bits = BmpImagePlugin.SAVE[other_im.mode][1] + if bits not in bits_used: + # Another image has been supplied for this size + # with a different bit depth + frames.append(other_im) + bits_used.append(bits) + break + else: + # TODO: invent a more convenient method for proportional scalings + frame = provided_im.copy() + frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None) + frames.append(frame) + fp.write(o16(len(frames))) # idCount(2) + offset = fp.tell() + len(frames) * 16 + for frame in frames: + width, height = frame.size # 0 means 256 - fp.write(struct.pack("B", width if width < 256 else 0)) # bWidth(1) - fp.write(struct.pack("B", height if height < 256 else 0)) # bHeight(1) - fp.write(b"\0") # bColorCount(1) + fp.write(o8(width if width < 256 else 0)) # bWidth(1) + fp.write(o8(height if height < 256 else 0)) # bHeight(1) + + bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0) + fp.write(o8(colors)) # bColorCount(1) fp.write(b"\0") # bReserved(1) fp.write(b"\0\0") # wPlanes(2) - fp.write(struct.pack("=8bpp) - 'reserved': i8(s[3]), - 'planes': i16(s[4:]), - 'bpp': i16(s[6:]), - 'size': i32(s[8:]), - 'offset': i32(s[12:]) + "width": s[0], + "height": s[1], + "nb_color": s[2], # No. of colors in image (0 if >=8bpp) + "reserved": s[3], + "planes": i16(s, 4), + "bpp": i16(s, 6), + "size": i32(s, 8), + "offset": i32(s, 12), } # See Wikipedia - for j in ('width', 'height'): + for j in ("width", "height"): if not icon_header[j]: icon_header[j] = 256 # See Wikipedia notes about color depth. # We need this just to differ images with equal sizes - icon_header['color_depth'] = (icon_header['bpp'] or - (icon_header['nb_color'] != 0 and - ceil(log(icon_header['nb_color'], - 2))) or 256) + icon_header["color_depth"] = ( + icon_header["bpp"] + or ( + icon_header["nb_color"] != 0 + and ceil(log(icon_header["nb_color"], 2)) + ) + or 256 + ) - icon_header['dim'] = (icon_header['width'], icon_header['height']) - icon_header['square'] = (icon_header['width'] * - icon_header['height']) + icon_header["dim"] = (icon_header["width"], icon_header["height"]) + icon_header["square"] = icon_header["width"] * icon_header["height"] self.entry.append(icon_header) - self.entry = sorted(self.entry, key=lambda x: x['color_depth']) + self.entry = sorted(self.entry, key=lambda x: x["color_depth"]) # ICO images are usually squares # self.entry = sorted(self.entry, key=lambda x: x['width']) - self.entry = sorted(self.entry, key=lambda x: x['square']) + self.entry = sorted(self.entry, key=lambda x: x["square"]) self.entry.reverse() def sizes(self): """ Get a list of all available icon sizes and color depths. """ - return {(h['width'], h['height']) for h in self.entry} + return {(h["width"], h["height"]) for h in self.entry} + + def getentryindex(self, size, bpp=False): + for (i, h) in enumerate(self.entry): + if size == h["dim"] and (bpp is False or bpp == h["color_depth"]): + return i + return 0 def getimage(self, size, bpp=False): """ Get an image from the icon """ - for (i, h) in enumerate(self.entry): - if size == h['dim'] and (bpp is False or bpp == h['color_depth']): - return self.frame(i) - return self.frame(0) + return self.frame(self.getentryindex(size, bpp)) def frame(self, idx): """ @@ -157,30 +202,26 @@ def frame(self, idx): header = self.entry[idx] - self.buf.seek(header['offset']) + self.buf.seek(header["offset"]) data = self.buf.read(8) - self.buf.seek(header['offset']) + self.buf.seek(header["offset"]) if data[:8] == PngImagePlugin._MAGIC: # png frame im = PngImagePlugin.PngImageFile(self.buf) + Image._decompression_bomb_check(im.size) else: # XOR + AND mask bmp frame im = BmpImagePlugin.DibImageFile(self.buf) + Image._decompression_bomb_check(im.size) # change tile dimension to only encompass XOR image - im.size = (im.size[0], int(im.size[1] / 2)) + im._size = (im.size[0], int(im.size[1] / 2)) d, e, o, a = im.tile[0] im.tile[0] = d, (0, 0) + im.size, o, a # figure out where AND mask image starts - mode = a[0] - bpp = 8 - for k, v in BmpImagePlugin.BIT2MODE.items(): - if mode == v[1]: - bpp = k - break - + bpp = header["bpp"] if 32 == bpp: # 32-bit color depth icon image allows semitransparent areas # PIL's DIB format ignores transparency bits, recover them. @@ -194,11 +235,11 @@ def frame(self, idx): # convert to an 8bpp grayscale image mask = Image.frombuffer( - 'L', # 8bpp - im.size, # (w, h) - alpha_bytes, # source chars - 'raw', # raw decoder - ('L', 0, -1) # 8bpp inverted, unpadded, reversed + "L", # 8bpp + im.size, # (w, h) + alpha_bytes, # source chars + "raw", # raw decoder + ("L", 0, -1), # 8bpp inverted, unpadded, reversed ) else: # get AND image from end of bitmap @@ -210,26 +251,25 @@ def frame(self, idx): # the total mask data is # padded row size * height / bits per char - and_mask_offset = o + int(im.size[0] * im.size[1] * - (bpp / 8.0)) total_bytes = int((w * im.size[1]) / 8) + and_mask_offset = header["offset"] + header["size"] - total_bytes self.buf.seek(and_mask_offset) mask_data = self.buf.read(total_bytes) # convert raw data to image mask = Image.frombuffer( - '1', # 1 bpp - im.size, # (w, h) - mask_data, # source chars - 'raw', # raw decoder - ('1;I', int(w/8), -1) # 1bpp inverted, padded, reversed + "1", # 1 bpp + im.size, # (w, h) + mask_data, # source chars + "raw", # raw decoder + ("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed ) # now we have two images, im is XOR image and mask is AND image # apply mask image as alpha channel - im = im.convert('RGBA') + im = im.convert("RGBA") im.putalpha(mask) return im @@ -238,6 +278,7 @@ def frame(self, idx): ## # Image plugin for Windows Icon files. + class IcoImageFile(ImageFile.ImageFile): """ PIL read-only image support for Microsoft Windows .ico files. @@ -250,31 +291,59 @@ class IcoImageFile(ImageFile.ImageFile): Handles classic, XP and Vista icon formats. + When saving, PNG compression is used. Support for this was only added in + Windows Vista. If you are unable to view the icon in Windows, convert the + image to "RGBA" mode before saving. + This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis . https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki """ + format = "ICO" format_description = "Windows Icon" def _open(self): self.ico = IcoFile(self.fp) - self.info['sizes'] = self.ico.sizes() - self.size = self.ico.entry[0]['dim'] + self.info["sizes"] = self.ico.sizes() + self.size = self.ico.entry[0]["dim"] self.load() + @property + def size(self): + return self._size + + @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") + self._size = value + def load(self): + if self.im is not None and self.im.size == self.size: + # Already loaded + return Image.Image.load(self) im = self.ico.getimage(self.size) # if tile is PNG, it won't really be loaded yet im.load() self.im = im.im self.mode = im.mode - self.size = im.size + if im.size != self.size: + warnings.warn("Image was not the expected size") + + index = self.ico.getentryindex(self.size) + sizes = list(self.info["sizes"]) + sizes[index] = im.size + self.info["sizes"] = set(sizes) + + self.size = im.size def load_seek(self): # Flag the ImageFile.Parser so that it # just does all the decode at the end. pass + + # # -------------------------------------------------------------------- @@ -282,3 +351,5 @@ def load_seek(self): Image.register_open(IcoImageFile.format, IcoImageFile, _accept) Image.register_save(IcoImageFile.format, _save) Image.register_extension(IcoImageFile.format, ".ico") + +Image.register_mime(IcoImageFile.format, "image/x-icon") diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 81b1d7b6c9c..31b0ff46901 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -26,12 +26,10 @@ # +import os import re -from . import Image, ImageFile, ImagePalette -from ._binary import i8 - -__version__ = "0.7" +from . import Image, ImageFile, ImagePalette # -------------------------------------------------------------------- # Standard tags @@ -46,8 +44,17 @@ SIZE = "Image size (x*y)" MODE = "Image type" -TAGS = {COMMENT: 0, DATE: 0, EQUIPMENT: 0, FRAMES: 0, LUT: 0, NAME: 0, - SCALE: 0, SIZE: 0, MODE: 0} +TAGS = { + COMMENT: 0, + DATE: 0, + EQUIPMENT: 0, + FRAMES: 0, + LUT: 0, + NAME: 0, + SCALE: 0, + SIZE: 0, + MODE: 0, +} OPEN = { # ifunc93/p3cfunc formats @@ -69,6 +76,7 @@ "RYB3 image": ("RGB", "RYB;T"), # extensions "LA image": ("LA", "LA;L"), + "PA image": ("LA", "PA;L"), "RGBA image": ("RGBA", "RGBA;L"), "RGBX image": ("RGBX", "RGBX;L"), "CMYK image": ("CMYK", "CMYK;L"), @@ -77,22 +85,22 @@ # ifunc95 extensions for i in ["8", "8S", "16", "16S", "32", "32F"]: - OPEN["L %s image" % i] = ("F", "F;%s" % i) - OPEN["L*%s image" % i] = ("F", "F;%s" % i) + OPEN[f"L {i} image"] = ("F", f"F;{i}") + OPEN[f"L*{i} image"] = ("F", f"F;{i}") for i in ["16", "16L", "16B"]: - OPEN["L %s image" % i] = ("I;%s" % i, "I;%s" % i) - OPEN["L*%s image" % i] = ("I;%s" % i, "I;%s" % i) + OPEN[f"L {i} image"] = (f"I;{i}", f"I;{i}") + OPEN[f"L*{i} image"] = (f"I;{i}", f"I;{i}") for i in ["32S"]: - OPEN["L %s image" % i] = ("I", "I;%s" % i) - OPEN["L*%s image" % i] = ("I", "I;%s" % i) + OPEN[f"L {i} image"] = ("I", f"I;{i}") + OPEN[f"L*{i} image"] = ("I", f"I;{i}") for i in range(2, 33): - OPEN["L*%s image" % i] = ("F", "F;%s" % i) + OPEN[f"L*{i} image"] = ("F", f"F;{i}") # -------------------------------------------------------------------- # Read IM directory -split = re.compile(br"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$") +split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$") def number(s): @@ -105,6 +113,7 @@ def number(s): ## # Image plugin for the IFUNC IM file format. + class ImImageFile(ImageFile.ImageFile): format = "IM" @@ -137,7 +146,7 @@ def _open(self): if s == b"\r": continue - if not s or s == b'\0' or s == b'\x1A': + if not s or s == b"\0" or s == b"\x1A": break # FIXME: this may read whole file if not a text file @@ -146,15 +155,15 @@ def _open(self): if len(s) > 100: raise SyntaxError("not an IM file") - if s[-2:] == b'\r\n': + if s[-2:] == b"\r\n": s = s[:-2] - elif s[-1:] == b'\n': + elif s[-1:] == b"\n": s = s[:-1] try: m = split.match(s) - except re.error as v: - raise SyntaxError("not an IM file") + except re.error as e: + raise SyntaxError("not an IM file") from e if m: @@ -162,8 +171,8 @@ def _open(self): # Don't know if this is the correct encoding, # but a decent guess (I guess) - k = k.decode('latin-1', 'replace') - v = v.decode('latin-1', 'replace') + k = k.decode("latin-1", "replace") + v = v.decode("latin-1", "replace") # Convert value as appropriate if k in [FRAMES, SCALE, SIZE]: @@ -189,18 +198,19 @@ def _open(self): else: - raise SyntaxError("Syntax error in IM header: " + - s.decode('ascii', 'replace')) + raise SyntaxError( + "Syntax error in IM header: " + s.decode("ascii", "replace") + ) if not n: raise SyntaxError("Not an IM file") # Basic attributes - self.size = self.info[SIZE] + self._size = self.info[SIZE] self.mode = self.info[MODE] # Skip forward to start of image data - while s and s[0:1] != b'\x1A': + while s and s[:1] != b"\x1A": s = self.fp.read(1) if not s: raise SyntaxError("File truncated") @@ -211,30 +221,31 @@ def _open(self): greyscale = 1 # greyscale palette linear = 1 # linear greyscale palette for i in range(256): - if palette[i] == palette[i+256] == palette[i+512]: - if i8(palette[i]) != i: + if palette[i] == palette[i + 256] == palette[i + 512]: + if palette[i] != i: linear = 0 else: greyscale = 0 - if self.mode == "L" or self.mode == "LA": + if self.mode in ["L", "LA", "P", "PA"]: if greyscale: if not linear: - self.lut = [i8(c) for c in palette[:256]] + self.lut = list(palette[:256]) else: - if self.mode == "L": + if self.mode in ["L", "P"]: self.mode = self.rawmode = "P" - elif self.mode == "LA": - self.mode = self.rawmode = "PA" + elif self.mode in ["LA", "PA"]: + self.mode = "PA" + self.rawmode = "PA;L" self.palette = ImagePalette.raw("RGB;L", palette) elif self.mode == "RGB": if not greyscale or not linear: - self.lut = [i8(c) for c in palette] + self.lut = list(palette) self.frame = 0 self.__offset = offs = self.fp.tell() - self.__fp = self.fp # FIXME: hack + self._fp = self.fp # FIXME: hack if self.rawmode[:2] == "F;": @@ -243,8 +254,7 @@ def _open(self): # use bit decoder (if necessary) bits = int(self.rawmode[2:]) if bits not in [8, 16, 32]: - self.tile = [("bit", (0, 0)+self.size, offs, - (bits, 8, 3, 0, -1))] + self.tile = [("bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1))] return except ValueError: pass @@ -253,13 +263,14 @@ def _open(self): # Old LabEye/3PC files. Would be very surprised if anyone # ever stumbled upon such a file ;-) size = self.size[0] * self.size[1] - self.tile = [("raw", (0, 0)+self.size, offs, ("G", 0, -1)), - ("raw", (0, 0)+self.size, offs+size, ("R", 0, -1)), - ("raw", (0, 0)+self.size, offs+2*size, ("B", 0, -1))] + self.tile = [ + ("raw", (0, 0) + self.size, offs, ("G", 0, -1)), + ("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)), + ("raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)), + ] else: # LabEye/IFUNC files - self.tile = [("raw", (0, 0)+self.size, offs, - (self.rawmode, 0, -1))] + self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] @property def n_frames(self): @@ -283,17 +294,19 @@ def seek(self, frame): size = ((self.size[0] * bits + 7) // 8) * self.size[1] offs = self.__offset + frame * size - self.fp = self.__fp + self.fp = self._fp - self.tile = [("raw", (0, 0)+self.size, offs, (self.rawmode, 0, -1))] + self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] def tell(self): return self.frame + # # -------------------------------------------------------------------- # Save IM files + SAVE = { # mode: (im type, raw mode) "1": ("0 1", "1"), @@ -310,7 +323,7 @@ def tell(self): "RGBA": ("RGBA", "RGBA;L"), "RGBX": ("RGBX", "RGBX;L"), "CMYK": ("CMYK", "CMYK;L"), - "YCbCr": ("YCC", "YCbCr;L") + "YCbCr": ("YCC", "YCbCr;L"), } @@ -318,27 +331,42 @@ def _save(im, fp, filename): try: image_type, rawmode = SAVE[im.mode] - except KeyError: - raise ValueError("Cannot save %s images as IM" % im.mode) + except KeyError as e: + raise ValueError(f"Cannot save {im.mode} images as IM") from e frames = im.encoderinfo.get("frames", 1) - fp.write(("Image type: %s image\r\n" % image_type).encode('ascii')) + fp.write(f"Image type: {image_type} image\r\n".encode("ascii")) if filename: - fp.write(("Name: %s\r\n" % filename).encode('ascii')) - fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode('ascii')) - fp.write(("File size (no of images): %d\r\n" % frames).encode('ascii')) - if im.mode == "P": + # Each line must be 100 characters or less, + # or: SyntaxError("not an IM file") + # 8 characters are used for "Name: " and "\r\n" + # Keep just the filename, ditch the potentially overlong path + name, ext = os.path.splitext(os.path.basename(filename)) + name = "".join([name[: 92 - len(ext)], ext]) + + fp.write(f"Name: {name}\r\n".encode("ascii")) + fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode("ascii")) + fp.write(f"File size (no of images): {frames}\r\n".encode("ascii")) + if im.mode in ["P", "PA"]: fp.write(b"Lut: 1\r\n") - fp.write(b"\000" * (511-fp.tell()) + b"\032") - if im.mode == "P": - fp.write(im.im.getpalette("RGB", "RGB;L")) # 768 bytes - ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0, (rawmode, 0, -1))]) + fp.write(b"\000" * (511 - fp.tell()) + b"\032") + if im.mode in ["P", "PA"]: + im_palette = im.im.getpalette("RGB", "RGB;L") + colors = len(im_palette) // 3 + palette = b"" + for i in range(3): + palette += im_palette[colors * i : colors * (i + 1)] + palette += b"\x00" * (256 - colors) + fp.write(palette) # 768 bytes + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]) + # # -------------------------------------------------------------------- # Registry + Image.register_open(ImImageFile.format, ImImageFile) Image.register_save(ImImageFile.format, _save) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 8d4d9c8a841..7faf0c2481b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -24,11 +24,57 @@ # See the README file for information on usage and redistribution. # -from . import VERSION, PILLOW_VERSION, _plugins - +import atexit +import builtins +import io import logging -import warnings import math +import os +import re +import struct +import sys +import tempfile +import warnings +from collections.abc import Callable, MutableMapping +from enum import IntEnum +from pathlib import Path + +try: + import defusedxml.ElementTree as ElementTree +except ImportError: + ElementTree = None + +# 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 ._binary import i32le, o32be, o32le +from ._deprecate import deprecate +from ._util import DeferredError, is_path + + +def __getattr__(name): + categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2} + if name in categories: + deprecate("Image categories", 10, "is_animated", plural=True) + return categories[name] + elif name in ("NEAREST", "NONE"): + deprecate(name, 10, "Resampling.NEAREST or Dither.NONE") + return 0 + old_resampling = { + "LINEAR": "BILINEAR", + "CUBIC": "BICUBIC", + "ANTIALIAS": "LANCZOS", + } + if name in old_resampling: + deprecate(name, 10, f"Resampling.{old_resampling[name]}") + return Resampling[old_resampling[name]] + for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize): + if name in enum.__members__: + deprecate(name, 10, f"{enum.__name__}.{name}") + return enum[name] + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + logger = logging.getLogger(__name__) @@ -36,16 +82,12 @@ class DecompressionBombWarning(RuntimeWarning): pass + class DecompressionBombError(Exception): pass -class _imaging_not_installed(object): - # module placeholder - def __getattr__(self, id): - raise ImportError("The _imaging C module is not installed") - -# Limit to around a quarter gigabyte for a 24 bit (3 bpp) image +# Limit to around a quarter gigabyte for a 24-bit (3 bpp) image MAX_IMAGE_PIXELS = int(1024 * 1024 * 1024 // 4 // 3) @@ -56,85 +98,38 @@ def __getattr__(self, id): # Also note that Image.core is not a publicly documented interface, # and should be considered private and subject to change. from . import _imaging as core - if PILLOW_VERSION != getattr(core, 'PILLOW_VERSION', None): - raise ImportError("The _imaging extension was built for another " - "version of Pillow or PIL:\n" - "Core version: %s\n" - "Pillow version: %s" % - (getattr(core, 'PILLOW_VERSION', None), - PILLOW_VERSION)) + + if __version__ != getattr(core, "PILLOW_VERSION", None): + raise ImportError( + "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__}" + ) except ImportError as v: - core = _imaging_not_installed() + core = DeferredError(ImportError("The _imaging C module is not installed.")) # Explanations for ways that we know we might have an import error if str(v).startswith("Module use of python"): # The _imaging C module is present, but not compiled for # the right version (windows only). Print a warning, if # possible. warnings.warn( - "The _imaging extension was built for another version " - "of Python.", - RuntimeWarning - ) + "The _imaging extension was built for another version of Python.", + RuntimeWarning, + ) elif str(v).startswith("The _imaging extension"): warnings.warn(str(v), RuntimeWarning) - elif "Symbol not found: _PyUnicodeUCS2_" in str(v): - # should match _PyUnicodeUCS2_FromString and - # _PyUnicodeUCS2_AsLatin1String - warnings.warn( - "The _imaging extension was built for Python with UCS2 support; " - "recompile Pillow or build Python --without-wide-unicode. ", - RuntimeWarning - ) - elif "Symbol not found: _PyUnicodeUCS4_" in str(v): - # should match _PyUnicodeUCS4_FromString and - # _PyUnicodeUCS4_AsLatin1String - warnings.warn( - "The _imaging extension was built for Python with UCS4 support; " - "recompile Pillow or build Python --with-wide-unicode. ", - RuntimeWarning - ) # Fail here anyway. Don't let people run with a mostly broken Pillow. # see docs/porting.rst raise -try: - import builtins -except ImportError: - import __builtin__ - builtins = __builtin__ - -from . import ImageMode -from ._binary import i8 -from ._util import isPath, isStringType, deferred_error - -import os -import sys -import io -import struct -import atexit - -# type stuff -import collections -import numbers # works everywhere, win for pypy, not cpython -USE_CFFI_ACCESS = hasattr(sys, 'pypy_version_info') +USE_CFFI_ACCESS = hasattr(sys, "pypy_version_info") try: import cffi - HAS_CFFI = True except ImportError: - HAS_CFFI = False - -try: - from pathlib import Path - HAS_PATHLIB = True -except ImportError: - try: - from pathlib2 import Path - HAS_PATHLIB = True - except ImportError: - HAS_PATHLIB = False + cffi = None def isImageType(t): @@ -152,55 +147,69 @@ def isImageType(t): # -# Constants (also defined in _imagingmodule.c!) - -NONE = 0 +# Constants # transpose -FLIP_LEFT_RIGHT = 0 -FLIP_TOP_BOTTOM = 1 -ROTATE_90 = 2 -ROTATE_180 = 3 -ROTATE_270 = 4 -TRANSPOSE = 5 -TRANSVERSE = 6 - -# transforms -AFFINE = 0 -EXTENT = 1 -PERSPECTIVE = 2 -QUAD = 3 -MESH = 4 - -# resampling filters -NEAREST = NONE = 0 -BOX = 4 -BILINEAR = LINEAR = 2 -HAMMING = 5 -BICUBIC = CUBIC = 3 -LANCZOS = ANTIALIAS = 1 +class Transpose(IntEnum): + FLIP_LEFT_RIGHT = 0 + FLIP_TOP_BOTTOM = 1 + ROTATE_90 = 2 + ROTATE_180 = 3 + ROTATE_270 = 4 + TRANSPOSE = 5 + TRANSVERSE = 6 + + +# transforms (also defined in Imaging.h) +class Transform(IntEnum): + AFFINE = 0 + EXTENT = 1 + PERSPECTIVE = 2 + QUAD = 3 + MESH = 4 + + +# resampling filters (also defined in Imaging.h) +class Resampling(IntEnum): + NEAREST = 0 + BOX = 4 + BILINEAR = 2 + HAMMING = 5 + BICUBIC = 3 + LANCZOS = 1 + + +_filters_support = { + Resampling.BOX: 0.5, + Resampling.BILINEAR: 1.0, + Resampling.HAMMING: 1.0, + Resampling.BICUBIC: 2.0, + Resampling.LANCZOS: 3.0, +} + # dithers -NEAREST = NONE = 0 -ORDERED = 1 # Not yet implemented -RASTERIZE = 2 # Not yet implemented -FLOYDSTEINBERG = 3 # default +class Dither(IntEnum): + NONE = 0 + ORDERED = 1 # Not yet implemented + RASTERIZE = 2 # Not yet implemented + FLOYDSTEINBERG = 3 # default + # palettes/quantizers -WEB = 0 -ADAPTIVE = 1 +class Palette(IntEnum): + WEB = 0 + ADAPTIVE = 1 -MEDIANCUT = 0 -MAXCOVERAGE = 1 -FASTOCTREE = 2 -LIBIMAGEQUANT = 3 -# categories -NORMAL = 0 -SEQUENCE = 1 -CONTAINER = 2 +class Quantize(IntEnum): + MEDIANCUT = 0 + MAXCOVERAGE = 1 + FASTOCTREE = 2 + LIBIMAGEQUANT = 3 -if hasattr(core, 'DEFAULT_STRATEGY'): + +if hasattr(core, "DEFAULT_STRATEGY"): DEFAULT_STRATEGY = core.DEFAULT_STRATEGY FILTERED = core.FILTERED HUFFMAN_ONLY = core.HUFFMAN_ONLY @@ -221,77 +230,21 @@ def isImageType(t): ENCODERS = {} # -------------------------------------------------------------------- -# Modes supported by this version - -_MODEINFO = { - # NOTE: this table will be removed in future versions. use - # getmode* functions or ImageMode descriptors instead. - - # official modes - "1": ("L", "L", ("1",)), - "L": ("L", "L", ("L",)), - "I": ("L", "I", ("I",)), - "F": ("L", "F", ("F",)), - "P": ("RGB", "L", ("P",)), - "RGB": ("RGB", "L", ("R", "G", "B")), - "RGBX": ("RGB", "L", ("R", "G", "B", "X")), - "RGBA": ("RGB", "L", ("R", "G", "B", "A")), - "CMYK": ("RGB", "L", ("C", "M", "Y", "K")), - "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), - "LAB": ("RGB", "L", ("L", "A", "B")), - "HSV": ("RGB", "L", ("H", "S", "V")), - - # Experimental modes include I;16, I;16L, I;16B, RGBa, BGR;15, and - # BGR;24. Use these modes only if you know exactly what you're - # doing... +# Modes -} - -if sys.byteorder == 'little': - _ENDIAN = '<' -else: - _ENDIAN = '>' - -_MODE_CONV = { - # official modes - "1": ('|b1', None), # Bits need to be extended to bytes - "L": ('|u1', None), - "LA": ('|u1', 2), - "I": (_ENDIAN + 'i4', None), - "F": (_ENDIAN + 'f4', None), - "P": ('|u1', None), - "RGB": ('|u1', 3), - "RGBX": ('|u1', 4), - "RGBA": ('|u1', 4), - "CMYK": ('|u1', 4), - "YCbCr": ('|u1', 3), - "LAB": ('|u1', 3), # UNDONE - unsigned |u1i1i1 - "HSV": ('|u1', 3), - # I;16 == I;16L, and I;32 == I;32L - "I;16": ('u2', None), - "I;16L": ('i2', None), - "I;16LS": ('u4', None), - "I;32L": ('i4', None), - "I;32LS": ('" def _conv_type_shape(im): - typ, extra = _MODE_CONV[im.mode] - if extra is None: - return (im.size[1], im.size[0]), typ - else: - return (im.size[1], im.size[0], extra), typ + m = ImageMode.getmode(im.mode) + shape = (im.height, im.width) + extra = len(m.bands) + if extra != 1: + shape += (extra,) + return shape, m.typestr -MODES = sorted(_MODEINFO) +MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr"] # raw modes that may be memory mapped. NOTE: if you change this, you # may have to modify the stride calculation in map.c too! @@ -356,7 +309,7 @@ def getmodebands(mode): def preinit(): - "Explicitly load standard file format drivers." + """Explicitly load standard file format drivers.""" global _initialized if _initialized >= 1: @@ -364,28 +317,39 @@ def preinit(): try: from . import BmpImagePlugin + + assert BmpImagePlugin except ImportError: pass try: from . import GifImagePlugin + + assert GifImagePlugin except ImportError: pass try: from . import JpegImagePlugin + + assert JpegImagePlugin except ImportError: pass try: from . import PpmImagePlugin + + assert PpmImagePlugin except ImportError: pass try: from . import PngImagePlugin + + assert PngImagePlugin except ImportError: pass -# try: -# import TiffImagePlugin -# except ImportError: -# pass + # try: + # import TiffImagePlugin + # assert TiffImagePlugin + # except ImportError: + # pass _initialized = 1 @@ -403,7 +367,7 @@ def init(): for plugin in _plugins: try: logger.debug("Importing %s", plugin) - __import__("PIL.%s" % plugin, globals(), locals(), []) + __import__(f"PIL.{plugin}", globals(), locals(), []) except ImportError as e: logger.debug("Image: failed to import %s: %s", plugin, e) @@ -415,6 +379,7 @@ def init(): # -------------------------------------------------------------------- # Codec factories (used by tobytes/frombytes and ImageFile.load) + def _getdecoder(mode, decoder_name, args, extra=()): # tweak arguments @@ -425,16 +390,17 @@ def _getdecoder(mode, decoder_name, args, extra=()): try: decoder = DECODERS[decoder_name] - return decoder(mode, *args + extra) except KeyError: pass + else: + return decoder(mode, *args + extra) + try: # get decoder decoder = getattr(core, decoder_name + "_decoder") - # print(decoder, mode, args + extra) - return decoder(mode, *args + extra) - except AttributeError: - raise IOError("decoder %s not available" % decoder_name) + except AttributeError as e: + raise OSError(f"decoder {decoder_name} not available") from e + return decoder(mode, *args + extra) def _getencoder(mode, encoder_name, args, extra=()): @@ -447,61 +413,75 @@ def _getencoder(mode, encoder_name, args, extra=()): try: encoder = ENCODERS[encoder_name] - return encoder(mode, *args + extra) except KeyError: pass + else: + return encoder(mode, *args + extra) + try: # get encoder encoder = getattr(core, encoder_name + "_encoder") - # print(encoder, mode, args + extra) - return encoder(mode, *args + extra) - except AttributeError: - raise IOError("encoder %s not available" % encoder_name) + except AttributeError as e: + raise OSError(f"encoder {encoder_name} not available") from e + return encoder(mode, *args + extra) # -------------------------------------------------------------------- # Simple expression analyzer + def coerce_e(value): - return value if isinstance(value, _E) else _E(value) + deprecate("coerce_e", 10) + return value if isinstance(value, _E) else _E(1, value) -class _E(object): - def __init__(self, data): +# _E(scale, offset) represents the affine transformation scale * x + offset. +# The "data" field is named for compatibility with the old implementation, +# and should be renamed once coerce_e is removed. +class _E: + def __init__(self, scale, data): + self.scale = scale self.data = data + def __neg__(self): + return _E(-self.scale, -self.data) + def __add__(self, other): - return _E((self.data, "__add__", coerce_e(other).data)) + if isinstance(other, _E): + return _E(self.scale + other.scale, self.data + other.data) + return _E(self.scale, self.data + other) + + __radd__ = __add__ + + def __sub__(self, other): + return self + -other + + def __rsub__(self, other): + return other + -self def __mul__(self, other): - return _E((self.data, "__mul__", coerce_e(other).data)) + if isinstance(other, _E): + return NotImplemented + return _E(self.scale * other, self.data * other) + + __rmul__ = __mul__ + + def __truediv__(self, other): + if isinstance(other, _E): + return NotImplemented + return _E(self.scale / other, self.data / other) def _getscaleoffset(expr): - stub = ["stub"] - data = expr(_E(stub)).data - try: - (a, b, c) = data # simplified syntax - if (a is stub and b == "__mul__" and isinstance(c, numbers.Number)): - return c, 0.0 - if a is stub and b == "__add__" and isinstance(c, numbers.Number): - return 1.0, c - except TypeError: - pass - try: - ((a, b, c), d, e) = data # full syntax - if (a is stub and b == "__mul__" and isinstance(c, numbers.Number) and - d == "__add__" and isinstance(e, numbers.Number)): - return c, e - except TypeError: - pass - raise ValueError("illegal expression") + a = expr(_E(1, 0)) + return (a.scale, a.data) if isinstance(a, _E) else (0, a) # -------------------------------------------------------------------- # Implementation wrapper -class Image(object): + +class Image: """ This class represents an image object. To create :py:class:`~PIL.Image.Image` objects, use the appropriate factory @@ -512,6 +492,7 @@ class Image(object): * :py:func:`~PIL.Image.new` * :py:func:`~PIL.Image.frombytes` """ + format = None format_description = None _close_exclusive_fp_after_loading = True @@ -521,12 +502,19 @@ def __init__(self): # FIXME: turn mode and size into delegating properties? self.im = None self.mode = "" - self.size = (0, 0) + self._size = (0, 0) self.palette = None self.info = {} - self.category = NORMAL + self._category = 0 self.readonly = 0 self.pyaccess = None + self._exif = None + + def __getattr__(self, name): + if name == "category": + deprecate("Image categories", 10, "is_animated", plural=True) + return self._category + raise AttributeError(name) @property def width(self): @@ -536,26 +524,38 @@ def width(self): def height(self): return self.size[1] + @property + def size(self): + return self._size + def _new(self, im): new = Image() new.im = im new.mode = im.mode - new.size = im.size - if im.mode in ('P', 'PA'): + new._size = im.size + if im.mode in ("P", "PA"): if self.palette: new.palette = self.palette.copy() else: from . import ImagePalette + new.palette = ImagePalette.ImagePalette() new.info = self.info.copy() return new - # Context Manager Support + # Context manager support def __enter__(self): return self def __exit__(self, *args): - self.close() + if hasattr(self, "fp") and getattr(self, "_exclusive_fp", False): + if getattr(self, "_fp", False): + if self._fp != self.fp: + self._fp.close() + self._fp = DeferredError(ValueError("Operation on closed image")) + if self.fp: + self.fp.close() + self.fp = None def close(self): """ @@ -564,30 +564,29 @@ def close(self): This operation will destroy the image core and release its memory. The image data will be unusable afterward. - This function is only required to close images that have not - had their file read and closed by the - :py:meth:`~PIL.Image.Image.load` method. + This function is required to close images that have multiple frames or + have not had their file read and closed by the + :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for + more information. """ try: - self.fp.close() + if getattr(self, "_fp", False): + if self._fp != self.fp: + self._fp.close() + self._fp = DeferredError(ValueError("Operation on closed image")) + if self.fp: + self.fp.close() self.fp = None except Exception as msg: logger.debug("Error closing: %s", msg) - if getattr(self, 'map', None): + if getattr(self, "map", None): self.map = None # Instead of simply setting to None, we're setting up a # deferred error that will better explain that the core image # object is gone. - self.im = deferred_error(ValueError("Operation on closed image")) - - if sys.version_info >= (3, 4, 0): - def __del__(self): - if (hasattr(self, 'fp') and hasattr(self, '_exclusive_fp') - and self.fp and self._exclusive_fp): - self.fp.close() - self.fp = None + self.im = DeferredError(ValueError("Operation on closed image")) def _copy(self): self.load() @@ -602,11 +601,9 @@ def _ensure_mutable(self): self.load() def _dump(self, file=None, format=None, **options): - import tempfile - - suffix = '' + suffix = "" if format: - suffix = '.'+format + suffix = "." + format if not file: f, filename = tempfile.mkstemp(suffix) @@ -626,35 +623,52 @@ def _dump(self, file=None, format=None, **options): return filename def __eq__(self, other): - return (isinstance(other, Image) and - self.__class__.__name__ == other.__class__.__name__ and - self.mode == other.mode and - self.size == other.size and - self.info == other.info and - self.category == other.category and - self.readonly == other.readonly and - self.getpalette() == other.getpalette() and - self.tobytes() == other.tobytes()) - - def __ne__(self, other): - eq = (self == other) - return not eq + return ( + self.__class__ is other.__class__ + and self.mode == other.mode + and self.size == other.size + and self.info == other.info + and self._category == other._category + and self.getpalette() == other.getpalette() + and self.tobytes() == other.tobytes() + ) def __repr__(self): return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % ( - self.__class__.__module__, self.__class__.__name__, - self.mode, self.size[0], self.size[1], - id(self) + self.__class__.__module__, + self.__class__.__name__, + self.mode, + self.size[0], + self.size[1], + id(self), + ) + + def _repr_pretty_(self, p, cycle): + """IPython plain text display support""" + + # Same as __repr__ but without unpredictable id(self), + # to keep Jupyter notebook `text/plain` output stable. + p.text( + "<%s.%s image mode=%s size=%dx%d>" + % ( + self.__class__.__module__, + self.__class__.__name__, + self.mode, + self.size[0], + self.size[1], ) + ) def _repr_png_(self): - """ iPython display hook support + """iPython display hook support :returns: png version of the image as bytes """ - from io import BytesIO - b = BytesIO() - self.save(b, 'PNG') + b = io.BytesIO() + try: + self.save(b, "PNG") + except Exception as e: + raise ValueError("Could not save to PNG for display") from e return b.getvalue() @property @@ -662,24 +676,31 @@ def __array_interface__(self): # numpy array interface support new = {} shape, typestr = _conv_type_shape(self) - new['shape'] = shape - new['typestr'] = typestr - new['version'] = 3 - if self.mode == '1': - # Binary images need to be extended from bits to bytes - # See: https://github.com/python-pillow/Pillow/issues/350 - new['data'] = self.tobytes('raw', 'L') - else: - new['data'] = self.tobytes() + new["shape"] = shape + new["typestr"] = typestr + new["version"] = 3 + try: + if self.mode == "1": + # Binary images need to be extended from bits to bytes + # See: https://github.com/python-pillow/Pillow/issues/350 + new["data"] = self.tobytes("raw", "L") + else: + new["data"] = self.tobytes() + except Exception as e: + if not isinstance(e, (MemoryError, RecursionError)): + try: + import numpy + from packaging.version import parse as parse_version + except ImportError: + pass + else: + if parse_version(numpy.__version__) < parse_version("1.23"): + warnings.warn(e) + raise return new def __getstate__(self): - return [ - self.info, - self.mode, - self.size, - self.getpalette(), - self.tobytes()] + return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()] def __setstate__(self, state): Image.__init__(self) @@ -687,9 +708,9 @@ def __setstate__(self, state): info, mode, size, palette, data = state self.info = info self.mode = mode - self.size = size + self._size = size self.im = core.new(mode, size) - if mode in ("L", "P") and palette: + if mode in ("L", "LA", "P", "PA") and palette: self.putpalette(palette) self.frombytes(data) @@ -706,8 +727,13 @@ def tobytes(self, encoder_name="raw", *args): :param encoder_name: What encoder to use. The default is to use the standard "raw" encoder. + + A list of C encoders can be seen under + codecs section of the function array in + :file:`_imaging.c`. Python encoders are + registered within the relevant plugins. :param args: Extra arguments to the encoder. - :rtype: A bytes object. + :returns: A :py:class:`bytes` object. """ # may pass tuple instead of argument list @@ -719,6 +745,9 @@ def tobytes(self, encoder_name="raw", *args): self.load() + if self.width == 0 or self.height == 0: + return b"" + # unpack data e = _getencoder(self.mode, encoder_name, args) e.setimage(self.im) @@ -732,14 +761,10 @@ def tobytes(self, encoder_name="raw", *args): if s: break if s < 0: - raise RuntimeError("encoder error %d in tobytes" % s) + raise RuntimeError(f"encoder error {s} in tobytes") return b"".join(data) - def tostring(self, *args, **kw): - raise NotImplementedError("tostring() has been removed. " - "Please call tobytes() instead.") - def tobitmap(self, name="image"): """ Returns the image converted to an X11 bitmap. @@ -755,11 +780,15 @@ def tobitmap(self, name="image"): if self.mode != "1": raise ValueError("not a bitmap") data = self.tobytes("xbm") - return b"".join([ - ("#define %s_width %d\n" % (name, self.size[0])).encode('ascii'), - ("#define %s_height %d\n" % (name, self.size[1])).encode('ascii'), - ("static char %s_bits[] = {\n" % name).encode('ascii'), data, b"};" - ]) + return b"".join( + [ + f"#define {name}_width {self.size[0]}\n".encode("ascii"), + f"#define {name}_height {self.size[1]}\n".encode("ascii"), + f"static char {name}_bits[] = {{\n".encode("ascii"), + data, + b"};", + ] + ) def frombytes(self, data, decoder_name="raw", *args): """ @@ -787,39 +816,44 @@ def frombytes(self, data, decoder_name="raw", *args): if s[1] != 0: raise ValueError("cannot decode image data") - def fromstring(self, *args, **kw): - raise NotImplementedError("fromstring() has been removed. " - "Please call frombytes() instead.") - def load(self): """ Allocates storage for the image and loads the pixel data. In normal cases, you don't need to call this method, since the Image class automatically loads an opened image when it is - accessed for the first time. This method will close the file - associated with the image. + accessed for the first time. + + If the file associated with the image was opened by Pillow, then this + method will close it. The exception to this is if the image has + multiple frames, in which case the file will be left open for seek + operations. See :ref:`file-handling` for more information. :returns: An image access object. :rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess` """ - if self.im and self.palette and self.palette.dirty: + if self.im is not None and self.palette and self.palette.dirty: # realize palette - self.im.putpalette(*self.palette.getdata()) + mode, arr = self.palette.getdata() + self.im.putpalette(mode, arr) self.palette.dirty = 0 - self.palette.mode = "RGB" self.palette.rawmode = None - if "transparency" in self.info: + if "transparency" in self.info and mode in ("LA", "PA"): if isinstance(self.info["transparency"], int): self.im.putpalettealpha(self.info["transparency"], 0) else: self.im.putpalettealphas(self.info["transparency"]) self.palette.mode = "RGBA" + else: + palette_mode = "RGBA" if mode.startswith("RGBA") else "RGB" + self.palette.mode = palette_mode + self.palette.palette = self.im.getpalette(palette_mode, palette_mode) - if self.im: - if HAS_CFFI and USE_CFFI_ACCESS: + if self.im is not None: + if cffi and USE_CFFI_ACCESS: if self.pyaccess: return self.pyaccess from . import PyAccess + self.pyaccess = PyAccess.new(self, self.readonly) if self.pyaccess: return self.pyaccess @@ -836,8 +870,9 @@ def verify(self): """ pass - def convert(self, mode=None, matrix=None, dither=None, - palette=WEB, colors=256): + def convert( + self, mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256 + ): """ Returns a converted copy of this image. For the "P" mode, this method translates pixels through the palette. If mode is @@ -845,10 +880,10 @@ def convert(self, mode=None, matrix=None, dither=None, and the palette can be represented without a palette. The current version supports all possible conversions between - "L", "RGB" and "CMYK." The **matrix** argument only supports "L" + "L", "RGB" and "CMYK". The ``matrix`` argument only supports "L" and "RGB". - When translating a color image to black and white (mode "L"), + When translating a color image to greyscale (mode "L"), the library uses the ITU-R 601-2 luma transform:: L = R * 299/1000 + G * 587/1000 + B * 114/1000 @@ -856,32 +891,44 @@ def convert(self, mode=None, matrix=None, dither=None, The default method of converting a greyscale ("L") or "RGB" image into a bilevel (mode "1") image uses Floyd-Steinberg dither to approximate the original image luminosity levels. If - dither is NONE, all non-zero values are set to 255 (white). To - use other thresholds, use the :py:meth:`~PIL.Image.Image.point` - method. + dither is ``None``, all values larger than 127 are set to 255 (white), + all other values to 0 (black). To use other thresholds, use the + :py:meth:`~PIL.Image.Image.point` method. + + When converting from "RGBA" to "P" without a ``matrix`` argument, + this passes the operation to :py:meth:`~PIL.Image.Image.quantize`, + and ``dither`` and ``palette`` are ignored. + + When converting from "PA", if an "RGBA" palette is present, the alpha + channel from the image will be used instead of the values from the palette. :param mode: The requested mode. See: :ref:`concept-modes`. :param matrix: An optional conversion matrix. If given, this should be 4- or 12-tuple containing floating point values. :param dither: Dithering method, used when converting from mode "RGB" to "P" or from "RGB" or "L" to "1". - Available methods are NONE or FLOYDSTEINBERG (default). + Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` + (default). Note that this is not used when ``matrix`` is supplied. :param palette: Palette to use when converting from mode "RGB" - to "P". Available palettes are WEB or ADAPTIVE. - :param colors: Number of colors to use for the ADAPTIVE palette. - Defaults to 256. + to "P". Available palettes are :data:`Palette.WEB` or + :data:`Palette.ADAPTIVE`. + :param colors: Number of colors to use for the :data:`Palette.ADAPTIVE` + palette. Defaults to 256. :rtype: :py:class:`~PIL.Image.Image` :returns: An :py:class:`~PIL.Image.Image` object. """ self.load() + has_transparency = self.info.get("transparency") is not None if not mode and self.mode == "P": # determine default mode if self.palette: mode = self.palette.mode else: mode = "RGB" + if mode == "RGB" and has_transparency: + mode = "RGBA" if not mode or (mode == self.mode and not matrix): return self.copy() @@ -890,7 +937,23 @@ def convert(self, mode=None, matrix=None, dither=None, if mode not in ("L", "RGB"): raise ValueError("illegal conversion") im = self.im.convert_matrix(mode, matrix) - return self._new(im) + new = self._new(im) + if has_transparency and self.im.bands == 3: + transparency = new.info["transparency"] + + def convert_transparency(m, v): + v = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5 + return max(0, min(255, int(v))) + + if mode == "L": + transparency = convert_transparency(matrix, transparency) + elif len(mode) == 3: + transparency = tuple( + convert_transparency(matrix[i * 4 : i * 4 + 4], transparency) + for i in range(0, len(transparency)) + ) + new.info["transparency"] = transparency + return new if mode == "P" and self.mode == "RGBA": return self.quantize(colors) @@ -898,47 +961,58 @@ def convert(self, mode=None, matrix=None, dither=None, trns = None delete_trns = False # transparency handling - if "transparency" in self.info and \ - self.info['transparency'] is not None: - if self.mode in ('L', 'RGB') and mode == 'RGBA': + if has_transparency: + if (self.mode in ("1", "L", "I") and mode in ("LA", "RGBA")) or ( + self.mode == "RGB" and mode == "RGBA" + ): # Use transparent conversion to promote from transparent # color to an alpha channel. - new_im = self._new(self.im.convert_transparent( - mode, self.info['transparency'])) - del(new_im.info['transparency']) + new_im = self._new( + self.im.convert_transparent(mode, self.info["transparency"]) + ) + del new_im.info["transparency"] return new_im - elif self.mode in ('L', 'RGB', 'P') and mode in ('L', 'RGB', 'P'): - t = self.info['transparency'] + elif self.mode in ("L", "RGB", "P") and mode in ("L", "RGB", "P"): + t = self.info["transparency"] if isinstance(t, bytes): # Dragons. This can't be represented by a single color - warnings.warn('Palette images with Transparency ' + - ' expressed in bytes should be converted ' + - 'to RGBA images') + warnings.warn( + "Palette images with Transparency expressed in bytes should be " + "converted to RGBA images" + ) delete_trns = True else: # get the new transparency color. # use existing conversions trns_im = Image()._new(core.new(self.mode, (1, 1))) - if self.mode == 'P': + if self.mode == "P": trns_im.putpalette(self.palette) if isinstance(t, tuple): + err = "Couldn't allocate a palette color for transparency" try: - t = trns_im.palette.getcolor(t) - except: - raise ValueError("Couldn't allocate a palette " - "color for transparency") - trns_im.putpixel((0, 0), t) - - if mode in ('L', 'RGB'): - trns_im = trns_im.convert(mode) + t = trns_im.palette.getcolor(t, self) + except ValueError as e: + if str(e) == "cannot allocate more than 256 colors": + # If all 256 colors are in use, + # then there is no need for transparency + t = None + else: + raise ValueError(err) from e + if t is None: + trns = None else: - # can't just retrieve the palette number, got to do it - # after quantization. - trns_im = trns_im.convert('RGB') - trns = trns_im.getpixel((0, 0)) - - elif self.mode == 'P' and mode == 'RGBA': - t = self.info['transparency'] + trns_im.putpixel((0, 0), t) + + if mode in ("L", "RGB"): + trns_im = trns_im.convert(mode) + else: + # can't just retrieve the palette number, got to do it + # after quantization. + trns_im = trns_im.convert("RGB") + trns = trns_im.getpixel((0, 0)) + + elif self.mode == "P" and mode in ("LA", "PA", "RGBA"): + t = self.info["transparency"] delete_trns = True if isinstance(t, bytes): @@ -946,71 +1020,114 @@ def convert(self, mode=None, matrix=None, dither=None, elif isinstance(t, int): self.im.putpalettealpha(t, 0) else: - raise ValueError("Transparency for P mode should" + - " be bytes or int") + raise ValueError("Transparency for P mode should be bytes or int") - if mode == "P" and palette == ADAPTIVE: + if mode == "P" and palette == Palette.ADAPTIVE: im = self.im.quantize(colors) new = self._new(im) from . import ImagePalette - new.palette = ImagePalette.raw("RGB", new.im.getpalette("RGB")) + + new.palette = ImagePalette.ImagePalette("RGB", new.im.getpalette("RGB")) if delete_trns: # This could possibly happen if we requantize to fewer colors. # The transparency would be totally off in that case. - del(new.info['transparency']) + del new.info["transparency"] if trns is not None: try: - new.info['transparency'] = new.palette.getcolor(trns) - except: + new.info["transparency"] = new.palette.getcolor(trns, new) + except Exception: # if we can't make a transparent color, don't leave the old # transparency hanging around to mess us up. - del(new.info['transparency']) - warnings.warn("Couldn't allocate palette entry " + - "for transparency") + del new.info["transparency"] + warnings.warn("Couldn't allocate palette entry for transparency") return new + if "LAB" in (self.mode, mode): + other_mode = mode if self.mode == "LAB" else self.mode + if other_mode in ("RGB", "RGBA", "RGBX"): + from . import ImageCms + + srgb = ImageCms.createProfile("sRGB") + lab = ImageCms.createProfile("LAB") + profiles = [lab, srgb] if self.mode == "LAB" else [srgb, lab] + transform = ImageCms.buildTransform( + profiles[0], profiles[1], self.mode, mode + ) + return transform.apply(self) + # colorspace conversion if dither is None: - dither = FLOYDSTEINBERG + dither = Dither.FLOYDSTEINBERG try: im = self.im.convert(mode, dither) except ValueError: try: # normalize source image and try again - im = self.im.convert(getmodebase(self.mode)) + modebase = getmodebase(self.mode) + if modebase == self.mode: + raise + im = self.im.convert(modebase) im = im.convert(mode, dither) - except KeyError: - raise ValueError("illegal conversion") + except KeyError as e: + raise ValueError("illegal conversion") from e new_im = self._new(im) + if mode == "P" and palette != Palette.ADAPTIVE: + from . import ImagePalette + + new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) if delete_trns: # crash fail if we leave a bytes transparency in an rgb/l mode. - del(new_im.info['transparency']) + del new_im.info["transparency"] if trns is not None: - if new_im.mode == 'P': + if new_im.mode == "P": try: - new_im.info['transparency'] = new_im.palette.getcolor(trns) - except: - del(new_im.info['transparency']) - warnings.warn("Couldn't allocate palette entry " + - "for transparency") + new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im) + except ValueError as e: + del new_im.info["transparency"] + if str(e) != "cannot allocate more than 256 colors": + # If all 256 colors are in use, + # then there is no need for transparency + warnings.warn( + "Couldn't allocate palette entry for transparency" + ) else: - new_im.info['transparency'] = trns + new_im.info["transparency"] = trns return new_im - def quantize(self, colors=256, method=None, kmeans=0, palette=None): + def quantize( + self, + colors=256, + method=None, + kmeans=0, + palette=None, + dither=Dither.FLOYDSTEINBERG, + ): """ Convert the image to 'P' mode with the specified number of colors. :param colors: The desired number of colors, <= 256 - :param method: 0 = median cut - 1 = maximum coverage - 2 = fast octree - 3 = libimagequant + :param method: :data:`Quantize.MEDIANCUT` (median cut), + :data:`Quantize.MAXCOVERAGE` (maximum coverage), + :data:`Quantize.FASTOCTREE` (fast octree), + :data:`Quantize.LIBIMAGEQUANT` (libimagequant; check support + using :py:func:`PIL.features.check_feature` with + ``feature="libimagequant"``). + + By default, :data:`Quantize.MEDIANCUT` will be used. + + The exception to this is RGBA images. :data:`Quantize.MEDIANCUT` + and :data:`Quantize.MAXCOVERAGE` do not support RGBA images, so + :data:`Quantize.FASTOCTREE` is used by default instead. :param kmeans: Integer - :param palette: Quantize to the palette of given :py:class:`PIL.Image.Image`. + :param palette: Quantize to the palette of given + :py:class:`PIL.Image.Image`. + :param dither: Dithering method, used when converting from + mode "RGB" to "P" or from "RGB" or "L" to "1". + Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` + (default). :returns: A new image """ @@ -1019,15 +1136,19 @@ def quantize(self, colors=256, method=None, kmeans=0, palette=None): if method is None: # defaults: - method = 0 - if self.mode == 'RGBA': - method = 2 + method = Quantize.MEDIANCUT + if self.mode == "RGBA": + method = Quantize.FASTOCTREE - if self.mode == 'RGBA' and method not in (2, 3): + if self.mode == "RGBA" and method not in ( + Quantize.FASTOCTREE, + Quantize.LIBIMAGEQUANT, + ): # Caller specified an invalid mode. raise ValueError( - 'Fast Octree (method == 2) and libimagequant (method == 3) ' + - 'are the only valid methods for quantizing RGBA images') + "Fast Octree (method == 2) and libimagequant (method == 3) " + "are the only valid methods for quantizing RGBA images" + ) if palette: # use palette from reference image @@ -1037,11 +1158,21 @@ def quantize(self, colors=256, method=None, kmeans=0, palette=None): if self.mode != "RGB" and self.mode != "L": raise ValueError( "only RGB or L mode images can be quantized to a palette" - ) - im = self.im.convert("P", 1, palette.im) - return self._new(im) + ) + im = self.im.convert("P", dither, palette.im) + new_im = self._new(im) + new_im.palette = palette.palette.copy() + return new_im - return self._new(self.im.quantize(colors, method, kmeans)) + im = self._new(self.im.quantize(colors, method, kmeans)) + + from . import ImagePalette + + mode = im.im.getpalettemode() + palette = im.im.getpalette(mode, mode)[: colors * len(mode)] + im.palette = ImagePalette.ImagePalette(mode, palette) + + return im def copy(self): """ @@ -1060,7 +1191,7 @@ def crop(self, box=None): """ Returns a rectangular region from this image. The box is a 4-tuple defining the left, upper, right, and lower pixel - coordinate. + coordinate. See :ref:`coordinate-system`. Note: Prior to Pillow 3.4.0, this was a lazy operation. @@ -1072,6 +1203,11 @@ def crop(self, box=None): if box is None: return self.copy() + if box[2] < box[0]: + raise ValueError("Coordinate 'right' is less than 'left'") + elif box[3] < box[1]: + raise ValueError("Coordinate 'lower' is less than 'upper'") + self.load() return self._new(self._crop(self.im, box)) @@ -1089,12 +1225,9 @@ def _crop(self, im, box): x0, y0, x1, y1 = map(int, map(round, box)) - if x1 < x0: - x1 = x0 - if y1 < y0: - y1 = y0 + absolute_values = (abs(x1 - x0), abs(y1 - y0)) - _decompression_bomb_check((x1, y1)) + _decompression_bomb_check(absolute_values) return im.crop((x0, y0, x1, y1)) @@ -1102,16 +1235,18 @@ def draft(self, mode, size): """ Configures the image file loader so it returns a version of the image that as closely as possible matches the given mode and - size. For example, you can use this method to convert a color - JPEG to greyscale while loading it, or to extract a 128x192 - version from a PCD file. + size. For example, you can use this method to convert a color + JPEG to greyscale while loading it. + + If any changes are made, returns a tuple with the chosen ``mode`` and + ``box`` with coordinates of the original image within the altered one. Note that this method modifies the :py:class:`~PIL.Image.Image` object - in place. If the image has already been loaded, this method has no + in place. If the image has already been loaded, this method has no effect. Note: This method is not implemented for most images. It is - currently implemented only for JPEG and PCD images. + currently implemented only for JPEG and MPO images. :param mode: The requested mode. :param size: The requested size. @@ -1130,17 +1265,18 @@ def filter(self, filter): available filters, see the :py:mod:`~PIL.ImageFilter` module. :param filter: Filter kernel. - :returns: An :py:class:`~PIL.Image.Image` object. """ + :returns: An :py:class:`~PIL.Image.Image` object.""" from . import ImageFilter self.load() - if isinstance(filter, collections.Callable): + if isinstance(filter, Callable): filter = filter() if not hasattr(filter, "filter"): - raise TypeError("filter argument should be ImageFilter.Filter " + - "instance or class") + raise TypeError( + "filter argument should be ImageFilter.Filter instance or class" + ) multiband = isinstance(filter, ImageFilter.MultibandFilter) if self.im.bands == 1 or multiband: @@ -1154,7 +1290,7 @@ def filter(self, filter): def getbands(self): """ Returns a tuple containing the name of each band in this image. - For example, **getbands** on an RGB image returns ("R", "G", "B"). + For example, ``getbands`` on an RGB image returns ("R", "G", "B"). :returns: A tuple containing band names. :rtype: tuple @@ -1167,8 +1303,9 @@ def getbbox(self): image. :returns: The bounding box is returned as a 4-tuple defining the - left, upper, right, and lower pixel coordinate. If the image - is completely empty, this method returns None. + left, upper, right, and lower pixel coordinate. See + :ref:`coordinate-system`. If the image is completely empty, this + method returns None. """ @@ -1179,6 +1316,10 @@ def getcolors(self, maxcolors=256): """ Returns a list of colors used in this image. + The colors will be in the image's mode. For example, an RGB image will + return a tuple of (red, green, blue) color values, and a P image will + return the index of the color in the palette. + :param maxcolors: Maximum number of colors. If this number is exceeded, this method returns None. The default limit is 256 colors. @@ -1207,7 +1348,7 @@ def getdata(self, band=None): Note that the sequence object returned by this method is an internal PIL data type, which only supports certain sequence operations. To convert it to an ordinary sequence (e.g. for - printing), use **list(im.getdata())**. + printing), use ``list(im.getdata())``. :param band: What band to return. The default is to return all bands. To return a single band, pass in the index @@ -1222,7 +1363,7 @@ def getdata(self, band=None): def getextrema(self): """ - Gets the the minimum and maximum pixel values for each band in + Gets the minimum and maximum pixel values for each band in the image. :returns: For a single-band image, a 2-tuple containing the @@ -1238,6 +1379,74 @@ def getextrema(self): return tuple(extrema) return self.im.getextrema() + def _getxmp(self, xmp_tags): + def get_name(tag): + return tag.split("}")[1] + + def get_value(element): + value = {get_name(k): v for k, v in element.attrib.items()} + children = list(element) + if children: + for child in children: + name = get_name(child.tag) + child_value = get_value(child) + if name in value: + if not isinstance(value[name], list): + value[name] = [value[name]] + value[name].append(child_value) + else: + value[name] = child_value + elif value: + if element.text: + value["text"] = element.text + else: + return element.text + return value + + if ElementTree is None: + warnings.warn("XMP data cannot be read without defusedxml dependency") + return {} + else: + root = ElementTree.fromstring(xmp_tags) + return {get_name(root.tag): get_value(root)} + + def getexif(self): + if self._exif is None: + self._exif = Exif() + self._exif._loaded = False + elif self._exif._loaded: + return self._exif + self._exif._loaded = True + + exif_info = self.info.get("exif") + if exif_info is None: + if "Raw profile type exif" in self.info: + exif_info = bytes.fromhex( + "".join(self.info["Raw profile type exif"].split("\n")[3:]) + ) + elif hasattr(self, "tag_v2"): + self._exif.bigtiff = self.tag_v2._bigtiff + self._exif.endian = self.tag_v2._endian + self._exif.load_from_fp(self.fp, self.tag_v2._offset) + if exif_info is not None: + self._exif.load(exif_info) + + # XMP tags + if 0x0112 not in self._exif: + xmp_tags = self.info.get("XML:com.adobe.xmp") + if xmp_tags: + match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) + if match: + self._exif[0x0112] = int(match[2]) + + return self._exif + + def _reload_exif(self): + if self._exif is None or not self._exif._loaded: + return + self._exif._loaded = False + self.getexif() + def getim(self): """ Returns a capsule that points to the internal image memory. @@ -1248,28 +1457,56 @@ def getim(self): self.load() return self.im.ptr - def getpalette(self): + def getpalette(self, rawmode="RGB"): """ Returns the image palette as a list. + :param rawmode: The mode in which to return the palette. ``None`` will + return the palette in its current mode. + + .. versionadded:: 9.1.0 + :returns: A list of color values [r, g, b, ...], or None if the image has no palette. """ self.load() try: - if bytes is str: - return [i8(c) for c in self.im.getpalette()] - else: - return list(self.im.getpalette()) + mode = self.im.getpalettemode() except ValueError: return None # no palette + if rawmode is None: + rawmode = mode + return list(self.im.getpalette(mode, rawmode)) + + 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. + """ + if self.mode != "P" or "transparency" not in self.info: + return + + from . import ImagePalette + + palette = self.getpalette("RGBA") + transparency = self.info["transparency"] + if isinstance(transparency, bytes): + for i, alpha in enumerate(transparency): + palette[i * 4 + 3] = alpha + else: + palette[transparency * 4 + 3] = 0 + self.palette = ImagePalette.ImagePalette("RGBA", bytes(palette)) + self.palette.dirty = 1 + + del self.info["transparency"] def getpixel(self, xy): """ Returns the pixel value at a given position. - :param xy: The coordinate, given as (x, y). + :param xy: The coordinate, given as (x, y). See + :ref:`coordinate-system`. :returns: The pixel value. If the image is a multi-layer image, this method returns a tuple. """ @@ -1289,15 +1526,16 @@ def getprojection(self): self.load() x, y = self.im.getprojection() - return [i8(c) for c in x], [i8(c) for c in y] + return list(x), list(y) def histogram(self, mask=None, extrema=None): """ - Returns a histogram for the image. The histogram is returned as - a list of pixel counts, one for each pixel value in the source - image. If the image has more than one band, the histograms for - all bands are concatenated (for example, the histogram for an - "RGB" image contains 768 values). + Returns a histogram for the image. The histogram is returned as a + list of pixel counts, one for each pixel value in the source + image. Counts are grouped into 256 bins for each band, even if + the image has more than 8 bits per band. If the image has more + than one band, the histograms for all bands are concatenated (for + example, the histogram for an "RGB" image contains 768 values). A bilevel image (mode "1") is treated as a greyscale ("L") image by this method. @@ -1308,6 +1546,7 @@ def histogram(self, mask=None, extrema=None): bi-level image (mode "1") or a greyscale image ("L"). :param mask: An optional mask. + :param extrema: An optional tuple of manually-specified extrema. :returns: A list containing pixel counts. """ self.load() @@ -1320,17 +1559,39 @@ def histogram(self, mask=None, extrema=None): return self.im.histogram(extrema) return self.im.histogram() - def offset(self, xoffset, yoffset=None): - raise NotImplementedError("offset() has been removed. " - "Please call ImageChops.offset() instead.") + def entropy(self, mask=None, extrema=None): + """ + Calculates and returns the entropy for the image. + + A bilevel image (mode "1") is treated as a greyscale ("L") + image by this method. + + If a mask is provided, the method employs the histogram for + those parts of the image where the mask image is non-zero. + The mask image must have the same size as the image, and be + either a bi-level image (mode "1") or a greyscale image ("L"). + + :param mask: An optional mask. + :param extrema: An optional tuple of manually-specified extrema. + :returns: A float value representing the image entropy + """ + self.load() + if mask: + mask.load() + return self.im.entropy((0, 0), mask.im) + if self.mode in ("I", "F"): + if extrema is None: + extrema = self.getextrema() + return self.im.entropy(extrema) + return self.im.entropy() def paste(self, im, box=None, mask=None): """ Pastes another image into this image. The box argument is either a 2-tuple giving the upper left corner, a 4-tuple defining the left, upper, right, and lower pixel coordinate, or None (same as - (0, 0)). If a 4-tuple is given, the size of the pasted image - must match the size of the region. + (0, 0)). See :ref:`coordinate-system`. If a 4-tuple is given, the size + of the pasted image must match the size of the region. If the modes don't match, the pasted image is converted to the mode of this image (see the :py:meth:`~PIL.Image.Image.convert` method for @@ -1342,8 +1603,8 @@ def paste(self, im, box=None, mask=None): also use color strings as supported by the ImageColor module. If a mask is given, this method updates only the regions - indicated by the mask. You can use either "1", "L" or "RGBA" - images (in the latter case, the alpha band is used as mask). + indicated by the mask. You can use either "1", "L", "LA", "RGBA" + or "RGBa" images (if present, the alpha band is used as mask). Where the mask is 255, the given image is copied as is. Where the mask is 0, the current value is preserved. Intermediate values will mix the two images together, including their alpha @@ -1380,19 +1641,18 @@ def paste(self, im, box=None, mask=None): size = mask.size else: # FIXME: use self.size here? - raise ValueError( - "cannot determine region size; use 4-item box" - ) - box += (box[0]+size[0], box[1]+size[1]) + raise ValueError("cannot determine region size; use 4-item box") + box += (box[0] + size[0], box[1] + size[1]) - if isStringType(im): + if isinstance(im, str): from . import ImageColor + im = ImageColor.getcolor(im, self.mode) elif isImageType(im): im.load() if self.mode != im.mode: - if self.mode != "RGB" or im.mode not in ("RGBA", "RGBa"): + if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): # should use an adapter for this! im = im.convert(self.mode) im = im.im @@ -1405,8 +1665,8 @@ def paste(self, im, box=None, mask=None): else: self.im.paste(im, box) - def alpha_composite(self, im, dest=(0,0), source=(0,0)): - """ 'In-place' analog of Image.alpha_composite. Composites an image + def alpha_composite(self, im, dest=(0, 0), source=(0, 0)): + """'In-place' analog of Image.alpha_composite. Composites an image onto this image. :param im: image to composite over this one @@ -1429,14 +1689,12 @@ def alpha_composite(self, im, dest=(0,0), source=(0,0)): raise ValueError("Destination must be a 2-tuple") if min(source) < 0: raise ValueError("Source must be non-negative") - if min(dest) < 0: - raise ValueError("Destination must be non-negative") if len(source) == 2: source = source + im.size # over image, crop if it's not the whole thing. - if source == (0,0) + im.size: + if source == (0, 0) + im.size: overlay = im else: overlay = im.crop(source) @@ -1445,7 +1703,7 @@ def alpha_composite(self, im, dest=(0,0), source=(0,0)): box = dest + (dest[0] + overlay.width, dest[1] + overlay.height) # destination image. don't copy if we're using the whole image. - if box == (0,0) + self.size: + if box == (0, 0) + self.size: background = self else: background = self.crop(box) @@ -1457,12 +1715,19 @@ def point(self, lut, mode=None): """ Maps this image through a lookup table or function. - :param lut: A lookup table, containing 256 (or 65336 if + :param lut: A lookup table, containing 256 (or 65536 if self.mode=="I" and mode == "L") values per band in the image. A function can be used instead, it should take a single argument. The function is called once for each possible pixel value, and the resulting table is applied to all bands of the image. + + It may also be an :py:class:`~PIL.Image.ImagePointHandler` + object:: + + class Example(Image.ImagePointHandler): + def point(self, data): + # Return result :param mode: Output mode (default is same as input). In the current version, this can only be used if the source image has mode "L" or "P", and the output has mode "1" or the @@ -1490,6 +1755,8 @@ def point(self, lut, mode=None): # FIXME: _imaging returns a confusing error message for this case raise ValueError("point operation not supported for this mode") + if mode != "F": + lut = [round(i) for i in lut] return self._new(self.im.point(lut, mode)) def putalpha(self, alpha): @@ -1505,24 +1772,24 @@ def putalpha(self, alpha): self._ensure_mutable() - if self.mode not in ("LA", "RGBA"): + if self.mode not in ("LA", "PA", "RGBA"): # attempt to promote self to a matching alpha mode try: mode = getmodebase(self.mode) + "A" try: self.im.setmode(mode) - except (AttributeError, ValueError): + except (AttributeError, ValueError) as e: # do things the hard way im = self.im.convert(mode) - if im.mode not in ("LA", "RGBA"): - raise ValueError # sanity check + if im.mode not in ("LA", "PA", "RGBA"): + raise ValueError from e # sanity check self.im = im self.pyaccess = None self.mode = self.im.mode - except (KeyError, ValueError): - raise ValueError("illegal image mode") + except KeyError as e: + raise ValueError("illegal image mode") from e - if self.mode == "LA": + if self.mode in ("LA", "PA"): band = 1 else: band = 3 @@ -1548,13 +1815,14 @@ def putalpha(self, alpha): def putdata(self, data, scale=1.0, offset=0.0): """ - Copies pixel data to this image. This method copies data from a - sequence object into the image, starting at the upper left - corner (0, 0), and continuing until either the image or the - sequence ends. The scale and offset values are used to adjust - the sequence values: **pixel = value*scale + offset**. + Copies pixel data from a flattened sequence object into the image. The + values should start at the upper left corner (0, 0), continue to the + end of the line, followed directly by the first value of the second + line, and so on. Data will be read until either the image or the + sequence ends. The scale and offset values are used to adjust the + sequence values: **pixel = value*scale + offset**. - :param data: A sequence object. + :param data: A flattened sequence object. :param scale: An optional scale value. The default is 1.0. :param offset: An optional offset value. The default is 0.0. """ @@ -1565,31 +1833,34 @@ def putdata(self, data, scale=1.0, offset=0.0): def putpalette(self, data, rawmode="RGB"): """ - Attaches a palette to this image. The image must be a "P" or - "L" image, and the palette sequence must contain 768 integer - values, where each group of three values represent the red, - green, and blue values for the corresponding pixel - index. Instead of an integer sequence, you can use an 8-bit - string. + Attaches a palette to this image. The image must be a "P", "PA", "L" + or "LA" image. + + The palette sequence must contain at most 256 colors, made up of one + integer value for each channel in the raw mode. + For example, if the raw mode is "RGB", then it can contain at most 768 + values, made up of red, green and blue values for the corresponding pixel + index in the 256 colors. + If the raw mode is "RGBA", then it can contain at most 1024 values, + containing red, green, blue and alpha values. + + Alternatively, an 8-bit string may be used instead of an integer sequence. :param data: A palette sequence (either a list or a string). - :param rawmode: The raw mode of the palette. + :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a mode + that can be transformed to "RGB" or "RGBA" (e.g. "R", "BGR;15", "RGBA;L"). """ from . import ImagePalette - if self.mode not in ("L", "P"): + if self.mode not in ("L", "LA", "P", "PA"): raise ValueError("illegal image mode") - self.load() if isinstance(data, ImagePalette.ImagePalette): palette = ImagePalette.raw(data.rawmode, data.palette) else: if not isinstance(data, bytes): - if bytes is str: - data = "".join(chr(x) for x in data) - else: - data = bytes(data) + data = bytes(data) palette = ImagePalette.raw(rawmode, data) - self.mode = "P" + self.mode = "PA" if "A" in self.mode else "P" self.palette = palette self.palette.mode = "RGB" self.load() # install new palette @@ -1598,7 +1869,8 @@ def putpixel(self, xy, value): """ Modifies the pixel at the given position. The color is given as a single numerical value for single-band images, and a tuple for - multi-band images. + multi-band images. In addition to this, RGB and RGBA tuples are + accepted for P and PA images. Note that this method is relatively slow. For more extensive changes, use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw` @@ -1610,7 +1882,8 @@ def putpixel(self, xy, value): * :py:meth:`~PIL.Image.Image.putdata` * :py:mod:`~PIL.ImageDraw` - :param xy: The pixel coordinate, given as (x, y). + :param xy: The pixel coordinate, given as (x, y). See + :ref:`coordinate-system`. :param value: The pixel value. """ @@ -1620,6 +1893,19 @@ def putpixel(self, xy, value): if self.pyaccess: return self.pyaccess.putpixel(xy, value) + + if ( + self.mode in ("P", "PA") + and isinstance(value, (list, tuple)) + and len(value) in [3, 4] + ): + # RGB or RGBA value for a P or PA image + if self.mode == "PA": + alpha = value[3] if len(value) == 4 else 255 + value = value[:3] + value = self.palette.getcolor(value, self) + if self.mode == "PA": + value = (value, alpha) return self.im.putpixel(xy, value) def remap_palette(self, dest_map, source_palette=None): @@ -1627,7 +1913,7 @@ def remap_palette(self, dest_map, source_palette=None): Rewrites the image to reorder the palette. :param dest_map: A list of indexes into the original palette. - e.g. [1,0] would swap a two item palette, and list(range(255)) + e.g. ``[1,0]`` would swap a two item palette, and ``list(range(256))`` is the identity transform. :param source_palette: Bytes or None. :returns: An :py:class:`~PIL.Image.Image` object. @@ -1638,20 +1924,26 @@ def remap_palette(self, dest_map, source_palette=None): if self.mode not in ("L", "P"): raise ValueError("illegal image mode") + bands = 3 + palette_mode = "RGB" if source_palette is None: if self.mode == "P": - real_source_palette = self.im.getpalette("RGB")[:768] + self.load() + palette_mode = self.im.getpalettemode() + if palette_mode == "RGBA": + bands = 4 + source_palette = self.im.getpalette(palette_mode, palette_mode) else: # L-mode - real_source_palette = bytearray(i//3 for i in range(768)) - else: - real_source_palette = source_palette + source_palette = bytearray(i // 3 for i in range(768)) palette_bytes = b"" - new_positions = [0]*256 + new_positions = [0] * 256 # pick only the used colors from the palette for i, oldPosition in enumerate(dest_map): - palette_bytes += real_source_palette[oldPosition*3:oldPosition*3+3] + palette_bytes += source_palette[ + oldPosition * bands : oldPosition * bands + bands + ] new_positions[oldPosition] = i # replace the palette color id of all pixel with the new id @@ -1675,56 +1967,116 @@ def remap_palette(self, dest_map, source_palette=None): mapping_palette = bytearray(new_positions) m_im = self.copy() - m_im.mode = 'P' + m_im.mode = "P" - m_im.palette = ImagePalette.ImagePalette("RGB", - palette=mapping_palette*3, - size=768) + m_im.palette = ImagePalette.ImagePalette( + palette_mode, palette=mapping_palette * bands + ) # possibly set palette dirty, then # m_im.putpalette(mapping_palette, 'L') # converts to 'P' # or just force it. # UNDONE -- this is part of the general issue with palettes - m_im.im.putpalette(*m_im.palette.getdata()) + m_im.im.putpalette(palette_mode + ";L", m_im.palette.tobytes()) + + m_im = m_im.convert("L") - m_im = m_im.convert('L') + m_im.putpalette(palette_bytes, palette_mode) + m_im.palette = ImagePalette.ImagePalette(palette_mode, palette=palette_bytes) - # Internally, we require 768 bytes for a palette. - new_palette_bytes = (palette_bytes + - (768 - len(palette_bytes)) * b'\x00') - m_im.putpalette(new_palette_bytes) - m_im.palette = ImagePalette.ImagePalette("RGB", - palette=palette_bytes, - size=len(palette_bytes)) + if "transparency" in self.info: + try: + m_im.info["transparency"] = dest_map.index(self.info["transparency"]) + except ValueError: + if "transparency" in m_im.info: + del m_im.info["transparency"] return m_im - def resize(self, size, resample=NEAREST, box=None): + def _get_safe_box(self, size, resample, box): + """Expands the box so it includes adjacent pixels + that may be used by resampling with the given resampling filter. + """ + filter_support = _filters_support[resample] - 0.5 + scale_x = (box[2] - box[0]) / size[0] + scale_y = (box[3] - box[1]) / size[1] + support_x = filter_support * scale_x + support_y = filter_support * scale_y + + return ( + max(0, int(box[0] - support_x)), + max(0, int(box[1] - support_y)), + min(self.size[0], math.ceil(box[2] + support_x)), + min(self.size[1], math.ceil(box[3] + support_y)), + ) + + def resize(self, size, resample=None, box=None, reducing_gap=None): """ Returns a resized copy of this image. :param size: The requested size in pixels, as a 2-tuple: (width, height). :param resample: An optional resampling filter. This can be - one of :py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BOX`, - :py:attr:`PIL.Image.BILINEAR`, :py:attr:`PIL.Image.HAMMING`, - :py:attr:`PIL.Image.BICUBIC` or :py:attr:`PIL.Image.LANCZOS`. - If omitted, or if the image has mode "1" or "P", it is - set :py:attr:`PIL.Image.NEAREST`. - See: :ref:`concept-filters`. - :param box: An optional 4-tuple of floats giving the region - of the source image which should be scaled. - The values should be within (0, 0, width, height) rectangle. + one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, + :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, + :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. + If the image has mode "1" or "P", it is always set to + :py:data:`Resampling.NEAREST`. If the image mode specifies a number + of bits, such as "I;16", then the default filter is + :py:data:`Resampling.NEAREST`. Otherwise, the default filter is + :py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`. + :param box: An optional 4-tuple of floats providing + the source image region to be scaled. + The values must be within (0, 0, width, height) rectangle. If omitted or None, the entire source is used. + :param reducing_gap: Apply optimization by resizing the image + in two steps. First, reducing the image by integer times + using :py:meth:`~PIL.Image.Image.reduce`. + Second, resizing using regular resampling. The last step + changes size no less than by ``reducing_gap`` times. + ``reducing_gap`` may be None (no first step is performed) + or should be greater than 1.0. The bigger ``reducing_gap``, + the closer the result to the fair resampling. + The smaller ``reducing_gap``, the faster resizing. + With ``reducing_gap`` greater or equal to 3.0, the result is + indistinguishable from fair resampling in most cases. + The default value is None (no optimization). :returns: An :py:class:`~PIL.Image.Image` object. """ - if resample not in ( - NEAREST, BILINEAR, BICUBIC, LANCZOS, BOX, HAMMING, + if resample is None: + type_special = ";" in self.mode + resample = Resampling.NEAREST if type_special else Resampling.BICUBIC + elif resample not in ( + Resampling.NEAREST, + Resampling.BILINEAR, + Resampling.BICUBIC, + Resampling.LANCZOS, + Resampling.BOX, + Resampling.HAMMING, ): - raise ValueError("unknown resampling filter") + message = f"Unknown resampling filter ({resample})." + + filters = [ + f"{filter[1]} ({filter[0]})" + for filter in ( + (Resampling.NEAREST, "Image.Resampling.NEAREST"), + (Resampling.LANCZOS, "Image.Resampling.LANCZOS"), + (Resampling.BILINEAR, "Image.Resampling.BILINEAR"), + (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), + (Resampling.BOX, "Image.Resampling.BOX"), + (Resampling.HAMMING, "Image.Resampling.HAMMING"), + ) + ] + raise ValueError( + message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] + ) + + if reducing_gap is not None and reducing_gap < 1.0: + raise ValueError("reducing_gap must be 1.0 or greater") size = tuple(size) + self.load() if box is None: box = (0, 0) + self.size else: @@ -1734,20 +2086,76 @@ def resize(self, size, resample=NEAREST, box=None): return self.copy() if self.mode in ("1", "P"): - resample = NEAREST + resample = Resampling.NEAREST - if self.mode == 'LA': - return self.convert('La').resize(size, resample, box).convert('LA') - - if self.mode == 'RGBA': - return self.convert('RGBa').resize(size, resample, box).convert('RGBA') + if self.mode in ["LA", "RGBA"] and resample != Resampling.NEAREST: + im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) + im = im.resize(size, resample, box) + return im.convert(self.mode) self.load() + if reducing_gap is not None and resample != Resampling.NEAREST: + factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1 + factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1 + if factor_x > 1 or factor_y > 1: + reduce_box = self._get_safe_box(size, resample, box) + factor = (factor_x, factor_y) + if callable(self.reduce): + self = self.reduce(factor, box=reduce_box) + else: + self = Image.reduce(self, factor, box=reduce_box) + box = ( + (box[0] - reduce_box[0]) / factor_x, + (box[1] - reduce_box[1]) / factor_y, + (box[2] - reduce_box[0]) / factor_x, + (box[3] - reduce_box[1]) / factor_y, + ) + return self._new(self.im.resize(size, resample, box)) - def rotate(self, angle, resample=NEAREST, expand=0, center=None, - translate=None): + def reduce(self, factor, box=None): + """ + Returns a copy of the image reduced ``factor`` times. + If the size of the image is not dividable by ``factor``, + the resulting size will be rounded up. + + :param factor: A greater than 0 integer or tuple of two integers + for width and height separately. + :param box: An optional 4-tuple of ints providing + the source image region to be reduced. + The values must be within ``(0, 0, width, height)`` rectangle. + If omitted or ``None``, the entire source is used. + """ + if not isinstance(factor, (list, tuple)): + factor = (factor, factor) + + if box is None: + box = (0, 0) + self.size + else: + box = tuple(box) + + if factor == (1, 1) and box == (0, 0) + self.size: + return self.copy() + + if self.mode in ["LA", "RGBA"]: + im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) + im = im.reduce(factor, box) + return im.convert(self.mode) + + self.load() + + return self._new(self.im.reduce(factor, box)) + + def rotate( + self, + angle, + resample=Resampling.NEAREST, + expand=0, + center=None, + translate=None, + fillcolor=None, + ): """ Returns a rotated copy of this image. This method returns a copy of this image, rotated the given number of degrees counter @@ -1755,12 +2163,12 @@ def rotate(self, angle, resample=NEAREST, expand=0, center=None, :param angle: In degrees counter clockwise. :param resample: An optional resampling filter. This can be - one of :py:attr:`PIL.Image.NEAREST` (use nearest neighbour), - :py:attr:`PIL.Image.BILINEAR` (linear interpolation in a 2x2 - environment), or :py:attr:`PIL.Image.BICUBIC` - (cubic spline interpolation in a 4x4 environment). - If omitted, or if the image has mode "1" or "P", it is - set :py:attr:`PIL.Image.NEAREST`. See :ref:`concept-filters`. + one of :py:data:`Resampling.NEAREST` (use nearest neighbour), + :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 + environment), or :py:data:`Resampling.BICUBIC` (cubic spline + interpolation in a 4x4 environment). If omitted, or if the image has + mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. + See :ref:`concept-filters`. :param expand: Optional expansion flag. If true, expands the output image to make it large enough to hold the entire rotated image. If false or omitted, make the output image the same size as the @@ -1769,6 +2177,7 @@ def rotate(self, angle, resample=NEAREST, expand=0, center=None, :param center: Optional center of rotation (a 2-tuple). Origin is the upper left corner. Default is the center of the image. :param translate: An optional post-rotate translation (a 2-tuple). + :param fillcolor: An optional color for area outside the rotated image. :returns: An :py:class:`~PIL.Image.Image` object. """ @@ -1780,11 +2189,11 @@ def rotate(self, angle, resample=NEAREST, expand=0, center=None, if angle == 0: return self.copy() if angle == 180: - return self.transpose(ROTATE_180) - if angle == 90 and expand: - return self.transpose(ROTATE_90) - if angle == 270 and expand: - return self.transpose(ROTATE_270) + return self.transpose(Transpose.ROTATE_180) + if angle in (90, 270) and (expand or self.width == self.height): + return self.transpose( + Transpose.ROTATE_90 if angle == 90 else Transpose.ROTATE_270 + ) # Calculate the affine matrix. Note that this is the reverse # transformation (from destination image to source) because we @@ -1811,22 +2220,28 @@ def rotate(self, angle, resample=NEAREST, expand=0, center=None, else: post_trans = translate if center is None: - rotn_center = (w / 2.0, h / 2.0) # FIXME These should be rounded to ints? + # FIXME These should be rounded to ints? + rotn_center = (w / 2.0, h / 2.0) else: rotn_center = center - angle = - math.radians(angle) + angle = -math.radians(angle) matrix = [ - round(math.cos(angle), 15), round(math.sin(angle), 15), 0.0, - round(-math.sin(angle), 15), round(math.cos(angle), 15), 0.0 + round(math.cos(angle), 15), + round(math.sin(angle), 15), + 0.0, + round(-math.sin(angle), 15), + round(math.cos(angle), 15), + 0.0, ] def transform(x, y, matrix): (a, b, c, d, e, f) = matrix - return a*x + b*y + c, d*x + e*y + f + return a * x + b * y + c, d * x + e * y + f - matrix[2], matrix[5] = transform(-rotn_center[0] - post_trans[0], - -rotn_center[1] - post_trans[1], matrix) + matrix[2], matrix[5] = transform( + -rotn_center[0] - post_trans[0], -rotn_center[1] - post_trans[1], matrix + ) matrix[2] += rotn_center[0] matrix[5] += rotn_center[1] @@ -1838,18 +2253,18 @@ def transform(x, y, matrix): x, y = transform(x, y, matrix) xx.append(x) yy.append(y) - nw = int(math.ceil(max(xx)) - math.floor(min(xx))) - nh = int(math.ceil(max(yy)) - math.floor(min(yy))) + nw = math.ceil(max(xx)) - math.floor(min(xx)) + nh = math.ceil(max(yy)) - math.floor(min(yy)) # We multiply a translation matrix from the right. Because of its # special form, this is the same as taking the image of the # translation vector as new translation vector. - matrix[2], matrix[5] = transform(-(nw - w) / 2.0, - -(nh - h) / 2.0, - matrix) + matrix[2], matrix[5] = transform(-(nw - w) / 2.0, -(nh - h) / 2.0, matrix) w, h = nw, nh - return self.transform((w, h), AFFINE, matrix, resample) + return self.transform( + (w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor + ) def save(self, fp, format=None, **params): """ @@ -1873,32 +2288,35 @@ def save(self, fp, format=None, **params): format to use is determined from the filename extension. If a file object was used instead of a filename, this parameter should always be used. - :param options: Extra parameters to the image writer. + :param params: Extra parameters to the image writer. :returns: None - :exception KeyError: If the output format could not be determined + :exception ValueError: If the output format could not be determined from the file name. Use the format option to solve this. - :exception IOError: If the file could not be written. The file + :exception OSError: If the file could not be written. The file may have been created, and may contain partial data. """ filename = "" open_fp = False - if isPath(fp): - filename = fp - open_fp = True - elif HAS_PATHLIB and isinstance(fp, Path): + if isinstance(fp, Path): filename = str(fp) open_fp = True - if not filename and hasattr(fp, "name") and isPath(fp.name): + elif is_path(fp): + filename = fp + open_fp = True + elif fp == sys.stdout: + try: + fp = sys.stdout.buffer + except AttributeError: + pass + if not filename and hasattr(fp, "name") and is_path(fp.name): # only set the name for metadata purposes filename = fp.name # may mutate self! - self.load() + self._ensure_mutable() - save_all = False - if 'save_all' in params: - save_all = params.pop('save_all') + save_all = params.pop("save_all", False) self.encoderinfo = params self.encoderconfig = () @@ -1911,8 +2329,8 @@ def save(self, fp, format=None, **params): init() try: format = EXTENSION[ext] - except KeyError: - raise ValueError('unknown file extension: {}'.format(ext)) + except KeyError as e: + raise ValueError(f"unknown file extension: {ext}") from e if format.upper() not in SAVE: init() @@ -1921,30 +2339,42 @@ def save(self, fp, format=None, **params): else: save_handler = SAVE[format.upper()] + created = False if open_fp: - # Open also for reading ("+"), because TIFF save_all - # writer needs to go back and edit the written data. - fp = builtins.open(filename, "w+b") + created = not os.path.exists(filename) + if params.get("append", False): + # Open also for reading ("+"), because TIFF save_all + # writer needs to go back and edit the written data. + fp = builtins.open(filename, "r+b") + else: + fp = builtins.open(filename, "w+b") try: save_handler(self, fp, filename) - finally: - # do what we can to clean up + except Exception: if open_fp: fp.close() + if created: + try: + os.remove(filename) + except PermissionError: + pass + raise + if open_fp: + fp.close() def seek(self, frame): """ Seeks to the given frame in this sequence file. If you seek beyond the end of the sequence, the method raises an - **EOFError** exception. When a sequence file is opened, the + ``EOFError`` exception. When a sequence file is opened, the library automatically seeks to frame 0. - Note that in the current version of the library, most sequence - formats only allows you to seek to the next frame. - See :py:meth:`~PIL.Image.Image.tell`. + If defined, :attr:`~PIL.Image.Image.n_frames` refers to the + number of available frames. + :param frame: Frame number, starting at 0. :exception EOFError: If the call attempts to seek beyond the end of the sequence. @@ -1954,27 +2384,27 @@ def seek(self, frame): if frame != 0: raise EOFError - def show(self, title=None, command=None): + def show(self, title=None): """ - Displays this image. This method is mainly intended for - debugging purposes. + Displays this image. This method is mainly intended for debugging purposes. + + This method calls :py:func:`PIL.ImageShow.show` internally. You can use + :py:func:`PIL.ImageShow.register` to override its default behaviour. - On Unix platforms, this method saves the image to a temporary - PPM file, and calls either the **xv** utility or the **display** - utility, depending on which one can be found. + The image is first saved to a temporary file. By default, it will be in + PNG format. - On macOS, this method saves the image to a temporary BMP file, and - opens it with the native Preview application. + On Unix, the image is then opened using the **display**, **eog** or + **xv** utility, depending on which one can be found. - On Windows, it saves the image to a temporary BMP file, and uses - the standard BMP display utility to show it (usually Paint). + On macOS, the image is opened with the native Preview application. - :param title: Optional title to use for the image window, - where possible. - :param command: command used to show the image + On Windows, the image is opened with the standard PNG display utility. + + :param title: Optional title to use for the image window, where possible. """ - _show(self, title=title, command=command) + _show(self, title=title) def split(self): """ @@ -2010,12 +2440,11 @@ def getchannel(self, channel): """ self.load() - if isStringType(channel): + if isinstance(channel, str): try: channel = self.getbands().index(channel) - except ValueError: - raise ValueError( - 'The image has no channel "{}"'.format(channel)) + except ValueError as e: + raise ValueError(f'The image has no channel "{channel}"') from e return self._new(self.im.getband(channel)) @@ -2023,11 +2452,14 @@ def tell(self): """ Returns the current frame number. See :py:meth:`~PIL.Image.Image.seek`. + If defined, :attr:`~PIL.Image.Image.n_frames` refers to the + number of available frames. + :returns: Frame number, starting with 0. """ return 0 - def thumbnail(self, size, resample=BICUBIC): + def thumbnail(self, size, resample=Resampling.BICUBIC, reducing_gap=2.0): """ Make this image into a thumbnail. This method modifies the image to contain a thumbnail version of itself, no larger than @@ -2043,41 +2475,86 @@ def thumbnail(self, size, resample=BICUBIC): :param size: Requested size. :param resample: Optional resampling filter. This can be one - of :py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BILINEAR`, - :py:attr:`PIL.Image.BICUBIC`, or :py:attr:`PIL.Image.LANCZOS`. - If omitted, it defaults to :py:attr:`PIL.Image.BICUBIC`. - (was :py:attr:`PIL.Image.NEAREST` prior to version 2.5.0) + of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, + :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, + :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. + If omitted, it defaults to :py:data:`Resampling.BICUBIC`. + (was :py:data:`Resampling.NEAREST` prior to version 2.5.0). + See: :ref:`concept-filters`. + :param reducing_gap: Apply optimization by resizing the image + in two steps. First, reducing the image by integer times + using :py:meth:`~PIL.Image.Image.reduce` or + :py:meth:`~PIL.Image.Image.draft` for JPEG images. + Second, resizing using regular resampling. The last step + changes size no less than by ``reducing_gap`` times. + ``reducing_gap`` may be None (no first step is performed) + or should be greater than 1.0. The bigger ``reducing_gap``, + the closer the result to the fair resampling. + The smaller ``reducing_gap``, the faster resizing. + With ``reducing_gap`` greater or equal to 3.0, the result is + indistinguishable from fair resampling in most cases. + The default value is 2.0 (very close to fair resampling + while still being faster in many cases). :returns: None """ - # preserve aspect ratio - x, y = self.size - if x > size[0]: - y = int(max(y * size[0] / x, 1)) - x = int(size[0]) - if y > size[1]: - x = int(max(x * size[1] / y, 1)) - y = int(size[1]) - size = x, y + provided_size = tuple(map(math.floor, size)) - if size == self.size: - return + def preserve_aspect_ratio(): + def round_aspect(number, key): + return max(min(math.floor(number), math.ceil(number), key=key), 1) + + x, y = provided_size + if x >= self.width and y >= self.height: + return + + aspect = self.width / self.height + if x / y >= aspect: + x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y)) + else: + y = round_aspect( + x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n) + ) + return x, y + + box = None + if reducing_gap is not None: + size = preserve_aspect_ratio() + if size is None: + return - self.draft(None, size) + res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap)) + if res is not None: + box = res[1] + if box is None: + self.load() + + # load() may have changed the size of the image + size = preserve_aspect_ratio() + if size is None: + return - im = self.resize(size, resample) + if self.size != size: + im = self.resize(size, resample, box=box, reducing_gap=reducing_gap) - self.im = im.im - self.mode = im.mode - self.size = size + self.im = im.im + self._size = size + self.mode = self.im.mode self.readonly = 0 self.pyaccess = None # FIXME: the different transform methods need further explanation # instead of bloating the method docs, add a separate chapter. - def transform(self, size, method, data=None, resample=NEAREST, - fill=1, fillcolor=None): + def transform( + self, + size, + method, + data=None, + resample=Resampling.NEAREST, + fill=1, + fillcolor=None, + ): """ Transforms this image. This method creates a new image with the given size, and the same mode as the original, and copies data @@ -2085,31 +2562,50 @@ def transform(self, size, method, data=None, resample=NEAREST, :param size: The output size. :param method: The transformation method. This is one of - :py:attr:`PIL.Image.EXTENT` (cut out a rectangular subregion), - :py:attr:`PIL.Image.AFFINE` (affine transform), - :py:attr:`PIL.Image.PERSPECTIVE` (perspective transform), - :py:attr:`PIL.Image.QUAD` (map a quadrilateral to a rectangle), or - :py:attr:`PIL.Image.MESH` (map a number of source quadrilaterals + :py:data:`Transform.EXTENT` (cut out a rectangular subregion), + :py:data:`Transform.AFFINE` (affine transform), + :py:data:`Transform.PERSPECTIVE` (perspective transform), + :py:data:`Transform.QUAD` (map a quadrilateral to a rectangle), or + :py:data:`Transform.MESH` (map a number of source quadrilaterals in one operation). + + It may also be an :py:class:`~PIL.Image.ImageTransformHandler` + object:: + + class Example(Image.ImageTransformHandler): + def transform(self, size, data, resample, fill=1): + # Return result + + It may also be an object with a ``method.getdata`` method + that returns a tuple supplying new ``method`` and ``data`` values:: + + class Example: + def getdata(self): + method = Image.Transform.EXTENT + data = (0, 0, 100, 100) + return method, data :param data: Extra data to the transformation method. :param resample: Optional resampling filter. It can be one of - :py:attr:`PIL.Image.NEAREST` (use nearest neighbour), - :py:attr:`PIL.Image.BILINEAR` (linear interpolation in a 2x2 - environment), or :py:attr:`PIL.Image.BICUBIC` (cubic spline + :py:data:`Resampling.NEAREST` (use nearest neighbour), + :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 + environment), or :py:data:`Resampling.BICUBIC` (cubic spline interpolation in a 4x4 environment). If omitted, or if the image - has mode "1" or "P", it is set to :py:attr:`PIL.Image.NEAREST`. - :param fillcolor: Optional fill color for the area outside the transform - in the output image. + has mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. + See: :ref:`concept-filters`. + :param fill: If ``method`` is an + :py:class:`~PIL.Image.ImageTransformHandler` object, this is one of + the arguments passed to it. Otherwise, it is unused. + :param fillcolor: Optional fill color for the area outside the + transform in the output image. :returns: An :py:class:`~PIL.Image.Image` object. """ - - if self.mode == 'LA': - return self.convert('La').transform( - size, method, data, resample, fill).convert('LA') - if self.mode == 'RGBA': - return self.convert('RGBa').transform( - size, method, data, resample, fill).convert('RGBA') + if self.mode in ("LA", "RGBA") and resample != Resampling.NEAREST: + return ( + self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) + .transform(size, method, data, resample, fill, fillcolor) + .convert(self.mode) + ) if isinstance(method, ImageTransformHandler): return method.transform(size, self, resample=resample, fill=fill) @@ -2122,63 +2618,98 @@ def transform(self, size, method, data=None, resample=NEAREST, raise ValueError("missing method data") im = new(self.mode, size, fillcolor) - if method == MESH: + if self.mode == "P" and self.palette: + im.palette = self.palette.copy() + im.info = self.info.copy() + if method == Transform.MESH: # list of quads for box, quad in data: - im.__transformer(box, self, QUAD, quad, resample, - fillcolor is None) + im.__transformer( + box, self, Transform.QUAD, quad, resample, fillcolor is None + ) else: - im.__transformer((0, 0)+size, self, method, data, - resample, fillcolor is None) + im.__transformer( + (0, 0) + size, self, method, data, resample, fillcolor is None + ) return im - def __transformer(self, box, image, method, data, - resample=NEAREST, fill=1): + def __transformer( + self, box, image, method, data, resample=Resampling.NEAREST, fill=1 + ): w = box[2] - box[0] h = box[3] - box[1] - if method == AFFINE: - data = data[0:6] + if method == Transform.AFFINE: + data = data[:6] - elif method == EXTENT: + elif method == Transform.EXTENT: # convert extent to an affine transform x0, y0, x1, y1 = data - xs = float(x1 - x0) / w - ys = float(y1 - y0) / h - method = AFFINE + xs = (x1 - x0) / w + ys = (y1 - y0) / h + method = Transform.AFFINE data = (xs, 0, x0, 0, ys, y0) - elif method == PERSPECTIVE: - data = data[0:8] + elif method == Transform.PERSPECTIVE: + data = data[:8] - elif method == QUAD: + elif method == Transform.QUAD: # quadrilateral warp. data specifies the four corners # given as NW, SW, SE, and NE. - nw = data[0:2] + nw = data[:2] sw = data[2:4] se = data[4:6] ne = data[6:8] x0, y0 = nw As = 1.0 / w At = 1.0 / h - data = (x0, (ne[0]-x0)*As, (sw[0]-x0)*At, - (se[0]-sw[0]-ne[0]+x0)*As*At, - y0, (ne[1]-y0)*As, (sw[1]-y0)*At, - (se[1]-sw[1]-ne[1]+y0)*As*At) + data = ( + x0, + (ne[0] - x0) * As, + (sw[0] - x0) * At, + (se[0] - sw[0] - ne[0] + x0) * As * At, + y0, + (ne[1] - y0) * As, + (sw[1] - y0) * At, + (se[1] - sw[1] - ne[1] + y0) * As * At, + ) else: raise ValueError("unknown transformation method") - if resample not in (NEAREST, BILINEAR, BICUBIC): - raise ValueError("unknown resampling filter") + if resample not in ( + Resampling.NEAREST, + Resampling.BILINEAR, + Resampling.BICUBIC, + ): + if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS): + message = { + 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})." + + filters = [ + f"{filter[1]} ({filter[0]})" + for filter in ( + (Resampling.NEAREST, "Image.Resampling.NEAREST"), + (Resampling.BILINEAR, "Image.Resampling.BILINEAR"), + (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), + ) + ] + raise ValueError( + message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] + ) image.load() self.load() if image.mode in ("1", "P"): - resample = NEAREST + resample = Resampling.NEAREST self.im.transform2(box, image.im, method, data, resample, fill) @@ -2186,10 +2717,10 @@ def transpose(self, method): """ Transpose image (flip or rotate in 90 degree steps) - :param method: One of :py:attr:`PIL.Image.FLIP_LEFT_RIGHT`, - :py:attr:`PIL.Image.FLIP_TOP_BOTTOM`, :py:attr:`PIL.Image.ROTATE_90`, - :py:attr:`PIL.Image.ROTATE_180`, :py:attr:`PIL.Image.ROTATE_270`, - :py:attr:`PIL.Image.TRANSPOSE` or :py:attr:`PIL.Image.TRANSVERSE`. + :param method: One of :py:data:`Transpose.FLIP_LEFT_RIGHT`, + :py:data:`Transpose.FLIP_TOP_BOTTOM`, :py:data:`Transpose.ROTATE_90`, + :py:data:`Transpose.ROTATE_180`, :py:data:`Transpose.ROTATE_270`, + :py:data:`Transpose.TRANSPOSE` or :py:data:`Transpose.TRANSVERSE`. :returns: Returns a flipped or rotated copy of this image. """ @@ -2208,6 +2739,7 @@ def effect_spread(self, distance): def toqimage(self): """Returns a QImage copy of this image""" from . import ImageQt + if not ImageQt.qt_is_installed: raise ImportError("Qt bindings are not installed") return ImageQt.toqimage(self) @@ -2215,6 +2747,7 @@ def toqimage(self): def toqpixmap(self): """Returns a QPixmap copy of this image""" from . import ImageQt + if not ImageQt.qt_is_installed: raise ImportError("Qt bindings are not installed") return ImageQt.toqpixmap(self) @@ -2223,13 +2756,22 @@ def toqpixmap(self): # -------------------------------------------------------------------- # Abstract handlers. -class ImagePointHandler(object): - # used as a mixin by point transforms (for use with im.point) + +class ImagePointHandler: + """ + Used as a mixin by point transforms + (for use with :py:meth:`~PIL.Image.Image.point`) + """ + pass -class ImageTransformHandler(object): - # used as a mixin by geometry transforms (for use with im.transform) +class ImageTransformHandler: + """ + Used as a mixin by geometry transforms + (for use with :py:meth:`~PIL.Image.Image.transform`) + """ + pass @@ -2239,8 +2781,9 @@ class ImageTransformHandler(object): # # Debugging + def _wedge(): - "Create greyscale wedge (for debugging only)" + """Create greyscale wedge (for debugging only)""" return Image()._new(core.wedge("L")) @@ -2285,13 +2828,21 @@ def new(mode, size, color=0): # don't initialize return Image()._new(core.new(mode, size)) - if isStringType(color): + if isinstance(color, str): # css3-style specifier from . import ImageColor + color = ImageColor.getcolor(color, mode) - return Image()._new(core.fill(mode, size, color)) + im = Image() + if mode == "P" and isinstance(color, (list, tuple)) and len(color) in [3, 4]: + # RGB or RGBA value for a P image + from . import ImagePalette + + im.palette = ImagePalette.ImagePalette() + color = im.palette.getcolor(color) + return im._new(core.fill(mode, size, color)) def frombytes(mode, size, data, decoder_name="raw", *args): @@ -2301,9 +2852,9 @@ def frombytes(mode, size, data, decoder_name="raw", *args): In its simplest form, this function takes three arguments (mode, size, and unpacked pixel data). - You can also use any pixel decoder supported by PIL. For more + You can also use any pixel decoder supported by PIL. For more information on available decoders, see the section - :ref:`Writing Your Own File Decoder `. + :ref:`Writing Your Own File Codec `. Note that this function decodes pixel data only, not entire images. If you have an entire image in a string, wrap it in a @@ -2332,11 +2883,6 @@ def frombytes(mode, size, data, decoder_name="raw", *args): return im -def fromstring(*args, **kw): - raise NotImplementedError("fromstring() has been removed. " + - "Please call frombytes() instead.") - - def frombuffer(mode, size, data, decoder_name="raw", *args): """ Creates an image memory referencing pixel data in a byte buffer. @@ -2348,7 +2894,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): Note that this function decodes pixel data only, not entire images. If you have an entire image file in a string, wrap it in a - **BytesIO** object, and use :py:func:`~PIL.Image.open` to load it. + :py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load it. In the current version, the default parameters used for the "raw" decoder differs from that used for :py:func:`~PIL.Image.frombytes`. This is a @@ -2380,18 +2926,14 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): if decoder_name == "raw": if args == (): - warnings.warn( - "the frombuffer defaults may change in a future release; " - "for portability, change the call to read:\n" - " frombuffer(mode, size, data, 'raw', mode, 0, 1)", - RuntimeWarning, stacklevel=2 - ) - args = mode, 0, -1 # may change to (mode, 0, 1) post-1.1.6 + args = mode, 0, 1 if args[0] in _MAPMODES: im = new(mode, (1, 1)) - im = im._new( - core.map_buffer(data, size, decoder_name, None, 0, args) - ) + im = im._new(core.map_buffer(data, size, decoder_name, 0, args)) + if mode == "P": + from . import ImagePalette + + im.palette = ImagePalette.ImagePalette("RGB", im.im.getpalette("RGB")) im.readonly = 1 return im @@ -2403,27 +2945,53 @@ def fromarray(obj, mode=None): Creates an image memory from an object exporting the array interface (using the buffer protocol). - If obj is not contiguous, then the tobytes method is called + If ``obj`` is not contiguous, then the ``tobytes`` method is called and :py:func:`~PIL.Image.frombuffer` is used. + If you have an image in NumPy:: + + from PIL import Image + import numpy as np + im = Image.open("hopper.jpg") + a = np.asarray(im) + + Then this can be used to convert it to a Pillow image:: + + im = Image.fromarray(a) + :param obj: Object with array interface - :param mode: Mode to use (will be determined from type if None) - See: :ref:`concept-modes`. + :param mode: Optional mode to use when reading ``obj``. Will be determined from + type if ``None``. + + This will not be used to convert the data after reading, but will be used to + change how the data is read:: + + from PIL import Image + import numpy as np + a = np.full((1, 1), 300) + im = Image.fromarray(a, mode="L") + im.getpixel((0, 0)) # 44 + im = Image.fromarray(a, mode="RGB") + im.getpixel((0, 0)) # (44, 1, 0) + + See: :ref:`concept-modes` for general information about modes. :returns: An image object. .. versionadded:: 1.1.6 """ arr = obj.__array_interface__ - shape = arr['shape'] + shape = arr["shape"] ndim = len(shape) - strides = arr.get('strides', None) + strides = arr.get("strides", None) if mode is None: try: - typekey = (1, 1) + shape[2:], arr['typestr'] + typekey = (1, 1) + shape[2:], arr["typestr"] + except KeyError as e: + raise TypeError("Cannot handle this data type") from e + try: mode, rawmode = _fromarray_typemap[typekey] - except KeyError: - # print(typekey) - raise TypeError("Cannot handle this data type") + except KeyError as e: + raise TypeError("Cannot handle this data type: %s, %s" % typekey) from e else: rawmode = mode if mode in ["1", "L", "I", "P", "F"]: @@ -2433,11 +3001,11 @@ def fromarray(obj, mode=None): else: ndmax = 4 if ndim > ndmax: - raise ValueError("Too many dimensions: %d > %d." % (ndim, ndmax)) + raise ValueError(f"Too many dimensions: {ndim} > {ndmax}.") - size = shape[1], shape[0] + size = 1 if ndim == 1 else shape[1], shape[0] if strides is not None: - if hasattr(obj, 'tobytes'): + if hasattr(obj, "tobytes"): obj = obj.tobytes() else: obj = obj.tostring() @@ -2448,6 +3016,7 @@ def fromarray(obj, mode=None): def fromqimage(im): """Creates an image instance from a QImage image""" from . import ImageQt + if not ImageQt.qt_is_installed: raise ImportError("Qt bindings are not installed") return ImageQt.fromqimage(im) @@ -2456,6 +3025,7 @@ def fromqimage(im): def fromqpixmap(im): """Creates an image instance from a QPixmap image""" from . import ImageQt + if not ImageQt.qt_is_installed: raise ImportError("Qt bindings are not installed") return ImageQt.fromqpixmap(im) @@ -2482,11 +3052,10 @@ def fromqpixmap(im): ((1, 1, 2), "|u1"): ("LA", "LA"), ((1, 1, 3), "|u1"): ("RGB", "RGB"), ((1, 1, 4), "|u1"): ("RGBA", "RGBA"), - } - -# shortcuts -_fromarray_typemap[((1, 1), _ENDIAN + "i4")] = ("I", "I") -_fromarray_typemap[((1, 1), _ENDIAN + "f4")] = ("F", "F") + # shortcuts: + ((1, 1), _ENDIAN + "i4"): ("I", "I"), + ((1, 1), _ENDIAN + "f4"): ("F", "F"), +} def _decompression_bomb_check(size): @@ -2497,19 +3066,19 @@ def _decompression_bomb_check(size): if pixels > 2 * MAX_IMAGE_PIXELS: raise DecompressionBombError( - "Image size (%d pixels) exceeds limit of %d pixels, " - "could be decompression bomb DOS attack." % - (pixels, 2* MAX_IMAGE_PIXELS)) + f"Image size ({pixels} pixels) exceeds limit of {2 * MAX_IMAGE_PIXELS} " + "pixels, could be decompression bomb DOS attack." + ) if pixels > MAX_IMAGE_PIXELS: warnings.warn( - "Image size (%d pixels) exceeds limit of %d pixels, " - "could be decompression bomb DOS attack." % - (pixels, MAX_IMAGE_PIXELS), - DecompressionBombWarning) + f"Image size ({pixels} pixels) exceeds limit of {MAX_IMAGE_PIXELS} pixels, " + "could be decompression bomb DOS attack.", + DecompressionBombWarning, + ) -def open(fp, mode="r"): +def open(fp, mode="r", formats=None): """ Opens and identifies the given image file. @@ -2517,27 +3086,46 @@ def open(fp, mode="r"): the file remains open and the actual image data is not read from the file until you try to process the data (or call the :py:meth:`~PIL.Image.Image.load` method). See - :py:func:`~PIL.Image.new`. + :py:func:`~PIL.Image.new`. See :ref:`file-handling`. :param fp: A filename (string), pathlib.Path object or a file object. - The file object must implement :py:meth:`~file.read`, - :py:meth:`~file.seek`, and :py:meth:`~file.tell` methods, + The file object must implement ``file.read``, + ``file.seek``, and ``file.tell`` methods, and be opened in binary mode. :param mode: The mode. If given, this argument must be "r". + :param formats: A list or tuple of formats to attempt to load the file in. + This can be used to restrict the set of formats checked. + Pass ``None`` to try all supported formats. You can print the set of + available formats by running ``python3 -m PIL`` or using + the :py:func:`PIL.features.pilinfo` function. :returns: An :py:class:`~PIL.Image.Image` object. - :exception IOError: If the file cannot be found, or the image cannot be - opened and identified. + :exception FileNotFoundError: If the file cannot be found. + :exception PIL.UnidentifiedImageError: If the image cannot be opened and + identified. + :exception ValueError: If the ``mode`` is not "r", or if a ``StringIO`` + instance is used for ``fp``. + :exception TypeError: If ``formats`` is not ``None``, a list or a tuple. """ if mode != "r": - raise ValueError("bad mode %r" % mode) + raise ValueError(f"bad mode {repr(mode)}") + elif isinstance(fp, io.StringIO): + raise ValueError( + "StringIO cannot be used to open an image. " + "Binary data must be used instead." + ) + + if formats is None: + formats = ID + elif not isinstance(formats, (list, tuple)): + raise TypeError("formats must be a list or tuple") exclusive_fp = False filename = "" - if isPath(fp): - filename = fp - elif HAS_PATHLIB and isinstance(fp, Path): + if isinstance(fp, Path): filename = str(fp.resolve()) + elif is_path(fp): + filename = fp if filename: fp = builtins.open(filename, "rb") @@ -2553,11 +3141,19 @@ def open(fp, mode="r"): preinit() - def _open_core(fp, filename, prefix): - for i in ID: + accept_warnings = [] + + def _open_core(fp, filename, prefix, formats): + for i in formats: + i = i.upper() + if i not in OPEN: + init() try: factory, accept = OPEN[i] - if not accept or accept(prefix): + result = not accept or accept(prefix) + if type(result) in [str, bytes]: + accept_warnings.append(result) + elif result: fp.seek(0) im = factory(fp, filename) _decompression_bomb_check(im.size) @@ -2567,13 +3163,17 @@ def _open_core(fp, filename, prefix): # opening failures that are entirely expected. # logger.debug("", exc_info=True) continue + except BaseException: + if exclusive_fp: + fp.close() + raise return None - im = _open_core(fp, filename, prefix) + im = _open_core(fp, filename, prefix, formats) if im is None: if init(): - im = _open_core(fp, filename, prefix) + im = _open_core(fp, filename, prefix, formats) if im: im._exclusive_fp = exclusive_fp @@ -2581,8 +3181,12 @@ def _open_core(fp, filename, prefix): if exclusive_fp: fp.close() - raise IOError("cannot identify image file %r" - % (filename if filename else fp)) + for message in accept_warnings: + warnings.warn(message) + raise UnidentifiedImageError( + "cannot identify image file %r" % (filename if filename else fp) + ) + # # Image processing. @@ -2606,7 +3210,7 @@ def alpha_composite(im1, im2): def blend(im1, im2, alpha): """ Creates a new image by interpolating between two input images, using - a constant alpha.:: + a constant alpha:: out = image1 * (1.0 - alpha) + image2 * alpha @@ -2686,6 +3290,7 @@ def merge(mode, bands): # -------------------------------------------------------------------- # Plugin registry + def register_open(id, factory, accept=None): """ Register an image file plugin. This function should not be used @@ -2745,6 +3350,7 @@ def register_extension(id, extension): """ EXTENSION[extension.lower()] = id.upper() + def register_extensions(id, extensions): """ Registers image extensions. This function should not be @@ -2756,6 +3362,7 @@ def register_extensions(id, extensions): for extension in extensions: register_extension(id, extension) + def registered_extensions(): """ Returns a dictionary containing all file extensions belonging @@ -2795,21 +3402,19 @@ def register_encoder(name, encoder): # -------------------------------------------------------------------- -# Simple display support. User code may override this. - -def _show(image, **options): - # override me, as necessary - _showxv(image, **options) +# Simple display support. -def _showxv(image, title=None, **options): +def _show(image, **options): from . import ImageShow - ImageShow.show(image, title, **options) + + ImageShow.show(image, **options) # -------------------------------------------------------------------- # Effects + def effect_mandelbrot(size, extent, quality): """ Generate a Mandelbrot set covering the given extent. @@ -2817,7 +3422,7 @@ def effect_mandelbrot(size, extent, quality): :param size: The requested size in pixels, as a 2-tuple: (width, height). :param extent: The extent to cover, as a 4-tuple: - (x0, y0, x1, y2). + (x0, y0, x1, y1). :param quality: Quality. """ return Image()._new(core.effect_mandelbrot(size, extent, quality)) @@ -2855,14 +3460,15 @@ def radial_gradient(mode): # -------------------------------------------------------------------- # Resources + def _apply_env_variables(env=None): if env is None: env = os.environ for var_name, setter in [ - ('PILLOW_ALIGNMENT', core.set_alignment), - ('PILLOW_BLOCK_SIZE', core.set_block_size), - ('PILLOW_BLOCKS_MAX', core.set_blocks_max), + ("PILLOW_ALIGNMENT", core.set_alignment), + ("PILLOW_BLOCK_SIZE", core.set_block_size), + ("PILLOW_BLOCKS_MAX", core.set_blocks_max), ]: if var_name not in env: continue @@ -2870,21 +3476,285 @@ def _apply_env_variables(env=None): var = env[var_name].lower() units = 1 - for postfix, mul in [('k', 1024), ('m', 1024*1024)]: + for postfix, mul in [("k", 1024), ("m", 1024 * 1024)]: if var.endswith(postfix): units = mul - var = var[:-len(postfix)] + var = var[: -len(postfix)] try: var = int(var) * units except ValueError: - warnings.warn("{0} is not int".format(var_name)) + warnings.warn(f"{var_name} is not int") continue try: setter(var) except ValueError as e: - warnings.warn("{0}: {1}".format(var_name, e)) + warnings.warn(f"{var_name}: {e}") + _apply_env_variables() atexit.register(core.clear_cache) + + +class Exif(MutableMapping): + endian = None + bigtiff = False + + def __init__(self): + self._data = {} + self._ifds = {} + self._info = None + self._loaded_exif = None + + def _fixup(self, value): + try: + if len(value) == 1 and isinstance(value, tuple): + return value[0] + except Exception: + pass + return value + + def _fixup_dict(self, src_dict): + # Helper function + # returns a dict with any single item tuples/lists as individual values + return {k: self._fixup(v) for k, v in src_dict.items()} + + def _get_ifd_dict(self, offset): + try: + # an offset pointer to the location of the nested embedded IFD. + # It should be a long, but may be corrupted. + self.fp.seek(offset) + except (KeyError, TypeError): + pass + else: + from . import TiffImagePlugin + + info = TiffImagePlugin.ImageFileDirectory_v2(self.head) + info.load(self.fp) + return self._fixup_dict(info) + + def _get_head(self): + version = b"\x2B" if self.bigtiff else b"\x2A" + if self.endian == "<": + head = b"II" + version + b"\x00" + o32le(8) + else: + head = b"MM\x00" + version + o32be(8) + if self.bigtiff: + head += o32le(8) if self.endian == "<" else o32be(8) + head += b"\x00\x00\x00\x00" + return head + + def load(self, data): + # Extract EXIF information. This is highly experimental, + # and is likely to be replaced with something better in a future + # version. + + # The EXIF record consists of a TIFF file embedded in a JPEG + # application marker (!). + if data == self._loaded_exif: + return + self._loaded_exif = data + self._data.clear() + self._ifds.clear() + if data and data.startswith(b"Exif\x00\x00"): + data = data[6:] + if not data: + self._info = None + return + + self.fp = io.BytesIO(data) + self.head = self.fp.read(8) + # process dictionary + from . import TiffImagePlugin + + self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) + self.endian = self._info._endian + self.fp.seek(self._info.next) + self._info.load(self.fp) + + def load_from_fp(self, fp, offset=None): + self._loaded_exif = None + self._data.clear() + self._ifds.clear() + + # process dictionary + from . import TiffImagePlugin + + self.fp = fp + if offset is not None: + self.head = self._get_head() + else: + self.head = self.fp.read(8) + self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) + if self.endian is None: + self.endian = self._info._endian + if offset is None: + offset = self._info.next + self.fp.seek(offset) + self._info.load(self.fp) + + def _get_merged_dict(self): + merged_dict = dict(self) + + # get EXIF extension + if 0x8769 in self: + ifd = self._get_ifd_dict(self[0x8769]) + if ifd: + merged_dict.update(ifd) + + # GPS + if 0x8825 in self: + merged_dict[0x8825] = self._get_ifd_dict(self[0x8825]) + + return merged_dict + + def tobytes(self, offset=8): + from . import TiffImagePlugin + + 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): + value = self.get_ifd(tag) + if ( + tag == 0x8769 + and 0xA005 in value + and not isinstance(value[0xA005], dict) + ): + value = value.copy() + value[0xA005] = self.get_ifd(0xA005) + 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 + from .TiffImagePlugin import ImageFileDirectory_v2 + + if tag_data[:8] == b"FUJIFILM": + ifd_offset = i32le(tag_data, 8) + ifd_data = tag_data[ifd_offset:] + + makernote = {} + for i in range(0, struct.unpack(" 4: + (offset,) = struct.unpack("H", tag_data[:2])[0]): + ifd_tag, typ, count, data = struct.unpack( + ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2] + ) + if ifd_tag == 0x1101: + # CameraInfo + (offset,) = struct.unpack(">L", data) + self.fp.seek(offset) + + camerainfo = {"ModelID": self.fp.read(4)} + + self.fp.read(4) + # Seconds since 2000 + camerainfo["TimeStamp"] = i32le(self.fp.read(12)) + + self.fp.read(4) + camerainfo["InternalSerialNumber"] = self.fp.read(4) + + self.fp.read(12) + parallax = self.fp.read(4) + handler = ImageFileDirectory_v2._load_dispatch[ + TiffTags.FLOAT + ][1] + camerainfo["Parallax"] = handler( + ImageFileDirectory_v2(), parallax, False + ) + + self.fp.read(4) + camerainfo["Category"] = self.fp.read(2) + + makernote = {0x1101: dict(self._fixup_dict(camerainfo))} + self._ifds[tag] = makernote + else: + # interop + self._ifds[tag] = self._get_ifd_dict(tag_data) + return self._ifds.get(tag, {}) + + def __str__(self): + if self._info is not None: + # Load all keys into self._data + for tag in self._info.keys(): + self[tag] + + return str(self._data) + + def __len__(self): + keys = set(self._data) + if self._info is not None: + keys.update(self._info) + return len(keys) + + def __getitem__(self, tag): + if self._info is not None and tag not in self._data and tag in self._info: + self._data[tag] = self._fixup(self._info[tag]) + del self._info[tag] + return self._data[tag] + + def __contains__(self, tag): + return tag in self._data or (self._info is not None and tag in self._info) + + def __setitem__(self, tag, value): + if self._info is not None and tag in self._info: + del self._info[tag] + self._data[tag] = value + + def __delitem__(self, tag): + if self._info is not None and tag in self._info: + del self._info[tag] + else: + del self._data[tag] + + def __iter__(self): + keys = set(self._data) + if self._info is not None: + keys.update(self._info) + return iter(keys) diff --git a/src/PIL/ImageChops.py b/src/PIL/ImageChops.py index 89016730e36..fec4694b290 100644 --- a/src/PIL/ImageChops.py +++ b/src/PIL/ImageChops.py @@ -70,8 +70,8 @@ def lighter(image1, image2): def darker(image1, image2): """ - Compares the two images, pixel by pixel, and returns a new image - containing the darker values. + Compares the two images, pixel by pixel, and returns a new image containing + the darker values. .. code-block:: python @@ -137,6 +137,42 @@ def screen(image1, image2): return image1._new(image1.im.chop_screen(image2.im)) +def soft_light(image1, image2): + """ + Superimposes two images on top of each other using the Soft Light algorithm + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_soft_light(image2.im)) + + +def hard_light(image1, image2): + """ + Superimposes two images on top of each other using the Hard Light algorithm + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_hard_light(image2.im)) + + +def overlay(image1, image2): + """ + Superimposes two images on top of each other using the Overlay algorithm + + :rtype: :py:class:`~PIL.Image.Image` + """ + + image1.load() + image2.load() + return image1._new(image1.im.chop_overlay(image2.im)) + + def add(image1, image2, scale=1.0, offset=0): """ Adds two images, dividing the result by scale and adding the @@ -156,8 +192,8 @@ def add(image1, image2, scale=1.0, offset=0): def subtract(image1, image2, scale=1.0, offset=0): """ - Subtracts two images, dividing the result by scale and adding the - offset. If omitted, scale defaults to 1.0, and offset to 0.0. + Subtracts two images, dividing the result by scale and adding the offset. + If omitted, scale defaults to 1.0, and offset to 0.0. .. code-block:: python @@ -204,6 +240,11 @@ def subtract_modulo(image1, image2): def logical_and(image1, image2): """Logical AND between two images. + Both of the images must have mode "1". If you would like to perform a + logical AND on an image with a mode other than "1", try + :py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask + as the second image. + .. code-block:: python out = ((image1 and image2) % MAX) @@ -219,6 +260,8 @@ def logical_and(image1, image2): def logical_or(image1, image2): """Logical OR between two images. + Both of the images must have mode "1". + .. code-block:: python out = ((image1 or image2) % MAX) @@ -234,6 +277,8 @@ def logical_or(image1, image2): def logical_xor(image1, image2): """Logical XOR between two images. + Both of the images must have mode "1". + .. code-block:: python out = ((bool(image1) != bool(image2)) % MAX) @@ -248,7 +293,7 @@ def logical_xor(image1, image2): def blend(image1, image2, alpha): """Blend images using constant transparency weight. Alias for - :py:meth:`PIL.Image.Image.blend`. + :py:func:`PIL.Image.blend`. :rtype: :py:class:`~PIL.Image.Image` """ @@ -258,7 +303,7 @@ def blend(image1, image2, alpha): def composite(image1, image2, mask): """Create composite using transparency mask. Alias for - :py:meth:`PIL.Image.Image.composite`. + :py:func:`PIL.Image.composite`. :rtype: :py:class:`~PIL.Image.Image` """ @@ -268,9 +313,10 @@ def composite(image1, image2, mask): def offset(image, xoffset, yoffset=None): """Returns a copy of the image where data has been offset by the given - distances. Data wraps around the edges. If **yoffset** is omitted, it - is assumed to be equal to **xoffset**. + distances. Data wraps around the edges. If ``yoffset`` is omitted, it + is assumed to be equal to ``xoffset``. + :param image: Input image. :param xoffset: The horizontal distance. :param yoffset: The vertical distance. If omitted, both distances are set to the same value. diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 40fb1ad0a68..605252d5d4c 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -15,18 +15,21 @@ # See the README file for information on usage and redistribution. See # below for the original description. -from __future__ import print_function import sys +from enum import IntEnum from PIL import Image + +from ._deprecate import deprecate + try: from PIL import _imagingcms except ImportError as ex: # Allow error import for doc purposes, but error out when accessing # anything in core. - from _util import deferred_error - _imagingcms = deferred_error(ex) -from PIL._util import isStringType + from ._util import DeferredError + + _imagingcms = DeferredError(ex) DESCRIPTION = """ pyCMS @@ -34,10 +37,10 @@ a Python / PIL interface to the littleCMS ICC Color Management System Copyright (C) 2002-2003 Kevin Cazabon kevin@cazabon.com - http://www.cazabon.com + https://www.cazabon.com - pyCMS home page: http://www.cazabon.com/pyCMS - littleCMS home page: http://www.littlecms.com + pyCMS home page: https://www.cazabon.com/pyCMS + littleCMS home page: https://www.littlecms.com (littleCMS is Copyright (C) 1998-2001 Marti Maria) Originally released under LGPL. Graciously donated to PIL in @@ -100,14 +103,29 @@ # # intent/direction values -INTENT_PERCEPTUAL = 0 -INTENT_RELATIVE_COLORIMETRIC = 1 -INTENT_SATURATION = 2 -INTENT_ABSOLUTE_COLORIMETRIC = 3 -DIRECTION_INPUT = 0 -DIRECTION_OUTPUT = 1 -DIRECTION_PROOF = 2 +class Intent(IntEnum): + PERCEPTUAL = 0 + RELATIVE_COLORIMETRIC = 1 + SATURATION = 2 + ABSOLUTE_COLORIMETRIC = 3 + + +class Direction(IntEnum): + INPUT = 0 + OUTPUT = 1 + PROOF = 2 + + +def __getattr__(name): + for enum, prefix in {Intent: "INTENT_", Direction: "DIRECTION_"}.items(): + if name.startswith(prefix): + name = name[len(prefix) :] + 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}'") + # # flags @@ -132,7 +150,7 @@ "SOFTPROOFING": 16384, # Do softproofing "PRESERVEBLACK": 32768, # Black preservation "NODEFAULTRESOURCEDEF": 16777216, # CRD special - "GRIDPOINTS": lambda n: ((n) & 0xFF) << 16 # Gridpoints + "GRIDPOINTS": lambda n: (n & 0xFF) << 16, # Gridpoints } _MAX_FLAG = 0 @@ -148,8 +166,8 @@ ## # Profile. -class ImageCmsProfile(object): +class ImageCmsProfile: def __init__(self, profile): """ :param profile: Either a string representing a filename, @@ -158,7 +176,15 @@ def __init__(self, profile): """ - if isStringType(profile): + if isinstance(profile, str): + if sys.platform == "win32": + profile_bytes_path = profile.encode() + try: + profile_bytes_path.decode("ascii") + except UnicodeDecodeError: + with open(profile, "rb") as f: + self._set(core.profile_frombytes(f.read())) + return self._set(core.profile_open(profile), profile) elif hasattr(profile, "read"): self._set(core.profile_frombytes(profile.read())) @@ -192,27 +218,36 @@ class ImageCmsTransform(Image.ImagePointHandler): """ Transform. This can be used with the procedural API, or with the standard - Image.point() method. - - Will return the output profile in the output.info['icc_profile']. - """ - - def __init__(self, input, output, input_mode, output_mode, - intent=INTENT_PERCEPTUAL, proof=None, - proof_intent=INTENT_ABSOLUTE_COLORIMETRIC, flags=0): + :py:func:`~PIL.Image.Image.point` method. + + Will return the output profile in the ``output.info['icc_profile']``. + """ + + def __init__( + self, + input, + output, + input_mode, + output_mode, + intent=Intent.PERCEPTUAL, + proof=None, + proof_intent=Intent.ABSOLUTE_COLORIMETRIC, + flags=0, + ): if proof is None: self.transform = core.buildTransform( - input.profile, output.profile, - input_mode, output_mode, - intent, - flags + input.profile, output.profile, input_mode, output_mode, intent, flags ) else: self.transform = core.buildProofTransform( - input.profile, output.profile, proof.profile, - input_mode, output_mode, - intent, proof_intent, - flags + input.profile, + output.profile, + proof.profile, + input_mode, + output_mode, + intent, + proof_intent, + flags, ) # Note: inputMode and outputMode are for pyCMS compatibility only self.input_mode = self.inputMode = input_mode @@ -228,7 +263,7 @@ def apply(self, im, imOut=None): if imOut is None: imOut = Image.new(self.output_mode, im.size, None) self.transform.apply(im.im.id, imOut.im.id) - imOut.info['icc_profile'] = self.output_profile.tobytes() + imOut.info["icc_profile"] = self.output_profile.tobytes() return imOut def apply_in_place(self, im): @@ -236,28 +271,28 @@ def apply_in_place(self, im): if im.mode != self.output_mode: raise ValueError("mode mismatch") # wrong output mode self.transform.apply(im.im.id, im.im.id) - im.info['icc_profile'] = self.output_profile.tobytes() + im.info["icc_profile"] = self.output_profile.tobytes() return im def get_display_profile(handle=None): - """ (experimental) Fetches the profile for the current display device. - :returns: None if the profile is not known. """ + (experimental) Fetches the profile for the current display device. - if sys.platform == "win32": - from PIL import ImageWin - if isinstance(handle, ImageWin.HDC): - profile = core.get_display_profile_win32(handle, 1) - else: - profile = core.get_display_profile_win32(handle or 0) + :returns: ``None`` if the profile is not known. + """ + + if sys.platform != "win32": + return None + + from PIL import ImageWin + + if isinstance(handle, ImageWin.HDC): + profile = core.get_display_profile_win32(handle, 1) else: - try: - get = _imagingcms.get_display_profile - except AttributeError: - return None - else: - profile = get() + profile = core.get_display_profile_win32(handle or 0) + if profile is None: + return None return ImageCmsProfile(profile) @@ -265,39 +300,48 @@ def get_display_profile(handle=None): # pyCMS compatible layer # --------------------------------------------------------------------. + class PyCMSError(Exception): - """ (pyCMS) Exception class. - This is used for all errors in the pyCMS API. """ + """(pyCMS) Exception class. + This is used for all errors in the pyCMS API.""" + pass def profileToProfile( - im, inputProfile, outputProfile, renderingIntent=INTENT_PERCEPTUAL, - outputMode=None, inPlace=0, flags=0): + im, + inputProfile, + outputProfile, + renderingIntent=Intent.PERCEPTUAL, + outputMode=None, + inPlace=False, + flags=0, +): """ (pyCMS) Applies an ICC transformation to a given image, mapping from - inputProfile to outputProfile. + ``inputProfile`` to ``outputProfile``. If the input or output profiles specified are not valid filenames, a - PyCMSError will be raised. If inPlace == TRUE and outputMode != im.mode, - a PyCMSError will be raised. If an error occurs during application of - the profiles, a PyCMSError will be raised. If outputMode is not a mode - supported by the outputProfile (or by pyCMS), a PyCMSError will be - raised. - - This function applies an ICC transformation to im from inputProfile's - color space to outputProfile's color space using the specified rendering + :exc:`PyCMSError` will be raised. If ``inPlace`` is ``True`` and + ``outputMode != im.mode``, a :exc:`PyCMSError` will be raised. + If an error occurs during application of the profiles, + a :exc:`PyCMSError` will be raised. + If ``outputMode`` is not a mode supported by the ``outputProfile`` (or by pyCMS), + a :exc:`PyCMSError` will be raised. + + This function applies an ICC transformation to im from ``inputProfile``'s + color space to ``outputProfile``'s color space using the specified rendering intent to decide how to handle out-of-gamut colors. - OutputMode can be used to specify that a color mode conversion is to + ``outputMode`` can be used to specify that a color mode conversion is to be done using these profiles, but the specified profiles must be able to handle that mode. I.e., if converting im from RGB to CMYK using profiles, the input profile must handle RGB data, and the output profile must handle CMYK data. - :param im: An open PIL image object (i.e. Image.new(...) or - Image.open(...), etc.) + :param im: An open :py:class:`~PIL.Image.Image` object (i.e. Image.new(...) + or Image.open(...), etc.) :param inputProfile: String, as a valid filename path to the ICC input profile you wish to use for this image, or a profile object :param outputProfile: String, as a valid filename path to the ICC output @@ -305,10 +349,10 @@ def profileToProfile( :param renderingIntent: Integer (0-3) specifying the rendering intent you wish to use for the transform - INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL) - INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC) - INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION) - INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC) + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 see the pyCMS documentation for details on rendering intents and what they do. @@ -317,12 +361,12 @@ def profileToProfile( MUST be the same mode as the input, or omitted completely. If omitted, the outputMode will be the same as the mode of the input image (im.mode) - :param inPlace: Boolean (1 = True, None or 0 = False). If True, the - original image is modified in-place, and None is returned. If False - (default), a new Image object is returned with the transform applied. + :param inPlace: Boolean. If ``True``, the original image is modified in-place, + and ``None`` is returned. If ``False`` (default), a new + :py:class:`~PIL.Image.Image` object is returned with the transform applied. :param flags: Integer (0-...) specifying additional flags - :returns: Either None or a new PIL image object, depending on value of - inPlace + :returns: Either None or a new :py:class:`~PIL.Image.Image` object, depending on + the value of ``inPlace`` :exception PyCMSError: """ @@ -333,8 +377,7 @@ def profileToProfile( raise PyCMSError("renderingIntent must be an integer between 0 and 3") if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError( - "flags must be an integer between 0 and %s" + _MAX_FLAG) + raise PyCMSError(f"flags must be an integer between 0 and {_MAX_FLAG}") try: if not isinstance(inputProfile, ImageCmsProfile): @@ -342,16 +385,20 @@ def profileToProfile( if not isinstance(outputProfile, ImageCmsProfile): outputProfile = ImageCmsProfile(outputProfile) transform = ImageCmsTransform( - inputProfile, outputProfile, im.mode, outputMode, - renderingIntent, flags=flags + inputProfile, + outputProfile, + im.mode, + outputMode, + renderingIntent, + flags=flags, ) if inPlace: transform.apply_in_place(im) imOut = None else: imOut = transform.apply(im) - except (IOError, TypeError, ValueError) as v: - raise PyCMSError(v) + except (OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v return imOut @@ -363,8 +410,8 @@ def getOpenProfile(profileFilename): The PyCMSProfile object can be passed back into pyCMS for use in creating transforms and such (as in ImageCms.buildTransformFromOpenProfiles()). - If profileFilename is not a vaild filename for an ICC profile, a PyCMSError - will be raised. + If ``profileFilename`` is not a valid filename for an ICC profile, + a :exc:`PyCMSError` will be raised. :param profileFilename: String, as a valid filename path to the ICC profile you wish to open, or a file-like object. @@ -374,29 +421,34 @@ def getOpenProfile(profileFilename): try: return ImageCmsProfile(profileFilename) - except (IOError, TypeError, ValueError) as v: - raise PyCMSError(v) + except (OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v def buildTransform( - inputProfile, outputProfile, inMode, outMode, - renderingIntent=INTENT_PERCEPTUAL, flags=0): - """ - (pyCMS) Builds an ICC transform mapping from the inputProfile to the - outputProfile. Use applyTransform to apply the transform to a given + inputProfile, + outputProfile, + inMode, + outMode, + renderingIntent=Intent.PERCEPTUAL, + flags=0, +): + """ + (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the + ``outputProfile``. Use applyTransform to apply the transform to a given image. If the input or output profiles specified are not valid filenames, a - PyCMSError will be raised. If an error occurs during creation of the - transform, a PyCMSError will be raised. + :exc:`PyCMSError` will be raised. If an error occurs during creation + of the transform, a :exc:`PyCMSError` will be raised. - If inMode or outMode are not a mode supported by the outputProfile (or - by pyCMS), a PyCMSError will be raised. + If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile`` + (or by pyCMS), a :exc:`PyCMSError` will be raised. - This function builds and returns an ICC transform from the inputProfile - to the outputProfile using the renderingIntent to determine what to do + This function builds and returns an ICC transform from the ``inputProfile`` + to the ``outputProfile`` using the ``renderingIntent`` to determine what to do with out-of-gamut colors. It will ONLY work for converting images that - are in inMode to images that are in outMode color format (PIL mode, + are in ``inMode`` to images that are in ``outMode`` color format (PIL mode, i.e. "RGB", "RGBA", "CMYK", etc.). Building the transform is a fair part of the overhead in @@ -409,7 +461,7 @@ def buildTransform( The reason pyCMS returns a class object rather than a handle directly to the transform is that it needs to keep track of the PIL input/output modes that the transform is meant for. These attributes are stored in - the "inMode" and "outMode" attributes of the object (which can be + the ``inMode`` and ``outMode`` attributes of the object (which can be manually overridden if you really want to, but I don't know of any time that would be of use, or would even work). @@ -424,10 +476,10 @@ def buildTransform( :param renderingIntent: Integer (0-3) specifying the rendering intent you wish to use for the transform - INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL) - INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC) - INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION) - INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC) + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 see the pyCMS documentation for details on rendering intents and what they do. @@ -440,8 +492,7 @@ def buildTransform( raise PyCMSError("renderingIntent must be an integer between 0 and 3") if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError( - "flags must be an integer between 0 and %s" + _MAX_FLAG) + raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) try: if not isinstance(inputProfile, ImageCmsProfile): @@ -449,37 +500,42 @@ def buildTransform( if not isinstance(outputProfile, ImageCmsProfile): outputProfile = ImageCmsProfile(outputProfile) return ImageCmsTransform( - inputProfile, outputProfile, inMode, outMode, - renderingIntent, flags=flags) - except (IOError, TypeError, ValueError) as v: - raise PyCMSError(v) + inputProfile, outputProfile, inMode, outMode, renderingIntent, flags=flags + ) + except (OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v def buildProofTransform( - inputProfile, outputProfile, proofProfile, inMode, outMode, - renderingIntent=INTENT_PERCEPTUAL, - proofRenderingIntent=INTENT_ABSOLUTE_COLORIMETRIC, - flags=FLAGS["SOFTPROOFING"]): - """ - (pyCMS) Builds an ICC transform mapping from the inputProfile to the - outputProfile, but tries to simulate the result that would be - obtained on the proofProfile device. + inputProfile, + outputProfile, + proofProfile, + inMode, + outMode, + renderingIntent=Intent.PERCEPTUAL, + proofRenderingIntent=Intent.ABSOLUTE_COLORIMETRIC, + flags=FLAGS["SOFTPROOFING"], +): + """ + (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the + ``outputProfile``, but tries to simulate the result that would be + obtained on the ``proofProfile`` device. If the input, output, or proof profiles specified are not valid - filenames, a PyCMSError will be raised. + filenames, a :exc:`PyCMSError` will be raised. - If an error occurs during creation of the transform, a PyCMSError will - be raised. + If an error occurs during creation of the transform, + a :exc:`PyCMSError` will be raised. - If inMode or outMode are not a mode supported by the outputProfile - (or by pyCMS), a PyCMSError will be raised. + If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile`` + (or by pyCMS), a :exc:`PyCMSError` will be raised. - This function builds and returns an ICC transform from the inputProfile - to the outputProfile, but tries to simulate the result that would be - obtained on the proofProfile device using renderingIntent and - proofRenderingIntent to determine what to do with out-of-gamut + This function builds and returns an ICC transform from the ``inputProfile`` + to the ``outputProfile``, but tries to simulate the result that would be + obtained on the ``proofProfile`` device using ``renderingIntent`` and + ``proofRenderingIntent`` to determine what to do with out-of-gamut colors. This is known as "soft-proofing". It will ONLY work for - converting images that are in inMode to images that are in outMode + converting images that are in ``inMode`` to images that are in outMode color format (PIL mode, i.e. "RGB", "RGBA", "CMYK", etc.). Usage of the resulting transform object is exactly the same as with @@ -487,7 +543,7 @@ def buildProofTransform( Proof profiling is generally used when using an output device to get a good idea of what the final printed/displayed image would look like on - the proofProfile device when it's quicker and easier to use the + the ``proofProfile`` device when it's quicker and easier to use the output device for judging color. Generally, this means that the output device is a monitor, or a dye-sub printer (etc.), and the simulated device is something more expensive, complicated, or time consuming @@ -512,20 +568,20 @@ def buildProofTransform( :param renderingIntent: Integer (0-3) specifying the rendering intent you wish to use for the input->proof (simulated) transform - INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL) - INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC) - INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION) - INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC) + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 see the pyCMS documentation for details on rendering intents and what they do. - :param proofRenderingIntent: Integer (0-3) specifying the rendering intent you - wish to use for proof->output transform + :param proofRenderingIntent: Integer (0-3) specifying the rendering intent + you wish to use for proof->output transform - INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL) - INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC) - INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION) - INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC) + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 see the pyCMS documentation for details on rendering intents and what they do. @@ -538,8 +594,7 @@ def buildProofTransform( raise PyCMSError("renderingIntent must be an integer between 0 and 3") if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError( - "flags must be an integer between 0 and %s" + _MAX_FLAG) + raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) try: if not isinstance(inputProfile, ImageCmsProfile): @@ -549,53 +604,61 @@ def buildProofTransform( if not isinstance(proofProfile, ImageCmsProfile): proofProfile = ImageCmsProfile(proofProfile) return ImageCmsTransform( - inputProfile, outputProfile, inMode, outMode, renderingIntent, - proofProfile, proofRenderingIntent, flags) - except (IOError, TypeError, ValueError) as v: - raise PyCMSError(v) + inputProfile, + outputProfile, + inMode, + outMode, + renderingIntent, + proofProfile, + proofRenderingIntent, + flags, + ) + except (OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v + buildTransformFromOpenProfiles = buildTransform buildProofTransformFromOpenProfiles = buildProofTransform -def applyTransform(im, transform, inPlace=0): +def applyTransform(im, transform, inPlace=False): """ (pyCMS) Applies a transform to a given image. - If im.mode != transform.inMode, a PyCMSError is raised. + If ``im.mode != transform.inMode``, a :exc:`PyCMSError` is raised. - If inPlace == TRUE and transform.inMode != transform.outMode, a - PyCMSError is raised. + If ``inPlace`` is ``True`` and ``transform.inMode != transform.outMode``, a + :exc:`PyCMSError` is raised. - If im.mode, transfer.inMode, or transfer.outMode is not supported by - pyCMSdll or the profiles you used for the transform, a PyCMSError is - raised. + If ``im.mode``, ``transform.inMode`` or ``transform.outMode`` is not + supported by pyCMSdll or the profiles you used for the transform, a + :exc:`PyCMSError` is raised. - If an error occurs while the transform is being applied, a PyCMSError - is raised. + If an error occurs while the transform is being applied, + a :exc:`PyCMSError` is raised. This function applies a pre-calculated transform (from ImageCms.buildTransform() or ImageCms.buildTransformFromOpenProfiles()) - to an image. The transform can be used for multiple images, saving + to an image. The transform can be used for multiple images, saving considerable calculation time if doing the same conversion multiple times. If you want to modify im in-place instead of receiving a new image as - the return value, set inPlace to TRUE. This can only be done if - transform.inMode and transform.outMode are the same, because we can't + the return value, set ``inPlace`` to ``True``. This can only be done if + ``transform.inMode`` and ``transform.outMode`` are the same, because we can't change the mode in-place (the buffer sizes for some modes are - different). The default behavior is to return a new Image object of - the same dimensions in mode transform.outMode. + different). The default behavior is to return a new :py:class:`~PIL.Image.Image` + object of the same dimensions in mode ``transform.outMode``. - :param im: A PIL Image object, and im.mode must be the same as the inMode - supported by the transform. + :param im: An :py:class:`~PIL.Image.Image` object, and im.mode must be the same + as the ``inMode`` supported by the transform. :param transform: A valid CmsTransform class object - :param inPlace: Bool (1 == True, 0 or None == False). If True, im is - modified in place and None is returned, if False, a new Image object - with the transform applied is returned (and im is not changed). The - default is False. - :returns: Either None, or a new PIL Image object, depending on the value of - inPlace. The profile will be returned in the image's - info['icc_profile']. + :param inPlace: Bool. If ``True``, ``im`` is modified in place and ``None`` is + returned, if ``False``, a new :py:class:`~PIL.Image.Image` object with the + transform applied is returned (and ``im`` is not changed). The default is + ``False``. + :returns: Either ``None``, or a new :py:class:`~PIL.Image.Image` object, + depending on the value of ``inPlace``. The profile will be returned in + the image's ``info['icc_profile']``. :exception PyCMSError: """ @@ -606,7 +669,7 @@ def applyTransform(im, transform, inPlace=0): else: imOut = transform.apply(im) except (TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v return imOut @@ -615,11 +678,14 @@ def createProfile(colorSpace, colorTemp=-1): """ (pyCMS) Creates a profile. - If colorSpace not in ["LAB", "XYZ", "sRGB"], a PyCMSError is raised + If colorSpace not in ``["LAB", "XYZ", "sRGB"]``, + a :exc:`PyCMSError` is raised. - If using LAB and colorTemp != a positive integer, a PyCMSError is raised. + If using LAB and ``colorTemp`` is not a positive integer, + a :exc:`PyCMSError` is raised. - If an error occurs while creating the profile, a PyCMSError is raised. + If an error occurs while creating the profile, + a :exc:`PyCMSError` is raised. Use this function to create common profiles on-the-fly instead of having to supply a profile on disk and knowing the path to it. It @@ -640,21 +706,21 @@ def createProfile(colorSpace, colorTemp=-1): if colorSpace not in ["LAB", "XYZ", "sRGB"]: raise PyCMSError( - "Color space not supported for on-the-fly profile creation (%s)" - % colorSpace) + f"Color space not supported for on-the-fly profile creation ({colorSpace})" + ) if colorSpace == "LAB": try: colorTemp = float(colorTemp) - except: + except (TypeError, ValueError) as e: raise PyCMSError( - "Color temperature must be numeric, \"%s\" not valid" - % colorTemp) + f'Color temperature must be numeric, "{colorTemp}" not valid' + ) from e try: return core.createProfile(colorSpace, colorTemp) except (TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def getProfileName(profile): @@ -662,9 +728,9 @@ def getProfileName(profile): (pyCMS) Gets the internal product name for the given profile. - If profile isn't a valid CmsProfile object or filename to a profile, - a PyCMSError is raised If an error occurs while trying to obtain the - name tag, a PyCMSError is raised. + If ``profile`` isn't a valid CmsProfile object or filename to a profile, + a :exc:`PyCMSError` is raised If an error occurs while trying + to obtain the name tag, a :exc:`PyCMSError` is raised. Use this function to obtain the INTERNAL name of the profile (stored in an ICC tag in the profile itself), usually the one used when the @@ -686,28 +752,28 @@ def getProfileName(profile): # // name was "%s - %s" (model, manufacturer) || Description , # // but if the Model and Manufacturer were the same or the model # // was long, Just the model, in 1.x - model = profile.profile.product_model - manufacturer = profile.profile.product_manufacturer + model = profile.profile.model + manufacturer = profile.profile.manufacturer if not (model or manufacturer): - return profile.profile.product_description + "\n" + return (profile.profile.profile_description or "") + "\n" if not manufacturer or len(model) > 30: return model + "\n" - return "%s - %s\n" % (model, manufacturer) + return f"{model} - {manufacturer}\n" - except (AttributeError, IOError, TypeError, ValueError) as v: - raise PyCMSError(v) + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v def getProfileInfo(profile): """ (pyCMS) Gets the internal product information for the given profile. - If profile isn't a valid CmsProfile object or filename to a profile, - a PyCMSError is raised. + If ``profile`` isn't a valid CmsProfile object or filename to a profile, + a :exc:`PyCMSError` is raised. - If an error occurs while trying to obtain the info tag, a PyCMSError - is raised + If an error occurs while trying to obtain the info tag, + a :exc:`PyCMSError` is raised. Use this function to obtain the information stored in the profile's info tag. This often contains details about the profile, and how it @@ -726,28 +792,28 @@ def getProfileInfo(profile): # add an extra newline to preserve pyCMS compatibility # Python, not C. the white point bits weren't working well, # so skipping. - # // info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint - description = profile.profile.product_description - cpright = profile.profile.product_copyright + # info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint + description = profile.profile.profile_description + cpright = profile.profile.copyright arr = [] for elt in (description, cpright): if elt: arr.append(elt) return "\r\n\r\n".join(arr) + "\r\n\r\n" - except (AttributeError, IOError, TypeError, ValueError) as v: - raise PyCMSError(v) + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v def getProfileCopyright(profile): """ (pyCMS) Gets the copyright for the given profile. - If profile isn't a valid CmsProfile object or filename to a profile, - a PyCMSError is raised. + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. - If an error occurs while trying to obtain the copyright tag, a PyCMSError - is raised + If an error occurs while trying to obtain the copyright tag, + a :exc:`PyCMSError` is raised. Use this function to obtain the information stored in the profile's copyright tag. @@ -762,20 +828,20 @@ def getProfileCopyright(profile): # add an extra newline to preserve pyCMS compatibility if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) - return profile.profile.product_copyright + "\n" - except (AttributeError, IOError, TypeError, ValueError) as v: - raise PyCMSError(v) + return (profile.profile.copyright or "") + "\n" + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v def getProfileManufacturer(profile): """ (pyCMS) Gets the manufacturer for the given profile. - If profile isn't a valid CmsProfile object or filename to a profile, - a PyCMSError is raised. + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. If an error occurs while trying to obtain the manufacturer tag, a - PyCMSError is raised + :exc:`PyCMSError` is raised. Use this function to obtain the information stored in the profile's manufacturer tag. @@ -790,20 +856,20 @@ def getProfileManufacturer(profile): # add an extra newline to preserve pyCMS compatibility if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) - return profile.profile.product_manufacturer + "\n" - except (AttributeError, IOError, TypeError, ValueError) as v: - raise PyCMSError(v) + return (profile.profile.manufacturer or "") + "\n" + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v def getProfileModel(profile): """ (pyCMS) Gets the model for the given profile. - If profile isn't a valid CmsProfile object or filename to a profile, - a PyCMSError is raised. + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. - If an error occurs while trying to obtain the model tag, a PyCMSError - is raised + If an error occurs while trying to obtain the model tag, + a :exc:`PyCMSError` is raised. Use this function to obtain the information stored in the profile's model tag. @@ -819,20 +885,20 @@ def getProfileModel(profile): # add an extra newline to preserve pyCMS compatibility if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) - return profile.profile.product_model + "\n" - except (AttributeError, IOError, TypeError, ValueError) as v: - raise PyCMSError(v) + return (profile.profile.model or "") + "\n" + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v def getProfileDescription(profile): """ (pyCMS) Gets the description for the given profile. - If profile isn't a valid CmsProfile object or filename to a profile, - a PyCMSError is raised. + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. - If an error occurs while trying to obtain the description tag, a PyCMSError - is raised + If an error occurs while trying to obtain the description tag, + a :exc:`PyCMSError` is raised. Use this function to obtain the information stored in the profile's description tag. @@ -848,20 +914,20 @@ def getProfileDescription(profile): # add an extra newline to preserve pyCMS compatibility if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) - return profile.profile.product_description + "\n" - except (AttributeError, IOError, TypeError, ValueError) as v: - raise PyCMSError(v) + return (profile.profile.profile_description or "") + "\n" + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v def getDefaultIntent(profile): """ (pyCMS) Gets the default intent name for the given profile. - If profile isn't a valid CmsProfile object or filename to a profile, - a PyCMSError is raised. + If ``profile`` isn't a valid CmsProfile object or filename to a profile, a + :exc:`PyCMSError` is raised. If an error occurs while trying to obtain the default intent, a - PyCMSError is raised. + :exc:`PyCMSError` is raised. Use this function to determine the default (and usually best optimized) rendering intent for this profile. Most profiles support multiple @@ -874,10 +940,10 @@ def getDefaultIntent(profile): :returns: Integer 0-3 specifying the default rendering intent for this profile. - INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL) - INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC) - INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION) - INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC) + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 see the pyCMS documentation for details on rendering intents and what they do. @@ -888,8 +954,8 @@ def getDefaultIntent(profile): if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) return profile.profile.rendering_intent - except (AttributeError, IOError, TypeError, ValueError) as v: - raise PyCMSError(v) + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v def isIntentSupported(profile, intent, direction): @@ -897,34 +963,34 @@ def isIntentSupported(profile, intent, direction): (pyCMS) Checks if a given intent is supported. Use this function to verify that you can use your desired - renderingIntent with profile, and that profile can be used for the + ``intent`` with ``profile``, and that ``profile`` can be used for the input/output/proof profile as you desire. Some profiles are created specifically for one "direction", can cannot - be used for others. Some profiles can only be used for certain - rendering intents... so it's best to either verify this before trying + be used for others. Some profiles can only be used for certain + rendering intents, so it's best to either verify this before trying to create a transform with them (using this function), or catch the - potential PyCMSError that will occur if they don't support the modes - you select. + potential :exc:`PyCMSError` that will occur if they don't + support the modes you select. :param profile: EITHER a valid CmsProfile object, OR a string of the filename of an ICC profile. :param intent: Integer (0-3) specifying the rendering intent you wish to use with this profile - INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL) - INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC) - INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION) - INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC) + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 see the pyCMS documentation for details on rendering intents and what they do. - :param direction: Integer specifying if the profile is to be used for input, - output, or proof + :param direction: Integer specifying if the profile is to be used for + input, output, or proof - INPUT = 0 (or use ImageCms.DIRECTION_INPUT) - OUTPUT = 1 (or use ImageCms.DIRECTION_OUTPUT) - PROOF = 2 (or use ImageCms.DIRECTION_PROOF) + INPUT = 0 (or use ImageCms.Direction.INPUT) + OUTPUT = 1 (or use ImageCms.Direction.OUTPUT) + PROOF = 2 (or use ImageCms.Direction.PROOF) :returns: 1 if the intent/direction are supported, -1 if they are not. :exception PyCMSError: @@ -939,8 +1005,8 @@ def isIntentSupported(profile, intent, direction): return 1 else: return -1 - except (AttributeError, IOError, TypeError, ValueError) as v: - raise PyCMSError(v) + except (AttributeError, OSError, TypeError, ValueError) as v: + raise PyCMSError(v) from v def versions(): @@ -948,26 +1014,4 @@ def versions(): (pyCMS) Fetches versions. """ - return ( - VERSION, core.littlecms_version, - sys.version.split()[0], Image.VERSION - ) - -# -------------------------------------------------------------------- - -if __name__ == "__main__": - # create a cheap manual from the __doc__ strings for the functions above - - print(__doc__) - - for f in dir(sys.modules[__name__]): - doc = None - try: - exec("doc = %s.__doc__" % (f)) - if "pyCMS" in doc: - # so we don't get the __doc__ string for imported modules - print("=" * 80) - print("%s" % f) - print(doc) - except (AttributeError, TypeError): - pass + return VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__ diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index 1c7bc31d59d..9cbce4143f3 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -17,20 +17,23 @@ # See the README file for information on usage and redistribution. # -from . import Image import re +from . import Image + def getrgb(color): """ - Convert a color string to an RGB tuple. If the string cannot be parsed, - this function raises a :py:exc:`ValueError` exception. + Convert a color string to an RGB or RGBA tuple. If the string cannot be + parsed, this function raises a :py:exc:`ValueError` exception. .. versionadded:: 1.1.4 :param color: A color string :return: ``(red, green, blue[, alpha])`` """ + if len(color) > 100: + raise ValueError("color specifier is too long") color = color.lower() rgb = colormap.get(color, None) @@ -41,104 +44,110 @@ def getrgb(color): return rgb # check for known string formats - if re.match('#[a-f0-9]{3}$', color): - return ( - int(color[1]*2, 16), - int(color[2]*2, 16), - int(color[3]*2, 16), - ) + if re.match("#[a-f0-9]{3}$", color): + return int(color[1] * 2, 16), int(color[2] * 2, 16), int(color[3] * 2, 16) - if re.match('#[a-f0-9]{4}$', color): + if re.match("#[a-f0-9]{4}$", color): return ( - int(color[1]*2, 16), - int(color[2]*2, 16), - int(color[3]*2, 16), - int(color[4]*2, 16), - ) + int(color[1] * 2, 16), + int(color[2] * 2, 16), + int(color[3] * 2, 16), + int(color[4] * 2, 16), + ) - if re.match('#[a-f0-9]{6}$', color): - return ( - int(color[1:3], 16), - int(color[3:5], 16), - int(color[5:7], 16), - ) + if re.match("#[a-f0-9]{6}$", color): + return int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16) - if re.match('#[a-f0-9]{8}$', color): + if re.match("#[a-f0-9]{8}$", color): return ( int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16), int(color[7:9], 16), - ) + ) m = re.match(r"rgb\(\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)) - ) + return int(m.group(1)), int(m.group(2)), int(m.group(3)) m = re.match(r"rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color) if m: return ( int((int(m.group(1)) * 255) / 100.0 + 0.5), int((int(m.group(2)) * 255) / 100.0 + 0.5), - int((int(m.group(3)) * 255) / 100.0 + 0.5) - ) + int((int(m.group(3)) * 255) / 100.0 + 0.5), + ) - m = re.match(r"hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color) + m = re.match( + r"hsl\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color + ) if m: from colorsys import hls_to_rgb + rgb = hls_to_rgb( float(m.group(1)) / 360.0, float(m.group(3)) / 100.0, float(m.group(2)) / 100.0, - ) + ) return ( int(rgb[0] * 255 + 0.5), int(rgb[1] * 255 + 0.5), - int(rgb[2] * 255 + 0.5) - ) + int(rgb[2] * 255 + 0.5), + ) - m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", - color) + m = re.match( + r"hs[bv]\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color + ) if m: + from colorsys import hsv_to_rgb + + rgb = hsv_to_rgb( + float(m.group(1)) / 360.0, + float(m.group(2)) / 100.0, + float(m.group(3)) / 100.0, + ) return ( - int(m.group(1)), - int(m.group(2)), - int(m.group(3)), - int(m.group(4)) - ) - raise ValueError("unknown color specifier: %r" % color) + int(rgb[0] * 255 + 0.5), + int(rgb[1] * 255 + 0.5), + int(rgb[2] * 255 + 0.5), + ) + + 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)}") def getcolor(color, mode): """ Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a - greyscale value if the mode is not color or a palette image. If the string + greyscale value if ``mode`` is not color or a palette image. If the string cannot be parsed, this function raises a :py:exc:`ValueError` exception. .. versionadded:: 1.1.4 :param color: A color string - :return: ``(graylevel [, alpha]) or (red, green, blue[, alpha])`` + :param mode: Convert result to this mode + :return: ``(graylevel[, alpha]) or (red, green, blue[, alpha])`` """ # same as getrgb, but converts the result to the given mode color, alpha = getrgb(color), 255 if len(color) == 4: - color, alpha = color[0:3], color[3] + color, alpha = color[:3], color[3] if Image.getmodebase(mode) == "L": r, g, b = color - color = (r*299 + g*587 + b*114)//1000 - if mode[-1] == 'A': - return (color, alpha) + # ITU-R Recommendation 601-2 for nonlinear RGB + # scaled to 24 bits to match the convert's implementation. + color = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16 + if mode[-1] == "A": + return color, alpha else: - if mode[-1] == 'A': + if mode[-1] == "A": return color + (alpha,) return color + colormap = { # X11 colour table from https://drafts.csswg.org/css-color-4/, with # gray/grey spelling issues fixed. This is a superset of HTML 4.0 diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 89df2733811..ff94f0ce3d6 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -30,10 +30,12 @@ # See the README file for information on usage and redistribution. # +import math import numbers +import warnings from . import Image, ImageColor -from ._util import isStringType +from ._deprecate import deprecate """ A simple 2D drawing interface for PIL images. @@ -43,7 +45,8 @@ """ -class ImageDraw(object): +class ImageDraw: + font = None def __init__(self, im, mode=None): """ @@ -71,29 +74,43 @@ def __init__(self, im, mode=None): self.palette = im.palette else: self.palette = None + self._image = im self.im = im.im self.draw = Image.core.draw(self.im, blend) self.mode = mode if mode in ("I", "F"): - self.ink = self.draw.draw_ink(1, mode) + self.ink = self.draw.draw_ink(1) else: - self.ink = self.draw.draw_ink(-1, mode) + self.ink = self.draw.draw_ink(-1) if mode in ("1", "P", "I", "F"): # FIXME: fix Fill2 to properly support matte for I+F images self.fontmode = "1" else: self.fontmode = "L" # aliasing is okay for other modes - self.fill = 0 - self.font = None + 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 from . import ImageFont + self.font = ImageFont.load_default() return self.font @@ -105,24 +122,24 @@ def _getink(self, ink, fill=None): ink = self.ink else: if ink is not None: - if isStringType(ink): + if isinstance(ink, str): ink = ImageColor.getcolor(ink, self.mode) if self.palette and not isinstance(ink, numbers.Number): - ink = self.palette.getcolor(ink) - ink = self.draw.draw_ink(ink, self.mode) + ink = self.palette.getcolor(ink, self._image) + ink = self.draw.draw_ink(ink) if fill is not None: - if isStringType(fill): + if isinstance(fill, str): fill = ImageColor.getcolor(fill, self.mode) if self.palette and not isinstance(fill, numbers.Number): - fill = self.palette.getcolor(fill) - fill = self.draw.draw_ink(fill, self.mode) + fill = self.palette.getcolor(fill, self._image) + fill = self.draw.draw_ink(fill) return ink, fill - def arc(self, xy, start, end, fill=None): + def arc(self, xy, start, end, fill=None, width=1): """Draw an arc.""" ink, fill = self._getink(fill) if ink is not None: - self.draw.draw_arc(xy, start, end, ink) + self.draw.draw_arc(xy, start, end, ink, width) def bitmap(self, xy, bitmap, fill=None): """Draw a bitmap.""" @@ -133,27 +150,81 @@ def bitmap(self, xy, bitmap, fill=None): if ink is not None: self.draw.draw_bitmap(xy, bitmap.im, ink) - def chord(self, xy, start, end, fill=None, outline=None): + def chord(self, xy, start, end, fill=None, outline=None, width=1): """Draw a chord.""" ink, fill = self._getink(outline, fill) if fill is not None: self.draw.draw_chord(xy, start, end, fill, 1) - if ink is not None: - self.draw.draw_chord(xy, start, end, ink, 0) + if ink is not None and ink != fill and width != 0: + self.draw.draw_chord(xy, start, end, ink, 0, width) - def ellipse(self, xy, fill=None, outline=None): + def ellipse(self, xy, fill=None, outline=None, width=1): """Draw an ellipse.""" ink, fill = self._getink(outline, fill) if fill is not None: self.draw.draw_ellipse(xy, fill, 1) - if ink is not None: - self.draw.draw_ellipse(xy, ink, 0) + if ink is not None and ink != fill and width != 0: + self.draw.draw_ellipse(xy, ink, 0, width) - def line(self, xy, fill=None, width=0): + def line(self, xy, fill=None, width=0, joint=None): """Draw a line, or a connected sequence of line segments.""" - ink, fill = self._getink(fill) + ink = self._getink(fill)[0] if ink is not None: self.draw.draw_lines(xy, ink, width) + if joint == "curve" and width > 4: + if not isinstance(xy[0], (list, tuple)): + xy = [tuple(xy[i : i + 2]) for i in range(0, len(xy), 2)] + for i in range(1, len(xy) - 1): + point = xy[i] + angles = [ + math.degrees(math.atan2(end[0] - start[0], start[1] - end[1])) + % 360 + for start, end in ((xy[i - 1], point), (point, xy[i + 1])) + ] + if angles[0] == angles[1]: + # This is a straight line, so no joint is required + continue + + def coord_at_angle(coord, angle): + x, y = coord + angle -= 90 + distance = width / 2 - 1 + return tuple( + p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d)) + for p, p_d in ( + (x, distance * math.cos(math.radians(angle))), + (y, distance * math.sin(math.radians(angle))), + ) + ) + + flipped = ( + angles[1] > angles[0] and angles[1] - 180 > angles[0] + ) or (angles[1] < angles[0] and angles[1] + 180 > angles[0]) + coords = [ + (point[0] - width / 2 + 1, point[1] - width / 2 + 1), + (point[0] + width / 2 - 1, point[1] + width / 2 - 1), + ] + if flipped: + start, end = (angles[1] + 90, angles[0] + 90) + else: + start, end = (angles[0] - 90, angles[1] - 90) + self.pieslice(coords, start - 90, end - 90, fill) + + if width > 8: + # Cover potential gaps between the line and the joint + if flipped: + gap_coords = [ + coord_at_angle(point, angles[0] + 90), + point, + coord_at_angle(point, angles[1] + 90), + ] + else: + gap_coords = [ + coord_at_angle(point, angles[0] - 90), + point, + coord_at_angle(point, angles[1] - 90), + ] + self.line(gap_coords, fill, width=3) def shape(self, shape, fill=None, outline=None): """(Experimental) Draw a shape.""" @@ -161,16 +232,16 @@ def shape(self, shape, fill=None, outline=None): ink, fill = self._getink(outline, fill) if fill is not None: self.draw.draw_outline(shape, fill, 1) - if ink is not None: + if ink is not None and ink != fill: self.draw.draw_outline(shape, ink, 0) - def pieslice(self, xy, start, end, fill=None, outline=None): + def pieslice(self, xy, start, end, fill=None, outline=None, width=1): """Draw a pieslice.""" ink, fill = self._getink(outline, fill) if fill is not None: self.draw.draw_pieslice(xy, start, end, fill, 1) - if ink is not None: - self.draw.draw_pieslice(xy, start, end, ink, 0) + if ink is not None and ink != fill and width != 0: + self.draw.draw_pieslice(xy, start, end, ink, 0, width) def point(self, xy, fill=None): """Draw one or more individual pixels.""" @@ -178,21 +249,133 @@ def point(self, xy, fill=None): if ink is not None: self.draw.draw_points(xy, ink) - def polygon(self, xy, fill=None, outline=None): + def polygon(self, xy, fill=None, outline=None, width=1): """Draw a polygon.""" ink, fill = self._getink(outline, fill) if fill is not None: self.draw.draw_polygon(xy, fill, 1) - if ink is not None: - self.draw.draw_polygon(xy, ink, 0) + if ink is not None and ink != fill and width != 0: + if width == 1: + self.draw.draw_polygon(xy, ink, 0, width) + else: + # To avoid expanding the polygon outwards, + # use the fill as a mask + mask = Image.new("1", self.im.size) + mask_ink = self._getink(1)[0] + + fill_im = mask.copy() + draw = Draw(fill_im) + draw.draw.draw_polygon(xy, mask_ink, 1) + + ink_im = mask.copy() + draw = Draw(ink_im) + width = width * 2 - 1 + draw.draw.draw_polygon(xy, mask_ink, 0, width) - def rectangle(self, xy, fill=None, outline=None): + mask.paste(ink_im, mask=fill_im) + + im = Image.new(self.mode, self.im.size) + draw = Draw(im) + draw.draw.draw_polygon(xy, ink, 0, width) + self.im.paste(im.im, (0, 0) + im.size, mask.im) + + def regular_polygon( + self, bounding_circle, n_sides, rotation=0, fill=None, outline=None + ): + """Draw a regular polygon.""" + xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) + self.polygon(xy, fill, outline) + + def rectangle(self, xy, fill=None, outline=None, width=1): """Draw a rectangle.""" ink, fill = self._getink(outline, fill) if fill is not None: self.draw.draw_rectangle(xy, fill, 1) - if ink is not None: - self.draw.draw_rectangle(xy, ink, 0) + if ink is not None and ink != fill and width != 0: + self.draw.draw_rectangle(xy, ink, 0, width) + + def rounded_rectangle(self, xy, radius=0, fill=None, outline=None, width=1): + """Draw a rounded rectangle.""" + if isinstance(xy[0], (list, tuple)): + (x0, y0), (x1, y1) = xy + else: + x0, y0, x1, y1 = xy + + d = radius * 2 + + full_x = d >= x1 - x0 + if full_x: + # The two left and two right corners are joined + d = x1 - x0 + full_y = d >= y1 - y0 + if full_y: + # The two top and two bottom corners are joined + d = y1 - y0 + if full_x and full_y: + # If all corners are joined, that is a circle + return self.ellipse(xy, fill, outline, width) + + if d == 0: + # If the corners have no curve, that is a rectangle + return self.rectangle(xy, fill, outline, width) + + r = d // 2 + ink, fill = self._getink(outline, fill) + + def draw_corners(pieslice): + if full_x: + # Draw top and bottom halves + parts = ( + ((x0, y0, x0 + d, y0 + d), 180, 360), + ((x0, y1 - d, x0 + d, y1), 0, 180), + ) + elif full_y: + # Draw left and right halves + parts = ( + ((x0, y0, x0 + d, y0 + d), 90, 270), + ((x1 - d, y0, x1, y0 + d), 270, 90), + ) + else: + # Draw four separate corners + parts = ( + ((x1 - d, y0, x1, y0 + d), 270, 360), + ((x1 - d, y1 - d, x1, y1), 0, 90), + ((x0, y1 - d, x0 + d, y1), 90, 180), + ((x0, y0, x0 + d, y0 + d), 180, 270), + ) + for part in parts: + if pieslice: + self.draw.draw_pieslice(*(part + (fill, 1))) + else: + self.draw.draw_arc(*(part + (ink, width))) + + if fill is not None: + draw_corners(True) + + if full_x: + self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill, 1) + else: + self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1) + if not full_x and not full_y: + self.draw.draw_rectangle((x0, y0 + r + 1, x0 + r, y1 - r - 1), fill, 1) + self.draw.draw_rectangle((x1 - r, y0 + r + 1, x1, y1 - r - 1), fill, 1) + if ink is not None and ink != fill and width != 0: + draw_corners(False) + + if not full_x: + self.draw.draw_rectangle( + (x0 + r + 1, y0, x1 - r - 1, y0 + width - 1), ink, 1 + ) + self.draw.draw_rectangle( + (x0 + r + 1, y1 - width + 1, x1 - r - 1, y1), ink, 1 + ) + if not full_y: + self.draw.draw_rectangle( + (x0, y0 + r + 1, x0 + width - 1, y1 - r - 1), ink, 1 + ) + self.draw.draw_rectangle( + (x1 - width + 1, y0 + r + 1, x1, y1 - r - 1), ink, 1 + ) def _multiline_check(self, text): """Draw text.""" @@ -205,73 +388,443 @@ def _multiline_split(self, text): return text.split(split_character) - def text(self, xy, text, fill=None, font=None, anchor=None, - *args, **kwargs): + def _multiline_spacing(self, font, spacing, stroke_width): + # this can be replaced with self.textbbox(...)[3] when textsize is removed + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + return ( + self.textsize( + "A", + font=font, + stroke_width=stroke_width, + )[1] + + spacing + ) + + def text( + self, + xy, + text, + fill=None, + font=None, + anchor=None, + spacing=4, + align="left", + direction=None, + features=None, + language=None, + stroke_width=0, + stroke_fill=None, + embedded_color=False, + *args, + **kwargs, + ): if self._multiline_check(text): - return self.multiline_text(xy, text, fill, font, anchor, - *args, **kwargs) - ink, fill = self._getink(fill) + return self.multiline_text( + xy, + text, + fill, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + stroke_fill, + embedded_color, + ) + + if embedded_color and self.mode not in ("RGB", "RGBA"): + raise ValueError("Embedded color supported only in RGB and RGBA modes") + if font is None: font = self.getfont() - if ink is None: - ink = fill - if ink is not None: + + def getink(fill): + ink, fill = self._getink(fill) + if ink is None: + return fill + return ink + + def draw_text(ink, stroke_width=0, stroke_offset=None): + mode = self.fontmode + if stroke_width == 0 and embedded_color: + mode = "RGBA" + coord = xy try: - mask, offset = font.getmask2(text, self.fontmode, *args, **kwargs) - xy = xy[0] + offset[0], xy[1] + offset[1] + mask, offset = font.getmask2( + text, + mode, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + anchor=anchor, + ink=ink, + *args, + **kwargs, + ) + coord = coord[0] + offset[0], coord[1] + offset[1] except AttributeError: try: - mask = font.getmask(text, self.fontmode, *args, **kwargs) + mask = font.getmask( + text, + mode, + direction, + features, + language, + stroke_width, + anchor, + ink, + *args, + **kwargs, + ) except TypeError: mask = font.getmask(text) - self.draw.draw_bitmap(xy, mask, ink) + if stroke_offset: + coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1] + if mode == "RGBA": + # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A + # 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) + self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask) + else: + self.draw.draw_bitmap(coord, mask, ink) + + ink = getink(fill) + if ink is not None: + stroke_ink = None + if stroke_width: + stroke_ink = getink(stroke_fill) if stroke_fill is not None else ink + + if stroke_ink is not None: + # Draw stroked text + draw_text(stroke_ink, stroke_width) + + # Draw normal text + draw_text(ink, 0) + else: + # Only draw normal text + draw_text(ink) + + def multiline_text( + self, + xy, + text, + fill=None, + font=None, + anchor=None, + spacing=4, + align="left", + direction=None, + features=None, + language=None, + stroke_width=0, + stroke_fill=None, + embedded_color=False, + ): + if direction == "ttb": + raise ValueError("ttb direction is unsupported for multiline text") + + if anchor is None: + anchor = "la" + elif len(anchor) != 2: + raise ValueError("anchor must be a 2 character string") + elif anchor[1] in "tb": + raise ValueError("anchor not supported for multiline text") - def multiline_text(self, xy, text, fill=None, font=None, anchor=None, - spacing=4, align="left", direction=None, features=None): widths = [] max_width = 0 lines = self._multiline_split(text) - line_spacing = self.textsize('A', font=font)[1] + spacing + line_spacing = self._multiline_spacing(font, spacing, stroke_width) for line in lines: - line_width, line_height = self.textsize(line, font) + line_width = self.textlength( + line, font, direction=direction, features=features, language=language + ) widths.append(line_width) max_width = max(max_width, line_width) - left, top = xy + + top = xy[1] + if anchor[1] == "m": + top -= (len(lines) - 1) * line_spacing / 2.0 + elif anchor[1] == "d": + top -= (len(lines) - 1) * line_spacing + for idx, line in enumerate(lines): + left = xy[0] + width_difference = max_width - widths[idx] + + # first align left by anchor + if anchor[0] == "m": + left -= width_difference / 2.0 + elif anchor[0] == "r": + left -= width_difference + + # then align by align parameter if align == "left": - pass # left = x + pass elif align == "center": - left += (max_width - widths[idx]) / 2.0 + left += width_difference / 2.0 elif align == "right": - left += (max_width - widths[idx]) + left += width_difference else: - assert False, 'align must be "left", "center" or "right"' - self.text((left, top), line, fill, font, anchor, - direction=direction, features=features) + raise ValueError('align must be "left", "center" or "right"') + + self.text( + (left, top), + line, + fill, + font, + anchor, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + stroke_fill=stroke_fill, + embedded_color=embedded_color, + ) top += line_spacing - left = xy[0] - def textsize(self, text, font=None, spacing=4, direction=None, - features=None): + def textsize( + self, + text, + font=None, + spacing=4, + direction=None, + features=None, + language=None, + stroke_width=0, + ): """Get the size of a given string, in pixels.""" + deprecate("textsize", 10, "textbbox or textlength") + if self._multiline_check(text): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + return self.multiline_textsize( + text, + font, + spacing, + direction, + features, + language, + stroke_width, + ) + + if font is None: + font = self.getfont() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + return font.getsize( + text, + direction, + features, + language, + stroke_width, + ) + + def multiline_textsize( + self, + text, + font=None, + spacing=4, + direction=None, + features=None, + language=None, + stroke_width=0, + ): + deprecate("multiline_textsize", 10, "multiline_textbbox") + max_width = 0 + lines = self._multiline_split(text) + line_spacing = self._multiline_spacing(font, spacing, stroke_width) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + for line in lines: + line_width, line_height = self.textsize( + line, + font, + spacing, + direction, + features, + language, + stroke_width, + ) + max_width = max(max_width, line_width) + return max_width, len(lines) * line_spacing - spacing + + def textlength( + self, + text, + font=None, + direction=None, + features=None, + language=None, + embedded_color=False, + ): + """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") + if embedded_color and self.mode not in ("RGB", "RGBA"): + raise ValueError("Embedded color supported only in RGB and RGBA modes") + + if font is None: + font = self.getfont() + mode = "RGBA" if embedded_color else self.fontmode + try: + return font.getlength(text, mode, direction, features, language) + except AttributeError: + deprecate("textlength support for fonts without getlength", 10) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + size = self.textsize( + text, + font, + direction=direction, + features=features, + language=language, + ) + if direction == "ttb": + return size[1] + return size[0] + + def textbbox( + self, + xy, + text, + font=None, + anchor=None, + spacing=4, + align="left", + direction=None, + features=None, + language=None, + stroke_width=0, + embedded_color=False, + ): + """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") + if self._multiline_check(text): - return self.multiline_textsize(text, font, spacing, - direction, features) + return self.multiline_textbbox( + xy, + text, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + embedded_color, + ) if font is None: font = self.getfont() - return font.getsize(text, direction, features) + mode = "RGBA" if embedded_color else self.fontmode + bbox = font.getbbox( + text, mode, direction, features, language, stroke_width, anchor + ) + return bbox[0] + xy[0], bbox[1] + xy[1], bbox[2] + xy[0], bbox[3] + xy[1] + + def multiline_textbbox( + self, + xy, + text, + font=None, + anchor=None, + spacing=4, + align="left", + direction=None, + features=None, + language=None, + stroke_width=0, + embedded_color=False, + ): + if direction == "ttb": + raise ValueError("ttb direction is unsupported for multiline text") - def multiline_textsize(self, text, font=None, spacing=4, direction=None, - features=None): + if anchor is None: + anchor = "la" + elif len(anchor) != 2: + raise ValueError("anchor must be a 2 character string") + elif anchor[1] in "tb": + raise ValueError("anchor not supported for multiline text") + + widths = [] max_width = 0 lines = self._multiline_split(text) - line_spacing = self.textsize('A', font=font)[1] + spacing + line_spacing = self._multiline_spacing(font, spacing, stroke_width) for line in lines: - line_width, line_height = self.textsize(line, font, spacing, - direction, features) + line_width = self.textlength( + line, + font, + direction=direction, + features=features, + language=language, + embedded_color=embedded_color, + ) + widths.append(line_width) max_width = max(max_width, line_width) - return max_width, len(lines)*line_spacing + + top = xy[1] + if anchor[1] == "m": + top -= (len(lines) - 1) * line_spacing / 2.0 + elif anchor[1] == "d": + top -= (len(lines) - 1) * line_spacing + + bbox = None + + for idx, line in enumerate(lines): + left = xy[0] + width_difference = max_width - widths[idx] + + # first align left by anchor + if anchor[0] == "m": + left -= width_difference / 2.0 + elif anchor[0] == "r": + left -= width_difference + + # then align by align parameter + if align == "left": + pass + elif align == "center": + left += width_difference / 2.0 + elif align == "right": + left += width_difference + else: + raise ValueError('align must be "left", "center" or "right"') + + bbox_line = self.textbbox( + (left, top), + line, + font, + anchor, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + embedded_color=embedded_color, + ) + if bbox is None: + bbox = bbox_line + else: + bbox = ( + min(bbox[0], bbox_line[0]), + min(bbox[1], bbox_line[1]), + max(bbox[2], bbox_line[2]), + max(bbox[3], bbox_line[3]), + ) + + top += line_spacing + + if bbox is None: + return xy[0], xy[1], xy[0], xy[1] + return bbox def Draw(im, mode=None): @@ -327,7 +880,8 @@ def floodfill(image, xy, value, border=None, thresh=0): (experimental) Fills a bounded region with a given color. :param image: Target image. - :param xy: Seed position (a 2-item coordinate tuple). + :param xy: Seed position (a 2-item coordinate tuple). See + :ref:`coordinate-system`. :param value: Fill color. :param border: Optional border value. If given, the region consists of pixels with a color different from the border color. If not given, @@ -335,10 +889,11 @@ def floodfill(image, xy, value, border=None, thresh=0): pixel. :param thresh: Optional threshold value which specifies a maximum tolerable difference of a pixel value from the 'background' in - order for it to be replaced. Useful for filling regions of non- - homogeneous, but similar, colors. + order for it to be replaced. Useful for filling regions of + non-homogeneous, but similar, colors. """ # based on an implementation by Eric S. Raymond + # amended by yo1995 @20180806 pixel = image.load() x, y = xy try: @@ -348,39 +903,156 @@ def floodfill(image, xy, value, border=None, thresh=0): pixel[x, y] = value except (ValueError, IndexError): return # seed point outside image - edge = [(x, y)] - if border is None: - while edge: - newedge = [] - for (x, y) in edge: - for (s, t) in ((x+1, y), (x-1, y), (x, y+1), (x, y-1)): - try: - p = pixel[s, t] - except IndexError: - pass + edge = {(x, y)} + # use a set to keep record of current and previous edge pixels + # to reduce memory consumption + full_edge = set() + while edge: + new_edge = set() + for (x, y) in edge: # 4 adjacent method + for (s, t) in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): + # If already processed, or if a coordinate is negative, skip + if (s, t) in full_edge or s < 0 or t < 0: + continue + try: + p = pixel[s, t] + except (ValueError, IndexError): + pass + else: + full_edge.add((s, t)) + if border is None: + fill = _color_diff(p, background) <= thresh else: - if _color_diff(p, background) <= thresh: - pixel[s, t] = value - newedge.append((s, t)) - edge = newedge + fill = p != value and p != border + if fill: + pixel[s, t] = value + new_edge.add((s, t)) + full_edge = edge # discard pixels processed + edge = new_edge + + +def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): + """ + Generate a list of vertices for a 2D regular polygon. + + :param bounding_circle: The bounding circle is a tuple defined + by a point and radius. The polygon is inscribed in this circle. + (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``) + :param n_sides: Number of sides + (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon) + :param rotation: Apply an arbitrary rotation to the polygon + (e.g. ``rotation=90``, applies a 90 degree rotation) + :return: List of regular polygon vertices + (e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``) + + How are the vertices computed? + 1. Compute the following variables + - theta: Angle between the apothem & the nearest polygon vertex + - side_length: Length of each polygon edge + - centroid: Center of bounding circle (1st, 2nd elements of bounding_circle) + - polygon_radius: Polygon radius (last element of bounding_circle) + - angles: Location of each polygon vertex in polar grid + (e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0]) + + 2. For each angle in angles, get the polygon vertex at that angle + The vertex is computed using the equation below. + X= xcos(φ) + ysin(φ) + Y= −xsin(φ) + ycos(φ) + + Note: + φ = angle in degrees + x = 0 + y = polygon_radius + + The formula above assumes rotation around the origin. + In our case, we are rotating around the centroid. + To account for this, we use the formula below + X = xcos(φ) + ysin(φ) + centroid_x + Y = −xsin(φ) + ycos(φ) + centroid_y + """ + # 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") + if n_sides < 3: + raise ValueError("n_sides should be an int > 2") + + # 1.2 Check `bounding_circle` has an appropriate value + if not isinstance(bounding_circle, (list, tuple)): + raise TypeError("bounding_circle should be a tuple") + + if len(bounding_circle) == 3: + *centroid, polygon_radius = bounding_circle + elif len(bounding_circle) == 2: + centroid, polygon_radius = bounding_circle else: - while edge: - newedge = [] - for (x, y) in edge: - for (s, t) in ((x+1, y), (x-1, y), (x, y+1), (x, y-1)): - try: - p = pixel[s, t] - except IndexError: - pass - else: - if p != value and p != border: - pixel[s, t] = value - newedge.append((s, t)) - edge = newedge + raise ValueError( + "bounding_circle should contain 2D coordinates " + "and a radius (e.g. (x, y, r) or ((x, y), r) )" + ) + + if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)): + raise ValueError("bounding_circle should only contain numeric data") + + if not len(centroid) == 2: + raise ValueError( + "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" + ) + + if polygon_radius <= 0: + raise ValueError("bounding_circle radius should be > 0") + # 1.3 Check `rotation` has an appropriate value + if not isinstance(rotation, (int, float)): + raise ValueError("rotation should be an int or float") -def _color_diff(rgb1, rgb2): + # 2. Define Helper Functions + def _apply_rotation(point, degrees, centroid): + return ( + round( + point[0] * math.cos(math.radians(360 - degrees)) + - point[1] * math.sin(math.radians(360 - degrees)) + + centroid[0], + 2, + ), + round( + point[1] * math.cos(math.radians(360 - degrees)) + + point[0] * math.sin(math.radians(360 - degrees)) + + centroid[1], + 2, + ), + ) + + def _compute_polygon_vertex(centroid, polygon_radius, angle): + start_point = [polygon_radius, 0] + return _apply_rotation(start_point, angle, centroid) + + def _get_angles(n_sides, rotation): + angles = [] + degrees = 360 / n_sides + # Start with the bottom left polygon vertex + current_angle = (270 - 0.5 * degrees) + rotation + for _ in range(0, n_sides): + angles.append(current_angle) + current_angle += degrees + if current_angle > 360: + current_angle -= 360 + return angles + + # 3. Variable Declarations + angles = _get_angles(n_sides, rotation) + + # 4. Compute Vertices + return [ + _compute_polygon_vertex(centroid, polygon_radius, angle) for angle in angles + ] + + +def _color_diff(color1, color2): """ - Uses 1-norm distance to calculate difference between two rgb values. + Uses 1-norm distance to calculate difference between two values. """ - return abs(rgb1[0]-rgb2[0]) + abs(rgb1[1]-rgb2[1]) + abs(rgb1[2]-rgb2[2]) + if isinstance(color2, tuple): + return sum(abs(color1[i] - color2[i]) for i in range(0, len(color2))) + else: + return abs(color1 - color2) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index a1763350d91..2667b77dd43 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -16,28 +16,48 @@ # See the README file for information on usage and redistribution. # + +""" +(Experimental) WCK-style drawing interface operations + +.. seealso:: :py:mod:`PIL.ImageDraw` +""" + + +import warnings + from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath +from ._deprecate import deprecate + +class Pen: + """Stores an outline color and width.""" -class Pen(object): def __init__(self, color, width=1, opacity=255): self.color = ImageColor.getrgb(color) self.width = width -class Brush(object): +class Brush: + """Stores a fill color""" + def __init__(self, color, opacity=255): self.color = ImageColor.getrgb(color) -class Font(object): +class Font: + """Stores a TrueType font and color""" + def __init__(self, color, file, size=12): # FIXME: add support for bitmap fonts self.color = ImageColor.getrgb(color) self.font = ImageFont.truetype(file, size) -class Draw(object): +class Draw: + """ + (Experimental) WCK-style drawing interface + """ def __init__(self, image, size=None, color=None): if not hasattr(image, "im"): @@ -74,38 +94,116 @@ def render(self, op, xy, pen, brush=None): getattr(self.draw, op)(xy, fill=fill, outline=outline) def settransform(self, offset): + """Sets a transformation offset.""" (xoffset, yoffset) = offset self.transform = (1, 0, xoffset, 0, 1, yoffset) def arc(self, xy, start, end, *options): + """ + Draws an arc (a portion of a circle outline) between the start and end + angles, inside the given bounding box. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc` + """ self.render("arc", xy, start, end, *options) def chord(self, xy, start, end, *options): + """ + Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points + with a straight line. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord` + """ self.render("chord", xy, start, end, *options) def ellipse(self, xy, *options): + """ + Draws an ellipse inside the given bounding box. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse` + """ self.render("ellipse", xy, *options) def line(self, xy, *options): + """ + Draws a line between the coordinates in the ``xy`` list. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line` + """ self.render("line", xy, *options) def pieslice(self, xy, start, end, *options): + """ + Same as arc, but also draws straight lines between the end points and the + center of the bounding box. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice` + """ self.render("pieslice", xy, start, end, *options) def polygon(self, xy, *options): + """ + Draws a polygon. + + The polygon outline consists of straight lines between the given + coordinates, plus a straight line between the last and the first + coordinate. + + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon` + """ self.render("polygon", xy, *options) def rectangle(self, xy, *options): - self.render("rectangle", xy, *options) + """ + Draws a rectangle. - def symbol(self, xy, symbol, *options): - raise NotImplementedError("not in this version") + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle` + """ + self.render("rectangle", xy, *options) def text(self, xy, text, font): + """ + Draws the string at the given position. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.text` + """ if self.transform: xy = ImagePath.Path(xy) xy.transform(self.transform) self.draw.text(xy, text, font=font.font, fill=font.color) def textsize(self, text, font): - return self.draw.textsize(text, font=font.font) + """ + .. deprecated:: 9.2.0 + + Return the size of the given string, in pixels. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textsize` + """ + deprecate("textsize", 10, "textbbox or textlength") + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + return self.draw.textsize(text, font=font.font) + + def textbbox(self, xy, text, font): + """ + Returns bounding box (in pixels) of given text. + + :return: ``(left, top, right, bottom)`` bounding box + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textbbox` + """ + if self.transform: + xy = ImagePath.Path(xy) + xy.transform(self.transform) + return self.draw.textbbox(xy, text, font=font.font) + + def textlength(self, text, font): + """ + Returns length (in pixels) of given text. + This is the amount by which following text should be offset. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textlength` + """ + return self.draw.textlength(text, font=font.font) diff --git a/src/PIL/ImageEnhance.py b/src/PIL/ImageEnhance.py index 11c9c3a06ac..3b79d5c46a1 100644 --- a/src/PIL/ImageEnhance.py +++ b/src/PIL/ImageEnhance.py @@ -21,8 +21,7 @@ from . import Image, ImageFilter, ImageStat -class _Enhance(object): - +class _Enhance: def enhance(self, factor): """ Returns an enhanced image. @@ -45,11 +44,12 @@ class Color(_Enhance): factor of 0.0 gives a black and white image. A factor of 1.0 gives the original image. """ + def __init__(self, image): self.image = image - self.intermediate_mode = 'L' - if 'A' in image.getbands(): - self.intermediate_mode = 'LA' + self.intermediate_mode = "L" + if "A" in image.getbands(): + self.intermediate_mode = "LA" self.degenerate = image.convert(self.intermediate_mode).convert(image.mode) @@ -61,13 +61,14 @@ class Contrast(_Enhance): to the contrast control on a TV set. An enhancement factor of 0.0 gives a solid grey image. A factor of 1.0 gives the original image. """ + def __init__(self, image): self.image = image mean = int(ImageStat.Stat(image.convert("L")).mean[0] + 0.5) self.degenerate = Image.new("L", image.size, mean).convert(image.mode) - if 'A' in image.getbands(): - self.degenerate.putalpha(image.getchannel('A')) + if "A" in image.getbands(): + self.degenerate.putalpha(image.getchannel("A")) class Brightness(_Enhance): @@ -77,12 +78,13 @@ class Brightness(_Enhance): enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the original image. """ + def __init__(self, image): self.image = image self.degenerate = Image.new(image.mode, image.size, 0) - if 'A' in image.getbands(): - self.degenerate.putalpha(image.getchannel('A')) + if "A" in image.getbands(): + self.degenerate.putalpha(image.getchannel("A")) class Sharpness(_Enhance): @@ -92,9 +94,10 @@ class Sharpness(_Enhance): enhancement factor of 0.0 gives a blurred image, a factor of 1.0 gives the original image, and a factor of 2.0 gives a sharpened image. """ + def __init__(self, image): self.image = image self.degenerate = image.filter(ImageFilter.SMOOTH) - if 'A' in image.getbands(): - self.degenerate.putalpha(image.getchannel('A')) + if "A" in image.getbands(): + self.degenerate.putalpha(image.getchannel("A")) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 458857f41cd..f281b9e14c4 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -27,42 +27,50 @@ # See the README file for information on usage and redistribution. # -from . import Image -from ._util import isPath import io -import os -import sys +import itertools import struct +import sys + +from . import Image +from ._util import is_path MAXBLOCK = 65536 -SAFEBLOCK = 1024*1024 +SAFEBLOCK = 1024 * 1024 LOAD_TRUNCATED_IMAGES = False +"""Whether or not to load truncated image files. User code may change this.""" ERRORS = { -1: "image buffer overrun error", -2: "decoding error", -3: "unknown error", -8: "bad configuration", - -9: "out of memory error" + -9: "out of memory error", } +""" +Dict of known error codes returned from :meth:`.PyDecoder.decode`, +:meth:`.PyEncoder.encode` :meth:`.PyEncoder.encode_to_pyfd` and +:meth:`.PyEncoder.encode_to_file`. +""" -def raise_ioerror(error): +# +# -------------------------------------------------------------------- +# Helpers + + +def raise_oserror(error): try: message = Image.core.getcodecstatus(error) except AttributeError: message = ERRORS.get(error) if not message: - message = "decoder error %d" % error - raise IOError(message + " when reading image file") + message = f"decoder error {error}" + raise OSError(message + " when reading image file") -# -# -------------------------------------------------------------------- -# Helpers - def _tilesort(t): # sort on offset return t[2] @@ -72,21 +80,26 @@ def _tilesort(t): # -------------------------------------------------------------------- # ImageFile base class + class ImageFile(Image.Image): - "Base class for image file format handlers." + """Base class for image file format handlers.""" def __init__(self, fp=None, filename=None): - Image.Image.__init__(self) + super().__init__() self._min_frame = 0 + self.custom_mimetype = None + self.tile = None + """ A list of tile descriptors, or ``None`` """ + self.readonly = 1 # until we know better self.decoderconfig = () self.decodermaxblock = MAXBLOCK - if isPath(fp): + if is_path(fp): # filename self.fp = open(fp, "rb") self.filename = fp @@ -99,27 +112,33 @@ def __init__(self, fp=None, filename=None): self._exclusive_fp = None try: - self._open() - except (IndexError, # end of data + try: + self._open() + except ( + IndexError, # end of data TypeError, # end of data (ord) KeyError, # unsupported mode EOFError, # got header but not the first frame - struct.error) as v: + struct.error, + ) as v: + 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") + except BaseException: # close the file only if we have opened it this constructor if self._exclusive_fp: self.fp.close() - raise SyntaxError(v) + raise - if not self.mode or self.size[0] <= 0: - raise SyntaxError("not identified by this driver") - - def draft(self, mode, size): - "Set draft mode" - - pass + def get_format_mimetype(self): + if self.custom_mimetype: + return self.custom_mimetype + if self.format is not None: + return Image.MIME.get(self.format.upper()) def verify(self): - "Check file integrity" + """Check file integrity""" # raise exception if something's wrong. must be called # directly after open, and closes file when finished. @@ -128,19 +147,19 @@ def verify(self): self.fp = None def load(self): - "Load image data based on tile list" - - pixel = Image.Image.load(self) + """Load image data based on tile list""" if self.tile is None: - raise IOError("cannot load this image") + raise OSError("cannot load this image") + + pixel = Image.Image.load(self) if not self.tile: return pixel self.map = None use_mmap = self.filename and len(self.tile) == 1 # As of pypy 2.1.0, memory mapping was failing here. - use_mmap = use_mmap and not hasattr(sys, 'pypy_version_info') + use_mmap = use_mmap and not hasattr(sys, "pypy_version_info") readonly = 0 @@ -161,34 +180,34 @@ def load(self): if use_mmap: # try memory mapping decoder_name, extents, offset, args = self.tile[0] - if decoder_name == "raw" and len(args) >= 3 and args[0] == self.mode \ - and args[0] in Image._MAPMODES: + if ( + decoder_name == "raw" + and len(args) >= 3 + and args[0] == self.mode + and args[0] in Image._MAPMODES + ): try: - if hasattr(Image.core, "map"): - # use built-in mapper WIN32 only - self.map = Image.core.map(self.filename) - self.map.seek(offset) - self.im = self.map.readimage( - self.mode, self.size, args[1], args[2] - ) - else: - # use mmap, if possible - import mmap - fp = open(self.filename, "r") - size = os.path.getsize(self.filename) - self.map = mmap.mmap(fp.fileno(), size, access=mmap.ACCESS_READ) - self.im = Image.core.map_buffer( - self.map, self.size, decoder_name, extents, offset, args - ) + # use mmap, if possible + import mmap + + with open(self.filename) as fp: + self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) + if offset + self.size[1] * args[1] > self.map.size(): + # buffer is not large enough + raise OSError + self.im = Image.core.map_buffer( + self.map, self.size, decoder_name, offset, args + ) readonly = 1 - # After trashing self.im, we might need to reload the palette data. + # After trashing self.im, + # we might need to reload the palette data. if self.palette: self.palette.dirty = 1 - except (AttributeError, EnvironmentError, ImportError): + except (AttributeError, OSError, ImportError): self.map = None self.load_prepare() - err_code = -3 # initialize to unknown error + err_code = -3 # initialize to unknown error if not self.map: # sort tiles in file order self.tile.sort(key=_tilesort) @@ -199,47 +218,52 @@ def load(self): except AttributeError: prefix = b"" + # Remove consecutive duplicates that only differ by their offset + self.tile = [ + list(tiles)[-1] + for _, tiles in itertools.groupby( + self.tile, lambda tile: (tile[0], tile[1], tile[3]) + ) + ] for decoder_name, extents, offset, args in self.tile: - decoder = Image._getdecoder(self.mode, decoder_name, - args, self.decoderconfig) seek(offset) - decoder.setimage(self.im, extents) - if decoder.pulls_fd: - decoder.setfd(self.fp) - status, err_code = decoder.decode(b"") - else: - b = prefix - while True: - try: - s = read(self.decodermaxblock) - except (IndexError, struct.error): # truncated png/gif - if LOAD_TRUNCATED_IMAGES: - break - else: - raise IOError("image file is truncated") - - if not s: # truncated jpeg - self.tile = [] - - # JpegDecode needs to clean things up here either way - # If we don't destroy the decompressor, - # we have a memory leak. - decoder.cleanup() - - if LOAD_TRUNCATED_IMAGES: + decoder = Image._getdecoder( + self.mode, decoder_name, args, self.decoderconfig + ) + try: + decoder.setimage(self.im, extents) + if decoder.pulls_fd: + decoder.setfd(self.fp) + err_code = decoder.decode(b"")[1] + else: + b = prefix + while True: + try: + s = read(self.decodermaxblock) + except (IndexError, struct.error) as e: + # truncated png/gif + if LOAD_TRUNCATED_IMAGES: + break + else: + raise OSError("image file is truncated") from e + + if not s: # truncated jpeg + if LOAD_TRUNCATED_IMAGES: + break + else: + raise OSError( + "image file is truncated " + f"({len(b)} bytes not processed)" + ) + + b = b + s + n, err_code = decoder.decode(b) + if n < 0: break - else: - raise IOError("image file is truncated " - "(%d bytes not processed)" % len(b)) - - b = b + s - n, err_code = decoder.decode(b) - if n < 0: - break - b = b[n:] - - # Need to cleanup here to prevent leaks in PyPy - decoder.cleanup() + b = b[n:] + finally: + # Need to cleanup here to prevent leaks + decoder.cleanup() self.tile = [] self.readonly = readonly @@ -252,14 +276,13 @@ def load(self): if not self.map and not LOAD_TRUNCATED_IMAGES and err_code < 0: # still raised if decoder fails to return anything - raise_ioerror(err_code) + raise_oserror(err_code) return Image.Image.load(self) def load_prepare(self): # create image memory if necessary - if not self.im or\ - self.im.mode != self.mode or self.im.size != self.size: + if not self.im or self.im.mode != self.mode or self.im.size != self.size: self.im = Image.core.new(self.mode, self.size) # create palette (optional) if self.mode == "P": @@ -278,11 +301,15 @@ def load_end(self): # pass def _seek_check(self, frame): - if (frame < self._min_frame or + if ( + frame < self._min_frame # Only check upper limit on frames if additional seek operations # are not required to do so - (not (hasattr(self, "_n_frames") and self._n_frames is None) and - frame >= self.n_frames+self._min_frame)): + or ( + not (hasattr(self, "_n_frames") and self._n_frames is None) + and frame >= self.n_frames + self._min_frame + ) + ): raise EOFError("attempt to seek outside sequence") return self.tell() != frame @@ -297,32 +324,30 @@ class StubImageFile(ImageFile): """ def _open(self): - raise NotImplementedError( - "StubImageFile subclass must implement _open" - ) + raise NotImplementedError("StubImageFile subclass must implement _open") def load(self): loader = self._load() if loader is None: - raise IOError("cannot find loader for this %s file" % self.format) + raise OSError(f"cannot find loader for this {self.format} file") image = loader.load(self) assert image is not None # become the other object (!) self.__class__ = image.__class__ self.__dict__ = image.__dict__ + return image.load() def _load(self): - "(Hook) Find actual image loader." - raise NotImplementedError( - "StubImageFile subclass must implement _load" - ) + """(Hook) Find actual image loader.""" + raise NotImplementedError("StubImageFile subclass must implement _load") -class Parser(object): +class Parser: """ Incremental image parser. This class implements the standard feed/close consumer interface. """ + incremental = None image = None data = None @@ -343,7 +368,7 @@ def feed(self, data): (Consumer) Feed data to the parser. :param data: A string buffer. - :exception IOError: If the parser failed to parse the image file. + :exception OSError: If the parser failed to parse the image file. """ # collect data @@ -375,7 +400,7 @@ def feed(self, data): if e < 0: # decoding error self.image = None - raise_ioerror(e) + raise_oserror(e) else: # end of image return @@ -394,7 +419,7 @@ def feed(self, data): try: with io.BytesIO(self.data) as fp: im = Image.open(fp) - except IOError: + except OSError: # traceback.print_exc() pass # not enough data else: @@ -407,15 +432,13 @@ def feed(self, data): im.load_prepare() d, e, o, a = im.tile[0] im.tile = [] - self.decoder = Image._getdecoder( - im.mode, d, a, im.decoderconfig - ) + self.decoder = Image._getdecoder(im.mode, d, a, im.decoderconfig) self.decoder.setimage(im.im, e) # calculate decoder offset self.offset = o if self.offset <= len(self.data): - self.data = self.data[self.offset:] + self.data = self.data[self.offset :] self.offset = 0 self.image = im @@ -431,7 +454,7 @@ def close(self): (Consumer) Close the stream. :returns: An image object. - :exception IOError: If the parser failed to parse the image file either + :exception OSError: If the parser failed to parse the image file either because it cannot be identified or cannot be decoded. """ @@ -441,9 +464,9 @@ def close(self): self.feed(b"") self.data = self.decoder = None if not self.finished: - raise IOError("image was incomplete") + raise OSError("image was incomplete") if not self.image: - raise IOError("cannot parse this image") + raise OSError("cannot parse this image") if self.data: # incremental parsing not possible; reopen the file # not that we have all data @@ -457,6 +480,7 @@ def close(self): # -------------------------------------------------------------------- + def _save(im, fp, tile, bufsize=0): """Helper to save image based on tile list @@ -475,50 +499,43 @@ def _save(im, fp, tile, bufsize=0): # But, it would need at least the image size in most cases. RawEncode is # a tricky case. bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c - if fp == sys.stdout: - fp.flush() - return try: fh = fp.fileno() fp.flush() - except (AttributeError, io.UnsupportedOperation): - # compress to Python file-compatible object - for e, b, o, a in tile: - e = Image._getencoder(im.mode, e, a, im.encoderconfig) - if o > 0: - fp.seek(o, 0) - e.setimage(im.im, b) - if e.pushes_fd: - e.setfd(fp) - l, s = e.encode_to_pyfd() - else: - while True: - l, s, d = e.encode(bufsize) - fp.write(d) - if s: - break - if s < 0: - raise IOError("encoder error %d when writing image file" % s) - e.cleanup() - else: - # slight speedup: compress to real file object - for e, b, o, a in tile: - e = Image._getencoder(im.mode, e, a, im.encoderconfig) - if o > 0: - fp.seek(o, 0) - e.setimage(im.im, b) - if e.pushes_fd: - e.setfd(fp) - l, s = e.encode_to_pyfd() - else: - s = e.encode_to_file(fh, bufsize) - if s < 0: - raise IOError("encoder error %d when writing image file" % s) - e.cleanup() + _encode_tile(im, fp, tile, bufsize, fh) + except (AttributeError, io.UnsupportedOperation) as exc: + _encode_tile(im, fp, tile, bufsize, None, exc) if hasattr(fp, "flush"): fp.flush() +def _encode_tile(im, fp, tile, bufsize, fh, exc=None): + for e, b, o, a in tile: + if o > 0: + fp.seek(o) + encoder = Image._getencoder(im.mode, e, a, im.encoderconfig) + try: + encoder.setimage(im.im, b) + if encoder.pushes_fd: + encoder.setfd(fp) + l, s = encoder.encode_to_pyfd() + else: + if exc: + # compress to Python file-compatible object + while True: + l, s, d = encoder.encode(bufsize) + fp.write(d) + if s: + break + else: + # 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 + finally: + encoder.cleanup() + + def _safe_read(fp, size): """ Reads large blocks in a safe way. Unlike fp.read(n), this function @@ -527,23 +544,32 @@ def _safe_read(fp, size): :param fp: File handle. Must implement a read method. :param size: Number of bytes to read. - :returns: A string containing up to size bytes of data. + :returns: A string containing size bytes of data. + + Raises an OSError if the file is truncated and the read cannot be completed + """ if size <= 0: return b"" if size <= SAFEBLOCK: - return fp.read(size) + data = fp.read(size) + if len(data) < size: + raise OSError("Truncated File Read") + return data data = [] - while size > 0: - block = fp.read(min(size, SAFEBLOCK)) + remaining_size = size + while remaining_size > 0: + block = fp.read(min(remaining_size, SAFEBLOCK)) if not block: break data.append(block) - size -= len(block) + remaining_size -= len(block) + if sum(len(d) for d in data) < size: + raise OSError("Truncated File Read") return b"".join(data) -class PyCodecState(object): +class PyCodecState: def __init__(self): self.xsize = 0 self.ysize = 0 @@ -551,20 +577,10 @@ def __init__(self): self.yoff = 0 def extents(self): - return (self.xoff, self.yoff, - self.xoff+self.xsize, self.yoff+self.ysize) - - -class PyDecoder(object): - """ - Python implementation of a format decoder. Override this class and - add the decoding logic in the `decode` method. - - See :ref:`Writing Your Own File Decoder in Python` - """ + return self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize - _pulls_fd = False +class PyCodec: def __init__(self, mode, *args): self.im = None self.state = PyCodecState() @@ -574,31 +590,16 @@ def __init__(self, mode, *args): def init(self, args): """ - Override to perform decoder specific initialization + Override to perform codec specific initialization :param args: Array of args items from the tile entry :returns: None """ self.args = args - @property - def pulls_fd(self): - return self._pulls_fd - - def decode(self, buffer): - """ - Override to perform the decoding process. - - :param buffer: A bytes object with the data to be decoded. If `handles_eof` - is set, then `buffer` will be empty and `self.fd` will be set. - :returns: A tuple of (bytes consumed, errcode). If finished with decoding - return <0 for the bytes consumed. Err codes are from `ERRORS` - """ - raise NotImplementedError() - def cleanup(self): """ - Override to perform decoder specific cleanup + Override to perform codec specific cleanup :returns: None """ @@ -606,16 +607,16 @@ def cleanup(self): def setfd(self, fd): """ - Called from ImageFile to set the python file-like object + Called from ImageFile to set the Python file-like object - :param fd: A python file-like object + :param fd: A Python file-like object :returns: None """ self.fd = fd def setimage(self, im, extents=None): """ - Called from ImageFile to set the core output image for the decoder + Called from ImageFile to set the core output image for the codec :param im: A core image object :param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle @@ -642,23 +643,51 @@ def setimage(self, im, extents=None): if self.state.xsize <= 0 or self.state.ysize <= 0: raise ValueError("Size cannot be negative") - if (self.state.xsize + self.state.xoff > self.im.size[0] or - self.state.ysize + self.state.yoff > self.im.size[1]): + 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") + +class PyDecoder(PyCodec): + """ + Python implementation of a format decoder. Override this class and + add the decoding logic in the :meth:`decode` method. + + See :ref:`Writing Your Own File Codec in Python` + """ + + _pulls_fd = False + + @property + def pulls_fd(self): + return self._pulls_fd + + def decode(self, buffer): + """ + Override to perform the decoding process. + + :param buffer: A bytes object with the data to be decoded. + :returns: A tuple of ``(bytes consumed, errcode)``. + If finished with decoding return -1 for the bytes consumed. + Err codes are from :data:`.ImageFile.ERRORS`. + """ + raise NotImplementedError() + def set_as_raw(self, data, rawmode=None): """ Convenience method to set the internal image from a stream of raw data :param data: Bytes to be set - :param rawmode: The rawmode to be used for the decoder. If not specified, - it will default to the mode of the image + :param rawmode: The rawmode to be used for the decoder. + If not specified, it will default to the mode of the image :returns: None """ if not rawmode: rawmode = self.mode - d = Image._getdecoder(self.mode, 'raw', (rawmode)) + d = Image._getdecoder(self.mode, "raw", rawmode) d.setimage(self.im, self.state.extents()) s = d.decode(data) @@ -666,3 +695,60 @@ def set_as_raw(self, data, rawmode=None): raise ValueError("not enough image data") if s[1] != 0: raise ValueError("cannot decode image data") + + +class PyEncoder(PyCodec): + """ + Python implementation of a format encoder. Override this class and + add the decoding logic in the :meth:`encode` method. + + See :ref:`Writing Your Own File Codec in Python` + """ + + _pushes_fd = False + + @property + def pushes_fd(self): + return self._pushes_fd + + def encode(self, bufsize): + """ + Override to perform the encoding process. + + :param bufsize: Buffer size. + :returns: A tuple of ``(bytes encoded, errcode, bytes)``. + If finished with encoding return 1 for the error code. + Err codes are from :data:`.ImageFile.ERRORS`. + """ + raise NotImplementedError() + + def encode_to_pyfd(self): + """ + If ``pushes_fd`` is ``True``, then this method will be used, + and ``encode()`` will only be called once. + + :returns: A tuple of ``(bytes consumed, errcode)``. + Err codes are from :data:`.ImageFile.ERRORS`. + """ + if not self.pushes_fd: + return 0, -8 # bad configuration + bytes_consumed, errcode, data = self.encode(0) + if data: + self.fd.write(data) + return bytes_consumed, errcode + + def encode_to_file(self, fh, bufsize): + """ + :param fh: File handle. + :param bufsize: Buffer size. + + :returns: If finished successfully, return 0. + Otherwise, return an error code. Err codes are from + :data:`.ImageFile.ERRORS`. + """ + errcode = 0 + while errcode == 0: + status, errcode, buf = self.encode(bufsize) + if status > 0: + fh.write(buf[status:]) + return errcode diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 100fea8bd28..e10c6fdf14d 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -14,11 +14,10 @@ # # See the README file for information on usage and redistribution. # - import functools -class Filter(object): +class Filter: pass @@ -26,7 +25,14 @@ class MultibandFilter(Filter): pass -class Kernel(MultibandFilter): +class BuiltinFilter(MultibandFilter): + def filter(self, image): + if image.mode == "P": + raise ValueError("cannot filter palette images") + return image.filter(*self.filterargs) + + +class Kernel(BuiltinFilter): """ Create a convolution kernel. The current version only supports 3x3 and 5x5 integer and floating point kernels. @@ -38,41 +44,34 @@ class Kernel(MultibandFilter): version, this must be (3,3) or (5,5). :param kernel: A sequence containing kernel weights. :param scale: Scale factor. If given, the result for each pixel is - divided by this value. the default is the sum of the + divided by this value. The default is the sum of the kernel weights. :param offset: Offset. If given, this value is added to the result, after it has been divided by the scale factor. """ + name = "Kernel" + def __init__(self, size, kernel, scale=None, offset=0): if scale is None: # default scale is sum of kernel - scale = functools.reduce(lambda a, b: a+b, kernel) + scale = functools.reduce(lambda a, b: a + b, kernel) if size[0] * size[1] != len(kernel): raise ValueError("not enough coefficients in kernel") self.filterargs = size, scale, offset, kernel - def filter(self, image): - if image.mode == "P": - raise ValueError("cannot filter palette images") - return image.filter(*self.filterargs) - - -class BuiltinFilter(Kernel): - def __init__(self): - pass - class RankFilter(Filter): """ Create a rank filter. The rank filter sorts all pixels in - a window of the given size, and returns the **rank**'th value. + a window of the given size, and returns the ``rank``'th value. :param size: The kernel size, in pixels. :param rank: What pixel value to pick. Use 0 for a min filter, ``size * size / 2`` for a median filter, ``size * size - 1`` for a max filter, etc. """ + name = "Rank" def __init__(self, size, rank): @@ -82,7 +81,7 @@ def __init__(self, size, rank): def filter(self, image): if image.mode == "P": raise ValueError("cannot filter palette images") - image = image.expand(self.size//2, self.size//2) + image = image.expand(self.size // 2, self.size // 2) return image.rankfilter(self.size, self.rank) @@ -93,11 +92,12 @@ class MedianFilter(RankFilter): :param size: The kernel size, in pixels. """ + name = "Median" def __init__(self, size=3): self.size = size - self.rank = size*size//2 + self.rank = size * size // 2 class MinFilter(RankFilter): @@ -107,6 +107,7 @@ class MinFilter(RankFilter): :param size: The kernel size, in pixels. """ + name = "Min" def __init__(self, size=3): @@ -121,22 +122,23 @@ class MaxFilter(RankFilter): :param size: The kernel size, in pixels. """ + name = "Max" def __init__(self, size=3): self.size = size - self.rank = size*size-1 + self.rank = size * size - 1 class ModeFilter(Filter): """ - Create a mode filter. Picks the most frequent pixel value in a box with the given size. Pixel values that occur only once or twice are ignored; if no pixel value occurs more than twice, the original pixel value is preserved. :param size: The kernel size, in pixels. """ + name = "Mode" def __init__(self, size=3): @@ -147,10 +149,13 @@ def filter(self, image): class GaussianBlur(MultibandFilter): - """Gaussian blur filter. + """Blurs the image with a sequence of extended box filters, which + approximates a Gaussian kernel. For details on accuracy see + - :param radius: Blur radius. + :param radius: Standard deviation of the Gaussian kernel. """ + name = "GaussianBlur" def __init__(self, radius=2): @@ -171,6 +176,7 @@ class BoxBlur(MultibandFilter): returns an identical image. Radius 1 takes 1 pixel in each direction, i.e. 9 pixels in total. """ + name = "BoxBlur" def __init__(self, radius): @@ -193,7 +199,8 @@ class UnsharpMask(MultibandFilter): .. _digital unsharp masking: https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking - """ + """ # noqa: E501 + name = "UnsharpMask" def __init__(self, radius=2, percent=150, threshold=3): @@ -207,93 +214,325 @@ def filter(self, image): class BLUR(BuiltinFilter): name = "Blur" + # fmt: off filterargs = (5, 5), 16, 0, ( - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 1, 1, 1, 1 - ) + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 1, + ) + # fmt: on class CONTOUR(BuiltinFilter): name = "Contour" + # fmt: off filterargs = (3, 3), 1, 255, ( -1, -1, -1, -1, 8, -1, - -1, -1, -1 - ) + -1, -1, -1, + ) + # fmt: on class DETAIL(BuiltinFilter): name = "Detail" + # fmt: off filterargs = (3, 3), 6, 0, ( - 0, -1, 0, + 0, -1, 0, -1, 10, -1, - 0, -1, 0 - ) + 0, -1, 0, + ) + # fmt: on class EDGE_ENHANCE(BuiltinFilter): name = "Edge-enhance" + # fmt: off filterargs = (3, 3), 2, 0, ( -1, -1, -1, -1, 10, -1, - -1, -1, -1 - ) + -1, -1, -1, + ) + # fmt: on class EDGE_ENHANCE_MORE(BuiltinFilter): name = "Edge-enhance More" + # fmt: off filterargs = (3, 3), 1, 0, ( -1, -1, -1, -1, 9, -1, - -1, -1, -1 - ) + -1, -1, -1, + ) + # fmt: on class EMBOSS(BuiltinFilter): name = "Emboss" + # fmt: off filterargs = (3, 3), 1, 128, ( - -1, 0, 0, - 0, 1, 0, - 0, 0, 0 - ) + -1, 0, 0, + 0, 1, 0, + 0, 0, 0, + ) + # fmt: on class FIND_EDGES(BuiltinFilter): name = "Find Edges" + # fmt: off filterargs = (3, 3), 1, 0, ( -1, -1, -1, -1, 8, -1, - -1, -1, -1 - ) + -1, -1, -1, + ) + # fmt: on + + +class SHARPEN(BuiltinFilter): + name = "Sharpen" + # fmt: off + filterargs = (3, 3), 16, 0, ( + -2, -2, -2, + -2, 32, -2, + -2, -2, -2, + ) + # fmt: on class SMOOTH(BuiltinFilter): name = "Smooth" + # fmt: off filterargs = (3, 3), 13, 0, ( - 1, 1, 1, - 1, 5, 1, - 1, 1, 1 - ) + 1, 1, 1, + 1, 5, 1, + 1, 1, 1, + ) + # fmt: on class SMOOTH_MORE(BuiltinFilter): name = "Smooth More" + # fmt: off filterargs = (5, 5), 100, 0, ( - 1, 1, 1, 1, 1, - 1, 5, 5, 5, 1, - 1, 5, 44, 5, 1, - 1, 5, 5, 5, 1, - 1, 1, 1, 1, 1 + 1, 1, 1, 1, 1, + 1, 5, 5, 5, 1, + 1, 5, 44, 5, 1, + 1, 5, 5, 5, 1, + 1, 1, 1, 1, 1, + ) + # fmt: on + + +class Color3DLUT(MultibandFilter): + """Three-dimensional color lookup table. + + Transforms 3-channel pixels using the values of the channels as coordinates + in the 3D lookup table and interpolating the nearest elements. + + This method allows you to apply almost any color transformation + in constant time by using pre-calculated decimated tables. + + .. versionadded:: 5.2.0 + + :param size: Size of the table. One int or tuple of (int, int, int). + Minimal size in any dimension is 2, maximum is 65. + :param table: Flat lookup table. A list of ``channels * size**3`` + float elements or a list of ``size**3`` channels-sized + tuples with floats. Channels are changed first, + then first dimension, then second, then third. + Value 0.0 corresponds lowest value of output, 1.0 highest. + :param channels: Number of channels in the table. Could be 3 or 4. + Default is 3. + :param target_mode: A mode for the result image. Should have not less + than ``channels`` channels. Default is ``None``, + which means that mode wouldn't be changed. + """ + + name = "Color 3D LUT" + + 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") + self.size = size = self._check_size(size) + self.channels = channels + self.mode = target_mode + + # Hidden flag `_copy_table=False` could be used to avoid extra copying + # of the table if the table is specially made for the constructor. + copy_table = kwargs.get("_copy_table", True) + items = size[0] * size[1] * size[2] + wrong_size = False + + numpy = None + if hasattr(table, "shape"): + try: + import numpy + except ImportError: # pragma: no cover + pass + + if numpy and isinstance(table, numpy.ndarray): + if copy_table: + table = table.copy() + + if table.shape in [ + (items * channels,), + (items, channels), + (size[2], size[1], size[0], channels), + ]: + table = table.reshape(items * channels) + else: + wrong_size = True + + else: + if copy_table: + table = list(table) + + # Convert to a flat list + if table and isinstance(table[0], (list, tuple)): + table, raw_table = [], table + for pixel in raw_table: + if len(pixel) != channels: + raise ValueError( + "The elements of the table should " + "have a length of {}.".format(channels) + ) + table.extend(pixel) + + if wrong_size or len(table) != items * channels: + raise ValueError( + "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)}" + ) + self.table = table + + @staticmethod + def _check_size(size): + try: + _, _, _ = size + except ValueError as e: + raise ValueError( + "Size should be either an integer or a tuple of three integers." + ) 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.") + return size + + @classmethod + def generate(cls, size, callback, channels=3, target_mode=None): + """Generates new LUT using provided callback. + + :param size: Size of the table. Passed to the constructor. + :param callback: Function with three parameters which correspond + three color channels. Will be called ``size**3`` + times with values from 0.0 to 1.0 and should return + a tuple with ``channels`` elements. + :param channels: The number of channels which should return callback. + :param target_mode: Passed to the constructor of the resulting + lookup table. + """ + 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") + + table = [0] * (size_1d * size_2d * size_3d * channels) + idx_out = 0 + for b in range(size_3d): + for g in range(size_2d): + for r in range(size_1d): + table[idx_out : idx_out + channels] = callback( + r / (size_1d - 1), g / (size_2d - 1), b / (size_3d - 1) + ) + idx_out += channels + + return cls( + (size_1d, size_2d, size_3d), + table, + channels=channels, + target_mode=target_mode, + _copy_table=False, ) + def transform(self, callback, with_normals=False, channels=None, target_mode=None): + """Transforms the table values using provided callback and returns + a new LUT with altered values. + + :param callback: A function which takes old lookup table values + and returns a new set of values. The number + of arguments which function should take is + ``self.channels`` or ``3 + self.channels`` + if ``with_normals`` flag is set. + Should return a tuple of ``self.channels`` or + ``channels`` elements if it is set. + :param with_normals: If true, ``callback`` will be called with + coordinates in the color cube as the first + three arguments. Otherwise, ``callback`` + will be called only with actual color values. + :param channels: The number of channels in the resulting lookup table. + :param target_mode: Passed to the constructor of the resulting + lookup table. + """ + if channels not in (None, 3, 4): + raise ValueError("Only 3 or 4 output channels are supported") + ch_in = self.channels + ch_out = channels or ch_in + size_1d, size_2d, size_3d = self.size + + table = [0] * (size_1d * size_2d * size_3d * ch_out) + idx_in = 0 + idx_out = 0 + for b in range(size_3d): + for g in range(size_2d): + for r in range(size_1d): + values = self.table[idx_in : idx_in + ch_in] + if with_normals: + values = callback( + r / (size_1d - 1), + g / (size_2d - 1), + b / (size_3d - 1), + *values, + ) + else: + values = callback(*values) + table[idx_out : idx_out + ch_out] = values + idx_in += ch_in + idx_out += ch_out + + return type(self)( + self.size, + table, + channels=ch_out, + target_mode=target_mode or self.mode, + _copy_table=False, + ) -class SHARPEN(BuiltinFilter): - name = "Sharpen" - filterargs = (3, 3), 16, 0, ( - -2, -2, -2, - -2, 32, -2, - -2, -2, -2 + def __repr__(self): + r = [ + f"{self.__class__.__name__} from {self.table.__class__.__name__}", + "size={:d}x{:d}x{:d}".format(*self.size), + f"channels={self.channels:d}", + ] + if self.mode: + r.append(f"target_mode={self.mode}") + return "<{}>".format(" ".join(r)) + + def filter(self, image): + from . import Image + + return image.color_lut_3d( + self.mode or image.mode, + Image.Resampling.BILINEAR, + self.channels, + self.size[0], + self.size[1], + self.size[2], + self.table, ) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a162b8ba650..457e906c872 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -25,15 +25,34 @@ # See the README file for information on usage and redistribution. # -from . import Image -from ._util import isDirectory, isPath +import base64 import os import sys +import warnings +from enum import IntEnum +from io import BytesIO + +from . import Image +from ._deprecate import deprecate +from ._util import is_directory, is_path + + +class Layout(IntEnum): + BASIC = 0 + RAQM = 1 + -LAYOUT_BASIC = 0 -LAYOUT_RAQM = 1 +def __getattr__(name): + for enum, prefix in {Layout: "LAYOUT_"}.items(): + if name.startswith(prefix): + name = name[len(prefix) :] + 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}'") -class _imagingft_not_installed(object): + +class _ImagingFtNotInstalled: # module placeholder def __getattr__(self, id): raise ImportError("The _imagingft C module is not installed") @@ -42,7 +61,10 @@ def __getattr__(self, id): try: from . import _imagingft as core except ImportError: - core = _imagingft_not_installed() + core = _ImagingFtNotInstalled() + + +_UNSPECIFIED = object() # FIXME: add support for pilfont2 format (see FontFile.py) @@ -61,27 +83,33 @@ def __getattr__(self, id): # -------------------------------------------------------------------- -class ImageFont(object): - "PIL font wrapper" +class ImageFont: + """PIL font wrapper""" def _load_pilfont(self, filename): with open(filename, "rb") as fp: + image = None for ext in (".png", ".gif", ".pbm"): + if image: + image.close() try: fullname = os.path.splitext(filename)[0] + ext image = Image.open(fullname) - except: + except Exception: pass else: if image and image.mode in ("1", "L"): break else: - raise IOError("cannot find glyph data file") + if image: + image.close() + raise OSError("cannot find glyph data file") self.file = fullname - return self._load_pilfont_data(fp, image) + self._load_pilfont_data(fp, image) + image.close() def _load_pilfont_data(self, file, image): @@ -97,7 +125,7 @@ def _load_pilfont_data(self, file, image): self.info.append(s) # read PILfont metrics - data = file.read(256*20) + data = file.read(256 * 20) # check image if image.mode not in ("1", "L"): @@ -108,21 +136,79 @@ def _load_pilfont_data(self, file, image): self.font = Image.core.font(image.im, data) def getsize(self, text, *args, **kwargs): + """ + .. deprecated:: 9.2.0 + + Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. + + See :ref:`deprecations ` for more information. + + Returns width and height (in pixels) of given text. + + :param text: Text to measure. + + :return: (width, height) + """ + deprecate("getsize", 10, "getbbox or getlength") return self.font.getsize(text) def getmask(self, text, mode="", *args, **kwargs): + """ + Create a bitmap for the text. + + If the font uses antialiasing, the bitmap should have mode ``L`` and use a + maximum value of 255. Otherwise, it should have mode ``1``. + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + .. versionadded:: 1.1.5 + + :return: An internal PIL storage memory instance as defined by the + :py:mod:`PIL.Image.core` interface module. + """ return self.font.getmask(text, mode) + def getbbox(self, text, *args, **kwargs): + """ + Returns bounding box (in pixels) of given text. + + .. versionadded:: 9.2.0 + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + :return: ``(left, top, right, bottom)`` bounding box + """ + width, height = self.font.getsize(text) + return 0, 0, width, height + + def getlength(self, text, *args, **kwargs): + """ + Returns length (in pixels) of given text. + This is the amount by which following text should be offset. + + .. versionadded:: 9.2.0 + """ + width, height = self.font.getsize(text) + return width + ## # Wrapper for FreeType fonts. Application code should use the # truetype factory function to create font objects. -class FreeTypeFont(object): - "FreeType font wrapper (requires _imagingft service)" - def __init__(self, font=None, size=10, index=0, encoding="", - layout_engine=None): +class FreeTypeFont: + """FreeType font wrapper (requires _imagingft service)""" + + def __init__(self, font=None, size=10, index=0, encoding="", layout_engine=None): # FIXME: use service provider instead self.path = font @@ -130,46 +216,552 @@ def __init__(self, font=None, size=10, index=0, encoding="", self.index = index self.encoding = encoding - if layout_engine not in (LAYOUT_BASIC, LAYOUT_RAQM): - layout_engine = LAYOUT_BASIC + if layout_engine not in (Layout.BASIC, Layout.RAQM): + layout_engine = Layout.BASIC if core.HAVE_RAQM: - layout_engine = LAYOUT_RAQM - if layout_engine == LAYOUT_RAQM and not core.HAVE_RAQM: - layout_engine = LAYOUT_BASIC + layout_engine = Layout.RAQM + elif layout_engine == Layout.RAQM and not core.HAVE_RAQM: + warnings.warn( + "Raqm layout was requested, but Raqm is not available. " + "Falling back to basic layout." + ) + layout_engine = Layout.BASIC self.layout_engine = layout_engine - if isPath(font): - self.font = core.getfont(font, size, index, encoding, layout_engine=layout_engine) - else: - self.font_bytes = font.read() + def load_from_bytes(f): + self.font_bytes = f.read() + self.font = core.getfont( + "", size, index, encoding, self.font_bytes, layout_engine + ) + + if is_path(font): + if sys.platform == "win32": + font_bytes_path = font if isinstance(font, bytes) else font.encode() + try: + font_bytes_path.decode("ascii") + except UnicodeDecodeError: + # FreeType cannot load fonts with non-ASCII characters on Windows + # So load it into memory first + with open(font, "rb") as f: + load_from_bytes(f) + return self.font = core.getfont( - "", size, index, encoding, self.font_bytes, layout_engine) + font, size, index, encoding, layout_engine=layout_engine + ) + else: + load_from_bytes(font) + + def __getstate__(self): + return [self.path, self.size, self.index, self.encoding, self.layout_engine] + + def __setstate__(self, state): + path, size, index, encoding, layout_engine = state + self.__init__(path, size, index, encoding, layout_engine) + + def _multiline_split(self, text): + split_character = "\n" if isinstance(text, str) else b"\n" + return text.split(split_character) def getname(self): + """ + :return: A tuple of the font family (e.g. Helvetica) and the font style + (e.g. Bold) + """ return self.font.family, self.font.style def getmetrics(self): + """ + :return: A tuple of the font ascent (the distance from the baseline to + the highest outline point) and descent (the distance from the + baseline to the lowest outline point, a negative value) + """ return self.font.ascent, self.font.descent - def getsize(self, text, direction=None, features=None): - size, offset = self.font.getsize(text, direction, features) - return (size[0] + offset[0], size[1] + offset[1]) + def getlength(self, text, mode="", direction=None, features=None, language=None): + """ + Returns length (in pixels with 1/64 precision) of given text when rendered + in font with provided direction, features, and language. + + This is the amount by which following text should be offset. + Text bounding box may extend past the length in some fonts, + e.g. when using italics or accents. + + The result is returned as a float; it is a whole number if using basic layout. + + Note that the sum of two lengths may not equal the length of a concatenated + string due to kerning. If you need to adjust for kerning, include the following + character and subtract its length. + + For example, instead of + + .. code-block:: python + + hello = font.getlength("Hello") + world = font.getlength("World") + hello_world = hello + world # not adjusted for kerning + assert hello_world == font.getlength("HelloWorld") # may fail + + use + + .. code-block:: python + + hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning + world = font.getlength("World") + hello_world = hello + world # adjusted for kerning + assert hello_world == font.getlength("HelloWorld") # True + + or disable kerning with (requires libraqm) + + .. code-block:: python + + hello = draw.textlength("Hello", font, features=["-kern"]) + world = draw.textlength("World", font, features=["-kern"]) + hello_world = hello + world # kerning is disabled, no need to adjust + assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"]) + + .. versionadded:: 8.0.0 + + :param text: Text to measure. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + :return: Width for horizontal, height for vertical text. + """ + return self.font.getlength(text, mode, direction, features, language) / 64 + + def getbbox( + self, + text, + mode="", + direction=None, + features=None, + language=None, + stroke_width=0, + anchor=None, + ): + """ + Returns bounding box (in pixels) of given text relative to given anchor + when rendered in font with provided direction, features, and language. + + Use :py:meth:`getlength()` to get the offset of following text with + 1/64 pixel precision. The bounding box includes extra margins for + some fonts, e.g. italics or accents. + + .. versionadded:: 8.0.0 + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + :param stroke_width: The width of the text stroke. + + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. + + :return: ``(left, top, right, bottom)`` bounding box + """ + size, offset = self.font.getsize( + text, mode, direction, features, language, anchor + ) + left, top = offset[0] - stroke_width, offset[1] - stroke_width + width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width + return left, top, left + width, top + height + + def getsize( + self, + text, + direction=None, + features=None, + language=None, + stroke_width=0, + ): + """ + .. deprecated:: 9.2.0 + + Use :py:meth:`getlength()` to measure the offset of following text with + 1/64 pixel precision. + Use :py:meth:`getbbox()` to get the exact bounding box based on an anchor. + + See :ref:`deprecations ` for more information. + + Returns width and height (in pixels) of given text if rendered in font with + provided direction, features, and language. + + .. note:: For historical reasons this function measures text height from + the ascender line instead of the top, see :ref:`text-anchors`. + If you wish to measure text height from the top, it is recommended + to use the bottom value of :meth:`getbbox` with ``anchor='lt'`` instead. + + :param text: Text to measure. + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :return: (width, height) + """ + deprecate("getsize", 10, "getbbox or getlength") + # vertical offset is added for historical reasons + # see https://github.com/python-pillow/Pillow/pull/4910#discussion_r486682929 + size, offset = self.font.getsize(text, "L", direction, features, language) + return ( + size[0] + stroke_width * 2, + size[1] + stroke_width * 2 + offset[1], + ) + + def getsize_multiline( + self, + text, + direction=None, + spacing=4, + features=None, + language=None, + stroke_width=0, + ): + """ + .. deprecated:: 9.2.0 + + Use :py:meth:`.ImageDraw.multiline_textbbox` instead. + + See :ref:`deprecations ` for more information. + + Returns width and height (in pixels) of given text if rendered in font + with provided direction, features, and language, while respecting + newline characters. + + :param text: Text to measure. + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + :param spacing: The vertical gap between lines, defaulting to 4 pixels. + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :return: (width, height) + """ + deprecate("getsize_multiline", 10, "ImageDraw.multiline_textbbox") + max_width = 0 + lines = self._multiline_split(text) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + line_spacing = self.getsize("A", stroke_width=stroke_width)[1] + spacing + for line in lines: + line_width, line_height = self.getsize( + line, direction, features, language, stroke_width + ) + max_width = max(max_width, line_width) + + return max_width, len(lines) * line_spacing - spacing def getoffset(self, text): + """ + .. deprecated:: 9.2.0 + + Use :py:meth:`.getbbox` instead. + + See :ref:`deprecations ` for more information. + + Returns the offset of given text. This is the gap between the + starting coordinate and the first marking. Note that this gap is + included in the result of :py:func:`~PIL.ImageFont.FreeTypeFont.getsize`. + + :param text: Text to measure. + + :return: A tuple of the x and y offset + """ + deprecate("getoffset", 10, "getbbox") return self.font.getsize(text)[1] - def getmask(self, text, mode="", direction=None, features=None): - return self.getmask2(text, mode, direction=direction, features=features)[0] + def getmask( + self, + text, + mode="", + direction=None, + features=None, + language=None, + stroke_width=0, + anchor=None, + ink=0, + ): + """ + Create a bitmap for the text. + + If the font uses antialiasing, the bitmap should have mode ``L`` and use a + maximum value of 255. If the font has embedded color data, the bitmap + should have mode ``RGBA``. Otherwise, it should have mode ``1``. + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + .. versionadded:: 1.1.5 + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. + + .. versionadded:: 8.0.0 + + :param ink: Foreground ink for rendering in RGBA mode. + + .. versionadded:: 8.0.0 + + :return: An internal PIL storage memory instance as defined by the + :py:mod:`PIL.Image.core` interface module. + """ + return self.getmask2( + text, + mode, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + anchor=anchor, + ink=ink, + )[0] + + def getmask2( + self, + text, + mode="", + fill=_UNSPECIFIED, + direction=None, + features=None, + language=None, + stroke_width=0, + anchor=None, + ink=0, + *args, + **kwargs, + ): + """ + Create a bitmap for the text. + + If the font uses antialiasing, the bitmap should have mode ``L`` and use a + maximum value of 255. If the font has embedded color data, the bitmap + should have mode ``RGBA``. Otherwise, it should have mode ``1``. + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + .. versionadded:: 1.1.5 + + :param fill: Optional fill function. By default, an internal Pillow function + will be used. + + Deprecated. This parameter will be removed in Pillow 10 + (2023-07-01). - def getmask2(self, text, mode="", fill=Image.core.fill, direction=None, features=None, *args, **kwargs): - size, offset = self.font.getsize(text, direction, features) - im = fill("L", size, 0) - self.font.render(text, im.id, mode == "1", direction, features) + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. + + .. versionadded:: 8.0.0 + + :param ink: Foreground ink for rendering in RGBA mode. + + .. versionadded:: 8.0.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 + """ + if fill is _UNSPECIFIED: + fill = Image.core.fill + else: + deprecate("fill", 10) + size, offset = self.font.getsize( + text, mode, direction, features, language, anchor + ) + size = size[0] + stroke_width * 2, size[1] + stroke_width * 2 + offset = offset[0] - stroke_width, offset[1] - stroke_width + Image._decompression_bomb_check(size) + im = fill("RGBA" if mode == "RGBA" else "L", size, 0) + self.font.render( + text, im.id, mode, direction, features, language, stroke_width, ink + ) return im, offset - def font_variant(self, font=None, size=None, index=None, encoding=None, - layout_engine=None): + def font_variant( + self, font=None, size=None, index=None, encoding=None, layout_engine=None + ): """ Create a copy of this FreeTypeFont object, using any specified arguments to override the settings. @@ -179,16 +771,75 @@ def font_variant(self, font=None, size=None, index=None, encoding=None, :return: A FreeTypeFont object. """ - return FreeTypeFont(font=self.path if font is None else font, - size=self.size if size is None else size, - index=self.index if index is None else index, - encoding=self.encoding if encoding is None else encoding, - layout_engine=self.layout_engine if layout_engine is None else layout_engine - ) + if font is None: + try: + font = BytesIO(self.font_bytes) + except AttributeError: + font = self.path + return FreeTypeFont( + font=font, + size=self.size if size is None else size, + index=self.index if index is None else index, + encoding=self.encoding if encoding is None else encoding, + layout_engine=layout_engine or self.layout_engine, + ) + + def get_variation_names(self): + """ + :returns: A list of the named styles in a variation font. + :exception OSError: If the font is not a variation font. + """ + try: + names = self.font.getvarnames() + except AttributeError as e: + raise NotImplementedError("FreeType 2.9.1 or greater is required") from e + return [name.replace(b"\x00", b"") for name in names] + def set_variation_by_name(self, name): + """ + :param name: The name of the style. + :exception OSError: If the font is not a variation font. + """ + names = self.get_variation_names() + if not isinstance(name, bytes): + name = name.encode() + index = names.index(name) + 1 + + if index == getattr(self, "_last_variation_index", None): + # When the same name is set twice in a row, + # there is an 'unknown freetype error' + # https://savannah.nongnu.org/bugs/?56186 + return + self._last_variation_index = index -class TransposedFont(object): - "Wrapper for writing rotated or mirrored text" + self.font.setvarname(index) + + def get_variation_axes(self): + """ + :returns: A list of the axes in a variation font. + :exception OSError: If the font is not a variation font. + """ + try: + axes = self.font.getvaraxes() + except AttributeError as e: + raise NotImplementedError("FreeType 2.9.1 or greater is required") from e + for axis in axes: + axis["name"] = axis["name"].replace(b"\x00", b"") + return axes + + def set_variation_by_axes(self, axes): + """ + :param axes: A list of values for each axis. + :exception OSError: If the font is not a variation font. + """ + try: + self.font.setvaraxes(axes) + except AttributeError as e: + raise NotImplementedError("FreeType 2.9.1 or greater is required") from e + + +class TransposedFont: + """Wrapper for writing rotated or mirrored text""" def __init__(self, font, orientation=None): """ @@ -197,15 +848,26 @@ def __init__(self, font, orientation=None): :param font: A font object. :param orientation: An optional orientation. If given, this should - be one of Image.FLIP_LEFT_RIGHT, Image.FLIP_TOP_BOTTOM, - Image.ROTATE_90, Image.ROTATE_180, or Image.ROTATE_270. + be one of Image.Transpose.FLIP_LEFT_RIGHT, Image.Transpose.FLIP_TOP_BOTTOM, + Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_180, or + Image.Transpose.ROTATE_270. """ self.font = font self.orientation = orientation # any 'transpose' argument, or None def getsize(self, text, *args, **kwargs): - w, h = self.font.getsize(text) - if self.orientation in (Image.ROTATE_90, Image.ROTATE_270): + """ + .. deprecated:: 9.2.0 + + Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. + + See :ref:`deprecations ` for more information. + """ + deprecate("getsize", 10, "getbbox or getlength") + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + w, h = self.font.getsize(text) + if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): return h, w return w, h @@ -215,6 +877,23 @@ def getmask(self, text, mode="", *args, **kwargs): return im.transpose(self.orientation) return im + def getbbox(self, text, *args, **kwargs): + # TransposedFont doesn't support getmask2, move top-left point to (0, 0) + # this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont + left, top, right, bottom = self.font.getbbox(text, *args, **kwargs) + width = right - left + height = bottom - top + if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): + return 0, 0, height, width + return 0, 0, width, height + + 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" + ) + return self.font.getlength(text, *args, **kwargs) + def load(filename): """ @@ -223,42 +902,81 @@ def load(filename): :param filename: Name of font file. :return: A font object. - :exception IOError: If the file could not be read. + :exception OSError: If the file could not be read. """ f = ImageFont() f._load_pilfont(filename) return f -def truetype(font=None, size=10, index=0, encoding="", - layout_engine=None): +def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): """ Load a TrueType or OpenType font from a file or file-like object, and create a font object. This function loads a font object from the given file or file-like object, and creates a font object for a font of the given size. + Pillow uses FreeType to open font files. On Windows, be aware that FreeType + will keep the file open as long as the FreeTypeFont object exists. Windows + limits the number of files that can be open in C at once to 512, so if many + fonts are opened simultaneously and that limit is approached, an + ``OSError`` may be thrown, reporting that FreeType "cannot open resource". + A workaround would be to copy the file(s) into memory, and open that instead. + This function requires the _imagingft service. :param font: A filename or file-like object containing a TrueType font. - Under Windows, if the file is not found in this filename, - the loader also looks in Windows :file:`fonts/` directory. - :param size: The requested size, in points. + If the file is not found in this filename, the loader may also + search in other directories, such as the :file:`fonts/` + directory on Windows or :file:`/Library/Fonts/`, + :file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/` on + macOS. + + :param size: The requested size, in pixels. :param index: Which font face to load (default is first available face). - :param encoding: Which font encoding to use (default is Unicode). Common - encodings are "unic" (Unicode), "symb" (Microsoft - Symbol), "ADOB" (Adobe Standard), "ADBE" (Adobe Expert), - and "armn" (Apple Roman). See the FreeType documentation - for more information. + :param encoding: Which font encoding to use (default is Unicode). Possible + encodings include (see the FreeType documentation for more + information): + + * "unic" (Unicode) + * "symb" (Microsoft Symbol) + * "ADOB" (Adobe Standard) + * "ADBE" (Adobe Expert) + * "ADBC" (Adobe Custom) + * "armn" (Apple Roman) + * "sjis" (Shift JIS) + * "gb " (PRC) + * "big5" + * "wans" (Extended Wansung) + * "joha" (Johab) + * "lat1" (Latin-1) + + This specifies the character set to use. It does not alter the + encoding of any text provided in subsequent operations. :param layout_engine: Which layout engine to use, if available: - `ImageFont.LAYOUT_BASIC` or `ImageFont.LAYOUT_RAQM`. + :data:`.ImageFont.Layout.BASIC` or :data:`.ImageFont.Layout.RAQM`. + If it is available, Raqm layout will be used by default. + Otherwise, basic layout will be used. + + Raqm layout is recommended for all non-English text. If Raqm layout + is not required, basic layout will have better performance. + + You can check support for Raqm layout using + :py:func:`PIL.features.check_feature` with ``feature="raqm"``. + + .. versionadded:: 4.2.0 :return: A font object. - :exception IOError: If the file could not be read. + :exception OSError: If the file could not be read. """ - try: + def freetype(font): return FreeTypeFont(font, size, index, encoding, layout_engine) - except IOError: + + try: + return freetype(font) + except OSError: + if not is_path(font): + raise ttf_filename = os.path.basename(font) dirs = [] @@ -269,17 +987,19 @@ def truetype(font=None, size=10, index=0, encoding="", windir = os.environ.get("WINDIR") if windir: dirs.append(os.path.join(windir, "fonts")) - elif sys.platform in ('linux', 'linux2'): + elif sys.platform in ("linux", "linux2"): lindirs = os.environ.get("XDG_DATA_DIRS", "") if not lindirs: # According to the freedesktop spec, XDG_DATA_DIRS should # default to /usr/share - lindirs = '/usr/share' - dirs += [os.path.join(lindir, "fonts") - for lindir in lindirs.split(":")] - elif sys.platform == 'darwin': - dirs += ['/Library/Fonts', '/System/Library/Fonts', - os.path.expanduser('~/Library/Fonts')] + lindirs = "/usr/share" + dirs += [os.path.join(lindir, "fonts") for lindir in lindirs.split(":")] + elif sys.platform == "darwin": + dirs += [ + "/Library/Fonts", + "/System/Library/Fonts", + os.path.expanduser("~/Library/Fonts"), + ] ext = os.path.splitext(ttf_filename)[1] first_font_with_a_different_extension = None @@ -287,17 +1007,15 @@ def truetype(font=None, size=10, index=0, encoding="", for walkroot, walkdir, walkfilenames in os.walk(directory): for walkfilename in walkfilenames: if ext and walkfilename == ttf_filename: - fontpath = os.path.join(walkroot, walkfilename) - return FreeTypeFont(fontpath, size, index, encoding, layout_engine) + return freetype(os.path.join(walkroot, walkfilename)) elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename: fontpath = os.path.join(walkroot, walkfilename) - if os.path.splitext(fontpath)[1] == '.ttf': - return FreeTypeFont(fontpath, size, index, encoding, layout_engine) + if os.path.splitext(fontpath)[1] == ".ttf": + return freetype(fontpath) if not ext and first_font_with_a_different_extension is None: first_font_with_a_different_extension = fontpath if first_font_with_a_different_extension: - return FreeTypeFont(first_font_with_a_different_extension, size, - index, encoding, layout_engine) + return freetype(first_font_with_a_different_extension) raise @@ -308,20 +1026,17 @@ def load_path(filename): :param filename: Name of font file. :return: A font object. - :exception IOError: If the file could not be read. + :exception OSError: If the file could not be read. """ for directory in sys.path: - if isDirectory(directory): + if is_directory(directory): if not isinstance(filename, str): - if bytes is str: - filename = filename.encode("utf-8") - else: - filename = filename.decode("utf-8") + filename = filename.decode("utf-8") try: return load(os.path.join(directory, filename)) - except IOError: + except OSError: pass - raise IOError("cannot find font file") + raise OSError("cannot find font file") def load_default(): @@ -331,12 +1046,12 @@ def load_default(): :return: A font object. """ - from io import BytesIO - import base64 f = ImageFont() f._load_pilfont_data( # courB08 - BytesIO(base64.b64decode(b''' + BytesIO( + base64.b64decode( + b""" UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA @@ -428,7 +1143,13 @@ def load_default(): pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA//// +QAGAAIAzgAKANUAEw== -''')), Image.open(BytesIO(base64.b64decode(b''' +""" + ) + ), + Image.open( + BytesIO( + base64.b64decode( + b""" iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9 M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g @@ -452,5 +1173,9 @@ def load_default(): AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v// Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR w7IkEbzhVQAAAABJRU5ErkJggg== -''')))) +""" + ) + ) + ), + ) return f diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 712b02cd0d9..38074cb1b0d 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -2,7 +2,7 @@ # The Python Imaging Library # $Id$ # -# screen grabber (macOS and Windows only) +# screen grabber # # History: # 2001-04-26 fl created @@ -15,35 +15,69 @@ # See the README file for information on usage and redistribution. # -from . import Image - +import os +import shutil +import subprocess import sys -if sys.platform not in ["win32", "darwin"]: - raise ImportError("ImageGrab is macOS and Windows only") +import tempfile -if sys.platform == "win32": - grabber = Image.core.grabscreen -elif sys.platform == "darwin": - import os - import tempfile - import subprocess +from . import Image -def grab(bbox=None): - if sys.platform == "darwin": - fh, filepath = tempfile.mkstemp('.png') - os.close(fh) - subprocess.call(['screencapture', '-x', filepath]) - im = Image.open(filepath) - im.load() - os.unlink(filepath) - else: - size, data = grabber() - im = Image.frombytes( - "RGB", size, data, - # RGB, 32-bit line padding, origin lower left corner - "raw", "BGR", (size[0]*3 + 3) & -4, -1 +def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None): + if xdisplay is None: + if sys.platform == "darwin": + fh, filepath = tempfile.mkstemp(".png") + os.close(fh) + args = ["screencapture"] + if bbox: + left, top, right, bottom = bbox + args += ["-R", f"{left},{top},{right-left},{bottom-top}"] + subprocess.call(args + ["-x", filepath]) + im = Image.open(filepath) + im.load() + os.unlink(filepath) + if bbox: + im_resized = im.resize((right - left, bottom - top)) + im.close() + return im_resized + return im + elif sys.platform == "win32": + offset, size, data = Image.core.grabscreen_win32( + include_layered_windows, all_screens ) + im = Image.frombytes( + "RGB", + size, + data, + # RGB, 32-bit line padding, origin lower left corner + "raw", + "BGR", + (size[0] * 3 + 3) & -4, + -1, + ) + if bbox: + x0, y0 = offset + left, top, right, bottom = bbox + im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) + return im + elif shutil.which("gnome-screenshot"): + fh, filepath = tempfile.mkstemp(".png") + os.close(fh) + subprocess.call(["gnome-screenshot", "-f", filepath]) + im = Image.open(filepath) + im.load() + os.unlink(filepath) + if bbox: + im_cropped = im.crop(bbox) + im.close() + return im_cropped + 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") + size, data = Image.core.grabscreen_x11(xdisplay) + im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) if bbox: im = im.crop(bbox) return im @@ -51,14 +85,16 @@ def grab(bbox=None): def grabclipboard(): if sys.platform == "darwin": - fh, filepath = tempfile.mkstemp('.jpg') + fh, filepath = tempfile.mkstemp(".jpg") os.close(fh) commands = [ - "set theFile to (open for access POSIX file \""+filepath+"\" with write permission)", + 'set theFile to (open for access POSIX file "' + + filepath + + '" with write permission)', "try", - "write (the clipboard as JPEG picture) to theFile", + " write (the clipboard as JPEG picture) to theFile", "end try", - "close access theFile" + "close access theFile", ] script = ["osascript"] for command in commands: @@ -71,10 +107,29 @@ def grabclipboard(): im.load() os.unlink(filepath) return im - else: - data = Image.core.grabclipboard() + elif sys.platform == "win32": + fmt, data = Image.core.grabclipboard_win32() + if fmt == "file": # CF_HDROP + import struct + + o = struct.unpack_from("I", data)[0] + if data[16] != 0: + files = data[o:].decode("utf-16le").split("\0") + else: + files = data[o:].decode("mbcs").split("\0") + return files[: files.index("")] if isinstance(data, bytes): - from . import BmpImagePlugin import io - return BmpImagePlugin.DibImageFile(io.BytesIO(data)) - return data + + data = io.BytesIO(data) + if fmt == "png": + from . import PngImagePlugin + + return PngImagePlugin.PngImageFile(data) + elif fmt == "DIB": + from . import BmpImagePlugin + + return BmpImagePlugin.DibImageFile(data) + return None + else: + raise NotImplementedError("ImageGrab.grabclipboard() is macOS and Windows only") diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 2ccd1891bb2..09d9898d750 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -15,22 +15,16 @@ # See the README file for information on usage and redistribution. # -from . import Image, _imagingmath - -try: - import builtins -except ImportError: - import __builtin__ - builtins = __builtin__ +import builtins -VERBOSE = 0 +from . import Image, _imagingmath def _isconstant(v): - return isinstance(v, int) or isinstance(v, float) + return isinstance(v, (int, float)) -class _Operand(object): +class _Operand: """Wraps an image operand, providing standard operators""" def __init__(self, im): @@ -45,7 +39,7 @@ def __fixup(self, im1): elif im1.im.mode in ("I", "F"): return im1.im else: - raise ValueError("unsupported mode: %s" % im1.im.mode) + raise ValueError(f"unsupported mode: {im1.im.mode}") else: # argument was a constant if _isconstant(im1) and self.im.mode in ("1", "L", "I"): @@ -60,9 +54,9 @@ def apply(self, op, im1, im2=None, mode=None): out = Image.new(mode or im1.mode, im1.size, None) im1.load() try: - op = getattr(_imagingmath, op+"_"+im1.mode) - except AttributeError: - raise TypeError("bad operand type for '%s'" % op) + op = getattr(_imagingmath, op + "_" + im1.mode) + except AttributeError as e: + raise TypeError(f"bad operand type for '{op}'") from e _imagingmath.unop(op, out.im.id, im1.im.id) else: # binary operation @@ -73,25 +67,20 @@ def apply(self, op, im1, im2=None, mode=None): im1 = im1.convert("F") if im2.mode != "F": im2 = im2.convert("F") - if im1.mode != im2.mode: - raise ValueError("mode mismatch") if im1.size != im2.size: # crop both arguments to a common size - size = (min(im1.size[0], im2.size[0]), - min(im1.size[1], im2.size[1])) + size = (min(im1.size[0], im2.size[0]), min(im1.size[1], im2.size[1])) if im1.size != size: im1 = im1.crop((0, 0) + size) if im2.size != size: im2 = im2.crop((0, 0) + size) - out = Image.new(mode or im1.mode, size, None) - else: - out = Image.new(mode or im1.mode, im1.size, None) + out = Image.new(mode or im1.mode, im1.size, None) im1.load() im2.load() try: - op = getattr(_imagingmath, op+"_"+im1.mode) - except AttributeError: - raise TypeError("bad operand type for '%s'" % op) + op = getattr(_imagingmath, op + "_" + im1.mode) + except AttributeError as e: + raise TypeError(f"bad operand type for '{op}'") from e _imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id) return _Operand(out) @@ -100,11 +89,6 @@ def __bool__(self): # an image is "true" if it contains at least one non-zero pixel return self.im.getbbox() is not None - if bytes is str: - # Provide __nonzero__ for pre-Py3k - __nonzero__ = __bool__ - del __bool__ - def __abs__(self): return self.apply("abs", self) @@ -151,13 +135,6 @@ def __pow__(self, other): def __rpow__(self, other): return self.apply("pow", other, self) - if bytes is str: - # Provide __div__ and __rdiv__ for pre-Py3k - __div__ = __truediv__ - __rdiv__ = __rtruediv__ - del __truediv__ - del __rtruediv__ - # bitwise def __invert__(self): return self.apply("invert", self) @@ -235,6 +212,7 @@ def imagemath_max(self, other): def imagemath_convert(self, mode): return _Operand(self.im.convert(mode)) + ops = {} for k, v in list(globals().items()): if k[:10] == "imagemath_": @@ -262,7 +240,19 @@ def eval(expression, _dict={}, **kw): if hasattr(v, "im"): args[k] = _Operand(v) - out = builtins.eval(expression, args) + compiled_code = compile(expression, "", "eval") + + def scan(code): + for const in code.co_consts: + if type(const) == type(compiled_code): + scan(const) + + for name in code.co_names: + if name not in args and name != "abs": + raise ValueError(f"'{name}' not allowed") + + scan(compiled_code) + out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args) try: return out.im except AttributeError: diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index b227f212775..0973536c934 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -13,18 +13,21 @@ # See the README file for information on usage and redistribution. # +import sys + # mode descriptor cache _modes = None -class ModeDescriptor(object): +class ModeDescriptor: """Wrapper for mode strings.""" - def __init__(self, mode, bands, basemode, basetype): + def __init__(self, mode, bands, basemode, basetype, typestr): self.mode = mode self.bands = bands self.basemode = basemode self.basetype = basetype + self.typestr = typestr def __str__(self): return self.mode @@ -35,21 +38,54 @@ def getmode(mode): global _modes if not _modes: # initialize mode cache - - from . import Image modes = {} - # core modes - for m, (basemode, basetype, bands) in Image._MODEINFO.items(): - modes[m] = ModeDescriptor(m, bands, basemode, basetype) - # extra experimental modes - modes["RGBa"] = ModeDescriptor("RGBa", ("R", "G", "B", "a"), "RGB", "L") - modes["LA"] = ModeDescriptor("LA", ("L", "A"), "L", "L") - modes["La"] = ModeDescriptor("La", ("L", "a"), "L", "L") - modes["PA"] = ModeDescriptor("PA", ("P", "A"), "RGB", "L") + endian = "<" if sys.byteorder == "little" else ">" + for m, (basemode, basetype, bands, typestr) in { + # core modes + # Bits need to be extended to bytes + "1": ("L", "L", ("1",), "|b1"), + "L": ("L", "L", ("L",), "|u1"), + "I": ("L", "I", ("I",), endian + "i4"), + "F": ("L", "F", ("F",), endian + "f4"), + "P": ("P", "L", ("P",), "|u1"), + "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"), + "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"), + "RGBA": ("RGB", "L", ("R", "G", "B", "A"), "|u1"), + "CMYK": ("RGB", "L", ("C", "M", "Y", "K"), "|u1"), + "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr"), "|u1"), + # UNDONE - unsigned |u1i1i1 + "LAB": ("RGB", "L", ("L", "A", "B"), "|u1"), + "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"), + # extra experimental modes + "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"), + "BGR;15": ("RGB", "L", ("B", "G", "R"), endian + "u2"), + "BGR;16": ("RGB", "L", ("B", "G", "R"), endian + "u2"), + "BGR;24": ("RGB", "L", ("B", "G", "R"), endian + "u3"), + "BGR;32": ("RGB", "L", ("B", "G", "R"), endian + "u4"), + "LA": ("L", "L", ("L", "A"), "|u1"), + "La": ("L", "L", ("L", "a"), "|u1"), + "PA": ("RGB", "L", ("P", "A"), "|u1"), + }.items(): + modes[m] = ModeDescriptor(m, bands, basemode, basetype, typestr) # mapping modes - modes["I;16"] = ModeDescriptor("I;16", "I", "L", "L") - modes["I;16L"] = ModeDescriptor("I;16L", "I", "L", "L") - modes["I;16B"] = ModeDescriptor("I;16B", "I", "L", "L") + for i16mode, typestr in { + # I;16 == I;16L, and I;32 == I;32L + "I;16": "u2", + "I;16BS": ">i2", + "I;16N": endian + "u2", + "I;16NS": endian + "i2", + "I;32": "u4", + "I;32L": "i4", + "I;32LS": " -from __future__ import print_function +import re from . import Image, _imagingmorph -import re LUT_SIZE = 1 << 9 - -class LutBuilder(object): +# fmt: off +ROTATION_MATRIX = [ + 6, 3, 0, + 7, 4, 1, + 8, 5, 2, +] +MIRROR_MATRIX = [ + 2, 1, 0, + 5, 4, 3, + 8, 7, 6, +] +# fmt: on + + +class LutBuilder: """A class for building a MorphLut from a descriptive language - The input patterns is a list of a strings sequences like these:: + The input patterns is a list of a strings sequences like these:: - 4:(... - .1. - 111)->1 + 4:(... + .1. + 111)->1 - (whitespaces including linebreaks are ignored). The option 4 - describes a series of symmetry operations (in this case a - 4-rotation), the pattern is described by: + (whitespaces including linebreaks are ignored). The option 4 + describes a series of symmetry operations (in this case a + 4-rotation), the pattern is described by: - - . or X - Ignore - - 1 - Pixel is on - - 0 - Pixel is off + - . or X - Ignore + - 1 - Pixel is on + - 0 - Pixel is off - The result of the operation is described after "->" string. + The result of the operation is described after "->" string. - The default is to return the current pixel value, which is - returned if no other match is found. + The default is to return the current pixel value, which is + returned if no other match is found. - Operations: + Operations: - - 4 - 4 way rotation - - N - Negate - - 1 - Dummy op for no other operation (an op must always be given) - - M - Mirroring + - 4 - 4 way rotation + - N - Negate + - 1 - Dummy op for no other operation (an op must always be given) + - M - Mirroring - Example:: + Example:: - lb = LutBuilder(patterns = ["4:(... .1. 111)->1"]) - lut = lb.build_lut() + lb = LutBuilder(patterns = ["4:(... .1. 111)->1"]) + lut = lb.build_lut() """ + def __init__(self, patterns=None, op_name=None): if patterns is not None: self.patterns = patterns @@ -56,20 +69,19 @@ def __init__(self, patterns=None, op_name=None): self.lut = None if op_name is not None: known_patterns = { - 'corner': ['1:(... ... ...)->0', - '4:(00. 01. ...)->1'], - 'dilation4': ['4:(... .0. .1.)->1'], - 'dilation8': ['4:(... .0. .1.)->1', - '4:(... .0. ..1)->1'], - 'erosion4': ['4:(... .1. .0.)->0'], - 'erosion8': ['4:(... .1. .0.)->0', - '4:(... .1. ..0)->0'], - 'edge': ['1:(... ... ...)->0', - '4:(.0. .1. ...)->1', - '4:(01. .1. ...)->1'] + "corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"], + "dilation4": ["4:(... .0. .1.)->1"], + "dilation8": ["4:(... .0. .1.)->1", "4:(... .0. ..1)->1"], + "erosion4": ["4:(... .1. .0.)->0"], + "erosion8": ["4:(... .1. .0.)->0", "4:(... .1. ..0)->0"], + "edge": [ + "1:(... ... ...)->0", + "4:(.0. .1. ...)->1", + "4:(01. .1. ...)->1", + ], } if op_name not in known_patterns: - raise Exception('Unknown pattern '+op_name+'!') + raise Exception("Unknown pattern " + op_name + "!") self.patterns = known_patterns[op_name] @@ -88,8 +100,8 @@ def _string_permute(self, pattern, permutation): """string_permute takes a pattern and a permutation and returns the string permuted according to the permutation list. """ - assert(len(permutation) == 9) - return ''.join(pattern[p] for p in permutation) + assert len(permutation) == 9 + return "".join(pattern[p] for p in permutation) def _pattern_permute(self, basic_pattern, options, basic_result): """pattern_permute takes a basic pattern and its result and clones @@ -98,32 +110,25 @@ def _pattern_permute(self, basic_pattern, options, basic_result): patterns = [(basic_pattern, basic_result)] # rotations - if '4' in options: + if "4" in options: res = patterns[-1][1] for i in range(4): patterns.append( - (self._string_permute(patterns[-1][0], [6, 3, 0, - 7, 4, 1, - 8, 5, 2]), res)) + (self._string_permute(patterns[-1][0], ROTATION_MATRIX), res) + ) # mirror - if 'M' in options: + if "M" in options: n = len(patterns) - for pattern, res in patterns[0:n]: - patterns.append( - (self._string_permute(pattern, [2, 1, 0, - 5, 4, 3, - 8, 7, 6]), res)) + for pattern, res in patterns[:n]: + patterns.append((self._string_permute(pattern, MIRROR_MATRIX), res)) # negate - if 'N' in options: + if "N" in options: n = len(patterns) - for pattern, res in patterns[0:n]: + for pattern, res in patterns[:n]: # Swap 0 and 1 - pattern = (pattern - .replace('0', 'Z') - .replace('1', '0') - .replace('Z', '1')) - res = 1-int(res) + pattern = pattern.replace("0", "Z").replace("1", "0").replace("Z", "1") + res = 1 - int(res) patterns.append((pattern, res)) return patterns @@ -138,27 +143,21 @@ def build_lut(self): # Parse and create symmetries of the patterns strings for p in self.patterns: - m = re.search( - r'(\w*):?\s*\((.+?)\)\s*->\s*(\d)', p.replace('\n', '')) + m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", "")) if not m: - raise Exception('Syntax error in pattern "'+p+'"') + raise Exception('Syntax error in pattern "' + p + '"') options = m.group(1) pattern = m.group(2) result = int(m.group(3)) # Get rid of spaces - pattern = pattern.replace(' ', '').replace('\n', '') + pattern = pattern.replace(" ", "").replace("\n", "") patterns += self._pattern_permute(pattern, options, result) -# # Debugging -# for p, r in patterns: -# print(p, r) -# print('--') - # compile the patterns into regular expressions for speed for i, pattern in enumerate(patterns): - p = pattern[0].replace('.', 'X').replace('X', '[01]') + p = pattern[0].replace(".", "X").replace("X", "[01]") p = re.compile(p) patterns[i] = (p, pattern[1]) @@ -168,7 +167,7 @@ def build_lut(self): for i in range(LUT_SIZE): # Build the bit pattern bitpattern = bin(i)[2:] - bitpattern = ('0'*(9-len(bitpattern)) + bitpattern)[::-1] + bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1] for p, r in patterns: if p.match(bitpattern): @@ -177,13 +176,10 @@ def build_lut(self): return self.lut -class MorphOp(object): +class MorphOp: """A class for binary morphological operators""" - def __init__(self, - lut=None, - op_name=None, - patterns=None): + def __init__(self, lut=None, op_name=None, patterns=None): """Create a binary morphological operator""" self.lut = lut if op_name is not None: @@ -197,13 +193,12 @@ def apply(self, image): Returns a tuple of the number of changed pixels and the morphed image""" if self.lut is None: - raise Exception('No operator loaded') + raise Exception("No operator loaded") - if image.mode != 'L': - raise Exception('Image must be binary, meaning it must use mode L') + if image.mode != "L": + raise ValueError("Image mode must be L") outimage = Image.new(image.mode, image.size, None) - count = _imagingmorph.apply( - bytes(self.lut), image.im.id, outimage.im.id) + count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) return count, outimage def match(self, image): @@ -211,38 +206,38 @@ def match(self, image): an image. Returns a list of tuples of (x,y) coordinates - of all matching pixels.""" + of all matching pixels. See :ref:`coordinate-system`.""" if self.lut is None: - raise Exception('No operator loaded') + raise Exception("No operator loaded") - if image.mode != 'L': - raise Exception('Image must be binary, meaning it must use mode L') + if image.mode != "L": + raise ValueError("Image mode must be L") return _imagingmorph.match(bytes(self.lut), image.im.id) def get_on_pixels(self, image): """Get a list of all turned on pixels in a binary image Returns a list of tuples of (x,y) coordinates - of all matching pixels.""" + of all matching pixels. See :ref:`coordinate-system`.""" - if image.mode != 'L': - raise Exception('Image must be binary, meaning it must use mode L') + if image.mode != "L": + raise ValueError("Image mode must be L") return _imagingmorph.get_on_pixels(image.im.id) def load_lut(self, filename): """Load an operator from an mrl file""" - with open(filename, 'rb') as f: + with open(filename, "rb") as f: self.lut = bytearray(f.read()) if len(self.lut) != LUT_SIZE: self.lut = None - raise Exception('Wrong size operator file!') + raise Exception("Wrong size operator file!") def save_lut(self, filename): """Save an operator to an mrl file""" if self.lut is None: - raise Exception('No operator loaded') - with open(filename, 'wb') as f: + raise Exception("No operator loaded") + with open(filename, "wb") as f: f.write(self.lut) def set_lut(self, lut): diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 25d491affa4..443c540b61a 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -17,16 +17,16 @@ # See the README file for information on usage and redistribution. # -from . import Image -from ._util import isStringType -import operator import functools -import warnings +import operator +import re +from . import Image, ImagePalette # # helpers + def _border(border): if isinstance(border, tuple): if len(border) == 2: @@ -39,8 +39,9 @@ def _border(border): def _color(color, mode): - if isStringType(color): + if isinstance(color, str): from . import ImageColor + color = ImageColor.getcolor(color, mode) return color @@ -54,29 +55,43 @@ def _lut(image, lut): lut = lut + lut + lut return image.point(lut) else: - raise IOError("not supported for this image mode") + raise OSError("not supported for this image mode") + # # actions -def autocontrast(image, cutoff=0, ignore=None): +def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): """ Maximize (normalize) image contrast. This function calculates a - histogram of the input image, removes **cutoff** percent of the + histogram of the input image (or mask region), removes ``cutoff`` percent of the lightest and darkest pixels from the histogram, and remaps the image so that the darkest pixel becomes black (0), and the lightest becomes white (255). :param image: The image to process. - :param cutoff: How many percent to cut off from the histogram. + :param cutoff: The percent to cut off from the histogram on the low and + high ends. Either a tuple of (low, high), or a single + number for both. :param ignore: The background pixel value (use None for no background). + :param mask: Histogram used in contrast operation is computed using pixels + within the mask. If no mask is given the entire image is used + for histogram computation. + :param preserve_tone: Preserve image tone in Photoshop-like style autocontrast. + + .. versionadded:: 8.2.0 + :return: An image. """ - histogram = image.histogram() + if preserve_tone: + histogram = image.convert("L").histogram(mask) + else: + histogram = image.histogram(mask) + lut = [] for layer in range(0, len(histogram), 256): - h = histogram[layer:layer+256] + h = histogram[layer : layer + 256] if ignore is not None: # get rid of outliers try: @@ -87,12 +102,14 @@ def autocontrast(image, cutoff=0, ignore=None): h[ix] = 0 if cutoff: # cut off pixels from both ends of the histogram + if not isinstance(cutoff, tuple): + cutoff = (cutoff, cutoff) # get number of pixels n = 0 for ix in range(256): n = n + h[ix] # remove cutoff% pixels from the low end - cut = n * cutoff // 100 + cut = n * cutoff[0] // 100 for lo in range(256): if cut > h[lo]: cut = cut - h[lo] @@ -102,8 +119,8 @@ def autocontrast(image, cutoff=0, ignore=None): cut = 0 if cut <= 0: break - # remove cutoff% samples from the hi end - cut = n * cutoff // 100 + # remove cutoff% samples from the high end + cut = n * cutoff[1] // 100 for hi in range(255, -1, -1): if cut > h[hi]: cut = cut - h[hi] @@ -136,32 +153,155 @@ def autocontrast(image, cutoff=0, ignore=None): return _lut(image, lut) -def colorize(image, black, white): +def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoint=127): """ - Colorize grayscale image. The **black** and **white** - arguments should be RGB tuples; this function calculates a color - wedge mapping all black pixels in the source image to the first - color, and all white pixels to the second color. + Colorize grayscale image. + This function calculates a color wedge which maps all black pixels in + the source image to the first color and all white pixels to the + second color. If ``mid`` is specified, it uses three-color mapping. + The ``black`` and ``white`` arguments should be RGB tuples or color names; + optionally you can use three-color mapping by also specifying ``mid``. + Mapping positions for any of the colors can be specified + (e.g. ``blackpoint``), where these parameters are the integer + value corresponding to where the corresponding color should be mapped. + These parameters must have logical order, such that + ``blackpoint <= midpoint <= whitepoint`` (if ``mid`` is specified). :param image: The image to colorize. :param black: The color to use for black input pixels. :param white: The color to use for white input pixels. + :param mid: The color to use for midtone input pixels. + :param blackpoint: an int value [0, 255] for the black mapping. + :param whitepoint: an int value [0, 255] for the white mapping. + :param midpoint: an int value [0, 255] for the midtone mapping. :return: An image. """ + + # Initial asserts assert image.mode == "L" + if mid is None: + assert 0 <= blackpoint <= whitepoint <= 255 + else: + assert 0 <= blackpoint <= midpoint <= whitepoint <= 255 + + # Define colors from arguments black = _color(black, "RGB") white = _color(white, "RGB") + if mid is not None: + mid = _color(mid, "RGB") + + # Empty lists for the mapping red = [] green = [] blue = [] - for i in range(256): - red.append(black[0]+i*(white[0]-black[0])//255) - green.append(black[1]+i*(white[1]-black[1])//255) - blue.append(black[2]+i*(white[2]-black[2])//255) + + # Create the low-end values + for i in range(0, blackpoint): + red.append(black[0]) + green.append(black[1]) + blue.append(black[2]) + + # Create the mapping (2-color) + if mid is None: + + range_map = range(0, whitepoint - blackpoint) + + for i in range_map: + red.append(black[0] + i * (white[0] - black[0]) // len(range_map)) + green.append(black[1] + i * (white[1] - black[1]) // len(range_map)) + blue.append(black[2] + i * (white[2] - black[2]) // len(range_map)) + + # Create the mapping (3-color) + else: + + range_map1 = range(0, midpoint - blackpoint) + range_map2 = range(0, whitepoint - midpoint) + + for i in range_map1: + red.append(black[0] + i * (mid[0] - black[0]) // len(range_map1)) + green.append(black[1] + i * (mid[1] - black[1]) // len(range_map1)) + blue.append(black[2] + i * (mid[2] - black[2]) // len(range_map1)) + for i in range_map2: + red.append(mid[0] + i * (white[0] - mid[0]) // len(range_map2)) + green.append(mid[1] + i * (white[1] - mid[1]) // len(range_map2)) + blue.append(mid[2] + i * (white[2] - mid[2]) // len(range_map2)) + + # Create the high-end values + for i in range(0, 256 - whitepoint): + red.append(white[0]) + green.append(white[1]) + blue.append(white[2]) + + # Return converted image image = image.convert("RGB") return _lut(image, red + green + blue) +def contain(image, size, method=Image.Resampling.BICUBIC): + """ + Returns a resized version of the image, set to the maximum width and height + within the requested size, while maintaining the original aspect ratio. + + :param image: The image to resize and crop. + :param size: The requested output size in pixels, given as a + (width, height) tuple. + :param method: Resampling method to use. Default is + :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. + :return: An image. + """ + + im_ratio = image.width / image.height + dest_ratio = size[0] / size[1] + + if im_ratio != dest_ratio: + if im_ratio > dest_ratio: + new_height = round(image.height / image.width * size[0]) + if new_height != size[1]: + size = (size[0], new_height) + else: + new_width = round(image.width / image.height * size[1]) + if new_width != size[0]: + size = (new_width, size[1]) + return image.resize(size, resample=method) + + +def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)): + """ + Returns a resized and padded version of the image, expanded to fill the + requested aspect ratio and size. + + :param image: The image to resize and crop. + :param size: The requested output size in pixels, given as a + (width, height) tuple. + :param method: Resampling method to use. Default is + :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. + :param color: The background color of the padded image. + :param centering: Control the position of the original image within the + padded version. + + (0.5, 0.5) will keep the image centered + (0, 0) will keep the image aligned to the top left + (1, 1) will keep the image aligned to the bottom + right + :return: An image. + """ + + resized = contain(image, size, method) + if resized.size == size: + out = resized + else: + out = Image.new(image.mode, size, color) + if resized.palette: + out.putpalette(resized.getpalette()) + if resized.width != size[0]: + x = round((size[0] - resized.width) * max(0, min(centering[0], 1))) + out.paste(resized, (x, 0)) + else: + y = round((size[1] - resized.height) * max(0, min(centering[1], 1))) + out.paste(resized, (0, y)) + return out + + def crop(image, border=0): """ Remove border from image. The same amount of pixels are removed @@ -174,12 +314,10 @@ def crop(image, border=0): :return: An image. """ left, top, right, bottom = _border(border) - return image.crop( - (left, top, image.size[0]-right, image.size[1]-bottom) - ) + return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) -def scale(image, factor, resample=Image.NEAREST): +def scale(image, factor, resample=Image.Resampling.BICUBIC): """ Returns a rescaled image by a specific factor given in parameter. A factor greater than 1 expands the image, between 0 and 1 contracts the @@ -187,8 +325,8 @@ def scale(image, factor, resample=Image.NEAREST): :param image: The image to rescale. :param factor: The expansion factor, as a float. - :param resample: An optional resampling filter. Same values possible as - in the PIL.Image.resize function. + :param resample: Resampling method to use. Default is + :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. :returns: An :py:class:`~PIL.Image.Image` object. """ if factor == 1: @@ -196,25 +334,24 @@ def scale(image, factor, resample=Image.NEAREST): elif factor <= 0: raise ValueError("the factor must be greater than 0") else: - size = (int(round(factor * image.width)), - int(round(factor * image.height))) + size = (round(factor * image.width), round(factor * image.height)) return image.resize(size, resample) -def deform(image, deformer, resample=Image.BILINEAR): +def deform(image, deformer, resample=Image.Resampling.BILINEAR): """ Deform the image. :param image: The image to deform. :param deformer: A deformer object. Any object that implements a - **getmesh** method can be used. + ``getmesh`` method can be used. :param resample: An optional resampling filter. Same values possible as in the PIL.Image.transform function. :return: An image. """ return image.transform( - image.size, Image.MESH, deformer.getmesh(image), resample - ) + image.size, Image.Transform.MESH, deformer.getmesh(image), resample + ) def equalize(image, mask=None): @@ -233,7 +370,7 @@ def equalize(image, mask=None): h = image.histogram(mask) lut = [] for b in range(0, len(h), 256): - histo = [_f for _f in h[b:b+256] if _f] + histo = [_f for _f in h[b : b + 256] if _f] if len(histo) <= 1: lut.extend(list(range(256))) else: @@ -244,7 +381,7 @@ def equalize(image, mask=None): n = step // 2 for i in range(256): lut.append(n // step) - n = n + h[i+b] + n = n + h[i + b] return _lut(image, lut) @@ -260,26 +397,36 @@ def expand(image, border=0, fill=0): left, top, right, bottom = _border(border) width = left + image.size[0] + right height = top + image.size[1] + bottom - out = Image.new(image.mode, (width, height), _color(fill, image.mode)) + color = _color(fill, image.mode) + if image.palette: + palette = ImagePalette.ImagePalette(palette=image.getpalette()) + if isinstance(color, tuple): + color = palette.getcolor(color) + else: + palette = None + out = Image.new(image.mode, (width, height), color) + if palette: + out.putpalette(palette.palette) out.paste(image, (left, top)) return out -def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)): +def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, 0.5)): """ - Returns a sized and cropped version of the image, cropped to the + Returns a resized and cropped version of the image, cropped to the requested aspect ratio and size. This function was contributed by Kevin Cazabon. - :param image: The image to size and crop. + :param image: The image to resize and crop. :param size: The requested output size in pixels, given as a (width, height) tuple. - :param method: What resampling method to use. Default is - :py:attr:`PIL.Image.NEAREST`. - :param bleed: Remove a border around the outside of the image (from all + :param method: Resampling method to use. Default is + :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. + :param bleed: Remove a border around the outside of the image from all four edges. The value is a decimal percentage (use 0.01 for one percent). The default value is 0 (no border). + Cannot be greater than or equal to 0.5. :param centering: Control the cropping position. Use (0.5, 0.5) for center cropping (e.g. if cropping the width, take 50% off of the left side, and therefore 50% off the right side). @@ -295,68 +442,58 @@ def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)): # by Kevin Cazabon, Feb 17/2000 # kevin@cazabon.com - # http://www.cazabon.com + # https://www.cazabon.com - # ensure inputs are valid - if not isinstance(centering, list): - centering = [centering[0], centering[1]] + # ensure centering is mutable + centering = list(centering) - if centering[0] > 1.0 or centering[0] < 0.0: - centering[0] = 0.50 - if centering[1] > 1.0 or centering[1] < 0.0: - centering[1] = 0.50 + if not 0.0 <= centering[0] <= 1.0: + centering[0] = 0.5 + if not 0.0 <= centering[1] <= 1.0: + centering[1] = 0.5 - if bleed > 0.49999 or bleed < 0.0: + if not 0.0 <= bleed < 0.5: bleed = 0.0 # calculate the area to use for resizing and cropping, subtracting # the 'bleed' around the edges # number of pixels to trim off on Top and Bottom, Left and Right - bleedPixels = ( - int((float(bleed) * float(image.size[0])) + 0.5), - int((float(bleed) * float(image.size[1])) + 0.5) - ) + bleed_pixels = (bleed * image.size[0], bleed * image.size[1]) - liveArea = (0, 0, image.size[0], image.size[1]) - if bleed > 0.0: - liveArea = ( - bleedPixels[0], bleedPixels[1], image.size[0] - bleedPixels[0] - 1, - image.size[1] - bleedPixels[1] - 1 - ) - - liveSize = (liveArea[2] - liveArea[0], liveArea[3] - liveArea[1]) + live_size = ( + image.size[0] - bleed_pixels[0] * 2, + image.size[1] - bleed_pixels[1] * 2, + ) - # calculate the aspect ratio of the liveArea - liveAreaAspectRatio = float(liveSize[0])/float(liveSize[1]) + # calculate the aspect ratio of the live_size + live_size_ratio = live_size[0] / live_size[1] # calculate the aspect ratio of the output image - aspectRatio = float(size[0]) / float(size[1]) + output_ratio = size[0] / size[1] # figure out if the sides or top/bottom will be cropped off - if liveAreaAspectRatio >= aspectRatio: - # liveArea is wider than what's needed, crop the sides - cropWidth = int((aspectRatio * float(liveSize[1])) + 0.5) - cropHeight = liveSize[1] + if live_size_ratio == output_ratio: + # live_size is already the needed ratio + crop_width = live_size[0] + crop_height = live_size[1] + elif live_size_ratio >= output_ratio: + # live_size is wider than what's needed, crop the sides + crop_width = output_ratio * live_size[1] + crop_height = live_size[1] else: - # liveArea is taller than what's needed, crop the top and bottom - cropWidth = liveSize[0] - cropHeight = int((float(liveSize[0])/aspectRatio) + 0.5) + # live_size is taller than what's needed, crop the top and bottom + crop_width = live_size[0] + crop_height = live_size[0] / output_ratio # make the crop - leftSide = int(liveArea[0] + (float(liveSize[0]-cropWidth) * centering[0])) - if leftSide < 0: - leftSide = 0 - topSide = int(liveArea[1] + (float(liveSize[1]-cropHeight) * centering[1])) - if topSide < 0: - topSide = 0 + crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering[0] + crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering[1] - out = image.crop( - (leftSide, topSide, leftSide + cropWidth, topSide + cropHeight) - ) + crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height) # resize the image and return it - return out.resize(size, method) + return image.resize(size, method, box=crop) def flip(image): @@ -366,7 +503,7 @@ def flip(image): :param image: The image to flip. :return: An image. """ - return image.transpose(Image.FLIP_TOP_BOTTOM) + return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) def grayscale(image): @@ -388,8 +525,8 @@ def invert(image): """ lut = [] for i in range(256): - lut.append(255-i) - return _lut(image, lut) + lut.append(255 - i) + return image.point(lut) if image.mode == "1" else _lut(image, lut) def mirror(image): @@ -399,7 +536,7 @@ def mirror(image): :param image: The image to mirror. :return: An image. """ - return image.transpose(Image.FLIP_LEFT_RIGHT) + return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) def posterize(image, bits): @@ -411,7 +548,7 @@ def posterize(image, bits): :return: An image. """ lut = [] - mask = ~(2**(8-bits)-1) + mask = ~(2 ** (8 - bits) - 1) for i in range(256): lut.append(i & mask) return _lut(image, lut) @@ -430,100 +567,50 @@ def solarize(image, threshold=128): if i < threshold: lut.append(i) else: - lut.append(255-i) + lut.append(255 - i) return _lut(image, lut) -# -------------------------------------------------------------------- -# PIL USM components, from Kevin Cazabon. - -def gaussian_blur(im, radius=None): - """ PIL_usm.gblur(im, [radius])""" - - warnings.warn( - 'PIL.ImageOps.gaussian_blur is deprecated. ' - 'Use PIL.ImageFilter.GaussianBlur instead. ' - 'This function will be removed in a future version.', - DeprecationWarning - ) - - if radius is None: - radius = 5.0 - - im.load() - - return im.im.gaussian_blur(radius) - - -def gblur(im, radius=None): - """ PIL_usm.gblur(im, [radius])""" - - warnings.warn( - 'PIL.ImageOps.gblur is deprecated. ' - 'Use PIL.ImageFilter.GaussianBlur instead. ' - 'This function will be removed in a future version.', - DeprecationWarning - ) - - return gaussian_blur(im, radius) - - -def unsharp_mask(im, radius=None, percent=None, threshold=None): - """ PIL_usm.usm(im, [radius, percent, threshold])""" - - warnings.warn( - 'PIL.ImageOps.unsharp_mask is deprecated. ' - 'Use PIL.ImageFilter.UnsharpMask instead. ' - 'This function will be removed in a future version.', - DeprecationWarning - ) - - if radius is None: - radius = 5.0 - if percent is None: - percent = 150 - if threshold is None: - threshold = 3 - - im.load() - - return im.im.unsharp_mask(radius, percent, threshold) - - -def usm(im, radius=None, percent=None, threshold=None): - """ PIL_usm.usm(im, [radius, percent, threshold])""" - - warnings.warn( - 'PIL.ImageOps.usm is deprecated. ' - 'Use PIL.ImageFilter.UnsharpMask instead. ' - 'This function will be removed in a future version.', - DeprecationWarning - ) - - return unsharp_mask(im, radius, percent, threshold) - - -def box_blur(image, radius): +def exif_transpose(image): """ - Blur the image by setting each pixel to the average value of the pixels - in a square box extending radius pixels in each direction. - Supports float radius of arbitrary size. Uses an optimized implementation - which runs in linear time relative to the size of the image - for any radius value. + If an image has an EXIF Orientation tag, other than 1, return a new image + that is transposed accordingly. The new image will have the orientation + data removed. + + Otherwise, return a copy of the image. - :param image: The image to blur. - :param radius: Size of the box in one direction. Radius 0 does not blur, - returns an identical image. Radius 1 takes 1 pixel - in each direction, i.e. 9 pixels in total. + :param image: The image to transpose. :return: An image. """ - warnings.warn( - 'PIL.ImageOps.box_blur is deprecated. ' - 'Use PIL.ImageFilter.BoxBlur instead. ' - 'This function will be removed in a future version.', - DeprecationWarning - ) - - image.load() - - return image._new(image.im.box_blur(radius)) + exif = image.getexif() + orientation = exif.get(0x0112) + method = { + 2: Image.Transpose.FLIP_LEFT_RIGHT, + 3: Image.Transpose.ROTATE_180, + 4: Image.Transpose.FLIP_TOP_BOTTOM, + 5: Image.Transpose.TRANSPOSE, + 6: Image.Transpose.ROTATE_270, + 7: Image.Transpose.TRANSVERSE, + 8: Image.Transpose.ROTATE_90, + }.get(orientation) + if method is not None: + transposed_image = image.transpose(method) + transposed_exif = transposed_image.getexif() + if 0x0112 in transposed_exif: + del transposed_exif[0x0112] + if "exif" in transposed_image.info: + transposed_image.info["exif"] = transposed_exif.tobytes() + elif "Raw profile type exif" in transposed_image.info: + transposed_image.info[ + "Raw profile type exif" + ] = transposed_exif.tobytes().hex() + elif "XML:com.adobe.xmp" in transposed_image.info: + for pattern in ( + r'tiff:Orientation="([0-9])"', + r"([0-9])", + ): + transposed_image.info["XML:com.adobe.xmp"] = re.sub( + pattern, "", transposed_image.info["XML:com.adobe.xmp"] + ) + return transposed_image + return image.copy() diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index cecc6458387..fe76c86f40e 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -17,33 +17,57 @@ # import array -from . import ImageColor, GimpPaletteFile, GimpGradientFile, PaletteFile +from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile +from ._deprecate import deprecate -class ImagePalette(object): + +class ImagePalette: """ Color palette for palette mapped images - :param mode: The mode to use for the Palette. See: + :param mode: The mode to use for the palette. See: :ref:`concept-modes`. Defaults to "RGB" :param palette: An optional palette. If given, it must be a bytearray, - an array or a list of ints between 0-255 and of length ``size`` - times the number of colors in ``mode``. The list must be aligned - by channel (All R values must be contiguous in the list before G - and B values.) Defaults to 0 through 255 per channel. - :param size: An optional palette size. If given, it cannot be equal to - or greater than 256. Defaults to 0. + an array or a list of ints between 0-255. The list must consist of + all channels for one color followed by the next color (e.g. RGBRGBRGB). + Defaults to an empty palette. """ def __init__(self, mode="RGB", palette=None, size=0): self.mode = mode self.rawmode = None # if set, palette contains raw data - self.palette = palette or bytearray(range(256))*len(self.mode) - self.colors = {} + self.palette = palette or bytearray() self.dirty = None - if ((size == 0 and len(self.mode)*256 != len(self.palette)) or - (size != 0 and size != len(self.palette))): - raise ValueError("wrong palette size") + if size != 0: + deprecate("The size parameter", 10, None) + if size != len(self.palette): + raise ValueError("wrong palette size") + + @property + def palette(self): + return self._palette + + @palette.setter + def palette(self, palette): + self._colors = None + self._palette = palette + + @property + def colors(self): + if self._colors is None: + mode_len = len(self.mode) + self._colors = {} + for i in range(0, len(self.palette), mode_len): + color = tuple(self.palette[i : i + mode_len]) + if color in self._colors: + continue + self._colors[color] = i // mode_len + return self._colors + + @colors.setter + def colors(self, colors): + self._colors = colors def copy(self): new = ImagePalette() @@ -52,21 +76,20 @@ def copy(self): new.rawmode = self.rawmode if self.palette is not None: new.palette = self.palette[:] - new.colors = self.colors.copy() new.dirty = self.dirty return new def getdata(self): """ - Get palette contents in format suitable # for the low-level + Get palette contents in format suitable for the low-level ``im.putpalette`` primitive. .. warning:: This method is experimental. """ if self.rawmode: return self.rawmode, self.palette - return self.mode + ";L", self.tobytes() + return self.mode, self.tobytes() def tobytes(self): """Convert palette to bytes. @@ -78,14 +101,12 @@ def tobytes(self): if isinstance(self.palette, bytes): return self.palette arr = array.array("B", self.palette) - if hasattr(arr, 'tobytes'): - return arr.tobytes() - return arr.tostring() + return arr.tobytes() # Declare tostring as an alias for tobytes tostring = tobytes - def getcolor(self, color): + def getcolor(self, color, image=None): """Given an rgb tuple, allocate palette entry. .. warning:: This method is experimental. @@ -93,23 +114,53 @@ def getcolor(self, color): if self.rawmode: raise ValueError("palette contains raw palette data") 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" + ) + color = color[:3] + elif self.mode == "RGBA": + if len(color) == 3: + color += (255,) try: return self.colors[color] - except KeyError: + except KeyError as e: # allocate new color slot - if isinstance(self.palette, bytes): - self.palette = bytearray(self.palette) - index = len(self.colors) + if not isinstance(self.palette, bytearray): + self._palette = bytearray(self.palette) + index = len(self.palette) // 3 + special_colors = () + if image: + special_colors = ( + image.info.get("background"), + image.info.get("transparency"), + ) + while index in special_colors: + index += 1 if index >= 256: - raise ValueError("cannot allocate more than 256 colors") + if image: + # Search for an unused index + for i, count in reversed(list(enumerate(image.histogram()))): + if count == 0 and i not in special_colors: + index = i + break + if index >= 256: + raise ValueError("cannot allocate more than 256 colors") from e self.colors[color] = index - self.palette[index] = color[0] - self.palette[index+256] = color[1] - self.palette[index+512] = color[2] + if index * 3 < len(self.palette): + self._palette = ( + self.palette[: index * 3] + + bytes(color) + + self.palette[index * 3 + 3 :] + ) + else: + self._palette += bytes(color) self.dirty = 1 return index else: - raise ValueError("unknown color specifier: %r" % color) + raise ValueError(f"unknown color specifier: {repr(color)}") def save(self, fp): """Save palette to text file. @@ -121,12 +172,12 @@ def save(self, fp): if isinstance(fp, str): fp = open(fp, "w") fp.write("# Palette\n") - fp.write("# Mode: %s\n" % self.mode) + fp.write(f"# Mode: {self.mode}\n") for i in range(256): - fp.write("%d" % i) - for j in range(i*len(self.mode), (i+1)*len(self.mode)): + fp.write(f"{i}") + for j in range(i * len(self.mode), (i + 1) * len(self.mode)): try: - fp.write(" %d" % self.palette[j]) + fp.write(f" {self.palette[j]}") except IndexError: fp.write(" 0") fp.write("\n") @@ -136,6 +187,7 @@ def save(self, fp): # -------------------------------------------------------------------- # Internal + def raw(rawmode, data): palette = ImagePalette() palette.rawmode = rawmode @@ -147,11 +199,12 @@ def raw(rawmode, data): # -------------------------------------------------------------------- # Factories + def make_linear_lut(black, white): lut = [] if black == 0: for i in range(256): - lut.append(white*i//255) + lut.append(white * i // 255) else: raise NotImplementedError # FIXME return lut @@ -165,29 +218,28 @@ def make_gamma_lut(exp): def negative(mode="RGB"): - palette = list(range(256)) + palette = list(range(256 * len(mode))) palette.reverse() - return ImagePalette(mode, palette * len(mode)) + return ImagePalette(mode, [i // len(mode) for i in palette]) def random(mode="RGB"): from random import randint + palette = [] - for i in range(256*len(mode)): + for i in range(256 * len(mode)): palette.append(randint(0, 255)) return ImagePalette(mode, palette) def sepia(white="#fff0c0"): - r, g, b = ImageColor.getrgb(white) - r = make_linear_lut(0, r) - g = make_linear_lut(0, g) - b = make_linear_lut(0, b) - return ImagePalette("RGB", r + g + b) + bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)] + return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)]) def wedge(mode="RGB"): - return ImagePalette(mode, list(range(256)) * len(mode)) + palette = list(range(256 * len(mode))) + return ImagePalette(mode, [i // len(mode) for i in palette]) def load(filename): @@ -199,7 +251,7 @@ def load(filename): for paletteHandler in [ GimpPaletteFile.GimpPaletteFile, GimpGradientFile.GimpGradientFile, - PaletteFile.PaletteFile + PaletteFile.PaletteFile, ]: try: fp.seek(0) @@ -211,6 +263,6 @@ def load(filename): # traceback.print_exc() pass else: - raise IOError("cannot load palette") + raise OSError("cannot load palette") return lut # data, rawmode diff --git a/src/PIL/ImagePath.py b/src/PIL/ImagePath.py index 1543508e4a1..3d3538c97b7 100644 --- a/src/PIL/ImagePath.py +++ b/src/PIL/ImagePath.py @@ -16,45 +16,4 @@ from . import Image - -# the Python class below is overridden by the C implementation. - - -class Path(object): - - def __init__(self, xy): - pass - - def compact(self, distance=2): - """ - Compacts the path, by removing points that are close to each other. - This method modifies the path in place. - """ - pass - - def getbbox(self): - """Gets the bounding box.""" - pass - - def map(self, function): - """Maps the path through a function.""" - pass - - def tolist(self, flat=0): - """ - Converts the path to Python list. - # - @param flat By default, this function returns a list of 2-tuples - [(x, y), ...]. If this argument is true, it returns a flat list - [x, y, ...] instead. - @return A list of coordinates. - """ - pass - - def transform(self, matrix): - """Transforms the path.""" - pass - - -# override with C implementation Path = Image.core.path diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 36b4e1ebc5c..a34678c7852 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -16,57 +16,78 @@ # See the README file for information on usage and redistribution. # -from . import Image -from ._util import isPath +import sys from io import BytesIO -qt_is_installed = True -qt_version = None -try: - from PyQt5.QtGui import QImage, qRgba, QPixmap - from PyQt5.QtCore import QBuffer, QIODevice - qt_version = '5' -except (ImportError, RuntimeError): +from . import Image +from ._deprecate import deprecate +from ._util import is_path + +qt_versions = [ + ["6", "PyQt6"], + ["side6", "PySide6"], + ["5", "PyQt5"], + ["side2", "PySide2"], +] + +# If a version has already been imported, attempt it first +qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules, reverse=True) +for qt_version, qt_module in qt_versions: try: - from PyQt4.QtGui import QImage, qRgba, QPixmap - from PyQt4.QtCore import QBuffer, QIODevice - qt_version = '4' + if qt_module == "PyQt6": + from PyQt6.QtCore import QBuffer, QIODevice + from PyQt6.QtGui import QImage, QPixmap, qRgba + elif qt_module == "PySide6": + from PySide6.QtCore import QBuffer, QIODevice + from PySide6.QtGui import QImage, QPixmap, qRgba + elif qt_module == "PyQt5": + from PyQt5.QtCore import QBuffer, QIODevice + from PyQt5.QtGui import QImage, QPixmap, qRgba + + deprecate("Support for PyQt5", 10, "PyQt6 or PySide6") + elif qt_module == "PySide2": + from PySide2.QtCore import QBuffer, QIODevice + from PySide2.QtGui import QImage, QPixmap, qRgba + + deprecate("Support for PySide2", 10, "PyQt6 or PySide6") except (ImportError, RuntimeError): - try: - from PySide.QtGui import QImage, qRgba, QPixmap - from PySide.QtCore import QBuffer, QIODevice - qt_version = 'side' - except ImportError: - qt_is_installed = False + continue + qt_is_installed = True + break +else: + qt_is_installed = False + qt_version = None def rgb(r, g, b, a=255): """(Internal) Turns an RGB color into a Qt compatible color integer.""" # use qRgb to pack the colors, and then turn the resulting long # into a negative integer with the same bitpattern. - return (qRgba(r, g, b, a) & 0xffffffff) + return qRgba(r, g, b, a) & 0xFFFFFFFF def fromqimage(im): """ - :param im: A PIL Image object, or a file name - (given either as Python string or a PyQt string object) + :param im: QImage or PIL ImageQt object """ buffer = QBuffer() - buffer.open(QIODevice.ReadWrite) - # preserve alha channel with png + if qt_version == "6": + try: + qt_openmode = QIODevice.OpenModeFlag + except AttributeError: + qt_openmode = QIODevice.OpenMode + else: + qt_openmode = QIODevice + buffer.open(qt_openmode.ReadWrite) + # preserve alpha channel with png # otherwise ppm is more friendly with Image.open if im.hasAlphaChannel(): - im.save(buffer, 'png') + im.save(buffer, "png") else: - im.save(buffer, 'ppm') + im.save(buffer, "ppm") b = BytesIO() - try: - b.write(buffer.data()) - except TypeError: - # workaround for Python 2 - b.write(str(buffer.data())) + b.write(buffer.data()) buffer.close() b.seek(0) @@ -92,11 +113,7 @@ def align8to32(bytes, width, mode): converts each scanline of data from 8 bit to 32 bit aligned """ - bits_per_pixel = { - '1': 1, - 'L': 8, - 'P': 8, - }[mode] + bits_per_pixel = {"1": 1, "L": 8, "P": 8, "I;16": 16}[mode] # calculate bytes per line and the extra padding if needed bits_per_line = bits_per_pixel * width @@ -111,79 +128,91 @@ def align8to32(bytes, width, mode): new_data = [] for i in range(len(bytes) // bytes_per_line): - new_data.append(bytes[i*bytes_per_line:(i+1)*bytes_per_line] + b'\x00' * extra_padding) + new_data.append( + bytes[i * bytes_per_line : (i + 1) * bytes_per_line] + + b"\x00" * extra_padding + ) - return b''.join(new_data) + return b"".join(new_data) def _toqclass_helper(im): data = None colortable = None + exclusive_fp = False # handle filename, if given instead of image name if hasattr(im, "toUtf8"): # FIXME - is this really the best way to do this? - if str is bytes: - im = unicode(im.toUtf8(), "utf-8") - else: - im = str(im.toUtf8(), "utf-8") - if isPath(im): + im = str(im.toUtf8(), "utf-8") + if is_path(im): im = Image.open(im) + exclusive_fp = True + qt_format = QImage.Format if qt_version == "6" else QImage if im.mode == "1": - format = QImage.Format_Mono + format = qt_format.Format_Mono elif im.mode == "L": - format = QImage.Format_Indexed8 + format = qt_format.Format_Indexed8 colortable = [] for i in range(256): colortable.append(rgb(i, i, i)) elif im.mode == "P": - format = QImage.Format_Indexed8 + format = qt_format.Format_Indexed8 colortable = [] palette = im.getpalette() for i in range(0, len(palette), 3): - colortable.append(rgb(*palette[i:i+3])) + colortable.append(rgb(*palette[i : i + 3])) elif im.mode == "RGB": - data = im.tobytes("raw", "BGRX") - format = QImage.Format_RGB32 + # Populate the 4th channel with 255 + im = im.convert("RGBA") + + data = im.tobytes("raw", "BGRA") + format = qt_format.Format_RGB32 elif im.mode == "RGBA": - try: - data = im.tobytes("raw", "BGRA") - except SystemError: - # workaround for earlier versions - r, g, b, a = im.split() - im = Image.merge("RGBA", (b, g, r, a)) - format = QImage.Format_ARGB32 + data = im.tobytes("raw", "BGRA") + format = qt_format.Format_ARGB32 + elif im.mode == "I;16" and hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+ + im = im.point(lambda i: i * 256) + + format = qt_format.Format_Grayscale16 else: - raise ValueError("unsupported image mode %r" % im.mode) + if exclusive_fp: + im.close() + raise ValueError(f"unsupported image mode {repr(im.mode)}") + + size = im.size + __data = data or align8to32(im.tobytes(), size[0], im.mode) + if exclusive_fp: + im.close() + return {"data": __data, "size": size, "format": format, "colortable": colortable} - __data = data or align8to32(im.tobytes(), im.size[0], im.mode) - return { - 'data': __data, 'im': im, 'format': format, 'colortable': colortable - } if qt_is_installed: - class ImageQt(QImage): + class ImageQt(QImage): def __init__(self, im): """ An PIL image wrapper for Qt. This is a subclass of PyQt's QImage class. - :param im: A PIL Image object, or a file name (given either as Python - string or a PyQt string object). + :param im: A PIL Image object, or a file name (given either as + Python string or a PyQt string object). """ im_data = _toqclass_helper(im) # must keep a reference, or Qt will crash! # All QImage constructors that take data operate on an existing # buffer, so this buffer has to hang on for the life of the image. # Fixes https://github.com/python-pillow/Pillow/issues/1370 - self.__data = im_data['data'] - QImage.__init__(self, - self.__data, im_data['im'].size[0], - im_data['im'].size[1], im_data['format']) - if im_data['colortable']: - self.setColorTable(im_data['colortable']) + self.__data = im_data["data"] + super().__init__( + self.__data, + im_data["size"][0], + im_data["size"][1], + im_data["format"], + ) + if im_data["colortable"]: + self.setColorTable(im_data["colortable"]) def toqimage(im): @@ -193,11 +222,7 @@ def toqimage(im): def toqpixmap(im): # # This doesn't work. For now using a dumb approach. # im_data = _toqclass_helper(im) - # result = QPixmap(im_data['im'].size[0], im_data['im'].size[1]) - # result.loadFromData(im_data['data']) - # Fix some strange bug that causes - if im.mode == 'RGB': - im = im.convert('RGBA') - + # result = QPixmap(im_data["size"][0], im_data["size"][1]) + # result.loadFromData(im_data["data"]) qimage = toqimage(im) return QPixmap.fromImage(qimage) diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index 1fc6e5de165..9df910a4330 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -16,7 +16,7 @@ ## -class Iterator(object): +class Iterator: """ This class implements an iterator object that can be used to loop over an image sequence. @@ -32,14 +32,14 @@ def __init__(self, im): if not hasattr(im, "seek"): raise AttributeError("im must have seek method") self.im = im - self.position = 0 + self.position = getattr(self.im, "_min_frame", 0) def __getitem__(self, ix): try: self.im.seek(ix) return self.im - except EOFError: - raise IndexError # end of sequence + except EOFError as e: + raise IndexError from e # end of sequence def __iter__(self): return self @@ -49,8 +49,27 @@ def __next__(self): self.im.seek(self.position) self.position += 1 return self.im - except EOFError: - raise StopIteration + except EOFError as e: + raise StopIteration from e - def next(self): - return self.__next__() + +def all_frames(im, func=None): + """ + Applies a given function to all frames in an image or a list of images. + The frames are returned as a list of separate images. + + :param im: An image, or a list of images. + :param func: The function to apply to all of the image frames. + :returns: A list of images. + """ + if not isinstance(im, list): + im = [im] + + ims = [] + for imSequence in im: + current = imSequence.tell() + + ims += [im_frame.copy() for im_frame in Iterator(imSequence)] + + imSequence.seek(current) + return [func(im) for im in ims] if func else ims diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index bf8c67eb54c..76f42a3072d 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -11,22 +11,33 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function - -from PIL import Image import os +import shutil +import subprocess import sys +from shlex import quote -if sys.version_info >= (3, 3): - from shlex import quote -else: - from pipes import quote +from PIL import Image + +from ._deprecate import deprecate _viewers = [] def register(viewer, order=1): + """ + The :py:func:`register` function is used to register additional viewers:: + + from PIL import ImageShow + ImageShow.register(MyViewer()) # MyViewer will be used as a last resort + ImageShow.register(MySecondViewer(), 0) # MySecondViewer will be prioritised + ImageShow.register(ImageShow.XVViewer(), 0) # XVViewer will be prioritised + + :param viewer: The viewer to be registered. + :param order: + Zero or a negative integer to prepend this viewer to the list, + a positive integer to append it. + """ try: if issubclass(viewer, Viewer): viewer = viewer() @@ -34,7 +45,7 @@ def register(viewer, order=1): pass # raised if viewer wasn't a class if order > 0: _viewers.append(viewer) - elif order < 0: + else: _viewers.insert(0, viewer) @@ -43,148 +54,339 @@ def show(image, title=None, **options): Display a given image. :param image: An image object. - :param title: Optional title. Not all viewers can display the title. + :param title: Optional title. Not all viewers can display the title. :param \**options: Additional viewer options. - :returns: True if a suitable viewer was found, false otherwise. + :returns: ``True`` if a suitable viewer was found, ``False`` otherwise. """ for viewer in _viewers: if viewer.show(image, title=title, **options): - return 1 - return 0 + return True + return False -class Viewer(object): +class Viewer: """Base class for viewers.""" # main api def show(self, image, **options): - - # save temporary image to disk - if image.mode[:4] == "I;16": - # @PIL88 @PIL101 - # "I;16" isn't an 'official' mode, but we still want to - # provide a simple way to show 16-bit images. - base = "L" - # FIXME: auto-contrast if max() > 255? - else: + """ + The main function for displaying an image. + Converts the given image to the target format and displays it. + """ + + if not ( + image.mode in ("1", "RGBA") + or (self.format == "PNG" and image.mode in ("I;16", "LA")) + ): base = Image.getmodebase(image.mode) - if base != image.mode and image.mode != "1" and image.mode != "RGBA": - image = image.convert(base) + if image.mode != base: + image = image.convert(base) return self.show_image(image, **options) # hook methods format = None + """The format to convert the image into.""" options = {} + """Additional options used to convert the image.""" def get_format(self, image): - """Return format name, or None to save as PGM/PPM""" + """Return format name, or ``None`` to save as PGM/PPM.""" return self.format def get_command(self, file, **options): + """ + Returns the command used to display the file. + Not implemented in the base class. + """ raise NotImplementedError def save_image(self, image): - """Save to temporary file, and return filename""" + """Save to temporary file and return filename.""" return image._dump(format=self.get_format(image), **self.options) def show_image(self, image, **options): - """Display given image""" + """Display the given image.""" return self.show_file(self.save_image(image), **options) - def show_file(self, file, **options): - """Display given file""" - os.system(self.get_command(file, **options)) + def show_file(self, path=None, **options): + """ + Display given file. + + Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, + and will be removed in Pillow 10.0.0 (2023-07-01). ``path`` should be used + instead. + """ + if path is None: + if "file" in options: + deprecate("The 'file' argument", 10, "'path'") + path = options.pop("file") + else: + raise TypeError("Missing required argument: 'path'") + os.system(self.get_command(path, **options)) return 1 + # -------------------------------------------------------------------- -if sys.platform == "win32": +class WindowsViewer(Viewer): + """The default viewer on Windows is the default system application for PNG files.""" + + format = "PNG" + options = {"compress_level": 1, "save_all": True} - class WindowsViewer(Viewer): - format = "BMP" + def get_command(self, file, **options): + return ( + f'start "Pillow" /WAIT "{file}" ' + "&& ping -n 4 127.0.0.1 >NUL " + f'&& del /f "{file}"' + ) - def get_command(self, file, **options): - return ('start "Pillow" /WAIT "%s" ' - '&& ping -n 2 127.0.0.1 >NUL ' - '&& del /f "%s"' % (file, file)) +if sys.platform == "win32": register(WindowsViewer) -elif sys.platform == "darwin": - class MacViewer(Viewer): - format = "PNG" - options = {'compress_level': 1} +class MacViewer(Viewer): + """The default viewer on macOS using ``Preview.app``.""" + + format = "PNG" + options = {"compress_level": 1, "save_all": True} + + def get_command(self, file, **options): + # on darwin open returns immediately resulting in the temp + # file removal while app is opening + command = "open -a Preview.app" + command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&" + return command + + def show_file(self, path=None, **options): + """ + Display given file. + + Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, + and will be removed in Pillow 10.0.0 (2023-07-01). ``path`` should be used + instead. + """ + if path is None: + if "file" in options: + deprecate("The 'file' argument", 10, "'path'") + path = options.pop("file") + else: + raise TypeError("Missing required argument: 'path'") + subprocess.call(["open", "-a", "Preview.app", path]) + executable = sys.executable or shutil.which("python3") + if executable: + subprocess.Popen( + [ + executable, + "-c", + "import os, sys, time; time.sleep(20); os.remove(sys.argv[1])", + path, + ] + ) + return 1 - def get_command(self, file, **options): - # on darwin open returns immediately resulting in the temp - # file removal while app is opening - command = "open -a /Applications/Preview.app" - command = "(%s %s; sleep 20; rm -f %s)&" % (command, quote(file), - quote(file)) - return command +if sys.platform == "darwin": register(MacViewer) -else: - # unixoids - - def which(executable): - path = os.environ.get("PATH") - if not path: - return None - for dirname in path.split(os.pathsep): - filename = os.path.join(dirname, executable) - if os.path.isfile(filename) and os.access(filename, os.X_OK): - return filename - return None - - class UnixViewer(Viewer): - format = "PNG" - options = {'compress_level': 1} - - def show_file(self, file, **options): - command, executable = self.get_command_ex(file, **options) - command = "(%s %s; rm -f %s)&" % (command, quote(file), - quote(file)) - os.system(command) - return 1 - - # implementations - - class DisplayViewer(UnixViewer): - def get_command_ex(self, file, **options): - command = executable = "display" - return command, executable - - if which("display"): - register(DisplayViewer) +class UnixViewer(Viewer): + format = "PNG" + options = {"compress_level": 1, "save_all": True} - class EogViewer(UnixViewer): - def get_command_ex(self, file, **options): - command = executable = "eog" - return command, executable + def get_command(self, file, **options): + command = self.get_command_ex(file, **options)[0] + return f"({command} {quote(file)}" + + +class XDGViewer(UnixViewer): + """ + The freedesktop.org ``xdg-open`` command. + """ + + def get_command_ex(self, file, **options): + command = executable = "xdg-open" + return command, executable + + def show_file(self, path=None, **options): + """ + Display given file. + + Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, + and will be removed in Pillow 10.0.0 (2023-07-01). ``path`` should be used + instead. + """ + if path is None: + if "file" in options: + deprecate("The 'file' argument", 10, "'path'") + path = options.pop("file") + else: + raise TypeError("Missing required argument: 'path'") + subprocess.Popen(["xdg-open", path]) + return 1 - if which("eog"): - register(EogViewer) - class XVViewer(UnixViewer): - def get_command_ex(self, file, title=None, **options): - # note: xv is pretty outdated. most modern systems have - # imagemagick's display command instead. - command = executable = "xv" - if title: - command += " -name %s" % quote(title) - return command, executable +class DisplayViewer(UnixViewer): + """ + The ImageMagick ``display`` command. + This viewer supports the ``title`` parameter. + """ + + def get_command_ex(self, file, title=None, **options): + command = executable = "display" + if title: + command += f" -title {quote(title)}" + return command, executable + + def show_file(self, path=None, **options): + """ + Display given file. + + Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, + and ``path`` should be used instead. + """ + if path is None: + if "file" in options: + deprecate("The 'file' argument", 10, "'path'") + path = options.pop("file") + else: + raise TypeError("Missing required argument: 'path'") + args = ["display"] + title = options.get("title") + if title: + args += ["-title", title] + args.append(path) + + subprocess.Popen(args) + return 1 + + +class GmDisplayViewer(UnixViewer): + """The GraphicsMagick ``gm display`` command.""" + + def get_command_ex(self, file, **options): + executable = "gm" + command = "gm display" + return command, executable + + def show_file(self, path=None, **options): + """ + Display given file. + + Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, + and ``path`` should be used instead. + """ + if path is None: + if "file" in options: + deprecate("The 'file' argument", 10, "'path'") + path = options.pop("file") + else: + raise TypeError("Missing required argument: 'path'") + subprocess.Popen(["gm", "display", path]) + return 1 + + +class EogViewer(UnixViewer): + """The GNOME Image Viewer ``eog`` command.""" + + def get_command_ex(self, file, **options): + executable = "eog" + command = "eog -n" + return command, executable + + def show_file(self, path=None, **options): + """ + Display given file. + + Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, + and ``path`` should be used instead. + """ + if path is None: + if "file" in options: + deprecate("The 'file' argument", 10, "'path'") + path = options.pop("file") + else: + raise TypeError("Missing required argument: 'path'") + subprocess.Popen(["eog", "-n", path]) + return 1 + + +class XVViewer(UnixViewer): + """ + The X Viewer ``xv`` command. + This viewer supports the ``title`` parameter. + """ + + def get_command_ex(self, file, title=None, **options): + # note: xv is pretty outdated. most modern systems have + # imagemagick's display command instead. + command = executable = "xv" + if title: + command += f" -name {quote(title)}" + return command, executable + + def show_file(self, path=None, **options): + """ + Display given file. + + Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, + and ``path`` should be used instead. + """ + if path is None: + if "file" in options: + deprecate("The 'file' argument", 10, "'path'") + path = options.pop("file") + else: + raise TypeError("Missing required argument: 'path'") + args = ["xv"] + title = options.get("title") + if title: + args += ["-name", title] + args.append(path) + + subprocess.Popen(args) + return 1 + - if which("xv"): +if sys.platform not in ("win32", "darwin"): # unixoids + if shutil.which("xdg-open"): + register(XDGViewer) + if shutil.which("display"): + register(DisplayViewer) + if shutil.which("gm"): + register(GmDisplayViewer) + if shutil.which("eog"): + register(EogViewer) + if shutil.which("xv"): register(XVViewer) + +class IPythonViewer(Viewer): + """The viewer for IPython frontends.""" + + def show_image(self, image, **options): + ipython_display(image) + return 1 + + +try: + from IPython.display import display as ipython_display +except ImportError: + pass +else: + register(IPythonViewer) + + if __name__ == "__main__": - # usage: python ImageShow.py imagefile [title] - print(show(Image.open(sys.argv[1]), *sys.argv[2:])) + + if len(sys.argv) < 2: + print("Syntax: python3 ImageShow.py imagefile [title]") + sys.exit() + + with Image.open(sys.argv[1]) as im: + print(show(im, *sys.argv[2:])) diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py index f3c138b3aa8..1baef7db499 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -21,13 +21,12 @@ # See the README file for information on usage and redistribution. # +import functools import math import operator -import functools -class Stat(object): - +class Stat: def __init__(self, image_or_list, mask=None): try: if mask: @@ -41,7 +40,7 @@ def __init__(self, image_or_list, mask=None): self.bands = list(range(len(self.h) // 256)) def __getattr__(self, id): - "Calculate missing attribute" + """Calculate missing attribute""" if id[:4] == "_get": raise AttributeError(id) # calculate missing attribute @@ -50,7 +49,7 @@ def __getattr__(self, id): return v def _getextrema(self): - "Get min/max values for each band in the image" + """Get min/max values for each band in the image""" def minmax(histogram): n = 255 @@ -67,37 +66,37 @@ def minmax(histogram): return v def _getcount(self): - "Get total number of pixels in each layer" + """Get total number of pixels in each layer""" v = [] for i in range(0, len(self.h), 256): - v.append(functools.reduce(operator.add, self.h[i:i+256])) + v.append(functools.reduce(operator.add, self.h[i : i + 256])) return v def _getsum(self): - "Get sum of all pixels in each layer" + """Get sum of all pixels in each layer""" v = [] for i in range(0, len(self.h), 256): - layerSum = 0.0 + layer_sum = 0.0 for j in range(256): - layerSum += j * self.h[i + j] - v.append(layerSum) + layer_sum += j * self.h[i + j] + v.append(layer_sum) return v def _getsum2(self): - "Get squared sum of all pixels in each layer" + """Get squared sum of all pixels in each layer""" v = [] for i in range(0, len(self.h), 256): sum2 = 0.0 for j in range(256): - sum2 += (j ** 2) * float(self.h[i + j]) + sum2 += (j**2) * float(self.h[i + j]) v.append(sum2) return v def _getmean(self): - "Get average pixel level for each layer" + """Get average pixel level for each layer""" v = [] for i in self.bands: @@ -105,22 +104,22 @@ def _getmean(self): return v def _getmedian(self): - "Get median pixel level for each layer" + """Get median pixel level for each layer""" v = [] for i in self.bands: s = 0 - l = self.count[i]//2 + half = self.count[i] // 2 b = i * 256 for j in range(256): - s = s + self.h[b+j] - if s > l: + s = s + self.h[b + j] + if s > half: break v.append(j) return v def _getrms(self): - "Get RMS for each layer" + """Get RMS for each layer""" v = [] for i in self.bands: @@ -128,20 +127,21 @@ def _getrms(self): return v def _getvar(self): - "Get variance for each layer" + """Get variance for each layer""" v = [] for i in self.bands: n = self.count[i] - v.append((self.sum2[i]-(self.sum[i]**2.0)/n)/n) + v.append((self.sum2[i] - (self.sum[i] ** 2.0) / n) / n) return v def _getstddev(self): - "Get standard deviation for each layer" + """Get standard deviation for each layer""" v = [] for i in self.bands: v.append(math.sqrt(self.var[i])) return v + Global = Stat # compatibility diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index a19ed1d0d3f..949cf1fbf9d 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -25,23 +25,11 @@ # See the README file for information on usage and redistribution. # -import sys - -if sys.version_info[0] > 2: - import tkinter -else: - import Tkinter as tkinter - -# required for pypy, which always has cffi installed -try: - from cffi import FFI - ffi = FFI() -except ImportError: - pass - -from . import Image +import tkinter from io import BytesIO +from . import Image +from ._deprecate import deprecate # -------------------------------------------------------------------- # Check for Tkinter interface hooks @@ -54,7 +42,7 @@ def _pilbitmap_check(): if _pilbitmap_ok is None: try: im = Image.new("1", (1, 1)) - tkinter.BitmapImage(data="PIL:%d" % im.im.id) + tkinter.BitmapImage(data=f"PIL:{im.im.id}") _pilbitmap_ok = 1 except tkinter.TclError: _pilbitmap_ok = 0 @@ -71,17 +59,31 @@ def _get_image_from_kw(kw): return Image.open(source) +def _pyimagingtkcall(command, photo, id): + tk = photo.tk + try: + tk.call(command, photo, id) + except tkinter.TclError: + # activate Tkinter hook + # may raise an error if it cannot attach to Tkinter + from . import _imagingtk + + _imagingtk.tkinit(tk.interpaddr()) + tk.call(command, photo, id) + + # -------------------------------------------------------------------- # PhotoImage -class PhotoImage(object): + +class PhotoImage: """ A Tkinter-compatible photo image. This can be used everywhere Tkinter expects an image object. If the image is an RGBA image, pixels having alpha 0 are treated as transparent. The constructor takes either a PIL image, or a mode and a size. - Alternatively, you can use the **file** or **data** options to initialize + Alternatively, you can use the ``file`` or ``data`` options to initialize the photo image object. :param image: Either a PIL image, or a mode string. If a mode string is @@ -105,6 +107,7 @@ def __init__(self, image=None, size=None, **kw): mode = image.mode if mode == "P": # palette mapped data + image.apply_transparency() image.load() try: mode = image.palette.mode @@ -131,7 +134,7 @@ def __del__(self): self.__photo.name = None try: self.__photo.tk.call("image", "delete", name) - except: + except Exception: pass # ignore internal errors def __str__(self): @@ -168,11 +171,13 @@ def paste(self, im, box=None): :param im: A PIL image. The size must match the target region. If the mode does not match, the image is converted to the mode of the bitmap image. - :param box: A 4-tuple defining the left, upper, right, and lower pixel - coordinate. If None is given instead of a tuple, all of - the image is assumed. + :param box: Deprecated. This parameter will be removed in Pillow 10 + (2023-07-01). """ + if box is not None: + deprecate("The box parameter", 10, None) + # convert to blittable im.load() image = im.im @@ -182,40 +187,21 @@ def paste(self, im, box=None): block = image.new_block(self.__mode, im.size) image.convert2(block, image) # convert directly between buffers - tk = self.__photo.tk + _pyimagingtkcall("PyImagingPhoto", self.__photo, block.id) - try: - tk.call("PyImagingPhoto", self.__photo, block.id) - except tkinter.TclError: - # activate Tkinter hook - try: - from . import _imagingtk - try: - if hasattr(tk, 'interp'): - # Pypy is using a ffi cdata element - # (Pdb) self.tk.interp - # - _imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp)), 1) - else: - _imagingtk.tkinit(tk.interpaddr(), 1) - except AttributeError: - _imagingtk.tkinit(id(tk), 0) - tk.call("PyImagingPhoto", self.__photo, block.id) - except (ImportError, AttributeError, tkinter.TclError): - raise # configuration problem; cannot attach to Tkinter # -------------------------------------------------------------------- # BitmapImage -class BitmapImage(object): +class BitmapImage: """ A Tkinter-compatible bitmap image. This can be used everywhere Tkinter expects an image object. The given image must have mode "1". Pixels having value 0 are treated as transparent. Options, if any, are passed on to Tkinter. The most commonly - used option is **foreground**, which is used to specify the color for the + used option is ``foreground``, which is used to specify the color for the non-transparent parts. See the Tkinter documentation for information on how to specify colours. @@ -234,7 +220,7 @@ def __init__(self, image=None, **kw): if _pilbitmap_check(): # fast way (requires the pilbitmap booster patch) image.load() - kw["data"] = "PIL:%d" % image.im.id + kw["data"] = f"PIL:{image.im.id}" self.__im = image # must keep a reference else: # slow but safe way @@ -246,7 +232,7 @@ def __del__(self): self.__photo.name = None try: self.__photo.tk.call("image", "delete", name) - except: + except Exception: pass # ignore internal errors def width(self): @@ -277,10 +263,13 @@ def __str__(self): def getimage(photo): - """ This function is unimplemented """ - """Copies the contents of a PhotoImage to a PIL image memory.""" - photo.tk.call("PyImagingPhotoGet", photo) + im = Image.new("RGBA", (photo.width(), photo.height())) + block = im.im + + _pyimagingtkcall("PyImagingPhotoGet", photo, block.id) + + return im def _show(image, title): @@ -292,11 +281,10 @@ def __init__(self, master, im): self.image = BitmapImage(im, foreground="white", master=master) else: self.image = PhotoImage(im, master=master) - tkinter.Label.__init__(self, master, image=self.image, - bg="black", bd=0) + super().__init__(master, image=self.image, bg="black", bd=0) if not tkinter._default_root: - raise IOError("tkinter not initialized") + raise OSError("tkinter not initialized") top = tkinter.Toplevel() if title: top.title(title) diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 4207ffb83de..7881f0d262b 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -46,7 +46,8 @@ class AffineTransform(Transform): :param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows from an affine transform matrix. """ - method = Image.AFFINE + + method = Image.Transform.AFFINE class ExtentTransform(Transform): @@ -65,9 +66,10 @@ class ExtentTransform(Transform): See :py:meth:`~PIL.Image.Image.transform` :param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the - input image's coordinate system. + input image's coordinate system. See :ref:`coordinate-system`. """ - method = Image.EXTENT + + method = Image.Transform.EXTENT class QuadTransform(Transform): @@ -83,7 +85,8 @@ class QuadTransform(Transform): upper left, lower left, lower right, and upper right corner of the source quadrilateral. """ - method = Image.QUAD + + method = Image.Transform.QUAD class MeshTransform(Transform): @@ -95,4 +98,5 @@ class MeshTransform(Transform): :param data: A list of (bbox, quad) tuples. """ - method = Image.MESH + + method = Image.Transform.MESH diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py index d8398e92bfa..ca9b14c8adf 100644 --- a/src/PIL/ImageWin.py +++ b/src/PIL/ImageWin.py @@ -20,12 +20,13 @@ from . import Image -class HDC(object): +class HDC: """ Wraps an HDC integer. The resulting object can be passed to the :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose` methods. """ + def __init__(self, dc): self.dc = dc @@ -33,12 +34,13 @@ def __int__(self): return self.dc -class HWND(object): +class HWND: """ Wraps an HWND integer. The resulting object can be passed to the :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose` methods, instead of a DC. """ + def __init__(self, wnd): self.wnd = wnd @@ -46,7 +48,7 @@ def __int__(self): return self.wnd -class Dib(object): +class Dib: """ A Windows bitmap with the given mode and size. The mode can be one of "1", "L", "P", or "RGB". @@ -57,7 +59,7 @@ class Dib(object): with 20 greylevels. To make sure that palettes work properly under Windows, you must call the - **palette** method upon certain events from Windows. + ``palette`` method upon certain events from Windows. :param image: Either a PIL image, or a mode string. If a mode string is used, a size must also be given. The mode can be one of "1", @@ -86,8 +88,8 @@ def expose(self, handle): Copy the bitmap contents to a device context. :param handle: Device context (HDC), cast to a Python integer, or an - HDC or HWND instance. In PythonWin, you can use the - :py:meth:`CDC.GetHandleAttrib` to get a suitable handle. + HDC or HWND instance. In PythonWin, you can use + ``CDC.GetHandleAttrib()`` to get a suitable handle. """ if isinstance(handle, HWND): dc = self.image.getdc(handle) @@ -154,8 +156,9 @@ def paste(self, im, box=None): If the mode does not match, the image is converted to the mode of the bitmap image. :param box: A 4-tuple defining the left, upper, right, and - lower pixel coordinate. If None is given instead of a - tuple, all of the image is assumed. + lower pixel coordinate. See :ref:`coordinate-system`. If + None is given instead of a tuple, all of the image is + assumed. """ im.load() if self.mode != im.mode: @@ -170,7 +173,7 @@ def frombytes(self, buffer): Load display memory contents from byte data. :param buffer: A buffer containing display data (usually - data returned from tobytes) + data returned from :py:func:`~PIL.ImageWin.Dib.tobytes`) """ return self.image.frombytes(buffer) @@ -183,13 +186,13 @@ def tobytes(self): return self.image.tobytes() -class Window(object): +class Window: """Create a Window with the given title size.""" def __init__(self, title="PIL", width=None, height=None): self.hwnd = Image.core.createwindow( title, self.__dispatcher, width or 0, height or 0 - ) + ) def __dispatcher(self, action, *args): return getattr(self, "ui_handle_" + action)(*args) @@ -221,7 +224,7 @@ def __init__(self, image, title="PIL"): image = Dib(image) self.image = image width, height = image.size - Window.__init__(self, title, width=width, height=height) + super().__init__(title, width=width, height=height) def ui_handle_repair(self, dc, x0, y0, x1, y1): self.image.draw(dc, (x0, y0, x1, y1)) diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index 05e8cd31a5c..dc707801274 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -19,18 +19,16 @@ from . import Image, ImageFile -__version__ = "0.2" - - # # -------------------------------------------------------------------- -field = re.compile(br"([a-z]*) ([^ \r\n]*)") +field = re.compile(rb"([a-z]*) ([^ \r\n]*)") ## # Image plugin for IM Tools images. + class ImtImageFile(ImageFile.ImageFile): format = "IMT" @@ -41,32 +39,44 @@ def _open(self): # Quick rejection: if there's not a LF among the first # 100 bytes, this is (probably) not a text header. - if b"\n" not in self.fp.read(100): + buffer = self.fp.read(100) + if b"\n" not in buffer: raise SyntaxError("not an IM file") - self.fp.seek(0) xsize = ysize = 0 while True: - s = self.fp.read(1) + if buffer: + s = buffer[:1] + buffer = buffer[1:] + else: + s = self.fp.read(1) if not s: break - if s == b'\x0C': + if s == b"\x0C": # image data begins - self.tile = [("raw", (0, 0)+self.size, - self.fp.tell(), - (self.mode, 0, 1))] + self.tile = [ + ( + "raw", + (0, 0) + self.size, + self.fp.tell() - len(buffer), + (self.mode, 0, 1), + ) + ] break else: # read key/value pair - # FIXME: dangerous, may read whole file - s = s + self.fp.readline() + if b"\n" not in buffer: + buffer += self.fp.read(100) + lines = buffer.split(b"\n") + s += lines.pop(0) + buffer = b"\n".join(lines) if len(s) == 1 or len(s) > 100: break if s[0] == ord(b"*"): @@ -76,13 +86,13 @@ def _open(self): if not m: break k, v = m.group(1, 2) - if k == "width": + if k == b"width": xsize = int(v) - self.size = xsize, ysize - elif k == "height": + self._size = xsize, ysize + elif k == b"height": ysize = int(v) - self.size = xsize, ysize - elif k == "pixel" and v == "n8": + self._size = xsize, ysize + elif k == b"pixel" and v == b"n8": self.mode = "L" diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index f5a8de17e3f..0bbe50668d8 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -14,20 +14,16 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function - -from . import Image, ImageFile -from ._binary import i8, i16be as i16, i32be as i32, o8 import os import tempfile -__version__ = "0.3" +from . import Image, ImageFile +from ._binary import i8 +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._binary import o8 -COMPRESSION = { - 1: "raw", - 5: "jpeg" -} +COMPRESSION = {1: "raw", 5: "jpeg"} PAD = o8(0) * 4 @@ -35,13 +31,14 @@ # # Helpers + def i(c): return i32((PAD + c)[-4:]) def dump(c): for i in c: - print("%02x" % i8(i), end=' ') + print("%02x" % i8(i), end=" ") print() @@ -49,6 +46,7 @@ def dump(c): # Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields # from TIFF and JPEG files, use the getiptcinfo function. + class IptcImageFile(ImageFile.ImageFile): format = "IPTC" @@ -64,22 +62,22 @@ def field(self): if not len(s): return None, 0 - tag = i8(s[1]), i8(s[2]) + tag = s[1], s[2] # syntax - if i8(s[0]) != 0x1C or tag[0] < 1 or tag[0] > 9: + if s[0] != 0x1C or tag[0] < 1 or tag[0] > 9: raise SyntaxError("invalid IPTC/NAA file") # field size - size = i8(s[3]) + size = s[3] if size > 132: - raise IOError("illegal field length in IPTC/NAA file") + raise OSError("illegal field length in IPTC/NAA file") elif size == 128: size = 0 elif size > 128: - size = i(self.fp.read(size-128)) + size = i(self.fp.read(size - 128)) else: - size = i16(s[3:]) + size = i16(s, 3) return tag, size @@ -103,13 +101,11 @@ def _open(self): else: self.info[tag] = tagdata - # print(tag, self.info[tag]) - # mode layers = i8(self.info[(3, 60)][0]) component = i8(self.info[(3, 60)][1]) if (3, 65) in self.info: - id = i8(self.info[(3, 65)][0])-1 + id = i8(self.info[(3, 65)][0]) - 1 else: id = 0 if layers == 1 and not component: @@ -120,18 +116,19 @@ def _open(self): self.mode = "CMYK"[id] # size - self.size = self.getint((3, 20)), self.getint((3, 30)) + self._size = self.getint((3, 20)), self.getint((3, 30)) # compression try: compression = COMPRESSION[self.getint((3, 120))] - except KeyError: - raise IOError("Unknown IPTC image compression") + except KeyError as e: + raise OSError("Unknown IPTC image compression") from e # tile if tag == (8, 10): - self.tile = [("iptc", (compression, offset), - (0, 0, self.size[0], self.size[1]))] + self.tile = [ + ("iptc", (compression, offset), (0, 0, self.size[0], self.size[1])) + ] def load(self): @@ -164,9 +161,9 @@ def load(self): o.close() try: - _im = Image.open(outfile) - _im.load() - self.im = _im.im + with Image.open(outfile) as _im: + _im.load() + self.im = _im.im finally: try: os.unlink(outfile) @@ -187,9 +184,10 @@ def getiptcinfo(im): :returns: A dictionary containing IPTC information, or None if no IPTC information block was found. """ - from . import TiffImagePlugin, JpegImagePlugin import io + from . import JpegImagePlugin, TiffImagePlugin + data = None if isinstance(im, IptcImageFile): @@ -198,35 +196,9 @@ def getiptcinfo(im): elif isinstance(im, JpegImagePlugin.JpegImageFile): # extract the IPTC/NAA resource - try: - app = im.app["APP13"] - if app[:14] == b"Photoshop 3.0\x00": - app = app[14:] - # parse the image resource block - offset = 0 - while app[offset:offset+4] == b"8BIM": - offset += 4 - # resource code - code = i16(app, offset) - offset += 2 - # resource name (usually empty) - name_len = i8(app[offset]) - # name = app[offset+1:offset+1+name_len] - offset = 1 + offset + name_len - if offset & 1: - offset += 1 - # resource data block - size = i32(app, offset) - offset += 4 - if code == 0x0404: - # 0x0404 contains IPTC/NAA data - data = app[offset:offset+size] - break - offset = offset + size - if offset & 1: - offset += 1 - except (AttributeError, KeyError): - pass + photoshop = im.info.get("photoshop") + if photoshop: + data = photoshop.get(0x0404) elif isinstance(im, TiffImagePlugin.TiffImageFile): # get raw data from the IPTC/NAA tag (PhotoShop tags the data @@ -240,8 +212,9 @@ def getiptcinfo(im): return None # no properties # create an IptcImagePlugin object without initializing it - class FakeImage(object): + class FakeImage: pass + im = FakeImage() im.__class__ = IptcImageFile diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index c67cc14bfdc..c67d8d6bf60 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -6,18 +6,91 @@ # # History: # 2014-03-12 ajh Created +# 2021-06-30 rogermb Extract dpi information from the 'resc' header box # # Copyright (c) 2014 Coriolis Systems Limited # Copyright (c) 2014 Alastair Houghton # # See the README file for information on usage and redistribution. # -from . import Image, ImageFile -import struct -import os import io +import os +import struct + +from . import Image, ImageFile + + +class BoxReader: + """ + A small helper class to read fields stored in JPEG2000 header boxes + and to easily step into and read sub-boxes. + """ + + def __init__(self, fp, length=-1): + self.fp = fp + self.has_length = length >= 0 + self.length = length + self.remaining_in_box = -1 + + def _can_read(self, num_bytes): + if self.has_length and self.fp.tell() + num_bytes > self.length: + # Outside box: ensure we don't read past the known file length + return False + if self.remaining_in_box >= 0: + # Inside box contents: ensure read does not go past box boundaries + return num_bytes <= self.remaining_in_box + else: + return True # No length known, just read + + def _read_bytes(self, num_bytes): + if not self._can_read(num_bytes): + raise SyntaxError("Not enough data in header") + + data = self.fp.read(num_bytes) + if len(data) < num_bytes: + raise OSError( + f"Expected to read {num_bytes} bytes but only got {len(data)}." + ) + + if self.remaining_in_box > 0: + self.remaining_in_box -= num_bytes + return data + + def read_fields(self, field_format): + size = struct.calcsize(field_format) + data = self._read_bytes(size) + return struct.unpack(field_format, data) + + def read_boxes(self): + size = self.remaining_in_box + data = self._read_bytes(size) + return BoxReader(io.BytesIO(data), size) + + def has_next_box(self): + if self.has_length: + return self.fp.tell() + self.remaining_in_box < self.length + else: + return True -__version__ = "0.1" + def next_box_type(self): + # Skip the rest of the box if it has not been read + if self.remaining_in_box > 0: + self.fp.seek(self.remaining_in_box, os.SEEK_CUR) + self.remaining_in_box = -1 + + # Read the length and type of the next box + lbox, tbox = self.read_fields(">I4s") + if lbox == 1: + lbox = self.read_fields(">Q")[0] + hlen = 16 + else: + hlen = 8 + + if lbox < hlen or not self._can_read(lbox - hlen): + raise SyntaxError("Invalid header length") + + self.remaining_in_box = lbox - hlen + return tbox def _parse_codestream(fp): @@ -25,127 +98,101 @@ def _parse_codestream(fp): count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" hdr = fp.read(2) - lsiz = struct.unpack('>H', hdr)[0] + lsiz = struct.unpack(">H", hdr)[0] siz = hdr + fp.read(lsiz - 2) - lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, xtsiz, ytsiz, \ - xtosiz, ytosiz, csiz \ - = struct.unpack('>HHIIIIIIIIH', siz[:38]) - ssiz = [None]*csiz - xrsiz = [None]*csiz - yrsiz = [None]*csiz + lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from( + ">HHIIIIIIIIH", siz + ) + ssiz = [None] * csiz + xrsiz = [None] * csiz + yrsiz = [None] * csiz for i in range(csiz): - ssiz[i], xrsiz[i], yrsiz[i] \ - = struct.unpack('>BBB', siz[36 + 3 * i:39 + 3 * i]) + ssiz[i], xrsiz[i], yrsiz[i] = struct.unpack_from(">BBB", siz, 36 + 3 * i) size = (xsiz - xosiz, ysiz - yosiz) if csiz == 1: - if (yrsiz[0] & 0x7f) > 8: - mode = 'I;16' + if (yrsiz[0] & 0x7F) > 8: + mode = "I;16" else: - mode = 'L' + mode = "L" elif csiz == 2: - mode = 'LA' + mode = "LA" elif csiz == 3: - mode = 'RGB' + mode = "RGB" elif csiz == 4: - mode = 'RGBA' + mode = "RGBA" else: mode = None - return (size, mode) + return size, mode + + +def _res_to_dpi(num, denom, exp): + """Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution, + calculated as (num / denom) * 10^exp and stored in dots per meter, + to floating-point dots per inch.""" + if denom != 0: + return (254 * num * (10**exp)) / (10000 * denom) def _parse_jp2_header(fp): - """Parse the JP2 header box to extract size, component count and - color space information, returning a PIL (size, mode) tuple.""" + """Parse the JP2 header box to extract size, component count, + color space information, and optionally DPI information, + returning a (size, mode, mimetype, dpi) tuple.""" # Find the JP2 header box + reader = BoxReader(fp) header = None - while True: - lbox, tbox = struct.unpack('>I4s', fp.read(8)) - if lbox == 1: - lbox = struct.unpack('>Q', fp.read(8))[0] - hlen = 16 - else: - hlen = 8 + mimetype = None + while reader.has_next_box(): + tbox = reader.next_box_type() - if lbox < hlen: - raise SyntaxError('Invalid JP2 header length') - - if tbox == b'jp2h': - header = fp.read(lbox - hlen) + if tbox == b"jp2h": + header = reader.read_boxes() break - else: - fp.seek(lbox - hlen, os.SEEK_CUR) - - if header is None: - raise SyntaxError('could not find JP2 header') + elif tbox == b"ftyp": + if reader.read_fields(">4s")[0] == b"jpx ": + mimetype = "image/jpx" size = None mode = None bpc = None nc = None + dpi = None # 2-tuple of DPI info, or None - hio = io.BytesIO(header) - while True: - lbox, tbox = struct.unpack('>I4s', hio.read(8)) - if lbox == 1: - lbox = struct.unpack('>Q', hio.read(8))[0] - hlen = 16 - else: - hlen = 8 - - content = hio.read(lbox - hlen) + while header.has_next_box(): + tbox = header.next_box_type() - if tbox == b'ihdr': - height, width, nc, bpc, c, unkc, ipr \ - = struct.unpack('>IIHBBBB', content) + if tbox == b"ihdr": + height, width, nc, bpc = header.read_fields(">IIHB") size = (width, height) - if unkc: - if nc == 1 and (bpc & 0x7f) > 8: - mode = 'I;16' - elif nc == 1: - mode = 'L' - elif nc == 2: - mode = 'LA' - elif nc == 3: - mode = 'RGB' - elif nc == 4: - mode = 'RGBA' - break - elif tbox == b'colr': - meth, prec, approx = struct.unpack('>BBB', content[:3]) - if meth == 1: - cs = struct.unpack('>I', content[3:7])[0] - if cs == 16: # sRGB - if nc == 1 and (bpc & 0x7f) > 8: - mode = 'I;16' - elif nc == 1: - mode = 'L' - elif nc == 3: - mode = 'RGB' - elif nc == 4: - mode = 'RGBA' - break - elif cs == 17: # grayscale - if nc == 1 and (bpc & 0x7f) > 8: - mode = 'I;16' - elif nc == 1: - mode = 'L' - elif nc == 2: - mode = 'LA' - break - elif cs == 18: # sYCC - if nc == 3: - mode = 'RGB' - elif nc == 4: - mode = 'RGBA' + if nc == 1 and (bpc & 0x7F) > 8: + mode = "I;16" + elif nc == 1: + mode = "L" + elif nc == 2: + mode = "LA" + elif nc == 3: + mode = "RGB" + elif nc == 4: + mode = "RGBA" + elif tbox == b"res ": + res = header.read_boxes() + while res.has_next_box(): + tres = res.next_box_type() + if tres == b"resc": + vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB") + hres = _res_to_dpi(hrcn, hrcd, hrce) + vres = _res_to_dpi(vrcn, vrcd, vrce) + if hres is not None and vres is not None: + dpi = (hres, vres) break if size is None or mode is None: - raise SyntaxError("Malformed jp2 header") + raise SyntaxError("Malformed JP2 header") + + return size, mode, mimetype, dpi - return (size, mode) ## # Image plugin for JPEG2000 images. @@ -157,22 +204,25 @@ class Jpeg2KImageFile(ImageFile.ImageFile): def _open(self): sig = self.fp.read(4) - if sig == b'\xff\x4f\xff\x51': + if sig == b"\xff\x4f\xff\x51": self.codec = "j2k" - self.size, self.mode = _parse_codestream(self.fp) + self._size, self.mode = _parse_codestream(self.fp) else: sig = sig + self.fp.read(8) - if sig == b'\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a': + if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a": self.codec = "jp2" - self.size, self.mode = _parse_jp2_header(self.fp) + header = _parse_jp2_header(self.fp) + self._size, self.mode, self.custom_mimetype, dpi = header + if dpi is not None: + self.info["dpi"] = dpi else: - raise SyntaxError('not a JPEG 2000 file') + raise SyntaxError("not a JPEG 2000 file") if self.size is None or self.mode is None: - raise SyntaxError('unable to determine size/mode') + raise SyntaxError("unable to determine size/mode") - self.reduce = 0 + self._reduce = 0 self.layers = 0 fd = -1 @@ -181,69 +231,102 @@ def _open(self): try: fd = self.fp.fileno() length = os.fstat(fd).st_size - except: + except Exception: fd = -1 try: pos = self.fp.tell() - self.fp.seek(0, 2) + self.fp.seek(0, io.SEEK_END) length = self.fp.tell() - self.fp.seek(pos, 0) - except: + self.fp.seek(pos) + except Exception: length = -1 - self.tile = [('jpeg2k', (0, 0) + self.size, 0, - (self.codec, self.reduce, self.layers, fd, length, self.fp))] + self.tile = [ + ( + "jpeg2k", + (0, 0) + self.size, + 0, + (self.codec, self._reduce, self.layers, fd, length), + ) + ] + + @property + def reduce(self): + # https://github.com/python-pillow/Pillow/issues/4343 found that the + # new Image 'reduce' method was shadowed by this plugin's 'reduce' + # property. This attempts to allow for both scenarios + return self._reduce or super().reduce + + @reduce.setter + def reduce(self, value): + self._reduce = value def load(self): - if self.reduce: - power = 1 << self.reduce + if self.tile and self._reduce: + power = 1 << self._reduce adjust = power >> 1 - self.size = (int((self.size[0] + adjust) / power), - int((self.size[1] + adjust) / power)) + self._size = ( + int((self.size[0] + adjust) / power), + int((self.size[1] + adjust) / power), + ) - if self.tile: # Update the reduce and layers settings t = self.tile[0] - t3 = (t[3][0], self.reduce, self.layers, t[3][3], t[3][4]) + t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4]) self.tile = [(t[0], (0, 0) + self.size, t[2], t3)] return ImageFile.ImageFile.load(self) def _accept(prefix): - return (prefix[:4] == b'\xff\x4f\xff\x51' or - prefix[:12] == b'\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a') + return ( + prefix[:4] == b"\xff\x4f\xff\x51" + or prefix[:12] == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" + ) # ------------------------------------------------------------ # Save support -def _save(im, fp, filename): - if filename.endswith('.j2k'): - kind = 'j2k' - else: - kind = 'jp2' +def _save(im, fp, filename): # Get the keyword arguments info = im.encoderinfo - offset = info.get('offset', None) - tile_offset = info.get('tile_offset', None) - tile_size = info.get('tile_size', None) - quality_mode = info.get('quality_mode', 'rates') - quality_layers = info.get('quality_layers', None) - num_resolutions = info.get('num_resolutions', 0) - cblk_size = info.get('codeblock_size', None) - precinct_size = info.get('precinct_size', None) - irreversible = info.get('irreversible', False) - progression = info.get('progression', 'LRCP') - cinema_mode = info.get('cinema_mode', 'no') + if filename.endswith(".j2k") or info.get("no_jp2", False): + kind = "j2k" + else: + kind = "jp2" + + offset = info.get("offset", None) + tile_offset = info.get("tile_offset", None) + tile_size = info.get("tile_size", None) + quality_mode = info.get("quality_mode", "rates") + quality_layers = info.get("quality_layers", None) + if quality_layers is not None and not ( + isinstance(quality_layers, (list, tuple)) + and all( + [ + isinstance(quality_layer, (int, float)) + for quality_layer in quality_layers + ] + ) + ): + raise ValueError("quality_layers must be a sequence of numbers") + + num_resolutions = info.get("num_resolutions", 0) + cblk_size = info.get("codeblock_size", None) + precinct_size = info.get("precinct_size", None) + irreversible = info.get("irreversible", False) + progression = info.get("progression", "LRCP") + cinema_mode = info.get("cinema_mode", "no") + mct = info.get("mct", 0) fd = -1 if hasattr(fp, "fileno"): try: fd = fp.fileno() - except: + except Exception: fd = -1 im.encoderconfig = ( @@ -258,18 +341,22 @@ def _save(im, fp, filename): irreversible, progression, cinema_mode, - fd + mct, + fd, ) - ImageFile._save(im, fp, [('jpeg2k', (0, 0)+im.size, 0, kind)]) + ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) + # ------------------------------------------------------------ # Registry stuff + Image.register_open(Jpeg2KImageFile.format, Jpeg2KImageFile, _accept) Image.register_save(Jpeg2KImageFile.format, _save) -Image.register_extensions(Jpeg2KImageFile.format, [".jp2", ".j2k", ".jpc", ".jpf", ".jpx", ".j2c"]) +Image.register_extensions( + Jpeg2KImageFile.format, [".jp2", ".j2k", ".jpc", ".jpf", ".jpx", ".j2c"] +) -Image.register_mime(Jpeg2KImageFile.format, 'image/jp2') -Image.register_mime(Jpeg2KImageFile.format, 'image/jpx') +Image.register_mime(Jpeg2KImageFile.format, "image/jp2") diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index d286f793f78..a6ed223bc6f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -31,26 +31,29 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import print_function - import array -import struct import io +import math +import os +import struct +import subprocess +import sys +import tempfile import warnings + from . import Image, ImageFile, TiffImagePlugin -from ._binary import i8, o8, i16be as i16 +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._binary import o8 +from ._deprecate import deprecate from .JpegPresets import presets -from ._util import isStringType - -__version__ = "0.6" - # # Parser + def Skip(self, marker): - n = i16(self.fp.read(2))-2 + n = i16(self.fp.read(2)) - 2 ImageFile._safe_read(self.fp, n) @@ -59,7 +62,7 @@ def APP(self, marker): # Application marker. Store these in the APP dictionary. # Also look for well-known application markers. - n = i16(self.fp.read(2))-2 + n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) app = "APP%d" % (marker & 15) @@ -73,9 +76,9 @@ def APP(self, marker): self.info["jfif_version"] = divmod(version, 256) # extract JFIF properties try: - jfif_unit = i8(s[7]) + jfif_unit = s[7] jfif_density = i16(s, 8), i16(s, 10) - except: + except Exception: pass else: if jfif_unit == 1: @@ -83,8 +86,9 @@ def APP(self, marker): self.info["jfif_unit"] = jfif_unit self.info["jfif_density"] = jfif_density elif marker == 0xFFE1 and s[:5] == b"Exif\0": - # extract Exif information (incomplete) - self.info["exif"] = s # FIXME: value will change + if "exif" not in self.info: + # extract EXIF information (incomplete) + self.info["exif"] = s # FIXME: value will change elif marker == 0xFFE2 and s[:5] == b"FPXR\0": # extract FlashPix information (incomplete) self.info["flashpix"] = s # FIXME: value will change @@ -101,12 +105,44 @@ def APP(self, marker): # reassemble the profile, rather than assuming that the APP2 # markers appear in the correct sequence. self.icclist.append(s) + elif marker == 0xFFED and s[:14] == b"Photoshop 3.0\x00": + # parse the image resource block + offset = 14 + photoshop = self.info.setdefault("photoshop", {}) + while s[offset : offset + 4] == b"8BIM": + try: + offset += 4 + # resource code + code = i16(s, offset) + offset += 2 + # resource name (usually empty) + name_len = s[offset] + # name = s[offset+1:offset+1+name_len] + offset += 1 + name_len + offset += offset & 1 # align + # resource data block + size = i32(s, offset) + offset += 4 + data = s[offset : offset + size] + if code == 0x03ED: # ResolutionInfo + data = { + "XResolution": i32(data, 0) / 65536, + "DisplayedUnitsX": i16(data, 4), + "YResolution": i32(data, 8) / 65536, + "DisplayedUnitsY": i16(data, 12), + } + photoshop[code] = data + offset += size + offset += offset & 1 # align + except struct.error: + break # insufficient data + elif marker == 0xFFEE and s[:5] == b"Adobe": self.info["adobe"] = i16(s, 5) # extract Adobe custom properties try: - adobe_transform = i8(s[1]) - except: + adobe_transform = s[11] + except IndexError: pass else: self.info["adobe_transform"] = adobe_transform @@ -120,30 +156,34 @@ def APP(self, marker): # If DPI isn't in JPEG header, fetch from EXIF if "dpi" not in self.info and "exif" in self.info: try: - exif = self._getexif() + exif = self.getexif() resolution_unit = exif[0x0128] x_resolution = exif[0x011A] try: - dpi = x_resolution[0] / x_resolution[1] + dpi = float(x_resolution[0]) / x_resolution[1] except TypeError: dpi = x_resolution + if math.isnan(dpi): + raise ValueError if resolution_unit == 3: # cm # 1 dpcm = 2.54 dpi dpi *= 2.54 self.info["dpi"] = dpi, dpi - except (KeyError, SyntaxError, ZeroDivisionError): - # SyntaxError for invalid/unreadable exif + except (TypeError, KeyError, SyntaxError, ValueError, ZeroDivisionError): + # SyntaxError for invalid/unreadable EXIF # KeyError for dpi not included # ZeroDivisionError for invalid dpi rational value + # ValueError or TypeError for dpi being an invalid float self.info["dpi"] = 72, 72 def COM(self, marker): # # Comment marker. Store these in the APP dictionary. - n = i16(self.fp.read(2))-2 + n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) + self.info["comment"] = s self.app["COM"] = s # compatibility self.applist.append(("COM", s)) @@ -156,15 +196,15 @@ def SOF(self, marker): # mode. Note that this could be made a bit brighter, by # looking for JFIF and Adobe APP markers. - n = i16(self.fp.read(2))-2 + n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) - self.size = i16(s[3:]), i16(s[1:]) + self._size = i16(s, 3), i16(s, 1) - self.bits = i8(s[0]) + self.bits = s[0] if self.bits != 8: - raise SyntaxError("cannot handle %d-bit layers" % self.bits) + raise SyntaxError(f"cannot handle {self.bits}-bit layers") - self.layers = i8(s[5]) + self.layers = s[5] if self.layers == 1: self.mode = "L" elif self.layers == 3: @@ -172,7 +212,7 @@ def SOF(self, marker): elif self.layers == 4: self.mode = "CMYK" else: - raise SyntaxError("cannot handle %d-layer images" % self.layers) + raise SyntaxError(f"cannot handle {self.layers}-layer images") if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]: self.info["progressive"] = self.info["progression"] = 1 @@ -180,7 +220,7 @@ def SOF(self, marker): if self.icclist: # fixup icc profile self.icclist.sort() # sort by sequence number - if i8(self.icclist[0][13]) == len(self.icclist): + if self.icclist[0][13] == len(self.icclist): profile = [] for p in self.icclist: profile.append(p[14:]) @@ -188,35 +228,35 @@ def SOF(self, marker): else: icc_profile = None # wrong number of fragments self.info["icc_profile"] = icc_profile - self.icclist = None + self.icclist = [] for i in range(6, len(s), 3): - t = s[i:i+3] + t = s[i : i + 3] # 4-tuples: id, vsamp, hsamp, qtable - self.layer.append((t[0], i8(t[1])//16, i8(t[1]) & 15, i8(t[2]))) + self.layer.append((t[0], t[1] // 16, t[1] & 15, t[2])) def DQT(self, marker): # - # Define quantization table. Support baseline 8-bit tables - # only. Note that there might be more than one table in - # each marker. + # Define quantization table. Note that there might be more + # than one table in each marker. # FIXME: The quantization tables can be used to estimate the # compression quality. - n = i16(self.fp.read(2))-2 + n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) while len(s): - if len(s) < 65: + v = s[0] + 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") - v = i8(s[0]) - if v//16 == 0: - self.quantization[v & 15] = array.array("B", s[1:65]) - s = s[65:] - else: - return # FIXME: add code to read 16-bit tables! - # raise SyntaxError, "bad quantization table element size" + data = array.array("B" if precision == 1 else "H", s[1:qt_length]) + if sys.byteorder == "little" and precision > 1: + data.byteswap() # the values are always big-endian + self.quantization[v & 15] = [data[i] for i in zigzag_index] + s = s[qt_length:] # @@ -285,17 +325,19 @@ def DQT(self, marker): 0xFFFB: ("JPG11", "Extension 11", None), 0xFFFC: ("JPG12", "Extension 12", None), 0xFFFD: ("JPG13", "Extension 13", None), - 0xFFFE: ("COM", "Comment", COM) + 0xFFFE: ("COM", "Comment", COM), } def _accept(prefix): - return prefix[0:1] == b"\377" + # Magic number was taken from https://en.wikipedia.org/wiki/JPEG + return prefix[:3] == b"\xFF\xD8\xFF" ## # Image plugin for JPEG and JFIF images. + class JpegImageFile(ImageFile.ImageFile): format = "JPEG" @@ -303,10 +345,11 @@ class JpegImageFile(ImageFile.ImageFile): def _open(self): - s = self.fp.read(1) + s = self.fp.read(3) - if i8(s) != 255: + if not _accept(s): raise SyntaxError("not a JPEG file") + s = b"\xFF" # Create attributes self.bits = self.layers = 0 @@ -322,7 +365,7 @@ def _open(self): while True: - i = i8(s) + i = s[0] if i == 0xFF: s = s + self.fp.read(1) i = i16(s) @@ -333,15 +376,13 @@ def _open(self): if i in MARKER: name, description, handler = MARKER[i] - # print(hex(i), name, description) if handler is not None: handler(self, i) if i == 0xFFDA: # start of scan rawmode = self.mode if self.mode == "CMYK": rawmode = "CMYK;I" # assume adobe conventions - self.tile = [("jpeg", (0, 0) + self.size, 0, - (rawmode, ""))] + self.tile = [("jpeg", (0, 0) + self.size, 0, (rawmode, ""))] # self.__offset = self.fp.tell() break s = self.fp.read(1) @@ -353,6 +394,22 @@ def _open(self): else: raise SyntaxError("no marker found") + def load_read(self, read_bytes): + """ + internal: read more image data + For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker + so libjpeg can finish decoding + """ + s = self.fp.read(read_bytes) + + if not s and ImageFile.LOAD_TRUNCATED_IMAGES and not hasattr(self, "_ended"): + # Premature EOF. + # Pretend file is finished adding EOI marker + self._ended = True + return b"\xFF\xD9" + + return s + def draft(self, mode, size): if len(self.tile) != 1: @@ -363,7 +420,8 @@ def draft(self, mode, size): return d, e, o, a = self.tile[0] - scale = 0 + scale = 1 + original_size = self.size if a[0] == "RGB" and mode in ["L", "YCbCr"]: self.mode = mode @@ -374,22 +432,25 @@ def draft(self, mode, size): for s in [8, 4, 2, 1]: if scale >= s: break - e = e[0], e[1], (e[2]-e[0]+s-1)//s+e[0], (e[3]-e[1]+s-1)//s+e[1] - self.size = ((self.size[0]+s-1)//s, (self.size[1]+s-1)//s) + e = ( + e[0], + e[1], + (e[2] - e[0] + s - 1) // s + e[0], + (e[3] - e[1] + s - 1) // s + e[1], + ) + self._size = ((self.size[0] + s - 1) // s, (self.size[1] + s - 1) // s) scale = s self.tile = [(d, e, o, a)] self.decoderconfig = (scale, 0) - return self + box = (0, 0, original_size[0] / scale, original_size[1] / scale) + return self.mode, box def load_djpeg(self): # ALTERNATIVE: handle JPEGs via the IJG command line utilities - import subprocess - import tempfile - import os f, path = tempfile.mkstemp() os.close(f) if os.path.exists(self.filename): @@ -398,9 +459,9 @@ def load_djpeg(self): raise ValueError("Invalid Filename") try: - _im = Image.open(path) - _im.load() - self.im = _im.im + with Image.open(path) as _im: + _im.load() + self.im = _im.im finally: try: os.unlink(path) @@ -408,7 +469,7 @@ def load_djpeg(self): pass self.mode = self.im.mode - self.size = self.im.size + self._size = self.im.size self.tile = [] @@ -418,70 +479,32 @@ def _getexif(self): def _getmp(self): return _getmp(self) + def getxmp(self): + """ + Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. -def _fixup_dict(src_dict): - # Helper function for _getexif() - # returns a dict with any single item tuples/lists as individual values - def _fixup(value): - try: - if len(value) == 1 and not isinstance(value, dict): - return value[0] - except: - pass - return value + :returns: XMP tags in a dictionary. + """ - return {k: _fixup(v) for k, v in src_dict.items()} + for segment, content in self.applist: + if segment == "APP1": + marker, xmp_tags = content.rsplit(b"\x00", 1) + if marker == b"http://ns.adobe.com/xap/1.0/": + return self._getxmp(xmp_tags) + return {} def _getexif(self): - # Extract EXIF information. This method is highly experimental, - # and is likely to be replaced with something better in a future - # version. - - # The EXIF record consists of a TIFF file embedded in a JPEG - # application marker (!). - try: - data = self.info["exif"] - except KeyError: + if "exif" not in self.info: return None - file = io.BytesIO(data[6:]) - head = file.read(8) - # process dictionary - info = TiffImagePlugin.ImageFileDirectory_v1(head) - info.load(file) - exif = dict(_fixup_dict(info)) - # get exif extension - try: - # exif field 0x8769 is an offset pointer to the location - # of the nested embedded exif ifd. - # It should be a long, but may be corrupted. - file.seek(exif[0x8769]) - except (KeyError, TypeError): - pass - else: - info = TiffImagePlugin.ImageFileDirectory_v1(head) - info.load(file) - exif.update(_fixup_dict(info)) - # get gpsinfo extension - try: - # exif field 0x8825 is an offset pointer to the location - # of the nested embedded gps exif ifd. - # It should be a long, but may be corrupted. - file.seek(exif[0x8825]) - except (KeyError, TypeError): - pass - else: - info = TiffImagePlugin.ImageFileDirectory_v1(head) - info.load(file) - exif[0x8825] = _fixup_dict(info) - - return exif + return self.getexif()._get_merged_dict() def _getmp(self): # Extract MP information. This method was inspired by the "highly # experimental" _getexif version that's been in use for years now, - # itself based on the ImageFileDirectory class in the TIFF plug-in. + # itself based on the ImageFileDirectory class in the TIFF plugin. # The MP record essentially consists of a TIFF file embedded in a JPEG # application marker. @@ -491,60 +514,57 @@ def _getmp(self): return None file_contents = io.BytesIO(data) head = file_contents.read(8) - endianness = '>' if head[:4] == b'\x4d\x4d\x00\x2a' else '<' + endianness = ">" if head[:4] == b"\x4d\x4d\x00\x2a" else "<" # process dictionary try: info = TiffImagePlugin.ImageFileDirectory_v2(head) + file_contents.seek(info.next) info.load(file_contents) mp = dict(info) - except: - raise SyntaxError("malformed MP Index (unreadable directory)") + except Exception as e: + raise SyntaxError("malformed MP Index (unreadable directory)") from e # it's an error not to have a number of images try: quant = mp[0xB001] - except KeyError: - raise SyntaxError("malformed MP Index (no number of images)") + except KeyError as e: + raise SyntaxError("malformed MP Index (no number of images)") from e # get MP entries mpentries = [] try: rawmpentries = mp[0xB002] for entrynum in range(0, quant): unpackedentry = struct.unpack_from( - '{}LLLHH'.format(endianness), rawmpentries, entrynum * 16) - labels = ('Attribute', 'Size', 'DataOffset', 'EntryNo1', - 'EntryNo2') + f"{endianness}LLLHH", rawmpentries, entrynum * 16 + ) + labels = ("Attribute", "Size", "DataOffset", "EntryNo1", "EntryNo2") mpentry = dict(zip(labels, unpackedentry)) mpentryattr = { - 'DependentParentImageFlag': bool(mpentry['Attribute'] & - (1 << 31)), - 'DependentChildImageFlag': bool(mpentry['Attribute'] & - (1 << 30)), - 'RepresentativeImageFlag': bool(mpentry['Attribute'] & - (1 << 29)), - 'Reserved': (mpentry['Attribute'] & (3 << 27)) >> 27, - 'ImageDataFormat': (mpentry['Attribute'] & (7 << 24)) >> 24, - 'MPType': mpentry['Attribute'] & 0x00FFFFFF + "DependentParentImageFlag": bool(mpentry["Attribute"] & (1 << 31)), + "DependentChildImageFlag": bool(mpentry["Attribute"] & (1 << 30)), + "RepresentativeImageFlag": bool(mpentry["Attribute"] & (1 << 29)), + "Reserved": (mpentry["Attribute"] & (3 << 27)) >> 27, + "ImageDataFormat": (mpentry["Attribute"] & (7 << 24)) >> 24, + "MPType": mpentry["Attribute"] & 0x00FFFFFF, } - if mpentryattr['ImageDataFormat'] == 0: - mpentryattr['ImageDataFormat'] = 'JPEG' + if mpentryattr["ImageDataFormat"] == 0: + mpentryattr["ImageDataFormat"] = "JPEG" else: raise SyntaxError("unsupported picture format in MPO") mptypemap = { - 0x000000: 'Undefined', - 0x010001: 'Large Thumbnail (VGA Equivalent)', - 0x010002: 'Large Thumbnail (Full HD Equivalent)', - 0x020001: 'Multi-Frame Image (Panorama)', - 0x020002: 'Multi-Frame Image: (Disparity)', - 0x020003: 'Multi-Frame Image: (Multi-Angle)', - 0x030000: 'Baseline MP Primary Image' + 0x000000: "Undefined", + 0x010001: "Large Thumbnail (VGA Equivalent)", + 0x010002: "Large Thumbnail (Full HD Equivalent)", + 0x020001: "Multi-Frame Image (Panorama)", + 0x020002: "Multi-Frame Image: (Disparity)", + 0x020003: "Multi-Frame Image: (Multi-Angle)", + 0x030000: "Baseline MP Primary Image", } - mpentryattr['MPType'] = mptypemap.get(mpentryattr['MPType'], - 'Unknown') - mpentry['Attribute'] = mpentryattr + mpentryattr["MPType"] = mptypemap.get(mpentryattr["MPType"], "Unknown") + mpentry["Attribute"] = mpentryattr mpentries.append(mpentry) mp[0xB002] = mpentries - except KeyError: - raise SyntaxError("malformed MP Index (bad MP Entry)") + except KeyError as e: + raise SyntaxError("malformed MP Index (bad MP Entry)") 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. @@ -563,73 +583,78 @@ def _getmp(self): "YCbCr": "YCbCr", } -zigzag_index = (0, 1, 5, 6, 14, 15, 27, 28, - 2, 4, 7, 13, 16, 26, 29, 42, - 3, 8, 12, 17, 25, 30, 41, 43, - 9, 11, 18, 24, 31, 40, 44, 53, - 10, 19, 23, 32, 39, 45, 52, 54, - 20, 22, 33, 38, 46, 51, 55, 60, - 21, 34, 37, 47, 50, 56, 59, 61, - 35, 36, 48, 49, 57, 58, 62, 63) - -samplings = {(1, 1, 1, 1, 1, 1): 0, - (2, 1, 1, 1, 1, 1): 1, - (2, 2, 1, 1, 1, 1): 2, - } +# fmt: off +zigzag_index = ( + 0, 1, 5, 6, 14, 15, 27, 28, + 2, 4, 7, 13, 16, 26, 29, 42, + 3, 8, 12, 17, 25, 30, 41, 43, + 9, 11, 18, 24, 31, 40, 44, 53, + 10, 19, 23, 32, 39, 45, 52, 54, + 20, 22, 33, 38, 46, 51, 55, 60, + 21, 34, 37, 47, 50, 56, 59, 61, + 35, 36, 48, 49, 57, 58, 62, 63, +) + +samplings = { + (1, 1, 1, 1, 1, 1): 0, + (2, 1, 1, 1, 1, 1): 1, + (2, 2, 1, 1, 1, 1): 2, +} +# fmt: on def convert_dict_qtables(qtables): - qtables = [qtables[key] for key in range(len(qtables)) if key in qtables] - for idx, table in enumerate(qtables): - qtables[idx] = [table[i] for i in zigzag_index] + deprecate("convert_dict_qtables", 10, action="Conversion is no longer needed") return qtables def get_sampling(im): - # There's no subsampling when image have only 1 layer + # There's no subsampling when images have only 1 layer # (grayscale images) or when they are CMYK (4 layers), - # so set subsampling to default value. + # so set subsampling to the default value. # # NOTE: currently Pillow can't encode JPEG to YCCK format. # If YCCK support is added in the future, subsampling code will have # to be updated (here and in JpegEncode.c) to deal with 4 layers. - if not hasattr(im, 'layers') or im.layers in (1, 4): + if not hasattr(im, "layers") or im.layers in (1, 4): return -1 sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3] return samplings.get(sampling, -1) def _save(im, fp, filename): + if im.width == 0 or im.height == 0: + raise ValueError("cannot write empty image as JPEG") try: rawmode = RAWMODE[im.mode] - except KeyError: - raise IOError("cannot write mode %s as JPEG" % im.mode) + except KeyError as e: + raise OSError(f"cannot write mode {im.mode} as JPEG") from e info = im.encoderinfo - dpi = [int(round(x)) for x in info.get("dpi", (0, 0))] + dpi = [round(x) for x in info.get("dpi", (0, 0))] - quality = info.get("quality", 0) + quality = info.get("quality", -1) subsampling = info.get("subsampling", -1) qtables = info.get("qtables") if quality == "keep": - quality = 0 + quality = -1 subsampling = "keep" qtables = "keep" elif quality in presets: preset = presets[quality] - quality = 0 - subsampling = preset.get('subsampling', -1) - qtables = preset.get('quantization') + quality = -1 + subsampling = preset.get("subsampling", -1) + qtables = preset.get("quantization") elif not isinstance(quality, int): raise ValueError("Invalid quality setting") else: if subsampling in presets: - subsampling = presets[subsampling].get('subsampling', -1) - if isStringType(qtables) and qtables in presets: - qtables = presets[qtables].get('quantization') + subsampling = presets[subsampling].get("subsampling", -1) + if isinstance(qtables, str) and qtables in presets: + qtables = presets[qtables].get("quantization") if subsampling == "4:4:4": subsampling = 0 @@ -643,24 +668,28 @@ 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") + raise ValueError("Cannot use 'keep' when original image is not a JPEG") subsampling = get_sampling(im) def validate_qtables(qtables): if qtables is None: return qtables - if isStringType(qtables): + if isinstance(qtables, str): try: - lines = [int(num) for line in qtables.splitlines() - for num in line.split('#', 1)[0].split()] - except ValueError: - raise ValueError("Invalid quantization table") + lines = [ + int(num) + for line in qtables.splitlines() + for num in line.split("#", 1)[0].split() + ] + except ValueError as e: + raise ValueError("Invalid quantization table") from e else: - qtables = [lines[s:s+64] for s in range(0, len(lines), 64)] + qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)] if isinstance(qtables, (tuple, list, dict)): if isinstance(qtables, dict): - qtables = convert_dict_qtables(qtables) + qtables = [ + qtables[key] for key in range(len(qtables)) if key in qtables + ] elif isinstance(qtables, tuple): qtables = list(qtables) if not (0 < len(qtables) < 5): @@ -668,22 +697,21 @@ def validate_qtables(qtables): for idx, table in enumerate(qtables): try: if len(table) != 64: - raise - table = array.array('B', table) - except TypeError: - raise ValueError("Invalid quantization table") + raise TypeError + table = array.array("H", table) + except TypeError as e: + raise ValueError("Invalid quantization table") 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") + raise ValueError("Cannot use 'keep' when original image is not a JPEG") qtables = getattr(im, "quantization", None) qtables = validate_qtables(qtables) - extra = b"" + extra = info.get("extra", b"") icc_profile = info.get("icc_profile") if icc_profile: @@ -697,18 +725,27 @@ def validate_qtables(qtables): i = 1 for marker in markers: size = struct.pack(">H", 2 + ICC_OVERHEAD_LEN + len(marker)) - extra += (b"\xFF\xE2" + size + b"ICC_PROFILE\0" + o8(i) + - o8(len(markers)) + marker) + extra += ( + b"\xFF\xE2" + + size + + b"ICC_PROFILE\0" + + o8(i) + + o8(len(markers)) + + marker + ) i += 1 # "progressive" is the official name, but older documentation # says "progression" # FIXME: issue a warning if the wrong form is used (post-1.1.7) - progressive = (info.get("progressive", False) or - info.get("progression", False)) + progressive = info.get("progressive", False) or info.get("progression", False) optimize = info.get("optimize", False) + exif = info.get("exif", b"") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + # get keyword arguments im.encoderconfig = ( quality, @@ -716,40 +753,38 @@ def validate_qtables(qtables): info.get("smooth", 0), optimize, info.get("streamtype", 0), - dpi[0], dpi[1], + dpi[0], + dpi[1], subsampling, qtables, extra, - info.get("exif", b"") - ) + exif, + ) # if we optimize, libjpeg needs a buffer big enough to hold the whole image - # in a shot. Guessing on the size, at im.size bytes. (raw pizel size is + # in a shot. Guessing on the size, at im.size bytes. (raw pixel size is # channels*size, this is a value that's been used in a django patch. # https://github.com/matthewwithanm/django-imagekit/issues/50 bufsize = 0 if optimize or progressive: # CMYK can be bigger - if im.mode == 'CMYK': + if im.mode == "CMYK": bufsize = 4 * im.size[0] * im.size[1] - # keep sets quality to 0, but the actual value may be high. - elif quality >= 95 or quality == 0: + # keep sets quality to -1, but the actual value may be high. + elif quality >= 95 or quality == -1: bufsize = 2 * im.size[0] * im.size[1] else: bufsize = im.size[0] * im.size[1] - # The exif info needs to be written as one block, + APP1, + one spare byte. + # The EXIF info needs to be written as one block, + APP1, + one spare byte. # Ensure that our buffer is big enough. Same with the icc_profile block. - bufsize = max(ImageFile.MAXBLOCK, bufsize, len(info.get("exif", b"")) + 5, - len(extra) + 1) + bufsize = max(ImageFile.MAXBLOCK, bufsize, len(exif) + 5, len(extra) + 1) - ImageFile._save(im, fp, [("jpeg", (0, 0)+im.size, 0, rawmode)], bufsize) + ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize) def _save_cjpeg(im, fp, filename): # ALTERNATIVE: handle JPEGs via the IJG command line utilities. - import os - import subprocess tempfile = im._dump() subprocess.check_call(["cjpeg", "-outfile", filename, tempfile]) try: @@ -767,17 +802,21 @@ def jpeg_factory(fp=None, filename=None): if mpheader[45057] > 1: # It's actually an MPO from .MpoImagePlugin import MpoImageFile - im = MpoImageFile(fp, filename) + + # Don't reload everything, just convert it. + im = MpoImageFile.adopt(im, mpheader) except (TypeError, IndexError): # It is really a JPEG pass except SyntaxError: - warnings.warn("Image appears to be a malformed MPO file, it will be " - "interpreted as a base JPEG file") + warnings.warn( + "Image appears to be a malformed MPO file, it will be " + "interpreted as a base JPEG file" + ) return im -# -------------------------------------------------------------------q- +# --------------------------------------------------------------------- # Registry stuff Image.register_open(JpegImageFile.format, jpeg_factory, _accept) diff --git a/src/PIL/JpegPresets.py b/src/PIL/JpegPresets.py index 5f01f0d2d1e..a678e248e9a 100644 --- a/src/PIL/JpegPresets.py +++ b/src/PIL/JpegPresets.py @@ -1,9 +1,11 @@ """ JPEG quality settings equivalent to the Photoshop settings. +Can be used when saving JPEG files. -More presets can be added to the presets dict if needed. - -Can be use when saving JPEG file. +The following presets are available by default: +``web_low``, ``web_medium``, ``web_high``, ``web_very_high``, ``web_maximum``, +``low``, ``medium``, ``high``, ``maximum``. +More presets can be added to the :py:data:`presets` dict if needed. To apply the preset, specify:: @@ -21,7 +23,6 @@ im.save("image_name.jpg", quality="web_high") - Subsampling ----------- @@ -33,7 +34,10 @@ 4:2:0. You can get the subsampling of a JPEG with the -`JpegImagePlugin.get_subsampling(im)` function. +:func:`.JpegImagePlugin.get_sampling` function. + +In JPEG compressed data a JPEG marker is used instead of an EXIF tag. +(ref.: https://exiv2.org/tags.html) Quantization tables @@ -48,24 +52,18 @@ im.quantization -This will return a dict with a number of arrays. You can pass this dict +This will return a dict with a number of lists. You can pass this dict directly as the qtables argument when saving a JPEG. -The tables format between im.quantization and quantization in presets differ in -3 ways: - -1. The base container of the preset is a list with sublists instead of dict. - dict[0] -> list[0], dict[1] -> list[1], ... -2. Each table in a preset is a list instead of an array. -3. The zigzag order is remove in the preset (needed by libjpeg >= 6a). - -You can convert the dict format to the preset format with the -`JpegImagePlugin.convert_dict_qtables(dict_qtables)` function. +The quantization table format in presets is a list with sublists. These formats +are interchangeable. -Libjpeg ref.: https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html +Libjpeg ref.: +https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html """ +# fmt: off presets = { 'web_low': {'subsampling': 2, # "4:2:0" 'quantization': [ @@ -107,16 +105,16 @@ ]}, 'web_high': {'subsampling': 0, # "4:4:4" 'quantization': [ - [6, 4, 4, 6, 9, 11, 12, 16, - 4, 5, 5, 6, 8, 10, 12, 12, - 4, 5, 5, 6, 10, 12, 14, 19, - 6, 6, 6, 11, 12, 15, 19, 28, - 9, 8, 10, 12, 16, 20, 27, 31, + [6, 4, 4, 6, 9, 11, 12, 16, + 4, 5, 5, 6, 8, 10, 12, 12, + 4, 5, 5, 6, 10, 12, 14, 19, + 6, 6, 6, 11, 12, 15, 19, 28, + 9, 8, 10, 12, 16, 20, 27, 31, 11, 10, 12, 15, 20, 27, 31, 31, 12, 12, 14, 19, 27, 31, 31, 31, 16, 12, 19, 28, 31, 31, 31, 31], - [7, 7, 13, 24, 26, 31, 31, 31, - 7, 12, 16, 21, 31, 31, 31, 31, + [7, 7, 13, 24, 26, 31, 31, 31, + 7, 12, 16, 21, 31, 31, 31, 31, 13, 16, 17, 31, 31, 31, 31, 31, 24, 21, 31, 31, 31, 31, 31, 31, 26, 31, 31, 31, 31, 31, 31, 31, @@ -126,18 +124,18 @@ ]}, 'web_very_high': {'subsampling': 0, # "4:4:4" 'quantization': [ - [2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 4, 5, 7, 9, - 2, 2, 2, 4, 5, 7, 9, 12, - 3, 3, 4, 5, 8, 10, 12, 12, - 4, 4, 5, 7, 10, 12, 12, 12, - 5, 5, 7, 9, 12, 12, 12, 12, - 6, 6, 9, 12, 12, 12, 12, 12], - [3, 3, 5, 9, 13, 15, 15, 15, - 3, 4, 6, 11, 14, 12, 12, 12, - 5, 6, 9, 14, 12, 12, 12, 12, - 9, 11, 14, 12, 12, 12, 12, 12, + [2, 2, 2, 2, 3, 4, 5, 6, + 2, 2, 2, 2, 3, 4, 5, 6, + 2, 2, 2, 2, 4, 5, 7, 9, + 2, 2, 2, 4, 5, 7, 9, 12, + 3, 3, 4, 5, 8, 10, 12, 12, + 4, 4, 5, 7, 10, 12, 12, 12, + 5, 5, 7, 9, 12, 12, 12, 12, + 6, 6, 9, 12, 12, 12, 12, 12], + [3, 3, 5, 9, 13, 15, 15, 15, + 3, 4, 6, 11, 14, 12, 12, 12, + 5, 6, 9, 14, 12, 12, 12, 12, + 9, 11, 14, 12, 12, 12, 12, 12, 13, 14, 12, 12, 12, 12, 12, 12, 15, 12, 12, 12, 12, 12, 12, 12, 15, 12, 12, 12, 12, 12, 12, 12, @@ -184,8 +182,8 @@ 'medium': {'subsampling': 2, # "4:2:0" 'quantization': [ [12, 8, 8, 12, 17, 21, 24, 17, - 8, 9, 9, 11, 15, 19, 12, 12, - 8, 9, 10, 12, 19, 12, 12, 12, + 8, 9, 9, 11, 15, 19, 12, 12, + 8, 9, 10, 12, 19, 12, 12, 12, 12, 11, 12, 21, 12, 12, 12, 12, 17, 15, 19, 12, 12, 12, 12, 12, 21, 19, 12, 12, 12, 12, 12, 12, @@ -202,16 +200,16 @@ ]}, 'high': {'subsampling': 0, # "4:4:4" 'quantization': [ - [6, 4, 4, 6, 9, 11, 12, 16, - 4, 5, 5, 6, 8, 10, 12, 12, - 4, 5, 5, 6, 10, 12, 12, 12, - 6, 6, 6, 11, 12, 12, 12, 12, - 9, 8, 10, 12, 12, 12, 12, 12, + [6, 4, 4, 6, 9, 11, 12, 16, + 4, 5, 5, 6, 8, 10, 12, 12, + 4, 5, 5, 6, 10, 12, 12, 12, + 6, 6, 6, 11, 12, 12, 12, 12, + 9, 8, 10, 12, 12, 12, 12, 12, 11, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 16, 12, 12, 12, 12, 12, 12, 12], - [7, 7, 13, 24, 20, 20, 17, 17, - 7, 12, 16, 14, 14, 12, 12, 12, + [7, 7, 13, 24, 20, 20, 17, 17, + 7, 12, 16, 14, 14, 12, 12, 12, 13, 16, 14, 14, 12, 12, 12, 12, 24, 14, 14, 12, 12, 12, 12, 12, 20, 14, 12, 12, 12, 12, 12, 12, @@ -221,21 +219,22 @@ ]}, 'maximum': {'subsampling': 0, # "4:4:4" 'quantization': [ - [2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 3, 4, 5, 6, - 2, 2, 2, 2, 4, 5, 7, 9, - 2, 2, 2, 4, 5, 7, 9, 12, - 3, 3, 4, 5, 8, 10, 12, 12, - 4, 4, 5, 7, 10, 12, 12, 12, - 5, 5, 7, 9, 12, 12, 12, 12, - 6, 6, 9, 12, 12, 12, 12, 12], - [3, 3, 5, 9, 13, 15, 15, 15, - 3, 4, 6, 10, 14, 12, 12, 12, - 5, 6, 9, 14, 12, 12, 12, 12, - 9, 10, 14, 12, 12, 12, 12, 12, + [2, 2, 2, 2, 3, 4, 5, 6, + 2, 2, 2, 2, 3, 4, 5, 6, + 2, 2, 2, 2, 4, 5, 7, 9, + 2, 2, 2, 4, 5, 7, 9, 12, + 3, 3, 4, 5, 8, 10, 12, 12, + 4, 4, 5, 7, 10, 12, 12, 12, + 5, 5, 7, 9, 12, 12, 12, 12, + 6, 6, 9, 12, 12, 12, 12, 12], + [3, 3, 5, 9, 13, 15, 15, 15, + 3, 4, 6, 10, 14, 12, 12, 12, + 5, 6, 9, 14, 12, 12, 12, 12, + 9, 10, 14, 12, 12, 12, 12, 12, 13, 14, 12, 12, 12, 12, 12, 12, 15, 12, 12, 12, 12, 12, 12, 12, 15, 12, 12, 12, 12, 12, 12, 12, 15, 12, 12, 12, 12, 12, 12, 12] ]}, } +# fmt: on diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 06da33f7778..cd047fe9d9d 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -17,9 +17,8 @@ # import struct -from . import Image, ImageFile -__version__ = "0.2" +from . import Image, ImageFile def _accept(s): @@ -29,6 +28,7 @@ def _accept(s): ## # Image plugin for McIdas area images. + class McIdasImageFile(ImageFile.ImageFile): format = "MCIDAS" @@ -59,10 +59,10 @@ def _open(self): raise SyntaxError("unsupported McIdas format") self.mode = mode - self.size = w[10], w[9] + self._size = w[10], w[9] offset = w[34] + w[15] - stride = w[15] + w[10]*w[11]*w[14] + stride = w[15] + w[10] * w[11] * w[14] self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))] diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 1dbb6a588e1..d4f6c90f778 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -17,12 +17,9 @@ # -from . import Image, TiffImagePlugin - import olefile -__version__ = "0.1" - +from . import Image, TiffImagePlugin # # -------------------------------------------------------------------- @@ -35,6 +32,7 @@ def _accept(prefix): ## # Image plugin for Microsoft's Image Composer file format. + class MicImageFile(TiffImagePlugin.TiffImageFile): format = "MIC" @@ -48,8 +46,8 @@ def _open(self): try: self.ole = olefile.OleFileIO(self.fp) - except IOError: - raise SyntaxError("not an MIC file; invalid OLE file") + except OSError as e: + raise SyntaxError("not an MIC file; invalid OLE file") from e # find ACI subfiles with Image members (maybe not the # best way to identify MIC files, but what the... ;-) @@ -64,29 +62,22 @@ def _open(self): if not self.images: raise SyntaxError("not an MIC file; no image entries") - self.__fp = self.fp self.frame = None + self._n_frames = len(self.images) + self.is_animated = self._n_frames > 1 if len(self.images) > 1: - self.category = Image.CONTAINER + self._category = Image.CONTAINER self.seek(0) - @property - def n_frames(self): - return len(self.images) - - @property - def is_animated(self): - return len(self.images) > 1 - def seek(self, frame): if not self._seek_check(frame): return try: filename = self.images[frame] - except IndexError: - raise EOFError("no such frame") + except IndexError as e: + raise EOFError("no such frame") from e self.fp = self.ole.openstream(filename) @@ -95,7 +86,6 @@ def seek(self, frame): self.frame = frame def tell(self): - return self.frame diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index fca7f9d9fe4..a358dfdce62 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -17,14 +17,11 @@ from . import Image, ImageFile from ._binary import i8 -__version__ = "0.1" - - # # Bitstream parser -class BitStream(object): +class BitStream: def __init__(self, fp): self.fp = fp self.bits = 0 @@ -59,6 +56,7 @@ def read(self, bits): # Image plugin for MPEG streams. This plugin can identify a stream, # but it cannot read it. + class MpegImageFile(ImageFile.ImageFile): format = "MPEG" @@ -72,7 +70,7 @@ def _open(self): raise SyntaxError("not an MPEG file") self.mode = "RGB" - self.size = s.read(12), s.read(12) + self._size = s.read(12), s.read(12) # -------------------------------------------------------------------- diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 8ad27b919c6..5bfd8efc1a6 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -18,74 +18,155 @@ # See the README file for information on usage and redistribution. # -from . import Image, JpegImagePlugin +import itertools +import os +import struct -__version__ = "0.1" +from . import Image, ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin +from ._binary import i16be as i16 +from ._binary import o32le - -def _accept(prefix): - return JpegImagePlugin._accept(prefix) +# def _accept(prefix): +# return JpegImagePlugin._accept(prefix) def _save(im, fp, filename): - # Note that we can only save the current frame at present - return JpegImagePlugin._save(im, fp, filename) + JpegImagePlugin._save(im, fp, filename) + + +def _save_all(im, fp, filename): + append_images = im.encoderinfo.get("append_images", []) + if not append_images: + try: + animated = im.is_animated + except AttributeError: + animated = False + if not animated: + _save(im, fp, filename) + return + + 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 + ) + JpegImagePlugin._save(im_frame, fp, filename) + offsets.append(fp.tell()) + else: + im_frame.save(fp, "JPEG") + offsets.append(fp.tell() - offsets[-1]) + + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[0xB001] = len(offsets) + + mpentries = b"" + data_offset = 0 + for i, size in enumerate(offsets): + if i == 0: + mptype = 0x030000 # Baseline MP Primary Image + else: + mptype = 0x000000 # Undefined + mpentries += struct.pack(" 1 + self._fp = self.fp # FIXME: hack + self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame self.__frame = 0 self.offset = 0 # for now we can only handle reading and individual frame extraction self.readonly = 1 def load_seek(self, pos): - self.__fp.seek(pos) - - @property - def n_frames(self): - return self.__framecount - - @property - def is_animated(self): - return self.__framecount > 1 + self._fp.seek(pos) def seek(self, frame): if not self._seek_check(frame): return - self.fp = self.__fp + self.fp = self._fp self.offset = self.__mpoffsets[frame] - self.tile = [ - ("jpeg", (0, 0) + self.size, self.offset, (self.mode, "")) - ] + + self.fp.seek(self.offset + 2) # skip SOI marker + segment = self.fp.read(2) + if not segment: + raise ValueError("No data found for frame") + self._size = self._initial_size + if i16(segment) == 0xFFE1: # APP1 + n = i16(self.fp.read(2)) - 2 + self.info["exif"] = ImageFile._safe_read(self.fp, n) + self._reload_exif() + + mptype = self.mpinfo[0xB002][frame]["Attribute"]["MPType"] + if mptype.startswith("Large Thumbnail"): + exif = self.getexif().get_ifd(0x8769) + if 40962 in exif and 40963 in exif: + self._size = (exif[40962], exif[40963]) + elif "exif" in self.info: + del self.info["exif"] + self._reload_exif() + + self.tile = [("jpeg", (0, 0) + self.size, self.offset, (self.mode, ""))] self.__frame = frame def tell(self): return self.__frame + @staticmethod + def adopt(jpeg_instance, mpheader=None): + """ + Transform the instance of JpegImageFile into + an instance of MpoImageFile. + After the call, the JpegImageFile is extended + to be an MpoImageFile. + + This is essentially useful when opening a JPEG + file that reveals itself as an MPO, to avoid + double call to _open. + """ + jpeg_instance.__class__ = MpoImageFile + jpeg_instance._after_jpeg_open(mpheader) + return jpeg_instance + -# -------------------------------------------------------------------q- +# --------------------------------------------------------------------- # Registry stuff # Note that since MPO shares a factory with JPEG, we do not need to do a @@ -93,6 +174,7 @@ def tell(self): # Image.register_open(MpoImageFile.format, # JpegImagePlugin.jpeg_factory, _accept) Image.register_save(MpoImageFile.format, _save) +Image.register_save_all(MpoImageFile.format, _save_all) Image.register_extension(MpoImageFile.format, ".mpo") diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 5ea3c1c49a2..c4d7ddbb4f8 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -21,15 +21,14 @@ # Figure 205. Windows Paint Version 1: "DanM" Format # Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03 # -# See also: http://www.fileformat.info/format/mspaint/egff.htm +# See also: https://www.fileformat.info/format/mspaint/egff.htm -from . import Image, ImageFile -from ._binary import i16le as i16, o16le as o16, i8 -import struct import io +import struct -__version__ = "0.1" - +from . import Image, ImageFile +from ._binary import i16le as i16 +from ._binary import o16le as o16 # # read MSP files @@ -43,6 +42,7 @@ def _accept(prefix): # Image plugin for Windows MSP images. This plugin supports both # uncompressed (Windows 1.0). + class MspImageFile(ImageFile.ImageFile): format = "MSP" @@ -52,28 +52,28 @@ def _open(self): # Header s = self.fp.read(32) - if s[:4] not in [b"DanM", b"LinS"]: + if not _accept(s): raise SyntaxError("not an MSP file") # Header checksum checksum = 0 for i in range(0, 32, 2): - checksum = checksum ^ i16(s[i:i+2]) + checksum = checksum ^ i16(s, i) if checksum != 0: raise SyntaxError("bad MSP checksum") self.mode = "1" - self.size = i16(s[4:]), i16(s[6:]) + self._size = i16(s, 4), i16(s, 6) if s[:4] == b"DanM": - self.tile = [("raw", (0, 0)+self.size, 32, ("1", 0, 1))] + self.tile = [("raw", (0, 0) + self.size, 32, ("1", 0, 1))] else: - self.tile = [("MSP", (0, 0)+self.size, 32, None)] + self.tile = [("MSP", (0, 0) + self.size, 32, None)] class MspDecoder(ImageFile.PyDecoder): # The algo for the MSP decoder is from - # http://www.fileformat.info/format/mspaint/egff.htm + # https://www.fileformat.info/format/mspaint/egff.htm # cc-by-attribution -- That page references is taken from the # Encyclopedia of Graphics File Formats and is licensed by # O'Reilly under the Creative Common/Attribution license @@ -111,13 +111,14 @@ class MspDecoder(ImageFile.PyDecoder): def decode(self, buffer): img = io.BytesIO() - blank_line = bytearray((0xff,)*((self.state.xsize+7)//8)) + blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) try: self.fd.seek(32) - rowmap = struct.unpack_from("<%dH" % (self.state.ysize), - self.fd.read(self.state.ysize*2)) - except struct.error: - raise IOError("Truncated MSP file in row map") + rowmap = struct.unpack_from( + f"<{self.state.ysize}H", self.fd.read(self.state.ysize * 2) + ) + except struct.error as e: + raise OSError("Truncated MSP file in row map") from e for x, rowlen in enumerate(rowmap): try: @@ -126,30 +127,31 @@ def decode(self, buffer): continue row = self.fd.read(rowlen) if len(row) != rowlen: - raise IOError("Truncated MSP file, expected %d bytes on row %s", - (rowlen, x)) + raise OSError( + "Truncated MSP file, expected %d bytes on row %s", (rowlen, x) + ) idx = 0 while idx < rowlen: - runtype = i8(row[idx]) + runtype = row[idx] idx += 1 if runtype == 0: - (runcount, runval) = struct.unpack("Bc", row[idx:idx+2]) + (runcount, runval) = struct.unpack_from("Bc", row, idx) img.write(runval * runcount) idx += 2 else: runcount = runtype - img.write(row[idx:idx+runcount]) + img.write(row[idx : idx + runcount]) idx += runcount - except struct.error: - raise IOError("Corrupted MSP file in row %d" % x) + except struct.error as e: + raise OSError(f"Corrupted MSP file in row {x}") from e self.set_as_raw(img.getvalue(), ("1", 0, 1)) - return 0, 0 + return -1, 0 -Image.register_decoder('MSP', MspDecoder) +Image.register_decoder("MSP", MspDecoder) # @@ -159,7 +161,7 @@ def decode(self, buffer): def _save(im, fp, filename): if im.mode != "1": - raise IOError("cannot write mode %s as MSP" % im.mode) + raise OSError(f"cannot write mode {im.mode} as MSP") # create MSP header header = [0] * 16 @@ -180,7 +182,7 @@ def _save(im, fp, filename): fp.write(o16(h)) # image body - ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 32, ("1", 0, 1))]) + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) # diff --git a/src/PIL/OleFileIO.py b/src/PIL/OleFileIO.py deleted file mode 100644 index b3caa10d5e4..00000000000 --- a/src/PIL/OleFileIO.py +++ /dev/null @@ -1,4 +0,0 @@ -raise ImportError( - 'PIL.OleFileIO is deprecated. Use the olefile Python package ' - 'instead. This module will be removed in a future version.' -) diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index fe0823860d7..13b3048f67e 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -2,7 +2,7 @@ # The Python Imaging Library # $Id$ # -# simple postscript graphics interface +# Simple PostScript graphics interface # # History: # 1996-04-20 fl Created @@ -15,49 +15,47 @@ # See the README file for information on usage and redistribution. # -from . import EpsImagePlugin import sys +from . import EpsImagePlugin + ## -# Simple Postscript graphics interface. +# Simple PostScript graphics interface. -class PSDraw(object): +class PSDraw: """ - Sets up printing to the given file. If **file** is omitted, - :py:attr:`sys.stdout` is assumed. + Sets up printing to the given file. If ``fp`` is omitted, + ``sys.stdout.buffer`` or ``sys.stdout`` is assumed. """ def __init__(self, fp=None): if not fp: - fp = sys.stdout + try: + fp = sys.stdout.buffer + except AttributeError: + fp = sys.stdout self.fp = fp - def _fp_write(self, to_write): - if bytes is str or self.fp == sys.stdout: - self.fp.write(to_write) - else: - self.fp.write(bytes(to_write, 'UTF-8')) - def begin_document(self, id=None): - """Set up printing of a document. (Write Postscript DSC header.)""" + """Set up printing of a document. (Write PostScript DSC header.)""" # FIXME: incomplete - self._fp_write("%!PS-Adobe-3.0\n" - "save\n" - "/showpage { } def\n" - "%%EndComments\n" - "%%BeginDocument\n") - # self._fp_write(ERROR_PS) # debugging! - self._fp_write(EDROFF_PS) - self._fp_write(VDI_PS) - self._fp_write("%%EndProlog\n") + self.fp.write( + b"%!PS-Adobe-3.0\n" + b"save\n" + b"/showpage { } def\n" + b"%%EndComments\n" + b"%%BeginDocument\n" + ) + # self.fp.write(ERROR_PS) # debugging! + self.fp.write(EDROFF_PS) + self.fp.write(VDI_PS) + self.fp.write(b"%%EndProlog\n") self.isofont = {} def end_document(self): - """Ends printing. (Write Postscript DSC footer.)""" - self._fp_write("%%EndDocument\n" - "restore showpage\n" - "%%End\n") + """Ends printing. (Write PostScript DSC footer.)""" + self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n") if hasattr(self.fp, "flush"): self.fp.flush() @@ -65,50 +63,44 @@ def setfont(self, font, size): """ Selects which font to use. - :param font: A Postscript font name + :param font: A PostScript font name :param size: Size in points. """ + font = bytes(font, "UTF-8") if font not in self.isofont: # reencode font - self._fp_write("/PSDraw-%s ISOLatin1Encoding /%s E\n" % - (font, font)) + self.fp.write(b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font, font)) self.isofont[font] = 1 # rough - self._fp_write("/F0 %d /PSDraw-%s F\n" % (size, font)) + self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font)) def line(self, xy0, xy1): """ Draws a line between the two points. Coordinates are given in - Postscript point coordinates (72 points per inch, (0, 0) is the lower + PostScript point coordinates (72 points per inch, (0, 0) is the lower left corner of the page). """ - xy = xy0 + xy1 - self._fp_write("%d %d %d %d Vl\n" % xy) + self.fp.write(b"%d %d %d %d Vl\n" % (*xy0, *xy1)) def rectangle(self, box): """ Draws a rectangle. - :param box: A 4-tuple of integers whose order and function is currently - undocumented. - - Hint: the tuple is passed into this format string: - - .. code-block:: python - - %d %d M %d %d 0 Vr\n + :param box: A tuple of four integers, specifying left, bottom, width and + height. """ - self._fp_write("%d %d M %d %d 0 Vr\n" % box) + self.fp.write(b"%d %d M 0 %d %d Vr\n" % box) def text(self, xy, text): """ Draws text at the given position. You must use :py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method. """ - text = "\\(".join(text.split("(")) - text = "\\)".join(text.split(")")) - xy = xy + (text,) - self._fp_write("%d %d M (%s) S\n" % xy) + text = bytes(text, "UTF-8") + text = b"\\(".join(text.split(b"(")) + text = b"\\)".join(text.split(b")")) + xy += (text,) + self.fp.write(b"%d %d M (%s) S\n" % xy) def image(self, box, im, dpi=None): """Draw a PIL image, centered in the given box.""" @@ -119,8 +111,8 @@ def image(self, box, im, dpi=None): else: dpi = 100 # greyscale # image size (on paper) - x = float(im.size[0] * 72) / dpi - y = float(im.size[1] * 72) / dpi + x = im.size[0] * 72 / dpi + y = im.size[1] * 72 / dpi # max allowed size xmax = float(box[2] - box[0]) ymax = float(box[3] - box[1]) @@ -132,20 +124,21 @@ def image(self, box, im, dpi=None): y = ymax dx = (xmax - x) / 2 + box[0] dy = (ymax - y) / 2 + box[1] - self._fp_write("gsave\n%f %f translate\n" % (dx, dy)) + self.fp.write(b"gsave\n%f %f translate\n" % (dx, dy)) if (x, y) != im.size: # EpsImagePlugin._save prints the image at (0,0,xsize,ysize) sx = x / im.size[0] sy = y / im.size[1] - self._fp_write("%f %f scale\n" % (sx, sy)) + self.fp.write(b"%f %f scale\n" % (sx, sy)) EpsImagePlugin._save(im, self.fp, None, 0) - self._fp_write("\ngrestore\n") + self.fp.write(b"\ngrestore\n") + # -------------------------------------------------------------------- -# Postscript driver +# PostScript driver # -# EDROFF.PS -- Postscript driver for Edroff 2 +# EDROFF.PS -- PostScript driver for Edroff 2 # # History: # 94-01-25 fl: created (edroff 2.04) @@ -153,7 +146,8 @@ def image(self, box, im, dpi=None): # Copyright (c) Fredrik Lundh 1994. # -EDROFF_PS = """\ + +EDROFF_PS = b"""\ /S { show } bind def /P { moveto show } bind def /M { moveto } bind def @@ -174,7 +168,7 @@ def image(self, box, im, dpi=None): """ # -# VDI.PS -- Postscript driver for VDI meta commands +# VDI.PS -- PostScript driver for VDI meta commands # # History: # 94-01-25 fl: created (edroff 2.04) @@ -182,16 +176,16 @@ def image(self, box, im, dpi=None): # Copyright (c) Fredrik Lundh 1994. # -VDI_PS = """\ +VDI_PS = b"""\ /Vm { moveto } bind def /Va { newpath arcn stroke } bind def /Vl { moveto lineto stroke } bind def /Vc { newpath 0 360 arc closepath } bind def /Vr { exch dup 0 rlineto - exch dup neg 0 exch rlineto + exch dup 0 exch rlineto exch neg 0 rlineto - 0 exch rlineto - 100 div setgray fill 0 setgray } bind def + 0 exch neg rlineto + setgray fill } bind def /Tm matrix def /Ve { Tm currentmatrix pop translate scale newpath 0 0 .5 0 360 arc closepath @@ -207,7 +201,7 @@ def image(self, box, im, dpi=None): # 89-11-21 fl: created (pslist 1.10) # -ERROR_PS = """\ +ERROR_PS = b"""\ /landscape false def /errorBUF 200 string def /errorNL { currentpoint 10 sub exch pop 72 exch moveto } def diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index 9ed69d687dc..ee9dca86017 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -16,10 +16,8 @@ from ._binary import o8 -## -# File handler for Teragon-style palette files. - -class PaletteFile(object): +class PaletteFile: + """File handler for Teragon-style palette files.""" rawmode = "RGB" @@ -33,7 +31,7 @@ def __init__(self, fp): if not s: break - if s[0:1] == b"#": + if s[:1] == b"#": continue if len(s) > 100: raise SyntaxError("bad palette file") diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index e2b9d15ec4f..700f10e3f79 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -8,10 +8,10 @@ ## from . import Image, ImageFile -from ._binary import o8, o16be as o16b - -__version__ = "1.0" +from ._binary import o8 +from ._binary import o16be as o16b +# fmt: off _Palm8BitColormapValues = ( (255, 255, 255), (255, 204, 255), (255, 153, 255), (255, 102, 255), (255, 51, 255), (255, 0, 255), (255, 255, 204), (255, 204, 204), @@ -31,15 +31,15 @@ (102, 255, 204), (102, 204, 204), (102, 153, 204), (102, 102, 204), (102, 51, 204), (102, 0, 204), (102, 255, 153), (102, 204, 153), (102, 153, 153), (102, 102, 153), (102, 51, 153), (102, 0, 153), - (51, 255, 255), (51, 204, 255), (51, 153, 255), (51, 102, 255), - (51, 51, 255), (51, 0, 255), (51, 255, 204), (51, 204, 204), - (51, 153, 204), (51, 102, 204), (51, 51, 204), (51, 0, 204), - (51, 255, 153), (51, 204, 153), (51, 153, 153), (51, 102, 153), - (51, 51, 153), (51, 0, 153), (0, 255, 255), (0, 204, 255), - (0, 153, 255), (0, 102, 255), (0, 51, 255), (0, 0, 255), - (0, 255, 204), (0, 204, 204), (0, 153, 204), (0, 102, 204), - (0, 51, 204), (0, 0, 204), (0, 255, 153), (0, 204, 153), - (0, 153, 153), (0, 102, 153), (0, 51, 153), (0, 0, 153), + (51, 255, 255), (51, 204, 255), (51, 153, 255), (51, 102, 255), + (51, 51, 255), (51, 0, 255), (51, 255, 204), (51, 204, 204), + (51, 153, 204), (51, 102, 204), (51, 51, 204), (51, 0, 204), + (51, 255, 153), (51, 204, 153), (51, 153, 153), (51, 102, 153), + (51, 51, 153), (51, 0, 153), (0, 255, 255), (0, 204, 255), + (0, 153, 255), (0, 102, 255), (0, 51, 255), (0, 0, 255), + (0, 255, 204), (0, 204, 204), (0, 153, 204), (0, 102, 204), + (0, 51, 204), (0, 0, 204), (0, 255, 153), (0, 204, 153), + (0, 153, 153), (0, 102, 153), (0, 51, 153), (0, 0, 153), (255, 255, 102), (255, 204, 102), (255, 153, 102), (255, 102, 102), (255, 51, 102), (255, 0, 102), (255, 255, 51), (255, 204, 51), (255, 153, 51), (255, 102, 51), (255, 51, 51), (255, 0, 51), @@ -58,25 +58,26 @@ (102, 255, 51), (102, 204, 51), (102, 153, 51), (102, 102, 51), (102, 51, 51), (102, 0, 51), (102, 255, 0), (102, 204, 0), (102, 153, 0), (102, 102, 0), (102, 51, 0), (102, 0, 0), - (51, 255, 102), (51, 204, 102), (51, 153, 102), (51, 102, 102), - (51, 51, 102), (51, 0, 102), (51, 255, 51), (51, 204, 51), - (51, 153, 51), (51, 102, 51), (51, 51, 51), (51, 0, 51), - (51, 255, 0), (51, 204, 0), (51, 153, 0), (51, 102, 0), - (51, 51, 0), (51, 0, 0), (0, 255, 102), (0, 204, 102), - (0, 153, 102), (0, 102, 102), (0, 51, 102), (0, 0, 102), - (0, 255, 51), (0, 204, 51), (0, 153, 51), (0, 102, 51), - (0, 51, 51), (0, 0, 51), (0, 255, 0), (0, 204, 0), - (0, 153, 0), (0, 102, 0), (0, 51, 0), (17, 17, 17), - (34, 34, 34), (68, 68, 68), (85, 85, 85), (119, 119, 119), + (51, 255, 102), (51, 204, 102), (51, 153, 102), (51, 102, 102), + (51, 51, 102), (51, 0, 102), (51, 255, 51), (51, 204, 51), + (51, 153, 51), (51, 102, 51), (51, 51, 51), (51, 0, 51), + (51, 255, 0), (51, 204, 0), (51, 153, 0), (51, 102, 0), + (51, 51, 0), (51, 0, 0), (0, 255, 102), (0, 204, 102), + (0, 153, 102), (0, 102, 102), (0, 51, 102), (0, 0, 102), + (0, 255, 51), (0, 204, 51), (0, 153, 51), (0, 102, 51), + (0, 51, 51), (0, 0, 51), (0, 255, 0), (0, 204, 0), + (0, 153, 0), (0, 102, 0), (0, 51, 0), (17, 17, 17), + (34, 34, 34), (68, 68, 68), (85, 85, 85), (119, 119, 119), (136, 136, 136), (170, 170, 170), (187, 187, 187), (221, 221, 221), (238, 238, 238), (192, 192, 192), (128, 0, 0), (128, 0, 128), - (0, 128, 0), (0, 128, 128), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), - (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0)) + (0, 128, 0), (0, 128, 128), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0)) +# fmt: on # so build a prototype image to be used for palette resampling @@ -86,10 +87,11 @@ def build_prototype_image(): palettedata = () for colormapValue in _Palm8BitColormapValues: palettedata += colormapValue - palettedata += (0, 0, 0)*(256 - len(_Palm8BitColormapValues)) + palettedata += (0, 0, 0) * (256 - len(_Palm8BitColormapValues)) image.putpalette(palettedata) return image + Palm8BitColormapImage = build_prototype_image() # OK, we now have in Palm8BitColormapImage, @@ -97,17 +99,9 @@ def build_prototype_image(): # # -------------------------------------------------------------------- -_FLAGS = { - "custom-colormap": 0x4000, - "is-compressed": 0x8000, - "has-transparent": 0x2000, - } +_FLAGS = {"custom-colormap": 0x4000, "is-compressed": 0x8000, "has-transparent": 0x2000} -_COMPRESSION_TYPES = { - "none": 0xFF, - "rle": 0x01, - "scanline": 0x00, - } +_COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00} # @@ -116,6 +110,7 @@ def build_prototype_image(): ## # (Internal) Image save plugin for the Palm format. + def _save(im, fp, filename): if im.mode == "P": @@ -127,28 +122,24 @@ def _save(im, fp, filename): bpp = 8 version = 1 - elif (im.mode == "L" and - "bpp" in im.encoderinfo and - im.encoderinfo["bpp"] in (1, 2, 4)): - - # this is 8-bit grayscale, so we shift it to get the high-order bits, - # and invert it because - # Palm does greyscale from white (0) to black (1) - bpp = im.encoderinfo["bpp"] - im = im.point( - lambda x, shift=8-bpp, maxval=(1 << bpp)-1: maxval - (x >> shift)) - # we ignore the palette here - im.mode = "P" - rawmode = "P;" + str(bpp) - version = 1 - - elif im.mode == "L" and "bpp" in im.info and im.info["bpp"] in (1, 2, 4): + elif im.mode == "L": + if im.encoderinfo.get("bpp") in (1, 2, 4): + # this is 8-bit grayscale, so we shift it to get the high-order bits, + # and invert it because + # Palm does greyscale from white (0) to black (1) + bpp = im.encoderinfo["bpp"] + im = im.point( + lambda x, shift=8 - bpp, maxval=(1 << bpp) - 1: maxval - (x >> shift) + ) + elif im.info.get("bpp") in (1, 2, 4): + # here we assume that even though the inherent mode is 8-bit grayscale, + # only the lower bpp bits are significant. + # We invert them to match the Palm. + 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") - # here we assume that even though the inherent mode is 8-bit grayscale, - # only the lower bpp bits are significant. - # We invert them to match the Palm. - bpp = im.info["bpp"] - im = im.point(lambda x, maxval=(1 << bpp)-1: maxval - (x & maxval)) # we ignore the palette here im.mode = "P" rawmode = "P;" + str(bpp) @@ -163,7 +154,7 @@ def _save(im, fp, filename): else: - raise IOError("cannot write mode %s as Palm" % im.mode) + raise OSError(f"cannot write mode {im.mode} as Palm") # # make sure image data is available @@ -174,7 +165,7 @@ def _save(im, fp, filename): cols = im.size[0] rows = im.size[1] - rowbytes = int((cols + (16//bpp - 1)) / (16 // bpp)) * 2 + rowbytes = int((cols + (16 // bpp - 1)) / (16 // bpp)) * 2 transparent_index = 0 compression_type = _COMPRESSION_TYPES["none"] @@ -198,7 +189,7 @@ def _save(im, fp, filename): fp.write(o16b(offset)) fp.write(o8(transparent_index)) fp.write(o8(compression_type)) - fp.write(o16b(0)) # reserved by Palm + fp.write(o16b(0)) # reserved by Palm # now write colormap if necessary @@ -206,20 +197,21 @@ def _save(im, fp, filename): fp.write(o16b(256)) for i in range(256): fp.write(o8(i)) - if colormapmode == 'RGB': + if colormapmode == "RGB": fp.write( - o8(colormap[3 * i]) + - o8(colormap[3 * i + 1]) + - o8(colormap[3 * i + 2])) - elif colormapmode == 'RGBA': + o8(colormap[3 * i]) + + o8(colormap[3 * i + 1]) + + o8(colormap[3 * i + 2]) + ) + elif colormapmode == "RGBA": fp.write( - o8(colormap[4 * i]) + - o8(colormap[4 * i + 1]) + - o8(colormap[4 * i + 2])) + o8(colormap[4 * i]) + + o8(colormap[4 * i + 1]) + + o8(colormap[4 * i + 2]) + ) # now convert data to raw form - ImageFile._save( - im, fp, [("raw", (0, 0)+im.size, 0, (rawmode, rowbytes, 1))]) + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))]) if hasattr(fp, "flush"): fp.flush() diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index fa95b5008f6..38caf5c63c1 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -16,16 +16,13 @@ from . import Image, ImageFile -from ._binary import i8 - -__version__ = "0.1" - ## # Image plugin for PhotoCD images. This plugin only reads the 768x512 # image from the file; higher resolutions are encoded in a proprietary # encoding. + class PcdImageFile(ImageFile.ImageFile): format = "PCD" @@ -40,7 +37,7 @@ def _open(self): if s[:4] != b"PCD_": raise SyntaxError("not a PCD file") - orientation = i8(s[1538]) & 3 + orientation = s[1538] & 3 self.tile_post_rotate = None if orientation == 1: self.tile_post_rotate = 90 @@ -48,14 +45,14 @@ def _open(self): self.tile_post_rotate = -90 self.mode = "RGB" - self.size = 768, 512 # FIXME: not correct for rotated images! - self.tile = [("pcd", (0, 0)+self.size, 96*2048, None)] + self._size = 768, 512 # FIXME: not correct for rotated images! + self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)] def load_end(self): if self.tile_post_rotate: # Handle rotated PCDs self.im = self.im.rotate(self.tile_post_rotate) - self.size = self.im.size + self._size = self.im.size # diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index eba85feb0fb..442ac70c49d 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -16,50 +16,56 @@ # See the README file for information on usage and redistribution. # -from . import Image, FontFile -from ._binary import i8, i16le as l16, i32le as l32, i16be as b16, i32be as b32 +import io + +from . import FontFile, Image +from ._binary import i8 +from ._binary import i16be as b16 +from ._binary import i16le as l16 +from ._binary import i32be as b32 +from ._binary import i32le as l32 # -------------------------------------------------------------------- # declarations PCF_MAGIC = 0x70636601 # "\x01fcp" -PCF_PROPERTIES = (1 << 0) -PCF_ACCELERATORS = (1 << 1) -PCF_METRICS = (1 << 2) -PCF_BITMAPS = (1 << 3) -PCF_INK_METRICS = (1 << 4) -PCF_BDF_ENCODINGS = (1 << 5) -PCF_SWIDTHS = (1 << 6) -PCF_GLYPH_NAMES = (1 << 7) -PCF_BDF_ACCELERATORS = (1 << 8) +PCF_PROPERTIES = 1 << 0 +PCF_ACCELERATORS = 1 << 1 +PCF_METRICS = 1 << 2 +PCF_BITMAPS = 1 << 3 +PCF_INK_METRICS = 1 << 4 +PCF_BDF_ENCODINGS = 1 << 5 +PCF_SWIDTHS = 1 << 6 +PCF_GLYPH_NAMES = 1 << 7 +PCF_BDF_ACCELERATORS = 1 << 8 BYTES_PER_ROW = [ - lambda bits: ((bits+7) >> 3), - lambda bits: ((bits+15) >> 3) & ~1, - lambda bits: ((bits+31) >> 3) & ~3, - lambda bits: ((bits+63) >> 3) & ~7, + lambda bits: ((bits + 7) >> 3), + lambda bits: ((bits + 15) >> 3) & ~1, + lambda bits: ((bits + 31) >> 3) & ~3, + lambda bits: ((bits + 63) >> 3) & ~7, ] def sz(s, o): - return s[o:s.index(b"\0", o)] - + return s[o : s.index(b"\0", o)] -## -# Font file plugin for the X11 PCF format. class PcfFontFile(FontFile.FontFile): + """Font file plugin for the X11 PCF format.""" name = "name" - def __init__(self, fp): + def __init__(self, fp, charset_encoding="iso8859-1"): + + self.charset_encoding = charset_encoding magic = l32(fp.read(4)) if magic != PCF_MAGIC: raise SyntaxError("not a PCF file") - FontFile.FontFile.__init__(self) + super().__init__() count = l32(fp.read(4)) self.toc = {} @@ -78,11 +84,10 @@ def __init__(self, fp): # # create glyph structure - for ch in range(256): - ix = encoding[ch] + for ch, ix in enumerate(encoding): if ix is not None: x, y, l, r, w, a, d, f = metrics[ix] - glyph = (w, 0), (l, d-y, x+l, d), (0, 0, x, y), bitmaps[ix] + glyph = (w, 0), (l, d - y, x + l, d), (0, 0, x, y), bitmaps[ix] self.glyph[ch] = glyph def _getformat(self, tag): @@ -117,7 +122,7 @@ def _load_properties(self): for i in range(nprops): p.append((i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4)))) if nprops & 3: - fp.seek(4 - (nprops & 3), 1) # pad + fp.seek(4 - (nprops & 3), io.SEEK_CUR) # pad data = fp.read(i32(fp.read(4))) @@ -140,7 +145,7 @@ def _load_metrics(self): append = metrics.append - if (format & 0xff00) == 0x100: + if (format & 0xFF00) == 0x100: # "compressed" metrics for i in range(i16(fp.read(2))): @@ -151,10 +156,7 @@ def _load_metrics(self): descent = i8(fp.read(1)) - 128 xsize = right - left ysize = ascent + descent - append( - (xsize, ysize, left, right, width, - ascent, descent, 0) - ) + append((xsize, ysize, left, right, width, ascent, descent, 0)) else: @@ -168,10 +170,7 @@ def _load_metrics(self): attributes = i16(fp.read(2)) xsize = right - left ysize = ascent + descent - append( - (xsize, ysize, left, right, width, - ascent, descent, attributes) - ) + append((xsize, ysize, left, right, width, ascent, descent, attributes)) return metrics @@ -187,21 +186,21 @@ def _load_bitmaps(self, metrics): nbitmaps = i32(fp.read(4)) if nbitmaps != len(metrics): - raise IOError("Wrong number of bitmaps") + raise OSError("Wrong number of bitmaps") offsets = [] for i in range(nbitmaps): offsets.append(i32(fp.read(4))) - bitmapSizes = [] + bitmap_sizes = [] for i in range(4): - bitmapSizes.append(i32(fp.read(4))) + bitmap_sizes.append(i32(fp.read(4))) # byteorder = format & 4 # non-zero => MSB - bitorder = format & 8 # non-zero => MSB + bitorder = format & 8 # non-zero => MSB padindex = format & 3 - bitmapsize = bitmapSizes[padindex] + bitmapsize = bitmap_sizes[padindex] offsets.append(bitmapsize) data = fp.read(bitmapsize) @@ -213,33 +212,35 @@ def _load_bitmaps(self, metrics): for i in range(nbitmaps): x, y, l, r, w, a, d, f = metrics[i] - b, e = offsets[i], offsets[i+1] - bitmaps.append( - Image.frombytes("1", (x, y), data[b:e], "raw", mode, pad(x)) - ) + b, e = offsets[i], offsets[i + 1] + bitmaps.append(Image.frombytes("1", (x, y), data[b:e], "raw", mode, pad(x))) return bitmaps def _load_encoding(self): - - # map character code to bitmap index - encoding = [None] * 256 - fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS) - firstCol, lastCol = i16(fp.read(2)), i16(fp.read(2)) - firstRow, lastRow = i16(fp.read(2)), i16(fp.read(2)) + first_col, last_col = i16(fp.read(2)), i16(fp.read(2)) + first_row, last_row = i16(fp.read(2)), i16(fp.read(2)) - default = i16(fp.read(2)) + i16(fp.read(2)) # default - nencoding = (lastCol - firstCol + 1) * (lastRow - firstRow + 1) + nencoding = (last_col - first_col + 1) * (last_row - first_row + 1) - for i in range(nencoding): - encodingOffset = i16(fp.read(2)) - if encodingOffset != 0xFFFF: - try: - encoding[i+firstCol] = encodingOffset - except IndexError: - break # only load ISO-8859-1 glyphs + # map character code to bitmap index + encoding = [None] * min(256, nencoding) + + encoding_offsets = [i16(fp.read(2)) for _ in range(nencoding)] + + for i in range(first_col, len(encoding)): + try: + encoding_offset = encoding_offsets[ + ord(bytearray([i]).decode(self.charset_encoding)) + ] + if encoding_offset != 0xFFFF: + encoding[i] = encoding_offset + except UnicodeDecodeError: + # character is not supported in selected encoding + pass return encoding diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index b847c9e7c37..841c18a2200 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -25,22 +25,25 @@ # See the README file for information on usage and redistribution. # +import io import logging + from . import Image, ImageFile, ImagePalette -from ._binary import i8, i16le as i16, o8, o16le as o16 +from ._binary import i16le as i16 +from ._binary import o8 +from ._binary import o16le as o16 logger = logging.getLogger(__name__) -__version__ = "0.6" - def _accept(prefix): - return i8(prefix[0]) == 10 and i8(prefix[1]) in [0, 2, 3, 5] + return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5] ## # Image plugin for Paintbrush images. + class PcxImageFile(ImageFile.ImageFile): format = "PCX" @@ -54,18 +57,23 @@ def _open(self): raise SyntaxError("not a PCX file") # image - bbox = i16(s, 4), i16(s, 6), i16(s, 8)+1, i16(s, 10)+1 + 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") logger.debug("BBox: %s %s %s %s", *bbox) # format - version = i8(s[1]) - bits = i8(s[3]) - planes = i8(s[65]) - stride = i16(s, 66) - logger.debug("PCX version %s, bits %s, planes %s, stride %s", - version, bits, planes, stride) + version = s[1] + bits = s[3] + planes = s[65] + provided_stride = i16(s, 66) + logger.debug( + "PCX version %s, bits %s, planes %s, stride %s", + version, + bits, + planes, + provided_stride, + ) self.info["dpi"] = i16(s, 12), i16(s, 14) @@ -80,12 +88,12 @@ def _open(self): elif version == 5 and bits == 8 and planes == 1: mode = rawmode = "L" # FIXME: hey, this doesn't work with the incremental loader !!! - self.fp.seek(-769, 2) + self.fp.seek(-769, io.SEEK_END) s = self.fp.read(769) - if len(s) == 769 and i8(s[0]) == 12: + if len(s) == 769 and s[0] == 12: # check if the palette is linear greyscale for i in range(256): - if s[i*3+1:i*3+4] != o8(i)*3: + if s[i * 3 + 1 : i * 3 + 4] != o8(i) * 3: mode = rawmode = "P" break if mode == "P": @@ -97,19 +105,31 @@ def _open(self): rawmode = "RGB;L" else: - raise IOError("unknown PCX mode") + raise OSError("unknown PCX mode") self.mode = mode - self.size = bbox[2]-bbox[0], bbox[3]-bbox[1] + self._size = bbox[2] - bbox[0], bbox[3] - bbox[1] + + # Don't trust the passed in stride. + # Calculate the approximate position for ourselves. + # CVE-2020-35653 + stride = (self._size[0] * bits + 7) // 8 + + # While the specification states that this must be even, + # not all images follow this + if provided_stride != stride: + stride += stride % 2 bbox = (0, 0) + self.size logger.debug("size: %sx%s", *self.size) self.tile = [("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))] + # -------------------------------------------------------------------- # save PCX files + SAVE = { # mode: (version, bits, planes, raw mode) "1": (2, 1, 1, "1"), @@ -123,8 +143,8 @@ def _save(im, fp, filename): try: version, bits, planes, rawmode = SAVE[im.mode] - except KeyError: - raise ValueError("Cannot save %s images as PCX" % im.mode) + except KeyError as e: + raise ValueError(f"Cannot save {im.mode} images as PCX") from e # bytes per plane stride = (im.size[0] * bits + 7) // 8 @@ -134,8 +154,12 @@ def _save(im, fp, filename): # Ideally it should be passed in in the state, but the bytes value # gets overwritten. - logger.debug("PcxImagePlugin._save: xwidth: %d, bits: %d, stride: %d", - im.size[0], bits, stride) + logger.debug( + "PcxImagePlugin._save: xwidth: %d, bits: %d, stride: %d", + im.size[0], + bits, + stride, + ) # under windows, we could determine the current screen size with # "Image.core.display_mode()[1]", but I think that's overkill... @@ -146,32 +170,51 @@ def _save(im, fp, filename): # PCX header fp.write( - o8(10) + o8(version) + o8(1) + o8(bits) + o16(0) + - o16(0) + o16(im.size[0]-1) + o16(im.size[1]-1) + o16(dpi[0]) + - o16(dpi[1]) + b"\0"*24 + b"\xFF"*24 + b"\0" + o8(planes) + - o16(stride) + o16(1) + o16(screen[0]) + o16(screen[1]) + - b"\0"*54 - ) + o8(10) + + o8(version) + + o8(1) + + o8(bits) + + o16(0) + + o16(0) + + o16(im.size[0] - 1) + + o16(im.size[1] - 1) + + o16(dpi[0]) + + o16(dpi[1]) + + b"\0" * 24 + + b"\xFF" * 24 + + b"\0" + + o8(planes) + + o16(stride) + + o16(1) + + o16(screen[0]) + + o16(screen[1]) + + b"\0" * 54 + ) assert fp.tell() == 128 - ImageFile._save(im, fp, [("pcx", (0, 0)+im.size, 0, - (rawmode, bits*planes))]) + ImageFile._save(im, fp, [("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))]) if im.mode == "P": # colour palette fp.write(o8(12)) - fp.write(im.im.getpalette("RGB", "RGB")) # 768 bytes + palette = im.im.getpalette("RGB", "RGB") + palette += b"\x00" * (768 - len(palette)) + fp.write(palette) # 768 bytes elif im.mode == "L": # greyscale palette fp.write(o8(12)) for i in range(256): - fp.write(o8(i)*3) + fp.write(o8(i) * 3) + # -------------------------------------------------------------------- # registry + Image.register_open(PcxImageFile.format, PcxImageFile, _accept) Image.register_save(PcxImageFile.format, _save) Image.register_extension(PcxImageFile.format, ".pcx") + +Image.register_mime(PcxImageFile.format, "image/x-pcx") diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 86bc9c8e93a..404759a7fcb 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -20,12 +20,12 @@ # Image plugin for PDF images (output only). ## -from . import Image, ImageFile, ImageSequence -from ._binary import i8 import io +import math +import os +import time -__version__ = "0.4" - +from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features # # -------------------------------------------------------------------- @@ -37,19 +37,6 @@ # 4. page # 5. page contents -def _obj(fp, obj, **dictionary): - fp.write("%d 0 obj\n" % obj) - if dictionary: - fp.write("<<\n") - for k, v in dictionary.items(): - if v is not None: - fp.write("/%s %s\n" % (k, v)) - fp.write(">>\n") - - -def _endobj(fp): - fp.write("endobj\n") - def _save_all(im, fp, filename): _save(im, fp, filename, save_all=True) @@ -58,78 +45,40 @@ def _save_all(im, fp, filename): ## # (Internal) Image save plugin for the PDF format. + def _save(im, fp, filename, save_all=False): + is_appending = im.encoderinfo.get("append", False) + if is_appending: + existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="r+b") + else: + existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") + resolution = im.encoderinfo.get("resolution", 72.0) + info = { + "title": None + if is_appending + else os.path.splitext(os.path.basename(filename))[0], + "author": None, + "subject": None, + "keywords": None, + "creator": None, + "producer": None, + "creationDate": None if is_appending else time.gmtime(), + "modDate": None if is_appending else time.gmtime(), + } + for k, default in info.items(): + v = im.encoderinfo.get(k) if k in im.encoderinfo else default + if v: + existing_pdf.info[k[0].upper() + k[1:]] = v + # # make sure image data is available im.load() - xref = [0] - - class TextWriter(object): - def __init__(self, fp): - self.fp = fp - - def __getattr__(self, name): - return getattr(self.fp, name) - - def write(self, value): - self.fp.write(value.encode('latin-1')) - - fp = TextWriter(fp) - - fp.write("%PDF-1.2\n") - fp.write("% created by PIL PDF driver " + __version__ + "\n") - - # FIXME: Should replace ASCIIHexDecode with RunLengthDecode (packbits) - # or LZWDecode (tiff/lzw compression). Note that PDF 1.2 also supports - # Flatedecode (zip compression). - - bits = 8 - params = None - - if im.mode == "1": - filter = "/ASCIIHexDecode" - colorspace = "/DeviceGray" - procset = "/ImageB" # grayscale - bits = 1 - elif im.mode == "L": - filter = "/DCTDecode" - # params = "<< /Predictor 15 /Columns %d >>" % (width-2) - colorspace = "/DeviceGray" - procset = "/ImageB" # grayscale - elif im.mode == "P": - filter = "/ASCIIHexDecode" - colorspace = "[ /Indexed /DeviceRGB 255 <" - palette = im.im.getpalette("RGB") - for i in range(256): - r = i8(palette[i*3]) - g = i8(palette[i*3+1]) - b = i8(palette[i*3+2]) - colorspace += "%02x%02x%02x " % (r, g, b) - colorspace += "> ]" - procset = "/ImageI" # indexed color - elif im.mode == "RGB": - filter = "/DCTDecode" - colorspace = "/DeviceRGB" - procset = "/ImageC" # color images - elif im.mode == "CMYK": - filter = "/DCTDecode" - colorspace = "/DeviceCMYK" - procset = "/ImageC" # color images - else: - raise ValueError("cannot save mode %s" % im.mode) - - # - # catalogue - - xref.append(fp.tell()) - _obj( - fp, 1, - Type="/Catalog", - Pages="2 0 R") - _endobj(fp) + existing_pdf.start_writing() + existing_pdf.write_header() + existing_pdf.write_comment(f"created by Pillow {__version__} PDF driver") # # pages @@ -137,132 +86,183 @@ def write(self, value): if save_all: append_images = im.encoderinfo.get("append_images", []) for append_im in append_images: - if append_im.mode != im.mode: - append_im = append_im.convert(im.mode) append_im.encoderinfo = im.encoderinfo.copy() ims.append(append_im) - numberOfPages = 0 + number_of_pages = 0 + image_refs = [] + page_refs = [] + contents_refs = [] for im in ims: - im_numberOfPages = 1 + im_number_of_pages = 1 if save_all: try: - im_numberOfPages = im.n_frames + im_number_of_pages = im.n_frames except AttributeError: - # Image format does not have n_frames. It is a single frame image + # Image format does not have n_frames. + # It is a single frame image pass - numberOfPages += im_numberOfPages - pages = [str(pageNumber*3+4)+" 0 R" - for pageNumber in range(0, numberOfPages)] - - xref.append(fp.tell()) - _obj( - fp, 2, - Type="/Pages", - Count=len(pages), - Kids="["+"\n".join(pages)+"]") - _endobj(fp) - - pageNumber = 0 - for imSequence in ims: - for im in ImageSequence.Iterator(imSequence): + number_of_pages += im_number_of_pages + for i in range(im_number_of_pages): + image_refs.append(existing_pdf.next_object_id(0)) + page_refs.append(existing_pdf.next_object_id(0)) + contents_refs.append(existing_pdf.next_object_id(0)) + existing_pdf.pages.append(page_refs[-1]) + + # + # catalog and list of pages + existing_pdf.write_catalog() + + page_number = 0 + for im_sequence in ims: + im_pages = ImageSequence.Iterator(im_sequence) if save_all else [im_sequence] + for im in im_pages: + # FIXME: Should replace ASCIIHexDecode with RunLengthDecode + # (packbits) or LZWDecode (tiff/lzw compression). Note that + # PDF 1.2 also supports Flatedecode (zip compression). + + bits = 8 + params = None + decode = None + + # + # Get image characteristics + + width, height = im.size + + if im.mode == "1": + if features.check("libtiff"): + filter = "CCITTFaxDecode" + bits = 1 + params = PdfParser.PdfArray( + [ + PdfParser.PdfDict( + { + "K": -1, + "BlackIs1": True, + "Columns": width, + "Rows": height, + } + ) + ] + ) + else: + filter = "DCTDecode" + colorspace = PdfParser.PdfName("DeviceGray") + procset = "ImageB" # grayscale + elif im.mode == "L": + filter = "DCTDecode" + # params = f"<< /Predictor 15 /Columns {width-2} >>" + colorspace = PdfParser.PdfName("DeviceGray") + procset = "ImageB" # grayscale + elif im.mode == "P": + filter = "ASCIIHexDecode" + palette = im.getpalette() + colorspace = [ + PdfParser.PdfName("Indexed"), + PdfParser.PdfName("DeviceRGB"), + 255, + PdfParser.PdfBinary(palette), + ] + procset = "ImageI" # indexed color + elif im.mode == "RGB": + filter = "DCTDecode" + colorspace = PdfParser.PdfName("DeviceRGB") + procset = "ImageC" # color images + elif im.mode == "CMYK": + filter = "DCTDecode" + colorspace = PdfParser.PdfName("DeviceCMYK") + procset = "ImageC" # color images + decode = [1, 0, 1, 0, 1, 0, 1, 0] + else: + raise ValueError(f"cannot save mode {im.mode}") + # # image op = io.BytesIO() - if filter == "/ASCIIHexDecode": - if bits == 1: - # FIXME: the hex encoder doesn't support packed 1-bit - # images; do things the hard way... - data = im.tobytes("raw", "1") - im = Image.new("L", (len(data), 1), None) - im.putdata(data) - ImageFile._save(im, op, [("hex", (0, 0)+im.size, 0, im.mode)]) - elif filter == "/DCTDecode": + if filter == "ASCIIHexDecode": + ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) + elif filter == "CCITTFaxDecode": + im.save( + op, + "TIFF", + compression="group4", + # use a single strip + strip_size=math.ceil(im.width / 8) * im.height, + ) + elif filter == "DCTDecode": Image.SAVE["JPEG"](im, op, filename) - elif filter == "/FlateDecode": - ImageFile._save(im, op, [("zip", (0, 0)+im.size, 0, im.mode)]) - elif filter == "/RunLengthDecode": - ImageFile._save(im, op, [("packbits", (0, 0)+im.size, 0, im.mode)]) + elif filter == "FlateDecode": + ImageFile._save(im, op, [("zip", (0, 0) + im.size, 0, im.mode)]) + elif filter == "RunLengthDecode": + ImageFile._save(im, op, [("packbits", (0, 0) + im.size, 0, im.mode)]) else: - raise ValueError("unsupported PDF filter (%s)" % filter) - - # - # Get image characteristics + raise ValueError(f"unsupported PDF filter ({filter})") - width, height = im.size + stream = op.getvalue() + if filter == "CCITTFaxDecode": + stream = stream[8:] + filter = PdfParser.PdfArray([PdfParser.PdfName(filter)]) + else: + filter = PdfParser.PdfName(filter) - xref.append(fp.tell()) - _obj( - fp, pageNumber*3+3, - Type="/XObject", - Subtype="/Image", + existing_pdf.write_obj( + image_refs[page_number], + stream=stream, + Type=PdfParser.PdfName("XObject"), + Subtype=PdfParser.PdfName("Image"), Width=width, # * 72.0 / resolution, Height=height, # * 72.0 / resolution, - Length=len(op.getvalue()), Filter=filter, BitsPerComponent=bits, - DecodeParams=params, - ColorSpace=colorspace) - - fp.write("stream\n") - fp.fp.write(op.getvalue()) - fp.write("\nendstream\n") - - _endobj(fp) + Decode=decode, + DecodeParms=params, + ColorSpace=colorspace, + ) # # page - xref.append(fp.tell()) - _obj(fp, pageNumber*3+4) - fp.write( - "<<\n/Type /Page\n/Parent 2 0 R\n" - "/Resources <<\n/ProcSet [ /PDF %s ]\n" - "/XObject << /image %d 0 R >>\n>>\n" - "/MediaBox [ 0 0 %d %d ]\n/Contents %d 0 R\n>>\n" % ( - procset, - pageNumber*3+3, - int(width * 72.0 / resolution), - int(height * 72.0 / resolution), - pageNumber*3+5)) - _endobj(fp) + existing_pdf.write_page( + page_refs[page_number], + Resources=PdfParser.PdfDict( + ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)], + XObject=PdfParser.PdfDict(image=image_refs[page_number]), + ), + MediaBox=[ + 0, + 0, + width * 72.0 / resolution, + height * 72.0 / resolution, + ], + Contents=contents_refs[page_number], + ) # # page contents - op = TextWriter(io.BytesIO()) - - op.write( - "q %d 0 0 %d 0 0 cm /image Do Q\n" % ( - int(width * 72.0 / resolution), - int(height * 72.0 / resolution))) + page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % ( + width * 72.0 / resolution, + height * 72.0 / resolution, + ) - xref.append(fp.tell()) - _obj(fp, pageNumber*3+5, Length=len(op.fp.getvalue())) + existing_pdf.write_obj(contents_refs[page_number], stream=page_contents) - fp.write("stream\n") - fp.fp.write(op.fp.getvalue()) - fp.write("\nendstream\n") - - _endobj(fp) - - pageNumber += 1 + page_number += 1 # # trailer - startxref = fp.tell() - fp.write("xref\n0 %d\n0000000000 65535 f \n" % len(xref)) - for x in xref[1:]: - fp.write("%010d 00000 n \n" % x) - fp.write("trailer\n<<\n/Size %d\n/Root 1 0 R\n>>\n" % len(xref)) - fp.write("startxref\n%d\n%%%%EOF\n" % startxref) + existing_pdf.write_xref_and_trailer() if hasattr(fp, "flush"): fp.flush() + existing_pdf.close() + # # -------------------------------------------------------------------- + Image.register_save("PDF", _save) Image.register_save_all("PDF", _save_all) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py new file mode 100644 index 00000000000..fd5cc5a61e3 --- /dev/null +++ b/src/PIL/PdfParser.py @@ -0,0 +1,998 @@ +import calendar +import codecs +import collections +import mmap +import os +import re +import time +import zlib + + +# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set +# on page 656 +def encode_text(s): + return codecs.BOM_UTF16_BE + s.encode("utf_16_be") + + +PDFDocEncoding = { + 0x16: "\u0017", + 0x18: "\u02D8", + 0x19: "\u02C7", + 0x1A: "\u02C6", + 0x1B: "\u02D9", + 0x1C: "\u02DD", + 0x1D: "\u02DB", + 0x1E: "\u02DA", + 0x1F: "\u02DC", + 0x80: "\u2022", + 0x81: "\u2020", + 0x82: "\u2021", + 0x83: "\u2026", + 0x84: "\u2014", + 0x85: "\u2013", + 0x86: "\u0192", + 0x87: "\u2044", + 0x88: "\u2039", + 0x89: "\u203A", + 0x8A: "\u2212", + 0x8B: "\u2030", + 0x8C: "\u201E", + 0x8D: "\u201C", + 0x8E: "\u201D", + 0x8F: "\u2018", + 0x90: "\u2019", + 0x91: "\u201A", + 0x92: "\u2122", + 0x93: "\uFB01", + 0x94: "\uFB02", + 0x95: "\u0141", + 0x96: "\u0152", + 0x97: "\u0160", + 0x98: "\u0178", + 0x99: "\u017D", + 0x9A: "\u0131", + 0x9B: "\u0142", + 0x9C: "\u0153", + 0x9D: "\u0161", + 0x9E: "\u017E", + 0xA0: "\u20AC", +} + + +def decode_text(b): + if b[: len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE: + return b[len(codecs.BOM_UTF16_BE) :].decode("utf_16_be") + else: + return "".join(PDFDocEncoding.get(byte, chr(byte)) for byte in b) + + +class PdfFormatError(RuntimeError): + """An error that probably indicates a syntactic or semantic error in the + PDF file structure""" + + pass + + +def check_format_condition(condition, error_message): + if not condition: + raise PdfFormatError(error_message) + + +class IndirectReference( + collections.namedtuple("IndirectReferenceTuple", ["object_id", "generation"]) +): + def __str__(self): + return "%s %s R" % self + + def __bytes__(self): + return self.__str__().encode("us-ascii") + + def __eq__(self, other): + return ( + other.__class__ is self.__class__ + and other.object_id == self.object_id + and other.generation == self.generation + ) + + def __ne__(self, other): + return not (self == other) + + def __hash__(self): + return hash((self.object_id, self.generation)) + + +class IndirectObjectDef(IndirectReference): + def __str__(self): + return "%s %s obj" % self + + +class XrefTable: + def __init__(self): + self.existing_entries = {} # object ID => (offset, generation) + self.new_entries = {} # object ID => (offset, generation) + self.deleted_entries = {0: 65536} # object ID => generation + self.reading_finished = False + + def __setitem__(self, key, value): + if self.reading_finished: + self.new_entries[key] = value + else: + self.existing_entries[key] = value + if key in self.deleted_entries: + del self.deleted_entries[key] + + def __getitem__(self, key): + try: + return self.new_entries[key] + except KeyError: + return self.existing_entries[key] + + def __delitem__(self, key): + if key in self.new_entries: + generation = self.new_entries[key][1] + 1 + del self.new_entries[key] + self.deleted_entries[key] = generation + elif key in self.existing_entries: + generation = self.existing_entries[key][1] + 1 + self.deleted_entries[key] = generation + elif key in self.deleted_entries: + generation = self.deleted_entries[key] + else: + raise IndexError( + "object ID " + str(key) + " cannot be deleted because it doesn't exist" + ) + + def __contains__(self, key): + return key in self.existing_entries or key in self.new_entries + + def __len__(self): + return len( + set(self.existing_entries.keys()) + | set(self.new_entries.keys()) + | set(self.deleted_entries.keys()) + ) + + def keys(self): + return ( + set(self.existing_entries.keys()) - set(self.deleted_entries.keys()) + ) | set(self.new_entries.keys()) + + def write(self, f): + keys = sorted(set(self.new_entries.keys()) | set(self.deleted_entries.keys())) + deleted_keys = sorted(set(self.deleted_entries.keys())) + startxref = f.tell() + f.write(b"xref\n") + while keys: + # find a contiguous sequence of object IDs + prev = None + for index, key in enumerate(keys): + if prev is None or prev + 1 == key: + prev = key + else: + contiguous_keys = keys[:index] + keys = keys[index:] + break + else: + contiguous_keys = keys + keys = None + f.write(b"%d %d\n" % (contiguous_keys[0], len(contiguous_keys))) + for object_id in contiguous_keys: + if object_id in self.new_entries: + f.write(b"%010d %05d n \n" % self.new_entries[object_id]) + else: + this_deleted_object_id = deleted_keys.pop(0) + check_format_condition( + object_id == this_deleted_object_id, + f"expected the next deleted object ID to be {object_id}, " + f"instead found {this_deleted_object_id}", + ) + try: + next_in_linked_list = deleted_keys[0] + except IndexError: + next_in_linked_list = 0 + f.write( + b"%010d %05d f \n" + % (next_in_linked_list, self.deleted_entries[object_id]) + ) + return startxref + + +class PdfName: + def __init__(self, name): + if isinstance(name, PdfName): + self.name = name.name + elif isinstance(name, bytes): + self.name = name + else: + self.name = name.encode("us-ascii") + + def name_as_str(self): + return self.name.decode("us-ascii") + + def __eq__(self, other): + return ( + isinstance(other, PdfName) and other.name == self.name + ) or other == self.name + + def __hash__(self): + return hash(self.name) + + def __repr__(self): + return f"PdfName({repr(self.name)})" + + @classmethod + def from_pdf_stream(cls, data): + return cls(PdfParser.interpret_name(data)) + + allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"} + + def __bytes__(self): + result = bytearray(b"/") + for b in self.name: + if b in self.allowed_chars: + result.append(b) + else: + result.extend(b"#%02X" % b) + return bytes(result) + + +class PdfArray(list): + def __bytes__(self): + return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" + + +class PdfDict(collections.UserDict): + def __setattr__(self, key, value): + if key == "data": + collections.UserDict.__setattr__(self, key, value) + else: + self[key.encode("us-ascii")] = value + + def __getattr__(self, key): + try: + value = self[key.encode("us-ascii")] + except KeyError as e: + raise AttributeError(key) from e + if isinstance(value, bytes): + value = decode_text(value) + if key.endswith("Date"): + if value.startswith("D:"): + value = value[2:] + + relationship = "Z" + if len(value) > 17: + relationship = value[14] + offset = int(value[15:17]) * 60 + if len(value) > 20: + offset += int(value[18:20]) + + format = "%Y%m%d%H%M%S"[: len(value) - 2] + value = time.strptime(value[: len(format) + 2], format) + if relationship in ["+", "-"]: + offset *= 60 + if relationship == "+": + offset *= -1 + value = time.gmtime(calendar.timegm(value) + offset) + return value + + def __bytes__(self): + out = bytearray(b"<<") + for key, value in self.items(): + if value is None: + continue + value = pdf_repr(value) + out.extend(b"\n") + out.extend(bytes(PdfName(key))) + out.extend(b" ") + out.extend(value) + out.extend(b"\n>>") + return bytes(out) + + +class PdfBinary: + def __init__(self, data): + self.data = data + + def __bytes__(self): + return b"<%s>" % b"".join(b"%02X" % b for b in self.data) + + +class PdfStream: + def __init__(self, dictionary, buf): + self.dictionary = dictionary + self.buf = buf + + def decode(self): + try: + filter = self.dictionary.Filter + except AttributeError: + return self.buf + if filter == b"FlateDecode": + try: + expected_length = self.dictionary.DL + except AttributeError: + 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" + ) + + +def pdf_repr(x): + if x is True: + return b"true" + elif x is False: + return b"false" + elif x is None: + return b"null" + elif isinstance(x, (PdfName, PdfDict, PdfArray, PdfBinary)): + return bytes(x) + elif isinstance(x, int): + return str(x).encode("us-ascii") + elif isinstance(x, float): + return str(x).encode("us-ascii") + elif isinstance(x, time.struct_time): + return b"(D:" + time.strftime("%Y%m%d%H%M%SZ", x).encode("us-ascii") + b")" + elif isinstance(x, dict): + return bytes(PdfDict(x)) + elif isinstance(x, list): + return bytes(PdfArray(x)) + elif isinstance(x, str): + return pdf_repr(encode_text(x)) + elif isinstance(x, bytes): + # XXX escape more chars? handle binary garbage + x = x.replace(b"\\", b"\\\\") + x = x.replace(b"(", b"\\(") + x = x.replace(b")", b"\\)") + return b"(" + x + b")" + else: + return bytes(x) + + +class PdfParser: + """Based on + https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf + Supports PDF up to 1.4 + """ + + 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") + self.filename = filename + self.buf = buf + self.f = f + self.start_offset = start_offset + self.should_close_buf = False + self.should_close_file = False + if filename is not None and f is None: + self.f = f = open(filename, mode) + self.should_close_file = True + if f is not None: + self.buf = buf = self.get_buf_from_file(f) + self.should_close_buf = True + if not filename and hasattr(f, "name"): + self.filename = f.name + self.cached_objects = {} + if buf: + self.read_pdf_info() + else: + self.file_size_total = self.file_size_this = 0 + self.root = PdfDict() + self.root_ref = None + self.info = PdfDict() + self.info_ref = None + self.page_tree_root = {} + self.pages = [] + self.orig_pages = [] + self.pages_ref = None + self.last_xref_section_offset = None + self.trailer_dict = {} + self.xref_table = XrefTable() + self.xref_table.reading_finished = True + if f: + self.seek_end() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + return False # do not suppress exceptions + + def start_writing(self): + self.close_buf() + self.seek_end() + + def close_buf(self): + try: + self.buf.close() + except AttributeError: + pass + self.buf = None + + def close(self): + if self.should_close_buf: + self.close_buf() + if self.f is not None and self.should_close_file: + self.f.close() + self.f = None + + def seek_end(self): + self.f.seek(0, os.SEEK_END) + + def write_header(self): + self.f.write(b"%PDF-1.4\n") + + def write_comment(self, s): + self.f.write(f"% {s}\n".encode()) + + def write_catalog(self): + self.del_root() + self.root_ref = self.next_object_id(self.f.tell()) + self.pages_ref = self.next_object_id(0) + self.rewrite_pages() + self.write_obj(self.root_ref, Type=PdfName(b"Catalog"), Pages=self.pages_ref) + self.write_obj( + self.pages_ref, + Type=PdfName(b"Pages"), + Count=len(self.pages), + Kids=self.pages, + ) + return self.root_ref + + def rewrite_pages(self): + pages_tree_nodes_to_delete = [] + for i, page_ref in enumerate(self.orig_pages): + page_info = self.cached_objects[page_ref] + del self.xref_table[page_ref.object_id] + pages_tree_nodes_to_delete.append(page_info[PdfName(b"Parent")]) + if page_ref not in self.pages: + # the page has been deleted + continue + # make dict keys into strings for passing to write_page + stringified_page_info = {} + for key, value in page_info.items(): + # key should be a PdfName + stringified_page_info[key.name_as_str()] = value + stringified_page_info["Parent"] = self.pages_ref + new_page_ref = self.write_page(None, **stringified_page_info) + for j, cur_page_ref in enumerate(self.pages): + if cur_page_ref == page_ref: + # replace the page reference with the new one + self.pages[j] = new_page_ref + # delete redundant Pages tree nodes from xref table + for pages_tree_node_ref in pages_tree_nodes_to_delete: + while pages_tree_node_ref: + pages_tree_node = self.cached_objects[pages_tree_node_ref] + if pages_tree_node_ref.object_id in self.xref_table: + del self.xref_table[pages_tree_node_ref.object_id] + pages_tree_node_ref = pages_tree_node.get(b"Parent", None) + self.orig_pages = [] + + def write_xref_and_trailer(self, new_root_ref=None): + if new_root_ref: + self.del_root() + self.root_ref = new_root_ref + if self.info: + self.info_ref = self.write_obj(None, self.info) + start_xref = self.xref_table.write(self.f) + num_entries = len(self.xref_table) + trailer_dict = {b"Root": self.root_ref, b"Size": num_entries} + if self.last_xref_section_offset is not None: + trailer_dict[b"Prev"] = self.last_xref_section_offset + if self.info: + trailer_dict[b"Info"] = self.info_ref + self.last_xref_section_offset = start_xref + self.f.write( + b"trailer\n" + + bytes(PdfDict(trailer_dict)) + + b"\nstartxref\n%d\n%%%%EOF" % start_xref + ) + + def write_page(self, ref, *objs, **dict_obj): + if isinstance(ref, int): + ref = self.pages[ref] + if "Type" not in dict_obj: + dict_obj["Type"] = PdfName(b"Page") + if "Parent" not in dict_obj: + dict_obj["Parent"] = self.pages_ref + return self.write_obj(ref, *objs, **dict_obj) + + def write_obj(self, ref, *objs, **dict_obj): + f = self.f + if ref is None: + ref = self.next_object_id(f.tell()) + else: + self.xref_table[ref.object_id] = (f.tell(), ref.generation) + f.write(bytes(IndirectObjectDef(*ref))) + stream = dict_obj.pop("stream", None) + if stream is not None: + dict_obj["Length"] = len(stream) + if dict_obj: + f.write(pdf_repr(dict_obj)) + for obj in objs: + f.write(pdf_repr(obj)) + if stream is not None: + f.write(b"stream\n") + f.write(stream) + f.write(b"\nendstream\n") + f.write(b"endobj\n") + return ref + + def del_root(self): + if self.root_ref is None: + return + del self.xref_table[self.root_ref.object_id] + del self.xref_table[self.root[b"Pages"].object_id] + + @staticmethod + def get_buf_from_file(f): + if hasattr(f, "getbuffer"): + return f.getbuffer() + elif hasattr(f, "getvalue"): + return f.getvalue() + else: + try: + return mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) + except ValueError: # cannot mmap an empty file + return b"" + + def read_pdf_info(self): + self.file_size_total = len(self.buf) + self.file_size_this = self.file_size_total - self.start_offset + self.read_trailer() + self.root_ref = self.trailer_dict[b"Root"] + self.info_ref = self.trailer_dict.get(b"Info", None) + self.root = PdfDict(self.read_indirect(self.root_ref)) + if self.info_ref is None: + self.info = PdfDict() + else: + self.info = PdfDict(self.read_indirect(self.info_ref)) + check_format_condition(b"Type" in self.root, "/Type missing in Root") + check_format_condition( + self.root[b"Type"] == b"Catalog", "/Type in Root is not /Catalog" + ) + check_format_condition(b"Pages" in self.root, "/Pages missing in Root") + check_format_condition( + isinstance(self.root[b"Pages"], IndirectReference), + "/Pages in Root is not an indirect reference", + ) + self.pages_ref = self.root[b"Pages"] + self.page_tree_root = self.read_indirect(self.pages_ref) + self.pages = self.linearize_page_tree(self.page_tree_root) + # save the original list of page references + # in case the user modifies, adds or deletes some pages + # and we need to rewrite the pages and their list + self.orig_pages = self.pages[:] + + def next_object_id(self, offset=None): + try: + # TODO: support reuse of deleted objects + reference = IndirectReference(max(self.xref_table.keys()) + 1, 0) + except ValueError: + reference = IndirectReference(1, 0) + if offset is not None: + self.xref_table[reference.object_id] = (offset, 0) + return reference + + delimiter = rb"[][()<>{}/%]" + delimiter_or_ws = rb"[][()<>{}/%\000\011\012\014\015\040]" + whitespace = rb"[\000\011\012\014\015\040]" + whitespace_or_hex = rb"[\000\011\012\014\015\0400-9a-fA-F]" + whitespace_optional = whitespace + b"*" + whitespace_mandatory = whitespace + b"+" + # No "\012" aka "\n" or "\015" aka "\r": + whitespace_optional_no_nl = rb"[\000\011\014\040]*" + newline_only = rb"[\r\n]+" + newline = whitespace_optional_no_nl + newline_only + whitespace_optional_no_nl + re_trailer_end = re.compile( + whitespace_mandatory + + rb"trailer" + + whitespace_optional + + rb"<<(.*>>)" + + newline + + rb"startxref" + + newline + + rb"([0-9]+)" + + newline + + rb"%%EOF" + + whitespace_optional + + rb"$", + re.DOTALL, + ) + re_trailer_prev = re.compile( + whitespace_optional + + rb"trailer" + + whitespace_optional + + rb"<<(.*?>>)" + + newline + + rb"startxref" + + newline + + rb"([0-9]+)" + + newline + + rb"%%EOF" + + whitespace_optional, + re.DOTALL, + ) + + def read_trailer(self): + search_start_offset = len(self.buf) - 16384 + if search_start_offset < self.start_offset: + search_start_offset = self.start_offset + m = self.re_trailer_end.search(self.buf, search_start_offset) + check_format_condition(m, "trailer end not found") + # make sure we found the LAST trailer + last_match = m + while m: + last_match = m + m = self.re_trailer_end.search(self.buf, m.start() + 16) + if not m: + m = last_match + trailer_data = m.group(1) + self.last_xref_section_offset = int(m.group(2)) + self.trailer_dict = self.interpret_trailer(trailer_data) + self.xref_table = XrefTable() + self.read_xref_table(xref_section_offset=self.last_xref_section_offset) + if b"Prev" in self.trailer_dict: + self.read_prev_trailer(self.trailer_dict[b"Prev"]) + + def read_prev_trailer(self, xref_section_offset): + trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset) + m = self.re_trailer_prev.search( + self.buf[trailer_offset : trailer_offset + 16384] + ) + check_format_condition(m, "previous trailer not found") + trailer_data = m.group(1) + check_format_condition( + int(m.group(2)) == xref_section_offset, + "xref section offset in previous trailer doesn't match what was expected", + ) + trailer_dict = self.interpret_trailer(trailer_data) + if b"Prev" in trailer_dict: + self.read_prev_trailer(trailer_dict[b"Prev"]) + + re_whitespace_optional = re.compile(whitespace_optional) + re_name = re.compile( + whitespace_optional + + rb"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?=" + + delimiter_or_ws + + rb")" + ) + re_dict_start = re.compile(whitespace_optional + rb"<<") + re_dict_end = re.compile(whitespace_optional + rb">>" + whitespace_optional) + + @classmethod + def interpret_trailer(cls, trailer_data): + trailer = {} + offset = 0 + while True: + m = cls.re_name.match(trailer_data, offset) + if not m: + m = cls.re_dict_end.match(trailer_data, offset) + check_format_condition( + m and m.end() == len(trailer_data), + "name not found in trailer, remaining data: " + + repr(trailer_data[offset:]), + ) + break + key = cls.interpret_name(m.group(1)) + value, offset = cls.get_value(trailer_data, m.end()) + trailer[key] = value + check_format_condition( + b"Size" in trailer and isinstance(trailer[b"Size"], int), + "/Size not in trailer or not an integer", + ) + check_format_condition( + b"Root" in trailer and isinstance(trailer[b"Root"], IndirectReference), + "/Root not in trailer or not an indirect reference", + ) + return trailer + + re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?") + + @classmethod + def interpret_name(cls, raw, as_text=False): + name = b"" + for m in cls.re_hashes_in_name.finditer(raw): + if m.group(3): + name += m.group(1) + bytearray.fromhex(m.group(3).decode("us-ascii")) + else: + name += m.group(1) + if as_text: + return name.decode("utf-8") + else: + return bytes(name) + + re_null = re.compile(whitespace_optional + rb"null(?=" + delimiter_or_ws + rb")") + re_true = re.compile(whitespace_optional + rb"true(?=" + delimiter_or_ws + rb")") + re_false = re.compile(whitespace_optional + rb"false(?=" + delimiter_or_ws + rb")") + re_int = re.compile( + whitespace_optional + rb"([-+]?[0-9]+)(?=" + delimiter_or_ws + rb")" + ) + re_real = re.compile( + whitespace_optional + + rb"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?=" + + delimiter_or_ws + + rb")" + ) + re_array_start = re.compile(whitespace_optional + rb"\[") + re_array_end = re.compile(whitespace_optional + rb"]") + re_string_hex = re.compile( + whitespace_optional + rb"<(" + whitespace_or_hex + rb"*)>" + ) + re_string_lit = re.compile(whitespace_optional + rb"\(") + re_indirect_reference = re.compile( + whitespace_optional + + rb"([-+]?[0-9]+)" + + whitespace_mandatory + + rb"([-+]?[0-9]+)" + + whitespace_mandatory + + rb"R(?=" + + delimiter_or_ws + + rb")" + ) + re_indirect_def_start = re.compile( + whitespace_optional + + rb"([-+]?[0-9]+)" + + whitespace_mandatory + + rb"([-+]?[0-9]+)" + + whitespace_mandatory + + rb"obj(?=" + + delimiter_or_ws + + rb")" + ) + re_indirect_def_end = re.compile( + whitespace_optional + rb"endobj(?=" + delimiter_or_ws + rb")" + ) + re_comment = re.compile( + rb"(" + whitespace_optional + rb"%[^\r\n]*" + newline + rb")*" + ) + re_stream_start = re.compile(whitespace_optional + rb"stream\r?\n") + re_stream_end = re.compile( + whitespace_optional + rb"endstream(?=" + delimiter_or_ws + rb")" + ) + + @classmethod + def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1): + if max_nesting == 0: + return None, None + m = cls.re_comment.match(data, offset) + if m: + offset = m.end() + m = cls.re_indirect_def_start.match(data, offset) + if m: + check_format_condition( + int(m.group(1)) > 0, + "indirect object definition: object ID must be greater than 0", + ) + check_format_condition( + int(m.group(2)) >= 0, + "indirect object definition: generation must be non-negative", + ) + check_format_condition( + expect_indirect is None + or expect_indirect + == IndirectReference(int(m.group(1)), int(m.group(2))), + "indirect object definition different than expected", + ) + object, offset = cls.get_value(data, m.end(), max_nesting=max_nesting - 1) + if offset is None: + return object, None + m = cls.re_indirect_def_end.match(data, offset) + check_format_condition(m, "indirect object definition end not found") + return object, m.end() + check_format_condition( + not expect_indirect, "indirect object definition not found" + ) + m = cls.re_indirect_reference.match(data, offset) + if m: + check_format_condition( + int(m.group(1)) > 0, + "indirect object reference: object ID must be greater than 0", + ) + check_format_condition( + int(m.group(2)) >= 0, + "indirect object reference: generation must be non-negative", + ) + return IndirectReference(int(m.group(1)), int(m.group(2))), m.end() + m = cls.re_dict_start.match(data, offset) + if m: + offset = m.end() + result = {} + m = cls.re_dict_end.match(data, offset) + while not m: + key, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1) + if offset is None: + return result, None + value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1) + result[key] = value + if offset is None: + return result, None + m = cls.re_dict_end.match(data, offset) + offset = m.end() + m = cls.re_stream_start.match(data, offset) + if m: + 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 + 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") + offset = m.end() + result = PdfStream(PdfDict(result), stream_data) + else: + result = PdfDict(result) + return result, offset + m = cls.re_array_start.match(data, offset) + if m: + offset = m.end() + result = [] + m = cls.re_array_end.match(data, offset) + while not m: + value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1) + result.append(value) + if offset is None: + return result, None + m = cls.re_array_end.match(data, offset) + return result, m.end() + m = cls.re_null.match(data, offset) + if m: + return None, m.end() + m = cls.re_true.match(data, offset) + if m: + return True, m.end() + m = cls.re_false.match(data, offset) + if m: + return False, m.end() + m = cls.re_name.match(data, offset) + if m: + return PdfName(cls.interpret_name(m.group(1))), m.end() + m = cls.re_int.match(data, offset) + if m: + return int(m.group(1)), m.end() + m = cls.re_real.match(data, offset) + if m: + # XXX Decimal instead of float??? + return float(m.group(1)), m.end() + m = cls.re_string_hex.match(data, offset) + if m: + # filter out whitespace + hex_string = bytearray( + b for b in m.group(1) if b in b"0123456789abcdefABCDEF" + ) + if len(hex_string) % 2 == 1: + # append a 0 if the length is not even - yes, at the end + hex_string.append(ord(b"0")) + return bytearray.fromhex(hex_string.decode("us-ascii")), m.end() + m = cls.re_string_lit.match(data, offset) + 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])) + + re_lit_str_token = re.compile( + rb"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))" + ) + escaped_chars = { + b"n": b"\n", + b"r": b"\r", + b"t": b"\t", + b"b": b"\b", + b"f": b"\f", + b"(": b"(", + b")": b")", + b"\\": b"\\", + ord(b"n"): b"\n", + ord(b"r"): b"\r", + ord(b"t"): b"\t", + ord(b"b"): b"\b", + ord(b"f"): b"\f", + ord(b"("): b"(", + ord(b")"): b")", + ord(b"\\"): b"\\", + } + + @classmethod + def get_literal_string(cls, data, offset): + nesting_depth = 0 + result = bytearray() + for m in cls.re_lit_str_token.finditer(data, offset): + result.extend(data[offset : m.start()]) + if m.group(1): + result.extend(cls.escaped_chars[m.group(1)[1]]) + elif m.group(2): + result.append(int(m.group(2)[1:], 8)) + elif m.group(3): + pass + elif m.group(5): + result.extend(b"\n") + elif m.group(6): + result.extend(b"(") + nesting_depth += 1 + elif m.group(7): + if nesting_depth == 0: + return bytes(result), m.end() + result.extend(b")") + nesting_depth -= 1 + offset = m.end() + raise PdfFormatError("unfinished literal string") + + re_xref_section_start = re.compile(whitespace_optional + rb"xref" + newline) + re_xref_subsection_start = re.compile( + whitespace_optional + + rb"([0-9]+)" + + whitespace_mandatory + + rb"([0-9]+)" + + whitespace_optional + + newline_only + ) + re_xref_entry = re.compile(rb"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)") + + def read_xref_table(self, xref_section_offset): + subsection_found = False + m = self.re_xref_section_start.match( + self.buf, xref_section_offset + self.start_offset + ) + check_format_condition(m, "xref section start not found") + offset = m.end() + while True: + m = self.re_xref_subsection_start.match(self.buf, offset) + if not m: + check_format_condition( + subsection_found, "xref subsection start not found" + ) + break + subsection_found = True + offset = m.end() + first_object = int(m.group(1)) + num_objects = int(m.group(2)) + for i in range(first_object, first_object + num_objects): + m = self.re_xref_entry.match(self.buf, offset) + check_format_condition(m, "xref entry not found") + offset = m.end() + is_free = m.group(3) == b"f" + generation = int(m.group(2)) + if not is_free: + new_entry = (int(m.group(1)), generation) + check_format_condition( + i not in self.xref_table or self.xref_table[i] == new_entry, + "xref entry duplicated (and not identical)", + ) + self.xref_table[i] = new_entry + return offset + + def read_indirect(self, ref, max_nesting=-1): + offset, generation = self.xref_table[ref[0]] + check_format_condition( + generation == ref[1], + f"expected to find generation {ref[1]} for object ID {ref[0]} in xref " + f"table, instead found generation {generation} at offset {offset}", + ) + value = self.get_value( + self.buf, + offset + self.start_offset, + expect_indirect=IndirectReference(*ref), + max_nesting=max_nesting, + )[0] + self.cached_objects[ref] = value + return value + + def linearize_page_tree(self, node=None): + if node is None: + node = self.page_tree_root + check_format_condition( + node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages" + ) + pages = [] + for kid in node[b"Kids"]: + kid_object = self.read_indirect(kid) + if kid_object[b"Type"] == b"Page": + pages.append(kid) + else: + pages.extend(self.linearize_page_tree(node=kid_object)) + return pages diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 220577ccec3..c4860b6c4f3 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -22,12 +22,10 @@ from . import Image, ImageFile from ._binary import i16le as i16 -__version__ = "0.1" - - # # helpers + def _accept(prefix): return prefix[:4] == b"\200\350\000\000" @@ -35,6 +33,7 @@ def _accept(prefix): ## # Image plugin for PIXAR raster images. + class PixarImageFile(ImageFile.ImageFile): format = "PIXAR" @@ -44,23 +43,23 @@ def _open(self): # assuming a 4-byte magic label s = self.fp.read(4) - if s != b"\200\350\000\000": + if not _accept(s): raise SyntaxError("not a PIXAR file") # read rest of header s = s + self.fp.read(508) - self.size = i16(s[418:420]), i16(s[416:418]) + self._size = i16(s, 418), i16(s, 416) # get channel/depth descriptions - mode = i16(s[424:426]), i16(s[426:428]) + mode = i16(s, 424), i16(s, 426) if mode == (14, 2): self.mode = "RGB" # FIXME: to be continued... # create tile descriptor (assuming "dumped") - self.tile = [("raw", (0, 0)+self.size, 1024, (self.mode, 0, 1))] + self.tile = [("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1))] # diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 2031b179eb4..2c53be109ae 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -31,19 +31,25 @@ # See the README file for information on usage and redistribution. # +import itertools import logging import re -import zlib import struct +import warnings +import zlib +from enum import IntEnum -from . import Image, ImageFile, ImagePalette -from ._binary import i8, i16be as i16, i32be as i32, o16be as o16, o32be as o32 - -__version__ = "0.9" +from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._binary import o8 +from ._binary import o16be as o16 +from ._binary import o32be as o32 +from ._deprecate import deprecate logger = logging.getLogger(__name__) -is_cid = re.compile(br"\w\w\w\w").match +is_cid = re.compile(rb"\w\w\w\w").match _MAGIC = b"\211PNG\r\n\032\n" @@ -51,31 +57,88 @@ _MODES = { # supported bits/color combinations, and corresponding modes/rawmodes - (1, 0): ("1", "1"), - (2, 0): ("L", "L;2"), - (4, 0): ("L", "L;4"), - (8, 0): ("L", "L"), + # Greyscale + (1, 0): ("1", "1"), + (2, 0): ("L", "L;2"), + (4, 0): ("L", "L;4"), + (8, 0): ("L", "L"), (16, 0): ("I", "I;16B"), - (8, 2): ("RGB", "RGB"), + # Truecolour + (8, 2): ("RGB", "RGB"), (16, 2): ("RGB", "RGB;16B"), - (1, 3): ("P", "P;1"), - (2, 3): ("P", "P;2"), - (4, 3): ("P", "P;4"), - (8, 3): ("P", "P"), - (8, 4): ("LA", "LA"), + # Indexed-colour + (1, 3): ("P", "P;1"), + (2, 3): ("P", "P;2"), + (4, 3): ("P", "P;4"), + (8, 3): ("P", "P"), + # Greyscale with alpha + (8, 4): ("LA", "LA"), (16, 4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available - (8, 6): ("RGBA", "RGBA"), + # Truecolour with alpha + (8, 6): ("RGBA", "RGBA"), (16, 6): ("RGBA", "RGBA;16B"), } -_simple_palette = re.compile(b'^\xff*\x00\xff*$') +_simple_palette = re.compile(b"^\xff*\x00\xff*$") -# Maximum decompressed size for a iTXt or zTXt chunk. -# Eliminates decompression bombs where compressed chunks can expand 1000x MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK -# Set the maximum total text chunk size. +""" +Maximum decompressed size for a iTXt or zTXt chunk. +Eliminates decompression bombs where compressed chunks can expand 1000x. +See :ref:`Text in PNG File Format`. +""" MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK +""" +Set the maximum total text chunk size. +See :ref:`Text in PNG File Format`. +""" + + +# APNG frame disposal modes +class Disposal(IntEnum): + OP_NONE = 0 + """ + No disposal is done on this frame before rendering the next frame. + See :ref:`Saving APNG sequences`. + """ + OP_BACKGROUND = 1 + """ + This frame’s modified region is cleared to fully transparent black before rendering + the next frame. + See :ref:`Saving APNG sequences`. + """ + OP_PREVIOUS = 2 + """ + This frame’s modified region is reverted to the previous frame’s contents before + rendering the next frame. + See :ref:`Saving APNG sequences`. + """ + + +# APNG frame blend modes +class Blend(IntEnum): + OP_SOURCE = 0 + """ + All color components of this frame, including alpha, overwrite the previous output + image contents. + See :ref:`Saving APNG sequences`. + """ + OP_OVER = 1 + """ + This frame should be alpha composited with the previous output image contents. + See :ref:`Saving APNG sequences`. + """ + + +def __getattr__(name): + for enum, prefix in {Disposal: "APNG_DISPOSE_", Blend: "APNG_BLEND_"}.items(): + if name.startswith(prefix): + name = name[len(prefix) :] + 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}'") def _safe_zlib_decompress(s): @@ -86,21 +149,22 @@ def _safe_zlib_decompress(s): return plaintext +def _crc32(data, seed=0): + return zlib.crc32(data, seed) & 0xFFFFFFFF + + # -------------------------------------------------------------------- # Support classes. Suitable for PNG and related formats like MNG etc. -class ChunkStream(object): +class ChunkStream: def __init__(self, fp): self.fp = fp self.queue = [] - if not hasattr(Image.core, "crc32"): - self.crc = self.crc_skip - def read(self): - "Fetch a new chunk. Returns header information." + """Fetch a new chunk. Returns header information.""" cid = None if self.queue: @@ -114,7 +178,7 @@ def read(self): if not is_cid(cid): if not ImageFile.LOAD_TRUNCATED_IMAGES: - raise SyntaxError("broken PNG file (chunk %s)" % repr(cid)) + raise SyntaxError(f"broken PNG file (chunk {repr(cid)})") return cid, pos, length @@ -125,39 +189,42 @@ def __exit__(self, *args): self.close() def close(self): - self.queue = self.crc = self.fp = None + self.queue = self.fp = None def push(self, cid, pos, length): self.queue.append((cid, pos, length)) def call(self, cid, pos, length): - "Call the appropriate chunk handler" + """Call the appropriate chunk handler""" logger.debug("STREAM %r %s %s", cid, pos, length) - return getattr(self, "chunk_" + cid.decode('ascii'))(pos, length) + return getattr(self, "chunk_" + cid.decode("ascii"))(pos, length) def crc(self, cid, data): - "Read and verify checksum" + """Read and verify checksum""" - # Skip CRC checks for ancillary chunks if allowed to load truncated images + # Skip CRC checks for ancillary chunks if allowed to load truncated + # images # 5th byte of first char is 1 [specs, section 5.4] - if ImageFile.LOAD_TRUNCATED_IMAGES and (i8(cid[0]) >> 5 & 1): + if ImageFile.LOAD_TRUNCATED_IMAGES and (cid[0] >> 5 & 1): self.crc_skip(cid, data) return try: - crc1 = Image.core.crc32(data, Image.core.crc32(cid)) - crc2 = i16(self.fp.read(2)), i16(self.fp.read(2)) + crc1 = _crc32(data, _crc32(cid)) + crc2 = i32(self.fp.read(4)) if crc1 != crc2: - raise SyntaxError("broken PNG file (bad header checksum in %r)" - % cid) - except struct.error: - raise SyntaxError("broken PNG file (incomplete checksum in %r)" - % cid) + raise SyntaxError( + f"broken PNG file (bad header checksum in {repr(cid)})" + ) + except struct.error as e: + raise SyntaxError( + f"broken PNG file (incomplete checksum in {repr(cid)})" + ) from e def crc_skip(self, cid, data): - "Read checksum. Used if the C module is not present" + """Read checksum""" self.fp.read(4) @@ -171,8 +238,8 @@ def verify(self, endchunk=b"IEND"): while True: try: cid, pos, length = self.read() - except struct.error: - raise IOError("truncated PNG file") + except struct.error as e: + raise OSError("truncated PNG file") from e if cid == endchunk: break @@ -188,8 +255,9 @@ class iTXt(str): keeping their extra information """ + @staticmethod - def __new__(cls, text, lang, tkey): + def __new__(cls, text, lang=None, tkey=None): """ :param cls: the class to use when creating the instance :param text: value for this key @@ -203,7 +271,7 @@ def __new__(cls, text, lang, tkey): return self -class PngInfo(object): +class PngInfo: """ PNG chunk container (for use with save(pnginfo=)) @@ -212,15 +280,20 @@ class PngInfo(object): def __init__(self): self.chunks = [] - def add(self, cid, data): + def add(self, cid, data, after_idat=False): """Appends an arbitrary chunk. Use with caution. :param cid: a byte string, 4 bytes long. :param data: a byte string of the encoded data + :param after_idat: for use with private chunks. Whether the chunk + should be written after IDAT """ - self.chunks.append((cid, data)) + chunk = [cid, data] + if after_idat: + chunk.append(True) + self.chunks.append(tuple(chunk)) def add_itxt(self, key, value, lang="", tkey="", zip=False): """Appends an iTXt chunk. @@ -243,11 +316,12 @@ def add_itxt(self, key, value, lang="", tkey="", zip=False): tkey = tkey.encode("utf-8", "strict") if zip: - self.add(b"iTXt", key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + - zlib.compress(value)) + self.add( + b"iTXt", + key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + zlib.compress(value), + ) else: - self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + - value) + self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value) def add_text(self, key, value, zip=False): """Appends a text chunk. @@ -264,12 +338,12 @@ def add_text(self, key, value, zip=False): # The tEXt chunk stores latin-1 text if not isinstance(value, bytes): try: - value = value.encode('latin-1', 'strict') + value = value.encode("latin-1", "strict") except UnicodeError: return self.add_itxt(key, value, zip=zip) if not isinstance(key, bytes): - key = key.encode('latin-1', 'strict') + key = key.encode("latin-1", "strict") if zip: self.add(b"zTXt", key + b"\0\0" + zlib.compress(value)) @@ -280,11 +354,10 @@ def add_text(self, key, value, zip=False): # -------------------------------------------------------------------- # PNG image stream (IHDR/IEND) -class PngStream(ChunkStream): +class PngStream(ChunkStream): def __init__(self, fp): - - ChunkStream.__init__(self, fp) + super().__init__(fp) # local copies of Image attributes self.im_info = {} @@ -293,14 +366,32 @@ def __init__(self, fp): self.im_mode = None self.im_tile = None self.im_palette = None + self.im_custom_mimetype = None + self.im_n_frames = None + self._seq_num = None + self.rewind_state = None self.text_memory = 0 def check_text_memory(self, chunklen): self.text_memory += chunklen if self.text_memory > MAX_TEXT_MEMORY: - raise ValueError("Too much memory used in text chunks: %s>MAX_TEXT_MEMORY" % - self.text_memory) + raise ValueError( + "Too much memory used in text chunks: " + f"{self.text_memory}>MAX_TEXT_MEMORY" + ) + + def save_rewind(self): + self.rewind_state = { + "info": self.im_info.copy(), + "tile": self.im_tile, + "seq_num": self._seq_num, + } + + def rewind(self): + self.im_info = self.rewind_state["info"] + self.im_tile = self.rewind_state["tile"] + self._seq_num = self.rewind_state["seq_num"] def chunk_iCCP(self, pos, length): @@ -313,13 +404,12 @@ def chunk_iCCP(self, pos, length): # Compressed profile n bytes (zlib with deflate compression) i = s.find(b"\0") logger.debug("iCCP profile name %r", s[:i]) - logger.debug("Compression method %s", i8(s[i])) - comp_method = i8(s[i]) + logger.debug("Compression method %s", s[i]) + comp_method = s[i] if comp_method != 0: - raise SyntaxError("Unknown compression method %s in iCCP chunk" % - comp_method) + raise SyntaxError(f"Unknown compression method {comp_method} in iCCP chunk") try: - icc_profile = _safe_zlib_decompress(s[i+2:]) + icc_profile = _safe_zlib_decompress(s[i + 2 :]) except ValueError: if ImageFile.LOAD_TRUNCATED_IMAGES: icc_profile = None @@ -334,21 +424,31 @@ def chunk_IHDR(self, pos, length): # image header s = ImageFile._safe_read(self.fp, length) - self.im_size = i32(s), i32(s[4:]) + if length < 13: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + raise ValueError("Truncated IHDR chunk") + self.im_size = i32(s, 0), i32(s, 4) try: - self.im_mode, self.im_rawmode = _MODES[(i8(s[8]), i8(s[9]))] - except: + self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])] + except Exception: pass - if i8(s[12]): + if s[12]: self.im_info["interlace"] = 1 - if i8(s[11]): + if s[11]: raise SyntaxError("unknown filter category") return s def chunk_IDAT(self, pos, length): # image data - self.im_tile = [("zip", (0, 0)+self.im_size, pos, self.im_rawmode)] + if "bbox" in self.im_info: + tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)] + else: + if self.im_n_frames is not None: + self.im_info["default_image"] = True + tile = [("zip", (0, 0) + self.im_size, pos, self.im_rawmode)] + self.im_tile = tile self.im_idat = length raise EOFError @@ -380,10 +480,10 @@ def chunk_tRNS(self, pos, length): # otherwise, we have a byte string with one alpha value # for each palette entry self.im_info["transparency"] = s - elif self.im_mode == "L": + elif self.im_mode in ("1", "L", "I"): self.im_info["transparency"] = i16(s) elif self.im_mode == "RGB": - self.im_info["transparency"] = i16(s), i16(s[2:]), i16(s[4:]) + self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4) return s def chunk_gAMA(self, pos, length): @@ -397,8 +497,8 @@ def chunk_cHRM(self, pos, length): # WP x,y, Red x,y, Green x,y Blue x,y s = ImageFile._safe_read(self.fp, length) - raw_vals = struct.unpack('>%dI' % (len(s) // 4), s) - self.im_info['chromaticity'] = tuple(elt/100000.0 for elt in raw_vals) + raw_vals = struct.unpack(">%dI" % (len(s) // 4), s) + self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals) return s def chunk_sRGB(self, pos, length): @@ -409,17 +509,25 @@ def chunk_sRGB(self, pos, length): # 3 absolute colorimetric s = ImageFile._safe_read(self.fp, length) - self.im_info['srgb'] = i8(s) + if length < 1: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + raise ValueError("Truncated sRGB chunk") + self.im_info["srgb"] = s[0] return s def chunk_pHYs(self, pos, length): # pixels per unit s = ImageFile._safe_read(self.fp, length) - px, py = i32(s), i32(s[4:]) - unit = i8(s[8]) + if length < 9: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + raise ValueError("Truncated pHYs chunk") + px, py = i32(s, 0), i32(s, 4) + unit = s[8] if unit == 1: # meter - dpi = int(px * 0.0254 + 0.5), int(py * 0.0254 + 0.5) + dpi = px * 0.0254, py * 0.0254 self.im_info["dpi"] = dpi elif unit == 0: self.im_info["aspect"] = px, py @@ -436,12 +544,12 @@ def chunk_tEXt(self, pos, length): k = s v = b"" if k: - if bytes is not str: - k = k.decode('latin-1', 'strict') - v = v.decode('latin-1', 'replace') + k = k.decode("latin-1", "strict") + v_str = v.decode("latin-1", "replace") - self.im_info[k] = self.im_text[k] = v - self.check_text_memory(len(v)) + self.im_info[k] = v if k == "exif" else v_str + self.im_text[k] = v_str + self.check_text_memory(len(v_str)) return s @@ -455,12 +563,11 @@ def chunk_zTXt(self, pos, length): k = s v = b"" if v: - comp_method = i8(v[0]) + comp_method = v[0] else: comp_method = 0 if comp_method != 0: - raise SyntaxError("Unknown compression method %s in zTXt chunk" % - comp_method) + raise SyntaxError(f"Unknown compression method {comp_method} in zTXt chunk") try: v = _safe_zlib_decompress(v[1:]) except ValueError: @@ -472,9 +579,8 @@ def chunk_zTXt(self, pos, length): v = b"" if k: - if bytes is not str: - k = k.decode('latin-1', 'strict') - v = v.decode('latin-1', 'replace') + k = k.decode("latin-1", "strict") + v = v.decode("latin-1", "replace") self.im_info[k] = self.im_text[k] = v self.check_text_memory(len(v)) @@ -491,7 +597,7 @@ def chunk_iTXt(self, pos, length): return s if len(r) < 2: return s - cf, cm, r = i8(r[0]), i8(r[1]), r[2:] + cf, cm, r = r[0], r[1], r[2:] try: lang, tk, v = r.split(b"\0", 2) except ValueError: @@ -509,24 +615,88 @@ def chunk_iTXt(self, pos, length): return s else: return s - if bytes is not str: - try: - k = k.decode("latin-1", "strict") - lang = lang.decode("utf-8", "strict") - tk = tk.decode("utf-8", "strict") - v = v.decode("utf-8", "strict") - except UnicodeError: - return s + try: + k = k.decode("latin-1", "strict") + lang = lang.decode("utf-8", "strict") + tk = tk.decode("utf-8", "strict") + v = v.decode("utf-8", "strict") + except UnicodeError: + return s self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk) self.check_text_memory(len(v)) return s + def chunk_eXIf(self, pos, length): + s = ImageFile._safe_read(self.fp, length) + self.im_info["exif"] = b"Exif\x00\x00" + s + return s + + # APNG chunks + def chunk_acTL(self, pos, length): + s = ImageFile._safe_read(self.fp, length) + if length < 8: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + raise ValueError("APNG contains truncated acTL chunk") + if self.im_n_frames is not None: + self.im_n_frames = None + warnings.warn("Invalid APNG, will use default PNG image if possible") + return s + n_frames = i32(s) + if n_frames == 0 or n_frames > 0x80000000: + warnings.warn("Invalid APNG, will use default PNG image if possible") + return s + self.im_n_frames = n_frames + self.im_info["loop"] = i32(s, 4) + self.im_custom_mimetype = "image/apng" + return s + + def chunk_fcTL(self, pos, length): + s = ImageFile._safe_read(self.fp, length) + if length < 26: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + raise ValueError("APNG contains truncated fcTL chunk") + 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") + 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") + self.im_info["bbox"] = (px, py, px + width, py + height) + delay_num, delay_den = i16(s, 20), i16(s, 22) + if delay_den == 0: + delay_den = 100 + self.im_info["duration"] = float(delay_num) / float(delay_den) * 1000 + self.im_info["disposal"] = s[24] + self.im_info["blend"] = s[25] + return s + + def chunk_fdAT(self, pos, length): + if length < 4: + if ImageFile.LOAD_TRUNCATED_IMAGES: + s = ImageFile._safe_read(self.fp, length) + return s + raise ValueError("APNG contains truncated fDAT chunk") + s = ImageFile._safe_read(self.fp, 4) + seq = i32(s) + if self._seq_num != seq - 1: + raise SyntaxError("APNG contains frame sequence errors") + self._seq_num = seq + return self.chunk_IDAT(pos + 4, length - 4) + # -------------------------------------------------------------------- # PNG reader + def _accept(prefix): return prefix[:8] == _MAGIC @@ -534,6 +704,7 @@ def _accept(prefix): ## # Image plugin for PNG images. + class PngImageFile(ImageFile.ImageFile): format = "PNG" @@ -541,12 +712,15 @@ class PngImageFile(ImageFile.ImageFile): def _open(self): - if self.fp.read(8) != _MAGIC: + if not _accept(self.fp.read(8)): raise SyntaxError("not a PNG file") + self._fp = self.fp + self.__frame = 0 # - # Parse headers up to the first IDAT chunk + # Parse headers up to the first IDAT or fDAT chunk + self.private_chunks = [] self.png = PngStream(self.fp) while True: @@ -563,6 +737,8 @@ def _open(self): except AttributeError: logger.debug("%r %s %s (unknown)", cid, pos, length) s = ImageFile._safe_read(self.fp, length) + if cid[1:2].islower(): + self.private_chunks.append((cid, s)) self.png.crc(cid, s) @@ -574,19 +750,51 @@ def _open(self): # (believe me, I've tried ;-) self.mode = self.png.im_mode - self.size = self.png.im_size + self._size = self.png.im_size self.info = self.png.im_info - self.text = self.png.im_text # experimental + self._text = None self.tile = self.png.im_tile + self.custom_mimetype = self.png.im_custom_mimetype + self.n_frames = self.png.im_n_frames or 1 + self.default_image = self.info.get("default_image", False) if self.png.im_palette: rawmode, data = self.png.im_palette self.palette = ImagePalette.raw(rawmode, data) - self.__idat = length # used by load_read() + if cid == b"fdAT": + self.__prepare_idat = length - 4 + else: + self.__prepare_idat = length # used by load_prepare() + + if self.png.im_n_frames is not None: + self._close_exclusive_fp_after_loading = False + self.png.save_rewind() + self.__rewind_idat = self.__prepare_idat + self.__rewind = self._fp.tell() + if self.default_image: + # IDAT chunk contains default image and not first animation frame + self.n_frames += 1 + self._seek(0) + self.is_animated = self.n_frames > 1 + + @property + def text(self): + # experimental + if self._text is None: + # iTxt, tEXt and zTXt chunks may appear at the end of the file + # So load the file to ensure that they are read + if self.is_animated: + frame = self.__frame + # for APNG, seek to the final frame before loading + self.seek(self.n_frames - 1) + self.load() + if self.is_animated: + self.seek(frame) + return self._text def verify(self): - "Verify PNG file" + """Verify PNG file""" if self.fp is None: raise RuntimeError("verify must be called directly after open") @@ -597,18 +805,128 @@ def verify(self): self.png.verify() self.png.close() + if self._exclusive_fp: + self.fp.close() self.fp = None + def seek(self, frame): + if not self._seek_check(frame): + return + if frame < self.__frame: + self._seek(0, True) + + last_frame = self.__frame + for f in range(self.__frame + 1, frame + 1): + try: + self._seek(f) + except EOFError as e: + self.seek(last_frame) + raise EOFError("no more images in APNG file") from e + + def _seek(self, frame, rewind=False): + if frame == 0: + if rewind: + self._fp.seek(self.__rewind) + self.png.rewind() + self.__prepare_idat = self.__rewind_idat + self.im = None + if self.pyaccess: + self.pyaccess = None + self.info = self.png.im_info + self.tile = self.png.im_tile + self.fp = self._fp + self._prev_im = None + self.dispose = None + self.default_image = self.info.get("default_image", False) + self.dispose_op = self.info.get("disposal") + self.blend_op = self.info.get("blend") + self.dispose_extent = self.info.get("bbox") + self.__frame = 0 + else: + if frame != self.__frame + 1: + raise ValueError(f"cannot seek to frame {frame}") + + # ensure previous frame was loaded + self.load() + + if self.dispose: + self.im.paste(self.dispose, self.dispose_extent) + self._prev_im = self.im.copy() + + self.fp = self._fp + + # advance to the next frame + if self.__prepare_idat: + ImageFile._safe_read(self.fp, self.__prepare_idat) + self.__prepare_idat = 0 + frame_start = False + while True: + self.fp.read(4) # CRC + + try: + cid, pos, length = self.png.read() + except (struct.error, SyntaxError): + break + + if cid == b"IEND": + raise EOFError("No more images in APNG file") + if cid == b"fcTL": + if frame_start: + # there must be at least one fdAT chunk between fcTL chunks + raise SyntaxError("APNG missing frame data") + frame_start = True + + try: + self.png.call(cid, pos, length) + except UnicodeDecodeError: + break + except EOFError: + if cid == b"fdAT": + length -= 4 + if frame_start: + self.__prepare_idat = length + break + ImageFile._safe_read(self.fp, length) + except AttributeError: + logger.debug("%r %s %s (unknown)", cid, pos, length) + ImageFile._safe_read(self.fp, length) + + self.__frame = frame + self.tile = self.png.im_tile + self.dispose_op = self.info.get("disposal") + self.blend_op = self.info.get("blend") + self.dispose_extent = self.info.get("bbox") + + if not self.tile: + raise EOFError + + # setup frame disposal (actual disposal done when needed in the next _seek()) + if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS: + self.dispose_op = Disposal.OP_BACKGROUND + + if self.dispose_op == Disposal.OP_PREVIOUS: + self.dispose = self._prev_im.copy() + self.dispose = self._crop(self.dispose, self.dispose_extent) + elif self.dispose_op == Disposal.OP_BACKGROUND: + self.dispose = Image.core.fill(self.mode, self.size) + self.dispose = self._crop(self.dispose, self.dispose_extent) + else: + self.dispose = None + + def tell(self): + return self.__frame + def load_prepare(self): - "internal: prepare to read PNG file" + """internal: prepare to read PNG file""" if self.info.get("interlace"): self.decoderconfig = self.decoderconfig + (1,) + self.__idat = self.__prepare_idat # used by load_read() ImageFile.ImageFile.load_prepare(self) def load_read(self, read_bytes): - "internal: read more image data" + """internal: read more image data""" while self.__idat == 0: # end of chunk, skip forward to next one @@ -617,11 +935,18 @@ def load_read(self, read_bytes): cid, pos, length = self.png.read() - if cid not in [b"IDAT", b"DDAT"]: + if cid not in [b"IDAT", b"DDAT", b"fdAT"]: self.png.push(cid, pos, length) return b"" - self.__idat = length # empty chunks are allowed + if cid == b"fdAT": + try: + self.png.call(cid, pos, length) + except EOFError: + pass + self.__idat = length - 4 # sequence_num has already been read + else: + self.__idat = length # empty chunks are allowed # read more data from this chunk if read_bytes <= 0: @@ -634,10 +959,77 @@ def load_read(self, read_bytes): return self.fp.read(read_bytes) def load_end(self): - "internal: finished reading image data" + """internal: finished reading image data""" + if self.__idat != 0: + self.fp.read(self.__idat) + while True: + self.fp.read(4) # CRC - self.png.close() - self.png = None + try: + cid, pos, length = self.png.read() + except (struct.error, SyntaxError): + break + + if cid == b"IEND": + break + elif cid == b"fcTL" and self.is_animated: + # start of the next frame, stop reading + self.__prepare_idat = 0 + self.png.push(cid, pos, length) + break + + try: + self.png.call(cid, pos, length) + except UnicodeDecodeError: + break + except EOFError: + if cid == b"fdAT": + length -= 4 + ImageFile._safe_read(self.fp, length) + except AttributeError: + logger.debug("%r %s %s (unknown)", cid, pos, length) + s = ImageFile._safe_read(self.fp, length) + if cid[1:2].islower(): + self.private_chunks.append((cid, s, True)) + self._text = self.png.im_text + if not self.is_animated: + self.png.close() + self.png = None + else: + if self._prev_im and self.blend_op == Blend.OP_OVER: + updated = self._crop(self.im, self.dispose_extent) + self._prev_im.paste( + updated, self.dispose_extent, updated.convert("RGBA") + ) + self.im = self._prev_im + if self.pyaccess: + self.pyaccess = None + + def _getexif(self): + if "exif" not in self.info: + self.load() + if "exif" not in self.info and "Raw profile type exif" not in self.info: + return None + return self.getexif()._get_merged_dict() + + def getexif(self): + if "exif" not in self.info: + self.load() + + return super().getexif() + + def getxmp(self): + """ + Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. + + :returns: XMP tags in a dictionary. + """ + return ( + self._getxmp(self.info["XML:com.adobe.xmp"]) + if "XML:com.adobe.xmp" in self.info + else {} + ) # -------------------------------------------------------------------- @@ -645,34 +1037,35 @@ def load_end(self): _OUTMODES = { # supported PIL modes, and corresponding rawmodes/bits/color combinations - "1": ("1", b'\x01\x00'), - "L;1": ("L;1", b'\x01\x00'), - "L;2": ("L;2", b'\x02\x00'), - "L;4": ("L;4", b'\x04\x00'), - "L": ("L", b'\x08\x00'), - "LA": ("LA", b'\x08\x04'), - "I": ("I;16B", b'\x10\x00'), - "P;1": ("P;1", b'\x01\x03'), - "P;2": ("P;2", b'\x02\x03'), - "P;4": ("P;4", b'\x04\x03'), - "P": ("P", b'\x08\x03'), - "RGB": ("RGB", b'\x08\x02'), - "RGBA": ("RGBA", b'\x08\x06'), + "1": ("1", b"\x01\x00"), + "L;1": ("L;1", b"\x01\x00"), + "L;2": ("L;2", b"\x02\x00"), + "L;4": ("L;4", b"\x04\x00"), + "L": ("L", b"\x08\x00"), + "LA": ("LA", b"\x08\x04"), + "I": ("I;16B", b"\x10\x00"), + "I;16": ("I;16B", b"\x10\x00"), + "P;1": ("P;1", b"\x01\x03"), + "P;2": ("P;2", b"\x02\x03"), + "P;4": ("P;4", b"\x04\x03"), + "P": ("P", b"\x08\x03"), + "RGB": ("RGB", b"\x08\x02"), + "RGBA": ("RGBA", b"\x08\x06"), } def putchunk(fp, cid, *data): - "Write a PNG chunk (including CRC field)" + """Write a PNG chunk (including CRC field)""" data = b"".join(data) fp.write(o32(len(data)) + cid) fp.write(data) - hi, lo = Image.core.crc32(data, Image.core.crc32(cid)) - fp.write(o16(hi) + o16(lo)) + crc = _crc32(data, _crc32(cid)) + fp.write(o32(crc)) -class _idat(object): +class _idat: # wrap output from the encoder in IDAT chunks def __init__(self, fp, chunk): @@ -683,10 +1076,171 @@ def write(self, data): self.chunk(self.fp, b"IDAT", data) -def _save(im, fp, filename, chunk=putchunk): +class _fdat: + # wrap encoder output in fdAT chunks + + def __init__(self, fp, chunk, seq_num): + self.fp = fp + self.chunk = chunk + self.seq_num = seq_num + + def write(self, data): + self.chunk(self.fp, b"fdAT", o32(self.seq_num), data) + self.seq_num += 1 + + +def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images): + duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) + loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) + disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE)) + blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE)) + + if default_image: + chain = itertools.chain(append_images) + else: + chain = itertools.chain([im], append_images) + + im_frames = [] + frame_count = 0 + for im_seq in chain: + for im_frame in ImageSequence.Iterator(im_seq): + if im_frame.mode == rawmode: + im_frame = im_frame.copy() + else: + if rawmode == "P": + im_frame = im_frame.convert(rawmode, palette=im.palette) + else: + im_frame = im_frame.convert(rawmode) + encoderinfo = im.encoderinfo.copy() + if isinstance(duration, (list, tuple)): + encoderinfo["duration"] = duration[frame_count] + if isinstance(disposal, (list, tuple)): + encoderinfo["disposal"] = disposal[frame_count] + if isinstance(blend, (list, tuple)): + encoderinfo["blend"] = blend[frame_count] + frame_count += 1 + + if im_frames: + previous = im_frames[-1] + prev_disposal = previous["encoderinfo"].get("disposal") + prev_blend = previous["encoderinfo"].get("blend") + if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2: + prev_disposal = Disposal.OP_BACKGROUND + + if prev_disposal == Disposal.OP_BACKGROUND: + base_im = previous["im"].copy() + dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0)) + bbox = previous["bbox"] + if bbox: + dispose = dispose.crop(bbox) + else: + bbox = (0, 0) + im.size + base_im.paste(dispose, bbox) + elif prev_disposal == Disposal.OP_PREVIOUS: + base_im = im_frames[-2]["im"] + else: + base_im = previous["im"] + delta = ImageChops.subtract_modulo( + im_frame.convert("RGB"), base_im.convert("RGB") + ) + bbox = delta.getbbox() + if ( + not bbox + and prev_disposal == encoderinfo.get("disposal") + and prev_blend == encoderinfo.get("blend") + ): + if isinstance(duration, (list, tuple)): + previous["encoderinfo"]["duration"] += encoderinfo["duration"] + continue + else: + bbox = None + im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) + + # animation control + chunk( + fp, + b"acTL", + o32(len(im_frames)), # 0: num_frames + o32(loop), # 4: num_plays + ) + + # default image IDAT (if it exists) + if default_image: + ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) + + seq_num = 0 + for frame, frame_data in enumerate(im_frames): + im_frame = frame_data["im"] + if not frame_data["bbox"]: + bbox = (0, 0) + im_frame.size + else: + bbox = frame_data["bbox"] + im_frame = im_frame.crop(bbox) + size = im_frame.size + encoderinfo = frame_data["encoderinfo"] + frame_duration = int(round(encoderinfo.get("duration", duration))) + frame_disposal = encoderinfo.get("disposal", disposal) + frame_blend = encoderinfo.get("blend", blend) + # frame control + chunk( + fp, + b"fcTL", + o32(seq_num), # sequence_number + o32(size[0]), # width + o32(size[1]), # height + o32(bbox[0]), # x_offset + o32(bbox[1]), # y_offset + o16(frame_duration), # delay_numerator + o16(1000), # delay_denominator + o8(frame_disposal), # dispose_op + o8(frame_blend), # blend_op + ) + seq_num += 1 + # frame data + if frame == 0 and not default_image: + # first frame must be in IDAT chunks for backwards compatibility + ImageFile._save( + im_frame, + _idat(fp, chunk), + [("zip", (0, 0) + im_frame.size, 0, rawmode)], + ) + else: + fdat_chunks = _fdat(fp, chunk, seq_num) + ImageFile._save( + im_frame, + fdat_chunks, + [("zip", (0, 0) + im_frame.size, 0, rawmode)], + ) + seq_num = fdat_chunks.seq_num + + +def _save_all(im, fp, filename): + _save(im, fp, filename, save_all=True) + + +def _save(im, fp, filename, chunk=putchunk, save_all=False): # save an image to disk (called by the save method) - mode = im.mode + if save_all: + default_image = im.encoderinfo.get( + "default_image", im.info.get("default_image") + ) + modes = set() + append_images = im.encoderinfo.get("append_images", []) + if default_image: + chain = itertools.chain(append_images) + else: + chain = itertools.chain([im], append_images) + for im_seq in chain: + for im_frame in ImageSequence.Iterator(im_seq): + modes.add(im_frame.mode) + for mode in ("RGBA", "RGB", "P"): + if mode in modes: + break + else: + mode = modes.pop() + else: + mode = im.mode if mode == "P": @@ -694,48 +1248,52 @@ def _save(im, fp, filename, chunk=putchunk): # attempt to minimize storage requirements for palette images if "bits" in im.encoderinfo: # number of bits specified by user - colors = 1 << im.encoderinfo["bits"] + colors = min(1 << im.encoderinfo["bits"], 256) else: # check palette contents if im.palette: - colors = max(min(len(im.palette.getdata()[1])//3, 256), 2) + colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1) else: colors = 256 - if colors <= 2: - bits = 1 - elif colors <= 4: - bits = 2 - elif colors <= 16: - bits = 4 - else: - bits = 8 - if bits != 8: - mode = "%s;%d" % (mode, bits) + if colors <= 16: + if colors <= 2: + bits = 1 + elif colors <= 4: + bits = 2 + else: + bits = 4 + mode = f"{mode};{bits}" # encoder options - im.encoderconfig = (im.encoderinfo.get("optimize", False), - im.encoderinfo.get("compress_level", -1), - im.encoderinfo.get("compress_type", -1), - im.encoderinfo.get("dictionary", b"")) + im.encoderconfig = ( + im.encoderinfo.get("optimize", False), + im.encoderinfo.get("compress_level", -1), + im.encoderinfo.get("compress_type", -1), + im.encoderinfo.get("dictionary", b""), + ) # get the corresponding PNG mode try: rawmode, mode = _OUTMODES[mode] - except KeyError: - raise IOError("cannot write mode %s as PNG" % mode) + except KeyError as e: + raise OSError(f"cannot write mode {mode} as PNG") from e # # write minimal PNG file fp.write(_MAGIC) - chunk(fp, b"IHDR", - o32(im.size[0]), o32(im.size[1]), # 0: size - mode, # 8: depth/type - b'\0', # 10: compression - b'\0', # 11: filter category - b'\0') # 12: interlace flag + chunk( + fp, + b"IHDR", + o32(im.size[0]), # 0: size + o32(im.size[1]), + mode, # 8: depth/type + b"\0", # 10: compression + b"\0", # 11: filter category + b"\0", # 12: interlace flag + ) chunks = [b"cHRM", b"gAMA", b"sBIT", b"sRGB", b"tIME"] @@ -750,7 +1308,7 @@ def _save(im, fp, filename, chunk=putchunk): name = b"ICC Profile" data = name + b"\0\0" + zlib.compress(icc) chunk(fp, b"iCCP", data) - + # You must either have sRGB or iCCP. # Disallow sRGB chunks when an iCCP-chunk has been emitted. chunks.remove(b"sRGB") @@ -758,34 +1316,39 @@ def _save(im, fp, filename, chunk=putchunk): info = im.encoderinfo.get("pnginfo") if info: chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"] - for cid, data in info.chunks: + for info_chunk in info.chunks: + cid, data = info_chunk[:2] if cid in chunks: chunks.remove(cid) chunk(fp, cid, data) elif cid in chunks_multiple_allowed: chunk(fp, cid, data) + elif cid[1:2].islower(): + # Private chunk + after_idat = info_chunk[2:3] + if not after_idat: + chunk(fp, cid, data) if im.mode == "P": - palette_byte_number = (2 ** bits) * 3 + palette_byte_number = colors * 3 palette_bytes = im.im.getpalette("RGB")[:palette_byte_number] while len(palette_bytes) < palette_byte_number: - palette_bytes += b'\0' + palette_bytes += b"\0" chunk(fp, b"PLTE", palette_bytes) - transparency = im.encoderinfo.get('transparency', - im.info.get('transparency', None)) + transparency = im.encoderinfo.get("transparency", im.info.get("transparency", None)) if transparency or transparency == 0: if im.mode == "P": # limit to actual palette size - alpha_bytes = 2**bits + alpha_bytes = colors if isinstance(transparency, bytes): chunk(fp, b"tRNS", transparency[:alpha_bytes]) else: transparency = max(0, min(255, transparency)) - alpha = b'\xFF' * transparency + b'\0' + alpha = b"\xFF" * transparency + b"\0" chunk(fp, b"tRNS", alpha[:alpha_bytes]) - elif im.mode == "L": + elif im.mode in ("1", "L", "I"): transparency = max(0, min(65535, transparency)) chunk(fp, b"tRNS", o16(transparency)) elif im.mode == "RGB": @@ -795,30 +1358,52 @@ def _save(im, fp, filename, chunk=putchunk): 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 IOError("cannot use transparency for this mode") + raise OSError("cannot use transparency for this mode") else: if im.mode == "P" and im.im.getpalettemode() == "RGBA": alpha = im.im.getpalette("RGBA", "A") - alpha_bytes = 2**bits + alpha_bytes = colors chunk(fp, b"tRNS", alpha[:alpha_bytes]) dpi = im.encoderinfo.get("dpi") if dpi: - chunk(fp, b"pHYs", - o32(int(dpi[0] / 0.0254 + 0.5)), - o32(int(dpi[1] / 0.0254 + 0.5)), - b'\x01') + chunk( + fp, + b"pHYs", + o32(int(dpi[0] / 0.0254 + 0.5)), + o32(int(dpi[1] / 0.0254 + 0.5)), + b"\x01", + ) - info = im.encoderinfo.get("pnginfo") if info: chunks = [b"bKGD", b"hIST"] - for cid, data in info.chunks: + for info_chunk in info.chunks: + cid, data = info_chunk[:2] if cid in chunks: chunks.remove(cid) chunk(fp, cid, data) - ImageFile._save(im, _idat(fp, chunk), - [("zip", (0, 0)+im.size, 0, rawmode)]) + exif = im.encoderinfo.get("exif", im.info.get("exif")) + if exif: + if isinstance(exif, Image.Exif): + exif = exif.tobytes(8) + if exif.startswith(b"Exif\x00\x00"): + exif = exif[6:] + chunk(fp, b"eXIf", exif) + + if save_all: + _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) + else: + ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) + + if info: + for info_chunk in info.chunks: + cid, data = info_chunk[:2] + if cid[1:2].islower(): + # Private chunk + after_idat = info_chunk[2:3] + if after_idat: + chunk(fp, cid, data) chunk(fp, b"IEND", b"") @@ -829,10 +1414,11 @@ def _save(im, fp, filename, chunk=putchunk): # -------------------------------------------------------------------- # PNG chunk converter + def getchunks(im, **params): """Return a list of PNG chunks representing this image.""" - class collector(object): + class collector: data = [] def write(self, data): @@ -843,8 +1429,7 @@ def append(self, chunk): def append(fp, cid, *data): data = b"".join(data) - hi, lo = Image.core.crc32(data, Image.core.crc32(cid)) - crc = o16(hi) + o16(lo) + crc = o32(_crc32(data, _crc32(cid))) fp.append((cid, data, crc)) fp = collector() @@ -863,7 +1448,8 @@ def append(fp, cid, *data): Image.register_open(PngImageFile.format, PngImageFile, _accept) Image.register_save(PngImageFile.format, _save) +Image.register_save_all(PngImageFile.format, _save_all) -Image.register_extension(PngImageFile.format, ".png") +Image.register_extensions(PngImageFile.format, [".png", ".apng"]) Image.register_mime(PngImageFile.format, "image/png") diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index be68d1b404f..392771d3e96 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -15,19 +15,21 @@ # -import string - from . import Image, ImageFile - -__version__ = "0.2" +from ._binary import i16be as i16 +from ._binary import o8 +from ._binary import o32le as o32 # # -------------------------------------------------------------------- -b_whitespace = b'\x20\x09\x0a\x0b\x0c\x0d' +b_whitespace = b"\x20\x09\x0a\x0b\x0c\x0d" MODES = { # standard + b"P1": "1", + b"P2": "L", + b"P3": "RGB", b"P4": "1", b"P5": "L", b"P6": "RGB", @@ -36,123 +38,305 @@ # PIL extensions (for test purposes only) b"PyP": "P", b"PyRGBA": "RGBA", - b"PyCMYK": "CMYK" + b"PyCMYK": "CMYK", } def _accept(prefix): - return prefix[0:1] == b"P" and prefix[1] in b"0456y" + return prefix[0:1] == b"P" and prefix[1] in b"0123456y" ## # Image plugin for PBM, PGM, and PPM images. + class PpmImageFile(ImageFile.ImageFile): format = "PPM" format_description = "Pbmplus image" - def _token(self, s=b""): - while True: # read until next whitespace + def _read_magic(self): + magic = b"" + # read until whitespace or longest available magic number + for _ in range(6): c = self.fp.read(1) if not c or c in b_whitespace: break - if c > b'\x79': - raise ValueError("Expected ASCII value, found binary") - s = s + c - if (len(s) > 9): - raise ValueError("Expected int, got > 9 digits") - return s + magic += c + return magic - def _open(self): + def _read_token(self): + token = b"" + while len(token) <= 10: # read until next whitespace or limit of 10 characters + c = self.fp.read(1) + if not c: + break + elif c in b_whitespace: # token ended + if not token: + # skip whitespace at start + continue + break + elif c == b"#": + # ignores rest of the line; stops at CR, LF or EOF + while self.fp.read(1) not in b"\r\n": + pass + continue + token += c + if not token: + # Token was not even 1 byte + raise ValueError("Reached EOF while reading header") + elif len(token) > 10: + raise ValueError(f"Token too long in file header: {token.decode()}") + return token - # check magic - s = self.fp.read(1) - if s != b"P": + def _open(self): + magic_number = self._read_magic() + try: + mode = MODES[magic_number] + except KeyError: raise SyntaxError("not a PPM file") - mode = MODES[self._token(s)] - if mode == "1": - self.mode = "1" - rawmode = "1;I" - else: - self.mode = rawmode = mode + if magic_number in (b"P1", b"P4"): + self.custom_mimetype = "image/x-portable-bitmap" + elif magic_number in (b"P2", b"P5"): + self.custom_mimetype = "image/x-portable-graymap" + elif magic_number in (b"P3", b"P6"): + self.custom_mimetype = "image/x-portable-pixmap" + maxval = None + decoder_name = "raw" + if magic_number in (b"P1", b"P2", b"P3"): + decoder_name = "ppm_plain" for ix in range(3): - while True: - while True: - s = self.fp.read(1) - if s not in b_whitespace: - break - if s == b"": - raise ValueError("File does not extend beyond magic number") - if s != b"#": - break - s = self.fp.readline() - s = int(self._token(s)) - if ix == 0: - xsize = s - elif ix == 1: - ysize = s + token = int(self._read_token()) + if ix == 0: # token is the x size + xsize = token + elif ix == 1: # token is the y size + ysize = token if mode == "1": + self.mode = "1" + rawmode = "1;I" break - elif ix == 2: - # maxgrey - if s > 255: - if not mode == 'L': - raise ValueError("Too many colors for band: %s" % s) - if s < 2**16: - self.mode = 'I' - rawmode = 'I;16B' - else: - self.mode = 'I' - rawmode = 'I;32B' - - self.size = xsize, ysize - self.tile = [("raw", - (0, 0, xsize, ysize), - self.fp.tell(), - (rawmode, 0, 1))] + else: + self.mode = rawmode = mode + 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" + ) + if maxval > 255 and mode == "L": + self.mode = "I" + + if decoder_name != "ppm_plain": + # If maxval matches a bit depth, use the raw decoder directly + if maxval == 65535 and mode == "L": + rawmode = "I;16B" + elif maxval != 255: + decoder_name = "ppm" + + args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval) + self._size = xsize, ysize + self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)] # # -------------------------------------------------------------------- + +class PpmPlainDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def _read_block(self): + return self.fd.read(ImageFile.SAFEBLOCK) + + def _find_comment_end(self, block, start=0): + a = block.find(b"\n", start) + b = block.find(b"\r", start) + return min(a, b) if a * b > 0 else max(a, b) # lowest nonnegative index (or -1) + + def _ignore_comments(self, block): + if self._comment_spans: + # Finish current comment + while block: + comment_end = self._find_comment_end(block) + if comment_end != -1: + # Comment ends in this block + # Delete tail of comment + block = block[comment_end + 1 :] + break + else: + # Comment spans whole block + # So read the next block, looking for the end + block = self._read_block() + + # Search for any further comments + self._comment_spans = False + while True: + comment_start = block.find(b"#") + if comment_start == -1: + # No comment found + break + comment_end = self._find_comment_end(block, comment_start) + if comment_end != -1: + # Comment ends in this block + # Delete comment + block = block[:comment_start] + block[comment_end + 1 :] + else: + # Comment continues to next block(s) + block = block[:comment_start] + self._comment_spans = True + break + return block + + def _decode_bitonal(self): + """ + This is a separate method because in the plain PBM format, all data tokens are + exactly one byte, so the inter-token whitespace is optional. + """ + data = bytearray() + total_bytes = self.state.xsize * self.state.ysize + + while len(data) != total_bytes: + block = self._read_block() # read next block + if not block: + # eof + break + + block = self._ignore_comments(block) + + 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])}") + data = (data + tokens)[:total_bytes] + invert = bytes.maketrans(b"01", b"\xFF\x00") + return data.translate(invert) + + def _decode_blocks(self, maxval): + data = bytearray() + max_len = 10 + out_byte_count = 4 if self.mode == "I" else 1 + out_max = 65535 if self.mode == "I" else 255 + bands = Image.getmodebands(self.mode) + total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count + + half_token = False + while len(data) != total_bytes: + block = self._read_block() # read next block + if not block: + if half_token: + block = bytearray(b" ") # flush half_token + else: + # eof + break + + block = self._ignore_comments(block) + + if half_token: + block = half_token + block # stitch half_token to new block + + tokens = block.split() + + 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]}" + ) + + for token in tokens: + if len(token) > max_len: + raise ValueError( + f"Token too long found in data: {token[:max_len + 1]}" + ) + value = int(token) + if value > maxval: + raise ValueError(f"Channel value too large for this mode: {value}") + value = round(value / maxval * out_max) + data += o32(value) if self.mode == "I" else o8(value) + if len(data) == total_bytes: # finished! + break + return data + + def decode(self, buffer): + self._comment_spans = False + if self.mode == "1": + data = self._decode_bitonal() + rawmode = "1;8" + else: + maxval = self.args[-1] + data = self._decode_blocks(maxval) + rawmode = "I;32" if self.mode == "I" else self.mode + self.set_as_raw(bytes(data), rawmode) + return -1, 0 + + +class PpmDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer): + data = bytearray() + maxval = self.args[-1] + in_byte_count = 1 if maxval < 256 else 2 + out_byte_count = 4 if self.mode == "I" else 1 + out_max = 65535 if self.mode == "I" else 255 + bands = Image.getmodebands(self.mode) + while len(data) < self.state.xsize * self.state.ysize * bands * out_byte_count: + pixels = self.fd.read(in_byte_count * bands) + if len(pixels) < in_byte_count * bands: + # eof + break + for b in range(bands): + value = ( + pixels[b] if in_byte_count == 1 else i16(pixels, b * in_byte_count) + ) + value = min(out_max, round(value / maxval * out_max)) + data += o32(value) if self.mode == "I" else o8(value) + rawmode = "I;32" if self.mode == "I" else self.mode + self.set_as_raw(bytes(data), rawmode) + return -1, 0 + + +# +# -------------------------------------------------------------------- + + def _save(im, fp, filename): if im.mode == "1": rawmode, head = "1;I", b"P4" elif im.mode == "L": rawmode, head = "L", b"P5" elif im.mode == "I": - if im.getextrema()[1] < 2**16: - rawmode, head = "I;16B", b"P5" - else: - rawmode, head = "I;32B", b"P5" - elif im.mode == "RGB": - rawmode, head = "RGB", b"P6" - elif im.mode == "RGBA": + rawmode, head = "I;16B", b"P5" + elif im.mode in ("RGB", "RGBA"): rawmode, head = "RGB", b"P6" else: - raise IOError("cannot write mode %s as PPM" % im.mode) - fp.write(head + ("\n%d %d\n" % im.size).encode('ascii')) + raise OSError(f"cannot write mode {im.mode} as PPM") + fp.write(head + b"\n%d %d\n" % im.size) if head == b"P6": fp.write(b"255\n") - if head == b"P5": + elif head == b"P5": if rawmode == "L": fp.write(b"255\n") - elif rawmode == "I;16B": + else: fp.write(b"65535\n") - elif rawmode == "I;32B": - fp.write(b"2147483648\n") - ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0, (rawmode, 0, 1))]) + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) # ALTERNATIVE: save via builtin debug function # im._dump(filename) + # # -------------------------------------------------------------------- + Image.register_open(PpmImageFile.format, PpmImageFile, _accept) Image.register_save(PpmImageFile.format, _save) -Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm"]) +Image.register_decoder("ppm", PpmDecoder) +Image.register_decoder("ppm_plain", PpmPlainDecoder) + +Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm"]) + +Image.register_mime(PpmImageFile.format, "image/x-portable-anymap") diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index ee803271743..bd10e3b95dd 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -16,10 +16,13 @@ # See the README file for information on usage and redistribution. # -__version__ = "0.4" +import io from . import Image, ImageFile, ImagePalette -from ._binary import i8, i16be as i16, i32be as i32 +from ._binary import i8 +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._binary import si16be as si16 MODES = { # (photoshop mode, bits) -> (pil mode, required channels) @@ -31,13 +34,14 @@ (4, 8): ("CMYK", 4), (7, 8): ("L", 1), # FIXME: multilayer (8, 8): ("L", 1), # duotone - (9, 8): ("LAB", 3) + (9, 8): ("LAB", 3), } # --------------------------------------------------------------------. # read PSD images + def _accept(prefix): return prefix[:4] == b"8BPS" @@ -45,10 +49,12 @@ def _accept(prefix): ## # Image plugin for Photoshop images. + class PsdImageFile(ImageFile.ImageFile): format = "PSD" format_description = "Adobe Photoshop" + _close_exclusive_fp_after_loading = False def _open(self): @@ -58,20 +64,23 @@ def _open(self): # header s = read(26) - if s[:4] != b"8BPS" or i16(s[4:]) != 1: + if not _accept(s) or i16(s, 4) != 1: raise SyntaxError("not a PSD file") - psd_bits = i16(s[22:]) - psd_channels = i16(s[12:]) - psd_mode = i16(s[24:]) + psd_bits = i16(s, 22) + psd_channels = i16(s, 12) + psd_mode = i16(s, 24) mode, channels = MODES[(psd_mode, psd_bits)] if channels > psd_channels: - raise IOError("not enough channels") + raise OSError("not enough channels") + if mode == "RGB" and psd_channels == 4: + mode = "RGBA" + channels = 4 self.mode = mode - self.size = i32(s[18:]), i32(s[14:]) + self._size = i32(s, 18), i32(s, 14) # # color mode data @@ -92,13 +101,13 @@ def _open(self): # load resources end = self.fp.tell() + size while self.fp.tell() < end: - signature = read(4) + read(4) # signature id = i16(read(2)) name = read(i8(read(1))) if not (len(name) & 1): read(1) # padding data = read(i32(read(4))) - if (len(data) & 1): + if len(data) & 1: read(1) # padding self.resources.append((id, name, data)) if id == 1039: # ICC profile @@ -114,8 +123,11 @@ def _open(self): end = self.fp.tell() + size size = i32(read(4)) if size: - self.layers = _layerinfo(self.fp) + _layer_data = io.BytesIO(ImageFile._safe_read(self.fp, size)) + self.layers = _layerinfo(_layer_data, size) self.fp.seek(end) + self.n_frames = len(self.layers) + self.is_animated = self.n_frames > 1 # # image descriptor @@ -127,48 +139,40 @@ def _open(self): self.frame = 1 self._min_frame = 1 - @property - def n_frames(self): - return len(self.layers) - - @property - def is_animated(self): - return len(self.layers) > 1 - def seek(self, layer): if not self._seek_check(layer): return # seek to given layer (1..max) try: - name, mode, bbox, tile = self.layers[layer-1] + name, mode, bbox, tile = self.layers[layer - 1] self.mode = mode self.tile = tile self.frame = layer self.fp = self._fp return name, bbox - except IndexError: - raise EOFError("no such layer") + except IndexError as e: + raise EOFError("no such layer") from e def tell(self): # return layer number (0=image, 1..max=layers) return self.frame - def load_prepare(self): - # create image memory if necessary - if not self.im or\ - self.im.mode != self.mode or self.im.size != self.size: - self.im = Image.core.fill(self.mode, self.size, 0) - # create palette (optional) - if self.mode == "P": - Image.Image.load(self) - -def _layerinfo(file): +def _layerinfo(fp, ct_bytes): # read layerinfo block layers = [] - read = file.read - for i in range(abs(i16(read(2)))): + + def read(size): + return ImageFile._safe_read(fp, size) + + ct = si16(read(2)) + + # sanity check + if ct_bytes < (abs(ct) * 20): + raise SyntaxError("Layer block too short for number of layers requested") + + for _ in range(abs(ct)): # bounding box y0 = i32(read(4)) @@ -177,13 +181,13 @@ def _layerinfo(file): x1 = i32(read(4)) # image info - info = [] mode = [] - types = list(range(i16(read(2)))) + ct_types = i16(read(2)) + types = list(range(ct_types)) if len(types) > 4: continue - for i in types: + for _ in types: type = i16(read(2)) if type == 65535: @@ -192,8 +196,7 @@ def _layerinfo(file): m = "RGBA"[type] mode.append(m) - size = i32(read(4)) - info.append((m, size)) + read(4) # size # figure out the image mode mode.sort() @@ -207,33 +210,27 @@ def _layerinfo(file): mode = None # unknown # skip over blend flags and extra information - filler = read(12) + read(12) # filler name = "" - size = i32(read(4)) - combined = 0 + size = i32(read(4)) # length of the extra data field if size: + data_end = fp.tell() + size + length = i32(read(4)) if length: - mask_y = i32(read(4)) - mask_x = i32(read(4)) - mask_h = i32(read(4)) - mask_y - mask_w = i32(read(4)) - mask_x - file.seek(length - 16, 1) - combined += length + 4 + fp.seek(length - 16, io.SEEK_CUR) length = i32(read(4)) if length: - file.seek(length, 1) - combined += length + 4 + fp.seek(length, io.SEEK_CUR) length = i8(read(1)) if length: # Don't know the proper encoding, # Latin-1 should be a good guess - name = read(length).decode('latin-1', 'replace') - combined += length + 1 + name = read(length).decode("latin-1", "replace") - file.seek(size - combined, 1) + fp.seek(data_end) layers.append((name, mode, (x0, y0, x1, y1))) # get tiles @@ -241,7 +238,7 @@ def _layerinfo(file): for name, mode, bbox in layers: tile = [] for m in mode: - t = _maketile(file, m, bbox, 1) + t = _maketile(fp, m, bbox, 1) if t: tile.extend(t) layers[i] = name, mode, bbox, tile @@ -271,7 +268,7 @@ def _maketile(file, mode, bbox, channels): if mode == "CMYK": layer += ";I" tile.append(("raw", bbox, offset, layer)) - offset = offset + xsize*ysize + offset = offset + xsize * ysize elif compression == 1: # @@ -284,11 +281,9 @@ def _maketile(file, mode, bbox, channels): layer = mode[channel] if mode == "CMYK": layer += ";I" - tile.append( - ("packbits", bbox, offset, layer) - ) + tile.append(("packbits", bbox, offset, layer)) for y in range(ysize): - offset = offset + i16(bytecount[i:i+2]) + offset = offset + i16(bytecount, i) i += 2 file.seek(offset) @@ -298,9 +293,13 @@ def _maketile(file, mode, bbox, channels): return tile + # -------------------------------------------------------------------- # registry + Image.register_open(PsdImageFile.format, PsdImageFile, _accept) Image.register_extension(PsdImageFile.format, ".psd") + +Image.register_mime(PsdImageFile.format, "image/vnd.adobe.photoshop") diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 7eec1b1606a..9a2ec48fc60 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -23,36 +23,43 @@ import logging import sys -from cffi import FFI - +try: + from cffi import FFI + + defs = """ + struct Pixel_RGBA { + unsigned char r,g,b,a; + }; + struct Pixel_I16 { + unsigned char l,r; + }; + """ + ffi = FFI() + ffi.cdef(defs) +except ImportError as ex: + # Allow error import for doc purposes, but error out when accessing + # anything in core. + from ._util import DeferredError + + FFI = ffi = DeferredError(ex) logger = logging.getLogger(__name__) -defs = """ -struct Pixel_RGBA { - unsigned char r,g,b,a; -}; -struct Pixel_I16 { - unsigned char l,r; -}; -""" -ffi = FFI() -ffi.cdef(defs) - - -class PyAccess(object): - +class PyAccess: def __init__(self, img, readonly=False): vals = dict(img.im.unsafe_ptrs) self.readonly = readonly - self.image8 = ffi.cast('unsigned char **', vals['image8']) - self.image32 = ffi.cast('int **', vals['image32']) - self.image = ffi.cast('unsigned char **', vals['image']) + self.image8 = ffi.cast("unsigned char **", vals["image8"]) + self.image32 = ffi.cast("int **", vals["image32"]) + self.image = ffi.cast("unsigned char **", vals["image"]) self.xsize, self.ysize = img.im.size + self._img = img # Keep pointer to im object to prevent dereferencing. self._im = img.im + if self._im.mode in ("P", "PA"): + self._palette = img.palette # Debugging is polluting test traces, only useful here # when hacking on PyAccess @@ -68,12 +75,32 @@ def __setitem__(self, xy, color): numerical value for single band images, and a tuple for multi-band images - :param xy: The pixel coordinate, given as (x, y). + :param xy: The pixel coordinate, given as (x, y). See + :ref:`coordinate-system`. :param color: The pixel value. """ if self.readonly: - raise ValueError('Attempt to putpixel a read only image') - (x, y) = self.check_xy(xy) + raise ValueError("Attempt to putpixel a read only image") + (x, y) = xy + if x < 0: + x = self.xsize + x + if y < 0: + y = self.ysize + y + (x, y) = self.check_xy((x, y)) + + if ( + self._im.mode in ("P", "PA") + and isinstance(color, (list, tuple)) + and len(color) in [3, 4] + ): + # RGB or RGBA value for a P or PA image + if self._im.mode == "PA": + alpha = color[3] if len(color) == 4 else 255 + color = color[:3] + color = self._palette.getcolor(color, self._img) + if self._im.mode == "PA": + color = (color, alpha) + return self.set_pixel(x, y, color) def __getitem__(self, xy): @@ -82,12 +109,17 @@ def __getitem__(self, xy): value for single band images or a tuple for multiple band images - :param xy: The pixel coordinate, given as (x, y). + :param xy: The pixel coordinate, given as (x, y). See + :ref:`coordinate-system`. :returns: a pixel value for single band images, a tuple of pixel values for multiband images. """ - - (x, y) = self.check_xy(xy) + (x, y) = xy + if x < 0: + x = self.xsize + x + if y < 0: + y = self.ysize + y + (x, y) = self.check_xy((x, y)) return self.get_pixel(x, y) putpixel = __setitem__ @@ -96,18 +128,19 @@ def __getitem__(self, xy): 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') + raise ValueError("pixel location out of range") return xy class _PyAccess32_2(PyAccess): - """ PA, LA, stored in first and last bytes of a 32 bit word """ + """PA, LA, stored in first and last bytes of a 32 bit word""" + def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) def get_pixel(self, x, y): pixel = self.pixels[y][x] - return (pixel.r, pixel.a) + return pixel.r, pixel.a def set_pixel(self, x, y, color): pixel = self.pixels[y][x] @@ -117,14 +150,14 @@ def set_pixel(self, x, y, color): class _PyAccess32_3(PyAccess): - """ RGB and friends, stored in the first three bytes of a 32 bit word """ + """RGB and friends, stored in the first three bytes of a 32 bit word""" def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) def get_pixel(self, x, y): pixel = self.pixels[y][x] - return (pixel.r, pixel.g, pixel.b) + return pixel.r, pixel.g, pixel.b def set_pixel(self, x, y, color): pixel = self.pixels[y][x] @@ -136,13 +169,14 @@ def set_pixel(self, x, y, color): class _PyAccess32_4(PyAccess): - """ RGBA etc, all 4 bytes of a 32 bit word """ + """RGBA etc, all 4 bytes of a 32 bit word""" + def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) def get_pixel(self, x, y): pixel = self.pixels[y][x] - return (pixel.r, pixel.g, pixel.b, pixel.a) + return pixel.r, pixel.g, pixel.b, pixel.a def set_pixel(self, x, y, color): pixel = self.pixels[y][x] @@ -154,7 +188,8 @@ def set_pixel(self, x, y, color): class _PyAccess8(PyAccess): - """ 1, L, P, 8 bit images stored as uint8 """ + """1, L, P, 8 bit images stored as uint8""" + def _post_init(self, *args, **kwargs): self.pixels = self.image8 @@ -171,9 +206,10 @@ def set_pixel(self, x, y, color): class _PyAccessI16_N(PyAccess): - """ I;16 access, native bitendian without conversion """ + """I;16 access, native bitendian without conversion""" + def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast('unsigned short **', self.image) + self.pixels = ffi.cast("unsigned short **", self.image) def get_pixel(self, x, y): return self.pixels[y][x] @@ -188,9 +224,10 @@ def set_pixel(self, x, y, color): class _PyAccessI16_L(PyAccess): - """ I;16L access, with conversion """ + """I;16L access, with conversion""" + def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast('struct Pixel_I16 **', self.image) + self.pixels = ffi.cast("struct Pixel_I16 **", self.image) def get_pixel(self, x, y): pixel = self.pixels[y][x] @@ -203,14 +240,15 @@ def set_pixel(self, x, y, color): except TypeError: color = min(color[0], 65535) - pixel.l = color & 0xFF + pixel.l = color & 0xFF # noqa: E741 pixel.r = color >> 8 class _PyAccessI16_B(PyAccess): - """ I;16B access, with conversion """ + """I;16B access, with conversion""" + def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast('struct Pixel_I16 **', self.image) + self.pixels = ffi.cast("struct Pixel_I16 **", self.image) def get_pixel(self, x, y): pixel = self.pixels[y][x] @@ -220,15 +258,16 @@ def set_pixel(self, x, y, color): pixel = self.pixels[y][x] try: color = min(color, 65535) - except: + except Exception: color = min(color[0], 65535) - pixel.l = color >> 8 + pixel.l = color >> 8 # noqa: E741 pixel.r = color & 0xFF class _PyAccessI32_N(PyAccess): - """ Signed Int32 access, native endian """ + """Signed Int32 access, native endian""" + def _post_init(self, *args, **kwargs): self.pixels = self.image32 @@ -240,16 +279,16 @@ def set_pixel(self, x, y, color): class _PyAccessI32_Swap(PyAccess): - """ I;32L/B access, with byteswapping conversion """ + """I;32L/B access, with byteswapping conversion""" + def _post_init(self, *args, **kwargs): self.pixels = self.image32 def reverse(self, i): - orig = ffi.new('int *', i) - chars = ffi.cast('unsigned char *', orig) - chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], \ - chars[1], chars[0] - return ffi.cast('int *', chars)[0] + orig = ffi.new("int *", i) + chars = ffi.cast("unsigned char *", orig) + chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], chars[1], chars[0] + return ffi.cast("int *", chars)[0] def get_pixel(self, x, y): return self.reverse(self.pixels[y][x]) @@ -259,9 +298,10 @@ def set_pixel(self, x, y, color): class _PyAccessF(PyAccess): - """ 32 bit float access """ + """32 bit float access""" + def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast('float **', self.image32) + self.pixels = ffi.cast("float **", self.image32) def get_pixel(self, x, y): return self.pixels[y][x] @@ -275,38 +315,39 @@ def set_pixel(self, x, y, color): self.pixels[y][x] = color[0] -mode_map = {'1': _PyAccess8, - 'L': _PyAccess8, - 'P': _PyAccess8, - 'LA': _PyAccess32_2, - 'La': _PyAccess32_2, - 'PA': _PyAccess32_2, - 'RGB': _PyAccess32_3, - 'LAB': _PyAccess32_3, - 'HSV': _PyAccess32_3, - 'YCbCr': _PyAccess32_3, - 'RGBA': _PyAccess32_4, - 'RGBa': _PyAccess32_4, - 'RGBX': _PyAccess32_4, - 'CMYK': _PyAccess32_4, - 'F': _PyAccessF, - 'I': _PyAccessI32_N, - } - -if sys.byteorder == 'little': - mode_map['I;16'] = _PyAccessI16_N - mode_map['I;16L'] = _PyAccessI16_N - mode_map['I;16B'] = _PyAccessI16_B - - mode_map['I;32L'] = _PyAccessI32_N - mode_map['I;32B'] = _PyAccessI32_Swap +mode_map = { + "1": _PyAccess8, + "L": _PyAccess8, + "P": _PyAccess8, + "LA": _PyAccess32_2, + "La": _PyAccess32_2, + "PA": _PyAccess32_2, + "RGB": _PyAccess32_3, + "LAB": _PyAccess32_3, + "HSV": _PyAccess32_3, + "YCbCr": _PyAccess32_3, + "RGBA": _PyAccess32_4, + "RGBa": _PyAccess32_4, + "RGBX": _PyAccess32_4, + "CMYK": _PyAccess32_4, + "F": _PyAccessF, + "I": _PyAccessI32_N, +} + +if sys.byteorder == "little": + mode_map["I;16"] = _PyAccessI16_N + mode_map["I;16L"] = _PyAccessI16_N + mode_map["I;16B"] = _PyAccessI16_B + + mode_map["I;32L"] = _PyAccessI32_N + mode_map["I;32B"] = _PyAccessI32_Swap else: - mode_map['I;16'] = _PyAccessI16_L - mode_map['I;16L'] = _PyAccessI16_L - mode_map['I;16B'] = _PyAccessI16_N + mode_map["I;16"] = _PyAccessI16_L + mode_map["I;16L"] = _PyAccessI16_L + mode_map["I;16B"] = _PyAccessI16_N - mode_map['I;32L'] = _PyAccessI32_Swap - mode_map['I;32B'] = _PyAccessI32_N + mode_map["I;32L"] = _PyAccessI32_Swap + mode_map["I;32B"] = _PyAccessI32_N def new(img, readonly=False): diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index 768f755882c..f0207bb7756 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -22,14 +22,12 @@ # -from . import Image, ImageFile -from ._binary import i8, o8, i16be as i16, o16be as o16 -import struct import os -import sys - +import struct -__version__ = "0.3" +from . import Image, ImageFile +from ._binary import i16be as i16 +from ._binary import o8 def _accept(prefix): @@ -44,7 +42,7 @@ def _accept(prefix): (1, 3, 3): "RGB", (2, 3, 3): "RGB;16B", (1, 3, 4): "RGBA", - (2, 3, 4): "RGBA;16B" + (2, 3, 4): "RGBA;16B", } @@ -61,27 +59,26 @@ def _open(self): headlen = 512 s = self.fp.read(headlen) - # magic number : 474 - if i16(s) != 474: + if not _accept(s): raise ValueError("Not an SGI image file") # compression : verbatim or RLE - compression = i8(s[2]) + compression = s[2] # bpc : 1 or 2 bytes (8bits or 16bits) - bpc = i8(s[3]) + bpc = s[3] # dimension : 1, 2 or 3 (depending on xsize, ysize and zsize) - dimension = i16(s[4:]) + dimension = i16(s, 4) # xsize : width - xsize = i16(s[6:]) + xsize = i16(s, 6) # ysize : height - ysize = i16(s[8:]) + ysize = i16(s, 8) # zsize : channels count - zsize = i16(s[10:]) + zsize = i16(s, 10) # layout layout = bpc, dimension, zsize @@ -96,8 +93,10 @@ def _open(self): if rawmode == "": raise ValueError("Unsupported SGI image mode") - self.size = xsize, ysize + self._size = xsize, ysize self.mode = rawmode.split(";")[0] + if self.mode == "RGB": + self.custom_mimetype = "image/rgb" # orientation -1 : scanlines begins at the bottom-left corner orientation = -1 @@ -106,19 +105,21 @@ def _open(self): if compression == 0: pagesize = xsize * ysize * bpc if bpc == 2: - self.tile = [("SGI16", (0, 0) + self.size, - headlen, (self.mode, 0, orientation))] + self.tile = [ + ("SGI16", (0, 0) + self.size, headlen, (self.mode, 0, orientation)) + ] else: self.tile = [] offset = headlen for layer in self.mode: self.tile.append( - ("raw", (0, 0) + self.size, - offset, (layer, 0, orientation))) + ("raw", (0, 0) + self.size, offset, (layer, 0, orientation)) + ) offset += pagesize elif compression == 1: - self.tile = [("sgi_rle", (0, 0) + self.size, - headlen, (rawmode, orientation, bpc))] + self.tile = [ + ("sgi_rle", (0, 0) + self.size, headlen, (rawmode, orientation, bpc)) + ] def _save(im, fp, filename): @@ -137,7 +138,7 @@ def _save(im, fp, filename): # Flip the image, since the origin of SGI file is the bottom-left corner orientation = -1 # Define the file as SGI File Format - magicNumber = 474 + magic_number = 474 # Run-Length Encoding Compression - Unsupported at this time rle = 0 @@ -157,42 +158,44 @@ def _save(im, fp, filename): # assert we've got the right number of bands. if len(im.getbands()) != z: - raise ValueError("incorrect number of bands in SGI write: %s vs %s" % - (z, len(im.getbands()))) + raise ValueError( + f"incorrect number of bands in SGI write: {z} vs {len(im.getbands())}" + ) # Minimum Byte value pinmin = 0 # Maximum Byte value (255 = 8bits per pixel) pinmax = 255 # Image name (79 characters max, truncated below in write) - imgName = os.path.splitext(os.path.basename(filename))[0] - if str is not bytes: - imgName = imgName.encode('ascii', 'ignore') + img_name = os.path.splitext(os.path.basename(filename))[0] + img_name = img_name.encode("ascii", "ignore") # Standard representation of pixel in the file colormap = 0 - fp.write(struct.pack('>h', magicNumber)) + fp.write(struct.pack(">h", magic_number)) fp.write(o8(rle)) fp.write(o8(bpc)) - fp.write(struct.pack('>H', dim)) - fp.write(struct.pack('>H', x)) - fp.write(struct.pack('>H', y)) - fp.write(struct.pack('>H', z)) - fp.write(struct.pack('>l', pinmin)) - fp.write(struct.pack('>l', pinmax)) - fp.write(struct.pack('4s', b'')) # dummy - fp.write(struct.pack('79s', imgName)) # truncates to 79 chars - fp.write(struct.pack('s', b'')) # force null byte after imgname - fp.write(struct.pack('>l', colormap)) - fp.write(struct.pack('404s', b'')) # dummy - - rawmode = 'L' + fp.write(struct.pack(">H", dim)) + fp.write(struct.pack(">H", x)) + fp.write(struct.pack(">H", y)) + fp.write(struct.pack(">H", z)) + fp.write(struct.pack(">l", pinmin)) + fp.write(struct.pack(">l", pinmax)) + fp.write(struct.pack("4s", b"")) # dummy + fp.write(struct.pack("79s", img_name)) # truncates to 79 chars + fp.write(struct.pack("s", b"")) # force null byte after img_name + fp.write(struct.pack(">l", colormap)) + fp.write(struct.pack("404s", b"")) # dummy + + rawmode = "L" if bpc == 2: - rawmode = 'L;16B' + rawmode = "L;16B" for channel in im.split(): - fp.write(channel.tobytes('raw', rawmode, 0, orientation)) + fp.write(channel.tobytes("raw", rawmode, 0, orientation)) + + if hasattr(fp, "flush"): + fp.flush() - fp.close() class SGI16Decoder(ImageFile.PyDecoder): _pulls_fd = True @@ -204,13 +207,15 @@ def decode(self, buffer): self.fd.seek(512) for band in range(zsize): - channel = Image.new('L', (self.state.xsize, self.state.ysize)) - channel.frombytes(self.fd.read(2 * pagesize), 'raw', - 'L;16B', stride, orientation) + channel = Image.new("L", (self.state.xsize, self.state.ysize)) + channel.frombytes( + self.fd.read(2 * pagesize), "raw", "L;16B", stride, orientation + ) self.im.putband(channel.im, band) return -1, 0 + # # registry @@ -219,7 +224,6 @@ def decode(self, buffer): Image.register_open(SgiImageFile.format, SgiImageFile, _accept) Image.register_save(SgiImageFile.format, _save) Image.register_mime(SgiImageFile.format, "image/sgi") -Image.register_mime(SgiImageFile.format, "image/rgb") Image.register_extensions(SgiImageFile.format, [".bw", ".rgb", ".rgba", ".sgi"]) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 5e5dde5a675..acafc320e64 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -15,7 +15,7 @@ # ## -# Image plugin for the Spider image format. This format is is used +# Image plugin for the Spider image format. This format is used # by the SPIDER software, in processing image data from electron # microscopy and tomography. ## @@ -32,27 +32,24 @@ # Details about the Spider image format: # https://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html # - -from __future__ import print_function - -from PIL import Image, ImageFile import os import struct import sys +from PIL import Image, ImageFile + def isInt(f): try: i = int(f) - if f-i == 0: + if f - i == 0: return 1 else: return 0 - except ValueError: - return 0 - except OverflowError: + except (ValueError, OverflowError): return 0 + iforms = [1, 3, -11, -12, -21, -22] @@ -61,8 +58,9 @@ def isInt(f): # Returns no. of bytes in the header, if it is a valid Spider header, # otherwise returns 0 + def isSpiderHeader(t): - h = (99,) + t # add 1 value so can use spider header index start=1 + h = (99,) + t # add 1 value so can use spider header index start=1 # header values 1,2,5,12,13,22,23 should be integers for i in [1, 2, 5, 12, 13, 22, 23]: if not isInt(h[i]): @@ -72,10 +70,9 @@ def isSpiderHeader(t): if iform not in iforms: return 0 # check other header values - labrec = int(h[13]) # no. records in file header - labbyt = int(h[22]) # total no. of bytes in header - lenbyt = int(h[23]) # record length in bytes - # print("labrec = %d, labbyt = %d, lenbyt = %d" % (labrec,labbyt,lenbyt)) + labrec = int(h[13]) # no. records in file header + labbyt = int(h[22]) # total no. of bytes in header + lenbyt = int(h[23]) # record length in bytes if labbyt != (labrec * lenbyt): return 0 # looks like a valid header @@ -83,12 +80,12 @@ def isSpiderHeader(t): def isSpiderImage(filename): - with open(filename, 'rb') as fp: - f = fp.read(92) # read 23 * 4 bytes - t = struct.unpack('>23f', f) # try big-endian first + with open(filename, "rb") as fp: + f = fp.read(92) # read 23 * 4 bytes + t = struct.unpack(">23f", f) # try big-endian first hdrlen = isSpiderHeader(t) if hdrlen == 0: - t = struct.unpack('<23f', f) # little-endian + t = struct.unpack("<23f", f) # little-endian hdrlen = isSpiderHeader(t) return hdrlen @@ -106,23 +103,23 @@ def _open(self): try: self.bigendian = 1 - t = struct.unpack('>27f', f) # try big-endian first + t = struct.unpack(">27f", f) # try big-endian first hdrlen = isSpiderHeader(t) if hdrlen == 0: self.bigendian = 0 - t = struct.unpack('<27f', f) # little-endian + t = struct.unpack("<27f", f) # little-endian hdrlen = isSpiderHeader(t) if hdrlen == 0: raise SyntaxError("not a valid Spider file") - except struct.error: - raise SyntaxError("not a valid Spider file") + except struct.error as e: + raise SyntaxError("not a valid Spider file") from e - h = (99,) + t # add 1 value : spider header index starts at 1 + 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") - self.size = int(h[12]), int(h[2]) # size in pixels (width, height) + self._size = int(h[12]), int(h[2]) # size in pixels (width, height) self.istack = int(h[24]) self.imgnumber = int(h[27]) @@ -151,10 +148,8 @@ def _open(self): self.rawmode = "F;32F" self.mode = "F" - self.tile = [ - ("raw", (0, 0) + self.size, offset, - (self.rawmode, 0, 1))] - self.__fp = self.fp # FIXME: hack + self.tile = [("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))] + self._fp = self.fp # FIXME: hack @property def n_frames(self): @@ -177,7 +172,7 @@ def seek(self, frame): if not self._seek_check(frame): return self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) - self.fp = self.__fp + self.fp = self._fp self.fp.seek(self.stkoffset) self._open() @@ -186,13 +181,14 @@ def convert2byte(self, depth=255): (minimum, maximum) = self.getextrema() m = 1 if maximum != minimum: - m = depth / (maximum-minimum) + m = depth / (maximum - minimum) b = -m * minimum return self.point(lambda i, m=m, b=b: i * m + b).convert("L") # returns a ImageTk.PhotoImage object, after rescaling to 0..255 def tkPhotoImage(self): from PIL import ImageTk + return ImageTk.PhotoImage(self.convert2byte(), palette=256) @@ -201,22 +197,23 @@ def tkPhotoImage(self): # given a list of filenames, return a list of images def loadImageSeries(filelist=None): - " create a list of Image.images for use in montage " + """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" if filelist is None or len(filelist) < 1: return imglist = [] for img in filelist: if not os.path.exists(img): - print("unable to find %s" % img) + print(f"unable to find {img}") continue try: - im = Image.open(img).convert2byte() - except: + with Image.open(img) as im: + im = im.convert2byte() + except Exception: if not isSpiderImage(img): print(img + " is not a Spider image file") continue - im.info['filename'] = img + im.info["filename"] = img imglist.append(im) return imglist @@ -224,26 +221,28 @@ def loadImageSeries(filelist=None): # -------------------------------------------------------------------- # For saving images in Spider format + def makeSpiderHeader(im): nsam, nrow = im.size lenbyt = nsam * 4 # There are labrec records in the header - labrec = 1024 / lenbyt + labrec = int(1024 / lenbyt) if 1024 % lenbyt != 0: labrec += 1 labbyt = labrec * lenbyt - hdr = [] nvalues = int(labbyt / 4) + if nvalues < 23: + return [] + + hdr = [] for i in range(nvalues): hdr.append(0.0) - if len(hdr) < 23: - return [] - # NB these are Fortran indices - hdr[1] = 1.0 # nslice (=1 for an image) - hdr[2] = float(nrow) # number of rows per slice - hdr[5] = 1.0 # iform for 2D image - hdr[12] = float(nsam) # number of pixels per line + hdr[1] = 1.0 # nslice (=1 for an image) + hdr[2] = float(nrow) # number of rows per slice + hdr[3] = float(nrow) # number of records in the image + hdr[5] = 1.0 # iform for 2D image + hdr[12] = float(nsam) # number of pixels per line hdr[13] = float(labrec) # number of records in file header hdr[22] = float(labbyt) # total number of bytes in header hdr[23] = float(lenbyt) # record length in bytes @@ -252,25 +251,22 @@ def makeSpiderHeader(im): hdr = hdr[1:] hdr.append(0.0) # pack binary data into a string - hdrstr = [] - for v in hdr: - hdrstr.append(struct.pack('f', v)) - return hdrstr + return [struct.pack("f", v) for v in hdr] def _save(im, fp, filename): if im.mode[0] != "F": - im = im.convert('F') + im = im.convert("F") hdr = makeSpiderHeader(im) if len(hdr) < 256: - raise IOError("Error creating Spider header") + raise OSError("Error creating Spider header") # write the SPIDER header fp.writelines(hdr) rawmode = "F;32NF" # 32-bit native floating point - ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0, (rawmode, 0, 1))]) + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) def _save_spider(im, fp, filename): @@ -279,15 +275,17 @@ def _save_spider(im, fp, filename): Image.register_extension(SpiderImageFile.format, ext) _save(im, fp, filename) + # -------------------------------------------------------------------- + Image.register_open(SpiderImageFile.format, SpiderImageFile) Image.register_save(SpiderImageFile.format, _save_spider) if __name__ == "__main__": - if not sys.argv[1:]: - print("Syntax: python SpiderImagePlugin.py [infile] [outfile]") + if len(sys.argv) < 2: + print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]") sys.exit() filename = sys.argv[1] @@ -295,22 +293,21 @@ def _save_spider(im, fp, filename): print("input image must be in Spider format") sys.exit() - outfile = "" - if len(sys.argv[1:]) > 1: - outfile = sys.argv[2] - - im = Image.open(filename) - print("image: " + str(im)) - print("format: " + str(im.format)) - print("size: " + str(im.size)) - print("mode: " + str(im.mode)) - print("max, min: ", end=' ') - print(im.getextrema()) - - if outfile != "": - # perform some image operation - im = im.transpose(Image.FLIP_LEFT_RIGHT) - print( - "saving a flipped version of %s as %s " % - (os.path.basename(filename), outfile)) - im.save(outfile, SpiderImageFile.format) + with Image.open(filename) as im: + print("image: " + str(im)) + print("format: " + str(im.format)) + print("size: " + str(im.size)) + print("mode: " + str(im.mode)) + print("max, min: ", end=" ") + print(im.getextrema()) + + if len(sys.argv) > 2: + outfile = sys.argv[2] + + # perform some image operation + im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + print( + f"saving a flipped version of {os.path.basename(filename)} " + f"as {outfile} " + ) + im.save(outfile, SpiderImageFile.format) diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index afee7d1a7d3..c03759a01e6 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -20,16 +20,15 @@ from . import Image, ImageFile, ImagePalette from ._binary import i32be as i32 -__version__ = "0.3" - def _accept(prefix): - return len(prefix) >= 4 and i32(prefix) == 0x59a66a95 + return len(prefix) >= 4 and i32(prefix) == 0x59A66A95 ## # Image plugin for Sun raster files. + class SunImageFile(ImageFile.ImageFile): format = "SUN" @@ -54,18 +53,18 @@ def _open(self): # HEAD s = self.fp.read(32) - if i32(s) != 0x59a66a95: + if not _accept(s): raise SyntaxError("not an SUN raster file") offset = 32 - self.size = i32(s[4:8]), i32(s[8:12]) + self._size = i32(s, 4), i32(s, 8) - depth = i32(s[12:16]) - data_length = i32(s[16:20]) # unreliable, ignore. - file_type = i32(s[20:24]) - palette_type = i32(s[24:28]) # 0: None, 1: RGB, 2: Raw/arbitrary - palette_length = i32(s[28:32]) + depth = i32(s, 12) + # data_length = i32(s, 16) # unreliable, ignore. + file_type = i32(s, 20) + palette_type = i32(s, 24) # 0: None, 1: RGB, 2: Raw/arbitrary + palette_length = i32(s, 28) if depth == 1: self.mode, rawmode = "1", "1;I" @@ -80,9 +79,9 @@ def _open(self): self.mode, rawmode = "RGB", "BGR" elif depth == 32: if file_type == 3: - self.mode, rawmode = 'RGB', 'RGBX' + self.mode, rawmode = "RGB", "RGBX" else: - self.mode, rawmode = 'RGB', 'BGRX' + self.mode, rawmode = "RGB", "BGRX" else: raise SyntaxError("Unsupported Mode/Bit Depth") @@ -97,7 +96,7 @@ def _open(self): self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length)) if self.mode == "L": self.mode = "P" - rawmode = rawmode.replace('L', 'P') + rawmode = rawmode.replace("L", "P") # 16 bit boundaries on stride stride = ((self.size[0] * depth + 15) // 16) * 2 @@ -121,15 +120,17 @@ def _open(self): # (https://www.fileformat.info/format/sunraster/egff.htm) if file_type in (0, 1, 3, 4, 5): - self.tile = [("raw", (0, 0)+self.size, offset, (rawmode, stride))] + self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride))] elif file_type == 2: - self.tile = [("sun_rle", (0, 0)+self.size, offset, rawmode)] + self.tile = [("sun_rle", (0, 0) + self.size, offset, rawmode)] else: - raise SyntaxError('Unsupported Sun Raster file type') + raise SyntaxError("Unsupported Sun Raster file type") + # # registry + Image.register_open(SunImageFile.format, SunImageFile, _accept) Image.register_extension(SunImageFile.format, ".ras") diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index 0e949ff88eb..d108362fc9f 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -14,14 +14,13 @@ # See the README file for information on usage and redistribution. # -from . import ContainerIO +import io +from . import ContainerIO -## -# A file object that provides read access to a given member of a TAR -# file. class TarIO(ContainerIO.ContainerIO): + """A file object that provides read access to a given member of a TAR file.""" def __init__(self, tarfile, file): """ @@ -30,18 +29,18 @@ def __init__(self, tarfile, file): :param tarfile: Name of TAR file. :param file: Name of member file. """ - fh = open(tarfile, "rb") + self.fh = open(tarfile, "rb") while True: - s = fh.read(512) + s = self.fh.read(512) if len(s) != 512: - raise IOError("unexpected end of tar file") + raise OSError("unexpected end of tar file") - name = s[:100].decode('utf-8') - i = name.find('\0') + name = s[:100].decode("utf-8") + i = name.find("\0") if i == 0: - raise IOError("cannot find subfile") + raise OSError("cannot find subfile") if i > 0: name = name[:i] @@ -50,7 +49,17 @@ def __init__(self, tarfile, file): if file == name: break - fh.seek((size + 511) & (~511), 1) + self.fh.seek((size + 511) & (~511), io.SEEK_CUR) # Open region - ContainerIO.ContainerIO.__init__(self, fh, fh.tell(), size) + super().__init__(self.fh, self.fh.tell(), size) + + # Context manager support + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + def close(self): + self.fh.close() diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index a9596bd5f24..cd454b755c0 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -17,11 +17,12 @@ # -from . import Image, ImageFile, ImagePalette -from ._binary import i8, i16le as i16, o8, o16le as o16 - -__version__ = "0.3" +import warnings +from . import Image, ImageFile, ImagePalette +from ._binary import i16le as i16 +from ._binary import o8 +from ._binary import o16le as o16 # # -------------------------------------------------------------------- @@ -30,9 +31,10 @@ MODES = { # map imagetype/depth to rawmode - (1, 8): "P", - (3, 1): "1", - (3, 8): "L", + (1, 8): "P", + (3, 1): "1", + (3, 8): "L", + (3, 16): "LA", (2, 16): "BGR;5", (2, 24): "BGR", (2, 32): "BGRA", @@ -42,6 +44,7 @@ ## # Image plugin for Targa files. + class TgaImageFile(ImageFile.ImageFile): format = "TGA" @@ -52,21 +55,24 @@ def _open(self): # process header s = self.fp.read(18) - idlen = i8(s[0]) + id_len = s[0] - colormaptype = i8(s[1]) - imagetype = i8(s[2]) + colormaptype = s[1] + imagetype = s[2] - depth = i8(s[16]) + depth = s[16] - flags = i8(s[17]) + flags = s[17] - self.size = i16(s[12:]), i16(s[14:]) + self._size = i16(s, 12), i16(s, 14) # validate header fields - if colormaptype not in (0, 1) or\ - self.size[0] <= 0 or self.size[1] <= 0 or\ - depth not in (1, 8, 16, 24, 32): + if ( + colormaptype not in (0, 1) + or self.size[0] <= 0 + or self.size[1] <= 0 + or depth not in (1, 8, 16, 24, 32) + ): raise SyntaxError("not a TGA file") # image mode @@ -74,6 +80,8 @@ def _open(self): self.mode = "L" if depth == 1: self.mode = "1" # ??? + elif depth == 16: + self.mode = "LA" elif imagetype in (1, 9): self.mode = "P" elif imagetype in (2, 10): @@ -85,9 +93,10 @@ def _open(self): # orientation orientation = flags & 0x30 - if orientation == 0x20: + self._flip_horizontally = orientation in [0x10, 0x30] + if orientation in [0x20, 0x30]: orientation = 1 - elif not orientation: + elif orientation in [0, 0x10]: orientation = -1 else: raise SyntaxError("unknown TGA orientation") @@ -97,42 +106,64 @@ def _open(self): if imagetype & 8: self.info["compression"] = "tga_rle" - if idlen: - self.info["id_section"] = self.fp.read(idlen) + if id_len: + self.info["id_section"] = self.fp.read(id_len) if colormaptype: # read palette - start, size, mapdepth = i16(s[3:]), i16(s[5:]), i16(s[7:]) + start, size, mapdepth = i16(s, 3), i16(s, 5), s[7] if mapdepth == 16: self.palette = ImagePalette.raw( - "BGR;16", b"\0"*2*start + self.fp.read(2*size)) + "BGR;15", b"\0" * 2 * start + self.fp.read(2 * size) + ) elif mapdepth == 24: self.palette = ImagePalette.raw( - "BGR", b"\0"*3*start + self.fp.read(3*size)) + "BGR", b"\0" * 3 * start + self.fp.read(3 * size) + ) elif mapdepth == 32: self.palette = ImagePalette.raw( - "BGRA", b"\0"*4*start + self.fp.read(4*size)) + "BGRA", b"\0" * 4 * start + self.fp.read(4 * size) + ) # setup tile descriptor try: rawmode = MODES[(imagetype & 7, depth)] if imagetype & 8: # compressed - self.tile = [("tga_rle", (0, 0)+self.size, - self.fp.tell(), (rawmode, orientation, depth))] + self.tile = [ + ( + "tga_rle", + (0, 0) + self.size, + self.fp.tell(), + (rawmode, orientation, depth), + ) + ] else: - self.tile = [("raw", (0, 0)+self.size, - self.fp.tell(), (rawmode, 0, orientation))] + self.tile = [ + ( + "raw", + (0, 0) + self.size, + self.fp.tell(), + (rawmode, 0, orientation), + ) + ] except KeyError: pass # cannot decode + def load_end(self): + if self._flip_horizontally: + self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + + # # -------------------------------------------------------------------- # Write TGA file + SAVE = { "1": ("1", 1, 0, 3), "L": ("L", 8, 0, 3), + "LA": ("LA", 16, 0, 3), "P": ("P", 8, 1, 1), "RGB": ("BGR", 24, 0, 2), "RGBA": ("BGRA", 32, 0, 2), @@ -143,50 +174,81 @@ def _save(im, fp, filename): try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] - except KeyError: - raise IOError("cannot write mode %s as TGA" % im.mode) + except KeyError as e: + raise OSError(f"cannot write mode {im.mode} as TGA") from e + + if "rle" in im.encoderinfo: + rle = im.encoderinfo["rle"] + else: + compression = im.encoderinfo.get("compression", im.info.get("compression")) + rle = compression == "tga_rle" + if rle: + imagetype += 8 + + id_section = im.encoderinfo.get("id_section", im.info.get("id_section", "")) + id_len = len(id_section) + if id_len > 255: + id_len = 255 + id_section = id_section[:255] + warnings.warn("id_section has been trimmed to 255 characters") if colormaptype: - colormapfirst, colormaplength, colormapentry = 0, 256, 24 + palette = im.im.getpalette("RGB", "BGR") + colormaplength, colormapentry = len(palette) // 3, 24 else: - colormapfirst, colormaplength, colormapentry = 0, 0, 0 + colormaplength, colormapentry = 0, 0 - if im.mode == "RGBA": + if im.mode in ("LA", "RGBA"): flags = 8 else: flags = 0 - orientation = im.info.get("orientation", -1) + orientation = im.encoderinfo.get("orientation", im.info.get("orientation", -1)) if orientation > 0: flags = flags | 0x20 - fp.write(b"\000" + - o8(colormaptype) + - o8(imagetype) + - o16(colormapfirst) + - o16(colormaplength) + - o8(colormapentry) + - o16(0) + - o16(0) + - o16(im.size[0]) + - o16(im.size[1]) + - o8(bits) + - o8(flags)) + fp.write( + o8(id_len) + + o8(colormaptype) + + o8(imagetype) + + o16(0) # colormapfirst + + o16(colormaplength) + + o8(colormapentry) + + o16(0) + + o16(0) + + o16(im.size[0]) + + o16(im.size[1]) + + o8(bits) + + o8(flags) + ) + + if id_section: + fp.write(id_section) if colormaptype: - fp.write(im.im.getpalette("RGB", "BGR")) + fp.write(palette) - ImageFile._save( - im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))]) + if rle: + ImageFile._save( + im, fp, [("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))] + ) + else: + ImageFile._save( + im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))] + ) # write targa version 2 footer fp.write(b"\000" * 8 + b"TRUEVISION-XFILE." + b"\000") + # # -------------------------------------------------------------------- # Registry + Image.register_open(TgaImageFile.format, TgaImageFile) Image.register_save(TgaImageFile.format, _save) -Image.register_extension(TgaImageFile.format, ".tga") +Image.register_extensions(TgaImageFile.format, [".tga", ".icb", ".vda", ".vst"]) + +Image.register_mime(TgaImageFile.format, "image/x-tga") diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index db3eaa50ec1..1dfd5275fa1 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -38,33 +38,30 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import division, print_function - -from . import Image, ImageFile, ImagePalette, TiffTags -from ._binary import i8, o8 - -import collections -from fractions import Fraction -from numbers import Number, Rational - import io import itertools +import logging +import math import os import struct -import sys import warnings +from collections.abc import MutableMapping +from fractions import Fraction +from numbers import Number, Rational +from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags +from ._binary import i16be as i16 +from ._binary import i32be as i32 +from ._binary import o8 from .TiffTags import TYPES - -__version__ = "1.3.5" -DEBUG = False # Needs to be merged with the new logging approach. +logger = logging.getLogger(__name__) # Set these to true to force use of libtiff for reading or writing. READ_LIBTIFF = False WRITE_LIBTIFF = False IFD_LEGACY_API = True +STRIP_SIZE = 65536 II = b"II" # little-endian (Intel style) MM = b"MM" # big-endian (Motorola style) @@ -89,21 +86,29 @@ Y_RESOLUTION = 283 PLANAR_CONFIGURATION = 284 RESOLUTION_UNIT = 296 +TRANSFERFUNCTION = 301 SOFTWARE = 305 DATE_TIME = 306 ARTIST = 315 PREDICTOR = 317 COLORMAP = 320 +TILEWIDTH = 322 +TILELENGTH = 323 TILEOFFSETS = 324 +TILEBYTECOUNTS = 325 +SUBIFD = 330 EXTRASAMPLES = 338 SAMPLEFORMAT = 339 JPEGTABLES = 347 +YCBCRSUBSAMPLING = 530 +REFERENCEBLACKWHITE = 532 COPYRIGHT = 33432 IPTC_NAA_CHUNK = 33723 # newsphoto properties PHOTOSHOP_CHUNK = 34377 # photoshop properties ICCPROFILE = 34675 EXIFIFD = 34665 XMP = 700 +JPEGQUALITY = 65537 # pseudo-tag by libtiff # https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java IMAGEJ_META_DATA_BYTE_COUNTS = 50838 @@ -125,6 +130,9 @@ 32946: "tiff_deflate", 34676: "tiff_sgilog", 34677: "tiff_sgilog24", + 34925: "lzma", + 50000: "zstd", + 50001: "webp", } COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()} @@ -140,7 +148,6 @@ (MM, 1, (1,), 1, (1,), ()): ("1", "1"), (II, 1, (1,), 2, (1,), ()): ("1", "1;R"), (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"), - (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"), (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"), (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), @@ -149,7 +156,6 @@ (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"), (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"), (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"), - (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"), (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"), (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), @@ -158,7 +164,6 @@ (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"), (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"), (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"), - (II, 0, (1,), 1, (8,), ()): ("L", "L;I"), (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"), (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"), @@ -167,14 +172,13 @@ (MM, 1, (1,), 1, (8,), ()): ("L", "L"), (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), - (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), - + (II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"), (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), + (II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"), (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"), (MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"), - (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"), (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"), (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"), @@ -182,10 +186,8 @@ (MM, 1, (2,), 1, (32,), ()): ("I", "I;32BS"), (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"), (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"), - (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), - (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), @@ -194,13 +196,24 @@ (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"), (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 - (II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"), (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"), (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"), @@ -211,7 +224,6 @@ (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"), (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"), (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"), - (II, 3, (1,), 1, (1,), ()): ("P", "P;1"), (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"), (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"), @@ -230,22 +242,30 @@ (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"), - (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), - - (II, 6, (1,), 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), - (MM, 6, (1,), 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), - + (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), + (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), + (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), + (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), + (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"), + # JPEG compressed images handled by LibTiff and auto-converted to RGBX + # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel + (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), + (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), (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 b"MM\x2A\x00", # Invalid TIFF header, assume big-endian b"II\x00\x2A", # Invalid TIFF header, assume little-endian + b"MM\x00\x2B", # BigTIFF with big-endian byte order + b"II\x2B\x00", # BigTIFF with little-endian byte order ] @@ -259,6 +279,20 @@ def _limit_rational(val, max_val): return n_d[::-1] if inv else n_d +def _limit_signed_rational(val, max_val, min_val): + frac = Fraction(val) + n_d = frac.numerator, frac.denominator + + if min(n_d) < min_val: + n_d = _limit_rational(val, abs(min_val)) + + if max(n_d) > max_val: + val = Fraction(*n_d) + n_d = _limit_rational(val, max_val) + + return n_d + + ## # Wrapper for TIFF IFDs. @@ -267,7 +301,7 @@ def _limit_rational(val, max_val): class IFDRational(Rational): - """ Implements a rational class where 0/0 is a legal value to match + """Implements a rational class where 0/0 is a legal value to match the in the wild use of exif rationals. e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used @@ -278,7 +312,7 @@ class IFDRational(Rational): """ - __slots__ = ('_numerator', '_denominator', '_val') + __slots__ = ("_numerator", "_denominator", "_val") def __init__(self, value, denominator=1): """ @@ -286,37 +320,33 @@ def __init__(self, value, denominator=1): float/rational/other number, or an IFDRational :param denominator: Optional integer denominator """ - self._denominator = denominator - self._numerator = value - self._val = float(1) - - if isinstance(value, Fraction): - self._numerator = value.numerator - self._denominator = value.denominator - self._val = value - if isinstance(value, IFDRational): - self._denominator = value.denominator self._numerator = value.numerator + self._denominator = value.denominator self._val = value._val return - if denominator == 0: - self._val = float('nan') - return + if isinstance(value, Fraction): + self._numerator = value.numerator + self._denominator = value.denominator + else: + self._numerator = value + self._denominator = denominator + if denominator == 0: + self._val = float("nan") elif denominator == 1: self._val = Fraction(value) else: self._val = Fraction(value, denominator) @property - def numerator(a): - return a._numerator + def numerator(self): + return self._numerator @property - def denominator(a): - return a._denominator + def denominator(self): + return self._denominator def limit_rational(self, max_denominator): """ @@ -326,10 +356,10 @@ def limit_rational(self, max_denominator): """ if self.denominator == 0: - return (self.numerator, self.denominator) + return self.numerator, self.denominator f = self._val.limit_denominator(max_denominator) - return (f.numerator, f.denominator) + return f.numerator, f.denominator def __repr__(self): return str(float(self._val)) @@ -338,52 +368,66 @@ def __hash__(self): return self._val.__hash__() def __eq__(self, other): - return self._val == other + val = self._val + if isinstance(other, IFDRational): + other = other._val + if isinstance(other, float): + val = float(val) + return val == other + + def __getstate__(self): + return [self._val, self._numerator, self._denominator] + + def __setstate__(self, state): + IFDRational.__init__(self, 0) + _val, _numerator, _denominator = state + self._val = _val + self._numerator = _numerator + self._denominator = _denominator def _delegate(op): def delegate(self, *args): return getattr(self._val, op)(*args) + return delegate - """ a = ['add','radd', 'sub', 'rsub','div', 'rdiv', 'mul', 'rmul', - 'truediv', 'rtruediv', 'floordiv', - 'rfloordiv','mod','rmod', 'pow','rpow', 'pos', 'neg', - 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'nonzero', + """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', + 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', + 'mod','rmod', 'pow','rpow', 'pos', 'neg', + 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool', 'ceil', 'floor', 'round'] print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a)) """ - __add__ = _delegate('__add__') - __radd__ = _delegate('__radd__') - __sub__ = _delegate('__sub__') - __rsub__ = _delegate('__rsub__') - __div__ = _delegate('__div__') - __rdiv__ = _delegate('__rdiv__') - __mul__ = _delegate('__mul__') - __rmul__ = _delegate('__rmul__') - __truediv__ = _delegate('__truediv__') - __rtruediv__ = _delegate('__rtruediv__') - __floordiv__ = _delegate('__floordiv__') - __rfloordiv__ = _delegate('__rfloordiv__') - __mod__ = _delegate('__mod__') - __rmod__ = _delegate('__rmod__') - __pow__ = _delegate('__pow__') - __rpow__ = _delegate('__rpow__') - __pos__ = _delegate('__pos__') - __neg__ = _delegate('__neg__') - __abs__ = _delegate('__abs__') - __trunc__ = _delegate('__trunc__') - __lt__ = _delegate('__lt__') - __gt__ = _delegate('__gt__') - __le__ = _delegate('__le__') - __ge__ = _delegate('__ge__') - __nonzero__ = _delegate('__nonzero__') - __ceil__ = _delegate('__ceil__') - __floor__ = _delegate('__floor__') - __round__ = _delegate('__round__') - - -class ImageFileDirectory_v2(collections.MutableMapping): + __add__ = _delegate("__add__") + __radd__ = _delegate("__radd__") + __sub__ = _delegate("__sub__") + __rsub__ = _delegate("__rsub__") + __mul__ = _delegate("__mul__") + __rmul__ = _delegate("__rmul__") + __truediv__ = _delegate("__truediv__") + __rtruediv__ = _delegate("__rtruediv__") + __floordiv__ = _delegate("__floordiv__") + __rfloordiv__ = _delegate("__rfloordiv__") + __mod__ = _delegate("__mod__") + __rmod__ = _delegate("__rmod__") + __pow__ = _delegate("__pow__") + __rpow__ = _delegate("__rpow__") + __pos__ = _delegate("__pos__") + __neg__ = _delegate("__neg__") + __abs__ = _delegate("__abs__") + __trunc__ = _delegate("__trunc__") + __lt__ = _delegate("__lt__") + __gt__ = _delegate("__gt__") + __le__ = _delegate("__le__") + __ge__ = _delegate("__ge__") + __bool__ = _delegate("__bool__") + __ceil__ = _delegate("__ceil__") + __floor__ = _delegate("__floor__") + __round__ = _delegate("__round__") + + +class ImageFileDirectory_v2(MutableMapping): """This class represents a TIFF tag directory. To speed things up, we don't decode tags unless they're asked for. @@ -391,7 +435,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): ifd = ImageFileDirectory_v2() ifd[key] = 'Some Data' - ifd.tagtype[key] = 2 + ifd.tagtype[key] = TiffTags.ASCII print(ifd[key]) 'Some Data' @@ -400,43 +444,51 @@ class ImageFileDirectory_v2(collections.MutableMapping): The tiff metadata type of each item is stored in a dictionary of tag types in - `~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types + :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types are read from a tiff file, guessed from the type added, or added manually. Data Structures: - * self.tagtype = {} + * ``self.tagtype = {}`` - * Key: numerical tiff tag number - * Value: integer corresponding to the data type from `~PIL.TiffTags.TYPES` + * Key: numerical TIFF tag number + * Value: integer corresponding to the data type from + :py:data:`.TiffTags.TYPES` - .. versionadded:: 3.0.0 - """ - """ - Documentation: - - 'internal' data structures: - * self._tags_v2 = {} Key: numerical tiff tag number - Value: decoded data, as tuple for multiple values - * self._tagdata = {} Key: numerical tiff tag number - Value: undecoded byte string from file - * self._tags_v1 = {} Key: numerical tiff tag number - Value: decoded data in the v1 format - - Tags will be found in the private attributes self._tagdata, and in - self._tags_v2 once decoded. - - Self.legacy_api is a value for internal use, and shouldn't be - changed from outside code. In cooperation with the - ImageFileDirectory_v1 class, if legacy_api is true, then decoded - tags will be populated into both _tags_v1 and _tags_v2. _Tags_v2 - will be used if this IFD is used in the TIFF save routine. Tags - should be read from tags_v1 if legacy_api == true. + .. versionadded:: 3.0.0 + + 'Internal' data structures: + + * ``self._tags_v2 = {}`` + + * Key: numerical TIFF tag number + * Value: decoded data, as tuple for multiple values + + * ``self._tagdata = {}`` + + * Key: numerical TIFF tag number + * Value: undecoded byte string from file + + * ``self._tags_v1 = {}`` + + * Key: numerical TIFF tag number + * Value: decoded data in the v1 format + + Tags will be found in the private attributes ``self._tagdata``, and in + ``self._tags_v2`` once decoded. + + ``self.legacy_api`` is a value for internal use, and shouldn't be changed + from outside code. In cooperation with + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api`` + is true, then decoded tags will be populated into both ``_tags_v1`` and + ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF + save routine. Tags should be read from ``_tags_v1`` if + ``legacy_api == true``. """ - def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None): + def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): """Initialize an ImageFileDirectory. To construct an ImageFileDirectory from a real file, pass the 8-byte @@ -447,8 +499,8 @@ def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None): endianness. :param prefix: Override the endianness of the file. """ - if ifh[:4] not in PREFIXES: - raise SyntaxError("not a TIFF file (header %r not valid)" % ifh) + if not _accept(ifh): + raise SyntaxError(f"not a TIFF file (header {repr(ifh)} not valid)") self._prefix = prefix if prefix is not None else ifh[:2] if self._prefix == MM: self._endian = ">" @@ -456,8 +508,14 @@ def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None): self._endian = "<" else: raise SyntaxError("not a TIFF IFD") + self._bigtiff = ifh[2] == 43 + self.group = group + self.tagtype = {} + """ Dictionary of tag types """ self.reset() - self.next, = self._unpack("L", ifh[4:]) + (self.next,) = ( + self._unpack("Q", ifh[8:]) if self._bigtiff else self._unpack("L", ifh[4:]) + ) self._legacy_api = False prefix = property(lambda self: self._prefix) @@ -472,7 +530,7 @@ def reset(self): self._tags_v1 = {} # will remain empty if legacy_api is false self._tags_v2 = {} # main tag storage self._tagdata = {} - self.tagtype = {} # added 2008-06-05 by Florian Hoech + self.tagtype = {} # added 2008-06-05 by Florian Hoech self._next = None self._offset = None @@ -485,8 +543,10 @@ def named(self): Returns the complete tag dictionary, with named tags where possible. """ - return dict((TiffTags.lookup(code).name, value) - for code, value in self.items()) + return { + TiffTags.lookup(code, self.group).name: value + for code, value in self.items() + } def __len__(self): return len(set(self._tagdata) | set(self._tags_v2)) @@ -499,54 +559,61 @@ def __getitem__(self, tag): self[tag] = handler(self, data, self.legacy_api) # check type val = self._tags_v2[tag] if self.legacy_api and not isinstance(val, (tuple, bytes)): - val = val, + val = (val,) return val def __contains__(self, tag): return tag in self._tags_v2 or tag in self._tagdata - if bytes is str: - def has_key(self, tag): - return tag in self - def __setitem__(self, tag, value): self._setitem(tag, value, self.legacy_api) def _setitem(self, tag, value, legacy_api): basetypes = (Number, bytes, str) - if bytes is str: - basetypes += unicode, - info = TiffTags.lookup(tag) + info = TiffTags.lookup(tag, self.group) values = [value] if isinstance(value, basetypes) else value if tag not in self.tagtype: if info.type: self.tagtype[tag] = info.type else: - self.tagtype[tag] = 7 + self.tagtype[tag] = TiffTags.UNDEFINED if all(isinstance(v, IFDRational) for v in values): - self.tagtype[tag] = 5 + self.tagtype[tag] = ( + TiffTags.RATIONAL + if all(v >= 0 for v in values) + else TiffTags.SIGNED_RATIONAL + ) elif all(isinstance(v, int) for v in values): - if all(v < 2 ** 16 for v in values): - self.tagtype[tag] = 3 + if all(0 <= v < 2**16 for v in values): + self.tagtype[tag] = TiffTags.SHORT + elif all(-(2**15) < v < 2**15 for v in values): + self.tagtype[tag] = TiffTags.SIGNED_SHORT else: - self.tagtype[tag] = 4 + self.tagtype[tag] = ( + TiffTags.LONG + if all(v >= 0 for v in values) + else TiffTags.SIGNED_LONG + ) elif all(isinstance(v, float) for v in values): - self.tagtype[tag] = 12 - else: - if bytes is str: - # Never treat data as binary by default on Python 2. - self.tagtype[tag] = 2 - else: - if all(isinstance(v, str) for v in values): - self.tagtype[tag] = 2 - - if self.tagtype[tag] == 7 and bytes is not str: - values = [value.encode("ascii", 'replace') if isinstance( - value, str) else value] - - values = tuple(info.cvt_enum(value) for value in values) + self.tagtype[tag] = TiffTags.DOUBLE + elif all(isinstance(v, str) for v in values): + self.tagtype[tag] = TiffTags.ASCII + elif all(isinstance(v, bytes) for v in values): + self.tagtype[tag] = TiffTags.BYTE + + if self.tagtype[tag] == TiffTags.UNDEFINED: + values = [ + v.encode("ascii", "replace") if isinstance(v, str) else v + for v in values + ] + elif self.tagtype[tag] == TiffTags.RATIONAL: + values = [float(v) if isinstance(v, int) else v for v in values] + + is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict) + if not is_ifd: + values = tuple(info.cvt_enum(value) for value in values) dest = self._tags_v1 if legacy_api else self._tags_v2 @@ -555,18 +622,25 @@ def _setitem(self, tag, value, legacy_api): # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed. # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple. # Don't mess with the legacy api, since it's frozen. - if ((info.length == 1) or - (info.length is None and len(values) == 1 and not legacy_api)): + if not is_ifd and ( + (info.length == 1) + or self.tagtype[tag] == TiffTags.BYTE + or (info.length is None and len(values) == 1 and not legacy_api) + ): # Don't mess with the legacy api, since it's frozen. - if legacy_api and self.tagtype[tag] in [5, 10]: # rationals - values = values, + if legacy_api and self.tagtype[tag] in [ + TiffTags.RATIONAL, + TiffTags.SIGNED_RATIONAL, + ]: # rationals + values = (values,) try: - dest[tag], = values + (dest[tag],) = values except ValueError: # We've got a builtin tag with 1 expected entry warnings.warn( - "Metadata Warning, tag %s had too many entries: %s, expected 1" % ( - tag, len(values))) + f"Metadata Warning, tag {tag} had too many entries: " + f"{len(values)}, expected 1" + ) dest[tag] = values[0] else: @@ -591,36 +665,53 @@ def _pack(self, fmt, *values): def _register_loader(idx, size): def decorator(func): from .TiffTags import TYPES + if func.__name__.startswith("load_"): TYPES[idx] = func.__name__[5:].replace("_", " ") - _load_dispatch[idx] = size, func + _load_dispatch[idx] = size, func # noqa: F821 return func + return decorator def _register_writer(idx): def decorator(func): - _write_dispatch[idx] = func + _write_dispatch[idx] = func # noqa: F821 return func + return decorator def _register_basic(idx_fmt_name): from .TiffTags import TYPES + idx, fmt, name = idx_fmt_name TYPES[idx] = name size = struct.calcsize("=" + fmt) - _load_dispatch[idx] = size, lambda self, data, legacy_api=True: ( - self._unpack("{}{}".format(len(data) // size, fmt), data)) - _write_dispatch[idx] = lambda self, *values: ( - b"".join(self._pack(fmt, value) for value in values)) - - list(map(_register_basic, - [(3, "H", "short"), - (4, "L", "long"), - (6, "b", "signed byte"), - (8, "h", "signed short"), - (9, "l", "signed long"), - (11, "f", "float"), - (12, "d", "double")])) + _load_dispatch[idx] = ( # noqa: F821 + size, + lambda self, data, legacy_api=True: ( + self._unpack(f"{len(data) // size}{fmt}", data) + ), + ) + _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 + b"".join(self._pack(fmt, value) for value in values) + ) + + list( + map( + _register_basic, + [ + (TiffTags.SHORT, "H", "short"), + (TiffTags.LONG, "L", "long"), + (TiffTags.SIGNED_BYTE, "b", "signed byte"), + (TiffTags.SIGNED_SHORT, "h", "signed short"), + (TiffTags.SIGNED_LONG, "l", "signed long"), + (TiffTags.FLOAT, "f", "float"), + (TiffTags.DOUBLE, "d", "double"), + (TiffTags.IFD, "L", "long"), + (TiffTags.LONG8, "Q", "long8"), + ], + ) + ) @_register_loader(1, 1) # Basic type, except for the legacy API. def load_byte(self, data, legacy_api=True): @@ -639,22 +730,24 @@ def load_string(self, data, legacy_api=True): @_register_writer(2) def write_string(self, value): # remerge of https://github.com/python-pillow/Pillow/pull/1416 - if sys.version_info[0] == 2: - value = value.decode('ascii', 'replace') - return b"" + value.encode('ascii', 'replace') + b"\0" + if not isinstance(value, bytes): + value = value.encode("ascii", "replace") + return value + b"\0" @_register_loader(5, 8) def load_rational(self, data, legacy_api=True): - vals = self._unpack("{}L".format(len(data) // 4), data) + vals = self._unpack(f"{len(data) // 4}L", data) - def combine(a, b): return (a, b) if legacy_api else IFDRational(a, b) - return tuple(combine(num, denom) - for num, denom in zip(vals[::2], vals[1::2])) + def combine(a, b): + return (a, b) if legacy_api else IFDRational(a, b) + + return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(5) def write_rational(self, *values): - return b"".join(self._pack("2L", *_limit_rational(frac, 2 ** 31)) - for frac in values) + return b"".join( + self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values + ) @_register_loader(7, 1) def load_undefined(self, data, legacy_api=True): @@ -666,23 +759,27 @@ def write_undefined(self, value): @_register_loader(10, 8) def load_signed_rational(self, data, legacy_api=True): - vals = self._unpack("{}l".format(len(data) // 4), data) + vals = self._unpack(f"{len(data) // 4}l", data) + + def combine(a, b): + return (a, b) if legacy_api else IFDRational(a, b) - def combine(a, b): return (a, b) if legacy_api else IFDRational(a, b) - return tuple(combine(num, denom) - for num, denom in zip(vals[::2], vals[1::2])) + return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(10) def write_signed_rational(self, *values): - return b"".join(self._pack("2L", *_limit_rational(frac, 2 ** 30)) - for frac in values) + return b"".join( + self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31))) + for frac in values + ) def _ensure_read(self, fp, size): ret = fp.read(size) if len(ret) != size: - raise IOError("Corrupt EXIF data. " + - "Expecting to read %d bytes but only got %d. " % - (size, len(ret))) + raise OSError( + "Corrupt EXIF data. " + f"Expecting to read {size} bytes but only got {len(ret)}. " + ) return ret def load(self, fp): @@ -691,28 +788,32 @@ def load(self, fp): self._offset = fp.tell() try: - for i in range(self._unpack("H", self._ensure_read(fp, 2))[0]): - tag, typ, count, data = self._unpack("HHL4s", - self._ensure_read(fp, 12)) - if DEBUG: - tagname = TiffTags.lookup(tag).name - typname = TYPES.get(typ, "unknown") - print("tag: %s (%d) - type: %s (%d)" % - (tagname, tag, typname, typ), end=" ") + tag_count = ( + self._unpack("Q", self._ensure_read(fp, 8)) + if self._bigtiff + else self._unpack("H", self._ensure_read(fp, 2)) + )[0] + for i in range(tag_count): + tag, typ, count, data = ( + self._unpack("HHQ8s", self._ensure_read(fp, 20)) + if self._bigtiff + else self._unpack("HHL4s", self._ensure_read(fp, 12)) + ) + + tagname = TiffTags.lookup(tag, self.group).name + typname = TYPES.get(typ, "unknown") + msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})" try: unit_size, handler = self._load_dispatch[typ] except KeyError: - if DEBUG: - print("- unsupported type", typ) + logger.debug(msg + f" - unsupported type {typ}") continue # ignore unsupported type size = count * unit_size - if size > 4: + if size > (8 if self._bigtiff else 4): here = fp.tell() - offset, = self._unpack("L", data) - if DEBUG: - print("Tag Location: %s - Data Location: %s" % - (here, offset), end=" ") + (offset,) = self._unpack("Q" if self._bigtiff else "L", data) + msg += f" Tag Location: {here} - Data Location: {offset}" fp.seek(offset) data = ImageFile._safe_read(fp, size) fp.seek(here) @@ -720,39 +821,41 @@ def load(self, fp): data = data[:size] if len(data) != size: - warnings.warn("Possibly corrupt EXIF data. " - "Expecting to read %d bytes but only got %d." - " Skipping tag %s" % (size, len(data), tag)) + warnings.warn( + "Possibly corrupt EXIF data. " + f"Expecting to read {size} bytes but only got {len(data)}." + f" Skipping tag {tag}" + ) + logger.debug(msg) continue if not data: + logger.debug(msg) continue self._tagdata[tag] = data self.tagtype[tag] = typ - if DEBUG: - if size > 32: - print("- value: " % size) - else: - print("- value:", self[tag]) - - self.next, = self._unpack("L", self._ensure_read(fp, 4)) - except IOError as msg: + msg += " - value: " + ( + "" % size if size > 32 else repr(data) + ) + logger.debug(msg) + + (self.next,) = ( + self._unpack("Q", self._ensure_read(fp, 8)) + if self._bigtiff + else self._unpack("L", self._ensure_read(fp, 4)) + ) + except OSError as msg: warnings.warn(str(msg)) return - def save(self, fp): - - if fp.tell() == 0: # skip TIFF header on subsequent pages - # tiff header -- PIL always starts the first IFD at offset 8 - fp.write(self._prefix + self._pack("HL", 42, 8)) - + def tobytes(self, offset=0): # FIXME What about tagdata? - fp.write(self._pack("H", len(self._tags_v2))) + result = self._pack("H", len(self._tags_v2)) entries = [] - offset = fp.tell() + len(self._tags_v2) * 12 + 4 + offset = offset + len(result) + len(self._tags_v2) * 12 + 4 stripoffsets = None # pass 1: convert tags to binary format @@ -761,55 +864,78 @@ def save(self, fp): if tag == STRIPOFFSETS: stripoffsets = len(entries) typ = self.tagtype.get(tag) - if DEBUG: - print("Tag %s, Type: %s, Value: %s" % (tag, typ, value)) - values = value if isinstance(value, tuple) else (value,) - data = self._write_dispatch[typ](self, *values) - if DEBUG: - tagname = TiffTags.lookup(tag).name - typname = TYPES.get(typ, "unknown") - print("save: %s (%d) - type: %s (%d)" % - (tagname, tag, typname, typ), end=" ") - if len(data) >= 16: - print("- value: " % len(data)) + logger.debug(f"Tag {tag}, Type: {typ}, Value: {repr(value)}") + is_ifd = typ == TiffTags.LONG and isinstance(value, dict) + if is_ifd: + if self._endian == "<": + ifh = b"II\x2A\x00\x08\x00\x00\x00" else: - print("- value:", values) + ifh = b"MM\x00\x2A\x00\x00\x00\x08" + ifd = ImageFileDirectory_v2(ifh, group=tag) + values = self._tags_v2[tag] + for ifd_tag, ifd_value in values.items(): + ifd[ifd_tag] = ifd_value + data = ifd.tobytes(offset) + else: + values = value if isinstance(value, tuple) else (value,) + data = self._write_dispatch[typ](self, *values) + + tagname = TiffTags.lookup(tag, self.group).name + typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") + msg = f"save: {tagname} ({tag}) - type: {typname} ({typ})" + msg += " - value: " + ( + "" % len(data) if len(data) >= 16 else str(values) + ) + logger.debug(msg) # count is sum of lengths for string and arbitrary data - count = len(data) if typ in [2, 7] else len(values) + if is_ifd: + count = 1 + elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]: + count = len(data) + else: + count = len(values) # figure out if data fits into the entry if len(data) <= 4: entries.append((tag, typ, count, data.ljust(4, b"\0"), b"")) else: - entries.append((tag, typ, count, self._pack("L", offset), - data)) + entries.append((tag, typ, count, self._pack("L", offset), data)) offset += (len(data) + 1) // 2 * 2 # pad to word # update strip offset data to point beyond auxiliary data if stripoffsets is not None: tag, typ, count, value, data = entries[stripoffsets] if data: - raise NotImplementedError( - "multistrip support not yet implemented") + raise NotImplementedError("multistrip support not yet implemented") value = self._pack("L", self._unpack("L", value)[0] + offset) entries[stripoffsets] = tag, typ, count, value, data # pass 2: write entries to file for tag, typ, count, value, data in entries: - if DEBUG > 1: - print(tag, typ, count, repr(value), repr(data)) - fp.write(self._pack("HHL4s", tag, typ, count, value)) + logger.debug(f"{tag} {typ} {count} {repr(value)} {repr(data)}") + result += self._pack("HHL4s", tag, typ, count, value) # -- overwrite here for multi-page -- - fp.write(b"\0\0\0\0") # end of entries + result += b"\0\0\0\0" # end of entries # pass 3: write auxiliary data to file for tag, typ, count, value, data in entries: - fp.write(data) + result += data if len(data) & 1: - fp.write(b"\0") + result += b"\0" - return offset + return result + + def save(self, fp): + + if fp.tell() == 0: # skip TIFF header on subsequent pages + # tiff header -- PIL always starts the first IFD at offset 8 + fp.write(self._prefix + self._pack("HL", 42, 8)) + + offset = fp.tell() + result = self.tobytes(offset) + fp.write(result) + return offset + len(result) ImageFileDirectory_v2._load_dispatch = _load_dispatch @@ -829,27 +955,32 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): ifd = ImageFileDirectory_v1() ifd[key] = 'Some Data' - ifd.tagtype[key] = 2 + ifd.tagtype[key] = TiffTags.ASCII print(ifd[key]) ('Some Data',) Also contains a dictionary of tag types as read from the tiff image file, - `~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`. + :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`. Values are returned as a tuple. .. deprecated:: 3.0.0 """ + def __init__(self, *args, **kwargs): - ImageFileDirectory_v2.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) self._legacy_api = True tags = property(lambda self: self._tags_v1) tagdata = property(lambda self: self._tagdata) + # defined in ImageFileDirectory_v2 + tagtype: dict + """Dictionary of tag types""" + @classmethod def from_v2(cls, original): - """ Returns an + """Returns an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` instance with the same data as is contained in the original :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` @@ -866,7 +997,7 @@ def from_v2(cls, original): return ifd def to_v2(self): - """ Returns an + """Returns an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` instance with the same data as is contained in the original :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` @@ -904,7 +1035,7 @@ def __getitem__(self, tag): self._setitem(tag, handler(self, data, legacy), legacy) val = self._tags_v1[tag] if not isinstance(val, (tuple, bytes)): - val = val, + val = (val,) return val @@ -915,36 +1046,45 @@ def __getitem__(self, tag): ## # Image plugin for TIFF files. + class TiffImageFile(ImageFile.ImageFile): format = "TIFF" format_description = "Adobe TIFF" _close_exclusive_fp_after_loading = False + def __init__(self, fp=None, filename=None): + self.tag_v2 = None + """ Image file directory (tag dictionary) """ + + self.tag = None + """ Legacy tag entries """ + + super().__init__(fp, filename) + def _open(self): - "Open the first image in a TIFF file" + """Open the first image in a TIFF file""" # Header ifh = self.fp.read(8) + if ifh[2] == 43: + ifh += self.fp.read(8) - # image file directory (tag dictionary) self.tag_v2 = ImageFileDirectory_v2(ifh) - # legacy tag/ifd entries will be filled in later - self.tag = self.ifd = None + # legacy IFD entries will be filled in later + self.ifd = None # setup frame pointers self.__first = self.__next = self.tag_v2.next self.__frame = -1 - self.__fp = self.fp + self._fp = self.fp self._frame_pos = [] self._n_frames = None - self._is_animated = None - if DEBUG: - print("*** TiffImageFile._open ***") - print("- __first:", self.__first) - print("- ifh: ", ifh) + logger.debug("*** TiffImageFile._open ***") + logger.debug(f"- __first: {self.__first}") + logger.debug(f"- ifh: {repr(ifh)}") # Use repr to avoid str(bytes) # and load the first frame self._seek(0) @@ -953,33 +1093,14 @@ def _open(self): def n_frames(self): if self._n_frames is None: current = self.tell() - try: - while True: - self._seek(self.tell() + 1) - except EOFError: - self._n_frames = self.tell() + 1 + self._seek(len(self._frame_pos)) + while self._n_frames is None: + self._seek(self.tell() + 1) self.seek(current) return self._n_frames - @property - def is_animated(self): - if self._is_animated is None: - if self._n_frames is not None: - self._is_animated = self._n_frames != 1 - else: - current = self.tell() - - try: - self.seek(1) - self._is_animated = True - except EOFError: - self._is_animated = False - - self.seek(current) - return self._is_animated - def seek(self, frame): - "Select a given frame as current image" + """Select a given frame as current image""" if not self._seek_check(frame): return self._seek(frame) @@ -990,81 +1111,160 @@ def seek(self, frame): self.im = Image.core.new(self.mode, self.size) def _seek(self, frame): - self.fp = self.__fp + self.fp = self._fp + + # reset buffered io handle in case fp + # was passed to libtiff, invalidating the buffer + self.fp.tell() + while len(self._frame_pos) <= frame: if not self.__next: raise EOFError("no more images in TIFF file") - if DEBUG: - print("Seeking to frame %s, on frame %s, " - "__next %s, location: %s" % - (frame, self.__frame, self.__next, self.fp.tell())) - # reset python3 buffered io handle in case fp - # was passed to libtiff, invalidating the buffer - self.fp.tell() + logger.debug( + f"Seeking to frame {frame}, on frame {self.__frame}, " + f"__next {self.__next}, location: {self.fp.tell()}" + ) self.fp.seek(self.__next) self._frame_pos.append(self.__next) - if DEBUG: - print("Loading tags, location: %s" % self.fp.tell()) + logger.debug("Loading tags, location: %s" % self.fp.tell()) self.tag_v2.load(self.fp) - self.__next = self.tag_v2.next + if self.tag_v2.next in self._frame_pos: + # This IFD has already been processed + # Declare this to be the end of the image + self.__next = 0 + else: + self.__next = self.tag_v2.next + if self.__next == 0: + self._n_frames = frame + 1 + if len(self._frame_pos) == 1: + self.is_animated = self.__next != 0 self.__frame += 1 self.fp.seek(self._frame_pos[frame]) self.tag_v2.load(self.fp) - self.__next = self.tag_v2.next + self._reload_exif() # fill the legacy tag/ifd entries self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) self.__frame = frame self._setup() def tell(self): - "Return the current frame number" + """Return the current frame number""" return self.__frame - def _decoder(self, rawmode, layer, tile=None): - "Setup decoder contexts" + 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. + Requires defusedxml to be installed. + + :returns: XMP tags in a dictionary. + """ + return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {} - args = None - if rawmode == "RGB" and self._planar_configuration == 2: - rawmode = rawmode[layer] - compression = self._compression - if compression == "raw": - args = (rawmode, 0, 1) - elif compression == "packbits": - args = rawmode + def get_photoshop_blocks(self): + """ + Returns a dictionary of Photoshop "Image Resource Blocks". + The keys are the image resource ID. For more information, see + https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727 - return args + :returns: Photoshop "Image Resource Blocks" in a dictionary. + """ + blocks = {} + val = self.tag_v2.get(0x8649) + if val: + while val[:4] == b"8BIM": + id = i16(val[4:6]) + n = math.ceil((val[6] + 1) / 2) * 2 + size = i32(val[6 + n : 10 + n]) + data = val[10 + n : 10 + n + size] + blocks[id] = {"data": data} + + val = val[math.ceil((10 + n + size) / 2) * 2 :] + return blocks def load(self): - if self.use_load_libtiff: + if self.tile and self.use_load_libtiff: return self._load_libtiff() - return super(TiffImageFile, self).load() + return super().load() def load_end(self): + if self._tile_orientation: + method = { + 2: Image.Transpose.FLIP_LEFT_RIGHT, + 3: Image.Transpose.ROTATE_180, + 4: Image.Transpose.FLIP_TOP_BOTTOM, + 5: Image.Transpose.TRANSPOSE, + 6: Image.Transpose.ROTATE_270, + 7: Image.Transpose.TRANSVERSE, + 8: Image.Transpose.ROTATE_90, + }.get(self._tile_orientation) + if method is not None: + self.im = self.im.transpose(method) + self._size = self.im.size + # allow closing if we're on the first frame, there's no next # This is the ImageFile.load path only, libtiff specific below. - if self.__frame == 0 and not self.__next: + if not self.is_animated: self._close_exclusive_fp_after_loading = True - def _load_libtiff(self): - """ Overload method triggered when we detect a compressed tiff - Calls out to libtiff """ + # reset buffered io handle in case fp + # was passed to libtiff, invalidating the buffer + self.fp.tell() + + # load IFD data from fp before it is closed + exif = self.getexif() + for key in TiffTags.TAGS_V2_GROUPS.keys(): + if key not in exif: + continue + exif.get_ifd(key) - pixel = Image.Image.load(self) + def _load_libtiff(self): + """Overload method triggered when we detect a compressed tiff + Calls out to libtiff""" - if self.tile is None: - raise IOError("cannot load this image") - if not self.tile: - return pixel + Image.Image.load(self) self.load_prepare() if not len(self.tile) == 1: - raise IOError("Not exactly one tile") + raise OSError("Not exactly one tile") # (self._compression, (extents tuple), # 0, (rawmode, self._compression, fp)) extents = self.tile[0][1] - args = list(self.tile[0][3]) + [self.tag_v2.offset] + args = list(self.tile[0][3]) # To be nice on memory footprint, if there's a # file descriptor, use that instead of reading @@ -1073,25 +1273,27 @@ def _load_libtiff(self): try: fp = hasattr(self.fp, "fileno") and os.dup(self.fp.fileno()) # flush the file descriptor, prevents error on pypy 2.4+ - # should also eliminate the need for fp.tell for py3 + # should also eliminate the need for fp.tell # in _seek if hasattr(self.fp, "flush"): self.fp.flush() - except IOError: - # io.BytesIO have a fileno, but returns an IOError if + except OSError: + # io.BytesIO have a fileno, but returns an OSError if # it doesn't use a file descriptor. fp = False if fp: args[2] = fp - decoder = Image._getdecoder(self.mode, 'libtiff', tuple(args), - self.decoderconfig) + decoder = Image._getdecoder( + self.mode, "libtiff", tuple(args), self.decoderconfig + ) try: decoder.setimage(self.im, extents) - except ValueError: - raise IOError("Couldn't set the image") + except ValueError as e: + raise OSError("Couldn't set the image") from e + close_self_fp = self._exclusive_fp and not self.is_animated if hasattr(self.fp, "getvalue"): # We've got a stringio like thing passed in. Yay for all in memory. # The decoder needs the entire file in one shot, so there's not @@ -1100,43 +1302,50 @@ def _load_libtiff(self): # underlying string for stringio. # # Rearranging for supporting byteio items, since they have a fileno - # that returns an IOError if there's no underlying fp. Easier to + # that returns an OSError if there's no underlying fp. Easier to # deal with here by reordering. - if DEBUG: - print("have getvalue. just sending in a string from getvalue") + logger.debug("have getvalue. just sending in a string from getvalue") n, err = decoder.decode(self.fp.getvalue()) - elif hasattr(self.fp, "fileno"): + elif fp: # we've got a actual file on disk, pass in the fp. - if DEBUG: - print("have fileno, calling fileno version of the decoder.") - self.fp.seek(0) + logger.debug("have fileno, calling fileno version of the decoder.") + if not close_self_fp: + self.fp.seek(0) # 4 bytes, otherwise the trace might error out n, err = decoder.decode(b"fpfp") else: # we have something else. - if DEBUG: - print("don't have fileno or getvalue. just reading") + logger.debug("don't have fileno or getvalue. just reading") + self.fp.seek(0) # UNDONE -- so much for that buffer size thing. n, err = decoder.decode(self.fp.read()) + if fp: + try: + os.close(fp) + except OSError: + pass + self.tile = [] self.readonly = 0 + + self.load_end() + # libtiff closed the fp in a, we need to close self.fp, if possible - if self._exclusive_fp: - if self.__frame == 0 and not self.__next: - self.fp.close() - self.fp = None # might be shared + if close_self_fp: + self.fp.close() + self.fp = None # might be shared if err < 0: - raise IOError(err) + raise OSError(err) return Image.Image.load(self) def _setup(self): - "Setup this image object based on current tags" + """Setup this image object based on current tags""" if 0xBC01 in self.tag_v2: - raise IOError("Windows Media Photo files not yet supported") + raise OSError("Windows Media Photo files not yet supported") # extract relevant tags self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)] @@ -1146,32 +1355,34 @@ def _setup(self): # the specification photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0) + # old style jpeg compression images most certainly are YCbCr + if self._compression == "tiff_jpeg": + photo = 6 + fillorder = self.tag_v2.get(FILLORDER, 1) - if DEBUG: - print("*** Summary ***") - print("- compression:", self._compression) - print("- photometric_interpretation:", photo) - print("- planar_configuration:", self._planar_configuration) - print("- fill_order:", fillorder) + logger.debug("*** Summary ***") + logger.debug(f"- compression: {self._compression}") + logger.debug(f"- photometric_interpretation: {photo}") + logger.debug(f"- planar_configuration: {self._planar_configuration}") + logger.debug(f"- fill_order: {fillorder}") + logger.debug(f"- YCbCr subsampling: {self.tag.get(YCBCRSUBSAMPLING)}") # size - xsize = self.tag_v2.get(IMAGEWIDTH) - ysize = self.tag_v2.get(IMAGELENGTH) - self.size = xsize, ysize + xsize = int(self.tag_v2.get(IMAGEWIDTH)) + ysize = int(self.tag_v2.get(IMAGELENGTH)) + self._size = xsize, ysize - if DEBUG: - print("- size:", self.size) + logger.debug(f"- size: {self.size}") - sampleFormat = self.tag_v2.get(SAMPLEFORMAT, (1,)) - if (len(sampleFormat) > 1 - and max(sampleFormat) == min(sampleFormat) == 1): + sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,)) + if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1: # SAMPLEFORMAT is properly per band, so an RGB image will # be (1,1,1). But, we don't support per band pixel types, # and anything more than one band is a uint8. So, just # take the first element. Revisit this if adding support # for more exotic images. - sampleFormat = (1,) + sample_format = (1,) bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,)) extra_tuple = self.tag_v2.get(EXTRASAMPLES, ()) @@ -1182,26 +1393,49 @@ def _setup(self): else: bps_count = 1 bps_count += len(extra_tuple) - # Some files have only one value in bps_tuple, - # while should have more. Fix it - if bps_count > len(bps_tuple) and len(bps_tuple) == 1: - bps_tuple = bps_tuple * bps_count + bps_actual_count = len(bps_tuple) + samples_per_pixel = self.tag_v2.get( + 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 + ) + raise SyntaxError("Invalid value for samples per pixel") + + if samples_per_pixel < bps_actual_count: + # If a file has more values in bps_tuple than expected, + # remove the excess. + bps_tuple = bps_tuple[:samples_per_pixel] + elif samples_per_pixel > bps_actual_count and bps_actual_count == 1: + # If a file has only one value in bps_tuple, when it should have more, + # presume it is the same number of bits for all of the samples. + bps_tuple = bps_tuple * samples_per_pixel + + if len(bps_tuple) != samples_per_pixel: + raise SyntaxError("unknown data organization") # mode: check photometric interpretation and bits per pixel - key = (self.tag_v2.prefix, photo, sampleFormat, fillorder, - bps_tuple, extra_tuple) - if DEBUG: - print("format key:", key) + key = ( + self.tag_v2.prefix, + photo, + sample_format, + fillorder, + bps_tuple, + extra_tuple, + ) + logger.debug(f"format key: {key}") try: self.mode, rawmode = OPEN_INFO[key] - except KeyError: - if DEBUG: - print("- unsupported format") - raise SyntaxError("unknown pixel mode") + except KeyError as e: + logger.debug("- unsupported format") + raise SyntaxError("unknown pixel mode") from e - if DEBUG: - print("- raw mode:", rawmode) - print("- pil mode:", self.mode) + logger.debug(f"- raw mode: {rawmode}") + logger.debug(f"- pil mode: {self.mode}") self.info["compression"] = self._compression @@ -1211,11 +1445,11 @@ def _setup(self): if xres and yres: resunit = self.tag_v2.get(RESOLUTION_UNIT) if resunit == 2: # dots per inch - self.info["dpi"] = xres, yres + self.info["dpi"] = (xres, yres) elif resunit == 3: # dots per centimeter. convert to dpi - self.info["dpi"] = xres * 2.54, yres * 2.54 + self.info["dpi"] = (xres * 2.54, yres * 2.54) elif resunit is None: # used to default to 1, but now 2) - self.info["dpi"] = xres, yres + self.info["dpi"] = (xres, yres) # For backward compatibility, # we also preserve the old behavior self.info["resolution"] = xres, yres @@ -1223,115 +1457,111 @@ def _setup(self): self.info["resolution"] = xres, yres # build tile descriptors - x = y = l = 0 + x = y = layer = 0 self.tile = [] - self.use_load_libtiff = False - if STRIPOFFSETS in self.tag_v2: + self.use_load_libtiff = READ_LIBTIFF or self._compression != "raw" + if self.use_load_libtiff: + # Decoder expects entire file as one tile. + # There's a buffer size limit in load (64k) + # so large g4 images will fail if we use that + # function. + # + # Setup the one tile for the whole image, then + # use the _load_libtiff function. + + # libtiff handles the fillmode for us, so 1;IR should + # actually be 1;I. Including the R double reverses the + # bits, so stripes of the image are reversed. See + # https://github.com/python-pillow/Pillow/issues/279 + if fillorder == 2: + # Replace fillorder with fillorder=1 + key = key[:3] + (1,) + key[4:] + logger.debug(f"format key: {key}") + # this should always work, since all the + # fillorder==2 modes have a corresponding + # fillorder=1 mode + self.mode, rawmode = OPEN_INFO[key] + # libtiff always returns the bytes in native order. + # we're expecting image byte order. So, if the rawmode + # contains I;16, we need to convert from native to image + # byte order. + if rawmode == "I;16": + rawmode = "I;16N" + if ";16B" in rawmode: + rawmode = rawmode.replace(";16B", ";16N") + if ";16L" in rawmode: + rawmode = rawmode.replace(";16L", ";16N") + + # YCbCr images with new jpeg compression with pixels in one plane + # unpacked straight into RGB values + if ( + photo == 6 + and self._compression == "jpeg" + and self._planar_configuration == 1 + ): + rawmode = "RGB" + + # Offset in the tile tuple is 0, we go from 0,0 to + # w,h, and we only do this once -- eds + a = (rawmode, self._compression, False, self.tag_v2.offset) + self.tile.append(("libtiff", (0, 0, xsize, ysize), 0, a)) + + elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2: # striped image - offsets = self.tag_v2[STRIPOFFSETS] - h = self.tag_v2.get(ROWSPERSTRIP, ysize) - w = self.size[0] - if READ_LIBTIFF or self._compression != 'raw': - # if DEBUG: - # print("Activating g4 compression for whole file") - - # Decoder expects entire file as one tile. - # There's a buffer size limit in load (64k) - # so large g4 images will fail if we use that - # function. - # - # Setup the one tile for the whole image, then - # use the _load_libtiff function. - - self.use_load_libtiff = True - - # libtiff handles the fillmode for us, so 1;IR should - # actually be 1;I. Including the R double reverses the - # bits, so stripes of the image are reversed. See - # https://github.com/python-pillow/Pillow/issues/279 - if fillorder == 2: - key = ( - self.tag_v2.prefix, photo, sampleFormat, 1, - self.tag_v2.get(BITSPERSAMPLE, (1,)), - self.tag_v2.get(EXTRASAMPLES, ()) - ) - if DEBUG: - print("format key:", key) - # this should always work, since all the - # fillorder==2 modes have a corresponding - # fillorder=1 mode - self.mode, rawmode = OPEN_INFO[key] - # libtiff always returns the bytes in native order. - # we're expecting image byte order. So, if the rawmode - # contains I;16, we need to convert from native to image - # byte order. - if rawmode == 'I;16': - rawmode = 'I;16N' - if ';16B' in rawmode: - rawmode = rawmode.replace(';16B', ';16N') - if ';16L' in rawmode: - rawmode = rawmode.replace(';16L', ';16N') - - - # Offset in the tile tuple is 0, we go from 0,0 to - # w,h, and we only do this once -- eds - a = (rawmode, self._compression, False) - self.tile.append( - (self._compression, - (0, 0, w, ysize), - 0, a)) - a = None - + if STRIPOFFSETS in self.tag_v2: + offsets = self.tag_v2[STRIPOFFSETS] + h = self.tag_v2.get(ROWSPERSTRIP, ysize) + w = self.size[0] else: - for i, offset in enumerate(offsets): - a = self._decoder(rawmode, l, i) - self.tile.append( - (self._compression, - (0, min(y, ysize), w, min(y+h, ysize)), - offset, a)) - if DEBUG: - print("tiles: ", self.tile) - y = y + h - if y >= self.size[1]: - x = y = 0 - l += 1 - a = None - elif TILEOFFSETS in self.tag_v2: - # tiled image - w = self.tag_v2.get(322) - h = self.tag_v2.get(323) - a = None - for o in self.tag_v2[TILEOFFSETS]: - if not a: - a = self._decoder(rawmode, l) - # FIXME: this doesn't work if the image size - # is not a multiple of the tile size... + # tiled image + offsets = self.tag_v2[TILEOFFSETS] + w = self.tag_v2.get(TILEWIDTH) + h = self.tag_v2.get(TILELENGTH) + + for offset in offsets: + if x + w > xsize: + stride = w * sum(bps_tuple) / 8 # bytes per line + else: + stride = 0 + + tile_rawmode = rawmode + if self._planar_configuration == 2: + # each band on it's own layer + tile_rawmode = rawmode[layer] + # adjust stride width accordingly + stride /= bps_count + + a = (tile_rawmode, int(stride), 1) self.tile.append( - (self._compression, - (x, y, x+w, y+h), - o, a)) + ( + self._compression, + (x, y, min(x + w, xsize), min(y + h, ysize)), + offset, + a, + ) + ) x = x + w if x >= self.size[0]: x, y = 0, y + h if y >= self.size[1]: x = y = 0 - l += 1 - a = None + layer += 1 else: - if DEBUG: - print("- unsupported data organization") + logger.debug("- unsupported data organization") raise SyntaxError("unknown data organization") # Fix up info. if ICCPROFILE in self.tag_v2: - self.info['icc_profile'] = self.tag_v2[ICCPROFILE] + self.info["icc_profile"] = self.tag_v2[ICCPROFILE] # fixup palette descriptor - if self.mode == "P": + if self.mode in ["P", "PA"]: palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) + self._tile_orientation = self.tag_v2.get(0x0112) + # # -------------------------------------------------------------------- @@ -1358,7 +1588,6 @@ def _setup(self): "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None), "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None), "LAB": ("LAB", II, 8, 1, (8, 8, 8), None), - "I;32BS": ("I;32BS", MM, 1, 2, (32,), None), "I;16B": ("I;16B", MM, 1, 1, (16,), None), "I;16BS": ("I;16BS", MM, 1, 2, (16,), None), @@ -1370,64 +1599,98 @@ def _save(im, fp, filename): try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] - except KeyError: - raise IOError("cannot write mode %s as TIFF" % im.mode) + except KeyError as e: + raise OSError(f"cannot write mode {im.mode} as TIFF") from e ifd = ImageFileDirectory_v2(prefix=prefix) - compression = im.encoderinfo.get('compression', - im.info.get('compression', 'raw')) - - libtiff = WRITE_LIBTIFF or compression != 'raw' + encoderinfo = im.encoderinfo + encoderconfig = im.encoderconfig + try: + compression = encoderinfo["compression"] + except KeyError: + compression = im.info.get("compression") + if isinstance(compression, int): + # compression value may be from BMP. Ignore it + compression = None + if compression is None: + compression = "raw" + elif compression == "tiff_jpeg": + # OJPEG is obsolete, so use new-style JPEG compression instead + compression = "jpeg" + elif compression == "tiff_deflate": + compression = "tiff_adobe_deflate" + + libtiff = WRITE_LIBTIFF or compression != "raw" # required for color libtiff images - ifd[PLANAR_CONFIGURATION] = getattr(im, '_planar_configuration', 1) + ifd[PLANAR_CONFIGURATION] = 1 ifd[IMAGEWIDTH] = im.size[0] ifd[IMAGELENGTH] = im.size[1] # write any arbitrary tags passed in as an ImageFileDirectory - info = im.encoderinfo.get("tiffinfo", {}) - if DEBUG: - print("Tiffinfo Keys: %s" % list(info)) + if "tiffinfo" in encoderinfo: + info = encoderinfo["tiffinfo"] + elif "exif" in encoderinfo: + info = encoderinfo["exif"] + if isinstance(info, bytes): + exif = Image.Exif() + exif.load(info) + info = exif + else: + info = {} + logger.debug("Tiffinfo Keys: %s" % list(info)) if isinstance(info, ImageFileDirectory_v1): info = info.to_v2() for key in info: - ifd[key] = info.get(key) + if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS.keys(): + ifd[key] = info.get_ifd(key) + else: + ifd[key] = info.get(key) try: ifd.tagtype[key] = info.tagtype[key] - except: - pass # might not be an IFD, Might not have populated type + except Exception: + pass # might not be an IFD. Might not have populated type # additions written by Greg Couch, gregc@cgl.ucsf.edu # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com - if hasattr(im, 'tag_v2'): + if hasattr(im, "tag_v2"): # preserve tags from original TIFF image file - for key in (RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION, - IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, XMP): + for key in ( + RESOLUTION_UNIT, + X_RESOLUTION, + Y_RESOLUTION, + IPTC_NAA_CHUNK, + PHOTOSHOP_CHUNK, + XMP, + ): if key in im.tag_v2: ifd[key] = im.tag_v2[key] ifd.tagtype[key] = im.tag_v2.tagtype[key] # preserve ICC profile (should also work when saving other formats # which support profiles as TIFF) -- 2008-06-06 Florian Hoech - if "icc_profile" in im.info: - ifd[ICCPROFILE] = im.info["icc_profile"] - - for key, name in [(IMAGEDESCRIPTION, "description"), - (X_RESOLUTION, "resolution"), - (Y_RESOLUTION, "resolution"), - (X_RESOLUTION, "x_resolution"), - (Y_RESOLUTION, "y_resolution"), - (RESOLUTION_UNIT, "resolution_unit"), - (SOFTWARE, "software"), - (DATE_TIME, "date_time"), - (ARTIST, "artist"), - (COPYRIGHT, "copyright")]: - if name in im.encoderinfo: - ifd[key] = im.encoderinfo[name] - - dpi = im.encoderinfo.get("dpi") + icc = encoderinfo.get("icc_profile", im.info.get("icc_profile")) + if icc: + ifd[ICCPROFILE] = icc + + for key, name in [ + (IMAGEDESCRIPTION, "description"), + (X_RESOLUTION, "resolution"), + (Y_RESOLUTION, "resolution"), + (X_RESOLUTION, "x_resolution"), + (Y_RESOLUTION, "y_resolution"), + (RESOLUTION_UNIT, "resolution_unit"), + (SOFTWARE, "software"), + (DATE_TIME, "date_time"), + (ARTIST, "artist"), + (COPYRIGHT, "copyright"), + ]: + if name in encoderinfo: + ifd[key] = encoderinfo[name] + + dpi = encoderinfo.get("dpi") if dpi: ifd[RESOLUTION_UNIT] = 2 ifd[X_RESOLUTION] = dpi[0] @@ -1442,23 +1705,75 @@ def _save(im, fp, filename): if format != 1: ifd[SAMPLEFORMAT] = format - ifd[PHOTOMETRIC_INTERPRETATION] = photo + if PHOTOMETRIC_INTERPRETATION not in ifd: + ifd[PHOTOMETRIC_INTERPRETATION] = photo + elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0: + if im.mode == "1": + inverted_im = im.copy() + px = inverted_im.load() + for y in range(inverted_im.height): + for x in range(inverted_im.width): + px[x, y] = 0 if px[x, y] == 255 else 255 + im = inverted_im + else: + im = ImageOps.invert(im) - if im.mode == "P": + if im.mode in ["P", "PA"]: lut = im.im.getpalette("RGB", "RGB;L") - ifd[COLORMAP] = tuple(i8(v) * 256 for v in lut) + colormap = [] + colors = len(lut) // 3 + for i in range(3): + colormap += [v * 256 for v in lut[colors * i : colors * (i + 1)]] + colormap += [0] * (256 - colors) + ifd[COLORMAP] = colormap # data orientation - stride = len(bits) * ((im.size[0]*bits[0]+7)//8) - ifd[ROWSPERSTRIP] = im.size[1] - ifd[STRIPBYTECOUNTS] = stride * im.size[1] - ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer + stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8) + # aim for given strip size (64 KB by default) when using libtiff writer + if libtiff: + im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) + rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1]) + # JPEG encoder expects multiple of 8 rows + if compression == "jpeg": + rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1]) + else: + rows_per_strip = im.size[1] + if rows_per_strip == 0: + rows_per_strip = 1 + strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip + strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip + ifd[ROWSPERSTRIP] = rows_per_strip + if strip_byte_counts >= 2**16: + ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG + ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( + stride * im.size[1] - strip_byte_counts * (strips_per_image - 1), + ) + ifd[STRIPOFFSETS] = tuple( + range(0, strip_byte_counts * strips_per_image, strip_byte_counts) + ) # this is adjusted by IFD writer # no compression by default: ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) + if im.mode == "YCbCr": + for tag, value in { + YCBCRSUBSAMPLING: (1, 1), + REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255), + }.items(): + ifd.setdefault(tag, value) + + blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS] if libtiff: - if DEBUG: - print("Saving using libtiff encoder") - print("Items: %s" % sorted(ifd.items())) + if "quality" in encoderinfo: + quality = encoderinfo["quality"] + if not isinstance(quality, int) or quality < 0 or quality > 100: + raise ValueError("Invalid quality setting") + if compression != "jpeg": + raise ValueError( + "quality setting only supported for 'jpeg' compression" + ) + ifd[JPEGQUALITY] = quality + + logger.debug("Saving using libtiff encoder") + logger.debug("Items: %s" % sorted(ifd.items())) _fp = 0 if hasattr(fp, "fileno"): try: @@ -1467,69 +1782,103 @@ def _save(im, fp, filename): except io.UnsupportedOperation: pass + # optional types for non core tags + types = {} # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library # based on the data in the strip. - blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS] - atts = {} + # The other tags expect arrays with a certain length (fixed or depending on + # BITSPERSAMPLE, etc), passing arrays with a different length will result in + # segfaults. Block these tags until we add extra validation. + # SUBIFD may also cause a segfault. + blocklist += [ + REFERENCEBLACKWHITE, + STRIPBYTECOUNTS, + STRIPOFFSETS, + TRANSFERFUNCTION, + SUBIFD, + ] + # bits per sample is a single short in the tiff directory, not a list. - atts[BITSPERSAMPLE] = bits[0] + atts = {BITSPERSAMPLE: bits[0]} # Merge the ones that we have with (optional) more bits from # the original file, e.g x,y resolution so that we can # save(load('')) == original file. legacy_ifd = {} - if hasattr(im, 'tag'): + if hasattr(im, "tag"): legacy_ifd = im.tag.to_v2() - for tag, value in itertools.chain(ifd.items(), - getattr(im, 'tag_v2', {}).items(), - legacy_ifd.items()): + + # SAMPLEFORMAT is determined by the image format and should not be copied + # from legacy_ifd. + supplied_tags = {**getattr(im, "tag_v2", {}), **legacy_ifd} + if SAMPLEFORMAT in supplied_tags: + del supplied_tags[SAMPLEFORMAT] + + for tag, value in itertools.chain(ifd.items(), supplied_tags.items()): # Libtiff can only process certain core items without adding - # them to the custom dictionary. It will segfault if it attempts - # to add a custom tag without the dictionary entry - # - # UNDONE -- add code for the custom dictionary + # them to the custom dictionary. + # Custom items are supported for int, float, unicode, string and byte + # values. Other types and tuples require a tagtype. if tag not in TiffTags.LIBTIFF_CORE: - continue + if not Image.core.libtiff_support_custom_tags: + continue + + if tag in ifd.tagtype: + types[tag] = ifd.tagtype[tag] + elif not (isinstance(value, (int, float, str, bytes))): + continue + else: + type = TiffTags.lookup(tag).type + if type: + types[tag] = type if tag not in atts and tag not in blocklist: - if isinstance(value, unicode if bytes is str else str): - atts[tag] = value.encode('ascii', 'replace') + b"\0" + if isinstance(value, str): + atts[tag] = value.encode("ascii", "replace") + b"\0" elif isinstance(value, IFDRational): atts[tag] = float(value) else: atts[tag] = value - if DEBUG: - print("Converted items: %s" % sorted(atts.items())) + if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1: + atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0] + + logger.debug("Converted items: %s" % sorted(atts.items())) # libtiff always expects the bytes in native order. # we're storing image byte order. So, if the rawmode # contains I;16, we need to convert from native to image # byte order. - if im.mode in ('I;16B', 'I;16'): - rawmode = 'I;16N' - - a = (rawmode, compression, _fp, filename, atts) - # print(im.mode, compression, a, im.encoderconfig) - e = Image._getencoder(im.mode, 'libtiff', a, im.encoderconfig) - e.setimage(im.im, (0, 0)+im.size) + if im.mode in ("I;16B", "I;16"): + rawmode = "I;16N" + + # Pass tags as sorted list so that the tags are set in a fixed order. + # This is required by libtiff for some tags. For example, the JPEGQUALITY + # pseudo tag requires that the COMPRESS tag was already set. + tags = list(atts.items()) + tags.sort() + a = (rawmode, compression, _fp, filename, tags, types) + e = Image._getencoder(im.mode, "libtiff", a, encoderconfig) + e.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - l, s, d = e.encode(16*1024) + l, s, d = e.encode(16 * 1024) if not _fp: fp.write(d) if s: break if s < 0: - raise IOError("encoder error %d when writing image file" % s) + raise OSError(f"encoder error {s} when writing image file") else: + for tag in blocklist: + del ifd[tag] offset = ifd.save(fp) - ImageFile._save(im, fp, [ - ("raw", (0, 0)+im.size, offset, (rawmode, stride, 1)) - ]) + ImageFile._save( + im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))] + ) # -- helper for multi-page save -- - if "_debug_multipage" in im.encoderinfo: + if "_debug_multipage" in encoderinfo: # just to access o32 and o16 (using correct byte order) im._debug_multipage = ifd @@ -1560,16 +1909,16 @@ class AppendingTiffWriter: Tags = {273, 288, 324, 519, 520, 521} def __init__(self, fn, new=False): - if hasattr(fn, 'read'): + if hasattr(fn, "read"): self.f = fn self.close_fp = False else: self.name = fn self.close_fp = True try: - self.f = io.open(fn, "w+b" if new else "r+b") - except IOError: - self.f = io.open(fn, "w+b") + self.f = open(fn, "w+b" if new else "r+b") + except OSError: + self.f = open(fn, "w+b") self.beginning = self.f.tell() self.setup() @@ -1580,16 +1929,16 @@ def setup(self): self.whereToWriteNewIFDOffset = None self.offsetOfNewPage = 0 - self.IIMM = IIMM = self.f.read(4) - if not IIMM: + self.IIMM = iimm = self.f.read(4) + if not iimm: # empty file - first page self.isFirst = True return self.isFirst = False - if IIMM == b"II\x2a\x00": + if iimm == b"II\x2a\x00": self.setEndian("<") - elif IIMM == b"MM\x00\x2a": + elif iimm == b"MM\x00\x2a": self.setEndian(">") else: raise RuntimeError("Invalid TIFF file header") @@ -1604,21 +1953,20 @@ def finalize(self): # fix offsets self.f.seek(self.offsetOfNewPage) - IIMM = self.f.read(4) - if not IIMM: + iimm = self.f.read(4) + if not iimm: # raise RuntimeError("nothing written into new page") # 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") + if iimm != self.IIMM: + raise RuntimeError("IIMM of new page doesn't match IIMM of first page") - IFDoffset = self.readLong() - IFDoffset += self.offsetOfNewPage + ifd_offset = self.readLong() + ifd_offset += self.offsetOfNewPage self.f.seek(self.whereToWriteNewIFDOffset) - self.writeLong(IFDoffset) - self.f.seek(IFDoffset) + self.writeLong(ifd_offset) + self.f.seek(ifd_offset) self.fixIFD() def newFrame(self): @@ -1637,7 +1985,7 @@ def __exit__(self, exc_type, exc_value, traceback): def tell(self): return self.f.tell() - self.offsetOfNewPage - def seek(self, offset, whence): + def seek(self, offset, whence=io.SEEK_SET): if whence == os.SEEK_SET: offset += self.offsetOfNewPage @@ -1649,9 +1997,9 @@ def goToEnd(self): pos = self.f.tell() # pad to 16 byte boundary - padBytes = 16 - pos % 16 - if 0 < padBytes < 16: - self.f.write(bytes(bytearray(padBytes))) + pad_bytes = 16 - pos % 16 + if 0 < pad_bytes < 16: + self.f.write(bytes(pad_bytes)) self.offsetOfNewPage = self.f.tell() def setEndian(self, endian): @@ -1662,94 +2010,90 @@ def setEndian(self, endian): def skipIFDs(self): while True: - IFDoffset = self.readLong() - if IFDoffset == 0: + ifd_offset = self.readLong() + if ifd_offset == 0: self.whereToWriteNewIFDOffset = self.f.tell() - 4 break - self.f.seek(IFDoffset) - numTags = self.readShort() - self.f.seek(numTags * 12, os.SEEK_CUR) + self.f.seek(ifd_offset) + num_tags = self.readShort() + self.f.seek(num_tags * 12, os.SEEK_CUR) def write(self, data): return self.f.write(data) def readShort(self): - value, = struct.unpack(self.shortFmt, self.f.read(2)) + (value,) = struct.unpack(self.shortFmt, self.f.read(2)) return value def readLong(self): - value, = struct.unpack(self.longFmt, self.f.read(4)) + (value,) = struct.unpack(self.longFmt, self.f.read(4)) return value def rewriteLastShortToLong(self, value): self.f.seek(-2, os.SEEK_CUR) - bytesWritten = self.f.write(struct.pack(self.longFmt, value)) - if bytesWritten is not None and bytesWritten != 4: - raise RuntimeError("wrote only %u bytes but wanted 4" % - bytesWritten) + 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") def rewriteLastShort(self, value): self.f.seek(-2, os.SEEK_CUR) - bytesWritten = self.f.write(struct.pack(self.shortFmt, value)) - if bytesWritten is not None and bytesWritten != 2: - raise RuntimeError("wrote only %u bytes but wanted 2" % - bytesWritten) + 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") def rewriteLastLong(self, value): self.f.seek(-4, os.SEEK_CUR) - bytesWritten = self.f.write(struct.pack(self.longFmt, value)) - if bytesWritten is not None and bytesWritten != 4: - raise RuntimeError("wrote only %u bytes but wanted 4" % - bytesWritten) + 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") def writeShort(self, value): - bytesWritten = self.f.write(struct.pack(self.shortFmt, value)) - if bytesWritten is not None and bytesWritten != 2: - raise RuntimeError("wrote only %u bytes but wanted 2" % - bytesWritten) + 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") def writeLong(self, value): - bytesWritten = self.f.write(struct.pack(self.longFmt, value)) - if bytesWritten is not None and bytesWritten != 4: - raise RuntimeError("wrote only %u bytes but wanted 4" % - bytesWritten) + 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") def close(self): self.finalize() self.f.close() def fixIFD(self): - numTags = self.readShort() + num_tags = self.readShort() - for i in range(numTags): - tag, fieldType, count = struct.unpack(self.tagFormat, - self.f.read(8)) + for i in range(num_tags): + tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8)) - fieldSize = self.fieldSizes[fieldType] - totalSize = fieldSize * count - isLocal = (totalSize <= 4) - if not isLocal: + field_size = self.fieldSizes[field_type] + total_size = field_size * count + is_local = total_size <= 4 + if not is_local: offset = self.readLong() offset += self.offsetOfNewPage self.rewriteLastLong(offset) if tag in self.Tags: - curPos = self.f.tell() + cur_pos = self.f.tell() - if isLocal: - self.fixOffsets(count, isShort=(fieldSize == 2), - isLong=(fieldSize == 4)) - self.f.seek(curPos + 4) + if is_local: + self.fixOffsets( + count, isShort=(field_size == 2), isLong=(field_size == 4) + ) + self.f.seek(cur_pos + 4) else: self.f.seek(offset) - self.fixOffsets(count, isShort=(fieldSize == 2), - isLong=(fieldSize == 4)) - self.f.seek(curPos) + self.fixOffsets( + count, isShort=(field_size == 2), isLong=(field_size == 4) + ) + self.f.seek(cur_pos) - offset = curPos = None + offset = cur_pos = None - elif isLocal: + elif is_local: # skip the locally stored value that is not an offset self.f.seek(4, os.SEEK_CUR) @@ -1769,7 +2113,7 @@ def fixOffsets(self, count, isShort=False, isLong=False): # local (not referenced with another offset) self.rewriteLastShortToLong(offset) self.f.seek(-10, os.SEEK_CUR) - self.writeShort(4) # rewrite the type to LONG + self.writeShort(TiffTags.LONG) # rewrite the type to LONG self.f.seek(8, os.SEEK_CUR) elif isShort: self.rewriteLastShort(offset) @@ -1787,7 +2131,7 @@ def _save_all(im, fp, filename): cur_idx = im.tell() try: with AppendingTiffWriter(fp) as tf: - for ims in [im]+append_images: + for ims in [im] + append_images: ims.encoderinfo = encoderinfo ims.encoderconfig = encoderconfig if not hasattr(ims, "n_frames"): diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index eba88ef8dd4..9b527713864 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -24,23 +24,33 @@ class TagInfo(namedtuple("_TagInfo", "value name type length enum")): __slots__ = [] def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None): - return super(TagInfo, cls).__new__( - cls, value, name, type, length, enum or {}) + return super().__new__(cls, value, name, type, length, enum or {}) def cvt_enum(self, value): - return self.enum.get(value, value) + # Using get will call hash(value), which can be expensive + # for some types (e.g. Fraction). Since self.enum is rarely + # used, it's usually better to test it first. + return self.enum.get(value, value) if self.enum else value -def lookup(tag): +def lookup(tag, group=None): """ :param tag: Integer tag number - :returns: Taginfo namedtuple, From the TAGS_V2 info if possible, - otherwise just populating the value and name from TAGS. + :param group: Which :py:data:`~PIL.TiffTags.TAGS_V2_GROUPS` to look in + + .. versionadded:: 8.3.0 + + :returns: Taginfo namedtuple, From the ``TAGS_V2`` info if possible, + otherwise just populating the value and name from ``TAGS``. If the tag is not recognized, "unknown" is returned for the name """ - return TAGS_V2.get(tag, TagInfo(tag, TAGS.get(tag, 'unknown'))) + if group is not None: + info = TAGS_V2_GROUPS[group].get(tag) if group in TAGS_V2_GROUPS else None + else: + info = TAGS_V2.get(tag) + return info or TagInfo(tag, TAGS.get(tag, "unknown")) ## @@ -60,32 +70,58 @@ def lookup(tag): SHORT = 3 LONG = 4 RATIONAL = 5 +SIGNED_BYTE = 6 UNDEFINED = 7 +SIGNED_SHORT = 8 +SIGNED_LONG = 9 SIGNED_RATIONAL = 10 +FLOAT = 11 DOUBLE = 12 +IFD = 13 +LONG8 = 16 TAGS_V2 = { - 254: ("NewSubfileType", LONG, 1), 255: ("SubfileType", SHORT, 1), 256: ("ImageWidth", LONG, 1), 257: ("ImageLength", LONG, 1), 258: ("BitsPerSample", SHORT, 0), - 259: ("Compression", SHORT, 1, - {"Uncompressed": 1, "CCITT 1d": 2, "Group 3 Fax": 3, "Group 4 Fax": 4, - "LZW": 5, "JPEG": 6, "PackBits": 32773}), - - 262: ("PhotometricInterpretation", SHORT, 1, - {"WhiteIsZero": 0, "BlackIsZero": 1, "RGB": 2, "RGB Palette": 3, - "Transparency Mask": 4, "CMYK": 5, "YCbCr": 6, "CieLAB": 8, - "CFA": 32803, # TIFF/EP, Adobe DNG - "LinearRaw": 32892}), # Adobe DNG + 259: ( + "Compression", + SHORT, + 1, + { + "Uncompressed": 1, + "CCITT 1d": 2, + "Group 3 Fax": 3, + "Group 4 Fax": 4, + "LZW": 5, + "JPEG": 6, + "PackBits": 32773, + }, + ), + 262: ( + "PhotometricInterpretation", + SHORT, + 1, + { + "WhiteIsZero": 0, + "BlackIsZero": 1, + "RGB": 2, + "RGB Palette": 3, + "Transparency Mask": 4, + "CMYK": 5, + "YCbCr": 6, + "CieLAB": 8, + "CFA": 32803, # TIFF/EP, Adobe DNG + "LinearRaw": 32892, # Adobe DNG + }, + ), 263: ("Threshholding", SHORT, 1), 264: ("CellWidth", SHORT, 1), 265: ("CellLength", SHORT, 1), 266: ("FillOrder", SHORT, 1), 269: ("DocumentName", ASCII, 1), - 270: ("ImageDescription", ASCII, 1), 271: ("Make", ASCII, 1), 272: ("Model", ASCII, 1), @@ -94,8 +130,7 @@ def lookup(tag): 277: ("SamplesPerPixel", SHORT, 1), 278: ("RowsPerStrip", LONG, 1), 279: ("StripByteCounts", LONG, 0), - - 280: ("MinSampleValue", LONG, 0), + 280: ("MinSampleValue", SHORT, 0), 281: ("MaxSampleValue", SHORT, 0), 282: ("XResolution", RATIONAL, 1), 283: ("YResolution", RATIONAL, 1), @@ -105,31 +140,27 @@ def lookup(tag): 287: ("YPosition", RATIONAL, 1), 288: ("FreeOffsets", LONG, 1), 289: ("FreeByteCounts", LONG, 1), - 290: ("GrayResponseUnit", SHORT, 1), 291: ("GrayResponseCurve", SHORT, 0), 292: ("T4Options", LONG, 1), 293: ("T6Options", LONG, 1), 296: ("ResolutionUnit", SHORT, 1, {"none": 1, "inch": 2, "cm": 3}), 297: ("PageNumber", SHORT, 2), - 301: ("TransferFunction", SHORT, 0), 305: ("Software", ASCII, 1), 306: ("DateTime", ASCII, 1), - 315: ("Artist", ASCII, 1), 316: ("HostComputer", ASCII, 1), 317: ("Predictor", SHORT, 1, {"none": 1, "Horizontal Differencing": 2}), 318: ("WhitePoint", RATIONAL, 2), - 319: ("PrimaryChromaticities", SHORT, 6), - + 319: ("PrimaryChromaticities", RATIONAL, 6), 320: ("ColorMap", SHORT, 0), 321: ("HalftoneHints", SHORT, 2), 322: ("TileWidth", LONG, 1), 323: ("TileLength", LONG, 1), 324: ("TileOffsets", LONG, 0), 325: ("TileByteCounts", LONG, 0), - + 330: ("SubIFDs", LONG, 0), 332: ("InkSet", SHORT, 1), 333: ("InkNames", ASCII, 1), 334: ("NumberOfInks", SHORT, 1), @@ -137,13 +168,10 @@ def lookup(tag): 337: ("TargetPrinter", ASCII, 1), 338: ("ExtraSamples", SHORT, 0), 339: ("SampleFormat", SHORT, 0), - 340: ("SMinSampleValue", DOUBLE, 0), 341: ("SMaxSampleValue", DOUBLE, 0), 342: ("TransferRange", SHORT, 6), - 347: ("JPEGTables", UNDEFINED, 1), - # obsolete JPEG tags 512: ("JPEGProc", SHORT, 1), 513: ("JPEGInterchangeFormat", LONG, 1), @@ -154,22 +182,21 @@ def lookup(tag): 519: ("JPEGQTables", LONG, 0), 520: ("JPEGDCTables", LONG, 0), 521: ("JPEGACTables", LONG, 0), - 529: ("YCbCrCoefficients", RATIONAL, 3), 530: ("YCbCrSubSampling", SHORT, 2), 531: ("YCbCrPositioning", SHORT, 1), - 532: ("ReferenceBlackWhite", LONG, 0), - - 700: ('XMP', BYTE, 1), - + 532: ("ReferenceBlackWhite", RATIONAL, 6), + 700: ("XMP", BYTE, 0), 33432: ("Copyright", ASCII, 1), - 34377: ('PhotoshopInfo', BYTE, 1), - + 33723: ("IptcNaaInfo", UNDEFINED, 1), + 34377: ("PhotoshopInfo", BYTE, 0), # FIXME add more tags here - 34665: ("ExifIFD", SHORT, 1), - 34675: ('ICCProfile', UNDEFINED, 1), - 34853: ('GPSInfoIFD', BYTE, 1), - + 34665: ("ExifIFD", LONG, 1), + 34675: ("ICCProfile", UNDEFINED, 1), + 34853: ("GPSInfoIFD", LONG, 1), + 36864: ("ExifVersion", UNDEFINED, 1), + 40965: ("InteroperabilityIFD", LONG, 1), + 41730: ("CFAPattern", UNDEFINED, 1), # MPInfo 45056: ("MPFVersion", UNDEFINED, 1), 45057: ("NumberOfImages", LONG, 1), @@ -190,159 +217,203 @@ def lookup(tag): 45579: ("YawAngle", SIGNED_RATIONAL, 1), 45580: ("PitchAngle", SIGNED_RATIONAL, 1), 45581: ("RollAngle", SIGNED_RATIONAL, 1), - + 40960: ("FlashPixVersion", UNDEFINED, 1), 50741: ("MakerNoteSafety", SHORT, 1, {"Unsafe": 0, "Safe": 1}), 50780: ("BestQualityScale", RATIONAL, 1), - 50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one - 50839: ("ImageJMetaData", UNDEFINED, 1) # see Issue #2006 + 50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one + 50839: ("ImageJMetaData", UNDEFINED, 1), # see Issue #2006 +} +TAGS_V2_GROUPS = { + # ExifIFD + 34665: { + 36864: ("ExifVersion", UNDEFINED, 1), + 40960: ("FlashPixVersion", UNDEFINED, 1), + 40965: ("InteroperabilityIFD", LONG, 1), + 41730: ("CFAPattern", UNDEFINED, 1), + }, + # GPSInfoIFD + 34853: { + 0: ("GPSVersionID", BYTE, 4), + 1: ("GPSLatitudeRef", ASCII, 2), + 2: ("GPSLatitude", RATIONAL, 3), + 3: ("GPSLongitudeRef", ASCII, 2), + 4: ("GPSLongitude", RATIONAL, 3), + 5: ("GPSAltitudeRef", BYTE, 1), + 6: ("GPSAltitude", RATIONAL, 1), + 7: ("GPSTimeStamp", RATIONAL, 3), + 8: ("GPSSatellites", ASCII, 0), + 9: ("GPSStatus", ASCII, 2), + 10: ("GPSMeasureMode", ASCII, 2), + 11: ("GPSDOP", RATIONAL, 1), + 12: ("GPSSpeedRef", ASCII, 2), + 13: ("GPSSpeed", RATIONAL, 1), + 14: ("GPSTrackRef", ASCII, 2), + 15: ("GPSTrack", RATIONAL, 1), + 16: ("GPSImgDirectionRef", ASCII, 2), + 17: ("GPSImgDirection", RATIONAL, 1), + 18: ("GPSMapDatum", ASCII, 0), + 19: ("GPSDestLatitudeRef", ASCII, 2), + 20: ("GPSDestLatitude", RATIONAL, 3), + 21: ("GPSDestLongitudeRef", ASCII, 2), + 22: ("GPSDestLongitude", RATIONAL, 3), + 23: ("GPSDestBearingRef", ASCII, 2), + 24: ("GPSDestBearing", RATIONAL, 1), + 25: ("GPSDestDistanceRef", ASCII, 2), + 26: ("GPSDestDistance", RATIONAL, 1), + 27: ("GPSProcessingMethod", UNDEFINED, 0), + 28: ("GPSAreaInformation", UNDEFINED, 0), + 29: ("GPSDateStamp", ASCII, 11), + 30: ("GPSDifferential", SHORT, 1), + }, + # InteroperabilityIFD + 40965: {1: ("InteropIndex", ASCII, 1), 2: ("InteropVersion", UNDEFINED, 1)}, } # Legacy Tags structure # these tags aren't included above, but were in the previous versions -TAGS = {347: 'JPEGTables', - 700: 'XMP', - - # Additional Exif Info - 32932: 'Wang Annotation', - 33434: 'ExposureTime', - 33437: 'FNumber', - 33445: 'MD FileTag', - 33446: 'MD ScalePixel', - 33447: 'MD ColorTable', - 33448: 'MD LabName', - 33449: 'MD SampleInfo', - 33450: 'MD PrepDate', - 33451: 'MD PrepTime', - 33452: 'MD FileUnits', - 33550: 'ModelPixelScaleTag', - 33723: 'IptcNaaInfo', - 33918: 'INGR Packet Data Tag', - 33919: 'INGR Flag Registers', - 33920: 'IrasB Transformation Matrix', - 33922: 'ModelTiepointTag', - 34264: 'ModelTransformationTag', - 34377: 'PhotoshopInfo', - 34735: 'GeoKeyDirectoryTag', - 34736: 'GeoDoubleParamsTag', - 34737: 'GeoAsciiParamsTag', - 34850: 'ExposureProgram', - 34852: 'SpectralSensitivity', - 34855: 'ISOSpeedRatings', - 34856: 'OECF', - 34864: 'SensitivityType', - 34865: 'StandardOutputSensitivity', - 34866: 'RecommendedExposureIndex', - 34867: 'ISOSpeed', - 34868: 'ISOSpeedLatitudeyyy', - 34869: 'ISOSpeedLatitudezzz', - 34908: 'HylaFAX FaxRecvParams', - 34909: 'HylaFAX FaxSubAddress', - 34910: 'HylaFAX FaxRecvTime', - 36864: 'ExifVersion', - 36867: 'DateTimeOriginal', - 36868: 'DateTImeDigitized', - 37121: 'ComponentsConfiguration', - 37122: 'CompressedBitsPerPixel', - 37724: 'ImageSourceData', - 37377: 'ShutterSpeedValue', - 37378: 'ApertureValue', - 37379: 'BrightnessValue', - 37380: 'ExposureBiasValue', - 37381: 'MaxApertureValue', - 37382: 'SubjectDistance', - 37383: 'MeteringMode', - 37384: 'LightSource', - 37385: 'Flash', - 37386: 'FocalLength', - 37396: 'SubjectArea', - 37500: 'MakerNote', - 37510: 'UserComment', - 37520: 'SubSec', - 37521: 'SubSecTimeOriginal', - 37522: 'SubsecTimeDigitized', - 40960: 'FlashPixVersion', - 40961: 'ColorSpace', - 40962: 'PixelXDimension', - 40963: 'PixelYDimension', - 40964: 'RelatedSoundFile', - 40965: 'InteroperabilityIFD', - 41483: 'FlashEnergy', - 41484: 'SpatialFrequencyResponse', - 41486: 'FocalPlaneXResolution', - 41487: 'FocalPlaneYResolution', - 41488: 'FocalPlaneResolutionUnit', - 41492: 'SubjectLocation', - 41493: 'ExposureIndex', - 41495: 'SensingMethod', - 41728: 'FileSource', - 41729: 'SceneType', - 41730: 'CFAPattern', - 41985: 'CustomRendered', - 41986: 'ExposureMode', - 41987: 'WhiteBalance', - 41988: 'DigitalZoomRatio', - 41989: 'FocalLengthIn35mmFilm', - 41990: 'SceneCaptureType', - 41991: 'GainControl', - 41992: 'Contrast', - 41993: 'Saturation', - 41994: 'Sharpness', - 41995: 'DeviceSettingDescription', - 41996: 'SubjectDistanceRange', - 42016: 'ImageUniqueID', - 42032: 'CameraOwnerName', - 42033: 'BodySerialNumber', - 42034: 'LensSpecification', - 42035: 'LensMake', - 42036: 'LensModel', - 42037: 'LensSerialNumber', - 42112: 'GDAL_METADATA', - 42113: 'GDAL_NODATA', - 42240: 'Gamma', - 50215: 'Oce Scanjob Description', - 50216: 'Oce Application Selector', - 50217: 'Oce Identification Number', - 50218: 'Oce ImageLogic Characteristics', - - # Adobe DNG - 50706: 'DNGVersion', - 50707: 'DNGBackwardVersion', - 50708: 'UniqueCameraModel', - 50709: 'LocalizedCameraModel', - 50710: 'CFAPlaneColor', - 50711: 'CFALayout', - 50712: 'LinearizationTable', - 50713: 'BlackLevelRepeatDim', - 50714: 'BlackLevel', - 50715: 'BlackLevelDeltaH', - 50716: 'BlackLevelDeltaV', - 50717: 'WhiteLevel', - 50718: 'DefaultScale', - 50719: 'DefaultCropOrigin', - 50720: 'DefaultCropSize', - 50721: 'ColorMatrix1', - 50722: 'ColorMatrix2', - 50723: 'CameraCalibration1', - 50724: 'CameraCalibration2', - 50725: 'ReductionMatrix1', - 50726: 'ReductionMatrix2', - 50727: 'AnalogBalance', - 50728: 'AsShotNeutral', - 50729: 'AsShotWhiteXY', - 50730: 'BaselineExposure', - 50731: 'BaselineNoise', - 50732: 'BaselineSharpness', - 50733: 'BayerGreenSplit', - 50734: 'LinearResponseLimit', - 50735: 'CameraSerialNumber', - 50736: 'LensInfo', - 50737: 'ChromaBlurRadius', - 50738: 'AntiAliasStrength', - 50740: 'DNGPrivateData', - 50778: 'CalibrationIlluminant1', - 50779: 'CalibrationIlluminant2', - 50784: 'Alias Layer Metadata' - } +TAGS = { + 347: "JPEGTables", + 700: "XMP", + # Additional Exif Info + 32932: "Wang Annotation", + 33434: "ExposureTime", + 33437: "FNumber", + 33445: "MD FileTag", + 33446: "MD ScalePixel", + 33447: "MD ColorTable", + 33448: "MD LabName", + 33449: "MD SampleInfo", + 33450: "MD PrepDate", + 33451: "MD PrepTime", + 33452: "MD FileUnits", + 33550: "ModelPixelScaleTag", + 33723: "IptcNaaInfo", + 33918: "INGR Packet Data Tag", + 33919: "INGR Flag Registers", + 33920: "IrasB Transformation Matrix", + 33922: "ModelTiepointTag", + 34264: "ModelTransformationTag", + 34377: "PhotoshopInfo", + 34735: "GeoKeyDirectoryTag", + 34736: "GeoDoubleParamsTag", + 34737: "GeoAsciiParamsTag", + 34850: "ExposureProgram", + 34852: "SpectralSensitivity", + 34855: "ISOSpeedRatings", + 34856: "OECF", + 34864: "SensitivityType", + 34865: "StandardOutputSensitivity", + 34866: "RecommendedExposureIndex", + 34867: "ISOSpeed", + 34868: "ISOSpeedLatitudeyyy", + 34869: "ISOSpeedLatitudezzz", + 34908: "HylaFAX FaxRecvParams", + 34909: "HylaFAX FaxSubAddress", + 34910: "HylaFAX FaxRecvTime", + 36864: "ExifVersion", + 36867: "DateTimeOriginal", + 36868: "DateTImeDigitized", + 37121: "ComponentsConfiguration", + 37122: "CompressedBitsPerPixel", + 37724: "ImageSourceData", + 37377: "ShutterSpeedValue", + 37378: "ApertureValue", + 37379: "BrightnessValue", + 37380: "ExposureBiasValue", + 37381: "MaxApertureValue", + 37382: "SubjectDistance", + 37383: "MeteringMode", + 37384: "LightSource", + 37385: "Flash", + 37386: "FocalLength", + 37396: "SubjectArea", + 37500: "MakerNote", + 37510: "UserComment", + 37520: "SubSec", + 37521: "SubSecTimeOriginal", + 37522: "SubsecTimeDigitized", + 40960: "FlashPixVersion", + 40961: "ColorSpace", + 40962: "PixelXDimension", + 40963: "PixelYDimension", + 40964: "RelatedSoundFile", + 40965: "InteroperabilityIFD", + 41483: "FlashEnergy", + 41484: "SpatialFrequencyResponse", + 41486: "FocalPlaneXResolution", + 41487: "FocalPlaneYResolution", + 41488: "FocalPlaneResolutionUnit", + 41492: "SubjectLocation", + 41493: "ExposureIndex", + 41495: "SensingMethod", + 41728: "FileSource", + 41729: "SceneType", + 41730: "CFAPattern", + 41985: "CustomRendered", + 41986: "ExposureMode", + 41987: "WhiteBalance", + 41988: "DigitalZoomRatio", + 41989: "FocalLengthIn35mmFilm", + 41990: "SceneCaptureType", + 41991: "GainControl", + 41992: "Contrast", + 41993: "Saturation", + 41994: "Sharpness", + 41995: "DeviceSettingDescription", + 41996: "SubjectDistanceRange", + 42016: "ImageUniqueID", + 42032: "CameraOwnerName", + 42033: "BodySerialNumber", + 42034: "LensSpecification", + 42035: "LensMake", + 42036: "LensModel", + 42037: "LensSerialNumber", + 42112: "GDAL_METADATA", + 42113: "GDAL_NODATA", + 42240: "Gamma", + 50215: "Oce Scanjob Description", + 50216: "Oce Application Selector", + 50217: "Oce Identification Number", + 50218: "Oce ImageLogic Characteristics", + # Adobe DNG + 50706: "DNGVersion", + 50707: "DNGBackwardVersion", + 50708: "UniqueCameraModel", + 50709: "LocalizedCameraModel", + 50710: "CFAPlaneColor", + 50711: "CFALayout", + 50712: "LinearizationTable", + 50713: "BlackLevelRepeatDim", + 50714: "BlackLevel", + 50715: "BlackLevelDeltaH", + 50716: "BlackLevelDeltaV", + 50717: "WhiteLevel", + 50718: "DefaultScale", + 50719: "DefaultCropOrigin", + 50720: "DefaultCropSize", + 50721: "ColorMatrix1", + 50722: "ColorMatrix2", + 50723: "CameraCalibration1", + 50724: "CameraCalibration2", + 50725: "ReductionMatrix1", + 50726: "ReductionMatrix2", + 50727: "AnalogBalance", + 50728: "AsShotNeutral", + 50729: "AsShotWhiteXY", + 50730: "BaselineExposure", + 50731: "BaselineNoise", + 50732: "BaselineSharpness", + 50733: "BayerGreenSplit", + 50734: "LinearResponseLimit", + 50735: "CameraSerialNumber", + 50736: "LensInfo", + 50737: "ChromaBlurRadius", + 50738: "AntiAliasStrength", + 50740: "DNGPrivateData", + 50778: "CalibrationIlluminant1", + 50779: "CalibrationIlluminant2", + 50784: "Alias Layer Metadata", +} def _populate(): @@ -355,6 +426,11 @@ def _populate(): TAGS_V2[k] = TagInfo(k, *v) + for group, tags in TAGS_V2_GROUPS.items(): + for k, v in tags.items(): + tags[k] = TagInfo(k, *v) + + _populate() ## # Map type numbers to type names -- defined in ImageFileDirectory. @@ -421,22 +497,58 @@ def _populate(): # 389: case TIFFTAG_REFERENCEBLACKWHITE: # 393: case TIFFTAG_INKNAMES: -# some of these are not in our TAGS_V2 dict and were included from tiff.h +# Following pseudo-tags are also handled by default in libtiff: +# TIFFTAG_JPEGQUALITY 65537 -LIBTIFF_CORE = {255, 256, 257, 258, 259, 262, 263, 266, 274, 277, - 278, 280, 281, 340, 341, 282, 283, 284, 286, 287, - 296, 297, 321, 320, 338, 32995, 322, 323, 32998, - 32996, 339, 32997, 330, 531, 530, 301, 532, 333, - # as above - 269 # this has been in our tests forever, and works - } +# some of these are not in our TAGS_V2 dict and were included from tiff.h -LIBTIFF_CORE.remove(320) # Array of short, crashes -LIBTIFF_CORE.remove(301) # Array of short, crashes -LIBTIFF_CORE.remove(532) # Array of long, crashes +# This list also exists in encode.c +LIBTIFF_CORE = { + 255, + 256, + 257, + 258, + 259, + 262, + 263, + 266, + 274, + 277, + 278, + 280, + 281, + 340, + 341, + 282, + 283, + 284, + 286, + 287, + 296, + 297, + 321, + 320, + 338, + 32995, + 322, + 323, + 32998, + 32996, + 339, + 32997, + 330, + 531, + 530, + 301, + 532, + 333, + # as above + 269, # this has been in our tests forever, and works + 65537, +} LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes -LIBTIFF_CORE.remove(322) # We don't have support for tiled images in libtiff +LIBTIFF_CORE.remove(322) # We don't have support for writing tiled images with libtiff LIBTIFF_CORE.remove(323) # Tiled images LIBTIFF_CORE.remove(333) # Ink Names either diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index 69964f7be3a..0dc695a88d4 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -1,4 +1,3 @@ -# encoding: utf-8 # # The Python Imaging Library. # $Id$ @@ -13,66 +12,64 @@ # See the README file for information on usage and redistribution. # -# NOTE: This format cannot be automatically recognized, so the reader -# is not registered for use with Image.open(). To open a WAL file, use -# the WalImageFile.open() function instead. +""" +This reader is based on the specification available from: +https://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml +and has been tested with a few sample files found using google. -# This reader is based on the specification available from: -# https://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml -# and has been tested with a few sample files found using google. +.. note:: + This format cannot be automatically recognized, so the reader + is not registered for use with :py:func:`PIL.Image.open()`. + To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead. +""" -from . import Image +from . import Image, ImageFile from ._binary import i32le as i32 -try: - import builtins -except ImportError: - import __builtin__ - builtins = __builtin__ +class WalImageFile(ImageFile.ImageFile): -def open(filename): - """ - Load texture from a Quake2 WAL texture file. + format = "WAL" + format_description = "Quake2 Texture" - By default, a Quake2 standard palette is attached to the texture. - To override the palette, use the putpalette method. - - :param filename: WAL file name, or an opened file handle. - :returns: An image instance. - """ - # FIXME: modify to return a WalImageFile instance instead of - # plain Image object ? + def _open(self): + self.mode = "P" - def imopen(fp): # read header fields - header = fp.read(32+24+32+12) - size = i32(header, 32), i32(header, 36) - offset = i32(header, 40) + header = self.fp.read(32 + 24 + 32 + 12) + self._size = i32(header, 32), i32(header, 36) + Image._decompression_bomb_check(self.size) # load pixel data - fp.seek(offset) - - Image._decompression_bomb_check(size) - im = Image.frombytes("P", size, fp.read(size[0] * size[1])) - im.putpalette(quake2palette) - - im.format = "WAL" - im.format_description = "Quake2 Texture" + offset = i32(header, 40) + self.fp.seek(offset) # strings are null-terminated - im.info["name"] = header[:32].split(b"\0", 1)[0] - next_name = header[56:56+32].split(b"\0", 1)[0] + self.info["name"] = header[:32].split(b"\0", 1)[0] + next_name = header[56 : 56 + 32].split(b"\0", 1)[0] if next_name: - im.info["next_name"] = next_name + self.info["next_name"] = next_name - return im + def load(self): + if not self.im: + self.im = Image.core.new(self.mode, self.size) + self.frombytes(self.fp.read(self.size[0] * self.size[1])) + self.putpalette(quake2palette) + return Image.Image.load(self) + + +def open(filename): + """ + Load texture from a Quake2 WAL texture file. + + By default, a Quake2 standard palette is attached to the texture. + To override the palette, use the :py:func:`PIL.Image.Image.putpalette()` method. + + :param filename: WAL file name, or an opened file handle. + :returns: An image instance. + """ + return WalImageFile(filename) - if hasattr(filename, "read"): - return imopen(filename) - else: - with builtins.open(filename, "rb") as fp: - return imopen(fp) quake2palette = ( # default palette taken from piffo 0.93 by Hans Häggström diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 39a8f2e35dd..5eaeb10ccd5 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -1,22 +1,24 @@ -from . import Image, ImageFile, _webp from io import BytesIO +from . import Image, ImageFile -_VALID_WEBP_MODES = { - "RGBX": True, - "RGBA": True, - } +try: + from . import _webp -_VALID_WEBP_LEGACY_MODES = { - "RGB": True, - "RGBA": True, - } + SUPPORTED = True +except ImportError: + SUPPORTED = False + + +_VALID_WEBP_MODES = {"RGBX": True, "RGBA": True, "RGB": True} + +_VALID_WEBP_LEGACY_MODES = {"RGB": True, "RGBA": True} _VP8_MODES_BY_IDENTIFIER = { b"VP8 ": "RGB", b"VP8X": "RGBA", b"VP8L": "RGBA", # lossless - } +} def _accept(prefix): @@ -24,27 +26,36 @@ def _accept(prefix): is_webp_file = prefix[8:12] == b"WEBP" is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER - return is_riff_file_format and is_webp_file and is_valid_vp8_mode + if is_riff_file_format and is_webp_file and is_valid_vp8_mode: + if not SUPPORTED: + return ( + "image file could not be identified because WEBP support not installed" + ) + return True class WebPImageFile(ImageFile.ImageFile): format = "WEBP" format_description = "WebP image" + __loaded = 0 + __logical_frame = 0 def _open(self): if not _webp.HAVE_WEBPANIM: # Legacy mode - data, width, height, self.mode, icc_profile, exif = \ - _webp.WebPDecode(self.fp.read()) + data, width, height, self.mode, icc_profile, exif = _webp.WebPDecode( + self.fp.read() + ) if icc_profile: self.info["icc_profile"] = icc_profile if exif: self.info["exif"] = exif - self.size = width, height + self._size = width, height self.fp = BytesIO(data) self.tile = [("raw", (0, 0) + self.size, 0, self.mode)] - self._n_frames = 1 + self.n_frames = 1 + self.is_animated = False return # Use the newer AnimDecoder API to parse the (possibly) animated file, @@ -52,18 +63,20 @@ def _open(self): self._decoder = _webp.WebPAnimDecoder(self.fp.read()) # Get info from decoder - width, height, loop_count, bgcolor, frame_count, mode = \ - self._decoder.get_info() - self.size = width, height + width, height, loop_count, bgcolor, frame_count, mode = self._decoder.get_info() + self._size = width, height self.info["loop"] = loop_count - bg_a, bg_r, bg_g, bg_b = \ - (bgcolor >> 24) & 0xFF, \ - (bgcolor >> 16) & 0xFF, \ - (bgcolor >> 8) & 0xFF, \ - bgcolor & 0xFF + bg_a, bg_r, bg_g, bg_b = ( + (bgcolor >> 24) & 0xFF, + (bgcolor >> 16) & 0xFF, + (bgcolor >> 8) & 0xFF, + bgcolor & 0xFF, + ) self.info["background"] = (bg_r, bg_g, bg_b, bg_a) - self._n_frames = frame_count - self.mode = mode + self.n_frames = frame_count + self.is_animated = self.n_frames > 1 + self.mode = "RGB" if mode == "RGBX" else mode + self.rawmode = mode self.tile = [] # Attempt to read ICC / EXIF / XMP chunks from file @@ -79,29 +92,15 @@ def _open(self): # Initialize seek state self._reset(reset=False) - self.seek(0) def _getexif(self): - from .JpegImagePlugin import _getexif - return _getexif(self) - - @property - def n_frames(self): - return self._n_frames - - @property - def is_animated(self): - return self._n_frames > 1 + if "exif" not in self.info: + return None + return self.getexif()._get_merged_dict() def seek(self, frame): - if not _webp.HAVE_WEBPANIM: - return super(WebPImageFile, self).seek(frame) - - # Perform some simple checks first - if frame >= self._n_frames: - raise EOFError("attempted to seek beyond end of sequence") - if frame < 0: - raise EOFError("negative frame index is not valid") + if not self._seek_check(frame): + return # Set logical frame to requested position self.__logical_frame = frame @@ -120,7 +119,7 @@ def _get_next(self): # Check if an error occurred if ret is None: - self._reset() # Reset just to be safe + self._reset() # Reset just to be safe self.seek(0) raise EOFError("failed to decode next frame in WebP file") @@ -135,11 +134,11 @@ def _get_next(self): def _seek(self, frame): if self.__physical_frame == frame: - return # Nothing to do + return # Nothing to do if frame < self.__physical_frame: - self._reset() # Rewind to beginning + self._reset() # Rewind to beginning while self.__physical_frame < frame: - self._get_next() # Advance to the requested frame + self._get_next() # Advance to the requested frame def load(self): if _webp.HAVE_WEBPANIM: @@ -153,14 +152,16 @@ def load(self): self.__loaded = self.__logical_frame # Set tile + if self.fp and self._exclusive_fp: + self.fp.close() self.fp = BytesIO(data) - self.tile = [("raw", (0, 0) + self.size, 0, self.mode)] + self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)] - return super(WebPImageFile, self).load() + return super().load() def tell(self): if not _webp.HAVE_WEBPANIM: - return super(WebPImageFile, self).tell() + return super().tell() return self.__logical_frame @@ -172,14 +173,28 @@ def _save_all(im, fp, filename): # If total frame count is 1, then save using the legacy API, which # will preserve non-alpha modes total = 0 - for ims in [im]+append_images: - total += 1 if not hasattr(ims, "n_frames") else ims.n_frames + for ims in [im] + append_images: + total += getattr(ims, "n_frames", 1) if total == 1: _save(im, fp, filename) return - background = encoderinfo.get("background", (0, 0, 0, 0)) - duration = im.encoderinfo.get("duration", 0) + background = (0, 0, 0, 0) + if "background" in encoderinfo: + background = encoderinfo["background"] + elif "background" in im.info: + background = im.info["background"] + if isinstance(background, int): + # GifImagePlugin stores a global color table index in + # info["background"]. So it must be converted to an RGBA value + palette = im.getpalette() + if palette: + r, g, b = palette[background * 3 : (background + 1) * 3] + background = (r, g, b, 255) + else: + background = (background, background, background, 255) + + duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) loop = im.encoderinfo.get("loop", 0) minimize_size = im.encoderinfo.get("minimize_size", False) kmin = im.encoderinfo.get("kmin", None) @@ -189,8 +204,10 @@ def _save_all(im, fp, filename): lossless = im.encoderinfo.get("lossless", False) quality = im.encoderinfo.get("quality", 80) method = im.encoderinfo.get("method", 0) - icc_profile = im.encoderinfo.get("icc_profile", "") + icc_profile = im.encoderinfo.get("icc_profile") or "" exif = im.encoderinfo.get("exif", "") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() xmp = im.encoderinfo.get("xmp", "") if allow_mixed: lossless = False @@ -202,10 +219,14 @@ def _save_all(im, fp, filename): kmax = 17 if lossless else 5 # Validate background color - if (not isinstance(background, (list, tuple)) or len(background) != 4 or - not all(v >= 0 and v < 256 for v in background)): - raise IOError("Background color is not an RGBA tuple clamped " - "to (0-255): %s" % str(background)) + if ( + not isinstance(background, (list, tuple)) + 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}" + ) # Convert to packed uint bg_r, bg_g, bg_b, bg_a = background @@ -213,13 +234,15 @@ def _save_all(im, fp, filename): # Setup the WebP animation encoder enc = _webp.WebPAnimEncoder( - im.size[0], im.size[1], + im.size[0], + im.size[1], background, loop, minimize_size, - kmin, kmax, + kmin, + kmax, allow_mixed, - verbose + verbose, ) # Add each frame @@ -227,12 +250,9 @@ def _save_all(im, fp, filename): timestamp = 0 cur_idx = im.tell() try: - for ims in [im]+append_images: + for ims in [im] + append_images: # Get # of frames in this image - if not hasattr(ims, "n_frames"): - nfr = 1 - else: - nfr = ims.n_frames + nfr = getattr(ims, "n_frames", 1) for idx in range(nfr): ims.seek(idx) @@ -240,19 +260,30 @@ def _save_all(im, fp, filename): # Make sure image mode is supported frame = ims + rawmode = ims.mode if ims.mode not in _VALID_WEBP_MODES: - alpha = ims.mode == 'P' and 'A' in ims.im.getpalettemode() - frame = ims.convert('RGBA' if alpha else 'RGBX') + alpha = ( + "A" in ims.mode + or "a" in ims.mode + or (ims.mode == "P" and "A" in ims.im.getpalettemode()) + ) + rawmode = "RGBA" if alpha else "RGB" + frame = ims.convert(rawmode) + + if rawmode == "RGB": + # For faster conversion, use RGBX + rawmode = "RGBX" # Append the frame to the animation encoder enc.add( - frame.tobytes(), + frame.tobytes("raw", rawmode), timestamp, - frame.size[0], frame.size[1], - frame.mode, + frame.size[0], + frame.size[1], + rawmode, lossless, quality, - method + method, ) # Update timestamp and frame index @@ -266,16 +297,12 @@ def _save_all(im, fp, filename): im.seek(cur_idx) # Force encoder to flush frames - enc.add( - None, - timestamp, - 0, 0, "", lossless, quality, 0 - ) + enc.add(None, timestamp, 0, 0, "", lossless, quality, 0) # Get the final output from the encoder data = enc.assemble(icc_profile, exif, xmp) if data is None: - raise IOError("cannot write file as WebP (encoder returned None)") + raise OSError("cannot write file as WebP (encoder returned None)") fp.write(data) @@ -283,13 +310,22 @@ def _save_all(im, fp, filename): def _save(im, fp, filename): lossless = im.encoderinfo.get("lossless", False) quality = im.encoderinfo.get("quality", 80) - icc_profile = im.encoderinfo.get("icc_profile", "") - exif = im.encoderinfo.get("exif", "") + icc_profile = im.encoderinfo.get("icc_profile") or "" + exif = im.encoderinfo.get("exif", b"") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + if exif.startswith(b"Exif\x00\x00"): + exif = exif[6:] xmp = im.encoderinfo.get("xmp", "") + method = im.encoderinfo.get("method", 4) if im.mode not in _VALID_WEBP_LEGACY_MODES: - alpha = im.mode == 'P' and 'A' in im.im.getpalettemode() - im = im.convert('RGBA' if alpha else 'RGB') + alpha = ( + "A" in im.mode + or "a" in im.mode + or (im.mode == "P" and "transparency" in im.info) + ) + im = im.convert("RGBA" if alpha else "RGB") data = _webp.WebPEncode( im.tobytes(), @@ -299,18 +335,20 @@ def _save(im, fp, filename): float(quality), im.mode, icc_profile, + method, exif, - xmp + xmp, ) if data is None: - raise IOError("cannot write file as WebP (encoder returned None)") + raise OSError("cannot write file as WebP (encoder returned None)") fp.write(data) Image.register_open(WebPImageFile.format, WebPImageFile, _accept) -Image.register_save(WebPImageFile.format, _save) -if _webp.HAVE_WEBPANIM: - Image.register_save_all(WebPImageFile.format, _save_all) -Image.register_extension(WebPImageFile.format, ".webp") -Image.register_mime(WebPImageFile.format, "image/webp") +if SUPPORTED: + Image.register_save(WebPImageFile.format, _save) + if _webp.HAVE_WEBPANIM: + Image.register_save_all(WebPImageFile.format, _save_all) + Image.register_extension(WebPImageFile.format, ".webp") + Image.register_mime(WebPImageFile.format, "image/webp") diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index fea436b3a63..2f54cdebbea 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -19,19 +19,13 @@ # http://wvware.sourceforge.net/caolan/index.html # http://wvware.sourceforge.net/caolan/ora-wmf.html -from __future__ import print_function - from . import Image, ImageFile -from ._binary import i16le as word, si16le as short, i32le as dword, si32le as _long - - -__version__ = "0.2" +from ._binary import i16le as word +from ._binary import si16le as short +from ._binary import si32le as _long _handler = None -if str != bytes: - long = int - def register_handler(handler): """ @@ -42,11 +36,11 @@ def register_handler(handler): global _handler _handler = handler + if hasattr(Image.core, "drawwmf"): # install default handler (windows only) - class WmfHandler(object): - + class WmfHandler: def open(self, im): im.mode = "RGB" self.bbox = im.info["wmf_bbox"] @@ -54,10 +48,14 @@ def open(self, im): def load(self, im): im.fp.seek(0) # rewind return Image.frombytes( - "RGB", im.size, + "RGB", + im.size, Image.core.drawwmf(im.fp.read(), im.size, self.bbox), - "raw", "BGR", (im.size[0]*3 + 3) & -4, -1 - ) + "raw", + "BGR", + (im.size[0] * 3 + 3) & -4, + -1, + ) register_handler(WmfHandler()) @@ -68,20 +66,21 @@ def load(self, im): def _accept(prefix): return ( - prefix[:6] == b"\xd7\xcd\xc6\x9a\x00\x00" or - prefix[:4] == b"\x01\x00\x00\x00" - ) + prefix[:6] == b"\xd7\xcd\xc6\x9a\x00\x00" or prefix[:4] == b"\x01\x00\x00\x00" + ) ## # Image plugin for Windows metafiles. + class WmfStubImageFile(ImageFile.StubImageFile): format = "WMF" format_description = "Windows Metafile" def _open(self): + self._inch = None # check placable header s = self.fp.read(80) @@ -91,7 +90,7 @@ def _open(self): # placeable windows metafile # get units per inch - inch = word(s, 14) + self._inch = word(s, 14) # get bounding box x0 = short(s, 6) @@ -100,19 +99,19 @@ def _open(self): y1 = short(s, 12) # normalize size to 72 dots per inch - size = (x1 - x0) * 72 // inch, (y1 - y0) * 72 // inch - - self.info["wmf_bbox"] = x0, y0, x1, y1 - self.info["dpi"] = 72 + size = ( + (x1 - x0) * self.info["dpi"] // self._inch, + (y1 - y0) * self.info["dpi"] // self._inch, + ) - # print(self.mode, self.size, self.info) + self.info["wmf_bbox"] = x0, y0, x1, y1 # sanity check (standard metafile header) if s[22:26] != b"\x01\x00\t\x00": raise SyntaxError("Unsupported WMF file format") - elif dword(s) == 1 and s[40:44] == b" EMF": + elif s[:4] == b"\x01\x00\x00\x00" and s[40:44] == b" EMF": # enhanced metafile # get bounding box @@ -124,12 +123,11 @@ def _open(self): # get frame (in 0.01 millimeter units) frame = _long(s, 24), _long(s, 28), _long(s, 32), _long(s, 36) - # normalize size to 72 dots per inch size = x1 - x0, y1 - y0 # calculate dots per inch from bbox and frame - xdpi = 2540 * (x1 - y0) // (frame[2] - frame[0]) - ydpi = 2540 * (y1 - y0) // (frame[3] - frame[1]) + xdpi = 2540.0 * (x1 - y0) / (frame[2] - frame[0]) + ydpi = 2540.0 * (y1 - y0) / (frame[3] - frame[1]) self.info["wmf_bbox"] = x0, y0, x1, y1 @@ -142,7 +140,7 @@ def _open(self): raise SyntaxError("Unsupported file format") self.mode = "RGB" - self.size = size + self._size = size loader = self._load() if loader: @@ -151,16 +149,28 @@ def _open(self): def _load(self): return _handler + def load(self, dpi=None): + if dpi is not None and self._inch is not None: + self.info["dpi"] = dpi + x0, y0, x1, y1 = self.info["wmf_bbox"] + self._size = ( + (x1 - x0) * self.info["dpi"] // self._inch, + (y1 - y0) * self.info["dpi"] // self._inch, + ) + return super().load() + def _save(im, fp, filename): if _handler is None or not hasattr(_handler, "save"): - raise IOError("WMF save handler not installed") + raise OSError("WMF save handler not installed") _handler.save(im, fp, filename) + # # -------------------------------------------------------------------- # Registry stuff + Image.register_open(WmfStubImageFile.format, WmfStubImageFile, _accept) Image.register_save(WmfStubImageFile.format, _save) diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index a7d39ed89d0..4efedb77ea7 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -18,9 +18,7 @@ # from . import Image, ImageFile, ImagePalette -from ._binary import i8, o8 - -__version__ = "0.1" +from ._binary import o8 _MAGIC = b"P7 332" @@ -29,7 +27,9 @@ for r in range(8): for g in range(8): for b in range(4): - PALETTE = PALETTE + (o8((r*255)//7)+o8((g*255)//7)+o8((b*255)//3)) + PALETTE = PALETTE + ( + o8((r * 255) // 7) + o8((g * 255) // 7) + o8((b * 255) // 3) + ) def _accept(prefix): @@ -39,6 +39,7 @@ def _accept(prefix): ## # Image plugin for XV thumbnail images. + class XVThumbImageFile(ImageFile.ImageFile): format = "XVThumb" @@ -58,21 +59,18 @@ def _open(self): s = self.fp.readline() if not s: raise SyntaxError("Unexpected EOF reading XV thumbnail file") - if i8(s[0]) != 35: # ie. when not a comment: '#' + if s[0] != 35: # ie. when not a comment: '#' break # parse header line (already read) s = s.strip().split() self.mode = "P" - self.size = int(s[0]), int(s[1]) + self._size = int(s[0]), int(s[1]) self.palette = ImagePalette.raw("RGB", PALETTE) - self.tile = [ - ("raw", (0, 0)+self.size, - self.fp.tell(), (self.mode, 0, 1) - )] + self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1))] # -------------------------------------------------------------------- diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index b43fbef5006..59acabebae3 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -20,19 +20,18 @@ # import re -from . import Image, ImageFile -__version__ = "0.6" +from . import Image, ImageFile # XBM header xbm_head = re.compile( - br"\s*#define[ \t]+.*_width[ \t]+(?P[0-9]+)[\r\n]+" + rb"\s*#define[ \t]+.*_width[ \t]+(?P[0-9]+)[\r\n]+" b"#define[ \t]+.*_height[ \t]+(?P[0-9]+)[\r\n]+" b"(?P" b"#define[ \t]+[^_]*_x_hot[ \t]+(?P[0-9]+)[\r\n]+" b"#define[ \t]+[^_]*_y_hot[ \t]+(?P[0-9]+)[\r\n]+" b")?" - b"[\\000-\\377]*_bits\\[\\]" + rb"[\000-\377]*_bits\[]" ) @@ -43,6 +42,7 @@ def _accept(prefix): ## # Image plugin for X11 bitmaps. + class XbmImageFile(ImageFile.ImageFile): format = "XBM" @@ -52,38 +52,37 @@ def _open(self): m = xbm_head.match(self.fp.read(512)) - if m: + if not m: + raise SyntaxError("not a XBM file") - xsize = int(m.group("width")) - ysize = int(m.group("height")) + xsize = int(m.group("width")) + ysize = int(m.group("height")) - if m.group("hotspot"): - self.info["hotspot"] = ( - int(m.group("xhot")), int(m.group("yhot")) - ) + if m.group("hotspot"): + self.info["hotspot"] = (int(m.group("xhot")), int(m.group("yhot"))) - self.mode = "1" - self.size = xsize, ysize + self.mode = "1" + self._size = xsize, ysize - self.tile = [("xbm", (0, 0)+self.size, m.end(), None)] + self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] def _save(im, fp, filename): if im.mode != "1": - raise IOError("cannot write mode %s as XBM" % im.mode) + raise OSError(f"cannot write mode {im.mode} as XBM") - fp.write(("#define im_width %d\n" % im.size[0]).encode('ascii')) - fp.write(("#define im_height %d\n" % im.size[1]).encode('ascii')) + fp.write(f"#define im_width {im.size[0]}\n".encode("ascii")) + fp.write(f"#define im_height {im.size[1]}\n".encode("ascii")) hotspot = im.encoderinfo.get("hotspot") if hotspot: - fp.write(("#define im_x_hot %d\n" % hotspot[0]).encode('ascii')) - fp.write(("#define im_y_hot %d\n" % hotspot[1]).encode('ascii')) + fp.write(f"#define im_x_hot {hotspot[0]}\n".encode("ascii")) + fp.write(f"#define im_y_hot {hotspot[1]}\n".encode("ascii")) fp.write(b"static char im_bits[] = {\n") - ImageFile._save(im, fp, [("xbm", (0, 0)+im.size, 0, None)]) + ImageFile._save(im, fp, [("xbm", (0, 0) + im.size, 0, None)]) fp.write(b"};\n") diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index b6e69802d11..aaed2039db4 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -16,13 +16,12 @@ import re -from . import Image, ImageFile, ImagePalette -from ._binary import i8, o8 -__version__ = "0.2" +from . import Image, ImageFile, ImagePalette +from ._binary import o8 # XPM header -xpm_head = re.compile(b"\"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)") +xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)') def _accept(prefix): @@ -32,6 +31,7 @@ def _accept(prefix): ## # Image plugin for X11 pixel maps. + class XpmImageFile(ImageFile.ImageFile): format = "XPM" @@ -51,7 +51,7 @@ def _open(self): if m: break - self.size = int(m.group(1)), int(m.group(2)) + self._size = int(m.group(1)), int(m.group(2)) pal = int(m.group(3)) bpp = int(m.group(4)) @@ -64,15 +64,15 @@ def _open(self): palette = [b"\0\0\0"] * 256 - for i in range(pal): + for _ in range(pal): s = self.fp.readline() - if s[-2:] == b'\r\n': + if s[-2:] == b"\r\n": s = s[:-2] - elif s[-1:] in b'\r\n': + elif s[-1:] in b"\r\n": s = s[:-1] - c = i8(s[1]) + c = s[1] s = s[2:-2].split() for i in range(0, len(s), 2): @@ -80,15 +80,15 @@ def _open(self): if s[i] == b"c": # process colour key - rgb = s[i+1] + rgb = s[i + 1] if rgb == b"None": self.info["transparency"] = c - elif rgb[0:1] == b"#": + elif rgb[:1] == b"#": # FIXME: handle colour names (see ImagePalette.py) rgb = int(rgb[1:], 16) - palette[c] = (o8((rgb >> 16) & 255) + - o8((rgb >> 8) & 255) + - o8(rgb & 255)) + palette[c] = ( + o8((rgb >> 16) & 255) + o8((rgb >> 8) & 255) + o8(rgb & 255) + ) else: # unknown colour raise ValueError("cannot read this XPM file") @@ -102,7 +102,7 @@ def _open(self): self.mode = "P" self.palette = ImagePalette.raw("RGB", b"".join(palette)) - self.tile = [("raw", (0, 0)+self.size, self.fp.tell(), ("P", 0, 1))] + self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] def load_read(self, bytes): @@ -114,13 +114,15 @@ def load_read(self, bytes): s = [None] * ysize for i in range(ysize): - s[i] = self.fp.readline()[1:xsize+1].ljust(xsize) + s[i] = self.fp.readline()[1 : xsize + 1].ljust(xsize) return b"".join(s) + # # Registry + Image.register_open(XpmImageFile.format, XpmImageFile, _accept) Image.register_extension(XpmImageFile.format, ".xpm") diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 5603bb05fbc..e65b155b2dc 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -1,64 +1,80 @@ -# -# The Python Imaging Library. -# $Id$ -# -# package placeholder -# -# Copyright (c) 1999 by Secret Labs AB. -# -# See the README file for information on usage and redistribution. -# +"""Pillow (Fork of the Python Imaging Library) -# ;-) +Pillow is the friendly PIL fork by Alex Clark and Contributors. + https://github.com/python-pillow/Pillow/ -from . import version +Pillow is forked from PIL 1.1.7. -VERSION = '1.1.7' # PIL Version -PILLOW_VERSION = version.__version__ +PIL is the Python Imaging Library by Fredrik Lundh and Contributors. +Copyright (c) 1999 by Secret Labs AB. -__version__ = PILLOW_VERSION +Use PIL.__version__ for this Pillow version. -_plugins = ['BmpImagePlugin', - 'BufrStubImagePlugin', - 'CurImagePlugin', - 'DcxImagePlugin', - 'DdsImagePlugin', - 'EpsImagePlugin', - 'FitsStubImagePlugin', - 'FliImagePlugin', - 'FpxImagePlugin', - 'FtexImagePlugin', - 'GbrImagePlugin', - 'GifImagePlugin', - 'GribStubImagePlugin', - 'Hdf5StubImagePlugin', - 'IcnsImagePlugin', - 'IcoImagePlugin', - 'ImImagePlugin', - 'ImtImagePlugin', - 'IptcImagePlugin', - 'JpegImagePlugin', - 'Jpeg2KImagePlugin', - 'McIdasImagePlugin', - 'MicImagePlugin', - 'MpegImagePlugin', - 'MpoImagePlugin', - 'MspImagePlugin', - 'PalmImagePlugin', - 'PcdImagePlugin', - 'PcxImagePlugin', - 'PdfImagePlugin', - 'PixarImagePlugin', - 'PngImagePlugin', - 'PpmImagePlugin', - 'PsdImagePlugin', - 'SgiImagePlugin', - 'SpiderImagePlugin', - 'SunImagePlugin', - 'TgaImagePlugin', - 'TiffImagePlugin', - 'WebPImagePlugin', - 'WmfImagePlugin', - 'XbmImagePlugin', - 'XpmImagePlugin', - 'XVThumbImagePlugin'] +;-) +""" + +from . import _version + +# VERSION was removed in Pillow 6.0.0. +# PILLOW_VERSION was removed in Pillow 9.0.0. +# Use __version__ instead. +__version__ = _version.__version__ +del _version + + +_plugins = [ + "BlpImagePlugin", + "BmpImagePlugin", + "BufrStubImagePlugin", + "CurImagePlugin", + "DcxImagePlugin", + "DdsImagePlugin", + "EpsImagePlugin", + "FitsImagePlugin", + "FitsStubImagePlugin", + "FliImagePlugin", + "FpxImagePlugin", + "FtexImagePlugin", + "GbrImagePlugin", + "GifImagePlugin", + "GribStubImagePlugin", + "Hdf5StubImagePlugin", + "IcnsImagePlugin", + "IcoImagePlugin", + "ImImagePlugin", + "ImtImagePlugin", + "IptcImagePlugin", + "JpegImagePlugin", + "Jpeg2KImagePlugin", + "McIdasImagePlugin", + "MicImagePlugin", + "MpegImagePlugin", + "MpoImagePlugin", + "MspImagePlugin", + "PalmImagePlugin", + "PcdImagePlugin", + "PcxImagePlugin", + "PdfImagePlugin", + "PixarImagePlugin", + "PngImagePlugin", + "PpmImagePlugin", + "PsdImagePlugin", + "SgiImagePlugin", + "SpiderImagePlugin", + "SunImagePlugin", + "TgaImagePlugin", + "TiffImagePlugin", + "WebPImagePlugin", + "WmfImagePlugin", + "XbmImagePlugin", + "XpmImagePlugin", + "XVThumbImagePlugin", +] + + +class UnidentifiedImageError(OSError): + """ + Raised in :py:meth:`PIL.Image.open` if an image cannot be opened and identified. + """ + + pass diff --git a/src/PIL/__main__.py b/src/PIL/__main__.py new file mode 100644 index 00000000000..a05323f93b6 --- /dev/null +++ b/src/PIL/__main__.py @@ -0,0 +1,3 @@ +from .features import pilinfo + +pilinfo() diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index b15f796c0da..a74ee9eb6f3 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -11,20 +11,19 @@ # See the README file for information on usage and redistribution. # -from struct import unpack, pack -if bytes is str: - def i8(c): - return ord(c) +"""Binary input/output support routines.""" - def o8(i): - return chr(i & 255) -else: - def i8(c): - return c if c.__class__ is int else c[0] - def o8(i): - return bytes((i & 255,)) +from struct import pack, unpack_from + + +def i8(c): + return c if c.__class__ is int else c[0] + + +def o8(i): + return bytes((i & 255,)) # Input, le = little endian, be = big endian @@ -32,48 +31,58 @@ def i16le(c, o=0): """ Converts a 2-bytes (16 bits) string to an unsigned integer. - c: string containing bytes to convert - o: offset of bytes to convert in string + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string """ - return unpack("h", c, o)[0] def i32le(c, o=0): """ Converts a 4-bytes (32 bits) string to an unsigned integer. - c: string containing bytes to convert - o: offset of bytes to convert in string + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string """ - return unpack("H", c[o:o+2])[0] + return unpack_from(">H", c, o)[0] def i32be(c, o=0): - return unpack(">I", c[o:o+4])[0] + return unpack_from(">I", c, o)[0] # Output, le = little endian, be = big endian diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py new file mode 100644 index 00000000000..30a8a897100 --- /dev/null +++ b/src/PIL/_deprecate.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import warnings + +from . import __version__ + + +def deprecate( + deprecated: str, + when: int | None, + replacement: str | None = None, + *, + action: str | None = None, + plural: bool = False, +) -> None: + """ + Deprecations helper. + + :param deprecated: Name of thing to be deprecated. + :param when: Pillow major version to be removed in. + :param replacement: Name of replacement. + :param action: Instead of "replacement", give a custom call to action + e.g. "Upgrade to new thing". + :param plural: if the deprecated thing is plural, needing "are" instead of "is". + + Usually of the form: + + "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd). + Use [replacement] instead." + + You can leave out the replacement sentence: + + "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd)" + + Or with another call to action: + + "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd). + [action]." + """ + + is_ = "are" if plural else "is" + + if when is None: + removed = "a future version" + elif when <= int(__version__.split(".")[0]): + raise RuntimeError(f"{deprecated} {is_} deprecated and should be removed.") + elif when == 10: + removed = "Pillow 10 (2023-07-01)" + else: + raise ValueError(f"Unknown removal version, update {__name__}?") + + if replacement and action: + raise ValueError("Use only one of 'replacement' and 'action'") + + if replacement: + action = f". Use {replacement} instead." + elif action: + action = f". {action.rstrip('.')}." + else: + action = "" + + warnings.warn( + f"{deprecated} {is_} deprecated and will be removed in {removed}{action}", + DeprecationWarning, + stacklevel=3, + ) diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index 21f0caa2fa3..5cd7e9b1fb2 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -1,20 +1,23 @@ """ Find compiled module linking to Tcl / Tk libraries """ import sys +import tkinter +from tkinter import _tkinter as tk -if sys.version_info[0] > 2: - from tkinter import _tkinter as tk -else: - from Tkinter import tkinter as tk +from ._deprecate import deprecate -if hasattr(sys, 'pypy_find_executable'): - # Tested with packages at https://bitbucket.org/pypy/pypy/downloads. - # PyPies 1.6, 2.0 do not have tkinter built in. PyPy3-2.3.1 gives an - # OSError trying to import tkinter. Otherwise: - try: # PyPy 5.1, 4.0.0, 2.6.1, 2.6.0 +try: + if hasattr(sys, "pypy_find_executable"): TKINTER_LIB = tk.tklib_cffi.__file__ - except AttributeError: - # PyPy3 2.4, 2.1-beta1; PyPy 2.5.1, 2.5.0, 2.4.0, 2.3, 2.2, 2.1 - TKINTER_LIB = tk.tkffi.verifier.modulefilename -else: - TKINTER_LIB = tk.__file__ + else: + TKINTER_LIB = tk.__file__ +except AttributeError: + # _tkinter may be compiled directly into Python, in which case __file__ is + # not available. load_tkinter_funcs will check the binary first in any case. + TKINTER_LIB = None + +tk_version = str(tkinter.TkVersion) +if tk_version == "8.4": + deprecate( + "Support for Tk/Tcl 8.4", 10, action="Please upgrade to Tk/Tcl 8.5 or newer" + ) diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 51c6f6887d3..ba27b7e49e9 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,25 +1,17 @@ import os +from pathlib import Path -if bytes is str: - def isStringType(t): - return isinstance(t, basestring) - def isPath(f): - return isinstance(f, basestring) -else: - def isStringType(t): - return isinstance(t, str) +def is_path(f): + return isinstance(f, (bytes, str, Path)) - def isPath(f): - return isinstance(f, (bytes, str)) +def is_directory(f): + """Checks if an object is a string, and that it points to a directory.""" + return is_path(f) and os.path.isdir(f) -# Checks if an object is a string, and that it points to a directory. -def isDirectory(f): - return isPath(f) and os.path.isdir(f) - -class deferred_error(object): +class DeferredError: def __init__(self, ex): self.ex = ex diff --git a/src/PIL/version.py b/src/PIL/_version.py similarity index 56% rename from src/PIL/version.py rename to src/PIL/_version.py index f8f2a9841ff..43896fabd19 100644 --- a/src/PIL/version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = '5.0.0' +__version__ = "9.3.0" diff --git a/src/PIL/features.py b/src/PIL/features.py index 9cbd523c90a..3838568f3a6 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -1,79 +1,320 @@ +import collections +import os +import sys +import warnings + +import PIL + from . import Image modules = { - "pil": "PIL._imaging", - "tkinter": "PIL._tkinter_finder", - "freetype2": "PIL._imagingft", - "littlecms2": "PIL._imagingcms", - "webp": "PIL._webp", + "pil": ("PIL._imaging", "PILLOW_VERSION"), + "tkinter": ("PIL._tkinter_finder", "tk_version"), + "freetype2": ("PIL._imagingft", "freetype2_version"), + "littlecms2": ("PIL._imagingcms", "littlecms_version"), + "webp": ("PIL._webp", "webpdecoder_version"), } + def check_module(feature): + """ + Checks if a module is available. + + :param feature: The module to check for. + :returns: ``True`` if available, ``False`` otherwise. + :raises ValueError: If the module is not defined in this version of Pillow. + """ if not (feature in modules): - raise ValueError("Unknown module %s" % feature) + raise ValueError(f"Unknown module {feature}") - module = modules[feature] + module, ver = modules[feature] try: - imported_module = __import__(module) + __import__(module) return True except ImportError: return False + +def version_module(feature): + """ + :param feature: The module to check for. + :returns: + The loaded version number as a string, or ``None`` if unknown or not available. + :raises ValueError: If the module is not defined in this version of Pillow. + """ + if not check_module(feature): + return None + + module, ver = modules[feature] + + if ver is None: + return None + + return getattr(__import__(module, fromlist=[ver]), ver) + + def get_supported_modules(): + """ + :returns: A list of all supported modules. + """ return [f for f in modules if check_module(f)] + codecs = { - "jpg": "jpeg", - "jpg_2000": "jpeg2k", - "zlib": "zip", - "libtiff": "libtiff" + "jpg": ("jpeg", "jpeglib"), + "jpg_2000": ("jpeg2k", "jp2klib"), + "zlib": ("zip", "zlib"), + "libtiff": ("libtiff", "libtiff"), } + def check_codec(feature): + """ + Checks if a codec is available. + + :param feature: The codec to check for. + :returns: ``True`` if available, ``False`` otherwise. + :raises ValueError: If the codec is not defined in this version of Pillow. + """ if feature not in codecs: - raise ValueError("Unknown codec %s" % feature) + raise ValueError(f"Unknown codec {feature}") - codec = codecs[feature] + codec, lib = codecs[feature] return codec + "_encoder" in dir(Image.core) +def version_codec(feature): + """ + :param feature: The codec to check for. + :returns: + The version number as a string, or ``None`` if not available. + Checked at compile time for ``jpg``, run-time otherwise. + :raises ValueError: If the codec is not defined in this version of Pillow. + """ + if not check_codec(feature): + return None + + codec, lib = codecs[feature] + + version = getattr(Image.core, lib + "_version") + + if feature == "libtiff": + return version.split("\n")[0].split("Version ")[1] + + return version + + def get_supported_codecs(): + """ + :returns: A list of all supported codecs. + """ return [f for f in codecs if check_codec(f)] + features = { - "webp_anim": ("PIL._webp", 'HAVE_WEBPANIM'), - "webp_mux": ("PIL._webp", 'HAVE_WEBPMUX'), - "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"), - "raqm": ("PIL._imagingft", "HAVE_RAQM") + "webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None), + "webp_mux": ("PIL._webp", "HAVE_WEBPMUX", None), + "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY", None), + "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), + "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), + "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), + "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"), + "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"), + "xcb": ("PIL._imaging", "HAVE_XCB", None), } + def check_feature(feature): + """ + Checks if a feature is available. + + :param feature: The feature to check for. + :returns: ``True`` if available, ``False`` if unavailable, ``None`` if unknown. + :raises ValueError: If the feature is not defined in this version of Pillow. + """ if feature not in features: - raise ValueError("Unknown feature %s" % feature) + raise ValueError(f"Unknown feature {feature}") - module, flag = features[feature] + module, flag, ver = features[feature] try: - imported_module = __import__(module, fromlist=['PIL']) + imported_module = __import__(module, fromlist=["PIL"]) return getattr(imported_module, flag) except ImportError: return None +def version_feature(feature): + """ + :param feature: The feature to check for. + :returns: The version number as a string, or ``None`` if not available. + :raises ValueError: If the feature is not defined in this version of Pillow. + """ + if not check_feature(feature): + return None + + module, flag, ver = features[feature] + + if ver is None: + return None + + return getattr(__import__(module, fromlist=[ver]), ver) + + def get_supported_features(): + """ + :returns: A list of all supported features. + """ return [f for f in features if check_feature(f)] def check(feature): - return (feature in modules and check_module(feature) or \ - feature in codecs and check_codec(feature) or \ - feature in features and check_feature(feature)) + """ + :param feature: A module, codec, or feature name. + :returns: + ``True`` if the module, codec, or feature is available, + ``False`` or ``None`` otherwise. + """ + + if feature in modules: + return check_module(feature) + if feature in codecs: + return check_codec(feature) + if feature in features: + return check_feature(feature) + warnings.warn(f"Unknown feature '{feature}'.", stacklevel=2) + return False + + +def version(feature): + """ + :param feature: + The module, codec, or feature to check for. + :returns: + The version number as a string, or ``None`` if unknown or not available. + """ + if feature in modules: + return version_module(feature) + if feature in codecs: + return version_codec(feature) + if feature in features: + return version_feature(feature) + return None + def get_supported(): + """ + :returns: A list of all supported modules, features, and codecs. + """ + ret = get_supported_modules() ret.extend(get_supported_features()) ret.extend(get_supported_codecs()) return ret + +def pilinfo(out=None, supported_formats=True): + """ + Prints information about this installation of Pillow. + This function can be called with ``python3 -m PIL``. + + :param out: + The output stream to print to. Defaults to ``sys.stdout`` if ``None``. + :param supported_formats: + If ``True``, a list of all supported image file formats will be printed. + """ + + if out is None: + out = sys.stdout + + Image.init() + + print("-" * 68, file=out) + print(f"Pillow {PIL.__version__}", file=out) + py_version = sys.version.splitlines() + print(f"Python {py_version[0].strip()}", file=out) + for py_version in py_version[1:]: + print(f" {py_version.strip()}", file=out) + print("-" * 68, file=out) + print( + f"Python modules loaded from {os.path.dirname(Image.__file__)}", + file=out, + ) + print( + f"Binary modules loaded from {os.path.dirname(Image.core.__file__)}", + file=out, + ) + print("-" * 68, file=out) + + for name, feature in [ + ("pil", "PIL CORE"), + ("tkinter", "TKINTER"), + ("freetype2", "FREETYPE2"), + ("littlecms2", "LITTLECMS2"), + ("webp", "WEBP"), + ("transp_webp", "WEBP Transparency"), + ("webp_mux", "WEBPMUX"), + ("webp_anim", "WEBP Animation"), + ("jpg", "JPEG"), + ("jpg_2000", "OPENJPEG (JPEG2000)"), + ("zlib", "ZLIB (PNG/ZIP)"), + ("libtiff", "LIBTIFF"), + ("raqm", "RAQM (Bidirectional Text)"), + ("libimagequant", "LIBIMAGEQUANT (Quantization method)"), + ("xcb", "XCB (X protocol)"), + ]: + if check(name): + if name == "jpg" and check_feature("libjpeg_turbo"): + v = "libjpeg-turbo " + version_feature("libjpeg_turbo") + else: + v = version(name) + if v is not None: + version_static = name in ("pil", "jpg") + if name == "littlecms2": + # this check is also in src/_imagingcms.c:setup_module() + version_static = tuple(int(x) for x in v.split(".")) < (2, 7) + t = "compiled for" if version_static else "loaded" + if name == "raqm": + for f in ("fribidi", "harfbuzz"): + v2 = version_feature(f) + if v2 is not None: + v += f", {f} {v2}" + print("---", feature, "support ok,", t, v, file=out) + else: + print("---", feature, "support ok", file=out) + else: + print("***", feature, "support not installed", file=out) + print("-" * 68, file=out) + + if supported_formats: + extensions = collections.defaultdict(list) + for ext, i in Image.EXTENSION.items(): + extensions[i].append(ext) + + for i in sorted(Image.ID): + line = f"{i}" + if i in Image.MIME: + line = f"{line} {Image.MIME[i]}" + print(line, file=out) + + if i in extensions: + print( + "Extensions: {}".format(", ".join(sorted(extensions[i]))), file=out + ) + + features = [] + if i in Image.OPEN: + features.append("open") + if i in Image.SAVE: + features.append("save") + if i in Image.SAVE_ALL: + features.append("save_all") + if i in Image.DECODERS: + features.append("decode") + if i in Image.ENCODERS: + features.append("encode") + + print("Features: {}".format(", ".join(features)), file=out) + print("-" * 68, file=out) diff --git a/Tk/_tkmini.h b/src/Tk/_tkmini.h similarity index 78% rename from Tk/_tkmini.h rename to src/Tk/_tkmini.h index adc470532ef..9852fc9d688 100644 --- a/Tk/_tkmini.h +++ b/src/Tk/_tkmini.h @@ -1,7 +1,7 @@ /* Small excerpts from the Tcl / Tk 8.6 headers * * License terms copied from: - * http://www.tcl.tk/software/tcltk/license.html + * https://www.tcl.tk/software/tcltk/license.html * as of 20 May 2016. * * Copyright (c) 1987-1994 The Regents of the University of California. @@ -79,18 +79,20 @@ typedef struct Tcl_Interp Tcl_Interp; typedef struct Tcl_Command_ *Tcl_Command; typedef void *ClientData; -typedef int (Tcl_CmdProc) (ClientData clientData, Tcl_Interp - *interp, int argc, const char *argv[]); -typedef void (Tcl_CmdDeleteProc) (ClientData clientData); +typedef int(Tcl_CmdProc)( + ClientData clientData, Tcl_Interp *interp, int argc, const char *argv[]); +typedef void(Tcl_CmdDeleteProc)(ClientData clientData); /* Typedefs derived from function signatures in Tcl header */ /* Tcl_CreateCommand */ -typedef Tcl_Command (*Tcl_CreateCommand_t)(Tcl_Interp *interp, - const char *cmdName, Tcl_CmdProc *proc, - ClientData clientData, - Tcl_CmdDeleteProc *deleteProc); +typedef Tcl_Command (*Tcl_CreateCommand_t)( + Tcl_Interp *interp, + const char *cmdName, + Tcl_CmdProc *proc, + ClientData clientData, + Tcl_CmdDeleteProc *deleteProc); /* Tcl_AppendResult */ -typedef void (*Tcl_AppendResult_t) (Tcl_Interp *interp, ...); +typedef void (*Tcl_AppendResult_t)(Tcl_Interp *interp, ...); /* Tk header excerpts */ @@ -107,8 +109,7 @@ typedef struct Tk_Window_ *Tk_Window; typedef void *Tk_PhotoHandle; -typedef struct Tk_PhotoImageBlock -{ +typedef struct Tk_PhotoImageBlock { unsigned char *pixelPtr; int width; int height; @@ -119,23 +120,30 @@ typedef struct Tk_PhotoImageBlock /* Typedefs derived from function signatures in Tk header */ /* Tk_PhotoPutBlock for Tk <= 8.4 */ -typedef void (*Tk_PhotoPutBlock_84_t) (Tk_PhotoHandle handle, - Tk_PhotoImageBlock *blockPtr, int x, int y, - int width, int height, int compRule); +typedef void (*Tk_PhotoPutBlock_84_t)( + Tk_PhotoHandle handle, + Tk_PhotoImageBlock *blockPtr, + int x, + int y, + int width, + int height, + int compRule); /* Tk_PhotoPutBlock for Tk >= 8.5 */ -typedef int (*Tk_PhotoPutBlock_85_t) (Tcl_Interp * interp, - Tk_PhotoHandle handle, - Tk_PhotoImageBlock * blockPtr, int x, int y, - int width, int height, int compRule); +typedef int (*Tk_PhotoPutBlock_85_t)( + Tcl_Interp *interp, + Tk_PhotoHandle handle, + Tk_PhotoImageBlock *blockPtr, + int x, + int y, + int width, + int height, + int compRule); /* Tk_PhotoSetSize for Tk <= 8.4 */ -typedef void (*Tk_PhotoSetSize_84_t) (Tk_PhotoHandle handle, - int width, int height); +typedef void (*Tk_PhotoSetSize_84_t)(Tk_PhotoHandle handle, int width, int height); /* Tk_FindPhoto */ -typedef Tk_PhotoHandle (*Tk_FindPhoto_t) (Tcl_Interp *interp, - const char *imageName); +typedef Tk_PhotoHandle (*Tk_FindPhoto_t)(Tcl_Interp *interp, const char *imageName); /* Tk_PhotoGetImage */ -typedef int (*Tk_PhotoGetImage_t) (Tk_PhotoHandle handle, - Tk_PhotoImageBlock * blockPtr); +typedef int (*Tk_PhotoGetImage_t)(Tk_PhotoHandle handle, Tk_PhotoImageBlock *blockPtr); /* * end block for C++ diff --git a/Tk/tkImaging.c b/src/Tk/tkImaging.c similarity index 61% rename from Tk/tkImaging.c rename to src/Tk/tkImaging.c index f448be16637..16b9a2eddb5 100644 --- a/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -16,7 +16,7 @@ * following lines to your Tcl_AppInit function (in tkappinit.c) * instead. Put them after the calls to Tcl_Init and Tk_Init: * - * { + * { * extern void TkImaging_Init(Tcl_Interp* interp); * TkImaging_Init(interp); * } @@ -29,7 +29,7 @@ * 1995-09-12 fl Created * 1996-04-08 fl Ready for release * 1997-05-09 fl Use command instead of image type - * 2001-03-18 fl Initialize alpha layer pointer (struct changed in 8.3) + * 2001-03-18 fl Initialize alpha layer pointer (struct changed in Tk 8.3) * 2003-04-23 fl Fixed building for Tk 8.4.1 and later (Jack Jansen) * 2004-06-24 fl Fixed building for Tk 8.4.6 and later. * @@ -39,7 +39,7 @@ * See the README file for information on usage and redistribution. */ -#include "Imaging.h" +#include "../libImaging/Imaging.h" #include "_tkmini.h" #include @@ -58,162 +58,159 @@ static Tk_PhotoSetSize_84_t TK_PHOTO_SET_SIZE_84; static Tk_PhotoPutBlock_85_t TK_PHOTO_PUT_BLOCK_85; static Imaging -ImagingFind(const char* name) -{ +ImagingFind(const char *name) { Py_ssize_t id; /* FIXME: use CObject instead? */ -#if defined(_MSC_VER) && defined(_WIN64) +#if defined(_WIN64) id = _atoi64(name); #else id = atol(name); #endif - if (!id) - return NULL; + if (!id) { + return NULL; + } - return (Imaging) id; + return (Imaging)id; } - static int -PyImagingPhotoPut(ClientData clientdata, Tcl_Interp* interp, - int argc, const char **argv) -{ +PyImagingPhotoPut( + ClientData clientdata, Tcl_Interp *interp, int argc, const char **argv) { Imaging im; Tk_PhotoHandle photo; Tk_PhotoImageBlock block; if (argc != 3) { - TCL_APPEND_RESULT(interp, "usage: ", argv[0], - " destPhoto srcImage", (char *) NULL); + TCL_APPEND_RESULT( + interp, "usage: ", argv[0], " destPhoto srcImage", (char *)NULL); return TCL_ERROR; } /* get Tcl PhotoImage handle */ photo = TK_FIND_PHOTO(interp, argv[1]); if (photo == NULL) { - TCL_APPEND_RESULT( - interp, "destination photo must exist", (char *) NULL - ); + TCL_APPEND_RESULT(interp, "destination photo must exist", (char *)NULL); return TCL_ERROR; } /* get PIL Image handle */ im = ImagingFind(argv[2]); if (!im) { - TCL_APPEND_RESULT(interp, "bad name", (char*) NULL); + TCL_APPEND_RESULT(interp, "bad name", (char *)NULL); return TCL_ERROR; } if (!im->block) { - TCL_APPEND_RESULT(interp, "bad display memory", (char*) NULL); + TCL_APPEND_RESULT(interp, "bad display memory", (char *)NULL); return TCL_ERROR; } - /* Active region */ -#if 0 - if (src_xoffset + xsize > im->xsize) - xsize = im->xsize - src_xoffset; - if (src_yoffset + ysize > im->ysize) - ysize = im->ysize - src_yoffset; - if (xsize < 0 || ysize < 0 - || src_xoffset >= im->xsize - || src_yoffset >= im->ysize) - return TCL_OK; -#endif - /* Mode */ if (strcmp(im->mode, "1") == 0 || strcmp(im->mode, "L") == 0) { - block.pixelSize = 1; - block.offset[0] = block.offset[1] = block.offset[2] = 0; + block.pixelSize = 1; + block.offset[0] = block.offset[1] = block.offset[2] = block.offset[3] = 0; } else if (strncmp(im->mode, "RGB", 3) == 0) { - block.pixelSize = 4; - block.offset[0] = 0; - block.offset[1] = 1; - block.offset[2] = 2; - if (strcmp(im->mode, "RGBA") == 0) - block.offset[3] = 3; /* alpha (or reserved, under 8.2) */ - else + block.pixelSize = 4; + block.offset[0] = 0; + block.offset[1] = 1; + block.offset[2] = 2; + if (strcmp(im->mode, "RGBA") == 0) { + block.offset[3] = 3; /* alpha (or reserved, under Tk 8.2) */ + } else { block.offset[3] = 0; /* no alpha */ + } } else { - TCL_APPEND_RESULT(interp, "Bad mode", (char*) NULL); - return TCL_ERROR; + TCL_APPEND_RESULT(interp, "Bad mode", (char *)NULL); + return TCL_ERROR; } block.width = im->xsize; block.height = im->ysize; block.pitch = im->linesize; - block.pixelPtr = (unsigned char*) im->block; -#if 0 - block.pixelPtr = (unsigned char*) im->block + - src_yoffset * im->linesize + - src_xoffset * im->pixelsize; -#endif + block.pixelPtr = (unsigned char *)im->block; if (TK_LT_85) { /* Tk 8.4 */ - TK_PHOTO_PUT_BLOCK_84(photo, &block, 0, 0, block.width, block.height, - TK_PHOTO_COMPOSITE_SET); - if (strcmp(im->mode, "RGBA") == 0) + TK_PHOTO_PUT_BLOCK_84( + photo, &block, 0, 0, block.width, block.height, TK_PHOTO_COMPOSITE_SET); + if (strcmp(im->mode, "RGBA") == 0) { /* Tk workaround: we need apply ToggleComplexAlphaIfNeeded */ /* (fixed in Tk 8.5a3) */ TK_PHOTO_SET_SIZE_84(photo, block.width, block.height); + } } else { /* Tk >=8.5 */ - TK_PHOTO_PUT_BLOCK_85(interp, photo, &block, 0, 0, block.width, - block.height, TK_PHOTO_COMPOSITE_SET); + TK_PHOTO_PUT_BLOCK_85( + interp, + photo, + &block, + 0, + 0, + block.width, + block.height, + TK_PHOTO_COMPOSITE_SET); } return TCL_OK; } -/* Warning -- this does not work at all */ static int -PyImagingPhotoGet(ClientData clientdata, Tcl_Interp* interp, - int argc, const char **argv) -{ +PyImagingPhotoGet( + ClientData clientdata, Tcl_Interp *interp, int argc, const char **argv) { + Imaging im; Tk_PhotoHandle photo; Tk_PhotoImageBlock block; + int x, y, z; - if (argc != 2) { - TCL_APPEND_RESULT(interp, "usage: ", argv[0], - " srcPhoto", (char *) NULL); + if (argc != 3) { + TCL_APPEND_RESULT( + interp, "usage: ", argv[0], " srcPhoto destImage", (char *)NULL); return TCL_ERROR; } /* get Tcl PhotoImage handle */ photo = TK_FIND_PHOTO(interp, argv[1]); if (photo == NULL) { - TCL_APPEND_RESULT( - interp, "source photo must exist", (char *) NULL - ); + TCL_APPEND_RESULT(interp, "source photo must exist", (char *)NULL); return TCL_ERROR; } - TK_PHOTO_GET_IMAGE(photo, &block); + /* get PIL Image handle */ + im = ImagingFind(argv[2]); + if (!im) { + TCL_APPEND_RESULT(interp, "bad name", (char *)NULL); + return TCL_ERROR; + } - printf("pixelPtr = %p\n", block.pixelPtr); - printf("width = %d\n", block.width); - printf("height = %d\n", block.height); - printf("pitch = %d\n", block.pitch); - printf("pixelSize = %d\n", block.pixelSize); - printf("offset = %d %d %d %d\n", block.offset[0], block.offset[1], - block.offset[2], block.offset[3]); + TK_PHOTO_GET_IMAGE(photo, &block); - TCL_APPEND_RESULT( - interp, "this function is not yet supported", (char *) NULL - ); + for (y = 0; y < block.height; y++) { + UINT8 *out = (UINT8 *)im->image32[y]; + for (x = 0; x < block.pitch; x += block.pixelSize) { + for (z = 0; z < block.pixelSize; z++) { + int offset = block.offset[z]; + out[x + offset] = block.pixelPtr[y * block.pitch + x + offset]; + } + } + } - return TCL_ERROR; + return TCL_OK; } - void -TkImaging_Init(Tcl_Interp* interp) -{ - TCL_CREATE_COMMAND(interp, "PyImagingPhoto", PyImagingPhotoPut, - (ClientData) 0, (Tcl_CmdDeleteProc*) NULL); - TCL_CREATE_COMMAND(interp, "PyImagingPhotoGet", PyImagingPhotoGet, - (ClientData) 0, (Tcl_CmdDeleteProc*) NULL); +TkImaging_Init(Tcl_Interp *interp) { + TCL_CREATE_COMMAND( + interp, + "PyImagingPhoto", + PyImagingPhotoPut, + (ClientData)0, + (Tcl_CmdDeleteProc *)NULL); + TCL_CREATE_COMMAND( + interp, + "PyImagingPhotoGet", + PyImagingPhotoGet, + (ClientData)0, + (Tcl_CmdDeleteProc *)NULL); } /* @@ -222,7 +219,7 @@ TkImaging_Init(Tcl_Interp* interp) #define TKINTER_FINDER "PIL._tkinter_finder" -#if defined(_WIN32) || defined(__WIN32__) || defined(WIN32) +#if defined(_WIN32) || defined(__WIN32__) || defined(WIN32) || defined(__CYGWIN__) /* * On Windows, we can't load the tkinter module to get the Tcl or Tk symbols, @@ -236,19 +233,15 @@ TkImaging_Init(Tcl_Interp* interp) #include /* Must be linked with 'psapi' library */ -#if PY_VERSION_HEX >= 0x03000000 #define TKINTER_PKG "tkinter" -#else -#define TKINTER_PKG "Tkinter" -#endif -FARPROC _dfunc(HMODULE lib_handle, const char *func_name) -{ +FARPROC +_dfunc(HMODULE lib_handle, const char *func_name) { /* * Load function `func_name` from `lib_handle`. * Set Python exception if we can't find `func_name` in `lib_handle`. * Returns function pointer or NULL if not present. - */ + */ char message[100]; @@ -260,24 +253,26 @@ FARPROC _dfunc(HMODULE lib_handle, const char *func_name) return func; } -int get_tcl(HMODULE hMod) -{ +int +get_tcl(HMODULE hMod) { /* * Try to fill Tcl global vars with function pointers. Return 0 for no * functions found, 1 for all functions found, -1 for some but not all * functions found. */ - if ((TCL_CREATE_COMMAND = (Tcl_CreateCommand_t) - GetProcAddress(hMod, "Tcl_CreateCommand")) == NULL) { + if ((TCL_CREATE_COMMAND = + (Tcl_CreateCommand_t)GetProcAddress(hMod, "Tcl_CreateCommand")) == NULL) { return 0; /* Maybe not Tcl module */ } - return ((TCL_APPEND_RESULT = (Tcl_AppendResult_t) _dfunc(hMod, - "Tcl_AppendResult")) == NULL) ? -1 : 1; + return ((TCL_APPEND_RESULT = + (Tcl_AppendResult_t)_dfunc(hMod, "Tcl_AppendResult")) == NULL) + ? -1 + : 1; } -int get_tk(HMODULE hMod) -{ +int +get_tk(HMODULE hMod) { /* * Try to fill Tk global vars with function pointers. Return 0 for no * functions found, 1 for all functions found, -1 for some but not all @@ -285,26 +280,31 @@ int get_tk(HMODULE hMod) */ FARPROC func = GetProcAddress(hMod, "Tk_PhotoPutBlock"); - if (func == NULL) { /* Maybe not Tk module */ + if (func == NULL) { /* Maybe not Tk module */ return 0; } - if ((TK_PHOTO_GET_IMAGE = (Tk_PhotoGetImage_t) - _dfunc(hMod, "Tk_PhotoGetImage")) == NULL) { return -1; }; - if ((TK_FIND_PHOTO = (Tk_FindPhoto_t) - _dfunc(hMod, "Tk_FindPhoto")) == NULL) { return -1; }; + if ((TK_PHOTO_GET_IMAGE = (Tk_PhotoGetImage_t)_dfunc(hMod, "Tk_PhotoGetImage")) == + NULL) { + return -1; + }; + if ((TK_FIND_PHOTO = (Tk_FindPhoto_t)_dfunc(hMod, "Tk_FindPhoto")) == NULL) { + return -1; + }; TK_LT_85 = GetProcAddress(hMod, "Tk_PhotoPutBlock_Panic") == NULL; /* Tk_PhotoPutBlock_Panic defined as of 8.5.0 */ if (TK_LT_85) { - TK_PHOTO_PUT_BLOCK_84 = (Tk_PhotoPutBlock_84_t) func; - return ((TK_PHOTO_SET_SIZE_84 = (Tk_PhotoSetSize_84_t) - _dfunc(hMod, "Tk_PhotoSetSize")) == NULL) ? -1 : 1; + TK_PHOTO_PUT_BLOCK_84 = (Tk_PhotoPutBlock_84_t)func; + return ((TK_PHOTO_SET_SIZE_84 = + (Tk_PhotoSetSize_84_t)_dfunc(hMod, "Tk_PhotoSetSize")) == NULL) + ? -1 + : 1; } - TK_PHOTO_PUT_BLOCK_85 = (Tk_PhotoPutBlock_85_t) func; + TK_PHOTO_PUT_BLOCK_85 = (Tk_PhotoPutBlock_85_t)func; return 1; } -int load_tkinter_funcs(void) -{ +int +load_tkinter_funcs(void) { /* * Load Tcl and Tk functions by searching all modules in current process. * Return 0 for success, non-zero for failure. @@ -356,7 +356,7 @@ int load_tkinter_funcs(void) return 1; } -#else /* not Windows */ +#else /* not Windows */ /* * On Unix, we can get the Tcl and Tk symbols from the tkinter module, because @@ -364,32 +364,17 @@ int load_tkinter_funcs(void) * tkinter dynamic library (module). */ -/* From module __file__ attribute to char *string for dlopen. */ -#if PY_VERSION_HEX >= 0x03000000 -char *fname2char(PyObject *fname) -{ - PyObject* bytes; - bytes = PyUnicode_EncodeFSDefault(fname); - if (bytes == NULL) { - return NULL; - } - return PyBytes_AsString(bytes); -} -#else -#define fname2char(s) (PyString_AsString(s)) -#endif - #include -void *_dfunc(void *lib_handle, const char *func_name) -{ +void * +_dfunc(void *lib_handle, const char *func_name) { /* * Load function `func_name` from `lib_handle`. * Set Python exception if we can't find `func_name` in `lib_handle`. * Returns function pointer or NULL if not present. */ - void* func; + void *func; /* Reset errors. */ dlerror(); func = dlsym(lib_handle, func_name); @@ -400,35 +385,44 @@ void *_dfunc(void *lib_handle, const char *func_name) return func; } -int _func_loader(void *lib) -{ +int +_func_loader(void *lib) { /* * Fill global function pointers from dynamic lib. * Return 1 if any pointer is NULL, 0 otherwise. */ - if ((TCL_CREATE_COMMAND = (Tcl_CreateCommand_t) - _dfunc(lib, "Tcl_CreateCommand")) == NULL) { return 1; } - if ((TCL_APPEND_RESULT = (Tcl_AppendResult_t) _dfunc(lib, - "Tcl_AppendResult")) == NULL) { return 1; } - if ((TK_PHOTO_GET_IMAGE = (Tk_PhotoGetImage_t) - _dfunc(lib, "Tk_PhotoGetImage")) == NULL) { return 1; } - if ((TK_FIND_PHOTO = (Tk_FindPhoto_t) - _dfunc(lib, "Tk_FindPhoto")) == NULL) { return 1; } + if ((TCL_CREATE_COMMAND = (Tcl_CreateCommand_t)_dfunc(lib, "Tcl_CreateCommand")) == + NULL) { + return 1; + } + if ((TCL_APPEND_RESULT = (Tcl_AppendResult_t)_dfunc(lib, "Tcl_AppendResult")) == + NULL) { + return 1; + } + if ((TK_PHOTO_GET_IMAGE = (Tk_PhotoGetImage_t)_dfunc(lib, "Tk_PhotoGetImage")) == + NULL) { + return 1; + } + if ((TK_FIND_PHOTO = (Tk_FindPhoto_t)_dfunc(lib, "Tk_FindPhoto")) == NULL) { + return 1; + } /* Tk_PhotoPutBlock_Panic defined as of 8.5.0 */ TK_LT_85 = (dlsym(lib, "Tk_PhotoPutBlock_Panic") == NULL); if (TK_LT_85) { - return (((TK_PHOTO_PUT_BLOCK_84 = (Tk_PhotoPutBlock_84_t) - _dfunc(lib, "Tk_PhotoPutBlock")) == NULL) || - ((TK_PHOTO_SET_SIZE_84 = (Tk_PhotoSetSize_84_t) - _dfunc(lib, "Tk_PhotoSetSize")) == NULL)); + return ( + ((TK_PHOTO_PUT_BLOCK_84 = + (Tk_PhotoPutBlock_84_t)_dfunc(lib, "Tk_PhotoPutBlock")) == NULL) || + ((TK_PHOTO_SET_SIZE_84 = + (Tk_PhotoSetSize_84_t)_dfunc(lib, "Tk_PhotoSetSize")) == NULL)); } - return ((TK_PHOTO_PUT_BLOCK_85 = (Tk_PhotoPutBlock_85_t) - _dfunc(lib, "Tk_PhotoPutBlock")) == NULL); + return ( + (TK_PHOTO_PUT_BLOCK_85 = + (Tk_PhotoPutBlock_85_t)_dfunc(lib, "Tk_PhotoPutBlock")) == NULL); } -int load_tkinter_funcs(void) -{ +int +load_tkinter_funcs(void) { /* * Load tkinter global funcs from tkinter compiled module. * Return 0 for success, non-zero for failure. @@ -437,11 +431,12 @@ int 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); if (_func_loader(main_program) == 0) { + dlclose(main_program); return 0; } /* Clear exception triggered when we didn't find symbols above */ @@ -456,22 +451,28 @@ int 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; } tkinter_lib = dlopen(tkinter_libname, RTLD_LAZY); if (tkinter_lib == NULL) { - PyErr_SetString(PyExc_RuntimeError, - "Cannot dlopen tkinter module file"); + PyErr_SetString(PyExc_RuntimeError, "Cannot dlopen tkinter module file"); goto exit; } ret = _func_loader(tkinter_lib); /* dlclose probably safe because tkinter has been imported. */ dlclose(tkinter_lib); 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 d90f23477bd..0888188fb20 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -71,44 +71,53 @@ * See the README file for information on usage and redistribution. */ +#define PY_SSIZE_T_CLEAN #include "Python.h" +#ifdef HAVE_LIBJPEG +#include "jconfig.h" +#endif + #ifdef HAVE_LIBZ #include "zlib.h" #endif -#include "Imaging.h" +#ifdef HAVE_LIBTIFF +#ifndef _TIFFIO_ +#include +#endif +#endif + +#include "libImaging/Imaging.h" -#include "py3.h" +#define _USE_MATH_DEFINES +#include /* Configuration stuff. Feel free to undef things you don't need. */ -#define WITH_IMAGECHOPS /* ImageChops support */ -#define WITH_IMAGEDRAW /* ImageDraw support */ -#define WITH_MAPPING /* use memory mapping to read some file formats */ -#define WITH_IMAGEPATH /* ImagePath stuff */ -#define WITH_ARROW /* arrow graphics stuff (experimental) */ -#define WITH_EFFECTS /* special effects */ -#define WITH_QUANTIZE /* quantization support */ -#define WITH_RANKFILTER /* rank filter */ -#define WITH_MODEFILTER /* mode filter */ -#define WITH_THREADING /* "friendly" threading support */ +#define WITH_IMAGECHOPS /* ImageChops support */ +#define WITH_IMAGEDRAW /* ImageDraw support */ +#define WITH_MAPPING /* use memory mapping to read some file formats */ +#define WITH_IMAGEPATH /* ImagePath stuff */ +#define WITH_ARROW /* arrow graphics stuff (experimental) */ +#define WITH_EFFECTS /* special effects */ +#define WITH_QUANTIZE /* quantization support */ +#define WITH_RANKFILTER /* rank filter */ +#define WITH_MODEFILTER /* mode filter */ +#define WITH_THREADING /* "friendly" threading support */ #define WITH_UNSHARPMASK /* Kevin Cazabon's unsharpmask module */ -#undef VERBOSE - -#define CLIP(x) ((x) <= 0 ? 0 : (x) < 256 ? (x) : 255) +#undef VERBOSE -#define B16(p, i) ((((int)p[(i)]) << 8) + p[(i)+1]) -#define L16(p, i) ((((int)p[(i)+1]) << 8) + p[(i)]) -#define S16(v) ((v) < 32768 ? (v) : ((v) - 65536)) +#define B16(p, i) ((((int)p[(i)]) << 8) + p[(i) + 1]) +#define L16(p, i) ((((int)p[(i) + 1]) << 8) + p[(i)]) +#define S16(v) ((v) < 32768 ? (v) : ((v)-65536)) /* -------------------------------------------------------------------- */ /* OBJECT ADMINISTRATION */ /* -------------------------------------------------------------------- */ typedef struct { - PyObject_HEAD - Imaging image; + PyObject_HEAD Imaging image; ImagingAccess access; } ImagingObject; @@ -116,8 +125,7 @@ static PyTypeObject Imaging_Type; #ifdef WITH_IMAGEDRAW -typedef struct -{ +typedef struct { /* to write a character, cut out sxy from glyph data, place at current position plus dxy, and advance by (dx, dy) */ int dx, dy; @@ -126,8 +134,7 @@ typedef struct } Glyph; typedef struct { - PyObject_HEAD - ImagingObject* ref; + PyObject_HEAD ImagingObject *ref; Imaging bitmap; int ysize; int baseline; @@ -137,8 +144,7 @@ typedef struct { static PyTypeObject ImagingFont_Type; typedef struct { - PyObject_HEAD - ImagingObject* image; + PyObject_HEAD ImagingObject *image; UINT8 ink[4]; int blend; } ImagingDrawObject; @@ -148,20 +154,19 @@ static PyTypeObject ImagingDraw_Type; #endif typedef struct { - PyObject_HEAD - ImagingObject* image; + PyObject_HEAD ImagingObject *image; int readonly; } PixelAccessObject; static PyTypeObject PixelAccess_Type; -PyObject* -PyImagingNew(Imaging imOut) -{ - ImagingObject* imagep; +PyObject * +PyImagingNew(Imaging imOut) { + ImagingObject *imagep; - if (!imOut) + if (!imOut) { return NULL; + } imagep = PyObject_New(ImagingObject, &Imaging_Type); if (imagep == NULL) { @@ -176,27 +181,26 @@ PyImagingNew(Imaging imOut) imagep->image = imOut; imagep->access = ImagingAccessNew(imOut); - return (PyObject*) imagep; + return (PyObject *)imagep; } static void -_dealloc(ImagingObject* imagep) -{ - +_dealloc(ImagingObject *imagep) { #ifdef VERBOSE printf("imaging %p deleted\n", imagep); #endif - if (imagep->access) + if (imagep->access) { ImagingAccessDelete(imagep->image, imagep->access); + } ImagingDelete(imagep->image); PyObject_Del(imagep); } #define PyImaging_Check(op) (Py_TYPE(op) == &Imaging_Type) -Imaging PyImaging_AsImaging(PyObject *op) -{ +Imaging +PyImaging_AsImaging(PyObject *op) { if (!PyImaging_Check(op)) { PyErr_BadInternalCall(); return NULL; @@ -205,22 +209,21 @@ Imaging PyImaging_AsImaging(PyObject *op) return ((ImagingObject *)op)->image; } - /* -------------------------------------------------------------------- */ /* THREAD HANDLING */ /* -------------------------------------------------------------------- */ -void ImagingSectionEnter(ImagingSectionCookie* cookie) -{ +void +ImagingSectionEnter(ImagingSectionCookie *cookie) { #ifdef WITH_THREADING - *cookie = (PyThreadState *) PyEval_SaveThread(); + *cookie = (PyThreadState *)PyEval_SaveThread(); #endif } -void ImagingSectionLeave(ImagingSectionCookie* cookie) -{ +void +ImagingSectionLeave(ImagingSectionCookie *cookie) { #ifdef WITH_THREADING - PyEval_RestoreThread((PyThreadState*) *cookie); + PyEval_RestoreThread((PyThreadState *)*cookie); #endif } @@ -229,47 +232,15 @@ void ImagingSectionLeave(ImagingSectionCookie* cookie) /* -------------------------------------------------------------------- */ /* Python compatibility API */ -int PyImaging_CheckBuffer(PyObject* buffer) -{ -#if PY_VERSION_HEX >= 0x03000000 +int +PyImaging_CheckBuffer(PyObject *buffer) { return PyObject_CheckBuffer(buffer); -#else - return PyObject_CheckBuffer(buffer) || PyObject_CheckReadBuffer(buffer); -#endif } -int PyImaging_GetBuffer(PyObject* buffer, Py_buffer *view) -{ +int +PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) { /* must call check_buffer first! */ -#if PY_VERSION_HEX >= 0x03000000 return PyObject_GetBuffer(buffer, view, PyBUF_SIMPLE); -#else - /* Use new buffer protocol if available - (mmap doesn't support this in 2.7, go figure) */ - if (PyObject_CheckBuffer(buffer)) { - int success = PyObject_GetBuffer(buffer, view, PyBUF_SIMPLE); - if (!success) { return success; } - PyErr_Clear(); - } - - /* Pretend we support the new protocol; PyBuffer_Release happily ignores - calling bf_releasebuffer on objects that don't support it */ - view->buf = NULL; - view->len = 0; - view->readonly = 1; - view->format = NULL; - view->ndim = 0; - view->shape = NULL; - view->strides = NULL; - view->suboffsets = NULL; - view->itemsize = 0; - view->internal = NULL; - - Py_INCREF(buffer); - view->obj = buffer; - - return PyObject_AsReadBuffer(buffer, (void *) &view->buf, &view->len); -#endif } /* -------------------------------------------------------------------- */ @@ -277,58 +248,50 @@ int PyImaging_GetBuffer(PyObject* buffer, Py_buffer *view) /* -------------------------------------------------------------------- */ /* error messages */ -static const char* must_be_sequence = "argument must be a sequence"; -static const char* must_be_two_coordinates = - "coordinate list must contain exactly 2 coordinates"; -static const char* wrong_mode = "unrecognized image mode"; -static const char* wrong_raw_mode = "unrecognized raw mode"; -static const char* outside_image = "image index out of range"; -static const char* outside_palette = "palette index out of range"; -static const char* wrong_palette_size = "invalid palette size"; -static const char* no_palette = "image has no palette"; -static const char* readonly = "image is readonly"; +static const char *must_be_sequence = "argument must be a sequence"; +static const char *must_be_two_coordinates = + "coordinate list must contain exactly 2 coordinates"; +static const char *wrong_mode = "unrecognized image mode"; +static const char *wrong_raw_mode = "unrecognized raw mode"; +static const char *outside_image = "image index out of range"; +static const char *outside_palette = "palette index out of range"; +static const char *wrong_palette_size = "invalid palette size"; +static const char *no_palette = "image has no palette"; +static const char *readonly = "image is readonly"; /* static const char* no_content = "image has no content"; */ void * -ImagingError_IOError(void) -{ - PyErr_SetString(PyExc_IOError, "error when accessing file"); +ImagingError_OSError(void) { + PyErr_SetString(PyExc_OSError, "error when accessing file"); return NULL; } void * -ImagingError_MemoryError(void) -{ +ImagingError_MemoryError(void) { return PyErr_NoMemory(); } void * -ImagingError_Mismatch(void) -{ +ImagingError_Mismatch(void) { PyErr_SetString(PyExc_ValueError, "images do not match"); return NULL; } void * -ImagingError_ModeError(void) -{ +ImagingError_ModeError(void) { PyErr_SetString(PyExc_ValueError, "image has wrong mode"); return NULL; } void * -ImagingError_ValueError(const char *message) -{ +ImagingError_ValueError(const char *message) { PyErr_SetString( - PyExc_ValueError, - (message) ? (char*) message : "unrecognized argument value" - ); + PyExc_ValueError, (message) ? (char *)message : "unrecognized argument value"); return NULL; } void -ImagingError_Clear(void) -{ +ImagingError_Clear(void) { PyErr_Clear(); } @@ -337,15 +300,15 @@ ImagingError_Clear(void) /* -------------------------------------------------------------------- */ static int -getbands(const char* mode) -{ +getbands(const char *mode) { Imaging im; int bands; /* FIXME: add primitive to libImaging to avoid extra allocation */ im = ImagingNew(mode, 0, 0); - if (!im) + if (!im) { return -1; + } bands = im->bands; @@ -354,14 +317,14 @@ getbands(const char* mode) return bands; } -#define TYPE_UINT8 (0x100|sizeof(UINT8)) -#define TYPE_INT32 (0x200|sizeof(INT32)) -#define TYPE_FLOAT32 (0x300|sizeof(FLOAT32)) -#define TYPE_DOUBLE (0x400|sizeof(double)) +#define TYPE_UINT8 (0x100 | sizeof(UINT8)) +#define TYPE_INT32 (0x200 | sizeof(INT32)) +#define TYPE_FLOAT16 (0x500 | sizeof(FLOAT16)) +#define TYPE_FLOAT32 (0x300 | sizeof(FLOAT32)) +#define TYPE_DOUBLE (0x400 | sizeof(double)) -static void* -getlist(PyObject* arg, Py_ssize_t* length, const char* wrong_length, int type) -{ +static void * +getlist(PyObject *arg, Py_ssize_t *length, const char *wrong_length, int type) { /* - allocates and returns a c array of the items in the python sequence arg. - the size of the returned array is in length @@ -375,16 +338,17 @@ getlist(PyObject* arg, Py_ssize_t* length, const char* wrong_length, int type) Py_ssize_t i, n; int itemp; double dtemp; - void* list; - PyObject* seq; - PyObject* op; + FLOAT32 ftemp; + UINT8 *list; + PyObject *seq; + PyObject *op; if (!PySequence_Check(arg)) { PyErr_SetString(PyExc_TypeError, must_be_sequence); return NULL; } - n = PyObject_Length(arg); + n = PySequence_Size(arg); if (length && wrong_length && n != *length) { PyErr_SetString(PyExc_ValueError, wrong_length); return NULL; @@ -393,13 +357,13 @@ getlist(PyObject* arg, Py_ssize_t* length, const char* wrong_length, int type) /* malloc check ok, type & ff is just a sizeof(something) calloc checks for overflow */ list = calloc(n, type & 0xff); - if (!list) - return PyErr_NoMemory(); + if (!list) { + return ImagingError_MemoryError(); + } seq = PySequence_Fast(arg, must_be_sequence); if (!seq) { free(list); - PyErr_SetString(PyExc_TypeError, must_be_sequence); return NULL; } @@ -408,44 +372,79 @@ getlist(PyObject* arg, Py_ssize_t* length, const char* wrong_length, int type) // DRY, branch prediction is going to work _really_ well // on this switch. And 3 fewer loops to copy/paste. switch (type) { - case TYPE_UINT8: - itemp = PyInt_AsLong(op); - ((UINT8*)list)[i] = CLIP(itemp); - break; - case TYPE_INT32: - itemp = PyInt_AsLong(op); - ((INT32*)list)[i] = itemp; - break; - case TYPE_FLOAT32: - dtemp = PyFloat_AsDouble(op); - ((FLOAT32*)list)[i] = (FLOAT32) dtemp; - break; - case TYPE_DOUBLE: - dtemp = PyFloat_AsDouble(op); - ((double*)list)[i] = (double) dtemp; - break; + case TYPE_UINT8: + itemp = PyLong_AsLong(op); + list[i] = CLIP8(itemp); + break; + case TYPE_INT32: + itemp = PyLong_AsLong(op); + memcpy(list + i * sizeof(INT32), &itemp, sizeof(itemp)); + break; + case TYPE_FLOAT32: + ftemp = (FLOAT32)PyFloat_AsDouble(op); + memcpy(list + i * sizeof(ftemp), &ftemp, sizeof(ftemp)); + break; + case TYPE_DOUBLE: + dtemp = PyFloat_AsDouble(op); + memcpy(list + i * sizeof(dtemp), &dtemp, sizeof(dtemp)); + break; } } - if (length) - *length = n; - - PyErr_Clear(); Py_DECREF(seq); + if (PyErr_Occurred()) { + free(list); + return NULL; + } + + if (length) { + *length = n; + } + return list; } -static inline PyObject* -getpixel(Imaging im, ImagingAccess access, int x, int y) -{ +FLOAT32 +float16tofloat32(const FLOAT16 in) { + UINT32 t1; + UINT32 t2; + UINT32 t3; + FLOAT32 out[1] = {0}; + + t1 = in & 0x7fff; // Non-sign bits + t2 = in & 0x8000; // Sign bit + t3 = in & 0x7c00; // Exponent + + t1 <<= 13; // Align mantissa on MSB + t2 <<= 16; // Shift sign bit into position + + t1 += 0x38000000; // Adjust bias + + t1 = (t3 == 0 ? 0 : t1); // Denormals-as-zero + + t1 |= t2; // Re-insert sign bit + + memcpy(out, &t1, 4); + return out[0]; +} + +static inline PyObject * +getpixel(Imaging im, ImagingAccess access, int x, int y) { union { - UINT8 b[4]; - UINT16 h; - INT32 i; - FLOAT32 f; + UINT8 b[4]; + UINT16 h; + INT32 i; + FLOAT32 f; } pixel; + if (x < 0) { + x = im->xsize + x; + } + if (y < 0) { + y = im->ysize + y; + } + if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { PyErr_SetString(PyExc_IndexError, outside_image); return NULL; @@ -454,26 +453,28 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) access->get_pixel(im, x, y, &pixel); switch (im->type) { - case IMAGING_TYPE_UINT8: - switch (im->bands) { - case 1: - return PyInt_FromLong(pixel.b[0]); - case 2: - return Py_BuildValue("BB", pixel.b[0], pixel.b[1]); - case 3: - return Py_BuildValue("BBB", pixel.b[0], pixel.b[1], pixel.b[2]); - case 4: - return Py_BuildValue("BBBB", pixel.b[0], pixel.b[1], pixel.b[2], pixel.b[3]); - } - break; - case IMAGING_TYPE_INT32: - return PyInt_FromLong(pixel.i); - case IMAGING_TYPE_FLOAT32: - return PyFloat_FromDouble(pixel.f); - case IMAGING_TYPE_SPECIAL: - if (strncmp(im->mode, "I;16", 4) == 0) - return PyInt_FromLong(pixel.h); - break; + case IMAGING_TYPE_UINT8: + switch (im->bands) { + case 1: + return PyLong_FromLong(pixel.b[0]); + case 2: + return Py_BuildValue("BB", pixel.b[0], pixel.b[1]); + case 3: + return Py_BuildValue("BBB", pixel.b[0], pixel.b[1], pixel.b[2]); + case 4: + return Py_BuildValue( + "BBBB", pixel.b[0], pixel.b[1], pixel.b[2], pixel.b[3]); + } + break; + case IMAGING_TYPE_INT32: + return PyLong_FromLong(pixel.i); + case IMAGING_TYPE_FLOAT32: + return PyFloat_FromDouble(pixel.f); + case IMAGING_TYPE_SPECIAL: + if (strncmp(im->mode, "I;16", 4) == 0) { + return PyLong_FromLong(pixel.h); + } + break; } /* unknown type */ @@ -481,99 +482,114 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) return Py_None; } -static char* -getink(PyObject* color, Imaging im, char* ink) -{ - int g=0, b=0, a=0; - double f=0; +static char * +getink(PyObject *color, Imaging im, char *ink) { + int g = 0, b = 0, a = 0; + double f = 0; /* Windows 64 bit longs are 32 bits, and 0xFFFFFFFF (white) is a python long (not int) that raises an overflow error when trying to return it into a 32 bit C long */ PY_LONG_LONG r = 0; + FLOAT32 ftmp; + INT32 itmp; /* fill ink buffer (four bytes) with something that can be cast to either UINT8 or INT32 */ int rIsInt = 0; - if (im->type == IMAGING_TYPE_UINT8 || - im->type == IMAGING_TYPE_INT32 || + if (PyTuple_Check(color) && PyTuple_GET_SIZE(color) == 1) { + color = PyTuple_GetItem(color, 0); + } + if (im->type == IMAGING_TYPE_UINT8 || im->type == IMAGING_TYPE_INT32 || im->type == IMAGING_TYPE_SPECIAL) { -#if PY_VERSION_HEX >= 0x03000000 - if (PyLong_Check(color)) { - r = PyLong_AsLongLong(color); -#else - if (PyInt_Check(color) || PyLong_Check(color)) { - if (PyInt_Check(color)) - r = PyInt_AS_LONG(color); - else - r = PyLong_AsLongLong(color); -#endif + if (PyLong_Check(color)) { + r = PyLong_AsLongLong(color); + if (r == -1 && PyErr_Occurred()) { + return NULL; + } rIsInt = 1; - } - if (r == -1 && PyErr_Occurred()) { - rIsInt = 0; + } else if (im->type == IMAGING_TYPE_UINT8) { + if (!PyTuple_Check(color)) { + PyErr_SetString(PyExc_TypeError, "color must be int or tuple"); + return NULL; + } + } else { + PyErr_SetString( + PyExc_TypeError, "color must be int or single-element tuple"); + return NULL; } } switch (im->type) { - case IMAGING_TYPE_UINT8: - /* unsigned integer */ - if (im->bands == 1) { - /* unsigned integer, single layer */ - if (rIsInt != 1) { - if (!PyArg_ParseTuple(color, "L", &r)) { - return NULL; + case IMAGING_TYPE_UINT8: + /* unsigned integer */ + if (im->bands == 1) { + /* unsigned integer, single layer */ + if (rIsInt != 1) { + if (PyTuple_GET_SIZE(color) != 1) { + PyErr_SetString(PyExc_TypeError, "color must be int or single-element tuple"); + return NULL; + } else if (!PyArg_ParseTuple(color, "L", &r)) { + return NULL; + } } - } - ink[0] = CLIP(r); - ink[1] = ink[2] = ink[3] = 0; - } else { - a = 255; - if (rIsInt) { - /* compatibility: ABGR */ - a = (UINT8) (r >> 24); - b = (UINT8) (r >> 16); - g = (UINT8) (r >> 8); - r = (UINT8) r; + ink[0] = (char)CLIP8(r); + ink[1] = ink[2] = ink[3] = 0; } else { - if (im->bands == 2) { - if (!PyArg_ParseTuple(color, "L|i", &r, &a)) - return NULL; - g = b = r; + a = 255; + if (rIsInt) { + /* compatibility: ABGR */ + a = (UINT8)(r >> 24); + b = (UINT8)(r >> 16); + g = (UINT8)(r >> 8); + r = (UINT8)r; } else { - if (!PyArg_ParseTuple(color, "Lii|i", &r, &g, &b, &a)) - return NULL; + int tupleSize = PyTuple_GET_SIZE(color); + if (im->bands == 2) { + if (tupleSize != 1 && tupleSize != 2) { + PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or two elements"); + return NULL; + } else if (!PyArg_ParseTuple(color, "L|i", &r, &a)) { + return NULL; + } + g = b = r; + } else { + if (tupleSize != 3 && tupleSize != 4) { + PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one, three or four elements"); + return NULL; + } else if (!PyArg_ParseTuple(color, "Lii|i", &r, &g, &b, &a)) { + return NULL; + } + } } + ink[0] = (char)CLIP8(r); + ink[1] = (char)CLIP8(g); + ink[2] = (char)CLIP8(b); + ink[3] = (char)CLIP8(a); } - ink[0] = CLIP(r); - ink[1] = CLIP(g); - ink[2] = CLIP(b); - ink[3] = CLIP(a); - } - return ink; - case IMAGING_TYPE_INT32: - /* signed integer */ - if (rIsInt != 1) - return NULL; - *(INT32*) ink = r; - return ink; - case IMAGING_TYPE_FLOAT32: - /* floating point */ - f = PyFloat_AsDouble(color); - if (f == -1.0 && PyErr_Occurred()) - return NULL; - *(FLOAT32*) ink = (FLOAT32) f; - return ink; - case IMAGING_TYPE_SPECIAL: - if (strncmp(im->mode, "I;16", 4) == 0) { - if (rIsInt != 1) + return ink; + case IMAGING_TYPE_INT32: + /* signed integer */ + itmp = r; + memcpy(ink, &itmp, sizeof(itmp)); + return ink; + case IMAGING_TYPE_FLOAT32: + /* floating point */ + f = PyFloat_AsDouble(color); + if (f == -1.0 && PyErr_Occurred()) { return NULL; - ink[0] = (UINT8) r; - ink[1] = (UINT8) (r >> 8); - ink[2] = ink[3] = 0; + } + ftmp = f; + memcpy(ink, &ftmp, sizeof(ftmp)); return ink; - } + case IMAGING_TYPE_SPECIAL: + if (strncmp(im->mode, "I;16", 4) == 0) { + ink[0] = (UINT8)r; + ink[1] = (UINT8)(r >> 8); + ink[2] = ink[3] = 0; + return ink; + } } PyErr_SetString(PyExc_ValueError, wrong_mode); @@ -584,24 +600,25 @@ getink(PyObject* color, Imaging im, char* ink) /* FACTORIES */ /* -------------------------------------------------------------------- */ -static PyObject* -_fill(PyObject* self, PyObject* args) -{ - char* mode; +static PyObject * +_fill(PyObject *self, PyObject *args) { + char *mode; int xsize, ysize; - PyObject* color; + PyObject *color; char buffer[4]; Imaging im; xsize = ysize = 256; color = NULL; - if (!PyArg_ParseTuple(args, "s|(ii)O", &mode, &xsize, &ysize, &color)) + if (!PyArg_ParseTuple(args, "s|(ii)O", &mode, &xsize, &ysize, &color)) { return NULL; + } im = ImagingNewDirty(mode, xsize, ysize); - if (!im) + if (!im) { return NULL; + } buffer[0] = buffer[1] = buffer[2] = buffer[3] = 0; if (color) { @@ -611,107 +628,274 @@ _fill(PyObject* self, PyObject* args) } } - - (void) ImagingFill(im, buffer); + (void)ImagingFill(im, buffer); return PyImagingNew(im); } -static PyObject* -_new(PyObject* self, PyObject* args) -{ - char* mode; +static PyObject * +_new(PyObject *self, PyObject *args) { + char *mode; int xsize, ysize; - if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) + if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { return NULL; + } return PyImagingNew(ImagingNew(mode, xsize, ysize)); } -static PyObject* -_new_block(PyObject* self, PyObject* args) -{ - char* mode; +static PyObject * +_new_block(PyObject *self, PyObject *args) { + char *mode; int xsize, ysize; - if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) + if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { return NULL; + } return PyImagingNew(ImagingNewBlock(mode, xsize, ysize)); } -static PyObject* -_linear_gradient(PyObject* self, PyObject* args) -{ - char* mode; +static PyObject * +_linear_gradient(PyObject *self, PyObject *args) { + char *mode; - if (!PyArg_ParseTuple(args, "s", &mode)) + if (!PyArg_ParseTuple(args, "s", &mode)) { return NULL; + } return PyImagingNew(ImagingFillLinearGradient(mode)); } -static PyObject* -_radial_gradient(PyObject* self, PyObject* args) -{ - char* mode; +static PyObject * +_radial_gradient(PyObject *self, PyObject *args) { + char *mode; - if (!PyArg_ParseTuple(args, "s", &mode)) + if (!PyArg_ParseTuple(args, "s", &mode)) { return NULL; + } return PyImagingNew(ImagingFillRadialGradient(mode)); } -static PyObject* -_alpha_composite(ImagingObject* self, PyObject* args) -{ - ImagingObject* imagep1; - ImagingObject* imagep2; +static PyObject * +_alpha_composite(ImagingObject *self, PyObject *args) { + ImagingObject *imagep1; + ImagingObject *imagep2; - if (!PyArg_ParseTuple(args, "O!O!", - &Imaging_Type, &imagep1, - &Imaging_Type, &imagep2)) + if (!PyArg_ParseTuple( + args, "O!O!", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2)) { return NULL; + } return PyImagingNew(ImagingAlphaComposite(imagep1->image, imagep2->image)); } -static PyObject* -_blend(ImagingObject* self, PyObject* args) -{ - ImagingObject* imagep1; - ImagingObject* imagep2; +static PyObject * +_blend(ImagingObject *self, PyObject *args) { + ImagingObject *imagep1; + ImagingObject *imagep2; double alpha; alpha = 0.5; - if (!PyArg_ParseTuple(args, "O!O!|d", - &Imaging_Type, &imagep1, - &Imaging_Type, &imagep2, - &alpha)) + if (!PyArg_ParseTuple( + args, "O!O!|d", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2, &alpha)) { return NULL; + } - return PyImagingNew(ImagingBlend(imagep1->image, imagep2->image, - (float) alpha)); + return PyImagingNew(ImagingBlend(imagep1->image, imagep2->image, (float)alpha)); } /* -------------------------------------------------------------------- */ -/* METHODS */ +/* METHODS */ /* -------------------------------------------------------------------- */ -static PyObject* -_convert(ImagingObject* self, PyObject* args) -{ - char* mode; +static INT16 * +_prepare_lut_table(PyObject *table, Py_ssize_t table_size) { + int i; + Py_buffer buffer_info; + INT32 data_type = TYPE_FLOAT32; + float item = 0; + void *table_data = NULL; + int free_table_data = 0; + INT16 *prepared; + +/* NOTE: This value should be the same as in ColorLUT.c */ +#define PRECISION_BITS (16 - 8 - 2) + + const char *wrong_size = + ("The table should have table_channels * " + "size1D * size2D * size3D float items."); + + if (PyObject_CheckBuffer(table)) { + if (!PyObject_GetBuffer(table, &buffer_info, PyBUF_CONTIG_RO | PyBUF_FORMAT)) { + if (buffer_info.ndim == 1 && buffer_info.shape[0] == table_size) { + if (strlen(buffer_info.format) == 1) { + switch (buffer_info.format[0]) { + case 'e': + data_type = TYPE_FLOAT16; + table_data = buffer_info.buf; + break; + case 'f': + data_type = TYPE_FLOAT32; + table_data = buffer_info.buf; + break; + case 'd': + data_type = TYPE_DOUBLE; + table_data = buffer_info.buf; + break; + } + } + } + PyBuffer_Release(&buffer_info); + } + } + + if (!table_data) { + free_table_data = 1; + table_data = getlist(table, &table_size, wrong_size, TYPE_FLOAT32); + if (!table_data) { + return NULL; + } + } + + /* malloc check ok, max is 2 * 4 * 65**3 = 2197000 */ + prepared = (INT16 *)malloc(sizeof(INT16) * table_size); + if (!prepared) { + if (free_table_data) { + free(table_data); + } + return (INT16 *)ImagingError_MemoryError(); + } + + for (i = 0; i < table_size; i++) { + FLOAT16 htmp; + double dtmp; + switch (data_type) { + case TYPE_FLOAT16: + memcpy(&htmp, ((char *)table_data) + i * sizeof(htmp), sizeof(htmp)); + item = float16tofloat32(htmp); + break; + case TYPE_FLOAT32: + memcpy( + &item, ((char *)table_data) + i * sizeof(FLOAT32), sizeof(FLOAT32)); + break; + case TYPE_DOUBLE: + memcpy(&dtmp, ((char *)table_data) + i * sizeof(dtmp), sizeof(dtmp)); + item = (FLOAT32)dtmp; + break; + } + /* Max value for INT16 */ + if (item >= (0x7fff - 0.5) / (255 << PRECISION_BITS)) { + prepared[i] = 0x7fff; + continue; + } + /* Min value for INT16 */ + if (item <= (-0x8000 + 0.5) / (255 << PRECISION_BITS)) { + prepared[i] = -0x8000; + continue; + } + if (item < 0) { + prepared[i] = item * (255 << PRECISION_BITS) - 0.5; + } else { + prepared[i] = item * (255 << PRECISION_BITS) + 0.5; + } + } + +#undef PRECISION_BITS + if (free_table_data) { + free(table_data); + } + return prepared; +} + +static PyObject * +_color_lut_3d(ImagingObject *self, PyObject *args) { + char *mode; + int filter; + int table_channels; + int size1D, size2D, size3D; + PyObject *table; + + INT16 *prepared_table; + Imaging imOut; + + if (!PyArg_ParseTuple( + args, + "siiiiiO:color_lut_3d", + &mode, + &filter, + &table_channels, + &size1D, + &size2D, + &size3D, + &table)) { + return NULL; + } + + /* actually, it is trilinear */ + if (filter != IMAGING_TRANSFORM_BILINEAR) { + PyErr_SetString(PyExc_ValueError, "Only LINEAR filter is supported."); + return NULL; + } + + if (1 > table_channels || table_channels > 4) { + PyErr_SetString(PyExc_ValueError, "table_channels should be from 1 to 4"); + return NULL; + } + + if (2 > size1D || size1D > 65 || 2 > size2D || size2D > 65 || 2 > size3D || + size3D > 65) { + PyErr_SetString( + PyExc_ValueError, "Table size in any dimension should be from 2 to 65"); + return NULL; + } + + prepared_table = + _prepare_lut_table(table, table_channels * size1D * size2D * size3D); + if (!prepared_table) { + return NULL; + } + + imOut = ImagingNewDirty(mode, self->image->xsize, self->image->ysize); + if (!imOut) { + free(prepared_table); + return NULL; + } + + if (!ImagingColorLUT3D_linear( + imOut, + self->image, + table_channels, + size1D, + size2D, + size3D, + prepared_table)) { + free(prepared_table); + ImagingDelete(imOut); + return NULL; + } + + free(prepared_table); + + return PyImagingNew(imOut); +} + +static PyObject * +_convert(ImagingObject *self, PyObject *args) { + char *mode; int dither = 0; ImagingObject *paletteimage = NULL; - if (!PyArg_ParseTuple(args, "s|iO", &mode, &dither, &paletteimage)) + if (!PyArg_ParseTuple(args, "s|iO", &mode, &dither, &paletteimage)) { return NULL; + } if (paletteimage != NULL) { if (!PyImaging_Check(paletteimage)) { PyObject_Print((PyObject *)paletteimage, stderr, 0); - PyErr_SetString(PyExc_ValueError, "palette argument must be image with mode 'P'"); + PyErr_SetString( + PyExc_ValueError, "palette argument must be image with mode 'P'"); return NULL; } if (paletteimage->image->palette == NULL) { @@ -720,37 +904,49 @@ _convert(ImagingObject* self, PyObject* args) } } - return PyImagingNew(ImagingConvert(self->image, mode, paletteimage ? paletteimage->image->palette : NULL, dither)); + return PyImagingNew(ImagingConvert( + self->image, mode, paletteimage ? paletteimage->image->palette : NULL, dither)); } -static PyObject* -_convert2(ImagingObject* self, PyObject* args) -{ - ImagingObject* imagep1; - ImagingObject* imagep2; - if (!PyArg_ParseTuple(args, "O!O!", - &Imaging_Type, &imagep1, - &Imaging_Type, &imagep2)) +static PyObject * +_convert2(ImagingObject *self, PyObject *args) { + ImagingObject *imagep1; + ImagingObject *imagep2; + if (!PyArg_ParseTuple( + args, "O!O!", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2)) { return NULL; + } - if (!ImagingConvert2(imagep1->image, imagep2->image)) + if (!ImagingConvert2(imagep1->image, imagep2->image)) { return NULL; + } Py_INCREF(Py_None); return Py_None; } -static PyObject* -_convert_matrix(ImagingObject* self, PyObject* args) -{ - char* mode; +static PyObject * +_convert_matrix(ImagingObject *self, PyObject *args) { + char *mode; float m[12]; - if (!PyArg_ParseTuple(args, "s(ffff)", &mode, m+0, m+1, m+2, m+3)) { + if (!PyArg_ParseTuple(args, "s(ffff)", &mode, m + 0, m + 1, m + 2, m + 3)) { PyErr_Clear(); - if (!PyArg_ParseTuple(args, "s(ffffffffffff)", &mode, - m+0, m+1, m+2, m+3, - m+4, m+5, m+6, m+7, - m+8, m+9, m+10, m+11)){ + if (!PyArg_ParseTuple( + args, + "s(ffffffffffff)", + &mode, + m + 0, + m + 1, + m + 2, + m + 3, + m + 4, + m + 5, + m + 6, + m + 7, + m + 8, + m + 9, + m + 10, + m + 11)) { return NULL; } } @@ -758,11 +954,10 @@ _convert_matrix(ImagingObject* self, PyObject* args) return PyImagingNew(ImagingConvertMatrix(self->image, mode, m)); } -static PyObject* -_convert_transparent(ImagingObject* self, PyObject* args) -{ - char* mode; - int r,g,b; +static PyObject * +_convert_transparent(ImagingObject *self, PyObject *args) { + char *mode; + int r, g, b; if (PyArg_ParseTuple(args, "s(iii)", &mode, &r, &g, &b)) { return PyImagingNew(ImagingConvertTransparent(self->image, mode, r, g, b)); } @@ -773,55 +968,56 @@ _convert_transparent(ImagingObject* self, PyObject* args) return NULL; } -static PyObject* -_copy(ImagingObject* self, PyObject* args) -{ - if (!PyArg_ParseTuple(args, "")) +static PyObject * +_copy(ImagingObject *self, PyObject *args) { + if (!PyArg_ParseTuple(args, "")) { return NULL; + } return PyImagingNew(ImagingCopy(self->image)); } -static PyObject* -_crop(ImagingObject* self, PyObject* args) -{ +static PyObject * +_crop(ImagingObject *self, PyObject *args) { int x0, y0, x1, y1; - if (!PyArg_ParseTuple(args, "(iiii)", &x0, &y0, &x1, &y1)) + if (!PyArg_ParseTuple(args, "(iiii)", &x0, &y0, &x1, &y1)) { return NULL; + } return PyImagingNew(ImagingCrop(self->image, x0, y0, x1, y1)); } -static PyObject* -_expand_image(ImagingObject* self, PyObject* args) -{ +static PyObject * +_expand_image(ImagingObject *self, PyObject *args) { int x, y; int mode = 0; - if (!PyArg_ParseTuple(args, "ii|i", &x, &y, &mode)) + if (!PyArg_ParseTuple(args, "ii|i", &x, &y, &mode)) { return NULL; + } return PyImagingNew(ImagingExpand(self->image, x, y, mode)); } -static PyObject* -_filter(ImagingObject* self, PyObject* args) -{ - PyObject* imOut; +static PyObject * +_filter(ImagingObject *self, PyObject *args) { + PyObject *imOut; Py_ssize_t kernelsize; - FLOAT32* kerneldata; + FLOAT32 *kerneldata; int xsize, ysize, i; float divisor, offset; - PyObject* kernel = NULL; - if (!PyArg_ParseTuple(args, "(ii)ffO", &xsize, &ysize, - &divisor, &offset, &kernel)) + PyObject *kernel = NULL; + if (!PyArg_ParseTuple( + args, "(ii)ffO", &xsize, &ysize, &divisor, &offset, &kernel)) { return NULL; + } /* get user-defined kernel */ kerneldata = getlist(kernel, &kernelsize, NULL, TYPE_FLOAT32); - if (!kerneldata) + if (!kerneldata) { return NULL; - if (kernelsize != (Py_ssize_t) xsize * (Py_ssize_t) ysize) { + } + if (kernelsize != (Py_ssize_t)xsize * (Py_ssize_t)ysize) { free(kerneldata); return ImagingError_ValueError("bad kernel size"); } @@ -830,9 +1026,7 @@ _filter(ImagingObject* self, PyObject* args) kerneldata[i] /= divisor; } - imOut = PyImagingNew( - ImagingFilter(self->image, xsize, ysize, kerneldata, offset) - ); + imOut = PyImagingNew(ImagingFilter(self->image, xsize, ysize, kerneldata, offset)); free(kerneldata); @@ -840,41 +1034,44 @@ _filter(ImagingObject* self, PyObject* args) } #ifdef WITH_UNSHARPMASK -static PyObject* -_gaussian_blur(ImagingObject* self, PyObject* args) -{ +static PyObject * +_gaussian_blur(ImagingObject *self, PyObject *args) { Imaging imIn; Imaging imOut; float radius = 0; int passes = 3; - if (!PyArg_ParseTuple(args, "f|i", &radius, &passes)) + if (!PyArg_ParseTuple(args, "f|i", &radius, &passes)) { return NULL; + } imIn = self->image; imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); - if (!imOut) + if (!imOut) { return NULL; + } - if (!ImagingGaussianBlur(imOut, imIn, radius, passes)) + if (!ImagingGaussianBlur(imOut, imIn, radius, passes)) { + ImagingDelete(imOut); return NULL; + } return PyImagingNew(imOut); } #endif -static PyObject* -_getpalette(ImagingObject* self, PyObject* args) -{ - PyObject* palette; - int palettesize = 256; +static PyObject * +_getpalette(ImagingObject *self, PyObject *args) { + PyObject *palette; + int palettesize; int bits; ImagingShuffler pack; - char* mode = "RGB"; - char* rawmode = "RGB"; - if (!PyArg_ParseTuple(args, "|ss", &mode, &rawmode)) + char *mode = "RGB"; + char *rawmode = "RGB"; + if (!PyArg_ParseTuple(args, "|ss", &mode, &rawmode)) { return NULL; + } if (!self->image->palette) { PyErr_SetString(PyExc_ValueError, no_palette); @@ -887,86 +1084,90 @@ _getpalette(ImagingObject* self, PyObject* args) return NULL; } + palettesize = self->image->palette->size; palette = PyBytes_FromStringAndSize(NULL, palettesize * bits / 8); - if (!palette) + if (!palette) { return NULL; + } - pack((UINT8*) PyBytes_AsString(palette), - self->image->palette->palette, palettesize); + pack( + (UINT8 *)PyBytes_AsString(palette), self->image->palette->palette, palettesize); return palette; } -static PyObject* -_getpalettemode(ImagingObject* self, PyObject* args) -{ +static PyObject * +_getpalettemode(ImagingObject *self) { if (!self->image->palette) { - PyErr_SetString(PyExc_ValueError, no_palette); - return NULL; + PyErr_SetString(PyExc_ValueError, no_palette); + return NULL; } return PyUnicode_FromString(self->image->palette->mode); } static inline int -_getxy(PyObject* xy, int* x, int *y) -{ - PyObject* value; +_getxy(PyObject *xy, int *x, int *y) { + PyObject *value; - if (!PyTuple_Check(xy) || PyTuple_GET_SIZE(xy) != 2) + if (!PyTuple_Check(xy) || PyTuple_GET_SIZE(xy) != 2) { goto badarg; + } value = PyTuple_GET_ITEM(xy, 0); - if (PyInt_Check(value)) - *x = PyInt_AS_LONG(value); - else if (PyFloat_Check(value)) - *x = (int) PyFloat_AS_DOUBLE(value); - else - goto badval; + if (PyLong_Check(value)) { + *x = PyLong_AS_LONG(value); + } else if (PyFloat_Check(value)) { + *x = (int)PyFloat_AS_DOUBLE(value); + } else { + PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); + if (int_value != NULL && PyLong_Check(int_value)) { + *x = PyLong_AS_LONG(int_value); + } else { + goto badval; + } + } value = PyTuple_GET_ITEM(xy, 1); - if (PyInt_Check(value)) - *y = PyInt_AS_LONG(value); - else if (PyFloat_Check(value)) - *y = (int) PyFloat_AS_DOUBLE(value); - else - goto badval; + if (PyLong_Check(value)) { + *y = PyLong_AS_LONG(value); + } else if (PyFloat_Check(value)) { + *y = (int)PyFloat_AS_DOUBLE(value); + } else { + PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); + if (int_value != NULL && PyLong_Check(int_value)) { + *y = PyLong_AS_LONG(int_value); + } else { + goto badval; + } + } return 0; - badarg: - PyErr_SetString( - PyExc_TypeError, - "argument must be sequence of length 2" - ); +badarg: + PyErr_SetString(PyExc_TypeError, "argument must be sequence of length 2"); return -1; - badval: - PyErr_SetString( - PyExc_TypeError, - "an integer is required" - ); +badval: + PyErr_SetString(PyExc_TypeError, "an integer is required"); return -1; } -static PyObject* -_getpixel(ImagingObject* self, PyObject* args) -{ - PyObject* xy; +static PyObject * +_getpixel(ImagingObject *self, PyObject *args) { + PyObject *xy; int x, y; if (PyTuple_GET_SIZE(args) != 1) { - PyErr_SetString( - PyExc_TypeError, - "argument 1 must be sequence of length 2" - ); + PyErr_SetString(PyExc_TypeError, "argument 1 must be sequence of length 2"); return NULL; } xy = PyTuple_GET_ITEM(args, 0); - if (_getxy(xy, &x, &y)) + if (_getxy(xy, &x, &y)) { return NULL; + } if (self->access == NULL) { Py_INCREF(Py_None); @@ -976,65 +1177,77 @@ _getpixel(ImagingObject* self, PyObject* args) return getpixel(self->image, self->access, x, y); } -static PyObject* -_histogram(ImagingObject* self, PyObject* args) -{ - ImagingHistogram h; - PyObject* list; - int i; - union { - UINT8 u[2]; - INT32 i[2]; - FLOAT32 f[2]; - } extrema; - void* ep; +union hist_extrema { + UINT8 u[2]; + INT32 i[2]; + FLOAT32 f[2]; +}; + +static union hist_extrema * +parse_histogram_extremap( + ImagingObject *self, PyObject *extremap, union hist_extrema *ep) { int i0, i1; double f0, f1; - PyObject* extremap = NULL; - ImagingObject* maskp = NULL; - if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) - return NULL; - if (extremap) { - ep = &extrema; switch (self->image->type) { - case IMAGING_TYPE_UINT8: - if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) - return NULL; - /* FIXME: clip */ - extrema.u[0] = i0; - extrema.u[1] = i1; - break; - case IMAGING_TYPE_INT32: - if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) - return NULL; - extrema.i[0] = i0; - extrema.i[1] = i1; - break; - case IMAGING_TYPE_FLOAT32: - if (!PyArg_ParseTuple(extremap, "dd", &f0, &f1)) + case IMAGING_TYPE_UINT8: + if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) { + return NULL; + } + ep->u[0] = CLIP8(i0); + ep->u[1] = CLIP8(i1); + break; + case IMAGING_TYPE_INT32: + if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) { + return NULL; + } + ep->i[0] = i0; + ep->i[1] = i1; + break; + case IMAGING_TYPE_FLOAT32: + if (!PyArg_ParseTuple(extremap, "dd", &f0, &f1)) { + return NULL; + } + ep->f[0] = (FLOAT32)f0; + ep->f[1] = (FLOAT32)f1; + break; + default: return NULL; - extrema.f[0] = (FLOAT32) f0; - extrema.f[1] = (FLOAT32) f1; - break; - default: - ep = NULL; - break; } - } else - ep = NULL; + } else { + return NULL; + } + return ep; +} + +static PyObject * +_histogram(ImagingObject *self, PyObject *args) { + ImagingHistogram h; + PyObject *list; + int i; + union hist_extrema extrema; + union hist_extrema *ep; + PyObject *extremap = NULL; + ImagingObject *maskp = NULL; + if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) { + return NULL; + } + + /* Using a var to avoid allocations. */ + ep = parse_histogram_extremap(self, extremap, &extrema); h = ImagingGetHistogram(self->image, (maskp) ? maskp->image : NULL, ep); - if (!h) - return NULL; + if (!h) { + return NULL; + } /* Build an integer list containing the histogram */ list = PyList_New(h->bands * 256); for (i = 0; i < h->bands * 256; i++) { - PyObject* item; - item = PyInt_FromLong(h->histogram[i]); + PyObject *item; + item = PyLong_FromLong(h->histogram[i]); if (item == NULL) { Py_DECREF(list); list = NULL; @@ -1043,142 +1256,198 @@ _histogram(ImagingObject* self, PyObject* args) PyList_SetItem(list, i, item); } + /* Destroy the histogram structure */ ImagingHistogramDelete(h); return list; } +static PyObject * +_entropy(ImagingObject *self, PyObject *args) { + ImagingHistogram h; + int idx, length; + long sum; + double entropy, fsum, p; + union hist_extrema extrema; + union hist_extrema *ep; + + PyObject *extremap = NULL; + ImagingObject *maskp = NULL; + if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) { + return NULL; + } + + /* Using a local var to avoid allocations. */ + ep = parse_histogram_extremap(self, extremap, &extrema); + h = ImagingGetHistogram(self->image, (maskp) ? maskp->image : NULL, ep); + + if (!h) { + return NULL; + } + + /* Calculate the histogram entropy */ + /* First, sum the histogram data */ + length = h->bands * 256; + sum = 0; + for (idx = 0; idx < length; idx++) { + sum += h->histogram[idx]; + } + + /* Next, normalize the histogram data, */ + /* using the histogram sum value */ + fsum = (double)sum; + entropy = 0.0; + for (idx = 0; idx < length; idx++) { + p = (double)h->histogram[idx] / fsum; + if (p != 0.0) { + entropy += p * log(p) * M_LOG2E; + } + } + + /* Destroy the histogram structure */ + ImagingHistogramDelete(h); + + return PyFloat_FromDouble(-entropy); +} + #ifdef WITH_MODEFILTER -static PyObject* -_modefilter(ImagingObject* self, PyObject* args) -{ +static PyObject * +_modefilter(ImagingObject *self, PyObject *args) { int size; - if (!PyArg_ParseTuple(args, "i", &size)) + if (!PyArg_ParseTuple(args, "i", &size)) { return NULL; + } return PyImagingNew(ImagingModeFilter(self->image, size)); } #endif -static PyObject* -_offset(ImagingObject* self, PyObject* args) -{ +static PyObject * +_offset(ImagingObject *self, PyObject *args) { int xoffset, yoffset; - if (!PyArg_ParseTuple(args, "ii", &xoffset, &yoffset)) + if (!PyArg_ParseTuple(args, "ii", &xoffset, &yoffset)) { return NULL; + } return PyImagingNew(ImagingOffset(self->image, xoffset, yoffset)); } -static PyObject* -_paste(ImagingObject* self, PyObject* args) -{ +static PyObject * +_paste(ImagingObject *self, PyObject *args) { int status; char ink[4]; - PyObject* source; + PyObject *source; int x0, y0, x1, y1; - ImagingObject* maskp = NULL; - if (!PyArg_ParseTuple(args, "O(iiii)|O!", - &source, - &x0, &y0, &x1, &y1, - &Imaging_Type, &maskp)) - return NULL; + ImagingObject *maskp = NULL; + if (!PyArg_ParseTuple( + args, "O(iiii)|O!", &source, &x0, &y0, &x1, &y1, &Imaging_Type, &maskp)) { + return NULL; + } - if (PyImaging_Check(source)) + if (PyImaging_Check(source)) { status = ImagingPaste( - self->image, PyImaging_AsImaging(source), + self->image, + PyImaging_AsImaging(source), (maskp) ? maskp->image : NULL, - x0, y0, x1, y1 - ); + x0, + y0, + x1, + y1); - else { - if (!getink(source, self->image, ink)) + } else { + if (!getink(source, self->image, ink)) { return NULL; + } status = ImagingFill2( - self->image, ink, - (maskp) ? maskp->image : NULL, - x0, y0, x1, y1 - ); + self->image, ink, (maskp) ? maskp->image : NULL, x0, y0, x1, y1); } - if (status < 0) + if (status < 0) { return NULL; + } Py_INCREF(Py_None); return Py_None; } -static PyObject* -_point(ImagingObject* self, PyObject* args) -{ - static const char* wrong_number = "wrong number of lut entries"; +static PyObject * +_point(ImagingObject *self, PyObject *args) { + static const char *wrong_number = "wrong number of lut entries"; Py_ssize_t n; int i, bands; Imaging im; - PyObject* list; - char* mode; - if (!PyArg_ParseTuple(args, "Oz", &list, &mode)) - return NULL; + PyObject *list; + char *mode; + if (!PyArg_ParseTuple(args, "Oz", &list, &mode)) { + return NULL; + } if (mode && !strcmp(mode, "F")) { - FLOAT32* data; + FLOAT32 *data; /* map from 8-bit data to floating point */ n = 256; data = getlist(list, &n, wrong_number, TYPE_FLOAT32); - if (!data) + if (!data) { return NULL; - im = ImagingPoint(self->image, mode, (void*) data); + } + im = ImagingPoint(self->image, mode, (void *)data); free(data); } else if (!strcmp(self->image->mode, "I") && mode && !strcmp(mode, "L")) { - UINT8* data; + UINT8 *data; /* map from 16-bit subset of 32-bit data to 8-bit */ /* FIXME: support arbitrary number of entries (requires API change) */ n = 65536; data = getlist(list, &n, wrong_number, TYPE_UINT8); - if (!data) + if (!data) { return NULL; - im = ImagingPoint(self->image, mode, (void*) data); + } + im = ImagingPoint(self->image, mode, (void *)data); free(data); } else { - INT32* data; + INT32 *data; UINT8 lut[1024]; if (mode) { bands = getbands(mode); - if (bands < 0) + if (bands < 0) { return NULL; - } else + } + } else { bands = self->image->bands; + } /* map to integer data */ n = 256 * bands; data = getlist(list, &n, wrong_number, TYPE_INT32); - if (!data) + if (!data) { return NULL; + } - if (mode && !strcmp(mode, "I")) - im = ImagingPoint(self->image, mode, (void*) data); - else if (mode && bands > 1) { + if (mode && !strcmp(mode, "I")) { + im = ImagingPoint(self->image, mode, (void *)data); + } else if (mode && bands > 1) { for (i = 0; i < 256; i++) { - lut[i*4] = CLIP(data[i]); - lut[i*4+1] = CLIP(data[i+256]); - lut[i*4+2] = CLIP(data[i+512]); - if (n > 768) - lut[i*4+3] = CLIP(data[i+768]); + lut[i * 4] = CLIP8(data[i]); + lut[i * 4 + 1] = CLIP8(data[i + 256]); + lut[i * 4 + 2] = CLIP8(data[i + 512]); + if (n > 768) { + lut[i * 4 + 3] = CLIP8(data[i + 768]); + } } - im = ImagingPoint(self->image, mode, (void*) lut); + im = ImagingPoint(self->image, mode, (void *)lut); } else { /* map individual bands */ - for (i = 0; i < n; i++) - lut[i] = CLIP(data[i]); - im = ImagingPoint(self->image, mode, (void*) lut); + for (i = 0; i < n; i++) { + lut[i] = CLIP8(data[i]); + } + im = ImagingPoint(self->image, mode, (void *)lut); } free(data); } @@ -1186,32 +1455,32 @@ _point(ImagingObject* self, PyObject* args) return PyImagingNew(im); } -static PyObject* -_point_transform(ImagingObject* self, PyObject* args) -{ +static PyObject * +_point_transform(ImagingObject *self, PyObject *args) { double scale = 1.0; double offset = 0.0; - if (!PyArg_ParseTuple(args, "|dd", &scale, &offset)) - return NULL; + if (!PyArg_ParseTuple(args, "|dd", &scale, &offset)) { + return NULL; + } return PyImagingNew(ImagingPointTransform(self->image, scale, offset)); } -static PyObject* -_putdata(ImagingObject* self, PyObject* args) -{ +static PyObject * +_putdata(ImagingObject *self, PyObject *args) { Imaging image; // i & n are # pixels, require py_ssize_t. x can be as large as n. y, just because. Py_ssize_t n, i, x, y; - PyObject* data; - PyObject* seq = NULL; - PyObject* op; + PyObject *data; + PyObject *seq = NULL; + PyObject *op; double scale = 1.0; double offset = 0.0; - if (!PyArg_ParseTuple(args, "O|dd", &data, &scale, &offset)) + if (!PyArg_ParseTuple(args, "O|dd", &data, &scale, &offset)) { return NULL; + } if (!PySequence_Check(data)) { PyErr_SetString(PyExc_TypeError, must_be_sequence); @@ -1221,58 +1490,69 @@ _putdata(ImagingObject* self, PyObject* args) image = self->image; n = PyObject_Length(data); - if (n > (Py_ssize_t) (image->xsize * image->ysize)) { + if (n > (Py_ssize_t)image->xsize * (Py_ssize_t)image->ysize) { PyErr_SetString(PyExc_TypeError, "too many data entries"); return NULL; } +#define set_value_to_item(seq, i) \ +op = PySequence_Fast_GET_ITEM(seq, i); \ +if (PySequence_Check(op)) { \ + PyErr_SetString(PyExc_TypeError, "sequence must be flattened"); \ + return NULL; \ +} else { \ + value = PyFloat_AsDouble(op); \ +} if (image->image8) { if (PyBytes_Check(data)) { - unsigned char* p; - p = (unsigned char*) PyBytes_AS_STRING(data); - if (scale == 1.0 && offset == 0.0) + unsigned char *p; + p = (unsigned char *)PyBytes_AS_STRING(data); + if (scale == 1.0 && offset == 0.0) { /* Plain string data */ for (i = y = 0; i < n; i += image->xsize, y++) { x = n - i; - if (x > (int) image->xsize) + if (x > (int)image->xsize) { x = image->xsize; - memcpy(image->image8[y], p+i, x); + } + memcpy(image->image8[y], p + i, x); } - else + } else { /* Scaled and clipped string data */ for (i = x = y = 0; i < n; i++) { - image->image8[y][x] = CLIP((int) (p[i] * scale + offset)); - if (++x >= (int) image->xsize) + image->image8[y][x] = CLIP8((int)(p[i] * scale + offset)); + if (++x >= (int)image->xsize) { x = 0, y++; + } } + } } else { - seq = PySequence_Fast(data, must_be_sequence); - if (!seq) { - PyErr_SetString(PyExc_TypeError, must_be_sequence); - return NULL; - } - if (scale == 1.0 && offset == 0.0) { - /* Clipped data */ - for (i = x = y = 0; i < n; i++) { - op = PySequence_Fast_GET_ITEM(seq, i); - image->image8[y][x] = (UINT8) CLIP(PyInt_AsLong(op)); - if (++x >= (int) image->xsize){ - x = 0, y++; - } - } + seq = PySequence_Fast(data, must_be_sequence); + if (!seq) { + PyErr_SetString(PyExc_TypeError, must_be_sequence); + return NULL; + } + double value; + if (scale == 1.0 && offset == 0.0) { + /* Clipped data */ + for (i = x = y = 0; i < n; i++) { + set_value_to_item(seq, i); + image->image8[y][x] = (UINT8)CLIP8(value); + if (++x >= (int)image->xsize) { + x = 0, y++; + } + } } else { - /* Scaled and clipped data */ - for (i = x = y = 0; i < n; i++) { - PyObject *op = PySequence_Fast_GET_ITEM(seq, i); - image->image8[y][x] = CLIP( - (int) (PyFloat_AsDouble(op) * scale + offset)); - if (++x >= (int) image->xsize){ - x = 0, y++; - } - } - } - PyErr_Clear(); /* Avoid weird exceptions */ + /* 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++; + } + } + } + PyErr_Clear(); /* Avoid weird exceptions */ } } else { /* 32-bit images */ @@ -1282,50 +1562,52 @@ _putdata(ImagingObject* self, PyObject* args) return NULL; } switch (image->type) { - case IMAGING_TYPE_INT32: - for (i = x = y = 0; i < n; i++) { - op = PySequence_Fast_GET_ITEM(seq, i); - IMAGING_PIXEL_INT32(image, x, y) = - (INT32) (PyFloat_AsDouble(op) * scale + offset); - if (++x >= (int) image->xsize){ - x = 0, y++; + case IMAGING_TYPE_INT32: + for (i = x = y = 0; i < n; i++) { + double value; + set_value_to_item(seq, i); + IMAGING_PIXEL_INT32(image, x, y) = + (INT32)(value * scale + offset); + if (++x >= (int)image->xsize) { + x = 0, y++; + } } - } - PyErr_Clear(); /* Avoid weird exceptions */ - break; - case IMAGING_TYPE_FLOAT32: - for (i = x = y = 0; i < n; i++) { - op = PySequence_Fast_GET_ITEM(seq, i); - IMAGING_PIXEL_FLOAT32(image, x, y) = - (FLOAT32) (PyFloat_AsDouble(op) * scale + offset); - if (++x >= (int) image->xsize){ - x = 0, y++; + PyErr_Clear(); /* Avoid weird exceptions */ + break; + case IMAGING_TYPE_FLOAT32: + for (i = x = y = 0; i < n; i++) { + double value; + set_value_to_item(seq, i); + IMAGING_PIXEL_FLOAT32(image, x, y) = + (FLOAT32)(value * scale + offset); + if (++x >= (int)image->xsize) { + x = 0, y++; + } } - } - PyErr_Clear(); /* Avoid weird exceptions */ - break; - default: - for (i = x = y = 0; i < n; i++) { - union { - char ink[4]; - INT32 inkint; - } u; + PyErr_Clear(); /* Avoid weird exceptions */ + break; + default: + for (i = x = y = 0; i < n; i++) { + union { + char ink[4]; + INT32 inkint; + } u; - u.inkint = 0; + u.inkint = 0; - op = PySequence_Fast_GET_ITEM(seq, i); - if (!op || !getink(op, image, u.ink)) { - Py_DECREF(seq); - return NULL; - } - /* FIXME: what about scale and offset? */ - image->image32[y][x] = u.inkint; - if (++x >= (int) image->xsize){ - x = 0, y++; + op = PySequence_Fast_GET_ITEM(seq, i); + if (!op || !getink(op, image, u.ink)) { + Py_DECREF(seq); + return NULL; + } + /* FIXME: what about scale and offset? */ + image->image32[y][x] = u.inkint; + if (++x >= (int)image->xsize) { + x = 0, y++; + } } - } - PyErr_Clear(); /* Avoid weird exceptions */ - break; + PyErr_Clear(); /* Avoid weird exceptions */ + break; } } @@ -1337,73 +1619,74 @@ _putdata(ImagingObject* self, PyObject* args) #ifdef WITH_QUANTIZE -static PyObject* -_quantize(ImagingObject* self, PyObject* args) -{ +static PyObject * +_quantize(ImagingObject *self, PyObject *args) { int colours = 256; int method = 0; int kmeans = 0; - if (!PyArg_ParseTuple(args, "|iii", &colours, &method, &kmeans)) + if (!PyArg_ParseTuple(args, "|iii", &colours, &method, &kmeans)) { return NULL; + } if (!self->image->xsize || !self->image->ysize) { /* no content; return an empty image */ - return PyImagingNew( - ImagingNew("P", self->image->xsize, self->image->ysize) - ); + return PyImagingNew(ImagingNew("P", self->image->xsize, self->image->ysize)); } return PyImagingNew(ImagingQuantize(self->image, colours, method, kmeans)); } #endif -static PyObject* -_putpalette(ImagingObject* self, PyObject* args) -{ +static PyObject * +_putpalette(ImagingObject *self, PyObject *args) { ImagingShuffler unpack; int bits; - char* rawmode; - UINT8* palette; - int palettesize; - if (!PyArg_ParseTuple(args, "s"PY_ARG_BYTES_LENGTH, &rawmode, &palette, &palettesize)) + char *rawmode, *palette_mode; + UINT8 *palette; + Py_ssize_t palettesize; + if (!PyArg_ParseTuple(args, "sy#", &rawmode, &palette, &palettesize)) { return NULL; + } - if (strcmp(self->image->mode, "L") != 0 && strcmp(self->image->mode, "P")) { + if (strcmp(self->image->mode, "L") && strcmp(self->image->mode, "LA") && + strcmp(self->image->mode, "P") && strcmp(self->image->mode, "PA")) { PyErr_SetString(PyExc_ValueError, wrong_mode); return NULL; } - unpack = ImagingFindUnpacker("RGB", rawmode, &bits); + palette_mode = strncmp("RGBA", rawmode, 4) == 0 ? "RGBA" : "RGB"; + unpack = ImagingFindUnpacker(palette_mode, rawmode, &bits); if (!unpack) { PyErr_SetString(PyExc_ValueError, wrong_raw_mode); return NULL; } - if ( palettesize * 8 / bits > 256) { + if (palettesize * 8 / bits > 256) { PyErr_SetString(PyExc_ValueError, wrong_palette_size); return NULL; } ImagingPaletteDelete(self->image->palette); - strcpy(self->image->mode, "P"); + strcpy(self->image->mode, strlen(self->image->mode) == 2 ? "PA" : "P"); - self->image->palette = ImagingPaletteNew("RGB"); + self->image->palette = ImagingPaletteNew(palette_mode); - unpack(self->image->palette->palette, palette, palettesize * 8 / bits); + self->image->palette->size = palettesize * 8 / bits; + unpack(self->image->palette->palette, palette, self->image->palette->size); Py_INCREF(Py_None); return Py_None; } -static PyObject* -_putpalettealpha(ImagingObject* self, PyObject* args) -{ +static PyObject * +_putpalettealpha(ImagingObject *self, PyObject *args) { int index; int alpha = 0; - if (!PyArg_ParseTuple(args, "i|i", &index, &alpha)) + if (!PyArg_ParseTuple(args, "i|i", &index, &alpha)) { return NULL; + } if (!self->image->palette) { PyErr_SetString(PyExc_ValueError, no_palette); @@ -1416,97 +1699,114 @@ _putpalettealpha(ImagingObject* self, PyObject* args) } strcpy(self->image->palette->mode, "RGBA"); - self->image->palette->palette[index*4+3] = (UINT8) alpha; + self->image->palette->palette[index * 4 + 3] = (UINT8)alpha; Py_INCREF(Py_None); return Py_None; } -static PyObject* -_putpalettealphas(ImagingObject* self, PyObject* args) -{ +static PyObject * +_putpalettealphas(ImagingObject *self, PyObject *args) { int i; UINT8 *values; - int length; - if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH, &values, &length)) + Py_ssize_t length; + if (!PyArg_ParseTuple(args, "y#", &values, &length)) { return NULL; + } if (!self->image->palette) { PyErr_SetString(PyExc_ValueError, no_palette); return NULL; } - if (length > 256) { + if (length > 256) { PyErr_SetString(PyExc_ValueError, outside_palette); return NULL; } strcpy(self->image->palette->mode, "RGBA"); - for (i=0; iimage->palette->palette[i*4+3] = (UINT8) values[i]; + for (i = 0; i < length; i++) { + self->image->palette->palette[i * 4 + 3] = (UINT8)values[i]; } Py_INCREF(Py_None); return Py_None; } -static PyObject* -_putpixel(ImagingObject* self, PyObject* args) -{ +static PyObject * +_putpixel(ImagingObject *self, PyObject *args) { Imaging im; char ink[4]; int x, y; - PyObject* color; - if (!PyArg_ParseTuple(args, "(ii)O", &x, &y, &color)) + PyObject *color; + if (!PyArg_ParseTuple(args, "(ii)O", &x, &y, &color)) { return NULL; + } im = self->image; + if (x < 0) { + x = im->xsize + x; + } + if (y < 0) { + y = im->ysize + y; + } + if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { PyErr_SetString(PyExc_IndexError, outside_image); return NULL; } - if (!getink(color, im, ink)) + if (!getink(color, im, ink)) { return NULL; + } - if (self->access) + if (self->access) { self->access->put_pixel(im, x, y, ink); + } Py_INCREF(Py_None); return Py_None; } #ifdef WITH_RANKFILTER -static PyObject* -_rankfilter(ImagingObject* self, PyObject* args) -{ +static PyObject * +_rankfilter(ImagingObject *self, PyObject *args) { int size, rank; - if (!PyArg_ParseTuple(args, "ii", &size, &rank)) + if (!PyArg_ParseTuple(args, "ii", &size, &rank)) { return NULL; + } return PyImagingNew(ImagingRankFilter(self->image, size, rank)); } #endif -static PyObject* -_resize(ImagingObject* self, PyObject* args) -{ +static PyObject * +_resize(ImagingObject *self, PyObject *args) { Imaging imIn; Imaging imOut; int xsize, ysize; int filter = IMAGING_TRANSFORM_NEAREST; float box[4] = {0, 0, 0, 0}; - + imIn = self->image; box[2] = imIn->xsize; box[3] = imIn->ysize; - - if (!PyArg_ParseTuple(args, "(ii)|i(ffff)", &xsize, &ysize, &filter, - &box[0], &box[1], &box[2], &box[3])) + + if (!PyArg_ParseTuple( + args, + "(ii)|i(ffff)", + &xsize, + &ysize, + &filter, + &box[0], + &box[1], + &box[2], + &box[3])) { return NULL; + } if (xsize < 1 || ysize < 1) { return ImagingError_ValueError("height and width must be > 0"); @@ -1525,48 +1825,95 @@ _resize(ImagingObject* self, PyObject* args) } // If box's coordinates are int and box size matches requested size - if (box[0] - (int) box[0] == 0 && box[2] - box[0] == xsize - && box[1] - (int) box[1] == 0 && box[3] - box[1] == ysize) { + if (box[0] - (int)box[0] == 0 && box[2] - box[0] == xsize && + 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) { + } else if (filter == IMAGING_TRANSFORM_NEAREST) { double a[6]; memset(a, 0, sizeof a); - a[0] = (double) (box[2] - box[0]) / xsize; - a[4] = (double) (box[3] - box[1]) / ysize; + a[0] = (double)(box[2] - box[0]) / xsize; + a[4] = (double)(box[3] - box[1]) / ysize; a[2] = box[0]; a[5] = box[1]; imOut = ImagingNewDirty(imIn->mode, xsize, ysize); imOut = ImagingTransform( - imOut, imIn, IMAGING_TRANSFORM_AFFINE, - 0, 0, xsize, ysize, - a, filter, 1); - } - else { + imOut, imIn, IMAGING_TRANSFORM_AFFINE, 0, 0, xsize, ysize, a, filter, 1); + } else { imOut = ImagingResample(imIn, xsize, ysize, filter, box); } return PyImagingNew(imOut); } +static PyObject * +_reduce(ImagingObject *self, PyObject *args) { + Imaging imIn; + Imaging imOut; + + int xscale, yscale; + int box[4] = {0, 0, 0, 0}; + + imIn = self->image; + box[2] = imIn->xsize; + box[3] = imIn->ysize; + + if (!PyArg_ParseTuple( + args, + "(ii)|(iiii)", + &xscale, + &yscale, + &box[0], + &box[1], + &box[2], + &box[3])) { + return NULL; + } + + if (xscale < 1 || yscale < 1) { + return ImagingError_ValueError("scale must be > 0"); + } + + if (box[0] < 0 || box[1] < 0) { + return ImagingError_ValueError("box offset can't be negative"); + } -#define IS_RGB(mode)\ + if (box[2] > imIn->xsize || box[3] > imIn->ysize) { + return ImagingError_ValueError("box can't exceed original image size"); + } + + if (box[2] <= box[0] || box[3] <= box[1]) { + return ImagingError_ValueError("box can't be empty"); + } + + if (xscale == 1 && yscale == 1) { + imOut = ImagingCrop(imIn, box[0], box[1], box[2], box[3]); + } else { + // Change box format: (left, top, width, height) + box[2] -= box[0]; + box[3] -= box[1]; + imOut = ImagingReduce(imIn, xscale, yscale, box); + } + + return PyImagingNew(imOut); +} + +#define IS_RGB(mode) \ (!strcmp(mode, "RGB") || !strcmp(mode, "RGBA") || !strcmp(mode, "RGBX")) -static PyObject* -im_setmode(ImagingObject* self, PyObject* args) -{ +static PyObject * +im_setmode(ImagingObject *self, PyObject *args) { /* attempt to modify the mode of an image in place */ Imaging im; - char* mode; - int modelen; - if (!PyArg_ParseTuple(args, "s#:setmode", &mode, &modelen)) - return NULL; + char *mode; + Py_ssize_t modelen; + if (!PyArg_ParseTuple(args, "s#:setmode", &mode, &modelen)) { + return NULL; + } im = self->image; @@ -1578,190 +1925,205 @@ im_setmode(ImagingObject* self, PyObject* args) /* color to color */ strcpy(im->mode, mode); im->bands = modelen; - if (!strcmp(mode, "RGBA")) - (void) ImagingFillBand(im, 3, 255); + if (!strcmp(mode, "RGBA")) { + (void)ImagingFillBand(im, 3, 255); + } } else { /* trying doing an in-place conversion */ - if (!ImagingConvertInPlace(im, mode)) + if (!ImagingConvertInPlace(im, mode)) { return NULL; + } } - if (self->access) + if (self->access) { ImagingAccessDelete(im, self->access); + } self->access = ImagingAccessNew(im); Py_INCREF(Py_None); return Py_None; } - -static PyObject* -_transform2(ImagingObject* self, PyObject* args) -{ - static const char* wrong_number = "wrong number of matrix entries"; +static PyObject * +_transform2(ImagingObject *self, PyObject *args) { + static const char *wrong_number = "wrong number of matrix entries"; Imaging imOut; Py_ssize_t n; double *a; - ImagingObject* imagep; + ImagingObject *imagep; int x0, y0, x1, y1; int method; - PyObject* data; + PyObject *data; int filter = IMAGING_TRANSFORM_NEAREST; int fill = 1; - if (!PyArg_ParseTuple(args, "(iiii)O!iO|ii", - &x0, &y0, &x1, &y1, - &Imaging_Type, &imagep, - &method, &data, - &filter, &fill)) - return NULL; + if (!PyArg_ParseTuple( + args, + "(iiii)O!iO|ii", + &x0, + &y0, + &x1, + &y1, + &Imaging_Type, + &imagep, + &method, + &data, + &filter, + &fill)) { + return NULL; + } switch (method) { - case IMAGING_TRANSFORM_AFFINE: - n = 6; - break; - case IMAGING_TRANSFORM_PERSPECTIVE: - n = 8; - break; - case IMAGING_TRANSFORM_QUAD: - n = 8; - break; - default: - n = -1; /* force error */ + case IMAGING_TRANSFORM_AFFINE: + n = 6; + break; + case IMAGING_TRANSFORM_PERSPECTIVE: + n = 8; + break; + case IMAGING_TRANSFORM_QUAD: + n = 8; + break; + default: + n = -1; /* force error */ } a = getlist(data, &n, wrong_number, TYPE_DOUBLE); - if (!a) + if (!a) { return NULL; + } imOut = ImagingTransform( - self->image, imagep->image, method, - x0, y0, x1, y1, a, filter, fill); + self->image, imagep->image, method, x0, y0, x1, y1, a, filter, fill); free(a); - if (!imOut) + if (!imOut) { return NULL; + } Py_INCREF(Py_None); return Py_None; } -static PyObject* -_transpose(ImagingObject* self, PyObject* args) -{ +static PyObject * +_transpose(ImagingObject *self, PyObject *args) { Imaging imIn; Imaging imOut; int op; - if (!PyArg_ParseTuple(args, "i", &op)) - return NULL; + if (!PyArg_ParseTuple(args, "i", &op)) { + return NULL; + } imIn = self->image; switch (op) { - case 0: /* flip left right */ - case 1: /* flip top bottom */ - case 3: /* rotate 180 */ - imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); - break; - case 2: /* rotate 90 */ - case 4: /* rotate 270 */ - case 5: /* transpose */ - case 6: /* transverse */ - imOut = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize); - break; - default: - PyErr_SetString(PyExc_ValueError, "No such transpose operation"); - return NULL; - } - - if (imOut) - switch (op) { - case 0: - (void) ImagingFlipLeftRight(imOut, imIn); - break; - case 1: - (void) ImagingFlipTopBottom(imOut, imIn); + case 0: /* flip left right */ + case 1: /* flip top bottom */ + case 3: /* rotate 180 */ + imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); break; - case 2: - (void) ImagingRotate90(imOut, imIn); - break; - case 3: - (void) ImagingRotate180(imOut, imIn); - break; - case 4: - (void) ImagingRotate270(imOut, imIn); - break; - case 5: - (void) ImagingTranspose(imOut, imIn); - break; - case 6: - (void) ImagingTransverse(imOut, imIn); + case 2: /* rotate 90 */ + case 4: /* rotate 270 */ + case 5: /* transpose */ + case 6: /* transverse */ + imOut = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize); break; + default: + PyErr_SetString(PyExc_ValueError, "No such transpose operation"); + return NULL; + } + + if (imOut) { + switch (op) { + case 0: + (void)ImagingFlipLeftRight(imOut, imIn); + break; + case 1: + (void)ImagingFlipTopBottom(imOut, imIn); + break; + case 2: + (void)ImagingRotate90(imOut, imIn); + break; + case 3: + (void)ImagingRotate180(imOut, imIn); + break; + case 4: + (void)ImagingRotate270(imOut, imIn); + break; + case 5: + (void)ImagingTranspose(imOut, imIn); + break; + case 6: + (void)ImagingTransverse(imOut, imIn); + break; } + } return PyImagingNew(imOut); } #ifdef WITH_UNSHARPMASK -static PyObject* -_unsharp_mask(ImagingObject* self, PyObject* args) -{ +static PyObject * +_unsharp_mask(ImagingObject *self, PyObject *args) { Imaging imIn; Imaging imOut; float radius; int percent, threshold; - if (!PyArg_ParseTuple(args, "fii", &radius, &percent, &threshold)) + if (!PyArg_ParseTuple(args, "fii", &radius, &percent, &threshold)) { return NULL; + } imIn = self->image; imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); - if (!imOut) + if (!imOut) { return NULL; + } - if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) + if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) { return NULL; + } return PyImagingNew(imOut); } #endif -static PyObject* -_box_blur(ImagingObject* self, PyObject* args) -{ +static PyObject * +_box_blur(ImagingObject *self, PyObject *args) { Imaging imIn; Imaging imOut; float radius; int n = 1; - if (!PyArg_ParseTuple(args, "f|i", &radius, &n)) + if (!PyArg_ParseTuple(args, "f|i", &radius, &n)) { return NULL; + } imIn = self->image; imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); - if (!imOut) + if (!imOut) { return NULL; + } - if (!ImagingBoxBlur(imOut, imIn, radius, n)) + if (!ImagingBoxBlur(imOut, imIn, radius, n)) { + ImagingDelete(imOut); return NULL; + } return PyImagingNew(imOut); } /* -------------------------------------------------------------------- */ -static PyObject* -_isblock(ImagingObject* self, PyObject* args) -{ +static PyObject * +_isblock(ImagingObject *self) { return PyBool_FromLong(self->image->block != NULL); } -static PyObject* -_getbbox(ImagingObject* self, PyObject* args) -{ +static PyObject * +_getbbox(ImagingObject *self) { int bbox[4]; if (!ImagingGetBBox(self->image, bbox)) { Py_INCREF(Py_None); @@ -1771,20 +2133,21 @@ _getbbox(ImagingObject* self, PyObject* args) return Py_BuildValue("iiii", bbox[0], bbox[1], bbox[2], bbox[3]); } -static PyObject* -_getcolors(ImagingObject* self, PyObject* args) -{ - ImagingColorItem* items; +static PyObject * +_getcolors(ImagingObject *self, PyObject *args) { + ImagingColorItem *items; int i, colors; - PyObject* out; + PyObject *out; int maxcolors = 256; - if (!PyArg_ParseTuple(args, "i:getcolors", &maxcolors)) + if (!PyArg_ParseTuple(args, "i:getcolors", &maxcolors)) { return NULL; + } items = ImagingGetColors(self->image, maxcolors, &colors); - if (!items) + if (!items) { return NULL; + } if (colors > maxcolors) { out = Py_None; @@ -1792,10 +2155,9 @@ _getcolors(ImagingObject* self, PyObject* args) } else { out = PyList_New(colors); for (i = 0; i < colors; i++) { - ImagingColorItem* v = &items[i]; - PyObject* item = Py_BuildValue( - "iN", v->count, getpixel(self->image, self->access, v->x, v->y) - ); + ImagingColorItem *v = &items[i]; + PyObject *item = Py_BuildValue( + "iN", v->count, getpixel(self->image, self->access, v->x, v->y)); PyList_SetItem(out, i, item); } } @@ -1805,40 +2167,45 @@ _getcolors(ImagingObject* self, PyObject* args) return out; } -static PyObject* -_getextrema(ImagingObject* self, PyObject* args) -{ +static PyObject * +_getextrema(ImagingObject *self) { union { UINT8 u[2]; INT32 i[2]; FLOAT32 f[2]; + UINT16 s[2]; } extrema; int status; status = ImagingGetExtrema(self->image, &extrema); - if (status < 0) + if (status < 0) { return NULL; + } - if (status) + if (status) { switch (self->image->type) { - case IMAGING_TYPE_UINT8: - return Py_BuildValue("BB", extrema.u[0], extrema.u[1]); - case IMAGING_TYPE_INT32: - return Py_BuildValue("ii", extrema.i[0], extrema.i[1]); - case IMAGING_TYPE_FLOAT32: - return Py_BuildValue("dd", extrema.f[0], extrema.f[1]); + case IMAGING_TYPE_UINT8: + return Py_BuildValue("BB", extrema.u[0], extrema.u[1]); + case IMAGING_TYPE_INT32: + return Py_BuildValue("ii", extrema.i[0], extrema.i[1]); + case IMAGING_TYPE_FLOAT32: + return Py_BuildValue("dd", extrema.f[0], extrema.f[1]); + case IMAGING_TYPE_SPECIAL: + if (strcmp(self->image->mode, "I;16") == 0) { + return Py_BuildValue("HH", extrema.s[0], extrema.s[1]); + } } + } Py_INCREF(Py_None); return Py_None; } -static PyObject* -_getprojection(ImagingObject* self, PyObject* args) -{ - unsigned char* xprofile; - unsigned char* yprofile; - PyObject* result; +static PyObject * +_getprojection(ImagingObject *self) { + unsigned char *xprofile; + unsigned char *yprofile; + PyObject *result; /* malloc check ok */ xprofile = malloc(self->image->xsize); @@ -1847,14 +2214,18 @@ _getprojection(ImagingObject* self, PyObject* args) if (xprofile == NULL || yprofile == NULL) { free(xprofile); free(yprofile); - return PyErr_NoMemory(); + return ImagingError_MemoryError(); } - ImagingGetProjection(self->image, (unsigned char *)xprofile, (unsigned char *)yprofile); + ImagingGetProjection( + self->image, (unsigned char *)xprofile, (unsigned char *)yprofile); - result = Py_BuildValue(PY_ARG_BYTES_LENGTH PY_ARG_BYTES_LENGTH, - xprofile, self->image->xsize, - yprofile, self->image->ysize); + result = Py_BuildValue( + "y#y#", + xprofile, + (Py_ssize_t)self->image->xsize, + yprofile, + (Py_ssize_t)self->image->ysize); free(xprofile); free(yprofile); @@ -1864,90 +2235,108 @@ _getprojection(ImagingObject* self, PyObject* args) /* -------------------------------------------------------------------- */ -static PyObject* -_getband(ImagingObject* self, PyObject* args) -{ +static PyObject * +_getband(ImagingObject *self, PyObject *args) { int band; - if (!PyArg_ParseTuple(args, "i", &band)) + if (!PyArg_ParseTuple(args, "i", &band)) { return NULL; + } return PyImagingNew(ImagingGetBand(self->image, band)); } -static PyObject* -_fillband(ImagingObject* self, PyObject* args) -{ +static PyObject * +_fillband(ImagingObject *self, PyObject *args) { int band; int color; - if (!PyArg_ParseTuple(args, "ii", &band, &color)) + if (!PyArg_ParseTuple(args, "ii", &band, &color)) { return NULL; + } - if (!ImagingFillBand(self->image, band, color)) + if (!ImagingFillBand(self->image, band, color)) { return NULL; + } Py_INCREF(Py_None); return Py_None; } -static PyObject* -_putband(ImagingObject* self, PyObject* args) -{ - ImagingObject* imagep; +static PyObject * +_putband(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; int band; - if (!PyArg_ParseTuple(args, "O!i", - &Imaging_Type, &imagep, - &band)) + if (!PyArg_ParseTuple(args, "O!i", &Imaging_Type, &imagep, &band)) { return NULL; + } - if (!ImagingPutBand(self->image, imagep->image, band)) + if (!ImagingPutBand(self->image, imagep->image, band)) { return NULL; + } Py_INCREF(Py_None); return Py_None; } -static PyObject* -_merge(PyObject* self, PyObject* args) -{ - char* mode; +static PyObject * +_merge(PyObject *self, PyObject *args) { + char *mode; ImagingObject *band0 = NULL; ImagingObject *band1 = NULL; ImagingObject *band2 = NULL; ImagingObject *band3 = NULL; Imaging bands[4] = {NULL, NULL, NULL, NULL}; - if (!PyArg_ParseTuple(args, "sO!|O!O!O!", &mode, - &Imaging_Type, &band0, &Imaging_Type, &band1, - &Imaging_Type, &band2, &Imaging_Type, &band3)) + if (!PyArg_ParseTuple( + args, + "sO!|O!O!O!", + &mode, + &Imaging_Type, + &band0, + &Imaging_Type, + &band1, + &Imaging_Type, + &band2, + &Imaging_Type, + &band3)) { return NULL; + } - if (band0) bands[0] = band0->image; - if (band1) bands[1] = band1->image; - if (band2) bands[2] = band2->image; - if (band3) bands[3] = band3->image; + if (band0) { + bands[0] = band0->image; + } + if (band1) { + bands[1] = band1->image; + } + if (band2) { + bands[2] = band2->image; + } + if (band3) { + bands[3] = band3->image; + } return PyImagingNew(ImagingMerge(mode, bands)); } -static PyObject* -_split(ImagingObject* self, PyObject* args) -{ +static PyObject * +_split(ImagingObject *self) { int fails = 0; Py_ssize_t i; - PyObject* list; - PyObject* imaging_object; + PyObject *list; + PyObject *imaging_object; Imaging bands[4] = {NULL, NULL, NULL, NULL}; - if ( ! ImagingSplit(self->image, bands)) + if (!ImagingSplit(self->image, bands)) { return NULL; + } list = PyTuple_New(self->image->bands); for (i = 0; i < self->image->bands; i++) { imaging_object = PyImagingNew(bands[i]); - if ( ! imaging_object) + if (!imaging_object) { fails += 1; + } PyTuple_SET_ITEM(list, i, imaging_object); } if (fails) { @@ -1961,179 +2350,204 @@ _split(ImagingObject* self, PyObject* args) #ifdef WITH_IMAGECHOPS -static PyObject* -_chop_invert(ImagingObject* self, PyObject* args) -{ +static PyObject * +_chop_invert(ImagingObject *self) { return PyImagingNew(ImagingNegative(self->image)); } -static PyObject* -_chop_lighter(ImagingObject* self, PyObject* args) -{ - ImagingObject* imagep; +static PyObject * +_chop_lighter(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } return PyImagingNew(ImagingChopLighter(self->image, imagep->image)); } -static PyObject* -_chop_darker(ImagingObject* self, PyObject* args) -{ - ImagingObject* imagep; +static PyObject * +_chop_darker(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } return PyImagingNew(ImagingChopDarker(self->image, imagep->image)); } -static PyObject* -_chop_difference(ImagingObject* self, PyObject* args) -{ - ImagingObject* imagep; +static PyObject * +_chop_difference(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } return PyImagingNew(ImagingChopDifference(self->image, imagep->image)); } -static PyObject* -_chop_multiply(ImagingObject* self, PyObject* args) -{ - ImagingObject* imagep; +static PyObject * +_chop_multiply(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } return PyImagingNew(ImagingChopMultiply(self->image, imagep->image)); } -static PyObject* -_chop_screen(ImagingObject* self, PyObject* args) -{ - ImagingObject* imagep; +static PyObject * +_chop_screen(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } return PyImagingNew(ImagingChopScreen(self->image, imagep->image)); } -static PyObject* -_chop_add(ImagingObject* self, PyObject* args) -{ - ImagingObject* imagep; +static PyObject * +_chop_add(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; float scale; int offset; scale = 1.0; offset = 0; - if (!PyArg_ParseTuple(args, "O!|fi", &Imaging_Type, &imagep, - &scale, &offset)) + if (!PyArg_ParseTuple(args, "O!|fi", &Imaging_Type, &imagep, &scale, &offset)) { return NULL; + } - return PyImagingNew(ImagingChopAdd(self->image, imagep->image, - scale, offset)); + return PyImagingNew(ImagingChopAdd(self->image, imagep->image, scale, offset)); } -static PyObject* -_chop_subtract(ImagingObject* self, PyObject* args) -{ - ImagingObject* imagep; +static PyObject * +_chop_subtract(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; float scale; int offset; scale = 1.0; offset = 0; - if (!PyArg_ParseTuple(args, "O!|fi", &Imaging_Type, &imagep, - &scale, &offset)) + if (!PyArg_ParseTuple(args, "O!|fi", &Imaging_Type, &imagep, &scale, &offset)) { + return NULL; + } + + return PyImagingNew(ImagingChopSubtract(self->image, imagep->image, scale, offset)); +} + +static PyObject * +_chop_and(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingChopAnd(self->image, imagep->image)); +} + +static PyObject * +_chop_or(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { + return NULL; + } + + return PyImagingNew(ImagingChopOr(self->image, imagep->image)); +} + +static PyObject * +_chop_xor(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; + + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } - return PyImagingNew(ImagingChopSubtract(self->image, imagep->image, - scale, offset)); + return PyImagingNew(ImagingChopXor(self->image, imagep->image)); } -static PyObject* -_chop_and(ImagingObject* self, PyObject* args) -{ - ImagingObject* imagep; +static PyObject * +_chop_add_modulo(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } - return PyImagingNew(ImagingChopAnd(self->image, imagep->image)); + return PyImagingNew(ImagingChopAddModulo(self->image, imagep->image)); } -static PyObject* -_chop_or(ImagingObject* self, PyObject* args) -{ - ImagingObject* imagep; +static PyObject * +_chop_subtract_modulo(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } - return PyImagingNew(ImagingChopOr(self->image, imagep->image)); + return PyImagingNew(ImagingChopSubtractModulo(self->image, imagep->image)); } -static PyObject* -_chop_xor(ImagingObject* self, PyObject* args) -{ - ImagingObject* imagep; +static PyObject * +_chop_soft_light(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } - return PyImagingNew(ImagingChopXor(self->image, imagep->image)); + return PyImagingNew(ImagingChopSoftLight(self->image, imagep->image)); } -static PyObject* -_chop_add_modulo(ImagingObject* self, PyObject* args) -{ - ImagingObject* imagep; +static PyObject * +_chop_hard_light(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } - return PyImagingNew(ImagingChopAddModulo(self->image, imagep->image)); + return PyImagingNew(ImagingChopHardLight(self->image, imagep->image)); } -static PyObject* -_chop_subtract_modulo(ImagingObject* self, PyObject* args) -{ - ImagingObject* imagep; +static PyObject * +_chop_overlay(ImagingObject *self, PyObject *args) { + ImagingObject *imagep; - if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) + if (!PyArg_ParseTuple(args, "O!", &Imaging_Type, &imagep)) { return NULL; + } - return PyImagingNew(ImagingChopSubtractModulo(self->image, imagep->image)); + return PyImagingNew(ImagingOverlay(self->image, imagep->image)); } - #endif - /* -------------------------------------------------------------------- */ #ifdef WITH_IMAGEDRAW -static PyObject* -_font_new(PyObject* self_, PyObject* args) -{ +static PyObject * +_font_new(PyObject *self_, PyObject *args) { ImagingFontObject *self; int i, y0, y1; - static const char* wrong_length = "descriptor table has wrong size"; + static const char *wrong_length = "descriptor table has wrong size"; - ImagingObject* imagep; - unsigned char* glyphdata; - int glyphdata_length; - if (!PyArg_ParseTuple(args, "O!"PY_ARG_BYTES_LENGTH, - &Imaging_Type, &imagep, - &glyphdata, &glyphdata_length)) + ImagingObject *imagep; + unsigned char *glyphdata; + Py_ssize_t glyphdata_length; + if (!PyArg_ParseTuple( + args, "O!y#", &Imaging_Type, &imagep, &glyphdata, &glyphdata_length)) { return NULL; + } if (glyphdata_length != 256 * 20) { PyErr_SetString(PyExc_ValueError, wrong_length); @@ -2141,8 +2555,9 @@ _font_new(PyObject* self_, PyObject* args) } self = PyObject_New(ImagingFontObject, &ImagingFont_Type); - if (self == NULL) + if (self == NULL) { return NULL; + } /* glyph bitmap */ self->bitmap = imagep->image; @@ -2161,10 +2576,12 @@ _font_new(PyObject* self_, PyObject* args) self->glyphs[i].sy0 = S16(B16(glyphdata, 14)); self->glyphs[i].sx1 = S16(B16(glyphdata, 16)); self->glyphs[i].sy1 = S16(B16(glyphdata, 18)); - if (self->glyphs[i].dy0 < y0) + if (self->glyphs[i].dy0 < y0) { y0 = self->glyphs[i].dy0; - if (self->glyphs[i].dy1 > y1) + } + if (self->glyphs[i].dy1 > y1) { y1 = self->glyphs[i].dy1; + } glyphdata += 20; } @@ -2175,37 +2592,37 @@ _font_new(PyObject* self_, PyObject* args) Py_INCREF(imagep); self->ref = imagep; - return (PyObject*) self; + return (PyObject *)self; } static void -_font_dealloc(ImagingFontObject* self) -{ +_font_dealloc(ImagingFontObject *self) { Py_XDECREF(self->ref); PyObject_Del(self); } static inline int -textwidth(ImagingFontObject* self, const unsigned char* text) -{ +textwidth(ImagingFontObject *self, const unsigned char *text) { int xsize; - for (xsize = 0; *text; text++) + for (xsize = 0; *text; text++) { xsize += self->glyphs[*text].dx; + } return xsize; } -void _font_text_asBytes(PyObject* encoded_string, unsigned char** text){ +void +_font_text_asBytes(PyObject *encoded_string, unsigned char **text) { /* Allocates *text, returns a 'new reference'. Caller is required to free */ - PyObject* bytes = NULL; + PyObject *bytes = NULL; Py_ssize_t len = 0; char *buffer; *text = NULL; - if (PyUnicode_CheckExact(encoded_string)){ + if (PyUnicode_CheckExact(encoded_string)) { bytes = PyUnicode_AsLatin1String(encoded_string); if (!bytes) { return; @@ -2215,7 +2632,7 @@ void _font_text_asBytes(PyObject* encoded_string, unsigned char** text){ PyBytes_AsStringAndSize(encoded_string, &buffer, &len); } - *text = calloc(len+1,1); + *text = calloc(len + 1, 1); if (*text) { memcpy(*text, buffer, len); } else { @@ -2228,23 +2645,21 @@ void _font_text_asBytes(PyObject* encoded_string, unsigned char** text){ return; } - -static PyObject* -_font_getmask(ImagingFontObject* self, PyObject* args) -{ +static PyObject * +_font_getmask(ImagingFontObject *self, PyObject *args) { Imaging im; Imaging bitmap; int x, b; - int i=0; + int i = 0; int status; - Glyph* glyph; + Glyph *glyph; - PyObject* encoded_string; + PyObject *encoded_string; - unsigned char* text; - char* mode = ""; + unsigned char *text; + char *mode = ""; - if (!PyArg_ParseTuple(args, "O|s:getmask", &encoded_string, &mode)){ + if (!PyArg_ParseTuple(args, "O|s:getmask", &encoded_string, &mode)) { return NULL; } @@ -2256,50 +2671,53 @@ _font_getmask(ImagingFontObject* self, PyObject* args) im = ImagingNew(self->bitmap->mode, textwidth(self, text), self->ysize); if (!im) { free(text); - ImagingError_MemoryError(); - return NULL; + return ImagingError_MemoryError(); } b = 0; - (void) ImagingFill(im, &b); + (void)ImagingFill(im, &b); b = self->baseline; for (x = 0; text[i]; i++) { glyph = &self->glyphs[text[i]]; - bitmap = ImagingCrop( - self->bitmap, - glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1 - ); - if (!bitmap) + bitmap = + ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1); + if (!bitmap) { goto failed; + } status = ImagingPaste( - im, bitmap, NULL, - glyph->dx0+x, glyph->dy0+b, glyph->dx1+x, glyph->dy1+b - ); + im, + bitmap, + NULL, + glyph->dx0 + x, + glyph->dy0 + b, + glyph->dx1 + x, + glyph->dy1 + b); ImagingDelete(bitmap); - if (status < 0) + if (status < 0) { goto failed; + } x = x + glyph->dx; b = b + glyph->dy; } free(text); return PyImagingNew(im); - failed: +failed: free(text); ImagingDelete(im); Py_RETURN_NONE; } -static PyObject* -_font_getsize(ImagingFontObject* self, PyObject* args) -{ - unsigned char* text; - PyObject* encoded_string; - PyObject* val; +static PyObject * +_font_getsize(ImagingFontObject *self, PyObject *args) { + unsigned char *text; + PyObject *encoded_string; + PyObject *val; - if (!PyArg_ParseTuple(args, "O:getsize", &encoded_string)) + if (!PyArg_ParseTuple(args, "O:getsize", &encoded_string)) { return NULL; + } _font_text_asBytes(encoded_string, &text); if (!text) { @@ -2312,26 +2730,27 @@ _font_getsize(ImagingFontObject* self, PyObject* args) } static struct PyMethodDef _font_methods[] = { - {"getmask", (PyCFunction)_font_getmask, 1}, - {"getsize", (PyCFunction)_font_getsize, 1}, + {"getmask", (PyCFunction)_font_getmask, METH_VARARGS}, + {"getsize", (PyCFunction)_font_getsize, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; /* -------------------------------------------------------------------- */ -static PyObject* -_draw_new(PyObject* self_, PyObject* args) -{ +static PyObject * +_draw_new(PyObject *self_, PyObject *args) { ImagingDrawObject *self; - ImagingObject* imagep; + ImagingObject *imagep; int blend = 0; - if (!PyArg_ParseTuple(args, "O!|i", &Imaging_Type, &imagep, &blend)) + if (!PyArg_ParseTuple(args, "O!|i", &Imaging_Type, &imagep, &blend)) { return NULL; + } self = PyObject_New(ImagingDrawObject, &ImagingDraw_Type); - if (self == NULL) + if (self == NULL) { return NULL; + } /* keep a reference to the image object */ Py_INCREF(imagep); @@ -2341,233 +2760,253 @@ _draw_new(PyObject* self_, PyObject* args) self->blend = blend; - return (PyObject*) self; + return (PyObject *)self; } static void -_draw_dealloc(ImagingDrawObject* self) -{ +_draw_dealloc(ImagingDrawObject *self) { Py_XDECREF(self->image); PyObject_Del(self); } -extern Py_ssize_t PyPath_Flatten(PyObject* data, double **xy); +extern Py_ssize_t +PyPath_Flatten(PyObject *data, double **xy); -static PyObject* -_draw_ink(ImagingDrawObject* self, PyObject* args) -{ +static PyObject * +_draw_ink(ImagingDrawObject *self, PyObject *args) { INT32 ink = 0; - PyObject* color; - char* mode = NULL; /* not used in this release */ - if (!PyArg_ParseTuple(args, "O|s", &color, &mode)) + PyObject *color; + if (!PyArg_ParseTuple(args, "O", &color)) { return NULL; + } - if (!getink(color, self->image->image, (char*) &ink)) + if (!getink(color, self->image->image, (char *)&ink)) { return NULL; + } - return PyInt_FromLong((int) ink); + return PyLong_FromLong((int)ink); } -static PyObject* -_draw_arc(ImagingDrawObject* self, PyObject* args) -{ - double* xy; +static PyObject * +_draw_arc(ImagingDrawObject *self, PyObject *args) { + double *xy; Py_ssize_t n; - PyObject* data; + PyObject *data; int ink; + int width = 0; float start, end; - int op = 0; - if (!PyArg_ParseTuple(args, "Offi|i", &data, &start, &end, &ink)) + if (!PyArg_ParseTuple(args, "Offi|i", &data, &start, &end, &ink, &width)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } if (n != 2) { PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); + free(xy); return NULL; } - n = ImagingDrawArc(self->image->image, - (int) xy[0], (int) xy[1], - (int) xy[2], (int) xy[3], - start, end, &ink, op - ); + n = ImagingDrawArc( + self->image->image, + (int)xy[0], + (int)xy[1], + (int)xy[2], + (int)xy[3], + start, + end, + &ink, + width, + self->blend); free(xy); - if (n < 0) + if (n < 0) { return NULL; + } Py_INCREF(Py_None); return Py_None; } -static PyObject* -_draw_bitmap(ImagingDrawObject* self, PyObject* args) -{ +static PyObject * +_draw_bitmap(ImagingDrawObject *self, PyObject *args) { double *xy; Py_ssize_t n; PyObject *data; - ImagingObject* bitmap; + ImagingObject *bitmap; int ink; - if (!PyArg_ParseTuple(args, "OO!i", &data, &Imaging_Type, &bitmap, &ink)) + if (!PyArg_ParseTuple(args, "OO!i", &data, &Imaging_Type, &bitmap, &ink)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } if (n != 1) { - PyErr_SetString(PyExc_TypeError, - "coordinate list must contain exactly 1 coordinate" - ); + PyErr_SetString( + PyExc_TypeError, "coordinate list must contain exactly 1 coordinate"); + free(xy); return NULL; } n = ImagingDrawBitmap( - self->image->image, (int) xy[0], (int) xy[1], bitmap->image, - &ink, self->blend - ); + self->image->image, (int)xy[0], (int)xy[1], bitmap->image, &ink, self->blend); free(xy); - if (n < 0) + if (n < 0) { return NULL; + } Py_INCREF(Py_None); return Py_None; } -static PyObject* -_draw_chord(ImagingDrawObject* self, PyObject* args) -{ - double* xy; +static PyObject * +_draw_chord(ImagingDrawObject *self, PyObject *args) { + double *xy; Py_ssize_t n; - PyObject* data; + PyObject *data; int ink, fill; + int width = 0; float start, end; - if (!PyArg_ParseTuple(args, "Offii", - &data, &start, &end, &ink, &fill)) + if (!PyArg_ParseTuple(args, "Offii|i", &data, &start, &end, &ink, &fill, &width)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } if (n != 2) { PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); + free(xy); return NULL; } - n = ImagingDrawChord(self->image->image, - (int) xy[0], (int) xy[1], - (int) xy[2], (int) xy[3], - start, end, &ink, fill, self->blend - ); + n = ImagingDrawChord( + self->image->image, + (int)xy[0], + (int)xy[1], + (int)xy[2], + (int)xy[3], + start, + end, + &ink, + fill, + width, + self->blend); free(xy); - if (n < 0) + if (n < 0) { return NULL; + } Py_INCREF(Py_None); return Py_None; } -static PyObject* -_draw_ellipse(ImagingDrawObject* self, PyObject* args) -{ - double* xy; +static PyObject * +_draw_ellipse(ImagingDrawObject *self, PyObject *args) { + double *xy; Py_ssize_t n; - PyObject* data; + PyObject *data; int ink; int fill = 0; - if (!PyArg_ParseTuple(args, "Oi|i", &data, &ink, &fill)) + int width = 0; + if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } if (n != 2) { PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); + free(xy); return NULL; } - n = ImagingDrawEllipse(self->image->image, - (int) xy[0], (int) xy[1], - (int) xy[2], (int) xy[3], - &ink, fill, self->blend - ); + n = ImagingDrawEllipse( + self->image->image, + (int)xy[0], + (int)xy[1], + (int)xy[2], + (int)xy[3], + &ink, + fill, + width, + self->blend); free(xy); - if (n < 0) - return NULL; - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject* -_draw_line(ImagingDrawObject* self, PyObject* args) -{ - int x0, y0, x1, y1; - int ink; - if (!PyArg_ParseTuple(args, "(ii)(ii)i", &x0, &y0, &x1, &y1, &ink)) - return NULL; - - if (ImagingDrawLine(self->image->image, x0, y0, x1, y1, - &ink, self->blend) < 0) + if (n < 0) { return NULL; + } Py_INCREF(Py_None); return Py_None; } -static PyObject* -_draw_lines(ImagingDrawObject* self, PyObject* args) -{ +static PyObject * +_draw_lines(ImagingDrawObject *self, PyObject *args) { double *xy; Py_ssize_t i, n; PyObject *data; int ink; int width = 0; - if (!PyArg_ParseTuple(args, "Oi|i", &data, &ink, &width)) + if (!PyArg_ParseTuple(args, "Oi|i", &data, &ink, &width)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } if (width <= 1) { double *p = NULL; - for (i = 0; i < n-1; i++) { - p = &xy[i+i]; + for (i = 0; i < n - 1; i++) { + p = &xy[i + i]; if (ImagingDrawLine( self->image->image, - (int) p[0], (int) p[1], (int) p[2], (int) p[3], - &ink, self->blend) < 0) { + (int)p[0], + (int)p[1], + (int)p[2], + (int)p[3], + &ink, + self->blend) < 0) { free(xy); return NULL; } } - if (p) /* draw last point */ + if (p) { /* draw last point */ ImagingDrawPoint( - self->image->image, - (int) p[2], (int) p[3], - &ink, self->blend - ); + self->image->image, (int)p[2], (int)p[3], &ink, self->blend); + } } else { - for (i = 0; i < n-1; i++) { - double *p = &xy[i+i]; + for (i = 0; i < n - 1; i++) { + double *p = &xy[i + i]; if (ImagingDrawWideLine( self->image->image, - (int) p[0], (int) p[1], (int) p[2], (int) p[3], - &ink, width, self->blend) < 0) { + (int)p[0], + (int)p[1], + (int)p[2], + (int)p[3], + &ink, + width, + self->blend) < 0) { free(xy); return NULL; } @@ -2580,43 +3019,29 @@ _draw_lines(ImagingDrawObject* self, PyObject* args) return Py_None; } -static PyObject* -_draw_point(ImagingDrawObject* self, PyObject* args) -{ - int x, y; - int ink; - if (!PyArg_ParseTuple(args, "(ii)i", &x, &y, &ink)) - return NULL; - - if (ImagingDrawPoint(self->image->image, x, y, &ink, self->blend) < 0) - return NULL; - - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject* -_draw_points(ImagingDrawObject* self, PyObject* args) -{ +static PyObject * +_draw_points(ImagingDrawObject *self, PyObject *args) { double *xy; Py_ssize_t i, n; PyObject *data; int ink; - if (!PyArg_ParseTuple(args, "Oi", &data, &ink)) + if (!PyArg_ParseTuple(args, "Oi", &data, &ink)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } for (i = 0; i < n; i++) { - double *p = &xy[i+i]; - if (ImagingDrawPoint(self->image->image, (int) p[0], (int) p[1], - &ink, self->blend) < 0) { - free(xy); - return NULL; - } + double *p = &xy[i + i]; + if (ImagingDrawPoint( + self->image->image, (int)p[0], (int)p[1], &ink, self->blend) < 0) { + free(xy); + return NULL; + } } free(xy); @@ -2625,21 +3050,22 @@ _draw_points(ImagingDrawObject* self, PyObject* args) return Py_None; } -#ifdef WITH_ARROW +#ifdef WITH_ARROW /* from outline.c */ -extern ImagingOutline PyOutline_AsOutline(PyObject* outline); +extern ImagingOutline +PyOutline_AsOutline(PyObject *outline); -static PyObject* -_draw_outline(ImagingDrawObject* self, PyObject* args) -{ +static PyObject * +_draw_outline(ImagingDrawObject *self, PyObject *args) { ImagingOutline outline; - PyObject* outline_; + PyObject *outline_; int ink; int fill = 0; - if (!PyArg_ParseTuple(args, "Oi|i", &outline_, &ink, &fill)) + if (!PyArg_ParseTuple(args, "Oi|i", &outline_, &ink, &fill)) { return NULL; + } outline = PyOutline_AsOutline(outline_); if (!outline) { @@ -2647,9 +3073,9 @@ _draw_outline(ImagingDrawObject* self, PyObject* args) return NULL; } - if (ImagingDrawOutline(self->image->image, outline, - &ink, fill, self->blend) < 0) - return NULL; + if (ImagingDrawOutline(self->image->image, outline, &ink, fill, self->blend) < 0) { + return NULL; + } Py_INCREF(Py_None); return Py_None; @@ -2657,76 +3083,92 @@ _draw_outline(ImagingDrawObject* self, PyObject* args) #endif -static PyObject* -_draw_pieslice(ImagingDrawObject* self, PyObject* args) -{ - double* xy; +static PyObject * +_draw_pieslice(ImagingDrawObject *self, PyObject *args) { + double *xy; Py_ssize_t n; - PyObject* data; + PyObject *data; int ink, fill; + int width = 0; float start, end; - if (!PyArg_ParseTuple(args, "Offii", &data, &start, &end, &ink, &fill)) + if (!PyArg_ParseTuple(args, "Offii|i", &data, &start, &end, &ink, &fill, &width)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } if (n != 2) { PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); + free(xy); return NULL; } - n = ImagingDrawPieslice(self->image->image, - (int) xy[0], (int) xy[1], - (int) xy[2], (int) xy[3], - start, end, &ink, fill, self->blend - ); + n = ImagingDrawPieslice( + self->image->image, + (int)xy[0], + (int)xy[1], + (int)xy[2], + (int)xy[3], + start, + end, + &ink, + fill, + width, + self->blend); free(xy); - if (n < 0) + if (n < 0) { return NULL; + } Py_INCREF(Py_None); return Py_None; } -static PyObject* -_draw_polygon(ImagingDrawObject* self, PyObject* args) -{ +static PyObject * +_draw_polygon(ImagingDrawObject *self, PyObject *args) { double *xy; int *ixy; Py_ssize_t n, i; - PyObject* data; + PyObject *data; int ink; int fill = 0; - if (!PyArg_ParseTuple(args, "Oi|i", &data, &ink, &fill)) + int width = 0; + if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } if (n < 2) { - PyErr_SetString(PyExc_TypeError, - "coordinate list must contain at least 2 coordinates" - ); + PyErr_SetString( + PyExc_TypeError, "coordinate list must contain at least 2 coordinates"); + free(xy); return NULL; } /* Copy list of vertices to array */ - ixy = (int*) calloc(n, 2 * sizeof(int)); + ixy = (int *)calloc(n, 2 * sizeof(int)); + if (ixy == NULL) { + free(xy); + return ImagingError_MemoryError(); + } for (i = 0; i < n; i++) { - ixy[i+i] = (int) xy[i+i]; - ixy[i+i+1] = (int) xy[i+i+1]; + ixy[i + i] = (int)xy[i + i]; + ixy[i + i + 1] = (int)xy[i + i + 1]; } free(xy); - if (ImagingDrawPolygon(self->image->image, n, ixy, - &ink, fill, self->blend) < 0) { + if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, width, self->blend) < 0) { free(ixy); return NULL; } @@ -2737,36 +3179,45 @@ _draw_polygon(ImagingDrawObject* self, PyObject* args) return Py_None; } -static PyObject* -_draw_rectangle(ImagingDrawObject* self, PyObject* args) -{ - double* xy; +static PyObject * +_draw_rectangle(ImagingDrawObject *self, PyObject *args) { + double *xy; Py_ssize_t n; - PyObject* data; + PyObject *data; int ink; int fill = 0; - if (!PyArg_ParseTuple(args, "Oi|i", &data, &ink, &fill)) + int width = 0; + if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) { return NULL; + } n = PyPath_Flatten(data, &xy); - if (n < 0) + if (n < 0) { return NULL; + } if (n != 2) { PyErr_SetString(PyExc_TypeError, must_be_two_coordinates); + free(xy); return NULL; } - n = ImagingDrawRectangle(self->image->image, - (int) xy[0], (int) xy[1], - (int) xy[2], (int) xy[3], - &ink, fill, self->blend - ); + n = ImagingDrawRectangle( + self->image->image, + (int)xy[0], + (int)xy[1], + (int)xy[2], + (int)xy[3], + &ink, + fill, + width, + self->blend); free(xy); - if (n < 0) + if (n < 0) { return NULL; + } Py_INCREF(Py_None); return Py_None; @@ -2775,40 +3226,38 @@ _draw_rectangle(ImagingDrawObject* self, PyObject* args) static struct PyMethodDef _draw_methods[] = { #ifdef WITH_IMAGEDRAW /* Graphics (ImageDraw) */ - {"draw_line", (PyCFunction)_draw_line, 1}, - {"draw_lines", (PyCFunction)_draw_lines, 1}, + {"draw_lines", (PyCFunction)_draw_lines, METH_VARARGS}, #ifdef WITH_ARROW - {"draw_outline", (PyCFunction)_draw_outline, 1}, + {"draw_outline", (PyCFunction)_draw_outline, METH_VARARGS}, #endif - {"draw_polygon", (PyCFunction)_draw_polygon, 1}, - {"draw_rectangle", (PyCFunction)_draw_rectangle, 1}, - {"draw_point", (PyCFunction)_draw_point, 1}, - {"draw_points", (PyCFunction)_draw_points, 1}, - {"draw_arc", (PyCFunction)_draw_arc, 1}, - {"draw_bitmap", (PyCFunction)_draw_bitmap, 1}, - {"draw_chord", (PyCFunction)_draw_chord, 1}, - {"draw_ellipse", (PyCFunction)_draw_ellipse, 1}, - {"draw_pieslice", (PyCFunction)_draw_pieslice, 1}, - {"draw_ink", (PyCFunction)_draw_ink, 1}, + {"draw_polygon", (PyCFunction)_draw_polygon, METH_VARARGS}, + {"draw_rectangle", (PyCFunction)_draw_rectangle, METH_VARARGS}, + {"draw_points", (PyCFunction)_draw_points, METH_VARARGS}, + {"draw_arc", (PyCFunction)_draw_arc, METH_VARARGS}, + {"draw_bitmap", (PyCFunction)_draw_bitmap, METH_VARARGS}, + {"draw_chord", (PyCFunction)_draw_chord, METH_VARARGS}, + {"draw_ellipse", (PyCFunction)_draw_ellipse, METH_VARARGS}, + {"draw_pieslice", (PyCFunction)_draw_pieslice, METH_VARARGS}, + {"draw_ink", (PyCFunction)_draw_ink, METH_VARARGS}, #endif {NULL, NULL} /* sentinel */ }; #endif - -static PyObject* -pixel_access_new(ImagingObject* imagep, PyObject* args) -{ +static PyObject * +pixel_access_new(ImagingObject *imagep, PyObject *args) { PixelAccessObject *self; int readonly = 0; - if (!PyArg_ParseTuple(args, "|i", &readonly)) + if (!PyArg_ParseTuple(args, "|i", &readonly)) { return NULL; + } self = PyObject_New(PixelAccessObject, &PixelAccess_Type); - if (self == NULL) + if (self == NULL) { return NULL; + } /* keep a reference to the image object */ Py_INCREF(imagep); @@ -2816,51 +3265,59 @@ pixel_access_new(ImagingObject* imagep, PyObject* args) self->readonly = readonly; - return (PyObject*) self; + return (PyObject *)self; } static void -pixel_access_dealloc(PixelAccessObject* self) -{ +pixel_access_dealloc(PixelAccessObject *self) { Py_XDECREF(self->image); PyObject_Del(self); } static PyObject * -pixel_access_getitem(PixelAccessObject *self, PyObject *xy) -{ +pixel_access_getitem(PixelAccessObject *self, PyObject *xy) { int x, y; - if (_getxy(xy, &x, &y)) + if (_getxy(xy, &x, &y)) { return NULL; + } return getpixel(self->image->image, self->image->access, x, y); } static int -pixel_access_setitem(PixelAccessObject *self, PyObject *xy, PyObject *color) -{ +pixel_access_setitem(PixelAccessObject *self, PyObject *xy, PyObject *color) { Imaging im = self->image->image; char ink[4]; int x, y; if (self->readonly) { - (void) ImagingError_ValueError(readonly); + (void)ImagingError_ValueError(readonly); return -1; } - if (_getxy(xy, &x, &y)) + if (_getxy(xy, &x, &y)) { return -1; + } + + if (x < 0) { + x = im->xsize + x; + } + if (y < 0) { + y = im->ysize + y; + } if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { PyErr_SetString(PyExc_IndexError, outside_image); return -1; } - if (!color) /* FIXME: raise exception? */ + if (!color) { /* FIXME: raise exception? */ return 0; + } - if (!getink(color, im, ink)) + if (!getink(color, im, ink)) { return -1; + } self->image->access->put_pixel(im, x, y, ink); @@ -2873,43 +3330,52 @@ pixel_access_setitem(PixelAccessObject *self, PyObject *xy, PyObject *color) #ifdef WITH_EFFECTS -static PyObject* -_effect_mandelbrot(ImagingObject* self, PyObject* args) -{ +static PyObject * +_effect_mandelbrot(ImagingObject *self, PyObject *args) { int xsize = 512; int ysize = 512; double extent[4]; int quality = 100; - extent[0] = -3; extent[1] = -2.5; - extent[2] = 2; extent[3] = 2.5; - - if (!PyArg_ParseTuple(args, "|(ii)(dddd)i", &xsize, &ysize, - &extent[0], &extent[1], &extent[2], &extent[3], - &quality)) - return NULL; + extent[0] = -3; + extent[1] = -2.5; + extent[2] = 2; + extent[3] = 2.5; + + if (!PyArg_ParseTuple( + args, + "|(ii)(dddd)i", + &xsize, + &ysize, + &extent[0], + &extent[1], + &extent[2], + &extent[3], + &quality)) { + return NULL; + } return PyImagingNew(ImagingEffectMandelbrot(xsize, ysize, extent, quality)); } -static PyObject* -_effect_noise(ImagingObject* self, PyObject* args) -{ +static PyObject * +_effect_noise(ImagingObject *self, PyObject *args) { int xsize, ysize; float sigma = 128; - if (!PyArg_ParseTuple(args, "(ii)|f", &xsize, &ysize, &sigma)) + if (!PyArg_ParseTuple(args, "(ii)|f", &xsize, &ysize, &sigma)) { return NULL; + } return PyImagingNew(ImagingEffectNoise(xsize, ysize, sigma)); } -static PyObject* -_effect_spread(ImagingObject* self, PyObject* args) -{ +static PyObject * +_effect_spread(ImagingObject *self, PyObject *args) { int dist; - if (!PyArg_ParseTuple(args, "i", &dist)) + if (!PyArg_ParseTuple(args, "i", &dist)) { return NULL; + } return PyImagingNew(ImagingEffectSpread(self->image, dist)); } @@ -2920,49 +3386,33 @@ _effect_spread(ImagingObject* self, PyObject* args) /* UTILITIES */ /* -------------------------------------------------------------------- */ -static PyObject* -_crc32(PyObject* self, PyObject* args) -{ - unsigned char* buffer; - int bytes; - int hi, lo; - UINT32 crc; - - hi = lo = 0; - - if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH"|(ii)", - &buffer, &bytes, &hi, &lo)) - return NULL; - - crc = ((UINT32) (hi & 0xFFFF) << 16) + (lo & 0xFFFF); - - crc = ImagingCRC32(crc, (unsigned char *)buffer, bytes); - - return Py_BuildValue("ii", (crc >> 16) & 0xFFFF, crc & 0xFFFF); -} - -static PyObject* -_getcodecstatus(PyObject* self, PyObject* args) -{ +static PyObject * +_getcodecstatus(PyObject *self, PyObject *args) { int status; - char* msg; + char *msg; - if (!PyArg_ParseTuple(args, "i", &status)) + if (!PyArg_ParseTuple(args, "i", &status)) { return NULL; + } switch (status) { - case IMAGING_CODEC_OVERRUN: - msg = "buffer overrun"; break; - case IMAGING_CODEC_BROKEN: - msg = "broken data stream"; break; - case IMAGING_CODEC_UNKNOWN: - msg = "unrecognized data stream contents"; break; - case IMAGING_CODEC_CONFIG: - msg = "codec configuration error"; break; - case IMAGING_CODEC_MEMORY: - msg = "out of memory"; break; - default: - Py_RETURN_NONE; + case IMAGING_CODEC_OVERRUN: + msg = "buffer overrun"; + break; + case IMAGING_CODEC_BROKEN: + msg = "broken data stream"; + break; + case IMAGING_CODEC_UNKNOWN: + msg = "unrecognized data stream contents"; + break; + case IMAGING_CODEC_CONFIG: + msg = "codec configuration error"; + break; + case IMAGING_CODEC_MEMORY: + msg = "out of memory"; + break; + default: + Py_RETURN_NONE; } return PyUnicode_FromString(msg); @@ -2972,23 +3422,22 @@ _getcodecstatus(PyObject* self, PyObject* args) /* DEBUGGING HELPERS */ /* -------------------------------------------------------------------- */ +static PyObject * +_save_ppm(ImagingObject *self, PyObject *args) { + char *filename; -static PyObject* -_save_ppm(ImagingObject* self, PyObject* args) -{ - char* filename; - - if (!PyArg_ParseTuple(args, "s", &filename)) + if (!PyArg_ParseTuple(args, "s", &filename)) { return NULL; + } - if (!ImagingSavePPM(self->image, filename)) + if (!ImagingSavePPM(self->image, filename)) { return NULL; + } Py_INCREF(Py_None); return Py_None; } - /* -------------------------------------------------------------------- */ /* methods */ @@ -2996,356 +3445,351 @@ _save_ppm(ImagingObject* self, PyObject* args) static struct PyMethodDef methods[] = { /* Put commonly used methods first */ - {"getpixel", (PyCFunction)_getpixel, 1}, - {"putpixel", (PyCFunction)_putpixel, 1}, + {"getpixel", (PyCFunction)_getpixel, METH_VARARGS}, + {"putpixel", (PyCFunction)_putpixel, METH_VARARGS}, - {"pixel_access", (PyCFunction)pixel_access_new, 1}, + {"pixel_access", (PyCFunction)pixel_access_new, METH_VARARGS}, /* Standard processing methods (Image) */ - {"convert", (PyCFunction)_convert, 1}, - {"convert2", (PyCFunction)_convert2, 1}, - {"convert_matrix", (PyCFunction)_convert_matrix, 1}, - {"convert_transparent", (PyCFunction)_convert_transparent, 1}, - {"copy", (PyCFunction)_copy, 1}, - {"crop", (PyCFunction)_crop, 1}, - {"expand", (PyCFunction)_expand_image, 1}, - {"filter", (PyCFunction)_filter, 1}, - {"histogram", (PyCFunction)_histogram, 1}, + {"color_lut_3d", (PyCFunction)_color_lut_3d, METH_VARARGS}, + {"convert", (PyCFunction)_convert, METH_VARARGS}, + {"convert2", (PyCFunction)_convert2, METH_VARARGS}, + {"convert_matrix", (PyCFunction)_convert_matrix, METH_VARARGS}, + {"convert_transparent", (PyCFunction)_convert_transparent, METH_VARARGS}, + {"copy", (PyCFunction)_copy, METH_VARARGS}, + {"crop", (PyCFunction)_crop, METH_VARARGS}, + {"expand", (PyCFunction)_expand_image, METH_VARARGS}, + {"filter", (PyCFunction)_filter, METH_VARARGS}, + {"histogram", (PyCFunction)_histogram, METH_VARARGS}, + {"entropy", (PyCFunction)_entropy, METH_VARARGS}, #ifdef WITH_MODEFILTER - {"modefilter", (PyCFunction)_modefilter, 1}, + {"modefilter", (PyCFunction)_modefilter, METH_VARARGS}, #endif - {"offset", (PyCFunction)_offset, 1}, - {"paste", (PyCFunction)_paste, 1}, - {"point", (PyCFunction)_point, 1}, - {"point_transform", (PyCFunction)_point_transform, 1}, - {"putdata", (PyCFunction)_putdata, 1}, + {"offset", (PyCFunction)_offset, METH_VARARGS}, + {"paste", (PyCFunction)_paste, METH_VARARGS}, + {"point", (PyCFunction)_point, METH_VARARGS}, + {"point_transform", (PyCFunction)_point_transform, METH_VARARGS}, + {"putdata", (PyCFunction)_putdata, METH_VARARGS}, #ifdef WITH_QUANTIZE - {"quantize", (PyCFunction)_quantize, 1}, + {"quantize", (PyCFunction)_quantize, METH_VARARGS}, #endif #ifdef WITH_RANKFILTER - {"rankfilter", (PyCFunction)_rankfilter, 1}, + {"rankfilter", (PyCFunction)_rankfilter, METH_VARARGS}, #endif - {"resize", (PyCFunction)_resize, 1}, - {"transpose", (PyCFunction)_transpose, 1}, - {"transform2", (PyCFunction)_transform2, 1}, + {"resize", (PyCFunction)_resize, METH_VARARGS}, + {"reduce", (PyCFunction)_reduce, METH_VARARGS}, + {"transpose", (PyCFunction)_transpose, METH_VARARGS}, + {"transform2", (PyCFunction)_transform2, METH_VARARGS}, - {"isblock", (PyCFunction)_isblock, 1}, + {"isblock", (PyCFunction)_isblock, METH_NOARGS}, - {"getbbox", (PyCFunction)_getbbox, 1}, - {"getcolors", (PyCFunction)_getcolors, 1}, - {"getextrema", (PyCFunction)_getextrema, 1}, - {"getprojection", (PyCFunction)_getprojection, 1}, + {"getbbox", (PyCFunction)_getbbox, METH_NOARGS}, + {"getcolors", (PyCFunction)_getcolors, METH_VARARGS}, + {"getextrema", (PyCFunction)_getextrema, METH_NOARGS}, + {"getprojection", (PyCFunction)_getprojection, METH_NOARGS}, - {"getband", (PyCFunction)_getband, 1}, - {"putband", (PyCFunction)_putband, 1}, - {"split", (PyCFunction)_split, 1}, - {"fillband", (PyCFunction)_fillband, 1}, + {"getband", (PyCFunction)_getband, METH_VARARGS}, + {"putband", (PyCFunction)_putband, METH_VARARGS}, + {"split", (PyCFunction)_split, METH_NOARGS}, + {"fillband", (PyCFunction)_fillband, METH_VARARGS}, - {"setmode", (PyCFunction)im_setmode, 1}, + {"setmode", (PyCFunction)im_setmode, METH_VARARGS}, - {"getpalette", (PyCFunction)_getpalette, 1}, - {"getpalettemode", (PyCFunction)_getpalettemode, 1}, - {"putpalette", (PyCFunction)_putpalette, 1}, - {"putpalettealpha", (PyCFunction)_putpalettealpha, 1}, - {"putpalettealphas", (PyCFunction)_putpalettealphas, 1}, + {"getpalette", (PyCFunction)_getpalette, METH_VARARGS}, + {"getpalettemode", (PyCFunction)_getpalettemode, METH_NOARGS}, + {"putpalette", (PyCFunction)_putpalette, METH_VARARGS}, + {"putpalettealpha", (PyCFunction)_putpalettealpha, METH_VARARGS}, + {"putpalettealphas", (PyCFunction)_putpalettealphas, METH_VARARGS}, #ifdef WITH_IMAGECHOPS /* Channel operations (ImageChops) */ - {"chop_invert", (PyCFunction)_chop_invert, 1}, - {"chop_lighter", (PyCFunction)_chop_lighter, 1}, - {"chop_darker", (PyCFunction)_chop_darker, 1}, - {"chop_difference", (PyCFunction)_chop_difference, 1}, - {"chop_multiply", (PyCFunction)_chop_multiply, 1}, - {"chop_screen", (PyCFunction)_chop_screen, 1}, - {"chop_add", (PyCFunction)_chop_add, 1}, - {"chop_subtract", (PyCFunction)_chop_subtract, 1}, - {"chop_add_modulo", (PyCFunction)_chop_add_modulo, 1}, - {"chop_subtract_modulo", (PyCFunction)_chop_subtract_modulo, 1}, - {"chop_and", (PyCFunction)_chop_and, 1}, - {"chop_or", (PyCFunction)_chop_or, 1}, - {"chop_xor", (PyCFunction)_chop_xor, 1}, + {"chop_invert", (PyCFunction)_chop_invert, METH_NOARGS}, + {"chop_lighter", (PyCFunction)_chop_lighter, METH_VARARGS}, + {"chop_darker", (PyCFunction)_chop_darker, METH_VARARGS}, + {"chop_difference", (PyCFunction)_chop_difference, METH_VARARGS}, + {"chop_multiply", (PyCFunction)_chop_multiply, METH_VARARGS}, + {"chop_screen", (PyCFunction)_chop_screen, METH_VARARGS}, + {"chop_add", (PyCFunction)_chop_add, METH_VARARGS}, + {"chop_subtract", (PyCFunction)_chop_subtract, METH_VARARGS}, + {"chop_add_modulo", (PyCFunction)_chop_add_modulo, METH_VARARGS}, + {"chop_subtract_modulo", (PyCFunction)_chop_subtract_modulo, METH_VARARGS}, + {"chop_and", (PyCFunction)_chop_and, METH_VARARGS}, + {"chop_or", (PyCFunction)_chop_or, METH_VARARGS}, + {"chop_xor", (PyCFunction)_chop_xor, METH_VARARGS}, + {"chop_soft_light", (PyCFunction)_chop_soft_light, METH_VARARGS}, + {"chop_hard_light", (PyCFunction)_chop_hard_light, METH_VARARGS}, + {"chop_overlay", (PyCFunction)_chop_overlay, METH_VARARGS}, + #endif #ifdef WITH_UNSHARPMASK /* Kevin Cazabon's unsharpmask extension */ - {"gaussian_blur", (PyCFunction)_gaussian_blur, 1}, - {"unsharp_mask", (PyCFunction)_unsharp_mask, 1}, + {"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS}, + {"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS}, #endif - {"box_blur", (PyCFunction)_box_blur, 1}, + {"box_blur", (PyCFunction)_box_blur, METH_VARARGS}, #ifdef WITH_EFFECTS /* Special effects */ - {"effect_spread", (PyCFunction)_effect_spread, 1}, + {"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS}, #endif /* Misc. */ - {"new_block", (PyCFunction)_new_block, 1}, + {"new_block", (PyCFunction)_new_block, METH_VARARGS}, - {"save_ppm", (PyCFunction)_save_ppm, 1}, + {"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; - /* attributes */ -static PyObject* -_getattr_mode(ImagingObject* self, void* closure) -{ +static PyObject * +_getattr_mode(ImagingObject *self, void *closure) { return PyUnicode_FromString(self->image->mode); } -static PyObject* -_getattr_size(ImagingObject* self, void* closure) -{ +static PyObject * +_getattr_size(ImagingObject *self, void *closure) { return Py_BuildValue("ii", self->image->xsize, self->image->ysize); } -static PyObject* -_getattr_bands(ImagingObject* self, void* closure) -{ - return PyInt_FromLong(self->image->bands); +static PyObject * +_getattr_bands(ImagingObject *self, void *closure) { + return PyLong_FromLong(self->image->bands); } -static PyObject* -_getattr_id(ImagingObject* self, void* closure) -{ - return PyInt_FromSsize_t((Py_ssize_t) self->image); +static PyObject * +_getattr_id(ImagingObject *self, void *closure) { + return PyLong_FromSsize_t((Py_ssize_t)self->image); } -static PyObject* -_getattr_ptr(ImagingObject* self, void* closure) -{ +static PyObject * +_getattr_ptr(ImagingObject *self, void *closure) { return PyCapsule_New(self->image, IMAGING_MAGIC, NULL); } -static PyObject* -_getattr_unsafe_ptrs(ImagingObject* self, void* closure) -{ - return Py_BuildValue("(sn)(sn)(sn)", - "image8", self->image->image8, - "image32", self->image->image32, - "image", self->image->image - ); +static PyObject * +_getattr_unsafe_ptrs(ImagingObject *self, void *closure) { + return Py_BuildValue( + "(sn)(sn)(sn)", + "image8", + self->image->image8, + "image32", + self->image->image32, + "image", + self->image->image); }; - static struct PyGetSetDef getsetters[] = { - { "mode", (getter) _getattr_mode }, - { "size", (getter) _getattr_size }, - { "bands", (getter) _getattr_bands }, - { "id", (getter) _getattr_id }, - { "ptr", (getter) _getattr_ptr }, - { "unsafe_ptrs", (getter) _getattr_unsafe_ptrs }, - { NULL } -}; + {"mode", (getter)_getattr_mode}, + {"size", (getter)_getattr_size}, + {"bands", (getter)_getattr_bands}, + {"id", (getter)_getattr_id}, + {"ptr", (getter)_getattr_ptr}, + {"unsafe_ptrs", (getter)_getattr_unsafe_ptrs}, + {NULL}}; /* basic sequence semantics */ static Py_ssize_t -image_length(ImagingObject *self) -{ +image_length(ImagingObject *self) { Imaging im = self->image; - return (Py_ssize_t) im->xsize * im->ysize; + return (Py_ssize_t)im->xsize * im->ysize; } static PyObject * -image_item(ImagingObject *self, Py_ssize_t i) -{ +image_item(ImagingObject *self, Py_ssize_t i) { int x, y; Imaging im = self->image; if (im->xsize > 0) { x = i % im->xsize; y = i / im->xsize; - } else + } else { x = y = 0; /* leave it to getpixel to raise an exception */ + } return getpixel(im, self->access, x, y); } static PySequenceMethods image_as_sequence = { - (lenfunc) image_length, /*sq_length*/ - (binaryfunc) NULL, /*sq_concat*/ - (ssizeargfunc) NULL, /*sq_repeat*/ - (ssizeargfunc) image_item, /*sq_item*/ - (ssizessizeargfunc) NULL, /*sq_slice*/ - (ssizeobjargproc) NULL, /*sq_ass_item*/ - (ssizessizeobjargproc) NULL, /*sq_ass_slice*/ + (lenfunc)image_length, /*sq_length*/ + (binaryfunc)NULL, /*sq_concat*/ + (ssizeargfunc)NULL, /*sq_repeat*/ + (ssizeargfunc)image_item, /*sq_item*/ + (ssizessizeargfunc)NULL, /*sq_slice*/ + (ssizeobjargproc)NULL, /*sq_ass_item*/ + (ssizessizeobjargproc)NULL, /*sq_ass_slice*/ }; - /* type description */ static PyTypeObject Imaging_Type = { - PyVarObject_HEAD_INIT(NULL, 0) - "ImagingCore", /*tp_name*/ - sizeof(ImagingObject), /*tp_size*/ - 0, /*tp_itemsize*/ + PyVarObject_HEAD_INIT(NULL, 0) "ImagingCore", /*tp_name*/ + sizeof(ImagingObject), /*tp_size*/ + 0, /*tp_itemsize*/ /* methods */ - (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - &image_as_sequence, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - getsetters, /*tp_getset*/ + (destructor)_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + &image_as_sequence, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + methods, /*tp_methods*/ + 0, /*tp_members*/ + getsetters, /*tp_getset*/ }; #ifdef WITH_IMAGEDRAW static PyTypeObject ImagingFont_Type = { - PyVarObject_HEAD_INIT(NULL, 0) - "ImagingFont", /*tp_name*/ - sizeof(ImagingFontObject), /*tp_size*/ - 0, /*tp_itemsize*/ + PyVarObject_HEAD_INIT(NULL, 0) "ImagingFont", /*tp_name*/ + sizeof(ImagingFontObject), /*tp_size*/ + 0, /*tp_itemsize*/ /* methods */ - (destructor)_font_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _font_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + (destructor)_font_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + _font_methods, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ }; static PyTypeObject ImagingDraw_Type = { - PyVarObject_HEAD_INIT(NULL, 0) - "ImagingDraw", /*tp_name*/ - sizeof(ImagingDrawObject), /*tp_size*/ - 0, /*tp_itemsize*/ + PyVarObject_HEAD_INIT(NULL, 0) "ImagingDraw", /*tp_name*/ + sizeof(ImagingDrawObject), /*tp_size*/ + 0, /*tp_itemsize*/ /* methods */ - (destructor)_draw_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _draw_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + (destructor)_draw_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + _draw_methods, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ }; #endif static PyMappingMethods pixel_access_as_mapping = { - (lenfunc) NULL, /*mp_length*/ - (binaryfunc) pixel_access_getitem, /*mp_subscript*/ - (objobjargproc) pixel_access_setitem, /*mp_ass_subscript*/ + (lenfunc)NULL, /*mp_length*/ + (binaryfunc)pixel_access_getitem, /*mp_subscript*/ + (objobjargproc)pixel_access_setitem, /*mp_ass_subscript*/ }; /* type description */ static PyTypeObject PixelAccess_Type = { - PyVarObject_HEAD_INIT(NULL, 0) - "PixelAccess", sizeof(PixelAccessObject), 0, + PyVarObject_HEAD_INIT(NULL, 0) "PixelAccess", + sizeof(PixelAccessObject), + 0, /* methods */ (destructor)pixel_access_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - &pixel_access_as_mapping, /*tp_as_mapping */ - 0 /*tp_hash*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + &pixel_access_as_mapping, /*tp_as_mapping */ + 0 /*tp_hash*/ }; /* -------------------------------------------------------------------- */ -static PyObject* -_get_stats(PyObject* self, PyObject* args) -{ - PyObject* d; +static PyObject * +_get_stats(PyObject *self, PyObject *args) { + PyObject *d; ImagingMemoryArena arena = &ImagingDefaultArena; - if (!PyArg_ParseTuple(args, ":get_stats")) + if (!PyArg_ParseTuple(args, ":get_stats")) { return NULL; + } d = PyDict_New(); - if ( ! d) - return NULL; - PyDict_SetItemString(d, "new_count", - PyInt_FromLong(arena->stats_new_count)); - PyDict_SetItemString(d, "allocated_blocks", - PyInt_FromLong(arena->stats_allocated_blocks)); - PyDict_SetItemString(d, "reused_blocks", - PyInt_FromLong(arena->stats_reused_blocks)); - PyDict_SetItemString(d, "reallocated_blocks", - PyInt_FromLong(arena->stats_reallocated_blocks)); - PyDict_SetItemString(d, "freed_blocks", - PyInt_FromLong(arena->stats_freed_blocks)); - PyDict_SetItemString(d, "blocks_cached", - PyInt_FromLong(arena->blocks_cached)); + if (!d) { + return NULL; + } + PyDict_SetItemString(d, "new_count", PyLong_FromLong(arena->stats_new_count)); + PyDict_SetItemString( + d, "allocated_blocks", PyLong_FromLong(arena->stats_allocated_blocks)); + PyDict_SetItemString( + d, "reused_blocks", PyLong_FromLong(arena->stats_reused_blocks)); + PyDict_SetItemString( + d, "reallocated_blocks", PyLong_FromLong(arena->stats_reallocated_blocks)); + PyDict_SetItemString(d, "freed_blocks", PyLong_FromLong(arena->stats_freed_blocks)); + PyDict_SetItemString(d, "blocks_cached", PyLong_FromLong(arena->blocks_cached)); return d; } -static PyObject* -_reset_stats(PyObject* self, PyObject* args) -{ +static PyObject * +_reset_stats(PyObject *self, PyObject *args) { ImagingMemoryArena arena = &ImagingDefaultArena; - if (!PyArg_ParseTuple(args, ":reset_stats")) + if (!PyArg_ParseTuple(args, ":reset_stats")) { return NULL; - + } + arena->stats_new_count = 0; arena->stats_allocated_blocks = 0; arena->stats_reused_blocks = 0; @@ -3356,40 +3800,40 @@ _reset_stats(PyObject* self, PyObject* args) return Py_None; } -static PyObject* -_get_alignment(PyObject* self, PyObject* args) -{ - if (!PyArg_ParseTuple(args, ":get_alignment")) +static PyObject * +_get_alignment(PyObject *self, PyObject *args) { + if (!PyArg_ParseTuple(args, ":get_alignment")) { return NULL; - - return PyInt_FromLong(ImagingDefaultArena.alignment); + } + + return PyLong_FromLong(ImagingDefaultArena.alignment); } -static PyObject* -_get_block_size(PyObject* self, PyObject* args) -{ - if (!PyArg_ParseTuple(args, ":get_block_size")) +static PyObject * +_get_block_size(PyObject *self, PyObject *args) { + if (!PyArg_ParseTuple(args, ":get_block_size")) { return NULL; - - return PyInt_FromLong(ImagingDefaultArena.block_size); + } + + return PyLong_FromLong(ImagingDefaultArena.block_size); } -static PyObject* -_get_blocks_max(PyObject* self, PyObject* args) -{ - if (!PyArg_ParseTuple(args, ":get_blocks_max")) +static PyObject * +_get_blocks_max(PyObject *self, PyObject *args) { + if (!PyArg_ParseTuple(args, ":get_blocks_max")) { return NULL; - - return PyInt_FromLong(ImagingDefaultArena.blocks_max); + } + + return PyLong_FromLong(ImagingDefaultArena.blocks_max); } -static PyObject* -_set_alignment(PyObject* self, PyObject* args) -{ +static PyObject * +_set_alignment(PyObject *self, PyObject *args) { int alignment; - if (!PyArg_ParseTuple(args, "i:set_alignment", &alignment)) + if (!PyArg_ParseTuple(args, "i:set_alignment", &alignment)) { return NULL; - + } + if (alignment < 1 || alignment > 128) { PyErr_SetString(PyExc_ValueError, "alignment should be from 1 to 128"); return NULL; @@ -3406,22 +3850,20 @@ _set_alignment(PyObject* self, PyObject* args) return Py_None; } -static PyObject* -_set_block_size(PyObject* self, PyObject* args) -{ +static PyObject * +_set_block_size(PyObject *self, PyObject *args) { int block_size; - if (!PyArg_ParseTuple(args, "i:set_block_size", &block_size)) + if (!PyArg_ParseTuple(args, "i:set_block_size", &block_size)) { return NULL; - + } + if (block_size <= 0) { - PyErr_SetString(PyExc_ValueError, - "block_size should be greater than 0"); + PyErr_SetString(PyExc_ValueError, "block_size should be greater than 0"); return NULL; } if (block_size & 0xfff) { - PyErr_SetString(PyExc_ValueError, - "block_size should be multiple of 4096"); + PyErr_SetString(PyExc_ValueError, "block_size should be multiple of 4096"); return NULL; } @@ -3431,36 +3873,39 @@ _set_block_size(PyObject* self, PyObject* args) return Py_None; } -static PyObject* -_set_blocks_max(PyObject* self, PyObject* args) -{ +static PyObject * +_set_blocks_max(PyObject *self, PyObject *args) { int blocks_max; - if (!PyArg_ParseTuple(args, "i:set_blocks_max", &blocks_max)) + if (!PyArg_ParseTuple(args, "i:set_blocks_max", &blocks_max)) { return NULL; - + } + if (blocks_max < 0) { - PyErr_SetString(PyExc_ValueError, - "blocks_max should be greater than 0"); + PyErr_SetString(PyExc_ValueError, "blocks_max should be greater than 0"); + return NULL; + } else if ( + (unsigned long)blocks_max > + SIZE_MAX / sizeof(ImagingDefaultArena.blocks_pool[0])) { + PyErr_SetString(PyExc_ValueError, "blocks_max is too large"); return NULL; } - if ( ! ImagingMemorySetBlocksMax(&ImagingDefaultArena, blocks_max)) { - ImagingError_MemoryError(); - return NULL; + if (!ImagingMemorySetBlocksMax(&ImagingDefaultArena, blocks_max)) { + return ImagingError_MemoryError(); } Py_INCREF(Py_None); return Py_None; } -static PyObject* -_clear_cache(PyObject* self, PyObject* args) -{ +static PyObject * +_clear_cache(PyObject *self, PyObject *args) { int i = 0; - if (!PyArg_ParseTuple(args, "|i:clear_cache", &i)) + if (!PyArg_ParseTuple(args, "|i:clear_cache", &i)) { return NULL; - + } + ImagingMemoryClearCache(&ImagingDefaultArena, i); Py_INCREF(Py_None); @@ -3473,247 +3918,343 @@ _clear_cache(PyObject* self, PyObject* args) pluggable codecs, but not before PIL 1.2 */ /* Decoders (in decode.c) */ -extern PyObject* PyImaging_BcnDecoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_BitDecoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_FliDecoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_GifDecoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_HexDecoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_JpegDecoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_Jpeg2KDecoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_LibTiffDecoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_PackbitsDecoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_PcdDecoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_PcxDecoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_RawDecoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_SgiRleDecoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_SunRleDecoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_TgaRleDecoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_XbmDecoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_ZipDecoderNew(PyObject* self, PyObject* args); +extern PyObject * +PyImaging_BcnDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_BitDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_FliDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_GifDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_HexDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_JpegDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_Jpeg2KDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_LibTiffDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_PackbitsDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_PcdDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_PcxDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_RawDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_SgiRleDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_SunRleDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_TgaRleDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_XbmDecoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_ZipDecoderNew(PyObject *self, PyObject *args); /* Encoders (in encode.c) */ -extern PyObject* PyImaging_EpsEncoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_GifEncoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_JpegEncoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_Jpeg2KEncoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_PcxEncoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_RawEncoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_XbmEncoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_ZipEncoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args); +extern PyObject * +PyImaging_EpsEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_GifEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_JpegEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_PcxEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_RawEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_TgaRleEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_XbmEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_ZipEncoderNew(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args); /* Display support etc (in display.c) */ #ifdef _WIN32 -extern PyObject* PyImaging_CreateWindowWin32(PyObject* self, PyObject* args); -extern PyObject* PyImaging_DisplayWin32(PyObject* self, PyObject* args); -extern PyObject* PyImaging_DisplayModeWin32(PyObject* self, PyObject* args); -extern PyObject* PyImaging_GrabScreenWin32(PyObject* self, PyObject* args); -extern PyObject* PyImaging_GrabClipboardWin32(PyObject* self, PyObject* args); -extern PyObject* PyImaging_ListWindowsWin32(PyObject* self, PyObject* args); -extern PyObject* PyImaging_EventLoopWin32(PyObject* self, PyObject* args); -extern PyObject* PyImaging_DrawWmf(PyObject* self, PyObject* args); +extern PyObject * +PyImaging_CreateWindowWin32(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_DisplayWin32(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_DisplayModeWin32(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_GrabScreenWin32(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_ListWindowsWin32(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_EventLoopWin32(PyObject *self, PyObject *args); +extern PyObject * +PyImaging_DrawWmf(PyObject *self, PyObject *args); +#endif +#ifdef HAVE_XCB +extern PyObject * +PyImaging_GrabScreenX11(PyObject *self, PyObject *args); #endif /* Experimental path stuff (in path.c) */ -extern PyObject* PyPath_Create(ImagingObject* self, PyObject* args); +extern PyObject * +PyPath_Create(ImagingObject *self, PyObject *args); /* Experimental outline stuff (in outline.c) */ -extern PyObject* PyOutline_Create(ImagingObject* self, PyObject* args); +extern PyObject * +PyOutline_Create(ImagingObject *self, PyObject *args); -extern PyObject* PyImaging_Mapper(PyObject* self, PyObject* args); -extern PyObject* PyImaging_MapBuffer(PyObject* self, PyObject* args); +extern PyObject * +PyImaging_MapBuffer(PyObject *self, PyObject *args); static PyMethodDef functions[] = { /* Object factories */ - {"alpha_composite", (PyCFunction)_alpha_composite, 1}, - {"blend", (PyCFunction)_blend, 1}, - {"fill", (PyCFunction)_fill, 1}, - {"new", (PyCFunction)_new, 1}, - {"merge", (PyCFunction)_merge, 1}, + {"alpha_composite", (PyCFunction)_alpha_composite, METH_VARARGS}, + {"blend", (PyCFunction)_blend, METH_VARARGS}, + {"fill", (PyCFunction)_fill, METH_VARARGS}, + {"new", (PyCFunction)_new, METH_VARARGS}, + {"merge", (PyCFunction)_merge, METH_VARARGS}, /* Functions */ - {"convert", (PyCFunction)_convert2, 1}, + {"convert", (PyCFunction)_convert2, METH_VARARGS}, /* Codecs */ - {"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, 1}, - {"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, 1}, - {"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, 1}, - {"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, 1}, - {"gif_decoder", (PyCFunction)PyImaging_GifDecoderNew, 1}, - {"gif_encoder", (PyCFunction)PyImaging_GifEncoderNew, 1}, - {"hex_decoder", (PyCFunction)PyImaging_HexDecoderNew, 1}, - {"hex_encoder", (PyCFunction)PyImaging_EpsEncoderNew, 1}, /* EPS=HEX! */ + {"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, METH_VARARGS}, + {"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, METH_VARARGS}, + {"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS}, + {"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, METH_VARARGS}, + {"gif_decoder", (PyCFunction)PyImaging_GifDecoderNew, METH_VARARGS}, + {"gif_encoder", (PyCFunction)PyImaging_GifEncoderNew, METH_VARARGS}, + {"hex_decoder", (PyCFunction)PyImaging_HexDecoderNew, METH_VARARGS}, + {"hex_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS}, /* EPS=HEX! */ #ifdef HAVE_LIBJPEG - {"jpeg_decoder", (PyCFunction)PyImaging_JpegDecoderNew, 1}, - {"jpeg_encoder", (PyCFunction)PyImaging_JpegEncoderNew, 1}, + {"jpeg_decoder", (PyCFunction)PyImaging_JpegDecoderNew, METH_VARARGS}, + {"jpeg_encoder", (PyCFunction)PyImaging_JpegEncoderNew, METH_VARARGS}, #endif #ifdef HAVE_OPENJPEG - {"jpeg2k_decoder", (PyCFunction)PyImaging_Jpeg2KDecoderNew, 1}, - {"jpeg2k_encoder", (PyCFunction)PyImaging_Jpeg2KEncoderNew, 1}, + {"jpeg2k_decoder", (PyCFunction)PyImaging_Jpeg2KDecoderNew, METH_VARARGS}, + {"jpeg2k_encoder", (PyCFunction)PyImaging_Jpeg2KEncoderNew, METH_VARARGS}, #endif #ifdef HAVE_LIBTIFF - {"libtiff_decoder", (PyCFunction)PyImaging_LibTiffDecoderNew, 1}, - {"libtiff_encoder", (PyCFunction)PyImaging_LibTiffEncoderNew, 1}, + {"libtiff_decoder", (PyCFunction)PyImaging_LibTiffDecoderNew, METH_VARARGS}, + {"libtiff_encoder", (PyCFunction)PyImaging_LibTiffEncoderNew, METH_VARARGS}, #endif - {"packbits_decoder", (PyCFunction)PyImaging_PackbitsDecoderNew, 1}, - {"pcd_decoder", (PyCFunction)PyImaging_PcdDecoderNew, 1}, - {"pcx_decoder", (PyCFunction)PyImaging_PcxDecoderNew, 1}, - {"pcx_encoder", (PyCFunction)PyImaging_PcxEncoderNew, 1}, - {"raw_decoder", (PyCFunction)PyImaging_RawDecoderNew, 1}, - {"raw_encoder", (PyCFunction)PyImaging_RawEncoderNew, 1}, - {"sgi_rle_decoder", (PyCFunction)PyImaging_SgiRleDecoderNew, 1}, - {"sun_rle_decoder", (PyCFunction)PyImaging_SunRleDecoderNew, 1}, - {"tga_rle_decoder", (PyCFunction)PyImaging_TgaRleDecoderNew, 1}, - {"xbm_decoder", (PyCFunction)PyImaging_XbmDecoderNew, 1}, - {"xbm_encoder", (PyCFunction)PyImaging_XbmEncoderNew, 1}, + {"packbits_decoder", (PyCFunction)PyImaging_PackbitsDecoderNew, METH_VARARGS}, + {"pcd_decoder", (PyCFunction)PyImaging_PcdDecoderNew, METH_VARARGS}, + {"pcx_decoder", (PyCFunction)PyImaging_PcxDecoderNew, METH_VARARGS}, + {"pcx_encoder", (PyCFunction)PyImaging_PcxEncoderNew, METH_VARARGS}, + {"raw_decoder", (PyCFunction)PyImaging_RawDecoderNew, METH_VARARGS}, + {"raw_encoder", (PyCFunction)PyImaging_RawEncoderNew, METH_VARARGS}, + {"sgi_rle_decoder", (PyCFunction)PyImaging_SgiRleDecoderNew, METH_VARARGS}, + {"sun_rle_decoder", (PyCFunction)PyImaging_SunRleDecoderNew, METH_VARARGS}, + {"tga_rle_decoder", (PyCFunction)PyImaging_TgaRleDecoderNew, METH_VARARGS}, + {"tga_rle_encoder", (PyCFunction)PyImaging_TgaRleEncoderNew, METH_VARARGS}, + {"xbm_decoder", (PyCFunction)PyImaging_XbmDecoderNew, METH_VARARGS}, + {"xbm_encoder", (PyCFunction)PyImaging_XbmEncoderNew, METH_VARARGS}, #ifdef HAVE_LIBZ - {"zip_decoder", (PyCFunction)PyImaging_ZipDecoderNew, 1}, - {"zip_encoder", (PyCFunction)PyImaging_ZipEncoderNew, 1}, + {"zip_decoder", (PyCFunction)PyImaging_ZipDecoderNew, METH_VARARGS}, + {"zip_encoder", (PyCFunction)PyImaging_ZipEncoderNew, METH_VARARGS}, #endif - /* Memory mapping */ +/* Memory mapping */ #ifdef WITH_MAPPING -#ifdef _WIN32 - {"map", (PyCFunction)PyImaging_Mapper, 1}, -#endif - {"map_buffer", (PyCFunction)PyImaging_MapBuffer, 1}, + {"map_buffer", (PyCFunction)PyImaging_MapBuffer, METH_VARARGS}, #endif - /* Display support */ +/* Display support */ #ifdef _WIN32 - {"display", (PyCFunction)PyImaging_DisplayWin32, 1}, - {"display_mode", (PyCFunction)PyImaging_DisplayModeWin32, 1}, - {"grabscreen", (PyCFunction)PyImaging_GrabScreenWin32, 1}, - {"grabclipboard", (PyCFunction)PyImaging_GrabClipboardWin32, 1}, - {"createwindow", (PyCFunction)PyImaging_CreateWindowWin32, 1}, - {"eventloop", (PyCFunction)PyImaging_EventLoopWin32, 1}, - {"listwindows", (PyCFunction)PyImaging_ListWindowsWin32, 1}, - {"drawwmf", (PyCFunction)PyImaging_DrawWmf, 1}, + {"display", (PyCFunction)PyImaging_DisplayWin32, METH_VARARGS}, + {"display_mode", (PyCFunction)PyImaging_DisplayModeWin32, METH_VARARGS}, + {"grabscreen_win32", (PyCFunction)PyImaging_GrabScreenWin32, METH_VARARGS}, + {"grabclipboard_win32", (PyCFunction)PyImaging_GrabClipboardWin32, METH_VARARGS}, + {"createwindow", (PyCFunction)PyImaging_CreateWindowWin32, METH_VARARGS}, + {"eventloop", (PyCFunction)PyImaging_EventLoopWin32, METH_VARARGS}, + {"listwindows", (PyCFunction)PyImaging_ListWindowsWin32, METH_VARARGS}, + {"drawwmf", (PyCFunction)PyImaging_DrawWmf, METH_VARARGS}, +#endif +#ifdef HAVE_XCB + {"grabscreen_x11", (PyCFunction)PyImaging_GrabScreenX11, METH_VARARGS}, #endif /* Utilities */ - {"crc32", (PyCFunction)_crc32, 1}, - {"getcodecstatus", (PyCFunction)_getcodecstatus, 1}, + {"getcodecstatus", (PyCFunction)_getcodecstatus, METH_VARARGS}, - /* Special effects (experimental) */ +/* Special effects (experimental) */ #ifdef WITH_EFFECTS - {"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, 1}, - {"effect_noise", (PyCFunction)_effect_noise, 1}, - {"linear_gradient", (PyCFunction)_linear_gradient, 1}, - {"radial_gradient", (PyCFunction)_radial_gradient, 1}, - {"wedge", (PyCFunction)_linear_gradient, 1}, /* Compatibility */ + {"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, METH_VARARGS}, + {"effect_noise", (PyCFunction)_effect_noise, METH_VARARGS}, + {"linear_gradient", (PyCFunction)_linear_gradient, METH_VARARGS}, + {"radial_gradient", (PyCFunction)_radial_gradient, METH_VARARGS}, + {"wedge", (PyCFunction)_linear_gradient, METH_VARARGS}, /* Compatibility */ #endif - /* Drawing support stuff */ +/* Drawing support stuff */ #ifdef WITH_IMAGEDRAW - {"font", (PyCFunction)_font_new, 1}, - {"draw", (PyCFunction)_draw_new, 1}, + {"font", (PyCFunction)_font_new, METH_VARARGS}, + {"draw", (PyCFunction)_draw_new, METH_VARARGS}, #endif - /* Experimental path stuff */ +/* Experimental path stuff */ #ifdef WITH_IMAGEPATH - {"path", (PyCFunction)PyPath_Create, 1}, + {"path", (PyCFunction)PyPath_Create, METH_VARARGS}, #endif - /* Experimental arrow graphics stuff */ +/* Experimental arrow graphics stuff */ #ifdef WITH_ARROW - {"outline", (PyCFunction)PyOutline_Create, 1}, + {"outline", (PyCFunction)PyOutline_Create, METH_VARARGS}, #endif /* Resource management */ - {"get_stats", (PyCFunction)_get_stats, 1}, - {"reset_stats", (PyCFunction)_reset_stats, 1}, - {"get_alignment", (PyCFunction)_get_alignment, 1}, - {"get_block_size", (PyCFunction)_get_block_size, 1}, - {"get_blocks_max", (PyCFunction)_get_blocks_max, 1}, - {"set_alignment", (PyCFunction)_set_alignment, 1}, - {"set_block_size", (PyCFunction)_set_block_size, 1}, - {"set_blocks_max", (PyCFunction)_set_blocks_max, 1}, - {"clear_cache", (PyCFunction)_clear_cache, 1}, + {"get_stats", (PyCFunction)_get_stats, METH_VARARGS}, + {"reset_stats", (PyCFunction)_reset_stats, METH_VARARGS}, + {"get_alignment", (PyCFunction)_get_alignment, METH_VARARGS}, + {"get_block_size", (PyCFunction)_get_block_size, METH_VARARGS}, + {"get_blocks_max", (PyCFunction)_get_blocks_max, METH_VARARGS}, + {"set_alignment", (PyCFunction)_set_alignment, METH_VARARGS}, + {"set_block_size", (PyCFunction)_set_block_size, METH_VARARGS}, + {"set_blocks_max", (PyCFunction)_set_blocks_max, METH_VARARGS}, + {"clear_cache", (PyCFunction)_clear_cache, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; static int -setup_module(PyObject* m) { - PyObject* d = PyModule_GetDict(m); - const char* version = (char*)PILLOW_VERSION; +setup_module(PyObject *m) { + PyObject *d = PyModule_GetDict(m); + const char *version = (char *)PILLOW_VERSION; /* Ready object types */ - if (PyType_Ready(&Imaging_Type) < 0) + if (PyType_Ready(&Imaging_Type) < 0) { return -1; + } #ifdef WITH_IMAGEDRAW - if (PyType_Ready(&ImagingFont_Type) < 0) + if (PyType_Ready(&ImagingFont_Type) < 0) { return -1; + } - if (PyType_Ready(&ImagingDraw_Type) < 0) + if (PyType_Ready(&ImagingDraw_Type) < 0) { return -1; + } #endif - if (PyType_Ready(&PixelAccess_Type) < 0) + if (PyType_Ready(&PixelAccess_Type) < 0) { return -1; + } ImagingAccessInit(); #ifdef HAVE_LIBJPEG - { - extern const char* ImagingJpegVersion(void); - PyDict_SetItemString(d, "jpeglib_version", PyUnicode_FromString(ImagingJpegVersion())); - } + { + extern const char *ImagingJpegVersion(void); + PyDict_SetItemString( + d, "jpeglib_version", PyUnicode_FromString(ImagingJpegVersion())); + } #endif #ifdef HAVE_OPENJPEG - { - extern const char *ImagingJpeg2KVersion(void); - PyDict_SetItemString(d, "jp2klib_version", PyUnicode_FromString(ImagingJpeg2KVersion())); - } + { + extern const char *ImagingJpeg2KVersion(void); + PyDict_SetItemString( + d, "jp2klib_version", PyUnicode_FromString(ImagingJpeg2KVersion())); + } +#endif + + PyObject *have_libjpegturbo; +#ifdef LIBJPEG_TURBO_VERSION + have_libjpegturbo = Py_True; +#define tostr1(a) #a +#define tostr(a) tostr1(a) + PyDict_SetItemString( + d, "libjpeg_turbo_version", PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION))); +#undef tostr +#undef tostr1 +#else + have_libjpegturbo = Py_False; +#endif + Py_INCREF(have_libjpegturbo); + PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo); + + PyObject *have_libimagequant; +#ifdef HAVE_LIBIMAGEQUANT + have_libimagequant = Py_True; + { + extern const char *ImagingImageQuantVersion(void); + PyDict_SetItemString( + d, "imagequant_version", PyUnicode_FromString(ImagingImageQuantVersion())); + } +#else + have_libimagequant = Py_False; #endif + Py_INCREF(have_libimagequant); + PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", have_libimagequant); #ifdef HAVE_LIBZ - /* zip encoding strategies */ - PyModule_AddIntConstant(m, "DEFAULT_STRATEGY", Z_DEFAULT_STRATEGY); - PyModule_AddIntConstant(m, "FILTERED", Z_FILTERED); - PyModule_AddIntConstant(m, "HUFFMAN_ONLY", Z_HUFFMAN_ONLY); - PyModule_AddIntConstant(m, "RLE", Z_RLE); - PyModule_AddIntConstant(m, "FIXED", Z_FIXED); - { - extern const char* ImagingZipVersion(void); - PyDict_SetItemString(d, "zlib_version", PyUnicode_FromString(ImagingZipVersion())); - } + /* zip encoding strategies */ + PyModule_AddIntConstant(m, "DEFAULT_STRATEGY", Z_DEFAULT_STRATEGY); + PyModule_AddIntConstant(m, "FILTERED", Z_FILTERED); + PyModule_AddIntConstant(m, "HUFFMAN_ONLY", Z_HUFFMAN_ONLY); + PyModule_AddIntConstant(m, "RLE", Z_RLE); + PyModule_AddIntConstant(m, "FIXED", Z_FIXED); + { + extern const char *ImagingZipVersion(void); + PyDict_SetItemString( + d, "zlib_version", PyUnicode_FromString(ImagingZipVersion())); + } +#endif + +#ifdef HAVE_LIBTIFF + { + extern const char *ImagingTiffVersion(void); + PyDict_SetItemString( + d, "libtiff_version", PyUnicode_FromString(ImagingTiffVersion())); + + // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 + PyObject *support_custom_tags; +#if TIFFLIB_VERSION >= 20111221 && TIFFLIB_VERSION != 20120218 && \ + TIFFLIB_VERSION != 20120922 + support_custom_tags = Py_True; +#else + support_custom_tags = Py_False; +#endif + PyDict_SetItemString(d, "libtiff_support_custom_tags", support_custom_tags); + } +#endif + + PyObject *have_xcb; +#ifdef HAVE_XCB + have_xcb = Py_True; +#else + have_xcb = Py_False; #endif + Py_INCREF(have_xcb); + PyModule_AddObject(m, "HAVE_XCB", have_xcb); PyDict_SetItemString(d, "PILLOW_VERSION", PyUnicode_FromString(version)); return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imaging(void) { - PyObject* m; + PyObject *m; static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imaging", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - functions, /* m_methods */ + "_imaging", /* m_name */ + NULL, /* m_doc */ + -1, /* m_size */ + functions, /* m_methods */ }; m = PyModule_Create(&module_def); - if (setup_module(m) < 0) + if (setup_module(m) < 0) { return NULL; + } return m; } -#else -PyMODINIT_FUNC -init_imaging(void) -{ - PyObject* m = Py_InitModule("_imaging", functions); - setup_module(m); -} -#endif - diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 5e4196cb71c..9b5a121d7d3 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -3,35 +3,36 @@ * a Python / PIL interface to the littleCMS ICC Color Management System * Copyright (C) 2002-2003 Kevin Cazabon * kevin@cazabon.com - * http://www.cazabon.com + * https://www.cazabon.com * Adapted/reworked for PIL by Fredrik Lundh * Copyright (c) 2009 Fredrik Lundh * Updated to LCMS2 * Copyright (c) 2013 Eric Soroos * - * pyCMS home page: http://www.cazabon.com/pyCMS - * littleCMS home page: http://www.littlecms.com + * pyCMS home page: https://www.cazabon.com/pyCMS + * littleCMS home page: https://www.littlecms.com * (littleCMS is Copyright (C) 1998-2001 Marti Maria) * * Originally released under LGPL. Graciously donated to PIL in * March 2009, for distribution under the standard PIL license */ -#define COPYRIGHTINFO "\ +#define COPYRIGHTINFO \ + "\ pyCMS\n\ a Python / PIL interface to the littleCMS ICC Color Management System\n\ Copyright (C) 2002-2003 Kevin Cazabon\n\ kevin@cazabon.com\n\ -http://www.cazabon.com\n\ +https://www.cazabon.com\n\ " -#include "Python.h" // Include before wchar.h so _GNU_SOURCE is set +#define PY_SSIZE_T_CLEAN +#include "Python.h" // Include before wchar.h so _GNU_SOURCE is set #include "wchar.h" #include "datetime.h" #include "lcms2.h" -#include "Imaging.h" -#include "py3.h" +#include "libImaging/Imaging.h" #define PYCMSVERSION "1.0.0 pil" @@ -41,10 +42,11 @@ kevin@cazabon.com\n\ 1.0.0 pil Integrating littleCMS2 0.1.0 pil integration & refactoring 0.0.2 alpha: Minor updates, added interfaces to littleCMS features, Jan 6, 2003 - - fixed some memory holes in how transforms/profiles were created and passed back to Python - due to improper destructor setup for PyCObjects + - fixed some memory holes in how transforms/profiles were created and passed back to + Python due to improper destructor setup for PyCObjects - added buildProofTransformFromOpenProfiles() function - - eliminated some code redundancy, centralizing several common tasks with internal functions + - eliminated some code redundancy, centralizing several common tasks with internal + functions 0.0.1 alpha: First public release Dec 26, 2002 @@ -74,125 +76,112 @@ kevin@cazabon.com\n\ /* a profile represents the ICC characteristics for a specific device */ typedef struct { - PyObject_HEAD - cmsHPROFILE profile; + PyObject_HEAD cmsHPROFILE profile; } CmsProfileObject; static PyTypeObject CmsProfile_Type; #define CmsProfile_Check(op) (Py_TYPE(op) == &CmsProfile_Type) -static PyObject* -cms_profile_new(cmsHPROFILE profile) -{ - CmsProfileObject* self; +static PyObject * +cms_profile_new(cmsHPROFILE profile) { + CmsProfileObject *self; self = PyObject_New(CmsProfileObject, &CmsProfile_Type); - if (!self) + if (!self) { return NULL; + } self->profile = profile; - return (PyObject*) self; + return (PyObject *)self; } -static PyObject* -cms_profile_open(PyObject* self, PyObject* args) -{ +static PyObject * +cms_profile_open(PyObject *self, PyObject *args) { cmsHPROFILE hProfile; - char* sProfile; - if (!PyArg_ParseTuple(args, "s:profile_open", &sProfile)) + char *sProfile; + if (!PyArg_ParseTuple(args, "s:profile_open", &sProfile)) { return NULL; + } hProfile = cmsOpenProfileFromFile(sProfile, "r"); if (!hProfile) { - PyErr_SetString(PyExc_IOError, "cannot open profile file"); + PyErr_SetString(PyExc_OSError, "cannot open profile file"); return NULL; } return cms_profile_new(hProfile); } -static PyObject* -cms_profile_fromstring(PyObject* self, PyObject* args) -{ +static PyObject * +cms_profile_fromstring(PyObject *self, PyObject *args) { cmsHPROFILE hProfile; - char* pProfile; - int nProfile; -#if PY_VERSION_HEX >= 0x03000000 - if (!PyArg_ParseTuple(args, "y#:profile_frombytes", &pProfile, &nProfile)) - return NULL; -#else - if (!PyArg_ParseTuple(args, "s#:profile_fromstring", &pProfile, &nProfile)) + char *pProfile; + Py_ssize_t nProfile; + if (!PyArg_ParseTuple(args, "y#:profile_frombytes", &pProfile, &nProfile)) { return NULL; -#endif + } hProfile = cmsOpenProfileFromMem(pProfile, nProfile); if (!hProfile) { - PyErr_SetString(PyExc_IOError, "cannot open profile from string"); + PyErr_SetString(PyExc_OSError, "cannot open profile from string"); return NULL; } return cms_profile_new(hProfile); } -static PyObject* -cms_profile_tobytes(PyObject* self, PyObject* args) -{ - char *pProfile =NULL; +static PyObject * +cms_profile_tobytes(PyObject *self, PyObject *args) { + char *pProfile = NULL; cmsUInt32Number nProfile; - PyObject* CmsProfile; + PyObject *CmsProfile; cmsHPROFILE *profile; - PyObject* ret; - if (!PyArg_ParseTuple(args, "O", &CmsProfile)){ + PyObject *ret; + if (!PyArg_ParseTuple(args, "O", &CmsProfile)) { return NULL; } - profile = ((CmsProfileObject*)CmsProfile)->profile; + profile = ((CmsProfileObject *)CmsProfile)->profile; if (!cmsSaveProfileToMem(profile, pProfile, &nProfile)) { - PyErr_SetString(PyExc_IOError, "Could not determine profile size"); + PyErr_SetString(PyExc_OSError, "Could not determine profile size"); return NULL; } - pProfile = (char*)malloc(nProfile); + pProfile = (char *)malloc(nProfile); if (!pProfile) { - PyErr_SetString(PyExc_IOError, "Out of Memory"); + PyErr_SetString(PyExc_OSError, "Out of Memory"); return NULL; } if (!cmsSaveProfileToMem(profile, pProfile, &nProfile)) { - PyErr_SetString(PyExc_IOError, "Could not get profile"); + PyErr_SetString(PyExc_OSError, "Could not get profile"); free(pProfile); return NULL; } -#if PY_VERSION_HEX >= 0x03000000 ret = PyBytes_FromStringAndSize(pProfile, (Py_ssize_t)nProfile); -#else - ret = PyString_FromStringAndSize(pProfile, (Py_ssize_t)nProfile); -#endif free(pProfile); return ret; } static void -cms_profile_dealloc(CmsProfileObject* self) -{ - (void) cmsCloseProfile(self->profile); +cms_profile_dealloc(CmsProfileObject *self) { + (void)cmsCloseProfile(self->profile); PyObject_Del(self); } /* a transform represents the mapping between two profiles */ typedef struct { - PyObject_HEAD - char mode_in[8]; + PyObject_HEAD char mode_in[8]; char mode_out[8]; cmsHTRANSFORM transform; } CmsTransformObject; @@ -201,26 +190,25 @@ static PyTypeObject CmsTransform_Type; #define CmsTransform_Check(op) (Py_TYPE(op) == &CmsTransform_Type) -static PyObject* -cms_transform_new(cmsHTRANSFORM transform, char* mode_in, char* mode_out) -{ - CmsTransformObject* self; +static PyObject * +cms_transform_new(cmsHTRANSFORM transform, char *mode_in, char *mode_out) { + CmsTransformObject *self; self = PyObject_New(CmsTransformObject, &CmsTransform_Type); - if (!self) + if (!self) { return NULL; + } self->transform = transform; strcpy(self->mode_in, mode_in); strcpy(self->mode_out, mode_out); - return (PyObject*) self; + return (PyObject *)self; } static void -cms_transform_dealloc(CmsTransformObject* self) -{ +cms_transform_dealloc(CmsTransformObject *self) { cmsDeleteTransform(self->transform); PyObject_Del(self); } @@ -228,61 +216,31 @@ cms_transform_dealloc(CmsTransformObject* self) /* -------------------------------------------------------------------- */ /* internal functions */ -static const char* -findICmode(cmsColorSpaceSignature cs) -{ - switch (cs) { - case cmsSigXYZData: return "XYZ"; - case cmsSigLabData: return "LAB"; - case cmsSigLuvData: return "LUV"; - case cmsSigYCbCrData: return "YCbCr"; - case cmsSigYxyData: return "YXY"; - case cmsSigRgbData: return "RGB"; - case cmsSigGrayData: return "L"; - case cmsSigHsvData: return "HSV"; - case cmsSigHlsData: return "HLS"; - case cmsSigCmykData: return "CMYK"; - case cmsSigCmyData: return "CMY"; - default: return ""; /* other TBA */ - } -} - static cmsUInt32Number -findLCMStype(char* PILmode) -{ +findLCMStype(char *PILmode) { if (strcmp(PILmode, "RGB") == 0) { return TYPE_RGBA_8; - } - else if (strcmp(PILmode, "RGBA") == 0) { + } else if (strcmp(PILmode, "RGBA") == 0) { return TYPE_RGBA_8; - } - else if (strcmp(PILmode, "RGBX") == 0) { + } else if (strcmp(PILmode, "RGBX") == 0) { return TYPE_RGBA_8; - } - else if (strcmp(PILmode, "RGBA;16B") == 0) { + } else if (strcmp(PILmode, "RGBA;16B") == 0) { return TYPE_RGBA_16; - } - else if (strcmp(PILmode, "CMYK") == 0) { + } else if (strcmp(PILmode, "CMYK") == 0) { return TYPE_CMYK_8; - } - else if (strcmp(PILmode, "L") == 0) { + } else if (strcmp(PILmode, "L") == 0) { return TYPE_GRAY_8; - } - else if (strcmp(PILmode, "L;16") == 0) { + } else if (strcmp(PILmode, "L;16") == 0) { return TYPE_GRAY_16; - } - else if (strcmp(PILmode, "L;16B") == 0) { + } else if (strcmp(PILmode, "L;16B") == 0) { return TYPE_GRAY_16_SE; - } - else if (strcmp(PILmode, "YCCA") == 0) { + } else if (strcmp(PILmode, "YCCA") == 0) { return TYPE_YCbCr_8; - } - else if (strcmp(PILmode, "YCC") == 0) { + } else if (strcmp(PILmode, "YCC") == 0) { return TYPE_YCbCr_8; - } - else if (strcmp(PILmode, "LAB") == 0) { + } else if (strcmp(PILmode, "LAB") == 0) { // LabX equivalent like ALab, but not reversed -- no #define in lcms2 - return (COLORSPACE_SH(PT_LabV2)|CHANNELS_SH(3)|BYTES_SH(1)|EXTRA_SH(1)); + return (COLORSPACE_SH(PT_LabV2) | CHANNELS_SH(3) | BYTES_SH(1) | EXTRA_SH(1)); } else { @@ -294,38 +252,35 @@ findLCMStype(char* PILmode) #define Cms_Min(a, b) ((a) < (b) ? (a) : (b)) static int -pyCMSgetAuxChannelChannel (cmsUInt32Number format, int auxChannelNdx) -{ +pyCMSgetAuxChannelChannel(cmsUInt32Number format, int auxChannelNdx) { int numColors = T_CHANNELS(format); int numExtras = T_EXTRA(format); if (T_SWAPFIRST(format) && T_DOSWAP(format)) { // reverse order, before anything but last extra is shifted last - if (auxChannelNdx == numExtras - 1) + if (auxChannelNdx == numExtras - 1) { return numColors + numExtras - 1; - else + } else { return numExtras - 2 - auxChannelNdx; - } - else if (T_SWAPFIRST(format)) { + } + } else if (T_SWAPFIRST(format)) { // in order, after color channels, but last extra is shifted to first - if (auxChannelNdx == numExtras - 1) + if (auxChannelNdx == numExtras - 1) { return 0; - else + } else { return numColors + 1 + auxChannelNdx; - } - else if (T_DOSWAP(format)) { + } + } else if (T_DOSWAP(format)) { // reverse order, before anything return numExtras - 1 - auxChannelNdx; - } - else { + } else { // in order, after color channels return numColors + auxChannelNdx; } } static void -pyCMScopyAux (cmsHTRANSFORM hTransform, Imaging imDst, const Imaging imSrc) -{ +pyCMScopyAux(cmsHTRANSFORM hTransform, Imaging imDst, const Imaging imSrc) { cmsUInt32Number dstLCMSFormat; cmsUInt32Number srcLCMSFormat; int numSrcExtras; @@ -339,23 +294,26 @@ pyCMScopyAux (cmsHTRANSFORM hTransform, Imaging imDst, const Imaging imSrc) int e; // trivially copied - if (imDst == imSrc) + if (imDst == imSrc) { return; + } dstLCMSFormat = cmsGetTransformOutputFormat(hTransform); srcLCMSFormat = cmsGetTransformInputFormat(hTransform); // currently, all Pillow formats are chunky formats, but check it anyway - if (T_PLANAR(dstLCMSFormat) || T_PLANAR(srcLCMSFormat)) + if (T_PLANAR(dstLCMSFormat) || T_PLANAR(srcLCMSFormat)) { return; + } // copy only if channel format is identical, except OPTIMIZED is ignored as it // does not affect the aux channel - if (T_FLOAT(dstLCMSFormat) != T_FLOAT(srcLCMSFormat) - || T_FLAVOR(dstLCMSFormat) != T_FLAVOR(srcLCMSFormat) - || T_ENDIAN16(dstLCMSFormat) != T_ENDIAN16(srcLCMSFormat) - || T_BYTES(dstLCMSFormat) != T_BYTES(srcLCMSFormat)) - return; + if (T_FLOAT(dstLCMSFormat) != T_FLOAT(srcLCMSFormat) || + T_FLAVOR(dstLCMSFormat) != T_FLAVOR(srcLCMSFormat) || + T_ENDIAN16(dstLCMSFormat) != T_ENDIAN16(srcLCMSFormat) || + T_BYTES(dstLCMSFormat) != T_BYTES(srcLCMSFormat)) { + return; + } numSrcExtras = T_EXTRA(srcLCMSFormat); numDstExtras = T_EXTRA(dstLCMSFormat); @@ -373,28 +331,33 @@ pyCMScopyAux (cmsHTRANSFORM hTransform, Imaging imDst, const Imaging imSrc) for (y = 0; y < ySize; y++) { int x; - char* pDstExtras = imDst->image[y] + dstChannel * channelSize; - const char* pSrcExtras = imSrc->image[y] + srcChannel * channelSize; - - for (x = 0; x < xSize; x++) - memcpy(pDstExtras + x * dstChunkSize, pSrcExtras + x * srcChunkSize, channelSize); + char *pDstExtras = imDst->image[y] + dstChannel * channelSize; + const char *pSrcExtras = imSrc->image[y] + srcChannel * channelSize; + + for (x = 0; x < xSize; x++) { + memcpy( + pDstExtras + x * dstChunkSize, + pSrcExtras + x * srcChunkSize, + channelSize); + } } } } static int -pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) -{ +pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) { int i; - if (im->xsize > imOut->xsize || im->ysize > imOut->ysize) + if (im->xsize > imOut->xsize || im->ysize > imOut->ysize) { return -1; + } Py_BEGIN_ALLOW_THREADS - // transform color channels only - for (i = 0; i < im->ysize; i++) + // transform color channels only + for (i = 0; i < im->ysize; i++) { cmsDoTransform(hTransform, im->image[i], imOut->image[i], im->xsize); + } // lcms by default does nothing to the auxiliary channels leaving those // unchanged. To do "the right thing" here, i.e. maintain identical results @@ -407,52 +370,69 @@ pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) Py_END_ALLOW_THREADS - return 0; + return 0; } static cmsHTRANSFORM -_buildTransform(cmsHPROFILE hInputProfile, cmsHPROFILE hOutputProfile, char *sInMode, char *sOutMode, int iRenderingIntent, cmsUInt32Number cmsFLAGS) -{ +_buildTransform( + cmsHPROFILE hInputProfile, + cmsHPROFILE hOutputProfile, + char *sInMode, + char *sOutMode, + int iRenderingIntent, + cmsUInt32Number cmsFLAGS) { cmsHTRANSFORM hTransform; Py_BEGIN_ALLOW_THREADS - /* create the transform */ - hTransform = cmsCreateTransform(hInputProfile, - findLCMStype(sInMode), - hOutputProfile, - findLCMStype(sOutMode), - iRenderingIntent, cmsFLAGS); + /* create the transform */ + hTransform = cmsCreateTransform( + hInputProfile, + findLCMStype(sInMode), + hOutputProfile, + findLCMStype(sOutMode), + iRenderingIntent, + cmsFLAGS); Py_END_ALLOW_THREADS - if (!hTransform) + if (!hTransform) { PyErr_SetString(PyExc_ValueError, "cannot build transform"); + } return hTransform; /* if NULL, an exception is set */ } static cmsHTRANSFORM -_buildProofTransform(cmsHPROFILE hInputProfile, cmsHPROFILE hOutputProfile, cmsHPROFILE hProofProfile, char *sInMode, char *sOutMode, int iRenderingIntent, int iProofIntent, cmsUInt32Number cmsFLAGS) -{ +_buildProofTransform( + cmsHPROFILE hInputProfile, + cmsHPROFILE hOutputProfile, + cmsHPROFILE hProofProfile, + char *sInMode, + char *sOutMode, + int iRenderingIntent, + int iProofIntent, + cmsUInt32Number cmsFLAGS) { cmsHTRANSFORM hTransform; Py_BEGIN_ALLOW_THREADS - /* create the transform */ - hTransform = cmsCreateProofingTransform(hInputProfile, - findLCMStype(sInMode), - hOutputProfile, - findLCMStype(sOutMode), - hProofProfile, - iRenderingIntent, - iProofIntent, - cmsFLAGS); + /* create the transform */ + hTransform = cmsCreateProofingTransform( + hInputProfile, + findLCMStype(sInMode), + hOutputProfile, + findLCMStype(sOutMode), + hProofProfile, + iRenderingIntent, + iProofIntent, + cmsFLAGS); Py_END_ALLOW_THREADS - if (!hTransform) + if (!hTransform) { PyErr_SetString(PyExc_ValueError, "cannot build proof transform"); + } return hTransform; /* if NULL, an exception is set */ } @@ -471,20 +451,37 @@ buildTransform(PyObject *self, PyObject *args) { cmsHTRANSFORM transform = NULL; - if (!PyArg_ParseTuple(args, "O!O!ss|ii:buildTransform", &CmsProfile_Type, &pInputProfile, &CmsProfile_Type, &pOutputProfile, &sInMode, &sOutMode, &iRenderingIntent, &cmsFLAGS)) + if (!PyArg_ParseTuple( + args, + "O!O!ss|ii:buildTransform", + &CmsProfile_Type, + &pInputProfile, + &CmsProfile_Type, + &pOutputProfile, + &sInMode, + &sOutMode, + &iRenderingIntent, + &cmsFLAGS)) { return NULL; + } - transform = _buildTransform(pInputProfile->profile, pOutputProfile->profile, sInMode, sOutMode, iRenderingIntent, cmsFLAGS); + transform = _buildTransform( + pInputProfile->profile, + pOutputProfile->profile, + sInMode, + sOutMode, + iRenderingIntent, + cmsFLAGS); - if (!transform) + if (!transform) { return NULL; + } return cms_transform_new(transform, sInMode, sOutMode); } static PyObject * -buildProofTransform(PyObject *self, PyObject *args) -{ +buildProofTransform(PyObject *self, PyObject *args) { CmsProfileObject *pInputProfile; CmsProfileObject *pOutputProfile; CmsProfileObject *pProofProfile; @@ -496,21 +493,42 @@ buildProofTransform(PyObject *self, PyObject *args) cmsHTRANSFORM transform = NULL; - if (!PyArg_ParseTuple(args, "O!O!O!ss|iii:buildProofTransform", &CmsProfile_Type, &pInputProfile, &CmsProfile_Type, &pOutputProfile, &CmsProfile_Type, &pProofProfile, &sInMode, &sOutMode, &iRenderingIntent, &iProofIntent, &cmsFLAGS)) + if (!PyArg_ParseTuple( + args, + "O!O!O!ss|iii:buildProofTransform", + &CmsProfile_Type, + &pInputProfile, + &CmsProfile_Type, + &pOutputProfile, + &CmsProfile_Type, + &pProofProfile, + &sInMode, + &sOutMode, + &iRenderingIntent, + &iProofIntent, + &cmsFLAGS)) { return NULL; + } - transform = _buildProofTransform(pInputProfile->profile, pOutputProfile->profile, pProofProfile->profile, sInMode, sOutMode, iRenderingIntent, iProofIntent, cmsFLAGS); + transform = _buildProofTransform( + pInputProfile->profile, + pOutputProfile->profile, + pProofProfile->profile, + sInMode, + sOutMode, + iRenderingIntent, + iProofIntent, + cmsFLAGS); - if (!transform) + if (!transform) { return NULL; + } return cms_transform_new(transform, sInMode, sOutMode); - } static PyObject * -cms_transform_apply(CmsTransformObject *self, PyObject *args) -{ +cms_transform_apply(CmsTransformObject *self, PyObject *args) { Py_ssize_t idIn; Py_ssize_t idOut; Imaging im; @@ -518,11 +536,12 @@ cms_transform_apply(CmsTransformObject *self, PyObject *args) int result; - if (!PyArg_ParseTuple(args, "nn:apply", &idIn, &idOut)) + if (!PyArg_ParseTuple(args, "nn:apply", &idIn, &idOut)) { return NULL; + } - im = (Imaging) idIn; - imOut = (Imaging) idOut; + im = (Imaging)idIn; + imOut = (Imaging)idOut; result = pyCMSdoTransform(im, imOut, self->transform); @@ -533,36 +552,36 @@ cms_transform_apply(CmsTransformObject *self, PyObject *args) /* Python-Callable On-The-Fly profile creation functions */ static PyObject * -createProfile(PyObject *self, PyObject *args) -{ +createProfile(PyObject *self, PyObject *args) { char *sColorSpace; cmsHPROFILE hProfile; cmsFloat64Number dColorTemp = 0.0; cmsCIExyY whitePoint; cmsBool result; - if (!PyArg_ParseTuple(args, "s|d:createProfile", &sColorSpace, &dColorTemp)) + if (!PyArg_ParseTuple(args, "s|d:createProfile", &sColorSpace, &dColorTemp)) { return NULL; + } if (strcmp(sColorSpace, "LAB") == 0) { if (dColorTemp > 0.0) { result = cmsWhitePointFromTemp(&whitePoint, dColorTemp); if (!result) { - PyErr_SetString(PyExc_ValueError, "ERROR: Could not calculate white point from color temperature provided, must be float in degrees Kelvin"); + PyErr_SetString( + PyExc_ValueError, + "ERROR: Could not calculate white point from color temperature " + "provided, must be float in degrees Kelvin"); return NULL; } hProfile = cmsCreateLab2Profile(&whitePoint); } else { hProfile = cmsCreateLab2Profile(NULL); } - } - else if (strcmp(sColorSpace, "XYZ") == 0) { + } else if (strcmp(sColorSpace, "XYZ") == 0) { hProfile = cmsCreateXYZProfile(); - } - else if (strcmp(sColorSpace, "sRGB") == 0) { + } else if (strcmp(sColorSpace, "sRGB") == 0) { hProfile = cmsCreate_sRGBProfile(); - } - else { + } else { hProfile = NULL; } @@ -578,20 +597,21 @@ createProfile(PyObject *self, PyObject *args) /* profile methods */ static PyObject * -cms_profile_is_intent_supported(CmsProfileObject *self, PyObject *args) -{ +cms_profile_is_intent_supported(CmsProfileObject *self, PyObject *args) { cmsBool result; int intent; int direction; - if (!PyArg_ParseTuple(args, "ii:is_intent_supported", &intent, &direction)) + if (!PyArg_ParseTuple(args, "ii:is_intent_supported", &intent, &direction)) { return NULL; + } result = cmsIsIntentSupported(self->profile, intent, direction); - /* printf("cmsIsIntentSupported(%p, %d, %d) => %d\n", self->profile, intent, direction, result); */ + /* printf("cmsIsIntentSupported(%p, %d, %d) => %d\n", self->profile, intent, + * direction, result); */ - return PyInt_FromLong(result != 0); + return PyLong_FromLong(result != 0); } #ifdef _WIN32 @@ -603,29 +623,31 @@ cms_profile_is_intent_supported(CmsProfileObject *self, PyObject *args) #endif static PyObject * -cms_get_display_profile_win32(PyObject* self, PyObject* args) -{ +cms_get_display_profile_win32(PyObject *self, PyObject *args) { char filename[MAX_PATH]; cmsUInt32Number filename_size; BOOL ok; HANDLE handle = 0; int is_dc = 0; - if (!PyArg_ParseTuple(args, "|" F_HANDLE "i:get_display_profile", &handle, &is_dc)) + if (!PyArg_ParseTuple( + args, "|" F_HANDLE "i:get_display_profile", &handle, &is_dc)) { return NULL; + } filename_size = sizeof(filename); if (is_dc) { - ok = GetICMProfile((HDC) handle, &filename_size, filename); + ok = GetICMProfile((HDC)handle, &filename_size, filename); } else { - HDC dc = GetDC((HWND) handle); + HDC dc = GetDC((HWND)handle); ok = GetICMProfile(dc, &filename_size, filename); - ReleaseDC((HWND) handle, dc); + ReleaseDC((HWND)handle, dc); } - if (ok) - return PyUnicode_FromStringAndSize(filename, filename_size-1); + if (ok) { + return PyUnicode_FromStringAndSize(filename, filename_size - 1); + } Py_INCREF(Py_None); return Py_None; @@ -635,9 +657,8 @@ cms_get_display_profile_win32(PyObject* self, PyObject* args) /* -------------------------------------------------------------------- */ /* Helper functions. */ -static PyObject* -_profile_read_mlu(CmsProfileObject* self, cmsTagSignature info) -{ +static PyObject * +_profile_read_mlu(CmsProfileObject *self, cmsTagSignature info) { PyObject *uni; char *lc = "en"; char *cc = cmsNoCountry; @@ -664,7 +685,7 @@ _profile_read_mlu(CmsProfileObject* self, cmsTagSignature info) buf = malloc(len); if (!buf) { - PyErr_SetString(PyExc_IOError, "Out of Memory"); + PyErr_SetString(PyExc_OSError, "Out of Memory"); return NULL; } /* Just in case the next call fails. */ @@ -678,30 +699,22 @@ _profile_read_mlu(CmsProfileObject* self, cmsTagSignature info) return uni; } - -static PyObject* -_profile_read_int_as_string(cmsUInt32Number nr) -{ - PyObject* ret; +static PyObject * +_profile_read_int_as_string(cmsUInt32Number nr) { + PyObject *ret; char buf[5]; - buf[0] = (char) ((nr >> 24) & 0xff); - buf[1] = (char) ((nr >> 16) & 0xff); - buf[2] = (char) ((nr >> 8) & 0xff); - buf[3] = (char) (nr & 0xff); + buf[0] = (char)((nr >> 24) & 0xff); + buf[1] = (char)((nr >> 16) & 0xff); + buf[2] = (char)((nr >> 8) & 0xff); + buf[3] = (char)(nr & 0xff); buf[4] = 0; -#if PY_VERSION_HEX >= 0x03000000 ret = PyUnicode_DecodeASCII(buf, 4, NULL); -#else - ret = PyString_FromStringAndSize(buf, 4); -#endif return ret; } - -static PyObject* -_profile_read_signature(CmsProfileObject* self, cmsTagSignature info) -{ +static PyObject * +_profile_read_signature(CmsProfileObject *self, cmsTagSignature info) { unsigned int *sig; if (!cmsIsTag(self->profile, info)) { @@ -709,7 +722,7 @@ _profile_read_signature(CmsProfileObject* self, cmsTagSignature info) return Py_None; } - sig = (unsigned int *) cmsReadTag(self->profile, info); + sig = (unsigned int *)cmsReadTag(self->profile, info); if (!sig) { Py_INCREF(Py_None); return Py_None; @@ -718,63 +731,74 @@ _profile_read_signature(CmsProfileObject* self, cmsTagSignature info) return _profile_read_int_as_string(*sig); } -static PyObject* -_xyz_py(cmsCIEXYZ* XYZ) -{ +static PyObject * +_xyz_py(cmsCIEXYZ *XYZ) { cmsCIExyY xyY; cmsXYZ2xyY(&xyY, XYZ); - return Py_BuildValue("((d,d,d),(d,d,d))", XYZ->X, XYZ->Y, XYZ->Z, xyY.x, xyY.y, xyY.Y); + return Py_BuildValue( + "((d,d,d),(d,d,d))", XYZ->X, XYZ->Y, XYZ->Z, xyY.x, xyY.y, xyY.Y); } -static PyObject* -_xyz3_py(cmsCIEXYZ* XYZ) -{ +static PyObject * +_xyz3_py(cmsCIEXYZ *XYZ) { cmsCIExyY xyY[3]; cmsXYZ2xyY(&xyY[0], &XYZ[0]); cmsXYZ2xyY(&xyY[1], &XYZ[1]); cmsXYZ2xyY(&xyY[2], &XYZ[2]); - return Py_BuildValue("(((d,d,d),(d,d,d),(d,d,d)),((d,d,d),(d,d,d),(d,d,d)))", - XYZ[0].X, XYZ[0].Y, XYZ[0].Z, - XYZ[1].X, XYZ[1].Y, XYZ[1].Z, - XYZ[2].X, XYZ[2].Y, XYZ[2].Z, - xyY[0].x, xyY[0].y, xyY[0].Y, - xyY[1].x, xyY[1].y, xyY[1].Y, - xyY[2].x, xyY[2].y, xyY[2].Y); + return Py_BuildValue( + "(((d,d,d),(d,d,d),(d,d,d)),((d,d,d),(d,d,d),(d,d,d)))", + XYZ[0].X, + XYZ[0].Y, + XYZ[0].Z, + XYZ[1].X, + XYZ[1].Y, + XYZ[1].Z, + XYZ[2].X, + XYZ[2].Y, + XYZ[2].Z, + xyY[0].x, + xyY[0].y, + xyY[0].Y, + xyY[1].x, + xyY[1].y, + xyY[1].Y, + xyY[2].x, + xyY[2].y, + xyY[2].Y); } -static PyObject* -_profile_read_ciexyz(CmsProfileObject* self, cmsTagSignature info, int multi) -{ - cmsCIEXYZ* XYZ; +static PyObject * +_profile_read_ciexyz(CmsProfileObject *self, cmsTagSignature info, int multi) { + cmsCIEXYZ *XYZ; if (!cmsIsTag(self->profile, info)) { Py_INCREF(Py_None); return Py_None; } - XYZ = (cmsCIEXYZ*) cmsReadTag(self->profile, info); + XYZ = (cmsCIEXYZ *)cmsReadTag(self->profile, info); if (!XYZ) { Py_INCREF(Py_None); return Py_None; } - if (multi) + if (multi) { return _xyz3_py(XYZ); - else + } else { return _xyz_py(XYZ); + } } -static PyObject* -_profile_read_ciexyy_triple(CmsProfileObject* self, cmsTagSignature info) -{ - cmsCIExyYTRIPLE* triple; +static PyObject * +_profile_read_ciexyy_triple(CmsProfileObject *self, cmsTagSignature info) { + cmsCIExyYTRIPLE *triple; if (!cmsIsTag(self->profile, info)) { Py_INCREF(Py_None); return Py_None; } - triple = (cmsCIExyYTRIPLE*) cmsReadTag(self->profile, info); + triple = (cmsCIExyYTRIPLE *)cmsReadTag(self->profile, info); if (!triple) { Py_INCREF(Py_None); return Py_None; @@ -782,26 +806,32 @@ _profile_read_ciexyy_triple(CmsProfileObject* self, cmsTagSignature info) /* Note: lcms does all the heavy lifting and error checking (nr of channels == 3). */ - return Py_BuildValue("((d,d,d),(d,d,d),(d,d,d)),", - triple->Red.x, triple->Red.y, triple->Red.Y, - triple->Green.x, triple->Green.y, triple->Green.Y, - triple->Blue.x, triple->Blue.y, triple->Blue.Y); + return Py_BuildValue( + "((d,d,d),(d,d,d),(d,d,d)),", + triple->Red.x, + triple->Red.y, + triple->Red.Y, + triple->Green.x, + triple->Green.y, + triple->Green.Y, + triple->Blue.x, + triple->Blue.y, + triple->Blue.Y); } -static PyObject* -_profile_read_named_color_list(CmsProfileObject* self, cmsTagSignature info) -{ - cmsNAMEDCOLORLIST* ncl; +static PyObject * +_profile_read_named_color_list(CmsProfileObject *self, cmsTagSignature info) { + cmsNAMEDCOLORLIST *ncl; int i, n; char name[cmsMAX_PATH]; - PyObject* result; + PyObject *result; if (!cmsIsTag(self->profile, info)) { Py_INCREF(Py_None); return Py_None; } - ncl = (cmsNAMEDCOLORLIST*) cmsReadTag(self->profile, info); + ncl = (cmsNAMEDCOLORLIST *)cmsReadTag(self->profile, info); if (ncl == NULL) { Py_INCREF(Py_None); return Py_None; @@ -815,23 +845,23 @@ _profile_read_named_color_list(CmsProfileObject* self, cmsTagSignature info) } for (i = 0; i < n; i++) { - PyObject* str; + PyObject *str; cmsNamedColorInfo(ncl, i, name, NULL, NULL, NULL, NULL); - str = PyUnicode_FromString(name); - if (str == NULL) { - Py_DECREF(result); - Py_INCREF(Py_None); - return Py_None; - } + str = PyUnicode_FromString(name); + if (str == NULL) { + Py_DECREF(result); + Py_INCREF(Py_None); + return Py_None; + } PyList_SET_ITEM(result, i, str); } return result; } -static cmsBool _calculate_rgb_primaries(CmsProfileObject* self, cmsCIEXYZTRIPLE* result) -{ - double input[3][3] = { { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 } }; +static cmsBool +_calculate_rgb_primaries(CmsProfileObject *self, cmsCIEXYZTRIPLE *result) { + double input[3][3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}; cmsHPROFILE hXYZ; cmsHTRANSFORM hTransform; @@ -839,39 +869,46 @@ static cmsBool _calculate_rgb_primaries(CmsProfileObject* self, cmsCIEXYZTRIPLE* // double array of RGB values with max on each identity hXYZ = cmsCreateXYZProfile(); - if (hXYZ == NULL) + if (hXYZ == NULL) { return 0; + } // transform from our profile to XYZ using doubles for highest precision - hTransform = cmsCreateTransform(self->profile, TYPE_RGB_DBL, - hXYZ, TYPE_XYZ_DBL, - INTENT_RELATIVE_COLORIMETRIC, - cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE); + hTransform = cmsCreateTransform( + self->profile, + TYPE_RGB_DBL, + hXYZ, + TYPE_XYZ_DBL, + INTENT_RELATIVE_COLORIMETRIC, + cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE); cmsCloseProfile(hXYZ); - if (hTransform == NULL) + if (hTransform == NULL) { return 0; + } - cmsDoTransform(hTransform, (void*) input, result, 3); + cmsDoTransform(hTransform, (void *)input, result, 3); cmsDeleteTransform(hTransform); return 1; } -static cmsBool _check_intent(int clut, cmsHPROFILE hProfile, cmsUInt32Number Intent, cmsUInt32Number UsedDirection) -{ +static cmsBool +_check_intent( + int clut, + cmsHPROFILE hProfile, + cmsUInt32Number Intent, + cmsUInt32Number UsedDirection) { if (clut) { return cmsIsCLUT(hProfile, Intent, UsedDirection); - } - else { + } else { return cmsIsIntentSupported(hProfile, Intent, UsedDirection); } } #define INTENTS 200 -static PyObject* -_is_intent_supported(CmsProfileObject* self, int clut) -{ - PyObject* result; +static PyObject * +_is_intent_supported(CmsProfileObject *self, int clut) { + PyObject *result; int n; int i; cmsUInt32Number intent_ids[INTENTS]; @@ -883,33 +920,36 @@ _is_intent_supported(CmsProfileObject* self, int clut) return Py_None; } - - n = cmsGetSupportedIntents(INTENTS, - intent_ids, - intent_descs); + n = cmsGetSupportedIntents(INTENTS, intent_ids, intent_descs); for (i = 0; i < n; i++) { - int intent = (int) intent_ids[i]; - PyObject* id; - PyObject* entry; - - /* Only valid for ICC Intents (otherwise we read invalid memory in lcms cmsio1.c). */ - if (!(intent == INTENT_PERCEPTUAL || intent == INTENT_RELATIVE_COLORIMETRIC - || intent == INTENT_SATURATION || intent == INTENT_ABSOLUTE_COLORIMETRIC)) - continue; - - id = PyInt_FromLong((long) intent); - entry = Py_BuildValue("(OOO)", - _check_intent(clut, self->profile, intent, LCMS_USED_AS_INPUT) ? Py_True : Py_False, - _check_intent(clut, self->profile, intent, LCMS_USED_AS_OUTPUT) ? Py_True : Py_False, - _check_intent(clut, self->profile, intent, LCMS_USED_AS_PROOF) ? Py_True : Py_False); - if (id == NULL || entry == NULL) { - Py_XDECREF(id); - Py_XDECREF(entry); - Py_XDECREF(result); - Py_INCREF(Py_None); - return Py_None; - } - PyDict_SetItem(result, id, entry); + int intent = (int)intent_ids[i]; + PyObject *id; + PyObject *entry; + + /* Only valid for ICC Intents (otherwise we read invalid memory in lcms + * cmsio1.c). */ + if (!(intent == INTENT_PERCEPTUAL || intent == INTENT_RELATIVE_COLORIMETRIC || + intent == INTENT_SATURATION || intent == INTENT_ABSOLUTE_COLORIMETRIC)) { + continue; + } + + id = PyLong_FromLong((long)intent); + entry = Py_BuildValue( + "(OOO)", + _check_intent(clut, self->profile, intent, LCMS_USED_AS_INPUT) ? Py_True + : Py_False, + _check_intent(clut, self->profile, intent, LCMS_USED_AS_OUTPUT) ? Py_True + : Py_False, + _check_intent(clut, self->profile, intent, LCMS_USED_AS_PROOF) ? Py_True + : Py_False); + if (id == NULL || entry == NULL) { + Py_XDECREF(id); + Py_XDECREF(entry); + Py_XDECREF(result); + Py_INCREF(Py_None); + return Py_None; + } + PyDict_SetItem(result, id, entry); } return result; } @@ -919,263 +959,171 @@ _is_intent_supported(CmsProfileObject* self, int clut) static PyMethodDef pyCMSdll_methods[] = { - {"profile_open", cms_profile_open, 1}, - {"profile_frombytes", cms_profile_fromstring, 1}, - {"profile_fromstring", cms_profile_fromstring, 1}, - {"profile_tobytes", cms_profile_tobytes, 1}, + {"profile_open", cms_profile_open, METH_VARARGS}, + {"profile_frombytes", cms_profile_fromstring, METH_VARARGS}, + {"profile_fromstring", cms_profile_fromstring, METH_VARARGS}, + {"profile_tobytes", cms_profile_tobytes, METH_VARARGS}, /* profile and transform functions */ - {"buildTransform", buildTransform, 1}, - {"buildProofTransform", buildProofTransform, 1}, - {"createProfile", createProfile, 1}, + {"buildTransform", buildTransform, METH_VARARGS}, + {"buildProofTransform", buildProofTransform, METH_VARARGS}, + {"createProfile", createProfile, METH_VARARGS}, - /* platform specific tools */ +/* platform specific tools */ #ifdef _WIN32 - {"get_display_profile_win32", cms_get_display_profile_win32, 1}, + {"get_display_profile_win32", cms_get_display_profile_win32, METH_VARARGS}, #endif - {NULL, NULL} -}; + {NULL, NULL}}; static struct PyMethodDef cms_profile_methods[] = { - {"is_intent_supported", (PyCFunction) cms_profile_is_intent_supported, 1}, + {"is_intent_supported", (PyCFunction)cms_profile_is_intent_supported, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; -static PyObject* -_profile_getattr(CmsProfileObject* self, cmsInfoType field) -{ - // UNDONE -- check that I'm getting the right fields on these. - // return PyUnicode_DecodeFSDefault(cmsTakeProductName(self->profile)); - //wchar_t buf[256]; -- UNDONE need wchar_t for unicode version. - char buf[256]; - cmsUInt32Number written; - written = cmsGetProfileInfoASCII(self->profile, - field, - "en", - "us", - buf, - 256); - if (written) { - return PyUnicode_FromString(buf); - } - // UNDONE suppressing error here by sending back blank string. - return PyUnicode_FromString(""); -} - -static PyObject* -cms_profile_getattr_product_desc(CmsProfileObject* self, void* closure) -{ - // description was Description != 'Copyright' || or "%s - %s" (manufacturer, model) in 1.x - return _profile_getattr(self, cmsInfoDescription); -} - -/* use these four for the individual fields. - */ -static PyObject* -cms_profile_getattr_product_description(CmsProfileObject* self, void* closure) -{ - return _profile_getattr(self, cmsInfoDescription); -} - -static PyObject* -cms_profile_getattr_product_model(CmsProfileObject* self, void* closure) -{ - return _profile_getattr(self, cmsInfoModel); -} - -static PyObject* -cms_profile_getattr_product_manufacturer(CmsProfileObject* self, void* closure) -{ - return _profile_getattr(self, cmsInfoManufacturer); -} - -static PyObject* -cms_profile_getattr_product_copyright(CmsProfileObject* self, void* closure) -{ - return _profile_getattr(self, cmsInfoCopyright); -} - -static PyObject* -cms_profile_getattr_rendering_intent(CmsProfileObject* self, void* closure) -{ - return PyInt_FromLong(cmsGetHeaderRenderingIntent(self->profile)); -} - -static PyObject* -cms_profile_getattr_pcs(CmsProfileObject* self, void* closure) -{ - return PyUnicode_DecodeFSDefault(findICmode(cmsGetPCS(self->profile))); -} - -static PyObject* -cms_profile_getattr_color_space(CmsProfileObject* self, void* closure) -{ - return PyUnicode_DecodeFSDefault(findICmode(cmsGetColorSpace(self->profile))); +static PyObject * +cms_profile_getattr_rendering_intent(CmsProfileObject *self, void *closure) { + return PyLong_FromLong(cmsGetHeaderRenderingIntent(self->profile)); } /* New-style unicode interfaces. */ -static PyObject* -cms_profile_getattr_copyright(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_copyright(CmsProfileObject *self, void *closure) { return _profile_read_mlu(self, cmsSigCopyrightTag); } -static PyObject* -cms_profile_getattr_target(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_target(CmsProfileObject *self, void *closure) { return _profile_read_mlu(self, cmsSigCharTargetTag); } -static PyObject* -cms_profile_getattr_manufacturer(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_manufacturer(CmsProfileObject *self, void *closure) { return _profile_read_mlu(self, cmsSigDeviceMfgDescTag); } -static PyObject* -cms_profile_getattr_model(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_model(CmsProfileObject *self, void *closure) { return _profile_read_mlu(self, cmsSigDeviceModelDescTag); } -static PyObject* -cms_profile_getattr_profile_description(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_profile_description(CmsProfileObject *self, void *closure) { return _profile_read_mlu(self, cmsSigProfileDescriptionTag); } -static PyObject* -cms_profile_getattr_screening_description(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_screening_description(CmsProfileObject *self, void *closure) { return _profile_read_mlu(self, cmsSigScreeningDescTag); } -static PyObject* -cms_profile_getattr_viewing_condition(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_viewing_condition(CmsProfileObject *self, void *closure) { return _profile_read_mlu(self, cmsSigViewingCondDescTag); } -static PyObject* -cms_profile_getattr_creation_date(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_creation_date(CmsProfileObject *self, void *closure) { cmsBool result; struct tm ct; result = cmsGetHeaderCreationDateTime(self->profile, &ct); - if (! result) { + if (!result) { Py_INCREF(Py_None); return Py_None; } - return PyDateTime_FromDateAndTime(1900 + ct.tm_year, ct.tm_mon, ct.tm_mday, - ct.tm_hour, ct.tm_min, ct.tm_sec, 0); + return PyDateTime_FromDateAndTime( + 1900 + ct.tm_year, ct.tm_mon, ct.tm_mday, ct.tm_hour, ct.tm_min, ct.tm_sec, 0); } -static PyObject* -cms_profile_getattr_version(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_version(CmsProfileObject *self, void *closure) { cmsFloat64Number version = cmsGetProfileVersion(self->profile); return PyFloat_FromDouble(version); } -static PyObject* -cms_profile_getattr_icc_version(CmsProfileObject* self, void* closure) -{ - return PyInt_FromLong((long) cmsGetEncodedICCversion(self->profile)); +static PyObject * +cms_profile_getattr_icc_version(CmsProfileObject *self, void *closure) { + return PyLong_FromLong((long)cmsGetEncodedICCversion(self->profile)); } -static PyObject* -cms_profile_getattr_attributes(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_attributes(CmsProfileObject *self, void *closure) { cmsUInt64Number attr; cmsGetHeaderAttributes(self->profile, &attr); /* This works just as well on Windows (LLP64), 32-bit Linux (ILP32) and 64-bit Linux (LP64) systems. */ - return PyLong_FromUnsignedLongLong((unsigned long long) attr); + return PyLong_FromUnsignedLongLong((unsigned long long)attr); } -static PyObject* -cms_profile_getattr_header_flags(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_header_flags(CmsProfileObject *self, void *closure) { cmsUInt32Number flags = cmsGetHeaderFlags(self->profile); - return PyInt_FromLong(flags); + return PyLong_FromLong(flags); } -static PyObject* -cms_profile_getattr_header_manufacturer(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_header_manufacturer(CmsProfileObject *self, void *closure) { return _profile_read_int_as_string(cmsGetHeaderManufacturer(self->profile)); } -static PyObject* -cms_profile_getattr_header_model(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_header_model(CmsProfileObject *self, void *closure) { return _profile_read_int_as_string(cmsGetHeaderModel(self->profile)); } -static PyObject* -cms_profile_getattr_device_class(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_device_class(CmsProfileObject *self, void *closure) { return _profile_read_int_as_string(cmsGetDeviceClass(self->profile)); } -/* Duplicate of pcs, but uninterpreted. */ -static PyObject* -cms_profile_getattr_connection_space(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_connection_space(CmsProfileObject *self, void *closure) { return _profile_read_int_as_string(cmsGetPCS(self->profile)); } -/* Duplicate of color_space, but uninterpreted. */ -static PyObject* -cms_profile_getattr_xcolor_space(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_xcolor_space(CmsProfileObject *self, void *closure) { return _profile_read_int_as_string(cmsGetColorSpace(self->profile)); } -static PyObject* -cms_profile_getattr_profile_id(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_profile_id(CmsProfileObject *self, void *closure) { cmsUInt8Number id[16]; cmsGetHeaderProfileID(self->profile, id); - return PyBytes_FromStringAndSize((char *) id, 16); + return PyBytes_FromStringAndSize((char *)id, 16); } -static PyObject* -cms_profile_getattr_is_matrix_shaper(CmsProfileObject* self, void* closure) -{ - return PyBool_FromLong((long) cmsIsMatrixShaper(self->profile)); +static PyObject * +cms_profile_getattr_is_matrix_shaper(CmsProfileObject *self, void *closure) { + return PyBool_FromLong((long)cmsIsMatrixShaper(self->profile)); } -static PyObject* -cms_profile_getattr_technology(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_technology(CmsProfileObject *self, void *closure) { return _profile_read_signature(self, cmsSigTechnologyTag); } -static PyObject* -cms_profile_getattr_colorimetric_intent(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_colorimetric_intent(CmsProfileObject *self, void *closure) { return _profile_read_signature(self, cmsSigColorimetricIntentImageStateTag); } -static PyObject* -cms_profile_getattr_perceptual_rendering_intent_gamut(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_perceptual_rendering_intent_gamut( + CmsProfileObject *self, void *closure) { return _profile_read_signature(self, cmsSigPerceptualRenderingIntentGamutTag); } -static PyObject* -cms_profile_getattr_saturation_rendering_intent_gamut(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_saturation_rendering_intent_gamut( + CmsProfileObject *self, void *closure) { return _profile_read_signature(self, cmsSigSaturationRenderingIntentGamutTag); } -static PyObject* -cms_profile_getattr_red_colorant(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_red_colorant(CmsProfileObject *self, void *closure) { if (!cmsIsMatrixShaper(self->profile)) { Py_INCREF(Py_None); return Py_None; @@ -1183,10 +1131,8 @@ cms_profile_getattr_red_colorant(CmsProfileObject* self, void* closure) return _profile_read_ciexyz(self, cmsSigRedColorantTag, 0); } - -static PyObject* -cms_profile_getattr_green_colorant(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_green_colorant(CmsProfileObject *self, void *closure) { if (!cmsIsMatrixShaper(self->profile)) { Py_INCREF(Py_None); return Py_None; @@ -1194,10 +1140,8 @@ cms_profile_getattr_green_colorant(CmsProfileObject* self, void* closure) return _profile_read_ciexyz(self, cmsSigGreenColorantTag, 0); } - -static PyObject* -cms_profile_getattr_blue_colorant(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_blue_colorant(CmsProfileObject *self, void *closure) { if (!cmsIsMatrixShaper(self->profile)) { Py_INCREF(Py_None); return Py_None; @@ -1205,10 +1149,10 @@ cms_profile_getattr_blue_colorant(CmsProfileObject* self, void* closure) return _profile_read_ciexyz(self, cmsSigBlueColorantTag, 0); } -static PyObject* -cms_profile_getattr_media_white_point_temperature(CmsProfileObject *self, void* closure) -{ - cmsCIEXYZ* XYZ; +static PyObject * +cms_profile_getattr_media_white_point_temperature( + CmsProfileObject *self, void *closure) { + cmsCIEXYZ *XYZ; cmsCIExyY xyY; cmsFloat64Number tempK; cmsTagSignature info = cmsSigMediaWhitePointTag; @@ -1219,11 +1163,7 @@ cms_profile_getattr_media_white_point_temperature(CmsProfileObject *self, void* return Py_None; } - XYZ = (cmsCIEXYZ*) cmsReadTag(self->profile, info); - if (!XYZ) { - Py_INCREF(Py_None); - return Py_None; - } + XYZ = (cmsCIEXYZ *)cmsReadTag(self->profile, info); if (XYZ == NULL || XYZ->X == 0) { Py_INCREF(Py_None); return Py_None; @@ -1238,46 +1178,40 @@ cms_profile_getattr_media_white_point_temperature(CmsProfileObject *self, void* return PyFloat_FromDouble(tempK); } -static PyObject* -cms_profile_getattr_media_white_point(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_media_white_point(CmsProfileObject *self, void *closure) { return _profile_read_ciexyz(self, cmsSigMediaWhitePointTag, 0); } - -static PyObject* -cms_profile_getattr_media_black_point(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_media_black_point(CmsProfileObject *self, void *closure) { return _profile_read_ciexyz(self, cmsSigMediaBlackPointTag, 0); } -static PyObject* -cms_profile_getattr_luminance(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_luminance(CmsProfileObject *self, void *closure) { return _profile_read_ciexyz(self, cmsSigLuminanceTag, 0); } -static PyObject* -cms_profile_getattr_chromatic_adaptation(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_chromatic_adaptation(CmsProfileObject *self, void *closure) { return _profile_read_ciexyz(self, cmsSigChromaticAdaptationTag, 1); } -static PyObject* -cms_profile_getattr_chromaticity(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_chromaticity(CmsProfileObject *self, void *closure) { return _profile_read_ciexyy_triple(self, cmsSigChromaticityTag); } -static PyObject* -cms_profile_getattr_red_primary(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_red_primary(CmsProfileObject *self, void *closure) { cmsBool result = 0; cmsCIEXYZTRIPLE primaries; - if (cmsIsMatrixShaper(self->profile)) + if (cmsIsMatrixShaper(self->profile)) { result = _calculate_rgb_primaries(self, &primaries); - if (! result) { + } + if (!result) { Py_INCREF(Py_None); return Py_None; } @@ -1285,93 +1219,87 @@ cms_profile_getattr_red_primary(CmsProfileObject* self, void* closure) return _xyz_py(&primaries.Red); } -static PyObject* -cms_profile_getattr_green_primary(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_green_primary(CmsProfileObject *self, void *closure) { cmsBool result = 0; cmsCIEXYZTRIPLE primaries; - if (cmsIsMatrixShaper(self->profile)) + if (cmsIsMatrixShaper(self->profile)) { result = _calculate_rgb_primaries(self, &primaries); - if (! result) { + } + if (!result) { Py_INCREF(Py_None); - return Py_None; + return Py_None; } return _xyz_py(&primaries.Green); } -static PyObject* -cms_profile_getattr_blue_primary(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_blue_primary(CmsProfileObject *self, void *closure) { cmsBool result = 0; cmsCIEXYZTRIPLE primaries; - if (cmsIsMatrixShaper(self->profile)) + if (cmsIsMatrixShaper(self->profile)) { result = _calculate_rgb_primaries(self, &primaries); - if (! result) { + } + if (!result) { Py_INCREF(Py_None); - return Py_None; + return Py_None; } return _xyz_py(&primaries.Blue); } -static PyObject* -cms_profile_getattr_colorant_table(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_colorant_table(CmsProfileObject *self, void *closure) { return _profile_read_named_color_list(self, cmsSigColorantTableTag); } -static PyObject* -cms_profile_getattr_colorant_table_out(CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_colorant_table_out(CmsProfileObject *self, void *closure) { return _profile_read_named_color_list(self, cmsSigColorantTableOutTag); } -static PyObject* -cms_profile_getattr_is_intent_supported (CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_is_intent_supported(CmsProfileObject *self, void *closure) { return _is_intent_supported(self, 0); } -static PyObject* -cms_profile_getattr_is_clut (CmsProfileObject* self, void* closure) -{ +static PyObject * +cms_profile_getattr_is_clut(CmsProfileObject *self, void *closure) { return _is_intent_supported(self, 1); } -static const char* -_illu_map(int i) -{ - switch(i) { - case 0: - return "unknown"; - case 1: - return "D50"; - case 2: - return "D65"; - case 3: - return "D93"; - case 4: - return "F2"; - case 5: - return "D55"; - case 6: - return "A"; - case 7: - return "E"; - case 8: - return "F8"; - default: - return NULL; +static const char * +_illu_map(int i) { + switch (i) { + case 0: + return "unknown"; + case 1: + return "D50"; + case 2: + return "D65"; + case 3: + return "D93"; + case 4: + return "F2"; + case 5: + return "D55"; + case 6: + return "A"; + case 7: + return "E"; + case 8: + return "F8"; + default: + return NULL; } } -static PyObject* -cms_profile_getattr_icc_measurement_condition (CmsProfileObject* self, void* closure) -{ - cmsICCMeasurementConditions* mc; +static PyObject * +cms_profile_getattr_icc_measurement_condition(CmsProfileObject *self, void *closure) { + cmsICCMeasurementConditions *mc; cmsTagSignature info = cmsSigMeasurementTag; const char *geo; @@ -1380,31 +1308,39 @@ cms_profile_getattr_icc_measurement_condition (CmsProfileObject* self, void* clo return Py_None; } - mc = (cmsICCMeasurementConditions*) cmsReadTag(self->profile, info); + mc = (cmsICCMeasurementConditions *)cmsReadTag(self->profile, info); if (!mc) { Py_INCREF(Py_None); return Py_None; } - if (mc->Geometry == 1) + if (mc->Geometry == 1) { geo = "45/0, 0/45"; - else if (mc->Geometry == 2) + } else if (mc->Geometry == 2) { geo = "0d, d/0"; - else + } else { geo = "unknown"; + } - return Py_BuildValue("{s:i,s:(ddd),s:s,s:d,s:s}", - "observer", mc->Observer, - "backing", mc->Backing.X, mc->Backing.Y, mc->Backing.Z, - "geo", geo, - "flare", mc->Flare, - "illuminant_type", _illu_map(mc->IlluminantType)); + return Py_BuildValue( + "{s:i,s:(ddd),s:s,s:d,s:s}", + "observer", + mc->Observer, + "backing", + mc->Backing.X, + mc->Backing.Y, + mc->Backing.Z, + "geo", + geo, + "flare", + mc->Flare, + "illuminant_type", + _illu_map(mc->IlluminantType)); } -static PyObject* -cms_profile_getattr_icc_viewing_condition (CmsProfileObject* self, void* closure) -{ - cmsICCViewingConditions* vc; +static PyObject * +cms_profile_getattr_icc_viewing_condition(CmsProfileObject *self, void *closure) { + cmsICCViewingConditions *vc; cmsTagSignature info = cmsSigViewingConditionsTag; if (!cmsIsTag(self->profile, info)) { @@ -1412,172 +1348,167 @@ cms_profile_getattr_icc_viewing_condition (CmsProfileObject* self, void* closure return Py_None; } - vc = (cmsICCViewingConditions*) cmsReadTag(self->profile, info); + vc = (cmsICCViewingConditions *)cmsReadTag(self->profile, info); if (!vc) { Py_INCREF(Py_None); return Py_None; } - return Py_BuildValue("{s:(ddd),s:(ddd),s:s}", - "illuminant", vc->IlluminantXYZ.X, vc->IlluminantXYZ.Y, vc->IlluminantXYZ.Z, - "surround", vc->SurroundXYZ.X, vc->SurroundXYZ.Y, vc->SurroundXYZ.Z, - "illuminant_type", _illu_map(vc->IlluminantType)); + return Py_BuildValue( + "{s:(ddd),s:(ddd),s:s}", + "illuminant", + vc->IlluminantXYZ.X, + vc->IlluminantXYZ.Y, + vc->IlluminantXYZ.Z, + "surround", + vc->SurroundXYZ.X, + vc->SurroundXYZ.Y, + vc->SurroundXYZ.Z, + "illuminant_type", + _illu_map(vc->IlluminantType)); } - static struct PyGetSetDef cms_profile_getsetters[] = { - /* Compatibility interfaces. */ - { "product_desc", (getter) cms_profile_getattr_product_desc }, - { "product_description", (getter) cms_profile_getattr_product_description }, - { "product_manufacturer", (getter) cms_profile_getattr_product_manufacturer }, - { "product_model", (getter) cms_profile_getattr_product_model }, - { "product_copyright", (getter) cms_profile_getattr_product_copyright }, - { "pcs", (getter) cms_profile_getattr_pcs }, - { "color_space", (getter) cms_profile_getattr_color_space }, - /* New style interfaces. */ - { "rendering_intent", (getter) cms_profile_getattr_rendering_intent }, - { "creation_date", (getter) cms_profile_getattr_creation_date }, - { "copyright", (getter) cms_profile_getattr_copyright }, - { "target", (getter) cms_profile_getattr_target }, - { "manufacturer", (getter) cms_profile_getattr_manufacturer }, - { "model", (getter) cms_profile_getattr_model }, - { "profile_description", (getter) cms_profile_getattr_profile_description }, - { "screening_description", (getter) cms_profile_getattr_screening_description }, - { "viewing_condition", (getter) cms_profile_getattr_viewing_condition }, - { "version", (getter) cms_profile_getattr_version }, - { "icc_version", (getter) cms_profile_getattr_icc_version }, - { "attributes", (getter) cms_profile_getattr_attributes }, - { "header_flags", (getter) cms_profile_getattr_header_flags }, - { "header_manufacturer", (getter) cms_profile_getattr_header_manufacturer }, - { "header_model", (getter) cms_profile_getattr_header_model }, - { "device_class", (getter) cms_profile_getattr_device_class }, - { "connection_space", (getter) cms_profile_getattr_connection_space }, - /* Similar to color_space, but with full 4-letter signature (including trailing whitespace). */ - { "xcolor_space", (getter) cms_profile_getattr_xcolor_space }, - { "profile_id", (getter) cms_profile_getattr_profile_id }, - { "is_matrix_shaper", (getter) cms_profile_getattr_is_matrix_shaper }, - { "technology", (getter) cms_profile_getattr_technology }, - { "colorimetric_intent", (getter) cms_profile_getattr_colorimetric_intent }, - { "perceptual_rendering_intent_gamut", (getter) cms_profile_getattr_perceptual_rendering_intent_gamut }, - { "saturation_rendering_intent_gamut", (getter) cms_profile_getattr_saturation_rendering_intent_gamut }, - { "red_colorant", (getter) cms_profile_getattr_red_colorant }, - { "green_colorant", (getter) cms_profile_getattr_green_colorant }, - { "blue_colorant", (getter) cms_profile_getattr_blue_colorant }, - { "red_primary", (getter) cms_profile_getattr_red_primary }, - { "green_primary", (getter) cms_profile_getattr_green_primary }, - { "blue_primary", (getter) cms_profile_getattr_blue_primary }, - { "media_white_point_temperature", (getter) cms_profile_getattr_media_white_point_temperature }, - { "media_white_point", (getter) cms_profile_getattr_media_white_point }, - { "media_black_point", (getter) cms_profile_getattr_media_black_point }, - { "luminance", (getter) cms_profile_getattr_luminance }, - { "chromatic_adaptation", (getter) cms_profile_getattr_chromatic_adaptation }, - { "chromaticity", (getter) cms_profile_getattr_chromaticity }, - { "colorant_table", (getter) cms_profile_getattr_colorant_table }, - { "colorant_table_out", (getter) cms_profile_getattr_colorant_table_out }, - { "intent_supported", (getter) cms_profile_getattr_is_intent_supported }, - { "clut", (getter) cms_profile_getattr_is_clut }, - { "icc_measurement_condition", (getter) cms_profile_getattr_icc_measurement_condition }, - { "icc_viewing_condition", (getter) cms_profile_getattr_icc_viewing_condition }, - - { NULL } -}; - + {"rendering_intent", (getter)cms_profile_getattr_rendering_intent}, + {"creation_date", (getter)cms_profile_getattr_creation_date}, + {"copyright", (getter)cms_profile_getattr_copyright}, + {"target", (getter)cms_profile_getattr_target}, + {"manufacturer", (getter)cms_profile_getattr_manufacturer}, + {"model", (getter)cms_profile_getattr_model}, + {"profile_description", (getter)cms_profile_getattr_profile_description}, + {"screening_description", (getter)cms_profile_getattr_screening_description}, + {"viewing_condition", (getter)cms_profile_getattr_viewing_condition}, + {"version", (getter)cms_profile_getattr_version}, + {"icc_version", (getter)cms_profile_getattr_icc_version}, + {"attributes", (getter)cms_profile_getattr_attributes}, + {"header_flags", (getter)cms_profile_getattr_header_flags}, + {"header_manufacturer", (getter)cms_profile_getattr_header_manufacturer}, + {"header_model", (getter)cms_profile_getattr_header_model}, + {"device_class", (getter)cms_profile_getattr_device_class}, + {"connection_space", (getter)cms_profile_getattr_connection_space}, + {"xcolor_space", (getter)cms_profile_getattr_xcolor_space}, + {"profile_id", (getter)cms_profile_getattr_profile_id}, + {"is_matrix_shaper", (getter)cms_profile_getattr_is_matrix_shaper}, + {"technology", (getter)cms_profile_getattr_technology}, + {"colorimetric_intent", (getter)cms_profile_getattr_colorimetric_intent}, + {"perceptual_rendering_intent_gamut", + (getter)cms_profile_getattr_perceptual_rendering_intent_gamut}, + {"saturation_rendering_intent_gamut", + (getter)cms_profile_getattr_saturation_rendering_intent_gamut}, + {"red_colorant", (getter)cms_profile_getattr_red_colorant}, + {"green_colorant", (getter)cms_profile_getattr_green_colorant}, + {"blue_colorant", (getter)cms_profile_getattr_blue_colorant}, + {"red_primary", (getter)cms_profile_getattr_red_primary}, + {"green_primary", (getter)cms_profile_getattr_green_primary}, + {"blue_primary", (getter)cms_profile_getattr_blue_primary}, + {"media_white_point_temperature", + (getter)cms_profile_getattr_media_white_point_temperature}, + {"media_white_point", (getter)cms_profile_getattr_media_white_point}, + {"media_black_point", (getter)cms_profile_getattr_media_black_point}, + {"luminance", (getter)cms_profile_getattr_luminance}, + {"chromatic_adaptation", (getter)cms_profile_getattr_chromatic_adaptation}, + {"chromaticity", (getter)cms_profile_getattr_chromaticity}, + {"colorant_table", (getter)cms_profile_getattr_colorant_table}, + {"colorant_table_out", (getter)cms_profile_getattr_colorant_table_out}, + {"intent_supported", (getter)cms_profile_getattr_is_intent_supported}, + {"clut", (getter)cms_profile_getattr_is_clut}, + {"icc_measurement_condition", + (getter)cms_profile_getattr_icc_measurement_condition}, + {"icc_viewing_condition", (getter)cms_profile_getattr_icc_viewing_condition}, + + {NULL}}; static PyTypeObject CmsProfile_Type = { - PyVarObject_HEAD_INIT(NULL, 0) - "PIL._imagingcms.CmsProfile", /*tp_name */ - sizeof(CmsProfileObject), 0,/*tp_basicsize, tp_itemsize */ + PyVarObject_HEAD_INIT(NULL, 0) "PIL._imagingcms.CmsProfile", /*tp_name */ + sizeof(CmsProfileObject), + 0, /*tp_basicsize, tp_itemsize */ /* methods */ - (destructor) cms_profile_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - cms_profile_methods, /*tp_methods*/ - 0, /*tp_members*/ - cms_profile_getsetters, /*tp_getset*/ + (destructor)cms_profile_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + cms_profile_methods, /*tp_methods*/ + 0, /*tp_members*/ + cms_profile_getsetters, /*tp_getset*/ }; static struct PyMethodDef cms_transform_methods[] = { - {"apply", (PyCFunction) cms_transform_apply, 1}, - {NULL, NULL} /* sentinel */ + {"apply", (PyCFunction)cms_transform_apply, 1}, {NULL, NULL} /* sentinel */ }; -static PyObject* -cms_transform_getattr_inputMode(CmsTransformObject* self, void* closure) -{ +static PyObject * +cms_transform_getattr_inputMode(CmsTransformObject *self, void *closure) { return PyUnicode_FromString(self->mode_in); } -static PyObject* -cms_transform_getattr_outputMode(CmsTransformObject* self, void* closure) -{ +static PyObject * +cms_transform_getattr_outputMode(CmsTransformObject *self, void *closure) { return PyUnicode_FromString(self->mode_out); } static struct PyGetSetDef cms_transform_getsetters[] = { - { "inputMode", (getter) cms_transform_getattr_inputMode }, - { "outputMode", (getter) cms_transform_getattr_outputMode }, - { NULL } -}; + {"inputMode", (getter)cms_transform_getattr_inputMode}, + {"outputMode", (getter)cms_transform_getattr_outputMode}, + {NULL}}; static PyTypeObject CmsTransform_Type = { - PyVarObject_HEAD_INIT(NULL, 0) - "CmsTransform", sizeof(CmsTransformObject), 0, + PyVarObject_HEAD_INIT(NULL, 0) "CmsTransform", + sizeof(CmsTransformObject), + 0, /* methods */ - (destructor) cms_transform_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - cms_transform_methods, /*tp_methods*/ - 0, /*tp_members*/ - cms_transform_getsetters, /*tp_getset*/ + (destructor)cms_transform_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + cms_transform_methods, /*tp_methods*/ + 0, /*tp_members*/ + cms_transform_getsetters, /*tp_getset*/ }; static int -setup_module(PyObject* m) { +setup_module(PyObject *m) { PyObject *d; PyObject *v; - - d = PyModule_GetDict(m); + int vn; CmsProfile_Type.tp_new = PyType_GenericNew; @@ -1590,40 +1521,41 @@ setup_module(PyObject* m) { d = PyModule_GetDict(m); - v = PyUnicode_FromFormat("%d.%d", LCMS_VERSION / 100, LCMS_VERSION % 100); + /* this check is also in PIL.features.pilinfo() */ +#if LCMS_VERSION < 2070 + vn = LCMS_VERSION; +#else + vn = cmsGetEncodedCMMversion(); +#endif + if (vn % 10) { + v = PyUnicode_FromFormat("%d.%d.%d", vn / 1000, (vn / 10) % 100, vn % 10); + } else { + v = PyUnicode_FromFormat("%d.%d", vn / 1000, (vn / 10) % 100); + } PyDict_SetItemString(d, "littlecms_version", v); return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imagingcms(void) { - PyObject* m; + PyObject *m; static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingcms", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - pyCMSdll_methods, /* m_methods */ + "_imagingcms", /* m_name */ + NULL, /* m_doc */ + -1, /* m_size */ + pyCMSdll_methods, /* m_methods */ }; m = PyModule_Create(&module_def); - if (setup_module(m) < 0) + if (setup_module(m) < 0) { return NULL; + } PyDateTime_IMPORT; return m; } -#else -PyMODINIT_FUNC -init_imagingcms(void) -{ - PyObject *m = Py_InitModule("_imagingcms", pyCMSdll_methods); - setup_module(m); - PyDateTime_IMPORT; -} -#endif diff --git a/src/_imagingft.c b/src/_imagingft.c index 92642e7338e..7cd6dfb1da7 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -18,22 +18,25 @@ * Copyright (c) 1998-2007 by Secret Labs AB */ +#define PY_SSIZE_T_CLEAN #include "Python.h" -#include "Imaging.h" +#include "libImaging/Imaging.h" #include #include FT_FREETYPE_H #include FT_GLYPH_H +#include FT_BITMAP_H +#include FT_STROKER_H +#include FT_MULTIPLE_MASTERS_H +#include FT_SFNT_NAMES_H +#ifdef FT_COLOR_H +#include FT_COLOR_H +#endif #define KEEP_PY_UNICODE -#include "py3.h" - -#if !defined(_MSC_VER) -#include -#endif #if !defined(FT_LOAD_TARGET_MONO) -#define FT_LOAD_TARGET_MONO FT_LOAD_MONOCHROME +#define FT_LOAD_TARGET_MONO FT_LOAD_MONOCHROME #endif /* -------------------------------------------------------------------- */ @@ -42,223 +45,118 @@ #undef FTERRORS_H #undef __FTERRORS_H__ -#define FT_ERRORDEF( e, v, s ) { e, s }, -#define FT_ERROR_START_LIST { -#define FT_ERROR_END_LIST { 0, 0 } }; +#define FT_ERRORDEF(e, v, s) {e, s}, +#define FT_ERROR_START_LIST { +#define FT_ERROR_END_LIST \ + { 0, 0 } \ + } \ + ; + +#ifdef HAVE_RAQM +# ifdef HAVE_RAQM_SYSTEM +# include +# else +# include "thirdparty/raqm/raqm.h" +# ifdef HAVE_FRIBIDI_SYSTEM +# include +# else +# include "thirdparty/fribidi-shim/fribidi.h" +# include +# endif +# endif +#endif -#include +static int have_raqm = 0; #define LAYOUT_FALLBACK 0 #define LAYOUT_RAQM 1 -typedef struct -{ - int index, x_offset, x_advance, y_offset; - unsigned int cluster; +typedef struct { + int index, x_offset, x_advance, y_offset, y_advance; + unsigned int cluster; } GlyphInfo; struct { int code; - const char* message; + const char *message; } ft_errors[] = #include FT_ERRORS_H -/* -------------------------------------------------------------------- */ -/* font objects */ + /* -------------------------------------------------------------------- */ + /* font objects */ -static FT_Library library; + static FT_Library library; typedef struct { - PyObject_HEAD - FT_Face face; + PyObject_HEAD FT_Face face; unsigned char *font_bytes; int layout_engine; } FontObject; static PyTypeObject Font_Type; -typedef raqm_t* (*t_raqm_create)(void); -typedef int (*t_raqm_set_text)(raqm_t *rq, - const uint32_t *text, - size_t len); -typedef bool (*t_raqm_set_text_utf8) (raqm_t *rq, - const char *text, - size_t len); -typedef bool (*t_raqm_set_par_direction) (raqm_t *rq, - raqm_direction_t dir); -typedef bool (*t_raqm_add_font_feature) (raqm_t *rq, - const char *feature, - int len); -typedef bool (*t_raqm_set_freetype_face) (raqm_t *rq, - FT_Face face); -typedef bool (*t_raqm_layout) (raqm_t *rq); -typedef raqm_glyph_t* (*t_raqm_get_glyphs) (raqm_t *rq, - size_t *length); -typedef raqm_glyph_t_01* (*t_raqm_get_glyphs_01) (raqm_t *rq, - size_t *length); -typedef void (*t_raqm_destroy) (raqm_t *rq); +/* round a 26.6 pixel coordinate to the nearest integer */ +#define PIXEL(x) ((((x) + 32) & -64) >> 6) -typedef struct { - void* raqm; - int version; - t_raqm_create create; - t_raqm_set_text set_text; - t_raqm_set_text_utf8 set_text_utf8; - t_raqm_set_par_direction set_par_direction; - t_raqm_add_font_feature add_font_feature; - t_raqm_set_freetype_face set_freetype_face; - t_raqm_layout layout; - t_raqm_get_glyphs get_glyphs; - t_raqm_get_glyphs_01 get_glyphs_01; - t_raqm_destroy destroy; -} p_raqm_func; - -static p_raqm_func p_raqm; - - -/* round a 26.6 pixel coordinate to the nearest larger integer */ -#define PIXEL(x) ((((x)+63) & -64)>>6) - -static PyObject* -geterror(int code) -{ +static PyObject * +geterror(int code) { int i; - for (i = 0; ft_errors[i].message; i++) + for (i = 0; ft_errors[i].message; i++) { if (ft_errors[i].code == code) { - PyErr_SetString(PyExc_IOError, ft_errors[i].message); + PyErr_SetString(PyExc_OSError, ft_errors[i].message); return NULL; } - - PyErr_SetString(PyExc_IOError, "unknown freetype error"); - return NULL; -} - -static int -setraqm(void) -{ - /* set the static function pointers for dynamic raqm linking */ - p_raqm.raqm = NULL; - - /* Microsoft needs a totally different system */ -#if !defined(_MSC_VER) - p_raqm.raqm = dlopen("libraqm.so.0", RTLD_LAZY); - if (!p_raqm.raqm) { - p_raqm.raqm = dlopen("libraqm.dylib", RTLD_LAZY); - } -#else - p_raqm.raqm = LoadLibrary("libraqm"); -#endif - - if (!p_raqm.raqm) { - return 1; - } - -#if !defined(_MSC_VER) - p_raqm.create = (t_raqm_create)dlsym(p_raqm.raqm, "raqm_create"); - p_raqm.set_text = (t_raqm_set_text)dlsym(p_raqm.raqm, "raqm_set_text"); - p_raqm.set_text_utf8 = (t_raqm_set_text_utf8)dlsym(p_raqm.raqm, "raqm_set_text_utf8"); - p_raqm.set_par_direction = (t_raqm_set_par_direction)dlsym(p_raqm.raqm, "raqm_set_par_direction"); - p_raqm.add_font_feature = (t_raqm_add_font_feature)dlsym(p_raqm.raqm, "raqm_add_font_feature"); - p_raqm.set_freetype_face = (t_raqm_set_freetype_face)dlsym(p_raqm.raqm, "raqm_set_freetype_face"); - p_raqm.layout = (t_raqm_layout)dlsym(p_raqm.raqm, "raqm_layout"); - p_raqm.destroy = (t_raqm_destroy)dlsym(p_raqm.raqm, "raqm_destroy"); - if(dlsym(p_raqm.raqm, "raqm_index_to_position")) { - p_raqm.get_glyphs = (t_raqm_get_glyphs)dlsym(p_raqm.raqm, "raqm_get_glyphs"); - p_raqm.version = 2; - } else { - p_raqm.version = 1; - p_raqm.get_glyphs_01 = (t_raqm_get_glyphs_01)dlsym(p_raqm.raqm, "raqm_get_glyphs"); - } - if (dlerror() || - !(p_raqm.create && - p_raqm.set_text && - p_raqm.set_text_utf8 && - p_raqm.set_par_direction && - p_raqm.add_font_feature && - p_raqm.set_freetype_face && - p_raqm.layout && - (p_raqm.get_glyphs || p_raqm.get_glyphs_01) && - p_raqm.destroy)) { - dlclose(p_raqm.raqm); - p_raqm.raqm = NULL; - return 2; - } -#else - p_raqm.create = (t_raqm_create)GetProcAddress(p_raqm.raqm, "raqm_create"); - p_raqm.set_text = (t_raqm_set_text)GetProcAddress(p_raqm.raqm, "raqm_set_text"); - p_raqm.set_text_utf8 = (t_raqm_set_text_utf8)GetProcAddress(p_raqm.raqm, "raqm_set_text_utf8"); - p_raqm.set_par_direction = (t_raqm_set_par_direction)GetProcAddress(p_raqm.raqm, "raqm_set_par_direction"); - p_raqm.add_font_feature = (t_raqm_add_font_feature)GetProcAddress(p_raqm.raqm, "raqm_add_font_feature"); - p_raqm.set_freetype_face = (t_raqm_set_freetype_face)GetProcAddress(p_raqm.raqm, "raqm_set_freetype_face"); - p_raqm.layout = (t_raqm_layout)GetProcAddress(p_raqm.raqm, "raqm_layout"); - p_raqm.destroy = (t_raqm_destroy)GetProcAddress(p_raqm.raqm, "raqm_destroy"); - if(GetProcAddress(p_raqm.raqm, "raqm_index_to_position")) { - p_raqm.get_glyphs = (t_raqm_get_glyphs)GetProcAddress(p_raqm.raqm, "raqm_get_glyphs"); - p_raqm.version = 2; - } else { - p_raqm.version = 1; - p_raqm.get_glyphs_01 = (t_raqm_get_glyphs_01)GetProcAddress(p_raqm.raqm, "raqm_get_glyphs"); - } - if (!(p_raqm.create && - p_raqm.set_text && - p_raqm.set_text_utf8 && - p_raqm.set_par_direction && - p_raqm.add_font_feature && - p_raqm.set_freetype_face && - p_raqm.layout && - (p_raqm.get_glyphs || p_raqm.get_glyphs_01) && - p_raqm.destroy)) { - FreeLibrary(p_raqm.raqm); - p_raqm.raqm = NULL; - return 2; } -#endif - return 0; + PyErr_SetString(PyExc_OSError, "unknown freetype error"); + return NULL; } -static PyObject* -getfont(PyObject* self_, PyObject* args, PyObject* kw) -{ +static PyObject * +getfont(PyObject *self_, PyObject *args, PyObject *kw) { /* create a font object from a file name and a size (in pixels) */ - FontObject* self; + FontObject *self; int error = 0; - char* filename = NULL; - int size; - int index = 0; - int layout_engine = 0; - unsigned char* encoding; - unsigned char* font_bytes; - int font_bytes_size = 0; - static char* kwlist[] = { - "filename", "size", "index", "encoding", "font_bytes", - "layout_engine", NULL - }; + char *filename = NULL; + Py_ssize_t size; + Py_ssize_t index = 0; + Py_ssize_t layout_engine = 0; + unsigned char *encoding; + unsigned char *font_bytes; + Py_ssize_t font_bytes_size = 0; + static char *kwlist[] = { + "filename", "size", "index", "encoding", "font_bytes", "layout_engine", NULL}; if (!library) { - PyErr_SetString( - PyExc_IOError, - "failed to initialize FreeType library" - ); + PyErr_SetString(PyExc_OSError, "failed to initialize FreeType library"); return NULL; } - if (!PyArg_ParseTupleAndKeywords(args, kw, "eti|is"PY_ARG_BYTES_LENGTH"i", - kwlist, - Py_FileSystemDefaultEncoding, &filename, - &size, &index, &encoding, &font_bytes, - &font_bytes_size, &layout_engine)) { + if (!PyArg_ParseTupleAndKeywords( + args, + kw, + "etn|nsy#n", + kwlist, + Py_FileSystemDefaultEncoding, + &filename, + &size, + &index, + &encoding, + &font_bytes, + &font_bytes_size, + &layout_engine)) { return NULL; } self = PyObject_New(FontObject, &Font_Type); if (!self) { - if (filename) + if (filename) { PyMem_Free(filename); + } return NULL; } @@ -273,198 +171,183 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) /* Don't free this before FT_Done_Face */ self->font_bytes = PyMem_Malloc(font_bytes_size); if (!self->font_bytes) { - error = 65; // Out of Memory in Freetype. + error = 65; // Out of Memory in Freetype. } if (!error) { memcpy(self->font_bytes, font_bytes, (size_t)font_bytes_size); - error = FT_New_Memory_Face(library, (FT_Byte*)self->font_bytes, - font_bytes_size, index, &self->face); + error = FT_New_Memory_Face( + library, + (FT_Byte *)self->font_bytes, + font_bytes_size, + index, + &self->face); } } - if (!error) + if (!error) { error = FT_Set_Pixel_Sizes(self->face, 0, size); + } - if (!error && encoding && strlen((char*) encoding) == 4) { - FT_Encoding encoding_tag = FT_MAKE_TAG( - encoding[0], encoding[1], encoding[2], encoding[3] - ); + if (!error && encoding && strlen((char *)encoding) == 4) { + FT_Encoding encoding_tag = + FT_MAKE_TAG(encoding[0], encoding[1], encoding[2], encoding[3]); error = FT_Select_Charmap(self->face, encoding_tag); } - if (filename) - PyMem_Free(filename); + if (filename) { + PyMem_Free(filename); + } if (error) { if (self->font_bytes) { PyMem_Free(self->font_bytes); + self->font_bytes = NULL; } Py_DECREF(self); return geterror(error); } - return (PyObject*) self; + return (PyObject *)self; } static int -font_getchar(PyObject* string, int index, FT_ULong* char_out) -{ +font_getchar(PyObject *string, int index, FT_ULong *char_out) { if (PyUnicode_Check(string)) { - Py_UNICODE* p = PyUnicode_AS_UNICODE(string); - int size = PyUnicode_GET_SIZE(string); - if (index >= size) - return 0; - *char_out = p[index]; - return 1; - } - -#if PY_VERSION_HEX < 0x03000000 - if (PyString_Check(string)) { - unsigned char* p = (unsigned char*) PyString_AS_STRING(string); - int size = PyString_GET_SIZE(string); - if (index >= size) + if (index >= PyUnicode_GET_LENGTH(string)) { return 0; - *char_out = (unsigned char) p[index]; + } + *char_out = PyUnicode_READ_CHAR(string, index); return 1; } -#endif - return 0; } +#ifdef HAVE_RAQM + static size_t -text_layout_raqm(PyObject* string, FontObject* self, const char* dir, - PyObject *features ,GlyphInfo **glyph_info, int mask) -{ - int i = 0; +text_layout_raqm( + PyObject *string, + FontObject *self, + const char *dir, + PyObject *features, + const char *lang, + GlyphInfo **glyph_info, + int mask, + int color) { + size_t i = 0, count = 0, start = 0; raqm_t *rq; - size_t count = 0; raqm_glyph_t *glyphs = NULL; - raqm_glyph_t_01 *glyphs_01 = NULL; raqm_direction_t direction; - rq = (*p_raqm.create)(); + rq = raqm_create(); if (rq == NULL) { PyErr_SetString(PyExc_ValueError, "raqm_create() failed."); goto failed; } if (PyUnicode_Check(string)) { - Py_UNICODE *text = PyUnicode_AS_UNICODE(string); - Py_ssize_t size = PyUnicode_GET_SIZE(string); - if (! size) { + Py_UCS4 *text = PyUnicode_AsUCS4Copy(string); + Py_ssize_t size = PyUnicode_GET_LENGTH(string); + if (!text || !size) { /* return 0 and clean up, no glyphs==no size, and raqm fails with empty strings */ goto failed; } - if (!(*p_raqm.set_text)(rq, (const uint32_t *)(text), size)) { + int set_text = raqm_set_text(rq, text, size); + PyMem_Free(text); + if (!set_text) { PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); goto failed; } - } -#if PY_VERSION_HEX < 0x03000000 - else if (PyString_Check(string)) { - char *text = PyString_AS_STRING(string); - int size = PyString_GET_SIZE(string); - if (! size) { - goto failed; - } - if (!(*p_raqm.set_text_utf8)(rq, text, size)) { - PyErr_SetString(PyExc_ValueError, "raqm_set_text_utf8() failed"); - goto failed; + if (lang) { + if (!raqm_set_language(rq, lang, start, size)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); + goto failed; + } } - } -#endif - else { + } else { PyErr_SetString(PyExc_TypeError, "expected string"); goto failed; } direction = RAQM_DIRECTION_DEFAULT; if (dir) { - if (strcmp(dir, "rtl") == 0) + if (strcmp(dir, "rtl") == 0) { direction = RAQM_DIRECTION_RTL; - else if (strcmp(dir, "ltr") == 0) + } else if (strcmp(dir, "ltr") == 0) { direction = RAQM_DIRECTION_LTR; - else if (strcmp(dir, "ttb") == 0) + } else if (strcmp(dir, "ttb") == 0) { direction = RAQM_DIRECTION_TTB; - else { - PyErr_SetString(PyExc_ValueError, "direction must be either 'rtl', 'ltr' or 'ttb'"); +#if !defined(RAQM_VERSION_ATLEAST) + /* RAQM_VERSION_ATLEAST was added in Raqm 0.7.0 */ + PyErr_SetString( + PyExc_ValueError, + "libraqm 0.7 or greater required for 'ttb' direction"); + goto failed; +#endif + } else { + PyErr_SetString( + PyExc_ValueError, "direction must be either 'rtl', 'ltr' or 'ttb'"); goto failed; } } - if (!(*p_raqm.set_par_direction)(rq, direction)) { + if (!raqm_set_par_direction(rq, direction)) { PyErr_SetString(PyExc_ValueError, "raqm_set_par_direction() failed"); goto failed; } if (features != Py_None) { - int len; + int j, len; PyObject *seq = PySequence_Fast(features, "expected a sequence"); if (!seq) { goto failed; } - len = PySequence_Size(seq); - for (i = 0; i < len; i++) { - PyObject *item = PySequence_Fast_GET_ITEM(seq, i); + len = PySequence_Fast_GET_SIZE(seq); + for (j = 0; j < len; j++) { + PyObject *item = PySequence_Fast_GET_ITEM(seq, j); char *feature = NULL; Py_ssize_t size = 0; PyObject *bytes; -#if PY_VERSION_HEX >= 0x03000000 if (!PyUnicode_Check(item)) { -#else - if (!PyUnicode_Check(item) && !PyString_Check(item)) { -#endif + 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); - } -#if PY_VERSION_HEX < 0x03000000 - else { - feature = PyString_AsString(item); - size = PyString_GET_SIZE(item); + bytes = PyUnicode_AsUTF8String(item); + if (bytes == NULL) { + Py_DECREF(seq); + goto failed; } -#endif - if (!(*p_raqm.add_font_feature)(rq, feature, size)) { + 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 (!(*p_raqm.set_freetype_face)(rq, self->face)) { + if (!raqm_set_freetype_face(rq, self->face)) { PyErr_SetString(PyExc_RuntimeError, "raqm_set_freetype_face() failed."); goto failed; } - if (!(*p_raqm.layout)(rq)) { + if (!raqm_layout(rq)) { PyErr_SetString(PyExc_RuntimeError, "raqm_layout() failed."); goto failed; } - if (p_raqm.version == 1){ - glyphs_01 = (*p_raqm.get_glyphs_01)(rq, &count); - if (glyphs_01 == NULL) { - PyErr_SetString(PyExc_ValueError, "raqm_get_glyphs() failed."); - count = 0; - goto failed; - } - } else { /* version == 2 */ - glyphs = (*p_raqm.get_glyphs)(rq, &count); - if (glyphs == NULL) { - PyErr_SetString(PyExc_ValueError, "raqm_get_glyphs() failed."); - count = 0; - goto failed; - } + glyphs = raqm_get_glyphs(rq, &count); + if (glyphs == NULL) { + PyErr_SetString(PyExc_ValueError, "raqm_get_glyphs() failed."); + count = 0; + goto failed; } (*glyph_info) = PyMem_New(GlyphInfo, count); @@ -474,33 +357,32 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, goto failed; } - if (p_raqm.version == 1){ - for (i = 0; i < count; i++) { - (*glyph_info)[i].index = glyphs_01[i].index; - (*glyph_info)[i].x_offset = glyphs_01[i].x_offset; - (*glyph_info)[i].x_advance = glyphs_01[i].x_advance; - (*glyph_info)[i].y_offset = glyphs_01[i].y_offset; - (*glyph_info)[i].cluster = glyphs_01[i].cluster; - } - } else { - for (i = 0; i < count; i++) { - (*glyph_info)[i].index = glyphs[i].index; - (*glyph_info)[i].x_offset = glyphs[i].x_offset; - (*glyph_info)[i].x_advance = glyphs[i].x_advance; - (*glyph_info)[i].y_offset = glyphs[i].y_offset; - (*glyph_info)[i].cluster = glyphs[i].cluster; - } + for (i = 0; i < count; i++) { + (*glyph_info)[i].index = glyphs[i].index; + (*glyph_info)[i].x_offset = glyphs[i].x_offset; + (*glyph_info)[i].x_advance = glyphs[i].x_advance; + (*glyph_info)[i].y_offset = glyphs[i].y_offset; + (*glyph_info)[i].y_advance = glyphs[i].y_advance; + (*glyph_info)[i].cluster = glyphs[i].cluster; } failed: - (*p_raqm.destroy)(rq); + raqm_destroy(rq); return count; } +#endif + static size_t -text_layout_fallback(PyObject* string, FontObject* self, const char* dir, - PyObject *features ,GlyphInfo **glyph_info, int mask) -{ +text_layout_fallback( + PyObject *string, + FontObject *self, + const char *dir, + PyObject *features, + const char *lang, + GlyphInfo **glyph_info, + int mask, + int color) { int error, load_flags; FT_ULong ch; Py_ssize_t count; @@ -509,21 +391,20 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, FT_UInt last_index = 0; int i; - if (features != Py_None || dir != NULL) { - PyErr_SetString(PyExc_KeyError, "setting text direction or font features is not supported without libraqm"); + if (features != Py_None || dir != NULL || lang != NULL) { + PyErr_SetString( + PyExc_KeyError, + "setting text direction, language or font features is not supported " + "without libraqm"); } -#if PY_VERSION_HEX >= 0x03000000 if (!PyUnicode_Check(string)) { -#else - if (!PyUnicode_Check(string) && !PyString_Check(string)) { -#endif PyErr_SetString(PyExc_TypeError, "expected string"); return 0; } count = 0; while (font_getchar(string, count, &ch)) { - count++; + count++; } if (count == 0) { return 0; @@ -535,10 +416,15 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, return 0; } - load_flags = FT_LOAD_RENDER|FT_LOAD_NO_BITMAP; + load_flags = FT_LOAD_DEFAULT; if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } +#ifdef FT_LOAD_COLOR + if (color) { + load_flags |= FT_LOAD_COLOR; + } +#endif for (i = 0; font_getchar(string, i, &ch); i++) { (*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); @@ -547,16 +433,24 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, return 0; } glyph = self->face->glyph; - (*glyph_info)[i].x_offset=0; - (*glyph_info)[i].y_offset=0; + (*glyph_info)[i].x_offset = 0; + (*glyph_info)[i].y_offset = 0; if (kerning && last_index && (*glyph_info)[i].index) { FT_Vector delta; - if (FT_Get_Kerning(self->face, last_index, (*glyph_info)[i].index, - ft_kerning_default,&delta) == 0) - (*glyph_info)[i-1].x_advance += PIXEL(delta.x); + if (FT_Get_Kerning( + self->face, + last_index, + (*glyph_info)[i].index, + ft_kerning_default, + &delta) == 0) { + (*glyph_info)[i - 1].x_advance += PIXEL(delta.x); + (*glyph_info)[i - 1].y_advance += PIXEL(delta.y); + } } (*glyph_info)[i].x_advance = glyph->metrics.horiAdvance; + // y_advance is only used in ttb, which is not supported by basic layout + (*glyph_info)[i].y_advance = 0; last_index = (*glyph_info)[i].index; (*glyph_info)[i].cluster = ch; } @@ -564,88 +458,193 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, } static size_t -text_layout(PyObject* string, FontObject* self, const char* dir, - PyObject *features, GlyphInfo **glyph_info, int mask) -{ +text_layout( + PyObject *string, + FontObject *self, + const char *dir, + PyObject *features, + const char *lang, + GlyphInfo **glyph_info, + int mask, + int color) { size_t count; - - if (p_raqm.raqm && self->layout_engine == LAYOUT_RAQM) { - count = text_layout_raqm(string, self, dir, features, glyph_info, mask); - } else { - count = text_layout_fallback(string, self, dir, features, glyph_info, mask); +#ifdef HAVE_RAQM + if (have_raqm && self->layout_engine == LAYOUT_RAQM) { + count = text_layout_raqm( + string, self, dir, features, lang, glyph_info, mask, color); + } else +#endif + { + count = text_layout_fallback( + string, self, dir, features, lang, glyph_info, mask, color); } return count; } -static PyObject* -font_getsize(FontObject* self, PyObject* args) -{ - int i, x, y_max, y_min; +static PyObject * +font_getlength(FontObject *self, PyObject *args) { + int length; /* length along primary axis, in 26.6 precision */ + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t i, count; /* glyph_info index and length */ + int horizontal_dir; /* is primary axis horizontal? */ + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + const char *mode = NULL; + const char *dir = NULL; + const char *lang = NULL; + PyObject *features = Py_None; + PyObject *string; + + /* calculate size and bearing for a given string */ + + if (!PyArg_ParseTuple( + args, "O|zzOz:getlength", &string, &mode, &dir, &features, &lang)) { + return NULL; + } + + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); + if (PyErr_Occurred()) { + return NULL; + } + + length = 0; + for (i = 0; i < count; i++) { + if (horizontal_dir) { + length += glyph_info[i].x_advance; + } else { + length -= glyph_info[i].y_advance; + } + } + + if (glyph_info) { + PyMem_Free(glyph_info); + glyph_info = NULL; + } + + return PyLong_FromLong(length); +} + +static PyObject * +font_getsize(FontObject *self, PyObject *args) { + int position; /* pen position along primary axis, in 26.6 precision */ + int advanced; /* pen position along primary axis, in pixels */ + int px, py; /* position of current glyph, in pixels */ + int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */ + int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */ + int load_flags; /* FreeType load_flags parameter */ + int error; FT_Face face; - int xoffset, yoffset; + FT_Glyph glyph; + FT_BBox bbox; /* glyph bounding box */ + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t i, count; /* glyph_info index and length */ + int horizontal_dir; /* is primary axis horizontal? */ + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + const char *mode = NULL; const char *dir = NULL; - size_t count; - GlyphInfo *glyph_info = NULL; + const char *lang = NULL; + const char *anchor = NULL; PyObject *features = Py_None; + PyObject *string; /* calculate size and bearing for a given string */ - PyObject* string; - if (!PyArg_ParseTuple(args, "O|zO:getsize", &string, &dir, &features)) + if (!PyArg_ParseTuple( + args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { return NULL; + } - face = NULL; - xoffset = yoffset = 0; - y_max = y_min = 0; + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; - count = text_layout(string, self, dir, features, &glyph_info, 0); + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + if (anchor == NULL) { + anchor = horizontal_dir ? "la" : "lt"; + } + if (strlen(anchor) != 2) { + goto bad_anchor; + } + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); if (PyErr_Occurred()) { return NULL; } + load_flags = FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } +#ifdef FT_LOAD_COLOR + if (color) { + load_flags |= FT_LOAD_COLOR; + } +#endif - for (x = i = 0; i < count; i++) { - int index, error; - FT_BBox bbox; - FT_Glyph glyph; + /* + * text bounds are given by: + * - bounding boxes of individual glyphs + * - pen line, i.e. 0 to `advanced` along primary axis + * this means point (0, 0) is part of the text bounding box + */ + face = NULL; + position = x_min = x_max = y_min = y_max = 0; + for (i = 0; i < count; i++) { face = self->face; - index = glyph_info[i].index; - /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 - * Yifu Yu, 2014-10-15 - */ - error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT|FT_LOAD_NO_BITMAP); - if (error) - return geterror(error); - if (i == 0 && face->glyph->metrics.horiBearingX < 0) { - xoffset = face->glyph->metrics.horiBearingX; - x -= xoffset; + if (horizontal_dir) { + px = PIXEL(position + glyph_info[i].x_offset); + py = PIXEL(glyph_info[i].y_offset); + + position += glyph_info[i].x_advance; + advanced = PIXEL(position); + if (advanced > x_max) { + x_max = advanced; + } + } else { + px = PIXEL(glyph_info[i].x_offset); + py = PIXEL(position + glyph_info[i].y_offset); + + position += glyph_info[i].y_advance; + advanced = PIXEL(position); + if (advanced < y_min) { + y_min = advanced; + } } - x += glyph_info[i].x_advance; + error = FT_Load_Glyph(face, glyph_info[i].index, load_flags); + if (error) { + return geterror(error); + } - if (i == count - 1) - { - int offset; - offset = glyph_info[i].x_advance - - face->glyph->metrics.width - - face->glyph->metrics.horiBearingX; - if (offset < 0) - x -= offset; + error = FT_Get_Glyph(face->glyph, &glyph); + if (error) { + return geterror(error); } - FT_Get_Glyph(face->glyph, &glyph); - FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_SUBPIXELS, &bbox); - bbox.yMax -= glyph_info[i].y_offset; - bbox.yMin -= glyph_info[i].y_offset; - if (bbox.yMax > y_max) + FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox); + bbox.xMax += px; + if (bbox.xMax > x_max) { + x_max = bbox.xMax; + } + bbox.xMin += px; + if (bbox.xMin < x_min) { + x_min = bbox.xMin; + } + bbox.yMax += py; + if (bbox.yMax > y_max) { y_max = bbox.yMax; - if (bbox.yMin < y_min) + } + bbox.yMin += py; + if (bbox.yMin < y_min) { y_min = bbox.yMin; - - /* find max distance of baseline from top */ - if (face->glyph->metrics.horiBearingY > yoffset) - yoffset = face->glyph->metrics.horiBearingY; + } FT_Done_Glyph(glyph); } @@ -654,94 +653,167 @@ font_getsize(FontObject* self, PyObject* args) PyMem_Free(glyph_info); glyph_info = NULL; } - - if (face) { - /* left bearing */ - if (xoffset < 0) - x -= xoffset; - else - xoffset = 0; - /* difference between the font ascender and the distance of - * the baseline from the top */ - yoffset = PIXEL(self->face->size->metrics.ascender - yoffset); + x_anchor = y_anchor = 0; + if (face) { + if (horizontal_dir) { + switch (anchor[0]) { + case 'l': // left + x_anchor = 0; + break; + case 'm': // middle (left + right) / 2 + x_anchor = PIXEL(position / 2); + break; + case 'r': // right + x_anchor = PIXEL(position); + break; + case 's': // vertical baseline + default: + goto bad_anchor; + } + switch (anchor[1]) { + case 'a': // ascender + y_anchor = PIXEL(self->face->size->metrics.ascender); + break; + case 't': // top + y_anchor = y_max; + break; + case 'm': // middle (ascender + descender) / 2 + y_anchor = PIXEL( + (self->face->size->metrics.ascender + + self->face->size->metrics.descender) / + 2); + break; + case 's': // horizontal baseline + y_anchor = 0; + break; + case 'b': // bottom + y_anchor = y_min; + break; + case 'd': // descender + y_anchor = PIXEL(self->face->size->metrics.descender); + break; + default: + goto bad_anchor; + } + } else { + switch (anchor[0]) { + case 'l': // left + x_anchor = x_min; + break; + case 'm': // middle (left + right) / 2 + x_anchor = (x_min + x_max) / 2; + break; + case 'r': // right + x_anchor = x_max; + break; + case 's': // vertical baseline + x_anchor = 0; + break; + default: + goto bad_anchor; + } + switch (anchor[1]) { + case 't': // top + y_anchor = 0; + break; + case 'm': // middle (top + bottom) / 2 + y_anchor = PIXEL(position / 2); + break; + case 'b': // bottom + y_anchor = PIXEL(position); + break; + case 'a': // ascender + case 's': // horizontal baseline + case 'd': // descender + default: + goto bad_anchor; + } + } } return Py_BuildValue( "(ii)(ii)", - PIXEL(x), PIXEL(y_max - y_min), - PIXEL(xoffset), yoffset - ); -} - -static PyObject* -font_getabc(FontObject* self, PyObject* args) -{ - FT_ULong ch; - FT_Face face; - double a, b, c; - - /* calculate ABC values for a given string */ + (x_max - x_min), + (y_max - y_min), + (-x_anchor + x_min), + -(-y_anchor + y_max)); - PyObject* string; - if (!PyArg_ParseTuple(args, "O:getabc", &string)) - return NULL; - -#if PY_VERSION_HEX >= 0x03000000 - if (!PyUnicode_Check(string)) { -#else - if (!PyUnicode_Check(string) && !PyString_Check(string)) { -#endif - PyErr_SetString(PyExc_TypeError, "expected string"); - return NULL; - } - - if (font_getchar(string, 0, &ch)) { - int index, error; - face = self->face; - index = FT_Get_Char_Index(face, ch); - /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ - error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT|FT_LOAD_NO_BITMAP); - if (error) - return geterror(error); - a = face->glyph->metrics.horiBearingX / 64.0; - b = face->glyph->metrics.width / 64.0; - c = (face->glyph->metrics.horiAdvance - - face->glyph->metrics.horiBearingX - - face->glyph->metrics.width) / 64.0; - } else - a = b = c = 0.0; - - return Py_BuildValue("ddd", a, b, c); +bad_anchor: + PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor); + return NULL; } -static PyObject* -font_render(FontObject* self, PyObject* args) -{ - int i, x, y; +static PyObject * +font_render(FontObject *self, PyObject *args) { + int x, y; /* pen position, in 26.6 precision */ + int px, py; /* position of current glyph, in pixels */ + int x_min, y_max; /* text offset in 26.6 precision */ + int load_flags; /* FreeType load_flags parameter */ + int error; + FT_Glyph glyph; + FT_GlyphSlot glyph_slot; + FT_Bitmap bitmap; + FT_Bitmap bitmap_converted; /* initialized lazily, for non-8bpp fonts */ + FT_BitmapGlyph bitmap_glyph; + FT_Stroker stroker = NULL; + int bitmap_converted_ready = 0; /* has bitmap_converted been initialized */ + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t i, count; /* glyph_info index and length */ + int xx, yy; /* pixel offset of current glyph bitmap */ + int x0, x1; /* horizontal bounds of glyph bitmap to copy */ + unsigned int bitmap_y; /* glyph bitmap y index */ + unsigned char *source; /* glyph bitmap source buffer */ + unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ Imaging im; - int index, error, ascender; - int load_flags; - unsigned char *source; - FT_GlyphSlot glyph; - /* render string into given buffer (the buffer *must* have - the right size, or this will crash) */ - PyObject* string; Py_ssize_t id; - int mask = 0; - int temp; - int xx, x0, x1; + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + int stroke_width = 0; + PY_LONG_LONG foreground_ink_long = 0; + unsigned int foreground_ink; + const char *mode = NULL; const char *dir = NULL; - size_t count; - GlyphInfo *glyph_info; - PyObject *features = NULL; + const char *lang = NULL; + PyObject *features = Py_None; + PyObject *string; - if (!PyArg_ParseTuple(args, "On|izO:render", &string, &id, &mask, &dir, &features)) { + /* render string into given buffer (the buffer *must* have + the right size, or this will crash) */ + + if (!PyArg_ParseTuple( + args, + "On|zzOziL:render", + &string, + &id, + &mode, + &dir, + &features, + &lang, + &stroke_width, + &foreground_ink_long)) { return NULL; } - glyph_info = NULL; - count = text_layout(string, self, dir, features, &glyph_info, mask); + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + foreground_ink = foreground_ink_long; + +#ifdef FT_COLOR_H + if (color) { + FT_Color foreground_color; + FT_Byte *ink = (FT_Byte *)&foreground_ink; + foreground_color.red = ink[0]; + foreground_color.green = ink[1]; + foreground_color.blue = ink[2]; + foreground_color.alpha = + (FT_Byte)255; /* ink alpha is handled in ImageDraw.text */ + FT_Palette_Set_Foreground_Color(self->face, foreground_color); + } +#endif + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); if (PyErr_Occurred()) { return NULL; } @@ -749,99 +821,400 @@ font_render(FontObject* self, PyObject* args) Py_RETURN_NONE; } - im = (Imaging) id; - /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ - load_flags = FT_LOAD_RENDER|FT_LOAD_NO_BITMAP; - if (mask) + if (stroke_width) { + error = FT_Stroker_New(library, &stroker); + if (error) { + return geterror(error); + } + + FT_Stroker_Set( + stroker, + (FT_Fixed)stroke_width * 64, + FT_STROKER_LINECAP_ROUND, + FT_STROKER_LINEJOIN_ROUND, + 0); + } + + im = (Imaging)id; + load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT; + if (mask) { load_flags |= FT_LOAD_TARGET_MONO; + } +#ifdef FT_LOAD_COLOR + if (color) { + load_flags |= FT_LOAD_COLOR; + } +#endif - ascender = 0; + /* + * calculate x_min and y_max + * must match font_getsize or there may be clipping! + */ + x = y = x_min = y_max = 0; for (i = 0; i < count; i++) { - index = glyph_info[i].index; - error = FT_Load_Glyph(self->face, index, load_flags); - if (error) + px = PIXEL(x + glyph_info[i].x_offset); + py = PIXEL(y + glyph_info[i].y_offset); + + error = + FT_Load_Glyph(self->face, glyph_info[i].index, load_flags | FT_LOAD_RENDER); + if (error) { return geterror(error); + } - glyph = self->face->glyph; - temp = (glyph->bitmap.rows - glyph->bitmap_top); - temp -= PIXEL(glyph_info[i].y_offset); - if (temp > ascender) - ascender = temp; + glyph_slot = self->face->glyph; + bitmap = glyph_slot->bitmap; + + if (glyph_slot->bitmap_top + py > y_max) { + y_max = glyph_slot->bitmap_top + py; + } + if (glyph_slot->bitmap_left + px < x_min) { + x_min = glyph_slot->bitmap_left + px; + } + + x += glyph_info[i].x_advance; + y += glyph_info[i].y_advance; } - for (x = i = 0; i < count; i++) { - if (i == 0 && self->face->glyph->metrics.horiBearingX < 0) - x = -self->face->glyph->metrics.horiBearingX; + /* set pen position to text origin */ + x = (-x_min + stroke_width) << 6; + y = (-y_max + (-stroke_width)) << 6; - index = glyph_info[i].index; - error = FT_Load_Glyph(self->face, index, load_flags); - if (error) + if (stroker == NULL) { + load_flags |= FT_LOAD_RENDER; + } + + for (i = 0; i < count; i++) { + px = PIXEL(x + glyph_info[i].x_offset); + py = PIXEL(y + glyph_info[i].y_offset); + + error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags); + if (error) { return geterror(error); + } + + glyph_slot = self->face->glyph; + if (stroker != NULL) { + error = FT_Get_Glyph(glyph_slot, &glyph); + if (!error) { + error = FT_Glyph_Stroke(&glyph, stroker, 1); + } + if (!error) { + FT_Vector origin = {0, 0}; + error = FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, &origin, 1); + } + if (error) { + return geterror(error); + } - if (i == 0 && self->face->glyph->metrics.horiBearingX < 0) { - x = -self->face->glyph->metrics.horiBearingX; - } + bitmap_glyph = (FT_BitmapGlyph)glyph; - glyph = self->face->glyph; + bitmap = bitmap_glyph->bitmap; + xx = px + bitmap_glyph->left; + yy = -(py + bitmap_glyph->top); + } else { + bitmap = glyph_slot->bitmap; + xx = px + glyph_slot->bitmap_left; + yy = -(py + glyph_slot->bitmap_top); + } - source = (unsigned char*) glyph->bitmap.buffer; - xx = PIXEL(x) + glyph->bitmap_left; - xx += PIXEL(glyph_info[i].x_offset); + /* convert non-8bpp bitmaps */ + switch (bitmap.pixel_mode) { + case FT_PIXEL_MODE_MONO: + convert_scale = 255; + break; + case FT_PIXEL_MODE_GRAY2: + convert_scale = 255 / 3; + break; + case FT_PIXEL_MODE_GRAY4: + convert_scale = 255 / 15; + break; + default: + convert_scale = 1; + } + switch (bitmap.pixel_mode) { + case FT_PIXEL_MODE_MONO: + case FT_PIXEL_MODE_GRAY2: + case FT_PIXEL_MODE_GRAY4: + if (!bitmap_converted_ready) { + FT_Bitmap_Init(&bitmap_converted); + bitmap_converted_ready = 1; + } + error = FT_Bitmap_Convert(library, &bitmap, &bitmap_converted, 1); + if (error) { + geterror(error); + goto glyph_error; + } + bitmap = bitmap_converted; + /* bitmap is now FT_PIXEL_MODE_GRAY, fall through */ + case FT_PIXEL_MODE_GRAY: + break; +#ifdef FT_LOAD_COLOR + case FT_PIXEL_MODE_BGRA: + if (color) { + break; + } + /* we didn't ask for color, fall through to default */ +#endif + default: + PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode"); + goto glyph_error; + } + + /* clip glyph bitmap width to target image bounds */ x0 = 0; - x1 = glyph->bitmap.width; - if (xx < 0) + x1 = bitmap.width; + if (xx < 0) { x0 = -xx; - if (xx + x1 > im->xsize) + } + if (xx + x1 > im->xsize) { x1 = im->xsize - xx; + } - if (mask) { - /* use monochrome mask (on palette images, etc) */ - for (y = 0; y < glyph->bitmap.rows; y++) { - int yy = y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender); - yy -= PIXEL(glyph_info[i].y_offset); - if (yy >= 0 && yy < im->ysize) { - /* blend this glyph into the buffer */ - unsigned char *target = im->image8[yy] + xx; - int i, j, m = 128; - for (i = j = 0; j < x1; j++) { - if (j >= x0 && (source[i] & m)) - target[j] = 255; - if (!(m >>= 1)) { - m = 128; - i++; + source = (unsigned char *)bitmap.buffer; + for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++, yy++) { + /* clip glyph bitmap height to target image bounds */ + if (yy >= 0 && yy < im->ysize) { + /* blend this glyph into the buffer */ + int k; + unsigned char v; + unsigned char *target; + if (color) { + /* target[RGB] returns the color, target[A] returns the mask */ + /* target bands get split again in ImageDraw.text */ + target = (unsigned char *)im->image[yy] + xx * 4; + } else { + target = im->image8[yy] + xx; + } +#ifdef FT_LOAD_COLOR + if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) { + /* paste color glyph */ + for (k = x0; k < x1; k++) { + if (target[k * 4 + 3] < source[k * 4 + 3]) { + /* unpremultiply BGRa to RGBA */ + target[k * 4 + 0] = CLIP8( + (255 * (int)source[k * 4 + 2]) / source[k * 4 + 3]); + target[k * 4 + 1] = CLIP8( + (255 * (int)source[k * 4 + 1]) / source[k * 4 + 3]); + target[k * 4 + 2] = CLIP8( + (255 * (int)source[k * 4 + 0]) / source[k * 4 + 3]); + target[k * 4 + 3] = source[k * 4 + 3]; } } - } - source += glyph->bitmap.pitch; - } - } else { - /* use antialiased rendering */ - for (y = 0; y < glyph->bitmap.rows; y++) { - int yy = y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender); - yy -= PIXEL(glyph_info[i].y_offset); - if (yy >= 0 && yy < im->ysize) { - /* blend this glyph into the buffer */ - - int i; - unsigned char *target = im->image8[yy] + xx; - for (i = x0; i < x1; i++) { - if (target[i] < source[i]) - target[i] = source[i]; + } else +#endif + if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { + if (color) { + unsigned char *ink = (unsigned char *)&foreground_ink; + for (k = x0; k < x1; k++) { + v = source[k] * convert_scale; + if (target[k * 4 + 3] < v) { + target[k * 4 + 0] = ink[0]; + target[k * 4 + 1] = ink[1]; + target[k * 4 + 2] = ink[2]; + target[k * 4 + 3] = v; + } + } + } else { + for (k = x0; k < x1; k++) { + v = source[k] * convert_scale; + if (target[k] < v) { + target[k] = v; + } + } } + } else { + PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode"); + goto glyph_error; } - source += glyph->bitmap.pitch; } + source += bitmap.pitch; } x += glyph_info[i].x_advance; + y += glyph_info[i].y_advance; + if (stroker != NULL) { + FT_Done_Glyph(glyph); + } } + if (bitmap_converted_ready) { + FT_Bitmap_Done(library, &bitmap_converted); + } + FT_Stroker_Done(stroker); PyMem_Del(glyph_info); Py_RETURN_NONE; + +glyph_error: + if (stroker != NULL) { + FT_Done_Glyph(glyph); + } + if (bitmap_converted_ready) { + FT_Bitmap_Done(library, &bitmap_converted); + } + FT_Stroker_Done(stroker); + PyMem_Del(glyph_info); + return NULL; +} + +#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ + (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) +static PyObject * +font_getvarnames(FontObject *self) { + int error; + FT_UInt i, j, num_namedstyles, name_count; + FT_MM_Var *master; + FT_SfntName name; + PyObject *list_names, *list_name; + + error = FT_Get_MM_Var(self->face, &master); + if (error) { + return geterror(error); + } + + num_namedstyles = master->num_namedstyles; + list_names = PyList_New(num_namedstyles); + + name_count = FT_Get_Sfnt_Name_Count(self->face); + for (i = 0; i < name_count; i++) { + error = FT_Get_Sfnt_Name(self->face, i, &name); + if (error) { + return geterror(error); + } + + for (j = 0; j < num_namedstyles; j++) { + if (PyList_GetItem(list_names, j) != NULL) { + continue; + } + + if (master->namedstyle[j].strid == name.name_id) { + list_name = Py_BuildValue("y#", name.string, name.string_len); + PyList_SetItem(list_names, j, list_name); + break; + } + } + } + + FT_Done_MM_Var(library, master); + + return list_names; } +static PyObject * +font_getvaraxes(FontObject *self) { + int error; + FT_UInt i, j, num_axis, name_count; + FT_MM_Var *master; + FT_Var_Axis axis; + FT_SfntName name; + PyObject *list_axes, *list_axis, *axis_name; + error = FT_Get_MM_Var(self->face, &master); + if (error) { + return geterror(error); + } + + num_axis = master->num_axis; + name_count = FT_Get_Sfnt_Name_Count(self->face); + + list_axes = PyList_New(num_axis); + for (i = 0; i < num_axis; i++) { + axis = master->axis[i]; + + list_axis = PyDict_New(); + PyDict_SetItemString( + list_axis, "minimum", PyLong_FromLong(axis.minimum / 65536)); + PyDict_SetItemString(list_axis, "default", PyLong_FromLong(axis.def / 65536)); + PyDict_SetItemString( + list_axis, "maximum", PyLong_FromLong(axis.maximum / 65536)); + + for (j = 0; j < name_count; j++) { + error = FT_Get_Sfnt_Name(self->face, j, &name); + if (error) { + return geterror(error); + } + + if (name.name_id == axis.strid) { + axis_name = Py_BuildValue("y#", name.string, name.string_len); + PyDict_SetItemString(list_axis, "name", axis_name); + break; + } + } + + PyList_SetItem(list_axes, i, list_axis); + } + + FT_Done_MM_Var(library, master); + + return list_axes; +} + +static PyObject * +font_setvarname(FontObject *self, PyObject *args) { + int error; + + int instance_index; + if (!PyArg_ParseTuple(args, "i", &instance_index)) { + return NULL; + } + + error = FT_Set_Named_Instance(self->face, instance_index); + if (error) { + return geterror(error); + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +font_setvaraxes(FontObject *self, PyObject *args) { + int error; + + PyObject *axes, *item; + Py_ssize_t i, num_coords; + FT_Fixed *coords; + FT_Fixed coord; + if (!PyArg_ParseTuple(args, "O", &axes)) { + return NULL; + } + + if (!PyList_Check(axes)) { + PyErr_SetString(PyExc_TypeError, "argument must be a list"); + return NULL; + } + + num_coords = PyObject_Length(axes); + coords = (FT_Fixed*)malloc(num_coords * sizeof(FT_Fixed)); + if (coords == NULL) { + return PyErr_NoMemory(); + } + for (i = 0; i < num_coords; i++) { + item = PyList_GET_ITEM(axes, i); + if (PyFloat_Check(item)) { + coord = PyFloat_AS_DOUBLE(item); + } else if (PyLong_Check(item)) { + coord = (float)PyLong_AS_LONG(item); + } else if (PyNumber_Check(item)) { + coord = PyFloat_AsDouble(item); + } else { + free(coords); + PyErr_SetString(PyExc_TypeError, "list must contain numbers"); + return NULL; + } + coords[i] = coord * 65536; + } + + error = FT_Set_Var_Design_Coordinates(self->face, num_coords, coords); + free(coords); + if (error) { + return geterror(error); + } + + Py_INCREF(Py_None); + return Py_None; +} +#endif + static void -font_dealloc(FontObject* self) -{ +font_dealloc(FontObject *self) { if (self->face) { FT_Done_Face(self->face); } @@ -852,128 +1225,115 @@ font_dealloc(FontObject* self) } static PyMethodDef font_methods[] = { - {"render", (PyCFunction) font_render, METH_VARARGS}, - {"getsize", (PyCFunction) font_getsize, METH_VARARGS}, - {"getabc", (PyCFunction) font_getabc, METH_VARARGS}, - {NULL, NULL} -}; + {"render", (PyCFunction)font_render, METH_VARARGS}, + {"getsize", (PyCFunction)font_getsize, METH_VARARGS}, + {"getlength", (PyCFunction)font_getlength, METH_VARARGS}, +#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ + (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) + {"getvarnames", (PyCFunction)font_getvarnames, METH_NOARGS}, + {"getvaraxes", (PyCFunction)font_getvaraxes, METH_NOARGS}, + {"setvarname", (PyCFunction)font_setvarname, METH_VARARGS}, + {"setvaraxes", (PyCFunction)font_setvaraxes, METH_VARARGS}, +#endif + {NULL, NULL}}; -static PyObject* -font_getattr_family(FontObject* self, void* closure) -{ -#if PY_VERSION_HEX >= 0x03000000 - if (self->face->family_name) +static PyObject * +font_getattr_family(FontObject *self, void *closure) { + if (self->face->family_name) { return PyUnicode_FromString(self->face->family_name); -#else - if (self->face->family_name) - return PyString_FromString(self->face->family_name); -#endif + } Py_RETURN_NONE; } -static PyObject* -font_getattr_style(FontObject* self, void* closure) -{ -#if PY_VERSION_HEX >= 0x03000000 - if (self->face->style_name) +static PyObject * +font_getattr_style(FontObject *self, void *closure) { + if (self->face->style_name) { return PyUnicode_FromString(self->face->style_name); -#else - if (self->face->style_name) - return PyString_FromString(self->face->style_name); -#endif + } Py_RETURN_NONE; } -static PyObject* -font_getattr_ascent(FontObject* self, void* closure) -{ - return PyInt_FromLong(PIXEL(self->face->size->metrics.ascender)); +static PyObject * +font_getattr_ascent(FontObject *self, void *closure) { + return PyLong_FromLong(PIXEL(self->face->size->metrics.ascender)); } -static PyObject* -font_getattr_descent(FontObject* self, void* closure) -{ - return PyInt_FromLong(-PIXEL(self->face->size->metrics.descender)); +static PyObject * +font_getattr_descent(FontObject *self, void *closure) { + return PyLong_FromLong(-PIXEL(self->face->size->metrics.descender)); } -static PyObject* -font_getattr_height(FontObject* self, void* closure) -{ - return PyInt_FromLong(PIXEL(self->face->size->metrics.height)); +static PyObject * +font_getattr_height(FontObject *self, void *closure) { + return PyLong_FromLong(PIXEL(self->face->size->metrics.height)); } -static PyObject* -font_getattr_x_ppem(FontObject* self, void* closure) -{ - return PyInt_FromLong(self->face->size->metrics.x_ppem); +static PyObject * +font_getattr_x_ppem(FontObject *self, void *closure) { + return PyLong_FromLong(self->face->size->metrics.x_ppem); } -static PyObject* -font_getattr_y_ppem(FontObject* self, void* closure) -{ - return PyInt_FromLong(self->face->size->metrics.y_ppem); +static PyObject * +font_getattr_y_ppem(FontObject *self, void *closure) { + return PyLong_FromLong(self->face->size->metrics.y_ppem); } - -static PyObject* -font_getattr_glyphs(FontObject* self, void* closure) -{ - return PyInt_FromLong(self->face->num_glyphs); +static PyObject * +font_getattr_glyphs(FontObject *self, void *closure) { + return PyLong_FromLong(self->face->num_glyphs); } static struct PyGetSetDef font_getsetters[] = { - { "family", (getter) font_getattr_family }, - { "style", (getter) font_getattr_style }, - { "ascent", (getter) font_getattr_ascent }, - { "descent", (getter) font_getattr_descent }, - { "height", (getter) font_getattr_height }, - { "x_ppem", (getter) font_getattr_x_ppem }, - { "y_ppem", (getter) font_getattr_y_ppem }, - { "glyphs", (getter) font_getattr_glyphs }, - { NULL } -}; + {"family", (getter)font_getattr_family}, + {"style", (getter)font_getattr_style}, + {"ascent", (getter)font_getattr_ascent}, + {"descent", (getter)font_getattr_descent}, + {"height", (getter)font_getattr_height}, + {"x_ppem", (getter)font_getattr_x_ppem}, + {"y_ppem", (getter)font_getattr_y_ppem}, + {"glyphs", (getter)font_getattr_glyphs}, + {NULL}}; static PyTypeObject Font_Type = { - PyVarObject_HEAD_INIT(NULL, 0) - "Font", sizeof(FontObject), 0, + PyVarObject_HEAD_INIT(NULL, 0) "Font", + sizeof(FontObject), + 0, /* methods */ (destructor)font_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - font_methods, /*tp_methods*/ - 0, /*tp_members*/ - font_getsetters, /*tp_getset*/ + 0, /* tp_print */ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + font_methods, /*tp_methods*/ + 0, /*tp_members*/ + font_getsetters, /*tp_getset*/ }; static PyMethodDef _functions[] = { - {"getfont", (PyCFunction) getfont, METH_VARARGS|METH_KEYWORDS}, - {NULL, NULL} -}; + {"getfont", (PyCFunction)getfont, METH_VARARGS | METH_KEYWORDS}, {NULL, NULL}}; static int -setup_module(PyObject* m) { - PyObject* d; - PyObject* v; +setup_module(PyObject *m) { + PyObject *d; + PyObject *v; int major, minor, patch; d = PyModule_GetDict(m); @@ -981,52 +1341,82 @@ setup_module(PyObject* m) { /* Ready object type */ PyType_Ready(&Font_Type); - if (FT_Init_FreeType(&library)) + if (FT_Init_FreeType(&library)) { return 0; /* leave it uninitialized */ + } FT_Library_Version(library, &major, &minor, &patch); -#if PY_VERSION_HEX >= 0x03000000 v = PyUnicode_FromFormat("%d.%d.%d", major, minor, patch); -#else - v = PyString_FromFormat("%d.%d.%d", major, minor, patch); -#endif PyDict_SetItemString(d, "freetype2_version", v); +#ifdef HAVE_RAQM +#if defined(HAVE_RAQM_SYSTEM) || defined(HAVE_FRIBIDI_SYSTEM) + have_raqm = 1; +#else + load_fribidi(); + have_raqm = !!p_fribidi; +#endif +#else + have_raqm = 0; +#endif - setraqm(); - v = PyBool_FromLong(!!p_raqm.raqm); + /* if we have Raqm, we have all three (but possibly no version info) */ + v = PyBool_FromLong(have_raqm); PyDict_SetItemString(d, "HAVE_RAQM", v); + PyDict_SetItemString(d, "HAVE_FRIBIDI", v); + PyDict_SetItemString(d, "HAVE_HARFBUZZ", v); + if (have_raqm) { +#ifdef RAQM_VERSION_MAJOR + v = PyUnicode_FromString(raqm_version_string()); +#else + v = Py_None; +#endif + PyDict_SetItemString(d, "raqm_version", v); + +#ifdef FRIBIDI_MAJOR_VERSION + { + const char *a = strchr(fribidi_version_info, ')'); + const char *b = strchr(fribidi_version_info, '\n'); + if (a && b && a + 2 < b) { + v = PyUnicode_FromStringAndSize(a + 2, b - (a + 2)); + } else { + v = Py_None; + } + } +#else + v = Py_None; +#endif + PyDict_SetItemString(d, "fribidi_version", v); + +#ifdef HB_VERSION_STRING + v = PyUnicode_FromString(hb_version_string()); +#else + v = Py_None; +#endif + PyDict_SetItemString(d, "harfbuzz_version", v); + } return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imagingft(void) { - PyObject* m; + PyObject *m; static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingft", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - _functions, /* m_methods */ + "_imagingft", /* m_name */ + NULL, /* m_doc */ + -1, /* m_size */ + _functions, /* m_methods */ }; m = PyModule_Create(&module_def); - if (setup_module(m) < 0) + if (setup_module(m) < 0) { return NULL; + } return m; } -#else -PyMODINIT_FUNC -init_imagingft(void) -{ - PyObject* m = Py_InitModule("_imagingft", _functions); - setup_module(m); -} -#endif - diff --git a/src/_imagingmath.c b/src/_imagingmath.c index ea9f103c683..067c165b248 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -15,8 +15,7 @@ #include "Python.h" -#include "Imaging.h" -#include "py3.h" +#include "libImaging/Imaging.h" #include "math.h" #include "float.h" @@ -24,50 +23,51 @@ #define MAX_INT32 2147483647.0 #define MIN_INT32 -2147483648.0 -#define UNOP(name, op, type)\ -void name(Imaging out, Imaging im1)\ -{\ - int x, y;\ - for (y = 0; y < out->ysize; y++) {\ - type* p0 = (type*) out->image[y];\ - type* p1 = (type*) im1->image[y];\ - for (x = 0; x < out->xsize; x++) {\ - *p0 = op(type, *p1);\ - p0++; p1++;\ - }\ - }\ -} - -#define BINOP(name, op, type)\ -void name(Imaging out, Imaging im1, Imaging im2)\ -{\ - int x, y;\ - for (y = 0; y < out->ysize; y++) {\ - type* p0 = (type*) out->image[y];\ - type* p1 = (type*) im1->image[y];\ - type* p2 = (type*) im2->image[y];\ - for (x = 0; x < out->xsize; x++) {\ - *p0 = op(type, *p1, *p2);\ - p0++; p1++; p2++;\ - }\ - }\ -} +#define UNOP(name, op, type) \ + void name(Imaging out, Imaging im1) { \ + int x, y; \ + for (y = 0; y < out->ysize; y++) { \ + type *p0 = (type *)out->image[y]; \ + type *p1 = (type *)im1->image[y]; \ + for (x = 0; x < out->xsize; x++) { \ + *p0 = op(type, *p1); \ + p0++; \ + p1++; \ + } \ + } \ + } + +#define BINOP(name, op, type) \ + void name(Imaging out, Imaging im1, Imaging im2) { \ + int x, y; \ + for (y = 0; y < out->ysize; y++) { \ + type *p0 = (type *)out->image[y]; \ + type *p1 = (type *)im1->image[y]; \ + type *p2 = (type *)im2->image[y]; \ + for (x = 0; x < out->xsize; x++) { \ + *p0 = op(type, *p1, *p2); \ + p0++; \ + p1++; \ + p2++; \ + } \ + } \ + } #define NEG(type, v1) -(v1) #define INVERT(type, v1) ~(v1) -#define ADD(type, v1, v2) (v1)+(v2) -#define SUB(type, v1, v2) (v1)-(v2) -#define MUL(type, v1, v2) (v1)*(v2) +#define ADD(type, v1, v2) (v1) + (v2) +#define SUB(type, v1, v2) (v1) - (v2) +#define MUL(type, v1, v2) (v1) * (v2) -#define MIN(type, v1, v2) ((v1)<(v2))?(v1):(v2) -#define MAX(type, v1, v2) ((v1)>(v2))?(v1):(v2) +#define MIN(type, v1, v2) ((v1) < (v2)) ? (v1) : (v2) +#define MAX(type, v1, v2) ((v1) > (v2)) ? (v1) : (v2) -#define AND(type, v1, v2) (v1)&(v2) -#define OR(type, v1, v2) (v1)|(v2) -#define XOR(type, v1, v2) (v1)^(v2) -#define LSHIFT(type, v1, v2) (v1)<<(v2) -#define RSHIFT(type, v1, v2) (v1)>>(v2) +#define AND(type, v1, v2) (v1) & (v2) +#define OR(type, v1, v2) (v1) | (v2) +#define XOR(type, v1, v2) (v1) ^ (v2) +#define LSHIFT(type, v1, v2) (v1) << (v2) +#define RSHIFT(type, v1, v2) (v1) >> (v2) #define ABS_I(type, v1) abs((v1)) #define ABS_F(type, v1) fabs((v1)) @@ -80,36 +80,38 @@ void name(Imaging out, Imaging im1, Imaging im2)\ * PyFPE_END_PROTECT(result) */ -#define DIV_I(type, v1, v2) ((v2)!=0)?(v1)/(v2):0 -#define DIV_F(type, v1, v2) ((v2)!=0.0F)?(v1)/(v2):0.0F +#define DIV_I(type, v1, v2) ((v2) != 0) ? (v1) / (v2) : 0 +#define DIV_F(type, v1, v2) ((v2) != 0.0F) ? (v1) / (v2) : 0.0F -#define MOD_I(type, v1, v2) ((v2)!=0)?(v1)%(v2):0 -#define MOD_F(type, v1, v2) ((v2)!=0.0F)?fmod((v1),(v2)):0.0F +#define MOD_I(type, v1, v2) ((v2) != 0) ? (v1) % (v2) : 0 +#define MOD_F(type, v1, v2) ((v2) != 0.0F) ? fmod((v1), (v2)) : 0.0F -static int powi(int x, int y) -{ +static int +powi(int x, int y) { double v = pow(x, y) + 0.5; - if (errno == EDOM) + if (errno == EDOM) { return 0; - if (v < MIN_INT32) + } + if (v < MIN_INT32) { v = MIN_INT32; - else if (v > MAX_INT32) + } else if (v > MAX_INT32) { v = MAX_INT32; - return (int) v; + } + return (int)v; } #define POW_I(type, v1, v2) powi(v1, v2) #define POW_F(type, v1, v2) powf(v1, v2) /* FIXME: EDOM handling */ -#define DIFF_I(type, v1, v2) abs((v1)-(v2)) -#define DIFF_F(type, v1, v2) fabs((v1)-(v2)) +#define DIFF_I(type, v1, v2) abs((v1) - (v2)) +#define DIFF_F(type, v1, v2) fabs((v1) - (v2)) -#define EQ(type, v1, v2) (v1)==(v2) -#define NE(type, v1, v2) (v1)!=(v2) -#define LT(type, v1, v2) (v1)<(v2) -#define LE(type, v1, v2) (v1)<=(v2) -#define GT(type, v1, v2) (v1)>(v2) -#define GE(type, v1, v2) (v1)>=(v2) +#define EQ(type, v1, v2) (v1) == (v2) +#define NE(type, v1, v2) (v1) != (v2) +#define LT(type, v1, v2) (v1) < (v2) +#define LE(type, v1, v2) (v1) <= (v2) +#define GT(type, v1, v2) (v1) > (v2) +#define GE(type, v1, v2) (v1) >= (v2) UNOP(abs_I, ABS_I, INT32) UNOP(neg_I, NEG, INT32) @@ -161,20 +163,20 @@ BINOP(gt_F, GT, FLOAT32) BINOP(ge_F, GE, FLOAT32) static PyObject * -_unop(PyObject* self, PyObject* args) -{ +_unop(PyObject *self, PyObject *args) { Imaging out; Imaging im1; void (*unop)(Imaging, Imaging); Py_ssize_t op, i0, i1; - if (!PyArg_ParseTuple(args, "nnn", &op, &i0, &i1)) + if (!PyArg_ParseTuple(args, "nnn", &op, &i0, &i1)) { return NULL; + } - out = (Imaging) i0; - im1 = (Imaging) i1; + out = (Imaging)i0; + im1 = (Imaging)i1; - unop = (void*) op; + unop = (void *)op; unop(out, im1); @@ -183,22 +185,22 @@ _unop(PyObject* self, PyObject* args) } static PyObject * -_binop(PyObject* self, PyObject* args) -{ +_binop(PyObject *self, PyObject *args) { Imaging out; Imaging im1; Imaging im2; void (*binop)(Imaging, Imaging, Imaging); Py_ssize_t op, i0, i1, i2; - if (!PyArg_ParseTuple(args, "nnnn", &op, &i0, &i1, &i2)) + if (!PyArg_ParseTuple(args, "nnnn", &op, &i0, &i1, &i2)) { return NULL; + } - out = (Imaging) i0; - im1 = (Imaging) i1; - im2 = (Imaging) i2; + out = (Imaging)i0; + im1 = (Imaging)i1; + im2 = (Imaging)i2; - binop = (void*) op; + binop = (void *)op; binop(out, im1, im2); @@ -207,23 +209,20 @@ _binop(PyObject* self, PyObject* args) } static PyMethodDef _functions[] = { - {"unop", _unop, 1}, - {"binop", _binop, 1}, - {NULL, NULL} -}; + {"unop", _unop, 1}, {"binop", _binop, 1}, {NULL, NULL}}; static void -install(PyObject *d, char* name, void* value) -{ - PyObject *v = PyInt_FromSsize_t((Py_ssize_t) value); - if (!v || PyDict_SetItemString(d, name, v)) +install(PyObject *d, char *name, void *value) { + PyObject *v = PyLong_FromSsize_t((Py_ssize_t)value); + if (!v || PyDict_SetItemString(d, name, v)) { PyErr_Clear(); + } Py_XDECREF(v); } static int -setup_module(PyObject* m) { - PyObject* d = PyModule_GetDict(m); +setup_module(PyObject *m) { + PyObject *d = PyModule_GetDict(m); install(d, "abs_I", abs_I); install(d, "neg_I", neg_I); @@ -273,32 +272,23 @@ setup_module(PyObject* m) { return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imagingmath(void) { - PyObject* m; + PyObject *m; static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingmath", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - _functions, /* m_methods */ + "_imagingmath", /* m_name */ + NULL, /* m_doc */ + -1, /* m_size */ + _functions, /* m_methods */ }; m = PyModule_Create(&module_def); - if (setup_module(m) < 0) + if (setup_module(m) < 0) { return NULL; + } return m; } -#else -PyMODINIT_FUNC -init_imagingmath(void) -{ - PyObject* m = Py_InitModule("_imagingmath", _functions); - setup_module(m); -} -#endif - diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 2b5e7bc5d4d..c0644b61609 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -12,10 +12,9 @@ */ #include "Python.h" -#include "Imaging.h" -#include "py3.h" +#include "libImaging/Imaging.h" -#define LUT_SIZE (1<<9) +#define LUT_SIZE (1 << 9) /* Apply a morphologic LUT to a binary image. Outputs a a new binary image. @@ -28,9 +27,8 @@ Returns number of changed pixels. */ -static PyObject* -apply(PyObject *self, PyObject* args) -{ +static PyObject * +apply(PyObject *self, PyObject *args) { const char *lut; PyObject *py_lut; Py_ssize_t lut_len, i0, i1; @@ -59,18 +57,16 @@ apply(PyObject *self, PyObject* args) lut = PyBytes_AsString(py_lut); - imgin = (Imaging) i0; - imgout = (Imaging) i1; + imgin = (Imaging)i0; + imgout = (Imaging)i1; width = imgin->xsize; height = imgin->ysize; - if (imgin->type != IMAGING_TYPE_UINT8 && - imgin->bands != 1) { + if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; } - if (imgout->type != IMAGING_TYPE_UINT8 && - imgout->bands != 1) { + if (imgout->type != IMAGING_TYPE_UINT8 || imgout->bands != 1) { PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; } @@ -78,55 +74,50 @@ apply(PyObject *self, PyObject* args) inrows = imgin->image8; outrows = imgout->image8; - for (row_idx=0; row_idx < height; row_idx++) { + for (row_idx = 0; row_idx < height; row_idx++) { UINT8 *outrow = outrows[row_idx]; UINT8 *inrow = inrows[row_idx]; UINT8 *prow, *nrow; /* Previous and next row */ /* zero boundary conditions. TBD support other modes */ - outrow[0] = outrow[width-1] = 0; - if (row_idx==0 || row_idx == height-1) { - for(col_idx=0; col_idxtype != IMAGING_TYPE_UINT8 && - imgin->bands != 1) { + if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; } @@ -177,39 +166,33 @@ match(PyObject *self, PyObject* args) width = imgin->xsize; height = imgin->ysize; - for (row_idx=1; row_idx < height-1; row_idx++) { + for (row_idx = 1; row_idx < height - 1; row_idx++) { UINT8 *inrow = inrows[row_idx]; UINT8 *prow, *nrow; - prow = inrows[row_idx-1]; - nrow = inrows[row_idx+1]; - - for (col_idx=1; col_idximage8; width = img->xsize; height = img->ysize; - for (row_idx=0; row_idx < height; row_idx++) { + for (row_idx = 0; row_idx < height; row_idx++) { UINT8 *row = rows[row_idx]; - for (col_idx=0; col_idx= 0x03000000 PyMODINIT_FUNC PyInit__imagingmorph(void) { - PyObject* m; + PyObject *m; static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingmorph", /* m_name */ - "A module for doing image morphology", /* m_doc */ - -1, /* m_size */ - functions, /* m_methods */ + "_imagingmorph", /* m_name */ + "A module for doing image morphology", /* m_doc */ + -1, /* m_size */ + functions, /* m_methods */ }; m = PyModule_Create(&module_def); - if (setup_module(m) < 0) + if (setup_module(m) < 0) { return NULL; + } return m; } -#else -PyMODINIT_FUNC -init_imagingmorph(void) -{ - PyObject* m = Py_InitModule("_imagingmorph", functions); - setup_module(m); -} -#endif - diff --git a/src/_imagingtk.c b/src/_imagingtk.c index d0295f317f9..b9273b0b882 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -4,53 +4,36 @@ * tkinter hooks * * history: - * 99-07-26 fl created - * 99-08-15 fl moved to its own support module + * 99-07-26 fl created + * 99-08-15 fl moved to its own support module * * Copyright (c) Secret Labs AB 1999. * * See the README file for information on usage and redistribution. */ - #include "Python.h" -#include "Imaging.h" +#include "libImaging/Imaging.h" -#include "_tkmini.h" +#include "Tk/_tkmini.h" /* must link with Tk/tkImaging.c */ -extern void TkImaging_Init(Tcl_Interp* interp); -extern int load_tkinter_funcs(void); - -/* copied from _tkinter.c (this isn't as bad as it may seem: for new - versions, we use _tkinter's interpaddr hook instead, and all older - versions use this structure layout) */ +extern void +TkImaging_Init(Tcl_Interp *interp); +extern int +load_tkinter_funcs(void); -typedef struct { - PyObject_HEAD - Tcl_Interp* interp; -} TkappObject; +static PyObject * +_tkinit(PyObject *self, PyObject *args) { + Tcl_Interp *interp; -static PyObject* -_tkinit(PyObject* self, PyObject* args) -{ - Tcl_Interp* interp; - - PyObject* arg; - int is_interp; - if (!PyArg_ParseTuple(args, "Oi", &arg, &is_interp)) + PyObject *arg; + if (!PyArg_ParseTuple(args, "O", &arg)) { return NULL; - - if (is_interp) - interp = (Tcl_Interp*)PyLong_AsVoidPtr(arg); - else { - TkappObject* app; - /* Do it the hard way. This will break if the TkappObject - layout changes */ - app = (TkappObject*)PyLong_AsVoidPtr(arg); - interp = app->interp; } + interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg); + /* This will bomb if interp is invalid... */ TkImaging_Init(interp); @@ -64,26 +47,16 @@ static PyMethodDef functions[] = { {NULL, NULL} /* sentinel */ }; -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__imagingtk(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingtk", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - functions, /* m_methods */ + "_imagingtk", /* m_name */ + NULL, /* m_doc */ + -1, /* m_size */ + functions, /* m_methods */ }; PyObject *m; m = PyModule_Create(&module_def); return (load_tkinter_funcs() == 0) ? m : NULL; } -#else -PyMODINIT_FUNC -init_imagingtk(void) -{ - Py_InitModule("_imagingtk", functions); - load_tkinter_funcs(); -} -#endif - diff --git a/src/_webp.c b/src/_webp.c index 67a9e6b667b..fd99116cb41 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -1,7 +1,6 @@ #define PY_SSIZE_T_CLEAN #include -#include "Imaging.h" -#include "py3.h" +#include "libImaging/Imaging.h" #include #include #include @@ -13,8 +12,8 @@ /* * Check the versions from mux.h and demux.h, to ensure the WebPAnimEncoder and * WebPAnimDecoder APIs are present (initial support was added in 0.5.0). The - * very early versions added had some significant differences, so we require - * later versions, before enabling animation support. + * very early versions had some significant differences, so we require later + * versions, before enabling animation support. */ #if WEBP_MUX_ABI_VERSION >= 0x0104 && WEBP_DEMUX_ABI_VERSION >= 0x0105 #define HAVE_WEBPANIM @@ -22,18 +21,31 @@ #endif +void +ImagingSectionEnter(ImagingSectionCookie *cookie) { + *cookie = (PyThreadState *)PyEval_SaveThread(); +} + +void +ImagingSectionLeave(ImagingSectionCookie *cookie) { + PyEval_RestoreThread((PyThreadState *)*cookie); +} + /* -------------------------------------------------------------------- */ /* WebP Muxer Error Handling */ /* -------------------------------------------------------------------- */ #ifdef HAVE_WEBPMUX -static const char* const kErrorMessages[-WEBP_MUX_NOT_ENOUGH_DATA + 1] = { - "WEBP_MUX_NOT_FOUND", "WEBP_MUX_INVALID_ARGUMENT", "WEBP_MUX_BAD_DATA", - "WEBP_MUX_MEMORY_ERROR", "WEBP_MUX_NOT_ENOUGH_DATA" -}; +static const char *const kErrorMessages[-WEBP_MUX_NOT_ENOUGH_DATA + 1] = { + "WEBP_MUX_NOT_FOUND", + "WEBP_MUX_INVALID_ARGUMENT", + "WEBP_MUX_BAD_DATA", + "WEBP_MUX_MEMORY_ERROR", + "WEBP_MUX_NOT_ENOUGH_DATA"}; -PyObject* HandleMuxError(WebPMuxError err, char* chunk) { +PyObject * +HandleMuxError(WebPMuxError err, char *chunk) { char message[100]; int message_len; assert(err <= WEBP_MUX_NOT_FOUND && err >= WEBP_MUX_NOT_ENOUGH_DATA); @@ -45,9 +57,11 @@ PyObject* HandleMuxError(WebPMuxError err, char* chunk) { // Create the error message if (chunk == NULL) { - message_len = sprintf(message, "could not assemble chunks: %s", kErrorMessages[-err]); + message_len = + sprintf(message, "could not assemble chunks: %s", kErrorMessages[-err]); } else { - message_len = sprintf(message, "could not set %.4s chunk: %s", chunk, kErrorMessages[-err]); + message_len = sprintf( + message, "could not set %.4s chunk: %s", chunk, kErrorMessages[-err]); } if (message_len < 0) { PyErr_SetString(PyExc_RuntimeError, "failed to construct error message"); @@ -63,7 +77,7 @@ PyObject* HandleMuxError(WebPMuxError err, char* chunk) { case WEBP_MUX_BAD_DATA: case WEBP_MUX_NOT_ENOUGH_DATA: - PyErr_SetString(PyExc_IOError, message); + PyErr_SetString(PyExc_OSError, message); break; default: @@ -83,8 +97,7 @@ PyObject* HandleMuxError(WebPMuxError err, char* chunk) { // Encoder type typedef struct { - PyObject_HEAD - WebPAnimEncoder* enc; + PyObject_HEAD WebPAnimEncoder *enc; WebPPicture frame; } WebPAnimEncoderObject; @@ -92,18 +105,17 @@ static PyTypeObject WebPAnimEncoder_Type; // Decoder type typedef struct { - PyObject_HEAD - WebPAnimDecoder* dec; + PyObject_HEAD WebPAnimDecoder *dec; WebPAnimInfo info; WebPData data; - char* mode; + char *mode; } WebPAnimDecoderObject; static PyTypeObject WebPAnimDecoder_Type; // Encoder functions -PyObject* _anim_encoder_new(PyObject* self, PyObject* args) -{ +PyObject * +_anim_encoder_new(PyObject *self, PyObject *args) { int width, height; uint32_t bgcolor; int loop_count; @@ -112,12 +124,21 @@ PyObject* _anim_encoder_new(PyObject* self, PyObject* args) int allow_mixed; int verbose; WebPAnimEncoderOptions enc_options; - WebPAnimEncoderObject* encp = NULL; - WebPAnimEncoder* enc = NULL; - - if (!PyArg_ParseTuple(args, "iiIiiiiii", - &width, &height, &bgcolor, &loop_count, &minimize_size, - &kmin, &kmax, &allow_mixed, &verbose)) { + WebPAnimEncoderObject *encp = NULL; + WebPAnimEncoder *enc = NULL; + + if (!PyArg_ParseTuple( + args, + "iiIiiiiii", + &width, + &height, + &bgcolor, + &loop_count, + &minimize_size, + &kmin, + &kmax, + &allow_mixed, + &verbose)) { return NULL; } @@ -147,7 +168,7 @@ PyObject* _anim_encoder_new(PyObject* self, PyObject* args) enc = WebPAnimEncoderNew(width, height, &enc_options); if (enc) { encp->enc = enc; - return (PyObject*) encp; + return (PyObject *)encp; } WebPPictureFree(&(encp->frame)); } @@ -157,33 +178,42 @@ PyObject* _anim_encoder_new(PyObject* self, PyObject* args) return NULL; } -PyObject* _anim_encoder_dealloc(PyObject* self) -{ - WebPAnimEncoderObject* encp = (WebPAnimEncoderObject*)self; +PyObject * +_anim_encoder_dealloc(PyObject *self) { + WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; WebPPictureFree(&(encp->frame)); WebPAnimEncoderDelete(encp->enc); Py_RETURN_NONE; } -PyObject* _anim_encoder_add(PyObject* self, PyObject* args) -{ - uint8_t* rgb; +PyObject * +_anim_encoder_add(PyObject *self, PyObject *args) { + uint8_t *rgb; Py_ssize_t size; int timestamp; int width; int height; - char* mode; + char *mode; int lossless; float quality_factor; int method; WebPConfig config; - WebPAnimEncoderObject* encp = (WebPAnimEncoderObject*)self; - WebPAnimEncoder* enc = encp->enc; - WebPPicture* frame = &(encp->frame); - - if (!PyArg_ParseTuple(args, "z#iiisifi", - (char**)&rgb, &size, ×tamp, &width, &height, &mode, - &lossless, &quality_factor, &method)) { + WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; + WebPAnimEncoder *enc = encp->enc; + WebPPicture *frame = &(encp->frame); + + if (!PyArg_ParseTuple( + args, + "z#iiisifi", + (char **)&rgb, + &size, + ×tamp, + &width, + &height, + &mode, + &lossless, + &quality_factor, + &method)) { return NULL; } @@ -211,11 +241,13 @@ PyObject* _anim_encoder_add(PyObject* self, PyObject* args) // Populate the frame with raw bytes passed to us frame->width = width; frame->height = height; - frame->use_argb = 1; // Don't convert RGB pixels to YUV - if (strcmp(mode, "RGBA")==0) { + frame->use_argb = 1; // Don't convert RGB pixels to YUV + if (strcmp(mode, "RGBA") == 0) { WebPPictureImportRGBA(frame, rgb, 4 * width); - } else if (strcmp(mode, "RGBX")==0) { + } else if (strcmp(mode, "RGBX") == 0) { WebPPictureImportRGBX(frame, rgb, 4 * width); + } else { + WebPPictureImportRGB(frame, rgb, 3 * width); } // Add the frame to the encoder @@ -227,22 +259,29 @@ PyObject* _anim_encoder_add(PyObject* self, PyObject* args) Py_RETURN_NONE; } -PyObject* _anim_encoder_assemble(PyObject* self, PyObject* args) -{ - uint8_t* icc_bytes; - uint8_t* exif_bytes; - uint8_t* xmp_bytes; +PyObject * +_anim_encoder_assemble(PyObject *self, PyObject *args) { + uint8_t *icc_bytes; + uint8_t *exif_bytes; + uint8_t *xmp_bytes; Py_ssize_t icc_size; Py_ssize_t exif_size; Py_ssize_t xmp_size; WebPData webp_data; - WebPAnimEncoderObject* encp = (WebPAnimEncoderObject*)self; - WebPAnimEncoder* enc = encp->enc; - WebPMux* mux = NULL; - PyObject* ret = NULL; - - if (!PyArg_ParseTuple(args, "s#s#s#", - &icc_bytes, &icc_size, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) { + WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; + WebPAnimEncoder *enc = encp->enc; + WebPMux *mux = NULL; + PyObject *ret = NULL; + + if (!PyArg_ParseTuple( + args, + "s#s#s#", + &icc_bytes, + &icc_size, + &exif_bytes, + &exif_size, + &xmp_bytes, + &xmp_size)) { return NULL; } @@ -261,9 +300,9 @@ PyObject* _anim_encoder_assemble(PyObject* self, PyObject* args) int i_icc_size = (int)icc_size; int i_exif_size = (int)exif_size; int i_xmp_size = (int)xmp_size; - WebPData icc_profile = { icc_bytes, i_icc_size }; - WebPData exif = { exif_bytes, i_exif_size }; - WebPData xmp = { xmp_bytes, i_xmp_size }; + WebPData icc_profile = {icc_bytes, i_icc_size}; + WebPData exif = {exif_bytes, i_exif_size}; + WebPData xmp = {xmp_bytes, i_xmp_size}; mux = WebPMuxCreate(&webp_data, 1); if (mux == NULL) { @@ -303,7 +342,7 @@ PyObject* _anim_encoder_assemble(PyObject* self, PyObject* args) } // Convert to Python bytes - ret = PyBytes_FromStringAndSize((char*)webp_data.bytes, webp_data.size); + ret = PyBytes_FromStringAndSize((char *)webp_data.bytes, webp_data.size); WebPDataClear(&webp_data); // If we had to re-mux, we should free it now that we're done with it @@ -315,21 +354,21 @@ PyObject* _anim_encoder_assemble(PyObject* self, PyObject* args) } // Decoder functions -PyObject* _anim_decoder_new(PyObject* self, PyObject* args) -{ +PyObject * +_anim_decoder_new(PyObject *self, PyObject *args) { PyBytesObject *webp_string; const uint8_t *webp; Py_ssize_t size; WebPData webp_src; - char* mode; + char *mode; WebPDecoderConfig config; - WebPAnimDecoderObject* decp = NULL; - WebPAnimDecoder* dec = NULL; + WebPAnimDecoderObject *decp = NULL; + WebPAnimDecoder *dec = NULL; if (!PyArg_ParseTuple(args, "S", &webp_string)) { return NULL; } - PyBytes_AsStringAndSize((PyObject *)webp_string, (char**)&webp, &size); + PyBytes_AsStringAndSize((PyObject *)webp_string, (char **)&webp, &size); webp_src.bytes = webp; webp_src.size = size; @@ -350,43 +389,45 @@ PyObject* _anim_decoder_new(PyObject* self, PyObject* args) if (dec) { if (WebPAnimDecoderGetInfo(dec, &(decp->info))) { decp->dec = dec; - return (PyObject*)decp; + return (PyObject *)decp; } } + WebPDataClear(&(decp->data)); } PyObject_Del(decp); } - PyErr_SetString(PyExc_RuntimeError, "could not create decoder object"); + PyErr_SetString(PyExc_OSError, "could not create decoder object"); return NULL; } -PyObject* _anim_decoder_dealloc(PyObject* self) -{ - WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self; +PyObject * +_anim_decoder_dealloc(PyObject *self) { + WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; WebPDataClear(&(decp->data)); WebPAnimDecoderDelete(decp->dec); Py_RETURN_NONE; } -PyObject* _anim_decoder_get_info(PyObject* self, PyObject* args) -{ - WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self; - WebPAnimInfo* info = &(decp->info); +PyObject * +_anim_decoder_get_info(PyObject *self) { + WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; + WebPAnimInfo *info = &(decp->info); - return Py_BuildValue("IIIIIs", - info->canvas_width, info->canvas_height, + return Py_BuildValue( + "IIIIIs", + info->canvas_width, + info->canvas_height, info->loop_count, info->bgcolor, info->frame_count, - decp->mode - ); + decp->mode); } -PyObject* _anim_decoder_get_chunk(PyObject* self, PyObject* args) -{ - char* mode; - WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self; - const WebPDemuxer* demux; +PyObject * +_anim_decoder_get_chunk(PyObject *self, PyObject *args) { + char *mode; + WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; + const WebPDemuxer *demux; WebPChunkIterator iter; PyObject *ret; @@ -399,38 +440,37 @@ PyObject* _anim_decoder_get_chunk(PyObject* self, PyObject* args) Py_RETURN_NONE; } - ret = PyBytes_FromStringAndSize((const char*)iter.chunk.bytes, iter.chunk.size); + ret = PyBytes_FromStringAndSize((const char *)iter.chunk.bytes, iter.chunk.size); WebPDemuxReleaseChunkIterator(&iter); return ret; } -PyObject* _anim_decoder_get_next(PyObject* self, PyObject* args) -{ - uint8_t* buf; +PyObject * +_anim_decoder_get_next(PyObject *self) { + uint8_t *buf; int timestamp; - PyObject* bytes; - WebPAnimDecoderObject* decp = (WebPAnimDecoderObject*)self; + PyObject *bytes; + PyObject *ret; + WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; if (!WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp)) { - PyErr_SetString(PyExc_IOError, "failed to read next frame"); + PyErr_SetString(PyExc_OSError, "failed to read next frame"); return NULL; } - bytes = PyBytes_FromStringAndSize((char *)buf, - decp->info.canvas_width * 4 * decp->info.canvas_height); - return Py_BuildValue("Si", bytes, timestamp); -} + bytes = PyBytes_FromStringAndSize( + (char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height); + + ret = Py_BuildValue("Si", bytes, timestamp); -PyObject* _anim_decoder_has_more_frames(PyObject* self, PyObject* args) -{ - WebPAnimDecoderObject* decp = (WebPAnimDecoderObject*)self; - return Py_BuildValue("i", WebPAnimDecoderHasMoreFrames(decp->dec)); + Py_DECREF(bytes); + return ret; } -PyObject* _anim_decoder_reset(PyObject* self, PyObject* args) -{ - WebPAnimDecoderObject* decp = (WebPAnimDecoderObject *)self; +PyObject * +_anim_decoder_reset(PyObject *self) { + WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; WebPAnimDecoderReset(decp->dec); Py_RETURN_NONE; } @@ -446,84 +486,81 @@ static struct PyMethodDef _anim_encoder_methods[] = { {NULL, NULL} /* sentinel */ }; -// WebPAnimDecoder type definition +// WebPAnimEncoder type definition static PyTypeObject WebPAnimEncoder_Type = { - PyVarObject_HEAD_INIT(NULL, 0) - "WebPAnimEncoder", /*tp_name */ - sizeof(WebPAnimEncoderObject), /*tp_size */ - 0, /*tp_itemsize */ + PyVarObject_HEAD_INIT(NULL, 0) "WebPAnimEncoder", /*tp_name */ + sizeof(WebPAnimEncoderObject), /*tp_size */ + 0, /*tp_itemsize */ /* methods */ (destructor)_anim_encoder_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _anim_encoder_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + _anim_encoder_methods, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ }; // WebPAnimDecoder methods static struct PyMethodDef _anim_decoder_methods[] = { - {"get_info", (PyCFunction)_anim_decoder_get_info, METH_VARARGS, "get_info"}, + {"get_info", (PyCFunction)_anim_decoder_get_info, METH_NOARGS, "get_info"}, {"get_chunk", (PyCFunction)_anim_decoder_get_chunk, METH_VARARGS, "get_chunk"}, - {"get_next", (PyCFunction)_anim_decoder_get_next, METH_VARARGS, "get_next"}, - {"has_more_frames", (PyCFunction)_anim_decoder_has_more_frames, METH_VARARGS, "has_more_frames"}, - {"reset", (PyCFunction)_anim_decoder_reset, METH_VARARGS, "reset"}, + {"get_next", (PyCFunction)_anim_decoder_get_next, METH_NOARGS, "get_next"}, + {"reset", (PyCFunction)_anim_decoder_reset, METH_NOARGS, "reset"}, {NULL, NULL} /* sentinel */ }; // WebPAnimDecoder type definition static PyTypeObject WebPAnimDecoder_Type = { - PyVarObject_HEAD_INIT(NULL, 0) - "WebPAnimDecoder", /*tp_name */ - sizeof(WebPAnimDecoderObject), /*tp_size */ - 0, /*tp_itemsize */ + PyVarObject_HEAD_INIT(NULL, 0) "WebPAnimDecoder", /*tp_name */ + sizeof(WebPAnimDecoderObject), /*tp_size */ + 0, /*tp_itemsize */ /* methods */ (destructor)_anim_decoder_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _anim_decoder_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + _anim_decoder_methods, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ }; #endif @@ -532,151 +569,203 @@ static PyTypeObject WebPAnimDecoder_Type = { /* Legacy WebP Support */ /* -------------------------------------------------------------------- */ -PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) -{ +PyObject * +WebPEncode_wrapper(PyObject *self, PyObject *args) { int width; int height; int lossless; float quality_factor; - uint8_t* rgb; - uint8_t* icc_bytes; - uint8_t* exif_bytes; - uint8_t* xmp_bytes; - uint8_t* output; - char* mode; + int method; + uint8_t *rgb; + uint8_t *icc_bytes; + uint8_t *exif_bytes; + uint8_t *xmp_bytes; + uint8_t *output; + char *mode; Py_ssize_t size; Py_ssize_t icc_size; Py_ssize_t exif_size; Py_ssize_t xmp_size; size_t ret_size; + int rgba_mode; + int channels; + int ok; + ImagingSectionCookie cookie; + WebPConfig config; + WebPMemoryWriter writer; + WebPPicture pic; + + if (!PyArg_ParseTuple( + args, + "y#iiifss#is#s#", + (char **)&rgb, + &size, + &width, + &height, + &lossless, + &quality_factor, + &mode, + &icc_bytes, + &icc_size, + &method, + &exif_bytes, + &exif_size, + &xmp_bytes, + &xmp_size)) { + return NULL; + } + + rgba_mode = strcmp(mode, "RGBA") == 0; + if (!rgba_mode && strcmp(mode, "RGB") != 0) { + Py_RETURN_NONE; + } + + channels = rgba_mode ? 4 : 3; + if (size < width * height * channels) { + Py_RETURN_NONE; + } - if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH"iiifss#s#s#", - (char**)&rgb, &size, &width, &height, &lossless, &quality_factor, &mode, - &icc_bytes, &icc_size, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) { + // Setup config for this frame + if (!WebPConfigInit(&config)) { + PyErr_SetString(PyExc_RuntimeError, "failed to initialize config!"); return NULL; } - if (strcmp(mode, "RGBA")==0){ - if (size < width * height * 4){ - Py_RETURN_NONE; - } - #if WEBP_ENCODER_ABI_VERSION >= 0x0100 - if (lossless) { - ret_size = WebPEncodeLosslessRGBA(rgb, width, height, 4 * width, &output); - } else - #endif - { - ret_size = WebPEncodeRGBA(rgb, width, height, 4 * width, quality_factor, &output); - } - } else if (strcmp(mode, "RGB")==0){ - if (size < width * height * 3){ - Py_RETURN_NONE; - } - #if WEBP_ENCODER_ABI_VERSION >= 0x0100 - if (lossless) { - ret_size = WebPEncodeLosslessRGB(rgb, width, height, 3 * width, &output); - } else - #endif - { - ret_size = WebPEncodeRGB(rgb, width, height, 3 * width, quality_factor, &output); - } + config.lossless = lossless; + config.quality = quality_factor; + config.method = method; + + // Validate the config + if (!WebPValidateConfig(&config)) { + PyErr_SetString(PyExc_ValueError, "invalid configuration"); + return NULL; + } + + if (!WebPPictureInit(&pic)) { + PyErr_SetString(PyExc_ValueError, "could not initialise picture"); + return NULL; + } + pic.width = width; + pic.height = height; + pic.use_argb = 1; // Don't convert RGB pixels to YUV + + if (rgba_mode) { + WebPPictureImportRGBA(&pic, rgb, channels * width); } else { - Py_RETURN_NONE; + WebPPictureImportRGB(&pic, rgb, channels * width); + } + + WebPMemoryWriterInit(&writer); + pic.writer = WebPMemoryWrite; + pic.custom_ptr = &writer; + + ImagingSectionEnter(&cookie); + ok = WebPEncode(&config, &pic); + ImagingSectionLeave(&cookie); + + WebPPictureFree(&pic); + if (!ok) { + PyErr_Format(PyExc_ValueError, "encoding error %d", (&pic)->error_code); + return NULL; } + output = writer.mem; + ret_size = writer.size; #ifndef HAVE_WEBPMUX if (ret_size > 0) { - PyObject *ret = PyBytes_FromStringAndSize((char*)output, ret_size); + PyObject *ret = PyBytes_FromStringAndSize((char *)output, ret_size); free(output); return ret; } #else { - /* I want to truncate the *_size items that get passed into WebP - data. Pypy2.1.0 had some issues where the Py_ssize_t items had - data in the upper byte. (Not sure why, it shouldn't have been there) - */ - int i_icc_size = (int)icc_size; - int i_exif_size = (int)exif_size; - int i_xmp_size = (int)xmp_size; - WebPData output_data = {0}; - WebPData image = { output, ret_size }; - WebPData icc_profile = { icc_bytes, i_icc_size }; - WebPData exif = { exif_bytes, i_exif_size }; - WebPData xmp = { xmp_bytes, i_xmp_size }; - WebPMuxError err; - int dbg = 0; - - int copy_data = 0; // value 1 indicates given data WILL be copied to the mux - // and value 0 indicates data will NOT be copied. - - WebPMux* mux = WebPMuxNew(); - WebPMuxSetImage(mux, &image, copy_data); - - if (dbg) { - /* was getting %ld icc_size == 0, icc_size>0 was true */ - fprintf(stderr, "icc size %d, %d \n", i_icc_size, i_icc_size > 0); - } - - if (i_icc_size > 0) { + /* I want to truncate the *_size items that get passed into WebP + data. Pypy2.1.0 had some issues where the Py_ssize_t items had + data in the upper byte. (Not sure why, it shouldn't have been there) + */ + int i_icc_size = (int)icc_size; + int i_exif_size = (int)exif_size; + int i_xmp_size = (int)xmp_size; + WebPData output_data = {0}; + WebPData image = {output, ret_size}; + WebPData icc_profile = {icc_bytes, i_icc_size}; + WebPData exif = {exif_bytes, i_exif_size}; + WebPData xmp = {xmp_bytes, i_xmp_size}; + WebPMuxError err; + int dbg = 0; + + int copy_data = 0; // value 1 indicates given data WILL be copied to the mux + // and value 0 indicates data will NOT be copied. + + WebPMux *mux = WebPMuxNew(); + WebPMuxSetImage(mux, &image, copy_data); + if (dbg) { - fprintf(stderr, "Adding ICC Profile\n"); + /* was getting %ld icc_size == 0, icc_size>0 was true */ + fprintf(stderr, "icc size %d, %d \n", i_icc_size, i_icc_size > 0); } - err = WebPMuxSetChunk(mux, "ICCP", &icc_profile, copy_data); - if (err != WEBP_MUX_OK) { - return HandleMuxError(err, "ICCP"); + + if (i_icc_size > 0) { + if (dbg) { + fprintf(stderr, "Adding ICC Profile\n"); + } + err = WebPMuxSetChunk(mux, "ICCP", &icc_profile, copy_data); + if (err != WEBP_MUX_OK) { + return HandleMuxError(err, "ICCP"); + } } - } - if (dbg) { - fprintf(stderr, "exif size %d \n", i_exif_size); - } - if (i_exif_size > 0) { if (dbg) { - fprintf(stderr, "Adding Exif Data\n"); + fprintf(stderr, "exif size %d \n", i_exif_size); } - err = WebPMuxSetChunk(mux, "EXIF", &exif, copy_data); - if (err != WEBP_MUX_OK) { - return HandleMuxError(err, "EXIF"); + if (i_exif_size > 0) { + if (dbg) { + fprintf(stderr, "Adding Exif Data\n"); + } + err = WebPMuxSetChunk(mux, "EXIF", &exif, copy_data); + if (err != WEBP_MUX_OK) { + return HandleMuxError(err, "EXIF"); + } } - } - if (dbg) { - fprintf(stderr, "xmp size %d \n", i_xmp_size); - } - if (i_xmp_size > 0) { - if (dbg){ - fprintf(stderr, "Adding XMP Data\n"); + if (dbg) { + fprintf(stderr, "xmp size %d \n", i_xmp_size); } - err = WebPMuxSetChunk(mux, "XMP ", &xmp, copy_data); - if (err != WEBP_MUX_OK) { - return HandleMuxError(err, "XMP "); + if (i_xmp_size > 0) { + if (dbg) { + fprintf(stderr, "Adding XMP Data\n"); + } + err = WebPMuxSetChunk(mux, "XMP ", &xmp, copy_data); + if (err != WEBP_MUX_OK) { + return HandleMuxError(err, "XMP "); + } } - } - WebPMuxAssemble(mux, &output_data); - WebPMuxDelete(mux); - free(output); + WebPMuxAssemble(mux, &output_data); + WebPMuxDelete(mux); + free(output); - ret_size = output_data.size; - if (ret_size > 0) { - PyObject *ret = PyBytes_FromStringAndSize((char*)output_data.bytes, ret_size); - WebPDataClear(&output_data); - return ret; - } + ret_size = output_data.size; + if (ret_size > 0) { + PyObject *ret = + PyBytes_FromStringAndSize((char *)output_data.bytes, ret_size); + WebPDataClear(&output_data); + return ret; + } } #endif Py_RETURN_NONE; } -PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) -{ - PyBytesObject* webp_string; - const uint8_t* webp; +PyObject * +WebPDecode_wrapper(PyObject *self, PyObject *args) { + PyBytesObject *webp_string; + const uint8_t *webp; Py_ssize_t size; - PyObject *ret = Py_None, *bytes = NULL, *pymode = NULL, *icc_profile = NULL, *exif = NULL; + PyObject *ret = Py_None, *bytes = NULL, *pymode = NULL, *icc_profile = NULL, + *exif = NULL; WebPDecoderConfig config; VP8StatusCode vp8_status_code = VP8_STATUS_OK; - char* mode = "RGB"; + char *mode = "RGB"; if (!PyArg_ParseTuple(args, "S", &webp_string)) { return NULL; @@ -686,7 +775,7 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) Py_RETURN_NONE; } - PyBytes_AsStringAndSize((PyObject*) webp_string, (char**)&webp, &size); + PyBytes_AsStringAndSize((PyObject *)webp_string, (char **)&webp, &size); vp8_status_code = WebPGetFeatures(webp, size, &config.input); if (vp8_status_code == VP8_STATUS_OK) { @@ -700,62 +789,67 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) #ifndef HAVE_WEBPMUX vp8_status_code = WebPDecode(webp, size, &config); #else - { - int copy_data = 0; - WebPData data = { webp, size }; - WebPMuxFrameInfo image; - WebPData icc_profile_data = {0}; - WebPData exif_data = {0}; - - WebPMux* mux = WebPMuxCreate(&data, copy_data); - if (NULL == mux) - goto end; - - if (WEBP_MUX_OK != WebPMuxGetFrame(mux, 1, &image)) { - WebPMuxDelete(mux); - goto end; - } + int copy_data = 0; + WebPData data = {webp, size}; + WebPMuxFrameInfo image; + WebPData icc_profile_data = {0}; + WebPData exif_data = {0}; + + WebPMux *mux = WebPMuxCreate(&data, copy_data); + if (NULL == mux) { + goto end; + } + + if (WEBP_MUX_OK != WebPMuxGetFrame(mux, 1, &image)) { + WebPMuxDelete(mux); + goto end; + } - webp = image.bitstream.bytes; - size = image.bitstream.size; + webp = image.bitstream.bytes; + size = image.bitstream.size; - vp8_status_code = WebPDecode(webp, size, &config); + vp8_status_code = WebPDecode(webp, size, &config); - if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "ICCP", &icc_profile_data)) - icc_profile = PyBytes_FromStringAndSize((const char*)icc_profile_data.bytes, icc_profile_data.size); + if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "ICCP", &icc_profile_data)) { + icc_profile = PyBytes_FromStringAndSize( + (const char *)icc_profile_data.bytes, icc_profile_data.size); + } - if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "EXIF", &exif_data)) - exif = PyBytes_FromStringAndSize((const char*)exif_data.bytes, exif_data.size); + if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "EXIF", &exif_data)) { + exif = PyBytes_FromStringAndSize( + (const char *)exif_data.bytes, exif_data.size); + } - WebPDataClear(&image.bitstream); - WebPMuxDelete(mux); + WebPDataClear(&image.bitstream); + WebPMuxDelete(mux); } #endif } - if (vp8_status_code != VP8_STATUS_OK) + if (vp8_status_code != VP8_STATUS_OK) { goto end; + } if (config.output.colorspace < MODE_YUV) { - bytes = PyBytes_FromStringAndSize((char*)config.output.u.RGBA.rgba, - config.output.u.RGBA.size); + bytes = PyBytes_FromStringAndSize( + (char *)config.output.u.RGBA.rgba, config.output.u.RGBA.size); } else { // Skipping YUV for now. Need Test Images. // UNDONE -- unclear if we'll ever get here if we set mode_rgb* - bytes = PyBytes_FromStringAndSize((char*)config.output.u.YUVA.y, - config.output.u.YUVA.y_size); + bytes = PyBytes_FromStringAndSize( + (char *)config.output.u.YUVA.y, config.output.u.YUVA.y_size); } -#if PY_VERSION_HEX >= 0x03000000 pymode = PyUnicode_FromString(mode); -#else - pymode = PyString_FromString(mode); -#endif - ret = Py_BuildValue("SiiSSS", bytes, config.output.width, - config.output.height, pymode, - NULL == icc_profile ? Py_None : icc_profile, - NULL == exif ? Py_None : exif); + ret = Py_BuildValue( + "SiiSSS", + bytes, + config.output.width, + config.output.height, + pymode, + NULL == icc_profile ? Py_None : icc_profile, + NULL == exif ? Py_None : exif); end: WebPFreeDecBuffer(&config.output); @@ -765,27 +859,45 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) Py_XDECREF(icc_profile); Py_XDECREF(exif); - if (Py_None == ret) + if (Py_None == ret) { Py_RETURN_NONE; + } return ret; } // Return the decoder's version number, packed in hexadecimal using 8bits for // each of major/minor/revision. E.g: v2.5.7 is 0x020507. -PyObject* WebPDecoderVersion_wrapper(PyObject* self, PyObject* args){ +PyObject * +WebPDecoderVersion_wrapper() { return Py_BuildValue("i", WebPGetDecoderVersion()); } +// Version as string +const char * +WebPDecoderVersion_str(void) { + static char version[20]; + int version_number = WebPGetDecoderVersion(); + sprintf( + version, + "%d.%d.%d", + version_number >> 16, + (version_number >> 8) % 0x100, + version_number % 0x100); + return version; +} + /* * The version of webp that ships with (0.1.3) Ubuntu 12.04 doesn't handle alpha well. * Files that are valid with 0.3 are reported as being invalid. */ -int WebPDecoderBuggyAlpha(void) { - return WebPGetDecoderVersion()==0x0103; +int +WebPDecoderBuggyAlpha(void) { + return WebPGetDecoderVersion() == 0x0103; } -PyObject* WebPDecoderBuggyAlpha_wrapper(PyObject* self, PyObject* args){ +PyObject * +WebPDecoderBuggyAlpha_wrapper() { return Py_BuildValue("i", WebPDecoderBuggyAlpha()); } @@ -793,78 +905,86 @@ PyObject* WebPDecoderBuggyAlpha_wrapper(PyObject* self, PyObject* args){ /* Module Setup */ /* -------------------------------------------------------------------- */ -static PyMethodDef webpMethods[] = -{ +static PyMethodDef webpMethods[] = { #ifdef HAVE_WEBPANIM {"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"}, {"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"}, #endif {"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"}, {"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"}, - {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_VARARGS, "WebPVersion"}, - {"WebPDecoderBuggyAlpha", WebPDecoderBuggyAlpha_wrapper, METH_VARARGS, "WebPDecoderBuggyAlpha"}, - {NULL, NULL} -}; - -void addMuxFlagToModule(PyObject* m) { + {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_NOARGS, "WebPVersion"}, + {"WebPDecoderBuggyAlpha", + WebPDecoderBuggyAlpha_wrapper, + METH_NOARGS, + "WebPDecoderBuggyAlpha"}, + {NULL, NULL}}; + +void +addMuxFlagToModule(PyObject *m) { + PyObject *have_webpmux; #ifdef HAVE_WEBPMUX - PyModule_AddObject(m, "HAVE_WEBPMUX", Py_True); + have_webpmux = Py_True; #else - PyModule_AddObject(m, "HAVE_WEBPMUX", Py_False); + have_webpmux = Py_False; #endif + Py_INCREF(have_webpmux); + PyModule_AddObject(m, "HAVE_WEBPMUX", have_webpmux); } -void addAnimFlagToModule(PyObject* m) { +void +addAnimFlagToModule(PyObject *m) { + PyObject *have_webpanim; #ifdef HAVE_WEBPANIM - PyModule_AddObject(m, "HAVE_WEBPANIM", Py_True); + have_webpanim = Py_True; #else - PyModule_AddObject(m, "HAVE_WEBPANIM", Py_False); + have_webpanim = Py_False; #endif + Py_INCREF(have_webpanim); + PyModule_AddObject(m, "HAVE_WEBPANIM", have_webpanim); } -void addTransparencyFlagToModule(PyObject* m) { - PyModule_AddObject(m, "HAVE_TRANSPARENCY", - PyBool_FromLong(!WebPDecoderBuggyAlpha())); +void +addTransparencyFlagToModule(PyObject *m) { + PyModule_AddObject( + m, "HAVE_TRANSPARENCY", PyBool_FromLong(!WebPDecoderBuggyAlpha())); } -static int setup_module(PyObject* m) { +static int +setup_module(PyObject *m) { + PyObject *d = PyModule_GetDict(m); addMuxFlagToModule(m); addAnimFlagToModule(m); addTransparencyFlagToModule(m); + PyDict_SetItemString( + d, "webpdecoder_version", PyUnicode_FromString(WebPDecoderVersion_str())); + #ifdef HAVE_WEBPANIM /* Ready object types */ if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || - PyType_Ready(&WebPAnimEncoder_Type) < 0) + PyType_Ready(&WebPAnimEncoder_Type) < 0) { return -1; + } #endif return 0; } -#if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC PyInit__webp(void) { - PyObject* m; + PyObject *m; static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_webp", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - webpMethods, /* m_methods */ + "_webp", /* m_name */ + NULL, /* m_doc */ + -1, /* m_size */ + webpMethods, /* m_methods */ }; m = PyModule_Create(&module_def); - if (setup_module(m) < 0) + if (setup_module(m) < 0) { return NULL; + } return m; } -#else -PyMODINIT_FUNC -init_webp(void) -{ - PyObject* m = Py_InitModule("_webp", webpMethods); - setup_module(m); -} -#endif diff --git a/src/decode.c b/src/decode.c index be1b503e6a4..7a9b956c559 100644 --- a/src/decode.c +++ b/src/decode.c @@ -29,60 +29,61 @@ /* FIXME: make these pluggable! */ +#define PY_SSIZE_T_CLEAN #include "Python.h" -#include "Imaging.h" -#include "py3.h" - -#include "Gif.h" -#include "Raw.h" -#include "Bit.h" -#include "Sgi.h" +#include "libImaging/Imaging.h" +#include "libImaging/Bit.h" +#include "libImaging/Bcn.h" +#include "libImaging/Gif.h" +#include "libImaging/Raw.h" +#include "libImaging/Sgi.h" /* -------------------------------------------------------------------- */ /* Common */ /* -------------------------------------------------------------------- */ typedef struct { - PyObject_HEAD - int (*decode)(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + PyObject_HEAD int (*decode)( + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); int (*cleanup)(ImagingCodecState state); struct ImagingCodecStateInstance state; Imaging im; - PyObject* lock; + PyObject *lock; int pulls_fd; } ImagingDecoderObject; static PyTypeObject ImagingDecoderType; -static ImagingDecoderObject* -PyImaging_DecoderNew(int contextsize) -{ +static ImagingDecoderObject * +PyImaging_DecoderNew(int contextsize) { ImagingDecoderObject *decoder; void *context; - if(PyType_Ready(&ImagingDecoderType) < 0) + if (PyType_Ready(&ImagingDecoderType) < 0) { return NULL; + } decoder = PyObject_New(ImagingDecoderObject, &ImagingDecoderType); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } /* Clear the decoder state */ memset(&decoder->state, 0, sizeof(decoder->state)); /* Allocate decoder context */ if (contextsize > 0) { - context = (void*) calloc(1, contextsize); + context = (void *)calloc(1, contextsize); if (!context) { Py_DECREF(decoder); - (void) PyErr_NoMemory(); + (void)ImagingError_MemoryError(); return NULL; } - } else + } else { context = 0; + } /* Initialize decoder context */ decoder->state.context = context; @@ -102,10 +103,10 @@ PyImaging_DecoderNew(int contextsize) } static void -_dealloc(ImagingDecoderObject* decoder) -{ - if (decoder->cleanup) +_dealloc(ImagingDecoderObject *decoder) { + if (decoder->cleanup) { decoder->cleanup(&decoder->state); + } free(decoder->state.buffer); free(decoder->state.context); Py_XDECREF(decoder->lock); @@ -113,15 +114,16 @@ _dealloc(ImagingDecoderObject* decoder) PyObject_Del(decoder); } -static PyObject* -_decode(ImagingDecoderObject* decoder, PyObject* args) -{ - UINT8* buffer; - int bufsize, status; +static PyObject * +_decode(ImagingDecoderObject *decoder, PyObject *args) { + UINT8 *buffer; + Py_ssize_t bufsize; + int status; ImagingSectionCookie cookie; - if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH, &buffer, &bufsize)) + if (!PyArg_ParseTuple(args, "y#", &buffer, &bufsize)) { return NULL; + } if (!decoder->pulls_fd) { ImagingSectionEnter(&cookie); @@ -136,26 +138,23 @@ _decode(ImagingDecoderObject* decoder, PyObject* args) return Py_BuildValue("ii", status, decoder->state.errcode); } -static PyObject* -_decode_cleanup(ImagingDecoderObject* decoder, PyObject* args) -{ +static PyObject * +_decode_cleanup(ImagingDecoderObject *decoder, PyObject *args) { int status = 0; - if (decoder->cleanup){ + if (decoder->cleanup) { status = decoder->cleanup(&decoder->state); } return Py_BuildValue("i", status); } +extern Imaging +PyImaging_AsImaging(PyObject *op); - -extern Imaging PyImaging_AsImaging(PyObject *op); - -static PyObject* -_setimage(ImagingDecoderObject* decoder, PyObject* args) -{ - PyObject* op; +static PyObject * +_setimage(ImagingDecoderObject *decoder, PyObject *args) { + PyObject *op; Imaging im; ImagingCodecState state; int x0, y0, x1, y1; @@ -163,11 +162,13 @@ _setimage(ImagingDecoderObject* decoder, PyObject* args) x0 = y0 = x1 = y1 = 0; /* FIXME: should publish the ImagingType descriptor */ - if (!PyArg_ParseTuple(args, "O|(iiii)", &op, &x0, &y0, &x1, &y1)) + if (!PyArg_ParseTuple(args, "O|(iiii)", &op, &x0, &y0, &x1, &y1)) { return NULL; + } im = PyImaging_AsImaging(op); - if (!im) + if (!im) { return NULL; + } decoder->im = im; @@ -184,10 +185,8 @@ _setimage(ImagingDecoderObject* decoder, PyObject* args) state->ysize = y1 - y0; } - if (state->xsize <= 0 || - state->xsize + state->xoff > (int) im->xsize || - state->ysize <= 0 || - state->ysize + state->yoff > (int) im->ysize) { + if (state->xsize <= 0 || state->xsize + state->xoff > (int)im->xsize || + state->ysize <= 0 || state->ysize + state->yoff > (int)im->ysize) { PyErr_SetString(PyExc_ValueError, "tile cannot extend outside image"); return NULL; } @@ -195,15 +194,16 @@ _setimage(ImagingDecoderObject* decoder, PyObject* args) /* Allocate memory buffer (if bits field is set) */ if (state->bits > 0) { if (!state->bytes) { - if (state->xsize > ((INT_MAX / state->bits)-7)){ - return PyErr_NoMemory(); + if (state->xsize > ((INT_MAX / state->bits) - 7)) { + return ImagingError_MemoryError(); } - state->bytes = (state->bits * state->xsize+7)/8; + state->bytes = (state->bits * state->xsize + 7) / 8; } /* malloc check ok, overflow checked above */ - state->buffer = (UINT8*) malloc(state->bytes); - if (!state->buffer) - return PyErr_NoMemory(); + state->buffer = (UINT8 *)calloc(1, state->bytes); + if (!state->buffer) { + return ImagingError_MemoryError(); + } } /* Keep a reference to the image object, to make sure it doesn't @@ -216,14 +216,14 @@ _setimage(ImagingDecoderObject* decoder, PyObject* args) return Py_None; } -static PyObject* -_setfd(ImagingDecoderObject* decoder, PyObject* args) -{ - PyObject* fd; +static PyObject * +_setfd(ImagingDecoderObject *decoder, PyObject *args) { + PyObject *fd; ImagingCodecState state; - if (!PyArg_ParseTuple(args, "O", &fd)) + if (!PyArg_ParseTuple(args, "O", &fd)) { return NULL; + } state = &decoder->state; @@ -234,75 +234,72 @@ _setfd(ImagingDecoderObject* decoder, PyObject* args) return Py_None; } - static PyObject * -_get_pulls_fd(ImagingDecoderObject *decoder) -{ +_get_pulls_fd(ImagingDecoderObject *decoder, void *closure) { return PyBool_FromLong(decoder->pulls_fd); } static struct PyMethodDef methods[] = { - {"decode", (PyCFunction)_decode, 1}, - {"cleanup", (PyCFunction)_decode_cleanup, 1}, - {"setimage", (PyCFunction)_setimage, 1}, - {"setfd", (PyCFunction)_setfd, 1}, + {"decode", (PyCFunction)_decode, METH_VARARGS}, + {"cleanup", (PyCFunction)_decode_cleanup, METH_VARARGS}, + {"setimage", (PyCFunction)_setimage, METH_VARARGS}, + {"setfd", (PyCFunction)_setfd, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; static struct PyGetSetDef getseters[] = { - {"pulls_fd", (getter)_get_pulls_fd, NULL, + {"pulls_fd", + (getter)_get_pulls_fd, + NULL, "True if this decoder expects to pull from self.fd itself.", NULL}, {NULL, NULL, NULL, NULL, NULL} /* sentinel */ }; static PyTypeObject ImagingDecoderType = { - PyVarObject_HEAD_INIT(NULL, 0) - "ImagingDecoder", /*tp_name*/ - sizeof(ImagingDecoderObject), /*tp_size*/ - 0, /*tp_itemsize*/ + PyVarObject_HEAD_INIT(NULL, 0) "ImagingDecoder", /*tp_name*/ + sizeof(ImagingDecoderObject), /*tp_size*/ + 0, /*tp_itemsize*/ /* methods */ - (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - getseters, /*tp_getset*/ + (destructor)_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + methods, /*tp_methods*/ + 0, /*tp_members*/ + getseters, /*tp_getset*/ }; /* -------------------------------------------------------------------- */ int -get_unpacker(ImagingDecoderObject* decoder, const char* mode, - const char* rawmode) -{ +get_unpacker(ImagingDecoderObject *decoder, const char *mode, const char *rawmode) { int bits; ImagingShuffler unpack; unpack = ImagingFindUnpacker(mode, rawmode, &bits); if (!unpack) { Py_DECREF(decoder); - PyErr_SetString(PyExc_ValueError, "unknown raw mode"); + PyErr_SetString(PyExc_ValueError, "unknown raw mode for given image mode"); return -1; } @@ -312,25 +309,23 @@ get_unpacker(ImagingDecoderObject* decoder, const char* mode, return 0; } - /* -------------------------------------------------------------------- */ /* BIT (packed fields) */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_BitDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; +PyObject * +PyImaging_BitDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; - char* mode; - int bits = 8; - int pad = 8; - int fill = 0; - int sign = 0; + char *mode; + int bits = 8; + int pad = 8; + int fill = 0; + int sign = 0; int ystep = 1; - if (!PyArg_ParseTuple(args, "s|iiiii", &mode, &bits, &pad, &fill, - &sign, &ystep)) + if (!PyArg_ParseTuple(args, "s|iiiii", &mode, &bits, &pad, &fill, &sign, &ystep)) { return NULL; + } if (strcmp(mode, "F") != 0) { PyErr_SetString(PyExc_ValueError, "bad image mode"); @@ -338,53 +333,55 @@ PyImaging_BitDecoderNew(PyObject* self, PyObject* args) } decoder = PyImaging_DecoderNew(sizeof(BITSTATE)); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } decoder->decode = ImagingBitDecode; decoder->state.ystep = ystep; - ((BITSTATE*)decoder->state.context)->bits = bits; - ((BITSTATE*)decoder->state.context)->pad = pad; - ((BITSTATE*)decoder->state.context)->fill = fill; - ((BITSTATE*)decoder->state.context)->sign = sign; + ((BITSTATE *)decoder->state.context)->bits = bits; + ((BITSTATE *)decoder->state.context)->pad = pad; + ((BITSTATE *)decoder->state.context)->fill = fill; + ((BITSTATE *)decoder->state.context)->sign = sign; - return (PyObject*) decoder; + return (PyObject *)decoder; } - /* -------------------------------------------------------------------- */ /* BCn: GPU block-compressed texture formats */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_BcnDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; +PyObject * +PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; - char* mode; - char* actual; + char *mode; + char *actual; int n = 0; - int ystep = 1; - if (!PyArg_ParseTuple(args, "s|ii", &mode, &n, &ystep)) + char *pixel_format = ""; + if (!PyArg_ParseTuple(args, "si|s", &mode, &n, &pixel_format)) { return NULL; + } switch (n) { - case 1: /* BC1: 565 color, 1-bit alpha */ - case 2: /* BC2: 565 color, 4-bit alpha */ - case 3: /* BC3: 565 color, 2-endpoint 8-bit interpolated alpha */ - case 5: /* BC5: 2-channel 8-bit via 2 BC3 alpha blocks */ - case 7: /* BC7: 4-channel 8-bit via everything */ - actual = "RGBA"; break; - case 4: /* BC4: 1-channel 8-bit via 1 BC3 alpha block */ - actual = "L"; break; - case 6: /* BC6: 3-channel 16-bit float */ - /* TODO: support 4-channel floating point images */ - actual = "RGBAF"; break; - default: - PyErr_SetString(PyExc_ValueError, "block compression type unknown"); - return NULL; + case 1: /* BC1: 565 color, 1-bit alpha */ + case 2: /* BC2: 565 color, 4-bit alpha */ + case 3: /* BC3: 565 color, 2-endpoint 8-bit interpolated alpha */ + case 7: /* BC7: 4-channel 8-bit via everything */ + actual = "RGBA"; + break; + case 4: /* BC4: 1-channel 8-bit via 1 BC3 alpha block */ + actual = "L"; + break; + case 5: /* BC5: 2-channel 8-bit via 2 BC3 alpha blocks */ + case 6: /* BC6: 3-channel 16-bit float */ + actual = "RGB"; + break; + default: + PyErr_SetString(PyExc_ValueError, "block compression type unknown"); + return NULL; } if (strcmp(mode, actual) != 0) { @@ -392,51 +389,51 @@ PyImaging_BcnDecoderNew(PyObject* self, PyObject* args) return NULL; } - decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + decoder = PyImaging_DecoderNew(sizeof(char *)); + if (decoder == NULL) { return NULL; + } decoder->decode = ImagingBcnDecode; decoder->state.state = n; - decoder->state.ystep = ystep; + ((BCNSTATE *)decoder->state.context)->pixel_format = pixel_format; - return (PyObject*) decoder; + return (PyObject *)decoder; } - /* -------------------------------------------------------------------- */ /* FLI */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_FliDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; +PyObject * +PyImaging_FliDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } decoder->decode = ImagingFliDecode; - return (PyObject*) decoder; + return (PyObject *)decoder; } - /* -------------------------------------------------------------------- */ /* GIF */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_GifDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; +PyObject * +PyImaging_GifDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; - char* mode; + char *mode; int bits = 8; int interlace = 0; - if (!PyArg_ParseTuple(args, "s|ii", &mode, &bits, &interlace)) + int transparency = -1; + if (!PyArg_ParseTuple(args, "s|iii", &mode, &bits, &interlace, &transparency)) { return NULL; + } if (strcmp(mode, "L") != 0 && strcmp(mode, "P") != 0) { PyErr_SetString(PyExc_ValueError, "bad image mode"); @@ -444,354 +441,364 @@ PyImaging_GifDecoderNew(PyObject* self, PyObject* args) } decoder = PyImaging_DecoderNew(sizeof(GIFDECODERSTATE)); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } decoder->decode = ImagingGifDecode; - ((GIFDECODERSTATE*)decoder->state.context)->bits = bits; - ((GIFDECODERSTATE*)decoder->state.context)->interlace = interlace; + ((GIFDECODERSTATE *)decoder->state.context)->bits = bits; + ((GIFDECODERSTATE *)decoder->state.context)->interlace = interlace; + ((GIFDECODERSTATE *)decoder->state.context)->transparency = transparency; - return (PyObject*) decoder; + return (PyObject *)decoder; } - /* -------------------------------------------------------------------- */ /* HEX */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_HexDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; +PyObject * +PyImaging_HexDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; - char* mode; - char* rawmode; - if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) + char *mode; + char *rawmode; + if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { return NULL; + } decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->decode = ImagingHexDecode; - return (PyObject*) decoder; + return (PyObject *)decoder; } - /* -------------------------------------------------------------------- */ /* LibTiff */ /* -------------------------------------------------------------------- */ #ifdef HAVE_LIBTIFF -#include "TiffDecode.h" +#include "libImaging/TiffDecode.h" #include -PyObject* -PyImaging_LibTiffDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; - char* mode; - char* rawmode; - char* compname; +PyObject * +PyImaging_LibTiffDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + char *mode; + char *rawmode; + char *compname; int fp; - int ifdoffset; + uint32_t ifdoffset; - if (! PyArg_ParseTuple(args, "sssii", &mode, &rawmode, &compname, &fp, &ifdoffset)) + if (!PyArg_ParseTuple(args, "sssiI", &mode, &rawmode, &compname, &fp, &ifdoffset)) { return NULL; + } TRACE(("new tiff decoder %s\n", compname)); decoder = PyImaging_DecoderNew(sizeof(TIFFSTATE)); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } - if (! ImagingLibTiffInit(&decoder->state, fp, ifdoffset)) { + if (!ImagingLibTiffInit(&decoder->state, fp, ifdoffset)) { Py_DECREF(decoder); PyErr_SetString(PyExc_RuntimeError, "tiff codec initialization failed"); return NULL; } - decoder->decode = ImagingLibTiffDecode; + decoder->decode = ImagingLibTiffDecode; - return (PyObject*) decoder; + return (PyObject *)decoder; } #endif - /* -------------------------------------------------------------------- */ /* PackBits */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_PackbitsDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; +PyObject * +PyImaging_PackbitsDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; - char* mode; - char* rawmode; - if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) + char *mode; + char *rawmode; + if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { return NULL; + } decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->decode = ImagingPackbitsDecode; - return (PyObject*) decoder; + return (PyObject *)decoder; } - /* -------------------------------------------------------------------- */ /* PCD */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_PcdDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; +PyObject * +PyImaging_PcdDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } /* Unpack from PhotoYCC to RGB */ - if (get_unpacker(decoder, "RGB", "YCC;P") < 0) + if (get_unpacker(decoder, "RGB", "YCC;P") < 0) { return NULL; + } decoder->decode = ImagingPcdDecode; - return (PyObject*) decoder; + return (PyObject *)decoder; } - /* -------------------------------------------------------------------- */ /* PCX */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_PcxDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; +PyObject * +PyImaging_PcxDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; - char* mode; - char* rawmode; + char *mode; + char *rawmode; int stride; - if (!PyArg_ParseTuple(args, "ssi", &mode, &rawmode, &stride)) + if (!PyArg_ParseTuple(args, "ssi", &mode, &rawmode, &stride)) { return NULL; + } decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->state.bytes = stride; decoder->decode = ImagingPcxDecode; - return (PyObject*) decoder; + return (PyObject *)decoder; } - /* -------------------------------------------------------------------- */ /* RAW */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_RawDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; +PyObject * +PyImaging_RawDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; - char* mode; - char* rawmode; + char *mode; + char *rawmode; int stride = 0; - int ystep = 1; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &stride, &ystep)) + int ystep = 1; + if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &stride, &ystep)) { return NULL; + } decoder = PyImaging_DecoderNew(sizeof(RAWSTATE)); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->decode = ImagingRawDecode; decoder->state.ystep = ystep; - ((RAWSTATE*)decoder->state.context)->stride = stride; + ((RAWSTATE *)decoder->state.context)->stride = stride; - return (PyObject*) decoder; + return (PyObject *)decoder; } - /* -------------------------------------------------------------------- */ /* SGI RLE */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_SgiRleDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; +PyObject * +PyImaging_SgiRleDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; - char* mode; - char* rawmode; + char *mode; + char *rawmode; int ystep = 1; int bpc = 1; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &ystep, &bpc)) + if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &ystep, &bpc)) { return NULL; + } decoder = PyImaging_DecoderNew(sizeof(SGISTATE)); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->pulls_fd = 1; decoder->decode = ImagingSgiRleDecode; decoder->state.ystep = ystep; - ((SGISTATE*)decoder->state.context)->bpc = bpc; + ((SGISTATE *)decoder->state.context)->bpc = bpc; - return (PyObject*) decoder; + return (PyObject *)decoder; } - /* -------------------------------------------------------------------- */ /* SUN RLE */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_SunRleDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; +PyObject * +PyImaging_SunRleDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; - char* mode; - char* rawmode; - if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) + char *mode; + char *rawmode; + if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { return NULL; + } decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->decode = ImagingSunRleDecode; - return (PyObject*) decoder; + return (PyObject *)decoder; } - /* -------------------------------------------------------------------- */ /* TGA RLE */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_TgaRleDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; +PyObject * +PyImaging_TgaRleDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; - char* mode; - char* rawmode; + char *mode; + char *rawmode; int ystep = 1; int depth = 8; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &ystep, &depth)) + if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &ystep, &depth)) { return NULL; + } decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->decode = ImagingTgaRleDecode; decoder->state.ystep = ystep; decoder->state.count = depth / 8; - return (PyObject*) decoder; + return (PyObject *)decoder; } - /* -------------------------------------------------------------------- */ /* XBM */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_XbmDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; +PyObject * +PyImaging_XbmDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, "1", "1;R") < 0) + if (get_unpacker(decoder, "1", "1;R") < 0) { return NULL; + } decoder->decode = ImagingXbmDecode; - return (PyObject*) decoder; + return (PyObject *)decoder; } - /* -------------------------------------------------------------------- */ /* ZIP */ /* -------------------------------------------------------------------- */ #ifdef HAVE_LIBZ -#include "Zip.h" +#include "libImaging/ZipCodecs.h" -PyObject* -PyImaging_ZipDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; +PyObject * +PyImaging_ZipDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; - char* mode; - char* rawmode; + char *mode; + char *rawmode; int interlaced = 0; - if (!PyArg_ParseTuple(args, "ss|i", &mode, &rawmode, &interlaced)) + if (!PyArg_ParseTuple(args, "ss|i", &mode, &rawmode, &interlaced)) { return NULL; + } decoder = PyImaging_DecoderNew(sizeof(ZIPSTATE)); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->decode = ImagingZipDecode; decoder->cleanup = ImagingZipDecodeCleanup; - ((ZIPSTATE*)decoder->state.context)->interlaced = interlaced; + ((ZIPSTATE *)decoder->state.context)->interlaced = interlaced; - return (PyObject*) decoder; + return (PyObject *)decoder; } #endif - /* -------------------------------------------------------------------- */ /* JPEG */ /* -------------------------------------------------------------------- */ @@ -801,39 +808,40 @@ PyImaging_ZipDecoderNew(PyObject* self, PyObject* args) /* We better define this decoder last in this file, so the following undef's won't mess things up for the Imaging library proper. */ -#undef HAVE_PROTOTYPES -#undef HAVE_STDDEF_H -#undef HAVE_STDLIB_H -#undef UINT8 -#undef UINT16 -#undef UINT32 -#undef INT8 -#undef INT16 -#undef INT32 - -#include "Jpeg.h" - -PyObject* -PyImaging_JpegDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; - - char* mode; - char* rawmode; /* what we want from the decoder */ - char* jpegmode; /* what's in the file */ +#undef HAVE_PROTOTYPES +#undef HAVE_STDDEF_H +#undef HAVE_STDLIB_H +#undef UINT8 +#undef UINT16 +#undef UINT32 +#undef INT8 +#undef INT16 +#undef INT32 + +#include "libImaging/Jpeg.h" + +PyObject * +PyImaging_JpegDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; + + char *mode; + char *rawmode; /* what we want from the decoder */ + char *jpegmode; /* what's in the file */ int scale = 1; int draft = 0; - if (!PyArg_ParseTuple(args, "ssz|ii", &mode, &rawmode, &jpegmode, - &scale, &draft)) + if (!PyArg_ParseTuple(args, "ssz|ii", &mode, &rawmode, &jpegmode, &scale, &draft)) { return NULL; + } - if (!jpegmode) + if (!jpegmode) { jpegmode = ""; + } decoder = PyImaging_DecoderNew(sizeof(JPEGSTATE)); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } // libjpeg-turbo supports different output formats. // We are choosing Pillow's native format (3 color bytes + 1 padding) @@ -842,19 +850,20 @@ PyImaging_JpegDecoderNew(PyObject* self, PyObject* args) rawmode = "RGBX"; } - if (get_unpacker(decoder, mode, rawmode) < 0) + if (get_unpacker(decoder, mode, rawmode) < 0) { return NULL; + } decoder->decode = ImagingJpegDecode; decoder->cleanup = ImagingJpegDecodeCleanup; - strncpy(((JPEGSTATE*)decoder->state.context)->rawmode, rawmode, 8); - strncpy(((JPEGSTATE*)decoder->state.context)->jpegmode, jpegmode, 8); + strncpy(((JPEGSTATE *)decoder->state.context)->rawmode, rawmode, 8); + strncpy(((JPEGSTATE *)decoder->state.context)->jpegmode, jpegmode, 8); - ((JPEGSTATE*)decoder->state.context)->scale = scale; - ((JPEGSTATE*)decoder->state.context)->draft = draft; + ((JPEGSTATE *)decoder->state.context)->scale = scale; + ((JPEGSTATE *)decoder->state.context)->draft = draft; - return (PyObject*) decoder; + return (PyObject *)decoder; } #endif @@ -864,38 +873,40 @@ PyImaging_JpegDecoderNew(PyObject* self, PyObject* args) #ifdef HAVE_OPENJPEG -#include "Jpeg2K.h" +#include "libImaging/Jpeg2K.h" -PyObject* -PyImaging_Jpeg2KDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; +PyObject * +PyImaging_Jpeg2KDecoderNew(PyObject *self, PyObject *args) { + ImagingDecoderObject *decoder; JPEG2KDECODESTATE *context; - char* mode; - char* format; + char *mode; + char *format; OPJ_CODEC_FORMAT codec_format; int reduce = 0; int layers = 0; int fd = -1; PY_LONG_LONG length = -1; - if (!PyArg_ParseTuple(args, "ss|iiiL", &mode, &format, - &reduce, &layers, &fd, &length)) + if (!PyArg_ParseTuple( + args, "ss|iiiL", &mode, &format, &reduce, &layers, &fd, &length)) { return NULL; + } - if (strcmp(format, "j2k") == 0) + if (strcmp(format, "j2k") == 0) { codec_format = OPJ_CODEC_J2K; - else if (strcmp(format, "jpt") == 0) + } else if (strcmp(format, "jpt") == 0) { codec_format = OPJ_CODEC_JPT; - else if (strcmp(format, "jp2") == 0) + } else if (strcmp(format, "jp2") == 0) { codec_format = OPJ_CODEC_JP2; - else + } else { return NULL; + } decoder = PyImaging_DecoderNew(sizeof(JPEG2KDECODESTATE)); - if (decoder == NULL) + if (decoder == NULL) { return NULL; + } decoder->pulls_fd = 1; decoder->decode = ImagingJpeg2KDecode; @@ -909,7 +920,6 @@ PyImaging_Jpeg2KDecoderNew(PyObject* self, PyObject* args) context->reduce = reduce; context->layers = layers; - return (PyObject*) decoder; + return (PyObject *)decoder; } #endif /* HAVE_OPENJPEG */ - diff --git a/src/display.c b/src/display.c index 49143b72e0e..0ce10e2493c 100644 --- a/src/display.c +++ b/src/display.c @@ -22,18 +22,17 @@ * See the README file for information on usage and redistribution. */ - +#define PY_SSIZE_T_CLEAN #include "Python.h" -#include "Imaging.h" -#include "py3.h" +#include "libImaging/Imaging.h" /* -------------------------------------------------------------------- */ -/* Windows DIB support */ +/* Windows DIB support */ #ifdef _WIN32 -#include "ImDib.h" +#include "libImaging/ImDib.h" #if SIZEOF_VOID_P == 8 #define F_HANDLE "K" @@ -42,47 +41,47 @@ #endif typedef struct { - PyObject_HEAD - ImagingDIB dib; + PyObject_HEAD ImagingDIB dib; } ImagingDisplayObject; static PyTypeObject ImagingDisplayType; -static ImagingDisplayObject* -_new(const char* mode, int xsize, int ysize) -{ +static ImagingDisplayObject * +_new(const char *mode, int xsize, int ysize) { ImagingDisplayObject *display; - if (PyType_Ready(&ImagingDisplayType) < 0) + if (PyType_Ready(&ImagingDisplayType) < 0) { return NULL; + } display = PyObject_New(ImagingDisplayObject, &ImagingDisplayType); - if (display == NULL) - return NULL; + if (display == NULL) { + return NULL; + } display->dib = ImagingNewDIB(mode, xsize, ysize); if (!display->dib) { - Py_DECREF(display); - return NULL; + Py_DECREF(display); + return NULL; } return display; } static void -_delete(ImagingDisplayObject* display) -{ - if (display->dib) - ImagingDeleteDIB(display->dib); +_delete(ImagingDisplayObject *display) { + if (display->dib) { + ImagingDeleteDIB(display->dib); + } PyObject_Del(display); } -static PyObject* -_expose(ImagingDisplayObject* display, PyObject* args) -{ +static PyObject * +_expose(ImagingDisplayObject *display, PyObject *args) { HDC hdc; - if (!PyArg_ParseTuple(args, F_HANDLE, &hdc)) - return NULL; + if (!PyArg_ParseTuple(args, F_HANDLE, &hdc)) { + return NULL; + } ImagingExposeDIB(display->dib, hdc); @@ -90,16 +89,25 @@ _expose(ImagingDisplayObject* display, PyObject* args) return Py_None; } -static PyObject* -_draw(ImagingDisplayObject* display, PyObject* args) -{ +static PyObject * +_draw(ImagingDisplayObject *display, PyObject *args) { HDC hdc; int dst[4]; int src[4]; - if (!PyArg_ParseTuple(args, F_HANDLE "(iiii)(iiii)", &hdc, - dst+0, dst+1, dst+2, dst+3, - src+0, src+1, src+2, src+3)) - return NULL; + if (!PyArg_ParseTuple( + args, + F_HANDLE "(iiii)(iiii)", + &hdc, + dst + 0, + dst + 1, + dst + 2, + dst + 3, + src + 0, + src + 1, + src + 2, + src + 3)) { + return NULL; + } ImagingDrawDIB(display->dib, hdc, dst, src); @@ -107,26 +115,30 @@ _draw(ImagingDisplayObject* display, PyObject* args) return Py_None; } -extern Imaging PyImaging_AsImaging(PyObject *op); +extern Imaging +PyImaging_AsImaging(PyObject *op); -static PyObject* -_paste(ImagingDisplayObject* display, PyObject* args) -{ +static PyObject * +_paste(ImagingDisplayObject *display, PyObject *args) { Imaging im; - PyObject* op; + PyObject *op; int xy[4]; xy[0] = xy[1] = xy[2] = xy[3] = 0; - if (!PyArg_ParseTuple(args, "O|(iiii)", &op, xy+0, xy+1, xy+2, xy+3)) - return NULL; + if (!PyArg_ParseTuple(args, "O|(iiii)", &op, xy + 0, xy + 1, xy + 2, xy + 3)) { + return NULL; + } im = PyImaging_AsImaging(op); - if (!im) - return NULL; + if (!im) { + return NULL; + } - if (xy[2] <= xy[0]) - xy[2] = xy[0] + im->xsize; - if (xy[3] <= xy[1]) - xy[3] = xy[1] + im->ysize; + if (xy[2] <= xy[0]) { + xy[2] = xy[0] + im->xsize; + } + if (xy[3] <= xy[1]) { + xy[3] = xy[1] + im->ysize; + } ImagingPasteDIB(display->dib, im, xy); @@ -134,46 +146,46 @@ _paste(ImagingDisplayObject* display, PyObject* args) return Py_None; } -static PyObject* -_query_palette(ImagingDisplayObject* display, PyObject* args) -{ +static PyObject * +_query_palette(ImagingDisplayObject *display, PyObject *args) { HDC hdc; int status; - if (!PyArg_ParseTuple(args, F_HANDLE, &hdc)) - return NULL; + if (!PyArg_ParseTuple(args, F_HANDLE, &hdc)) { + return NULL; + } status = ImagingQueryPaletteDIB(display->dib, hdc); return Py_BuildValue("i", status); } -static PyObject* -_getdc(ImagingDisplayObject* display, PyObject* args) -{ +static PyObject * +_getdc(ImagingDisplayObject *display, PyObject *args) { HWND window; HDC dc; - if (!PyArg_ParseTuple(args, F_HANDLE, &window)) - return NULL; + if (!PyArg_ParseTuple(args, F_HANDLE, &window)) { + return NULL; + } dc = GetDC(window); if (!dc) { - PyErr_SetString(PyExc_IOError, "cannot create dc"); + PyErr_SetString(PyExc_OSError, "cannot create dc"); return NULL; } return Py_BuildValue(F_HANDLE, dc); } -static PyObject* -_releasedc(ImagingDisplayObject* display, PyObject* args) -{ +static PyObject * +_releasedc(ImagingDisplayObject *display, PyObject *args) { HWND window; HDC dc; - if (!PyArg_ParseTuple(args, F_HANDLE F_HANDLE, &window, &dc)) - return NULL; + if (!PyArg_ParseTuple(args, F_HANDLE F_HANDLE, &window, &dc)) { + return NULL; + } ReleaseDC(window, dc); @@ -181,19 +193,14 @@ _releasedc(ImagingDisplayObject* display, PyObject* args) return Py_None; } -static PyObject* -_frombytes(ImagingDisplayObject* display, PyObject* args) -{ - char* ptr; - int bytes; +static PyObject * +_frombytes(ImagingDisplayObject *display, PyObject *args) { + char *ptr; + Py_ssize_t bytes; -#if PY_VERSION_HEX >= 0x03000000 - if (!PyArg_ParseTuple(args, "y#:frombytes", &ptr, &bytes)) - return NULL; -#else - if (!PyArg_ParseTuple(args, "s#:fromstring", &ptr, &bytes)) + if (!PyArg_ParseTuple(args, "y#:frombytes", &ptr, &bytes)) { return NULL; -#endif + } if (display->dib->ysize * display->dib->linesize != bytes) { PyErr_SetString(PyExc_ValueError, "wrong size"); @@ -206,108 +213,94 @@ _frombytes(ImagingDisplayObject* display, PyObject* args) return Py_None; } -static PyObject* -_tobytes(ImagingDisplayObject* display, PyObject* args) -{ -#if PY_VERSION_HEX >= 0x03000000 - if (!PyArg_ParseTuple(args, ":tobytes")) - return NULL; -#else - if (!PyArg_ParseTuple(args, ":tostring")) +static PyObject * +_tobytes(ImagingDisplayObject *display, PyObject *args) { + if (!PyArg_ParseTuple(args, ":tobytes")) { return NULL; -#endif + } return PyBytes_FromStringAndSize( - display->dib->bits, display->dib->ysize * display->dib->linesize - ); + display->dib->bits, display->dib->ysize * display->dib->linesize); } static struct PyMethodDef methods[] = { - {"draw", (PyCFunction)_draw, 1}, - {"expose", (PyCFunction)_expose, 1}, - {"paste", (PyCFunction)_paste, 1}, - {"query_palette", (PyCFunction)_query_palette, 1}, - {"getdc", (PyCFunction)_getdc, 1}, - {"releasedc", (PyCFunction)_releasedc, 1}, - {"frombytes", (PyCFunction)_frombytes, 1}, - {"tobytes", (PyCFunction)_tobytes, 1}, - {"fromstring", (PyCFunction)_frombytes, 1}, - {"tostring", (PyCFunction)_tobytes, 1}, + {"draw", (PyCFunction)_draw, METH_VARARGS}, + {"expose", (PyCFunction)_expose, METH_VARARGS}, + {"paste", (PyCFunction)_paste, METH_VARARGS}, + {"query_palette", (PyCFunction)_query_palette, METH_VARARGS}, + {"getdc", (PyCFunction)_getdc, METH_VARARGS}, + {"releasedc", (PyCFunction)_releasedc, METH_VARARGS}, + {"frombytes", (PyCFunction)_frombytes, METH_VARARGS}, + {"tobytes", (PyCFunction)_tobytes, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; -static PyObject* -_getattr_mode(ImagingDisplayObject* self, void* closure) -{ - return Py_BuildValue("s", self->dib->mode); +static PyObject * +_getattr_mode(ImagingDisplayObject *self, void *closure) { + return Py_BuildValue("s", self->dib->mode); } -static PyObject* -_getattr_size(ImagingDisplayObject* self, void* closure) -{ - return Py_BuildValue("ii", self->dib->xsize, self->dib->ysize); +static PyObject * +_getattr_size(ImagingDisplayObject *self, void *closure) { + return Py_BuildValue("ii", self->dib->xsize, self->dib->ysize); } static struct PyGetSetDef getsetters[] = { - { "mode", (getter) _getattr_mode }, - { "size", (getter) _getattr_size }, - { NULL } -}; + {"mode", (getter)_getattr_mode}, {"size", (getter)_getattr_size}, {NULL}}; static PyTypeObject ImagingDisplayType = { - PyVarObject_HEAD_INIT(NULL, 0) - "ImagingDisplay", /*tp_name*/ - sizeof(ImagingDisplayObject), /*tp_size*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_delete, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - getsetters, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0) "ImagingDisplay", /*tp_name*/ + sizeof(ImagingDisplayObject), /*tp_size*/ + 0, /*tp_itemsize*/ + /* methods */ + (destructor)_delete, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + methods, /*tp_methods*/ + 0, /*tp_members*/ + getsetters, /*tp_getset*/ }; -PyObject* -PyImaging_DisplayWin32(PyObject* self, PyObject* args) -{ - ImagingDisplayObject* display; +PyObject * +PyImaging_DisplayWin32(PyObject *self, PyObject *args) { + ImagingDisplayObject *display; char *mode; int xsize, ysize; - if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) - return NULL; + if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { + return NULL; + } display = _new(mode, xsize, ysize); - if (display == NULL) - return NULL; + if (display == NULL) { + return NULL; + } - return (PyObject*) display; + return (PyObject *)display; } -PyObject* -PyImaging_DisplayModeWin32(PyObject* self, PyObject* args) -{ +PyObject * +PyImaging_DisplayModeWin32(PyObject *self, PyObject *args) { char *mode; int size[2]; @@ -319,14 +312,24 @@ PyImaging_DisplayModeWin32(PyObject* self, PyObject* args) /* -------------------------------------------------------------------- */ /* Windows screen grabber */ -PyObject* -PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) -{ - int width, height; +typedef HANDLE(__stdcall *Func_SetThreadDpiAwarenessContext)(HANDLE); + +PyObject * +PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { + int x = 0, y = 0, width, height; + int includeLayeredWindows = 0, all_screens = 0; HBITMAP bitmap; BITMAPCOREHEADER core; HDC screen, screen_copy; - PyObject* buffer; + DWORD rop; + PyObject *buffer; + HANDLE dpiAwareness; + HMODULE user32; + Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function; + + if (!PyArg_ParseTuple(args, "|ii", &includeLayeredWindows, &all_screens)) { + return NULL; + } /* step 1: create a memory DC large enough to hold the entire screen */ @@ -334,44 +337,83 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) screen = CreateDC("DISPLAY", NULL, NULL, NULL); screen_copy = CreateCompatibleDC(screen); - width = GetDeviceCaps(screen, HORZRES); - height = GetDeviceCaps(screen, VERTRES); + // added in Windows 10 (1607) + // loaded dynamically to avoid link errors + user32 = LoadLibraryA("User32.dll"); + SetThreadDpiAwarenessContext_function = + (Func_SetThreadDpiAwarenessContext)GetProcAddress( + user32, "SetThreadDpiAwarenessContext"); + if (SetThreadDpiAwarenessContext_function != NULL) { + // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3) + dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE)-3); + } + + if (all_screens) { + x = GetSystemMetrics(SM_XVIRTUALSCREEN); + y = GetSystemMetrics(SM_YVIRTUALSCREEN); + width = GetSystemMetrics(SM_CXVIRTUALSCREEN); + height = GetSystemMetrics(SM_CYVIRTUALSCREEN); + } else { + width = GetDeviceCaps(screen, HORZRES); + height = GetDeviceCaps(screen, VERTRES); + } + + if (SetThreadDpiAwarenessContext_function != NULL) { + SetThreadDpiAwarenessContext_function(dpiAwareness); + } + + FreeLibrary(user32); bitmap = CreateCompatibleBitmap(screen, width, height); - if (!bitmap) + if (!bitmap) { goto error; + } - if (!SelectObject(screen_copy, bitmap)) + if (!SelectObject(screen_copy, bitmap)) { goto error; + } /* step 2: copy bits into memory DC bitmap */ - if (!BitBlt(screen_copy, 0, 0, width, height, screen, 0, 0, SRCCOPY)) + rop = SRCCOPY; + if (includeLayeredWindows) { + rop |= CAPTUREBLT; + } + if (!BitBlt(screen_copy, 0, 0, width, height, screen, x, y, rop)) { goto error; + } /* step 3: extract bits from bitmap */ - buffer = PyBytes_FromStringAndSize(NULL, height * ((width*3 + 3) & -4)); - if (!buffer) + buffer = PyBytes_FromStringAndSize(NULL, height * ((width * 3 + 3) & -4)); + if (!buffer) { return NULL; + } core.bcSize = sizeof(core); core.bcWidth = width; core.bcHeight = height; core.bcPlanes = 1; core.bcBitCount = 24; - if (!GetDIBits(screen_copy, bitmap, 0, height, PyBytes_AS_STRING(buffer), - (BITMAPINFO*) &core, DIB_RGB_COLORS)) + if (!GetDIBits( + screen_copy, + bitmap, + 0, + height, + PyBytes_AS_STRING(buffer), + (BITMAPINFO *)&core, + DIB_RGB_COLORS)) { goto error; + } DeleteObject(bitmap); DeleteDC(screen_copy); DeleteDC(screen); - return Py_BuildValue("(ii)N", width, height, buffer); + return Py_BuildValue("(ii)(ii)N", x, y, width, height, buffer); error: - PyErr_SetString(PyExc_IOError, "screen grab failed"); + PyErr_SetString(PyExc_OSError, "screen grab failed"); DeleteDC(screen_copy); DeleteDC(screen); @@ -379,11 +421,11 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) return NULL; } -static BOOL CALLBACK list_windows_callback(HWND hwnd, LPARAM lParam) -{ - PyObject* window_list = (PyObject*) lParam; - PyObject* item; - PyObject* title; +static BOOL CALLBACK +list_windows_callback(HWND hwnd, LPARAM lParam) { + PyObject *window_list = (PyObject *)lParam; + PyObject *item; + PyObject *title; RECT inner, outer; int title_size; int status; @@ -392,45 +434,57 @@ static BOOL CALLBACK list_windows_callback(HWND hwnd, LPARAM lParam) title_size = GetWindowTextLength(hwnd); if (title_size > 0) { title = PyUnicode_FromStringAndSize(NULL, title_size); - if (title) - GetWindowTextW(hwnd, PyUnicode_AS_UNICODE(title), title_size+1); - } else + if (title) { + GetWindowTextW(hwnd, PyUnicode_AS_UNICODE(title), title_size + 1); + } + } else { title = PyUnicode_FromString(""); - if (!title) + } + if (!title) { return 0; + } /* get bounding boxes */ GetClientRect(hwnd, &inner); GetWindowRect(hwnd, &outer); item = Py_BuildValue( - F_HANDLE "N(iiii)(iiii)", hwnd, title, - inner.left, inner.top, inner.right, inner.bottom, - outer.left, outer.top, outer.right, outer.bottom - ); - if (!item) + F_HANDLE "N(iiii)(iiii)", + hwnd, + title, + inner.left, + inner.top, + inner.right, + inner.bottom, + outer.left, + outer.top, + outer.right, + outer.bottom); + if (!item) { return 0; + } status = PyList_Append(window_list, item); Py_DECREF(item); - if (status < 0) + if (status < 0) { return 0; + } return 1; } -PyObject* -PyImaging_ListWindowsWin32(PyObject* self, PyObject* args) -{ - PyObject* window_list; +PyObject * +PyImaging_ListWindowsWin32(PyObject *self, PyObject *args) { + PyObject *window_list; window_list = PyList_New(0); - if (!window_list) + if (!window_list) { return NULL; + } - EnumWindows(list_windows_callback, (LPARAM) window_list); + EnumWindows(list_windows_callback, (LPARAM)window_list); if (PyErr_Occurred()) { Py_DECREF(window_list); @@ -443,37 +497,48 @@ PyImaging_ListWindowsWin32(PyObject* self, PyObject* args) /* -------------------------------------------------------------------- */ /* Windows clipboard grabber */ -PyObject* -PyImaging_GrabClipboardWin32(PyObject* self, PyObject* args) -{ +PyObject * +PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) { int clip; - HANDLE handle; + HANDLE handle = NULL; int size; - void* data; - PyObject* result; + void *data; + PyObject *result; + UINT format; + UINT formats[] = {CF_DIB, CF_DIBV5, CF_HDROP, RegisterClipboardFormatA("PNG"), 0}; + LPCSTR format_names[] = {"DIB", "DIB", "file", "png", NULL}; + + if (!OpenClipboard(NULL)) { + PyErr_SetString(PyExc_OSError, "failed to open clipboard"); + return NULL; + } - clip = OpenClipboard(NULL); - /* FIXME: check error status */ + // find best format as set by clipboard owner + format = 0; + while (!handle && (format = EnumClipboardFormats(format))) { + for (UINT i = 0; formats[i] != 0; i++) { + if (format == formats[i]) { + handle = GetClipboardData(format); + format = i; + break; + } + } + } - handle = GetClipboardData(CF_DIB); if (!handle) { - /* FIXME: add CF_HDROP support to allow cut-and-paste from - the explorer */ CloseClipboard(); - Py_INCREF(Py_None); - return Py_None; + return Py_BuildValue("zO", NULL, Py_None); } - size = GlobalSize(handle); data = GlobalLock(handle); + size = GlobalSize(handle); result = PyBytes_FromStringAndSize(data, size); GlobalUnlock(handle); - CloseClipboard(); - return result; + return Py_BuildValue("zN", format_names[format], result); } /* -------------------------------------------------------------------- */ @@ -486,15 +551,14 @@ PyImaging_GrabClipboardWin32(PyObject* self, PyObject* args) static int mainloop = 0; static void -callback_error(const char* handler) -{ - PyObject* sys_stderr; +callback_error(const char *handler) { + PyObject *sys_stderr; sys_stderr = PySys_GetObject("stderr"); if (sys_stderr) { PyFile_WriteString("*** ImageWin: error in ", sys_stderr); - PyFile_WriteString((char*) handler, sys_stderr); + PyFile_WriteString((char *)handler, sys_stderr); PyFile_WriteString(":\n", sys_stderr); } @@ -503,103 +567,119 @@ callback_error(const char* handler) } static LRESULT CALLBACK -windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) -{ +windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; - PyObject* callback = NULL; - PyObject* result; - PyThreadState* threadstate; - PyThreadState* current_threadstate; + PyObject *callback = NULL; + PyObject *result; + PyThreadState *threadstate; + PyThreadState *current_threadstate; HDC dc; RECT rect; LRESULT status = 0; /* set up threadstate for messages that calls back into python */ switch (message) { - case WM_CREATE: - mainloop++; - break; - case WM_DESTROY: - mainloop--; - /* fall through... */ - case WM_PAINT: - case WM_SIZE: - callback = (PyObject*) GetWindowLongPtr(wnd, 0); - if (callback) { - threadstate = (PyThreadState*) - GetWindowLongPtr(wnd, sizeof(PyObject*)); - current_threadstate = PyThreadState_Swap(NULL); - PyEval_RestoreThread(threadstate); - } else - return DefWindowProc(wnd, message, wParam, lParam); + case WM_CREATE: + mainloop++; + break; + case WM_DESTROY: + mainloop--; + /* fall through... */ + case WM_PAINT: + case WM_SIZE: + callback = (PyObject *)GetWindowLongPtr(wnd, 0); + if (callback) { + threadstate = + (PyThreadState *)GetWindowLongPtr(wnd, sizeof(PyObject *)); + current_threadstate = PyThreadState_Swap(NULL); + PyEval_RestoreThread(threadstate); + } else { + return DefWindowProc(wnd, message, wParam, lParam); + } } /* process message */ switch (message) { - - case WM_PAINT: - /* redraw (part of) window. this generates a WCK-style - damage/clear/repair cascade */ - BeginPaint(wnd, &ps); - dc = GetDC(wnd); - GetWindowRect(wnd, &rect); /* in screen coordinates */ - - result = PyObject_CallFunction( - callback, "siiii", "damage", - ps.rcPaint.left, ps.rcPaint.top, - ps.rcPaint.right, ps.rcPaint.bottom - ); - if (result) - Py_DECREF(result); - else - callback_error("window damage callback"); - - result = PyObject_CallFunction( - callback, "s" F_HANDLE "iiii", "clear", dc, - 0, 0, rect.right-rect.left, rect.bottom-rect.top - ); - if (result) - Py_DECREF(result); - else - callback_error("window clear callback"); - - result = PyObject_CallFunction( - callback, "s" F_HANDLE "iiii", "repair", dc, - 0, 0, rect.right-rect.left, rect.bottom-rect.top - ); - if (result) - Py_DECREF(result); - else - callback_error("window repair callback"); - - ReleaseDC(wnd, dc); - EndPaint(wnd, &ps); - break; - - case WM_SIZE: - /* resize window */ - result = PyObject_CallFunction( - callback, "sii", "resize", LOWORD(lParam), HIWORD(lParam) - ); - if (result) { - InvalidateRect(wnd, NULL, 1); - Py_DECREF(result); - } else - callback_error("window resize callback"); - break; - - case WM_DESTROY: - /* destroy window */ - result = PyObject_CallFunction(callback, "s", "destroy"); - if (result) - Py_DECREF(result); - else - callback_error("window destroy callback"); - Py_DECREF(callback); - break; - - default: - status = DefWindowProc(wnd, message, wParam, lParam); + case WM_PAINT: + /* redraw (part of) window. this generates a WCK-style + damage/clear/repair cascade */ + BeginPaint(wnd, &ps); + dc = GetDC(wnd); + GetWindowRect(wnd, &rect); /* in screen coordinates */ + + result = PyObject_CallFunction( + callback, + "siiii", + "damage", + ps.rcPaint.left, + ps.rcPaint.top, + ps.rcPaint.right, + ps.rcPaint.bottom); + if (result) { + Py_DECREF(result); + } else { + callback_error("window damage callback"); + } + + result = PyObject_CallFunction( + callback, + "s" F_HANDLE "iiii", + "clear", + dc, + 0, + 0, + rect.right - rect.left, + rect.bottom - rect.top); + if (result) { + Py_DECREF(result); + } else { + callback_error("window clear callback"); + } + + result = PyObject_CallFunction( + callback, + "s" F_HANDLE "iiii", + "repair", + dc, + 0, + 0, + rect.right - rect.left, + rect.bottom - rect.top); + if (result) { + Py_DECREF(result); + } else { + callback_error("window repair callback"); + } + + ReleaseDC(wnd, dc); + EndPaint(wnd, &ps); + break; + + case WM_SIZE: + /* resize window */ + result = PyObject_CallFunction( + callback, "sii", "resize", LOWORD(lParam), HIWORD(lParam)); + if (result) { + InvalidateRect(wnd, NULL, 1); + Py_DECREF(result); + } else { + callback_error("window resize callback"); + } + break; + + case WM_DESTROY: + /* destroy window */ + result = PyObject_CallFunction(callback, "s", "destroy"); + if (result) { + Py_DECREF(result); + } else { + callback_error("window destroy callback"); + } + Py_DECREF(callback); + break; + + default: + status = DefWindowProc(wnd, message, wParam, lParam); } if (callback) { @@ -611,27 +691,29 @@ windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) return status; } -PyObject* -PyImaging_CreateWindowWin32(PyObject* self, PyObject* args) -{ +PyObject * +PyImaging_CreateWindowWin32(PyObject *self, PyObject *args) { HWND wnd; WNDCLASS windowClass; - char* title; - PyObject* callback; + char *title; + PyObject *callback; int width = 0, height = 0; - if (!PyArg_ParseTuple(args, "sO|ii", &title, &callback, &width, &height)) - return NULL; + if (!PyArg_ParseTuple(args, "sO|ii", &title, &callback, &width, &height)) { + return NULL; + } - if (width <= 0) + if (width <= 0) { width = CW_USEDEFAULT; - if (height <= 0) + } + if (height <= 0) { height = CW_USEDEFAULT; + } /* register toplevel window class */ windowClass.style = CS_CLASSDC; windowClass.cbClsExtra = 0; - windowClass.cbWndExtra = sizeof(PyObject*) + sizeof(PyThreadState*); + windowClass.cbWndExtra = sizeof(PyObject *) + sizeof(PyThreadState *); windowClass.hInstance = GetModuleHandle(NULL); /* windowClass.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1); */ windowClass.hbrBackground = NULL; @@ -644,92 +726,100 @@ PyImaging_CreateWindowWin32(PyObject* self, PyObject* args) RegisterClass(&windowClass); /* FIXME: check return status */ wnd = CreateWindowEx( - 0, windowClass.lpszClassName, title, + 0, + windowClass.lpszClassName, + title, WS_OVERLAPPEDWINDOW, - CW_USEDEFAULT, CW_USEDEFAULT, width, height, - HWND_DESKTOP, NULL, NULL, NULL - ); + CW_USEDEFAULT, + CW_USEDEFAULT, + width, + height, + HWND_DESKTOP, + NULL, + NULL, + NULL); if (!wnd) { - PyErr_SetString(PyExc_IOError, "failed to create window"); + PyErr_SetString(PyExc_OSError, "failed to create window"); return NULL; } /* register window callback */ Py_INCREF(callback); - SetWindowLongPtr(wnd, 0, (LONG_PTR) callback); - SetWindowLongPtr(wnd, sizeof(callback), (LONG_PTR) PyThreadState_Get()); + SetWindowLongPtr(wnd, 0, (LONG_PTR)callback); + SetWindowLongPtr(wnd, sizeof(callback), (LONG_PTR)PyThreadState_Get()); - Py_BEGIN_ALLOW_THREADS - ShowWindow(wnd, SW_SHOWNORMAL); + Py_BEGIN_ALLOW_THREADS ShowWindow(wnd, SW_SHOWNORMAL); SetForegroundWindow(wnd); /* to make sure it's visible */ Py_END_ALLOW_THREADS - return Py_BuildValue(F_HANDLE, wnd); + return Py_BuildValue(F_HANDLE, wnd); } -PyObject* -PyImaging_EventLoopWin32(PyObject* self, PyObject* args) -{ +PyObject * +PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { MSG msg; - Py_BEGIN_ALLOW_THREADS - while (mainloop && GetMessage(&msg, NULL, 0, 0)) { + Py_BEGIN_ALLOW_THREADS while (mainloop && GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } Py_END_ALLOW_THREADS - Py_INCREF(Py_None); + Py_INCREF(Py_None); return Py_None; } /* -------------------------------------------------------------------- */ /* windows WMF renderer */ -#define GET32(p,o) ((DWORD*)(p+o))[0] +#define GET32(p, o) ((DWORD *)(p + o))[0] PyObject * -PyImaging_DrawWmf(PyObject* self, PyObject* args) -{ +PyImaging_DrawWmf(PyObject *self, PyObject *args) { HBITMAP bitmap; HENHMETAFILE meta; BITMAPCOREHEADER core; HDC dc; RECT rect; - PyObject* buffer = NULL; - char* ptr; + PyObject *buffer = NULL; + char *ptr; - char* data; - int datasize; + char *data; + Py_ssize_t datasize; int width, height; int x0, y0, x1, y1; - if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH"(ii)(iiii):_load", &data, &datasize, - &width, &height, &x0, &x1, &y0, &y1)) + if (!PyArg_ParseTuple( + args, + "y#(ii)(iiii):_load", + &data, + &datasize, + &width, + &height, + &x0, + &x1, + &y0, + &y1)) { return NULL; + } /* step 1: copy metafile contents into METAFILE object */ if (datasize > 22 && GET32(data, 0) == 0x9ac6cdd7) { - /* placeable windows metafile (22-byte aldus header) */ - meta = SetWinMetaFileBits(datasize-22, data+22, NULL, NULL); - - } else if (datasize > 80 && GET32(data, 0) == 1 && - GET32(data, 40) == 0x464d4520) { + meta = SetWinMetaFileBits(datasize - 22, data + 22, NULL, NULL); + } else if (datasize > 80 && GET32(data, 0) == 1 && GET32(data, 40) == 0x464d4520) { /* enhanced metafile */ meta = SetEnhMetaFileBits(datasize, data); } else { - /* unknown meta format */ meta = NULL; - } if (!meta) { - PyErr_SetString(PyExc_IOError, "cannot load metafile"); + PyErr_SetString(PyExc_OSError, "cannot load metafile"); return NULL; } @@ -743,17 +833,15 @@ PyImaging_DrawWmf(PyObject* self, PyObject* args) dc = CreateCompatibleDC(NULL); - bitmap = CreateDIBSection( - dc, (BITMAPINFO*) &core, DIB_RGB_COLORS, &ptr, NULL, 0 - ); + bitmap = CreateDIBSection(dc, (BITMAPINFO *)&core, DIB_RGB_COLORS, &ptr, NULL, 0); if (!bitmap) { - PyErr_SetString(PyExc_IOError, "cannot create bitmap"); + PyErr_SetString(PyExc_OSError, "cannot create bitmap"); goto error; } if (!SelectObject(dc, bitmap)) { - PyErr_SetString(PyExc_IOError, "cannot select bitmap"); + PyErr_SetString(PyExc_OSError, "cannot select bitmap"); goto error; } @@ -767,7 +855,7 @@ PyImaging_DrawWmf(PyObject* self, PyObject* args) FillRect(dc, &rect, GetStockObject(WHITE_BRUSH)); if (!PlayEnhMetaFile(dc, meta, &rect)) { - PyErr_SetString(PyExc_IOError, "cannot render metafile"); + PyErr_SetString(PyExc_OSError, "cannot render metafile"); goto error; } @@ -775,13 +863,14 @@ PyImaging_DrawWmf(PyObject* self, PyObject* args) GdiFlush(); - buffer = PyBytes_FromStringAndSize(ptr, height * ((width*3 + 3) & -4)); + buffer = PyBytes_FromStringAndSize(ptr, height * ((width * 3 + 3) & -4)); error: DeleteEnhMetaFile(meta); - if (bitmap) + if (bitmap) { DeleteObject(bitmap); + } DeleteDC(dc); @@ -789,3 +878,105 @@ PyImaging_DrawWmf(PyObject* self, PyObject* args) } #endif /* _WIN32 */ + +/* -------------------------------------------------------------------- */ +/* X11 support */ + +#ifdef HAVE_XCB +#include + +/* -------------------------------------------------------------------- */ +/* X11 screen grabber */ + +PyObject * +PyImaging_GrabScreenX11(PyObject *self, PyObject *args) { + int width, height; + char *display_name; + xcb_connection_t *connection; + int screen_number; + xcb_screen_iterator_t iter; + xcb_screen_t *screen = NULL; + xcb_get_image_reply_t *reply; + xcb_generic_error_t *error; + PyObject *buffer = NULL; + + if (!PyArg_ParseTuple(args, "|z", &display_name)) { + return NULL; + } + + /* connect to X and get screen data */ + + connection = xcb_connect(display_name, &screen_number); + if (xcb_connection_has_error(connection)) { + PyErr_Format( + PyExc_OSError, + "X connection failed: error %i", + xcb_connection_has_error(connection)); + xcb_disconnect(connection); + return NULL; + } + + iter = xcb_setup_roots_iterator(xcb_get_setup(connection)); + for (; iter.rem; --screen_number, xcb_screen_next(&iter)) { + if (screen_number == 0) { + screen = iter.data; + break; + } + } + if (screen == NULL || screen->root == 0) { + // this case is usually caught with "X connection failed: error 6" above + xcb_disconnect(connection); + PyErr_SetString(PyExc_OSError, "X screen not found"); + return NULL; + } + + width = screen->width_in_pixels; + height = screen->height_in_pixels; + + /* get image data */ + + reply = xcb_get_image_reply( + connection, + xcb_get_image( + connection, + XCB_IMAGE_FORMAT_Z_PIXMAP, + screen->root, + 0, + 0, + width, + height, + 0x00ffffff), + &error); + if (reply == NULL) { + PyErr_Format( + PyExc_OSError, + "X get_image failed: error %i (%i, %i, %i)", + error->error_code, + error->major_code, + error->minor_code, + error->resource_id); + free(error); + xcb_disconnect(connection); + return NULL; + } + + /* store data in Python buffer */ + + if (reply->depth == 24) { + buffer = PyBytes_FromStringAndSize( + (char *)xcb_get_image_data(reply), xcb_get_image_data_length(reply)); + } else { + PyErr_Format(PyExc_OSError, "unsupported bit depth: %i", reply->depth); + } + + free(reply); + xcb_disconnect(connection); + + if (!buffer) { + return NULL; + } + + return Py_BuildValue("(ii)N", width, height, buffer); +} + +#endif /* HAVE_XCB */ diff --git a/src/encode.c b/src/encode.c index ae4277c047d..72c7f64d0a3 100644 --- a/src/encode.c +++ b/src/encode.c @@ -22,11 +22,11 @@ /* FIXME: make these pluggable! */ +#define PY_SSIZE_T_CLEAN #include "Python.h" -#include "Imaging.h" -#include "py3.h" -#include "Gif.h" +#include "libImaging/Imaging.h" +#include "libImaging/Gif.h" #ifdef HAVE_UNISTD_H #include /* write */ @@ -37,44 +37,45 @@ /* -------------------------------------------------------------------- */ typedef struct { - PyObject_HEAD - int (*encode)(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); + PyObject_HEAD int (*encode)( + Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); int (*cleanup)(ImagingCodecState state); struct ImagingCodecStateInstance state; Imaging im; - PyObject* lock; + PyObject *lock; int pushes_fd; } ImagingEncoderObject; static PyTypeObject ImagingEncoderType; -static ImagingEncoderObject* -PyImaging_EncoderNew(int contextsize) -{ +static ImagingEncoderObject * +PyImaging_EncoderNew(int contextsize) { ImagingEncoderObject *encoder; void *context; - if(PyType_Ready(&ImagingEncoderType) < 0) + if (PyType_Ready(&ImagingEncoderType) < 0) { return NULL; + } encoder = PyObject_New(ImagingEncoderObject, &ImagingEncoderType); - if (encoder == NULL) + if (encoder == NULL) { return NULL; + } /* Clear the encoder state */ memset(&encoder->state, 0, sizeof(encoder->state)); /* Allocate encoder context */ if (contextsize > 0) { - context = (void*) calloc(1, contextsize); + context = (void *)calloc(1, contextsize); if (!context) { Py_DECREF(encoder); - (void) PyErr_NoMemory(); + (void)ImagingError_MemoryError(); return NULL; } - } else + } else { context = 0; + } /* Initialize encoder context */ encoder->state.context = context; @@ -91,10 +92,10 @@ PyImaging_EncoderNew(int contextsize) } static void -_dealloc(ImagingEncoderObject* encoder) -{ - if (encoder->cleanup) +_dealloc(ImagingEncoderObject *encoder) { + if (encoder->cleanup) { encoder->cleanup(&encoder->state); + } free(encoder->state.buffer); free(encoder->state.context); Py_XDECREF(encoder->lock); @@ -102,42 +103,43 @@ _dealloc(ImagingEncoderObject* encoder) PyObject_Del(encoder); } -static PyObject* -_encode_cleanup(ImagingEncoderObject* encoder, PyObject* args) -{ +static PyObject * +_encode_cleanup(ImagingEncoderObject *encoder, PyObject *args) { int status = 0; - if (encoder->cleanup){ + if (encoder->cleanup) { status = encoder->cleanup(&encoder->state); } return Py_BuildValue("i", status); } -static PyObject* -_encode(ImagingEncoderObject* encoder, PyObject* args) -{ - PyObject* buf; - PyObject* result; +static PyObject * +_encode(ImagingEncoderObject *encoder, PyObject *args) { + PyObject *buf; + PyObject *result; int status; /* Encode to a Python string (allocated by this method) */ - int bufsize = 16384; + Py_ssize_t bufsize = 16384; - if (!PyArg_ParseTuple(args, "|i", &bufsize)) + if (!PyArg_ParseTuple(args, "|n", &bufsize)) { return NULL; + } buf = PyBytes_FromStringAndSize(NULL, bufsize); - if (!buf) + if (!buf) { return NULL; + } - status = encoder->encode(encoder->im, &encoder->state, - (UINT8*) PyBytes_AsString(buf), bufsize); + status = encoder->encode( + encoder->im, &encoder->state, (UINT8 *)PyBytes_AsString(buf), bufsize); /* adjust string length to avoid slicing in encoder */ - if (_PyBytes_Resize(&buf, (status > 0) ? status : 0) < 0) + if (_PyBytes_Resize(&buf, (status > 0) ? status : 0) < 0) { return NULL; + } result = Py_BuildValue("iiO", status, encoder->state.errcode, buf); @@ -146,63 +148,61 @@ _encode(ImagingEncoderObject* encoder, PyObject* args) return result; } -static PyObject* -_encode_to_pyfd(ImagingEncoderObject* encoder, PyObject* args) -{ - +static PyObject * +_encode_to_pyfd(ImagingEncoderObject *encoder) { PyObject *result; int status; if (!encoder->pushes_fd) { // UNDONE, appropriate errcode??? - result = Py_BuildValue("ii", 0, IMAGING_CODEC_CONFIG);; + result = Py_BuildValue("ii", 0, IMAGING_CODEC_CONFIG); return result; } - status = encoder->encode(encoder->im, &encoder->state, - (UINT8*) NULL, 0); + status = encoder->encode(encoder->im, &encoder->state, (UINT8 *)NULL, 0); result = Py_BuildValue("ii", status, encoder->state.errcode); return result; } -static PyObject* -_encode_to_file(ImagingEncoderObject* encoder, PyObject* args) -{ - UINT8* buf; +static PyObject * +_encode_to_file(ImagingEncoderObject *encoder, PyObject *args) { + UINT8 *buf; int status; ImagingSectionCookie cookie; /* Encode to a file handle */ - int fh; - int bufsize = 16384; + Py_ssize_t fh; + Py_ssize_t bufsize = 16384; - if (!PyArg_ParseTuple(args, "i|i", &fh, &bufsize)) + if (!PyArg_ParseTuple(args, "n|n", &fh, &bufsize)) { return NULL; + } /* Allocate an encoder buffer */ /* malloc check ok, either constant int, or checked by PyArg_ParseTuple */ - buf = (UINT8*) malloc(bufsize); - if (!buf) - return PyErr_NoMemory(); + buf = (UINT8 *)malloc(bufsize); + if (!buf) { + return ImagingError_MemoryError(); + } ImagingSectionEnter(&cookie); do { - /* This replaces the inner loop in the ImageFile _save function. */ status = encoder->encode(encoder->im, &encoder->state, buf, bufsize); - if (status > 0) + if (status > 0) { if (write(fh, buf, status) < 0) { ImagingSectionLeave(&cookie); free(buf); - return PyErr_SetFromErrno(PyExc_IOError); + return PyErr_SetFromErrno(PyExc_OSError); } + } } while (encoder->state.errcode == 0); @@ -213,26 +213,28 @@ _encode_to_file(ImagingEncoderObject* encoder, PyObject* args) return Py_BuildValue("i", encoder->state.errcode); } -extern Imaging PyImaging_AsImaging(PyObject *op); +extern Imaging +PyImaging_AsImaging(PyObject *op); -static PyObject* -_setimage(ImagingEncoderObject* encoder, PyObject* args) -{ - PyObject* op; +static PyObject * +_setimage(ImagingEncoderObject *encoder, PyObject *args) { + PyObject *op; Imaging im; ImagingCodecState state; - int x0, y0, x1, y1; + Py_ssize_t x0, y0, x1, y1; /* Define where image data should be stored */ x0 = y0 = x1 = y1 = 0; /* FIXME: should publish the ImagingType descriptor */ - if (!PyArg_ParseTuple(args, "O|(iiii)", &op, &x0, &y0, &x1, &y1)) + if (!PyArg_ParseTuple(args, "O|(nnnn)", &op, &x0, &y0, &x1, &y1)) { return NULL; + } im = PyImaging_AsImaging(op); - if (!im) + if (!im) { return NULL; + } encoder->im = im; @@ -248,24 +250,23 @@ _setimage(ImagingEncoderObject* encoder, PyObject* args) state->ysize = y1 - y0; } - if (state->xsize <= 0 || - state->xsize + state->xoff > im->xsize || - state->ysize <= 0 || - state->ysize + state->yoff > im->ysize) { + if (state->xsize <= 0 || state->xsize + state->xoff > im->xsize || + state->ysize <= 0 || state->ysize + state->yoff > im->ysize) { PyErr_SetString(PyExc_SystemError, "tile cannot extend outside image"); return NULL; } /* Allocate memory buffer (if bits field is set) */ if (state->bits > 0) { - if (state->xsize > ((INT_MAX / state->bits)-7)) { - return PyErr_NoMemory(); + if (state->xsize > ((INT_MAX / state->bits) - 7)) { + return ImagingError_MemoryError(); } - state->bytes = (state->bits * state->xsize+7)/8; + state->bytes = (state->bits * state->xsize + 7) / 8; /* malloc check ok, overflow checked above */ - state->buffer = (UINT8*) malloc(state->bytes); - if (!state->buffer) - return PyErr_NoMemory(); + state->buffer = (UINT8 *)calloc(1, state->bytes); + if (!state->buffer) { + return ImagingError_MemoryError(); + } } /* Keep a reference to the image object, to make sure it doesn't @@ -278,14 +279,14 @@ _setimage(ImagingEncoderObject* encoder, PyObject* args) return Py_None; } -static PyObject* -_setfd(ImagingEncoderObject* encoder, PyObject* args) -{ - PyObject* fd; +static PyObject * +_setfd(ImagingEncoderObject *encoder, PyObject *args) { + PyObject *fd; ImagingCodecState state; - if (!PyArg_ParseTuple(args, "O", &fd)) + if (!PyArg_ParseTuple(args, "O", &fd)) { return NULL; + } state = &encoder->state; @@ -297,68 +298,66 @@ _setfd(ImagingEncoderObject* encoder, PyObject* args) } static PyObject * -_get_pushes_fd(ImagingEncoderObject *encoder) -{ +_get_pushes_fd(ImagingEncoderObject *encoder, void *closure) { return PyBool_FromLong(encoder->pushes_fd); } static struct PyMethodDef methods[] = { - {"encode", (PyCFunction)_encode, 1}, - {"cleanup", (PyCFunction)_encode_cleanup, 1}, - {"encode_to_file", (PyCFunction)_encode_to_file, 1}, - {"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, 1}, - {"setimage", (PyCFunction)_setimage, 1}, - {"setfd", (PyCFunction)_setfd, 1}, + {"encode", (PyCFunction)_encode, METH_VARARGS}, + {"cleanup", (PyCFunction)_encode_cleanup, METH_VARARGS}, + {"encode_to_file", (PyCFunction)_encode_to_file, METH_VARARGS}, + {"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, METH_NOARGS}, + {"setimage", (PyCFunction)_setimage, METH_VARARGS}, + {"setfd", (PyCFunction)_setfd, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; static struct PyGetSetDef getseters[] = { - {"pushes_fd", (getter)_get_pushes_fd, NULL, + {"pushes_fd", + (getter)_get_pushes_fd, + NULL, "True if this decoder expects to push directly to self.fd", NULL}, {NULL, NULL, NULL, NULL, NULL} /* sentinel */ }; static PyTypeObject ImagingEncoderType = { - PyVarObject_HEAD_INIT(NULL, 0) - "ImagingEncoder", /*tp_name*/ - sizeof(ImagingEncoderObject), /*tp_size*/ - 0, /*tp_itemsize*/ + PyVarObject_HEAD_INIT(NULL, 0) "ImagingEncoder", /*tp_name*/ + sizeof(ImagingEncoderObject), /*tp_size*/ + 0, /*tp_itemsize*/ /* methods */ - (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - getseters, /*tp_getset*/ + (destructor)_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + methods, /*tp_methods*/ + 0, /*tp_members*/ + getseters, /*tp_getset*/ }; /* -------------------------------------------------------------------- */ int -get_packer(ImagingEncoderObject* encoder, const char* mode, - const char* rawmode) -{ +get_packer(ImagingEncoderObject *encoder, const char *mode, const char *rawmode) { int bits; ImagingShuffler pack; @@ -375,72 +374,70 @@ get_packer(ImagingEncoderObject* encoder, const char* mode, return 0; } - /* -------------------------------------------------------------------- */ /* EPS */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_EpsEncoderNew(PyObject* self, PyObject* args) -{ - ImagingEncoderObject* encoder; +PyObject * +PyImaging_EpsEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; encoder = PyImaging_EncoderNew(0); - if (encoder == NULL) + if (encoder == NULL) { return NULL; + } encoder->encode = ImagingEpsEncode; - return (PyObject*) encoder; + return (PyObject *)encoder; } - /* -------------------------------------------------------------------- */ /* GIF */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_GifEncoderNew(PyObject* self, PyObject* args) -{ - ImagingEncoderObject* encoder; +PyObject * +PyImaging_GifEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; char *mode; char *rawmode; - int bits = 8; - int interlace = 0; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &bits, &interlace)) + Py_ssize_t bits = 8; + Py_ssize_t interlace = 0; + if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &bits, &interlace)) { return NULL; + } encoder = PyImaging_EncoderNew(sizeof(GIFENCODERSTATE)); - if (encoder == NULL) + if (encoder == NULL) { return NULL; + } - if (get_packer(encoder, mode, rawmode) < 0) + if (get_packer(encoder, mode, rawmode) < 0) { return NULL; + } encoder->encode = ImagingGifEncode; - ((GIFENCODERSTATE*)encoder->state.context)->bits = bits; - ((GIFENCODERSTATE*)encoder->state.context)->interlace = interlace; + ((GIFENCODERSTATE *)encoder->state.context)->bits = bits; + ((GIFENCODERSTATE *)encoder->state.context)->interlace = interlace; - return (PyObject*) encoder; + return (PyObject *)encoder; } - /* -------------------------------------------------------------------- */ /* PCX */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_PcxEncoderNew(PyObject* self, PyObject* args) -{ - ImagingEncoderObject* encoder; +PyObject * +PyImaging_PcxEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; char *mode; char *rawmode; - int bits = 8; + Py_ssize_t bits = 8; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &bits)) { + if (!PyArg_ParseTuple(args, "ss|n", &mode, &rawmode, &bits)) { return NULL; } @@ -455,126 +452,501 @@ PyImaging_PcxEncoderNew(PyObject* self, PyObject* args) encoder->encode = ImagingPcxEncode; - return (PyObject*) encoder; + return (PyObject *)encoder; } - /* -------------------------------------------------------------------- */ /* RAW */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_RawEncoderNew(PyObject* self, PyObject* args) -{ - ImagingEncoderObject* encoder; +PyObject * +PyImaging_RawEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; char *mode; char *rawmode; - int stride = 0; - int ystep = 1; + Py_ssize_t stride = 0; + Py_ssize_t ystep = 1; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &stride, &ystep)) + if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &stride, &ystep)) { return NULL; + } encoder = PyImaging_EncoderNew(0); - if (encoder == NULL) + if (encoder == NULL) { return NULL; + } - if (get_packer(encoder, mode, rawmode) < 0) + if (get_packer(encoder, mode, rawmode) < 0) { return NULL; + } encoder->encode = ImagingRawEncode; encoder->state.ystep = ystep; encoder->state.count = stride; - return (PyObject*) encoder; + return (PyObject *)encoder; } +/* -------------------------------------------------------------------- */ +/* TGA */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_TgaRleEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; + + char *mode; + char *rawmode; + Py_ssize_t ystep = 1; + + if (!PyArg_ParseTuple(args, "ss|n", &mode, &rawmode, &ystep)) { + return NULL; + } + + encoder = PyImaging_EncoderNew(0); + if (encoder == NULL) { + return NULL; + } + + if (get_packer(encoder, mode, rawmode) < 0) { + return NULL; + } + + encoder->encode = ImagingTgaRleEncode; + + encoder->state.ystep = ystep; + + return (PyObject *)encoder; +} /* -------------------------------------------------------------------- */ /* XBM */ /* -------------------------------------------------------------------- */ -PyObject* -PyImaging_XbmEncoderNew(PyObject* self, PyObject* args) -{ - ImagingEncoderObject* encoder; +PyObject * +PyImaging_XbmEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; encoder = PyImaging_EncoderNew(0); - if (encoder == NULL) + if (encoder == NULL) { return NULL; + } - if (get_packer(encoder, "1", "1;R") < 0) + if (get_packer(encoder, "1", "1;R") < 0) { return NULL; + } encoder->encode = ImagingXbmEncode; - return (PyObject*) encoder; + return (PyObject *)encoder; } - /* -------------------------------------------------------------------- */ /* ZIP */ /* -------------------------------------------------------------------- */ #ifdef HAVE_LIBZ -#include "Zip.h" - -PyObject* -PyImaging_ZipEncoderNew(PyObject* self, PyObject* args) -{ - ImagingEncoderObject* encoder; - - char* mode; - char* rawmode; - int optimize = 0; - int compress_level = -1; - int compress_type = -1; - char* dictionary = NULL; - int dictionary_size = 0; - if (!PyArg_ParseTuple(args, "ss|iii"PY_ARG_BYTES_LENGTH, &mode, &rawmode, - &optimize, - &compress_level, &compress_type, - &dictionary, &dictionary_size)) +#include "libImaging/ZipCodecs.h" + +PyObject * +PyImaging_ZipEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; + + char *mode; + char *rawmode; + Py_ssize_t optimize = 0; + Py_ssize_t compress_level = -1; + Py_ssize_t compress_type = -1; + char *dictionary = NULL; + Py_ssize_t dictionary_size = 0; + if (!PyArg_ParseTuple( + args, + "ss|nnny#", + &mode, + &rawmode, + &optimize, + &compress_level, + &compress_type, + &dictionary, + &dictionary_size)) { return NULL; + } /* Copy to avoid referencing Python's memory */ if (dictionary && dictionary_size > 0) { /* malloc check ok, size comes from PyArg_ParseTuple */ - char* p = malloc(dictionary_size); - if (!p) - return PyErr_NoMemory(); + char *p = malloc(dictionary_size); + if (!p) { + return ImagingError_MemoryError(); + } memcpy(p, dictionary, dictionary_size); dictionary = p; - } else + } else { dictionary = NULL; + } encoder = PyImaging_EncoderNew(sizeof(ZIPSTATE)); - if (encoder == NULL) + if (encoder == NULL) { + free(dictionary); return NULL; + } - if (get_packer(encoder, mode, rawmode) < 0) + if (get_packer(encoder, mode, rawmode) < 0) { + free(dictionary); return NULL; + } encoder->encode = ImagingZipEncode; encoder->cleanup = ImagingZipEncodeCleanup; - if (rawmode[0] == 'P') + if (rawmode[0] == 'P') { /* disable filtering */ - ((ZIPSTATE*)encoder->state.context)->mode = ZIP_PNG_PALETTE; + ((ZIPSTATE *)encoder->state.context)->mode = ZIP_PNG_PALETTE; + } - ((ZIPSTATE*)encoder->state.context)->optimize = optimize; - ((ZIPSTATE*)encoder->state.context)->compress_level = compress_level; - ((ZIPSTATE*)encoder->state.context)->compress_type = compress_type; - ((ZIPSTATE*)encoder->state.context)->dictionary = dictionary; - ((ZIPSTATE*)encoder->state.context)->dictionary_size = dictionary_size; + ((ZIPSTATE *)encoder->state.context)->optimize = optimize; + ((ZIPSTATE *)encoder->state.context)->compress_level = compress_level; + ((ZIPSTATE *)encoder->state.context)->compress_type = compress_type; + ((ZIPSTATE *)encoder->state.context)->dictionary = dictionary; + ((ZIPSTATE *)encoder->state.context)->dictionary_size = dictionary_size; - return (PyObject*) encoder; + return (PyObject *)encoder; } #endif +/* -------------------------------------------------------------------- */ +/* LibTiff */ +/* -------------------------------------------------------------------- */ + +#ifdef HAVE_LIBTIFF + +#include "libImaging/TiffDecode.h" + +#include + +PyObject * +PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; + + char *mode; + char *rawmode; + char *compname; + char *filename; + Py_ssize_t fp; + + PyObject *tags, *types; + PyObject *key, *value; + Py_ssize_t pos = 0; + int key_int, status, is_core_tag, is_var_length, num_core_tags, i; + TIFFDataType type = TIFF_NOTYPE; + // This list also exists in TiffTags.py + const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274, + 277, 278, 280, 281, 340, 341, 282, 283, 284, + 286, 287, 296, 297, 320, 321, 338, 32995, 32998, + 32996, 339, 32997, 330, 531, 530, 65537, 301, 532}; + + Py_ssize_t tags_size; + PyObject *item; + + if (!PyArg_ParseTuple( + args, + "sssnsOO", + &mode, + &rawmode, + &compname, + &fp, + &filename, + &tags, + &types)) { + return NULL; + } + + if (!PyList_Check(tags)) { + PyErr_SetString(PyExc_ValueError, "Invalid tags list"); + return NULL; + } else { + tags_size = PyList_Size(tags); + TRACE(("tags size: %d\n", (int)tags_size)); + for (pos = 0; pos < tags_size; pos++) { + item = PyList_GetItem(tags, pos); + if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) { + PyErr_SetString(PyExc_ValueError, "Invalid tags list"); + return NULL; + } + } + pos = 0; + } + if (!PyDict_Check(types)) { + PyErr_SetString(PyExc_ValueError, "Invalid types dictionary"); + return NULL; + } + + TRACE(("new tiff encoder %s fp: %d, filename: %s \n", compname, fp, filename)); + + encoder = PyImaging_EncoderNew(sizeof(TIFFSTATE)); + if (encoder == NULL) { + return NULL; + } + + if (get_packer(encoder, mode, rawmode) < 0) { + return NULL; + } + + if (!ImagingLibTiffEncodeInit(&encoder->state, filename, fp)) { + Py_DECREF(encoder); + PyErr_SetString(PyExc_RuntimeError, "tiff codec initialization failed"); + return NULL; + } + + num_core_tags = sizeof(core_tags) / sizeof(int); + for (pos = 0; pos < tags_size; pos++) { + item = PyList_GetItem(tags, pos); + // We already checked that tags is a 2-tuple list. + key = PyTuple_GetItem(item, 0); + key_int = (int)PyLong_AsLong(key); + value = PyTuple_GetItem(item, 1); + status = 0; + is_core_tag = 0; + is_var_length = 0; + type = TIFF_NOTYPE; + + for (i = 0; i < num_core_tags; i++) { + if (core_tags[i] == key_int) { + is_core_tag = 1; + break; + } + } + + if (!is_core_tag) { + PyObject *tag_type = PyDict_GetItem(types, key); + if (tag_type) { + int type_int = PyLong_AsLong(tag_type); + if (type_int >= TIFF_BYTE && type_int <= TIFF_DOUBLE) { + type = (TIFFDataType)type_int; + } + } + } + + if (type == TIFF_NOTYPE) { + // Autodetect type. Types should not be changed for backwards + // compatibility. + if (PyLong_Check(value)) { + type = TIFF_LONG; + } else if (PyFloat_Check(value)) { + type = TIFF_DOUBLE; + } else if (PyBytes_Check(value)) { + type = TIFF_ASCII; + } + } + + if (PyTuple_Check(value)) { + Py_ssize_t len; + len = PyTuple_Size(value); + + is_var_length = 1; + + if (!len) { + continue; + } + + if (type == TIFF_NOTYPE) { + // Autodetect type based on first item. Types should not be + // changed for backwards compatibility. + if (PyLong_Check(PyTuple_GetItem(value, 0))) { + type = TIFF_LONG; + } else if (PyFloat_Check(PyTuple_GetItem(value, 0))) { + type = TIFF_FLOAT; + } + } + } + + if (!is_core_tag) { + // Register field for non core tags. + if (type == TIFF_BYTE) { + is_var_length = 1; + } + if (ImagingLibTiffMergeFieldInfo( + &encoder->state, type, key_int, is_var_length)) { + continue; + } + } + + if (type == TIFF_BYTE || type == TIFF_UNDEFINED) { + status = ImagingLibTiffSetField( + &encoder->state, + (ttag_t)key_int, + PyBytes_Size(value), + PyBytes_AsString(value)); + } else if (is_var_length) { + Py_ssize_t len, i; + TRACE(("Setting from Tuple: %d \n", key_int)); + len = PyTuple_Size(value); + + if (key_int == TIFFTAG_COLORMAP) { + int stride = 256; + if (len != 768) { + PyErr_SetString( + PyExc_ValueError, "Requiring 768 items for Colormap"); + return NULL; + } + UINT16 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(UINT16)); + if (av) { + for (i = 0; i < len; i++) { + av[i] = (UINT16)PyLong_AsLong(PyTuple_GetItem(value, i)); + } + status = ImagingLibTiffSetField( + &encoder->state, + (ttag_t)key_int, + av, + av + stride, + av + stride * 2); + free(av); + } + } else if (key_int == TIFFTAG_YCBCRSUBSAMPLING) { + status = ImagingLibTiffSetField( + &encoder->state, + (ttag_t)key_int, + (UINT16)PyLong_AsLong(PyTuple_GetItem(value, 0)), + (UINT16)PyLong_AsLong(PyTuple_GetItem(value, 1))); + } else if (type == TIFF_SHORT) { + UINT16 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(UINT16)); + if (av) { + for (i = 0; i < len; i++) { + av[i] = (UINT16)PyLong_AsLong(PyTuple_GetItem(value, i)); + } + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, len, av); + free(av); + } + } else if (type == TIFF_LONG) { + UINT32 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(UINT32)); + if (av) { + for (i = 0; i < len; i++) { + av[i] = (UINT32)PyLong_AsLong(PyTuple_GetItem(value, i)); + } + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, len, av); + free(av); + } + } else if (type == TIFF_SBYTE) { + INT8 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(INT8)); + if (av) { + for (i = 0; i < len; i++) { + av[i] = (INT8)PyLong_AsLong(PyTuple_GetItem(value, i)); + } + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, len, av); + free(av); + } + } else if (type == TIFF_SSHORT) { + INT16 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(INT16)); + if (av) { + for (i = 0; i < len; i++) { + av[i] = (INT16)PyLong_AsLong(PyTuple_GetItem(value, i)); + } + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, len, av); + free(av); + } + } else if (type == TIFF_SLONG) { + INT32 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(INT32)); + if (av) { + for (i = 0; i < len; i++) { + av[i] = (INT32)PyLong_AsLong(PyTuple_GetItem(value, i)); + } + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, len, av); + free(av); + } + } else if (type == TIFF_FLOAT) { + FLOAT32 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(FLOAT32)); + if (av) { + for (i = 0; i < len; i++) { + av[i] = (FLOAT32)PyFloat_AsDouble(PyTuple_GetItem(value, i)); + } + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, len, av); + free(av); + } + } else if (type == TIFF_DOUBLE) { + FLOAT64 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(FLOAT64)); + if (av) { + for (i = 0; i < len; i++) { + av[i] = PyFloat_AsDouble(PyTuple_GetItem(value, i)); + } + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, len, av); + free(av); + } + } + } else { + if (type == TIFF_SHORT) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, (UINT16)PyLong_AsLong(value)); + } else if (type == TIFF_LONG) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, (UINT32)PyLong_AsLong(value)); + } else if (type == TIFF_SSHORT) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, (INT16)PyLong_AsLong(value)); + } else if (type == TIFF_SLONG) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, (INT32)PyLong_AsLong(value)); + } else if (type == TIFF_FLOAT) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, (FLOAT32)PyFloat_AsDouble(value)); + } else if (type == TIFF_DOUBLE) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value)); + } else if (type == TIFF_SBYTE) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, (INT8)PyLong_AsLong(value)); + } else if (type == TIFF_ASCII) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, PyBytes_AsString(value)); + } else if (type == TIFF_RATIONAL) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value)); + } else { + TRACE( + ("Unhandled type for key %d : %s \n", + key_int, + PyBytes_AsString(PyObject_Str(value)))); + } + } + if (!status) { + TRACE(("Error setting Field\n")); + Py_DECREF(encoder); + PyErr_SetString(PyExc_RuntimeError, "Error setting from dictionary"); + return NULL; + } + } + + encoder->encode = ImagingLibTiffEncode; + + return (PyObject *)encoder; +} + +#endif /* -------------------------------------------------------------------- */ /* JPEG */ @@ -585,26 +957,27 @@ PyImaging_ZipEncoderNew(PyObject* self, PyObject* args) /* We better define this encoder last in this file, so the following undef's won't mess things up for the Imaging library proper. */ -#undef HAVE_PROTOTYPES -#undef HAVE_STDDEF_H -#undef HAVE_STDLIB_H -#undef UINT8 -#undef UINT16 -#undef UINT32 -#undef INT8 -#undef INT16 -#undef INT32 - -#include "Jpeg.h" - -static unsigned int* get_qtables_arrays(PyObject* qtables, int* qtablesLen) { - PyObject* tables; - PyObject* table; - PyObject* table_data; +#undef HAVE_PROTOTYPES +#undef HAVE_STDDEF_H +#undef HAVE_STDLIB_H +#undef UINT8 +#undef UINT16 +#undef UINT32 +#undef INT8 +#undef INT16 +#undef INT32 + +#include "libImaging/Jpeg.h" + +static unsigned int * +get_qtables_arrays(PyObject *qtables, int *qtablesLen) { + PyObject *tables; + PyObject *table; + PyObject *table_data; int i, j, num_tables; unsigned int *qarrays; - if ((qtables == NULL) || (qtables == Py_None)) { + if ((qtables == NULL) || (qtables == Py_None)) { return NULL; } @@ -616,17 +989,17 @@ static unsigned int* get_qtables_arrays(PyObject* qtables, int* qtablesLen) { tables = PySequence_Fast(qtables, "expected a sequence"); num_tables = PySequence_Size(qtables); if (num_tables < 1 || num_tables > NUM_QUANT_TBLS) { - PyErr_SetString(PyExc_ValueError, + PyErr_SetString( + PyExc_ValueError, "Not a valid number of quantization tables. Should be between 1 and 4."); Py_DECREF(tables); return NULL; } /* malloc check ok, num_tables <4, DCTSIZE2 == 64 from jpeglib.h */ - qarrays = (unsigned int*) malloc(num_tables * DCTSIZE2 * sizeof(unsigned int)); + qarrays = (unsigned int *)malloc(num_tables * DCTSIZE2 * sizeof(unsigned int)); if (!qarrays) { Py_DECREF(tables); - PyErr_NoMemory(); - return NULL; + return ImagingError_MemoryError(); } for (i = 0; i < num_tables; i++) { table = PySequence_Fast_GET_ITEM(tables, i); @@ -640,7 +1013,8 @@ static unsigned int* get_qtables_arrays(PyObject* qtables, int* qtablesLen) { } table_data = PySequence_Fast(table, "expected a sequence"); for (j = 0; j < DCTSIZE2; j++) { - qarrays[i * DCTSIZE2 + j] = PyInt_AS_LONG(PySequence_Fast_GET_ITEM(table_data, j)); + qarrays[i * DCTSIZE2 + j] = + PyLong_AS_LONG(PySequence_Fast_GET_ITEM(table_data, j)); } Py_DECREF(table_data); } @@ -658,38 +1032,52 @@ static unsigned int* get_qtables_arrays(PyObject* qtables, int* qtablesLen) { return qarrays; } -PyObject* -PyImaging_JpegEncoderNew(PyObject* self, PyObject* args) -{ - ImagingEncoderObject* encoder; +PyObject * +PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; char *mode; char *rawmode; - int quality = 0; - int progressive = 0; - int smooth = 0; - int optimize = 0; - int streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */ - int xdpi = 0, ydpi = 0; - int subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */ - PyObject* qtables=NULL; + Py_ssize_t quality = 0; + Py_ssize_t progressive = 0; + Py_ssize_t smooth = 0; + Py_ssize_t optimize = 0; + Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */ + Py_ssize_t xdpi = 0, ydpi = 0; + Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */ + PyObject *qtables = NULL; unsigned int *qarrays = NULL; int qtablesLen = 0; - char* extra = NULL; - int extra_size; - char* rawExif = NULL; - int rawExifLen = 0; - - if (!PyArg_ParseTuple(args, "ss|iiiiiiiiO"PY_ARG_BYTES_LENGTH""PY_ARG_BYTES_LENGTH, - &mode, &rawmode, &quality, - &progressive, &smooth, &optimize, &streamtype, - &xdpi, &ydpi, &subsampling, &qtables, &extra, &extra_size, - &rawExif, &rawExifLen)) + char *extra = NULL; + Py_ssize_t extra_size; + char *rawExif = NULL; + Py_ssize_t rawExifLen = 0; + + if (!PyArg_ParseTuple( + args, + "ss|nnnnnnnnOy#y#", + &mode, + &rawmode, + &quality, + &progressive, + &smooth, + &optimize, + &streamtype, + &xdpi, + &ydpi, + &subsampling, + &qtables, + &extra, + &extra_size, + &rawExif, + &rawExifLen)) { return NULL; + } encoder = PyImaging_EncoderNew(sizeof(JPEGENCODERSTATE)); - if (encoder == NULL) + if (encoder == NULL) { return NULL; + } // libjpeg-turbo supports different output formats. // We are choosing Pillow's native format (3 color bytes + 1 padding) @@ -698,225 +1086,91 @@ PyImaging_JpegEncoderNew(PyObject* self, PyObject* args) rawmode = "RGBX"; } - if (get_packer(encoder, mode, rawmode) < 0) + if (get_packer(encoder, mode, rawmode) < 0) { return NULL; + } // Freed in JpegEncode, Case 5 qarrays = get_qtables_arrays(qtables, &qtablesLen); if (extra && extra_size > 0) { /* malloc check ok, length is from python parsearg */ - char* p = malloc(extra_size); // Freed in JpegEncode, Case 5 - if (!p) - return PyErr_NoMemory(); + char *p = malloc(extra_size); // Freed in JpegEncode, Case 5 + if (!p) { + return ImagingError_MemoryError(); + } memcpy(p, extra, extra_size); extra = p; - } else + } else { extra = NULL; + } if (rawExif && rawExifLen > 0) { /* malloc check ok, length is from python parsearg */ - char* pp = malloc(rawExifLen); // Freed in JpegEncode, Case 5 - if (!pp) - return PyErr_NoMemory(); + char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 5 + if (!pp) { + if (extra) { + free(extra); + } + return ImagingError_MemoryError(); + } memcpy(pp, rawExif, rawExifLen); rawExif = pp; - } else - rawExif = NULL; - - encoder->encode = ImagingJpegEncode; - - strncpy(((JPEGENCODERSTATE*)encoder->state.context)->rawmode, rawmode, 8); - - ((JPEGENCODERSTATE*)encoder->state.context)->quality = quality; - ((JPEGENCODERSTATE*)encoder->state.context)->qtables = qarrays; - ((JPEGENCODERSTATE*)encoder->state.context)->qtablesLen = qtablesLen; - ((JPEGENCODERSTATE*)encoder->state.context)->subsampling = subsampling; - ((JPEGENCODERSTATE*)encoder->state.context)->progressive = progressive; - ((JPEGENCODERSTATE*)encoder->state.context)->smooth = smooth; - ((JPEGENCODERSTATE*)encoder->state.context)->optimize = optimize; - ((JPEGENCODERSTATE*)encoder->state.context)->streamtype = streamtype; - ((JPEGENCODERSTATE*)encoder->state.context)->xdpi = xdpi; - ((JPEGENCODERSTATE*)encoder->state.context)->ydpi = ydpi; - ((JPEGENCODERSTATE*)encoder->state.context)->extra = extra; - ((JPEGENCODERSTATE*)encoder->state.context)->extra_size = extra_size; - ((JPEGENCODERSTATE*)encoder->state.context)->rawExif = rawExif; - ((JPEGENCODERSTATE*)encoder->state.context)->rawExifLen = rawExifLen; - - return (PyObject*) encoder; -} - -#endif - -/* -------------------------------------------------------------------- */ -/* LibTiff */ -/* -------------------------------------------------------------------- */ - -#ifdef HAVE_LIBTIFF - -#include "TiffDecode.h" - -#include - -PyObject* -PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) -{ - ImagingEncoderObject* encoder; - - char* mode; - char* rawmode; - char* compname; - char* filename; - int fp; - - PyObject *dir; - PyObject *key, *value; - Py_ssize_t pos = 0; - int status; - - Py_ssize_t d_size; - PyObject *keys, *values; - - - if (! PyArg_ParseTuple(args, "sssisO", &mode, &rawmode, &compname, &fp, &filename, &dir)) { - return NULL; - } - - if (!PyDict_Check(dir)) { - PyErr_SetString(PyExc_ValueError, "Invalid Dictionary"); - return NULL; } else { - d_size = PyDict_Size(dir); - TRACE(("dict size: %d\n", (int)d_size)); - keys = PyDict_Keys(dir); - values = PyDict_Values(dir); - for (pos=0;posstate, filename, fp)) { - Py_DECREF(encoder); - PyErr_SetString(PyExc_RuntimeError, "tiff codec initialization failed"); - return NULL; + rawExif = NULL; } - for (pos = 0; pos < d_size; pos++) { - key = PyList_GetItem(keys, pos); - value = PyList_GetItem(values, pos); - status = 0; - TRACE(("Attempting to set key: %d\n", (int)PyInt_AsLong(key))); - if (PyInt_Check(value)) { - TRACE(("Setting from Int: %d %ld \n", (int)PyInt_AsLong(key),PyInt_AsLong(value))); - status = ImagingLibTiffSetField(&encoder->state, - (ttag_t) PyInt_AsLong(key), - PyInt_AsLong(value)); - } else if (PyFloat_Check(value)) { - TRACE(("Setting from Float: %d, %f \n", (int)PyInt_AsLong(key),PyFloat_AsDouble(value))); - status = ImagingLibTiffSetField(&encoder->state, - (ttag_t) PyInt_AsLong(key), - (float)PyFloat_AsDouble(value)); - } else if (PyBytes_Check(value)) { - TRACE(("Setting from Bytes: %d, %s \n", (int)PyInt_AsLong(key),PyBytes_AsString(value))); - status = ImagingLibTiffSetField(&encoder->state, - (ttag_t) PyInt_AsLong(key), - PyBytes_AsString(value)); - } else if (PyTuple_Check(value)) { - Py_ssize_t len,i; - float *floatav; - int *intav; - TRACE(("Setting from Tuple: %d \n", (int)PyInt_AsLong(key))); - len = PyTuple_Size(value); - if (len) { - if (PyInt_Check(PyTuple_GetItem(value,0))) { - TRACE((" %d elements, setting as ints \n", (int)len)); - /* malloc check ok, calloc checks for overflow */ - intav = calloc(len, sizeof(int)); - if (intav) { - for (i=0;istate, - (ttag_t) PyInt_AsLong(key), - len, intav); - free(intav); - } - } else if (PyFloat_Check(PyTuple_GetItem(value,0))) { - TRACE((" %d elements, setting as floats \n", (int)len)); - /* malloc check ok, calloc checks for overflow */ - floatav = calloc(len, sizeof(float)); - if (floatav) { - for (i=0;istate, - (ttag_t) PyInt_AsLong(key), - len, floatav); - free(floatav); - } - } else { - TRACE(("Unhandled type in tuple for key %d : %s \n", - (int)PyInt_AsLong(key), - PyBytes_AsString(PyObject_Str(value)))); - } - } - } else { - TRACE(("Unhandled type for key %d : %s \n", - (int)PyInt_AsLong(key), - PyBytes_AsString(PyObject_Str(value)))); - } - if (!status) { - TRACE(("Error setting Field\n")); - Py_DECREF(encoder); - PyErr_SetString(PyExc_RuntimeError, "Error setting from dictionary"); - return NULL; - } - } + encoder->encode = ImagingJpegEncode; - encoder->encode = ImagingLibTiffEncode; + strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8); + + ((JPEGENCODERSTATE *)encoder->state.context)->quality = quality; + ((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays; + ((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen; + ((JPEGENCODERSTATE *)encoder->state.context)->subsampling = subsampling; + ((JPEGENCODERSTATE *)encoder->state.context)->progressive = progressive; + ((JPEGENCODERSTATE *)encoder->state.context)->smooth = smooth; + ((JPEGENCODERSTATE *)encoder->state.context)->optimize = optimize; + ((JPEGENCODERSTATE *)encoder->state.context)->streamtype = streamtype; + ((JPEGENCODERSTATE *)encoder->state.context)->xdpi = xdpi; + ((JPEGENCODERSTATE *)encoder->state.context)->ydpi = ydpi; + ((JPEGENCODERSTATE *)encoder->state.context)->extra = extra; + ((JPEGENCODERSTATE *)encoder->state.context)->extra_size = extra_size; + ((JPEGENCODERSTATE *)encoder->state.context)->rawExif = rawExif; + ((JPEGENCODERSTATE *)encoder->state.context)->rawExifLen = rawExifLen; - return (PyObject*) encoder; + return (PyObject *)encoder; } #endif /* -------------------------------------------------------------------- */ -/* JPEG 2000 */ +/* JPEG 2000 */ /* -------------------------------------------------------------------- */ #ifdef HAVE_OPENJPEG -#include "Jpeg2K.h" +#include "libImaging/Jpeg2K.h" static void -j2k_decode_coord_tuple(PyObject *tuple, int *x, int *y) -{ +j2k_decode_coord_tuple(PyObject *tuple, int *x, int *y) { *x = *y = 0; if (tuple && PyTuple_Check(tuple) && PyTuple_GET_SIZE(tuple) == 2) { - *x = (int)PyInt_AsLong(PyTuple_GET_ITEM(tuple, 0)); - *y = (int)PyInt_AsLong(PyTuple_GET_ITEM(tuple, 1)); + *x = (int)PyLong_AsLong(PyTuple_GET_ITEM(tuple, 0)); + *y = (int)PyLong_AsLong(PyTuple_GET_ITEM(tuple, 1)); - if (*x < 0) + if (*x < 0) { *x = 0; - if (*y < 0) + } + if (*y < 0) { *y = 0; + } } } -PyObject* -PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) -{ +PyObject * +PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; JPEG2KENCODESTATE *context; @@ -926,59 +1180,77 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) PyObject *offset = NULL, *tile_offset = NULL, *tile_size = NULL; char *quality_mode = "rates"; PyObject *quality_layers = NULL; - int num_resolutions = 0; + Py_ssize_t num_resolutions = 0; PyObject *cblk_size = NULL, *precinct_size = NULL; PyObject *irreversible = NULL; char *progression = "LRCP"; OPJ_PROG_ORDER prog_order; char *cinema_mode = "no"; OPJ_CINEMA_MODE cine_mode; - int fd = -1; - - if (!PyArg_ParseTuple(args, "ss|OOOsOIOOOssi", &mode, &format, - &offset, &tile_offset, &tile_size, - &quality_mode, &quality_layers, &num_resolutions, - &cblk_size, &precinct_size, - &irreversible, &progression, &cinema_mode, - &fd)) + char mct = 0; + Py_ssize_t fd = -1; + + if (!PyArg_ParseTuple( + args, + "ss|OOOsOnOOOssbn", + &mode, + &format, + &offset, + &tile_offset, + &tile_size, + &quality_mode, + &quality_layers, + &num_resolutions, + &cblk_size, + &precinct_size, + &irreversible, + &progression, + &cinema_mode, + &mct, + &fd)) { return NULL; + } - if (strcmp (format, "j2k") == 0) + if (strcmp(format, "j2k") == 0) { codec_format = OPJ_CODEC_J2K; - else if (strcmp (format, "jpt") == 0) + } else if (strcmp(format, "jpt") == 0) { codec_format = OPJ_CODEC_JPT; - else if (strcmp (format, "jp2") == 0) + } else if (strcmp(format, "jp2") == 0) { codec_format = OPJ_CODEC_JP2; - else + } else { return NULL; + } - if (strcmp(progression, "LRCP") == 0) + if (strcmp(progression, "LRCP") == 0) { prog_order = OPJ_LRCP; - else if (strcmp(progression, "RLCP") == 0) + } else if (strcmp(progression, "RLCP") == 0) { prog_order = OPJ_RLCP; - else if (strcmp(progression, "RPCL") == 0) + } else if (strcmp(progression, "RPCL") == 0) { prog_order = OPJ_RPCL; - else if (strcmp(progression, "PCRL") == 0) + } else if (strcmp(progression, "PCRL") == 0) { prog_order = OPJ_PCRL; - else if (strcmp(progression, "CPRL") == 0) + } else if (strcmp(progression, "CPRL") == 0) { prog_order = OPJ_CPRL; - else + } else { return NULL; + } - if (strcmp(cinema_mode, "no") == 0) + if (strcmp(cinema_mode, "no") == 0) { cine_mode = OPJ_OFF; - else if (strcmp(cinema_mode, "cinema2k-24") == 0) + } else if (strcmp(cinema_mode, "cinema2k-24") == 0) { cine_mode = OPJ_CINEMA2K_24; - else if (strcmp(cinema_mode, "cinema2k-48") == 0) + } else if (strcmp(cinema_mode, "cinema2k-48") == 0) { cine_mode = OPJ_CINEMA2K_48; - else if (strcmp(cinema_mode, "cinema4k-24") == 0) + } else if (strcmp(cinema_mode, "cinema4k-24") == 0) { cine_mode = OPJ_CINEMA4K_24; - else + } else { return NULL; + } encoder = PyImaging_EncoderNew(sizeof(JPEG2KENCODESTATE)); - if (!encoder) + if (!encoder) { return NULL; + } encoder->encode = ImagingJpeg2KEncode; encoder->cleanup = ImagingJpeg2KEncodeCleanup; @@ -990,51 +1262,49 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) context->format = codec_format; context->offset_x = context->offset_y = 0; - j2k_decode_coord_tuple(offset, &context->offset_x, &context->offset_y); - j2k_decode_coord_tuple(tile_offset, - &context->tile_offset_x, - &context->tile_offset_y); - j2k_decode_coord_tuple(tile_size, - &context->tile_size_x, - &context->tile_size_y); + j2k_decode_coord_tuple( + tile_offset, &context->tile_offset_x, &context->tile_offset_y); + j2k_decode_coord_tuple(tile_size, &context->tile_size_x, &context->tile_size_y); /* Error on illegal tile offsets */ if (context->tile_size_x && context->tile_size_y) { - if (context->tile_offset_x <= context->offset_x - context->tile_size_x - || context->tile_offset_y <= context->offset_y - context->tile_size_y) { - PyErr_SetString(PyExc_ValueError, - "JPEG 2000 tile offset too small; top left tile must " - "intersect image area"); + if (context->tile_offset_x <= context->offset_x - context->tile_size_x || + context->tile_offset_y <= context->offset_y - context->tile_size_y) { + PyErr_SetString( + PyExc_ValueError, + "JPEG 2000 tile offset too small; top left tile must " + "intersect image area"); + Py_DECREF(encoder); + return NULL; } - if (context->tile_offset_x > context->offset_x - || context->tile_offset_y > context->offset_y) { - PyErr_SetString(PyExc_ValueError, - "JPEG 2000 tile offset too large to cover image area"); + if (context->tile_offset_x > context->offset_x || + context->tile_offset_y > context->offset_y) { + PyErr_SetString( + PyExc_ValueError, + "JPEG 2000 tile offset too large to cover image area"); Py_DECREF(encoder); return NULL; } } if (quality_layers && PySequence_Check(quality_layers)) { - context->quality_is_in_db = strcmp (quality_mode, "dB") == 0; + context->quality_is_in_db = strcmp(quality_mode, "dB") == 0; context->quality_layers = quality_layers; Py_INCREF(quality_layers); } context->num_resolutions = num_resolutions; - j2k_decode_coord_tuple(cblk_size, - &context->cblk_width, - &context->cblk_height); - j2k_decode_coord_tuple(precinct_size, - &context->precinct_width, - &context->precinct_height); + j2k_decode_coord_tuple(cblk_size, &context->cblk_width, &context->cblk_height); + j2k_decode_coord_tuple( + precinct_size, &context->precinct_width, &context->precinct_height); context->irreversible = PyObject_IsTrue(irreversible); context->progression = prog_order; context->cinema_mode = cine_mode; + context->mct = mct; return (PyObject *)encoder; } diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 292968f1c46..514fb292913 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -9,32 +9,34 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" -/* use Tests/make_hash.py to calculate these values */ +/* use make_hash.py from the pillow-scripts repository to calculate these values */ #define ACCESS_TABLE_SIZE 27 #define ACCESS_TABLE_HASH 3078 static struct ImagingAccessInstance access_table[ACCESS_TABLE_SIZE]; static inline UINT32 -hash(const char* mode) -{ +hash(const char *mode) { UINT32 i = ACCESS_TABLE_HASH; - while (*mode) - i = ((i<<5) + i) ^ (UINT8) *mode++; + while (*mode) { + i = ((i << 5) + i) ^ (UINT8)*mode++; + } return i % ACCESS_TABLE_SIZE; } static ImagingAccess -add_item(const char* mode) -{ +add_item(const char *mode) { UINT32 i = hash(mode); /* printf("hash %s => %d\n", mode, i); */ if (access_table[i].mode && strcmp(access_table[i].mode, mode) != 0) { - fprintf(stderr, "AccessInit: hash collision: %d for both %s and %s\n", - i, mode, access_table[i].mode); + fprintf( + stderr, + "AccessInit: hash collision: %d for both %s and %s\n", + i, + mode, + access_table[i].mode); exit(1); } access_table[i].mode = mode; @@ -43,37 +45,33 @@ add_item(const char* mode) /* fetch pointer to pixel line */ -static void* -line_8(Imaging im, int x, int y) -{ +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_16(Imaging im, int x, int y) { + return &im->image8[y][x + x]; } -static void* -line_32(Imaging im, int x, int y) -{ +static void * +line_32(Imaging im, int x, int y) { return &im->image32[y][x]; } /* fetch individual pixel */ static void -get_pixel(Imaging im, int x, int y, void* color) -{ - char* out = color; +get_pixel(Imaging im, int x, int y, void *color) { + char *out = color; /* generic pixel access*/ if (im->image8) { out[0] = im->image8[y][x]; } else { - UINT8* p = (UINT8*) &im->image32[y][x]; + UINT8 *p = (UINT8 *)&im->image32[y][x]; if (im->type == IMAGING_TYPE_UINT8 && im->bands == 2) { out[0] = p[0]; out[1] = p[3]; @@ -84,118 +82,98 @@ get_pixel(Imaging im, int x, int y, void* color) } static void -get_pixel_8(Imaging im, int x, int y, void* color) -{ - char* out = color; +get_pixel_8(Imaging im, int x, int y, void *color) { + char *out = color; out[0] = im->image8[y][x]; } static void -get_pixel_16L(Imaging im, int x, int y, void* color) -{ - UINT8* in = (UINT8*) &im->image[y][x+x]; - UINT16* out = color; +get_pixel_16L(Imaging im, int x, int y, void *color) { + UINT8 *in = (UINT8 *)&im->image[y][x + x]; #ifdef WORDS_BIGENDIAN - out[0] = in[0] + (in[1]<<8); + UINT16 out = in[0] + (in[1] << 8); + memcpy(color, &out, sizeof(out)); #else - out[0] = *(UINT16*) in; + memcpy(color, in, sizeof(UINT16)); #endif } static void -get_pixel_16B(Imaging im, int x, int y, void* color) -{ - UINT8* in = (UINT8*) &im->image[y][x+x]; - UINT16* out = color; +get_pixel_16B(Imaging im, int x, int y, void *color) { + UINT8 *in = (UINT8 *)&im->image[y][x + x]; #ifdef WORDS_BIGENDIAN - out[0] = *(UINT16*) in; + memcpy(color, in, sizeof(UINT16)); #else - out[0] = in[1] + (in[0]<<8); + UINT16 out = in[1] + (in[0] << 8); + memcpy(color, &out, sizeof(out)); #endif } static void -get_pixel_32(Imaging im, int x, int y, void* color) -{ - INT32* out = color; - out[0] = im->image32[y][x]; +get_pixel_32(Imaging im, int x, int y, void *color) { + memcpy(color, &im->image32[y][x], sizeof(INT32)); } static void -get_pixel_32L(Imaging im, int x, int y, void* color) -{ - UINT8* in = (UINT8*) &im->image[y][x*4]; - INT32* out = color; +get_pixel_32L(Imaging im, int x, int y, void *color) { + UINT8 *in = (UINT8 *)&im->image[y][x * 4]; #ifdef WORDS_BIGENDIAN - out[0] = in[0] + (in[1]<<8) + (in[2]<<16) + (in[3]<<24); + INT32 out = in[0] + (in[1] << 8) + (in[2] << 16) + (in[3] << 24); + memcpy(color, &out, sizeof(out)); #else - out[0] = *(INT32*) in; + memcpy(color, in, sizeof(INT32)); #endif } static void -get_pixel_32B(Imaging im, int x, int y, void* color) -{ - UINT8* in = (UINT8*) &im->image[y][x*4]; - INT32* out = color; +get_pixel_32B(Imaging im, int x, int y, void *color) { + UINT8 *in = (UINT8 *)&im->image[y][x * 4]; #ifdef WORDS_BIGENDIAN - out[0] = *(INT32*) in; + memcpy(color, in, sizeof(INT32)); #else - out[0] = in[3] + (in[2]<<8) + (in[1]<<16) + (in[0]<<24); + INT32 out = in[3] + (in[2] << 8) + (in[1] << 16) + (in[0] << 24); + memcpy(color, &out, sizeof(out)); #endif } /* store individual pixel */ static void -put_pixel(Imaging im, int x, int y, const void* color) -{ - if (im->image8) - im->image8[y][x] = *((UINT8*) color); - else - im->image32[y][x] = *((INT32*) color); +put_pixel(Imaging im, int x, int y, const void *color) { + if (im->image8) { + im->image8[y][x] = *((UINT8 *)color); + } else { + memcpy(&im->image32[y][x], color, sizeof(INT32)); + } } static void -put_pixel_8(Imaging im, int x, int y, const void* color) -{ - im->image8[y][x] = *((UINT8*) color); +put_pixel_8(Imaging im, int x, int y, const void *color) { + im->image8[y][x] = *((UINT8 *)color); } static void -put_pixel_16L(Imaging im, int x, int y, const void* color) -{ - const char* in = color; - UINT8* out = (UINT8*) &im->image8[y][x+x]; - out[0] = in[0]; - out[1] = in[1]; +put_pixel_16L(Imaging im, int x, int y, const void *color) { + memcpy(&im->image8[y][x + x], color, 2); } static void -put_pixel_16B(Imaging im, int x, int y, const void* color) -{ - const char* in = color; - UINT8* out = (UINT8*) &im->image8[y][x+x]; +put_pixel_16B(Imaging im, int x, int y, const void *color) { + const char *in = color; + UINT8 *out = (UINT8 *)&im->image8[y][x + x]; out[0] = in[1]; out[1] = in[0]; } static void -put_pixel_32L(Imaging im, int x, int y, const void* color) -{ - const char* in = color; - UINT8* out = (UINT8*) &im->image8[y][x*4]; - out[0] = in[0]; - out[1] = in[1]; - out[2] = in[2]; - out[3] = in[3]; +put_pixel_32L(Imaging im, int x, int y, const void *color) { + memcpy(&im->image8[y][x * 4], color, 4); } static void -put_pixel_32B(Imaging im, int x, int y, const void* color) -{ - const char* in = color; - UINT8* out = (UINT8*) &im->image8[y][x*4]; +put_pixel_32B(Imaging im, int x, int y, const void *color) { + const char *in = color; + UINT8 *out = (UINT8 *)&im->image8[y][x * 4]; out[0] = in[3]; out[1] = in[2]; out[2] = in[1]; @@ -203,19 +181,18 @@ put_pixel_32B(Imaging im, int x, int y, const void* color) } static void -put_pixel_32(Imaging im, int x, int y, const void* color) -{ - im->image32[y][x] = *((INT32*) color); +put_pixel_32(Imaging im, int x, int y, const void *color) { + memcpy(&im->image32[y][x], color, sizeof(INT32)); } void -ImagingAccessInit() -{ -#define ADD(mode_, line_, get_pixel_, put_pixel_) \ - { ImagingAccess access = add_item(mode_); \ - access->line = line_; \ - access->get_pixel = get_pixel_; \ - access->put_pixel = put_pixel_; \ +ImagingAccessInit() { +#define ADD(mode_, line_, 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 */ @@ -243,16 +220,13 @@ ImagingAccessInit() } ImagingAccess -ImagingAccessNew(Imaging im) -{ +ImagingAccessNew(Imaging im) { ImagingAccess access = &access_table[hash(im->mode)]; - if (im->mode[0] != access->mode[0] || strcmp(im->mode, access->mode) != 0) + if (im->mode[0] != access->mode[0] || strcmp(im->mode, access->mode) != 0) { return NULL; + } return access; } void -_ImagingAccessDelete(Imaging im, ImagingAccess access) -{ - -} +_ImagingAccessDelete(Imaging im, ImagingAccess access) {} diff --git a/src/libImaging/AlphaComposite.c b/src/libImaging/AlphaComposite.c index a074334aaab..6d728f9088b 100644 --- a/src/libImaging/AlphaComposite.c +++ b/src/libImaging/AlphaComposite.c @@ -8,51 +8,45 @@ * See the README file for details on usage and redistribution. */ - #include "Imaging.h" #define PRECISION_BITS 7 -typedef struct -{ +typedef struct { UINT8 r; UINT8 g; UINT8 b; UINT8 a; } rgba8; - - Imaging -ImagingAlphaComposite(Imaging imDst, Imaging imSrc) -{ +ImagingAlphaComposite(Imaging imDst, Imaging imSrc) { Imaging imOut; int x, y; /* Check arguments */ - if (!imDst || !imSrc || - strcmp(imDst->mode, "RGBA") || - imDst->type != IMAGING_TYPE_UINT8 || - imDst->bands != 4) + if (!imDst || !imSrc || strcmp(imDst->mode, "RGBA") || + imDst->type != IMAGING_TYPE_UINT8 || imDst->bands != 4) { return ImagingError_ModeError(); + } - if (strcmp(imDst->mode, imSrc->mode) || - imDst->type != imSrc->type || - imDst->bands != imSrc->bands || - imDst->xsize != imSrc->xsize || - imDst->ysize != imSrc->ysize) + if (strcmp(imDst->mode, imSrc->mode) || imDst->type != imSrc->type || + imDst->bands != imSrc->bands || imDst->xsize != imSrc->xsize || + imDst->ysize != imSrc->ysize) { return ImagingError_Mismatch(); + } imOut = ImagingNewDirty(imDst->mode, imDst->xsize, imDst->ysize); - if (!imOut) + if (!imOut) { return NULL; + } for (y = 0; y < imDst->ysize; y++) { - rgba8* dst = (rgba8*) imDst->image[y]; - rgba8* src = (rgba8*) imSrc->image[y]; - rgba8* out = (rgba8*) imOut->image[y]; + rgba8 *dst = (rgba8 *)imDst->image[y]; + rgba8 *src = (rgba8 *)imSrc->image[y]; + rgba8 *out = (rgba8 *)imOut->image[y]; - for (x = 0; x < imDst->xsize; x ++) { + for (x = 0; x < imDst->xsize; x++) { if (src->a == 0) { // Copy 4 bytes at once. *out = *dst; @@ -66,21 +60,25 @@ ImagingAlphaComposite(Imaging imDst, Imaging imSrc) UINT32 outa255 = src->a * 255 + blend; // There we use 7 bits for precision. // We could use more, but we go beyond 32 bits. - UINT32 coef1 = src->a * 255 * 255 * (1<a * 255 * 255 * (1 << PRECISION_BITS) / outa255; + UINT32 coef2 = 255 * (1 << PRECISION_BITS) - coef1; tmpr = src->r * coef1 + dst->r * coef2; tmpg = src->g * coef1 + dst->g * coef2; tmpb = src->b * coef1 + dst->b * coef2; - out->r = SHIFTFORDIV255(tmpr + (0x80<> PRECISION_BITS; - out->g = SHIFTFORDIV255(tmpg + (0x80<> PRECISION_BITS; - out->b = SHIFTFORDIV255(tmpb + (0x80<> PRECISION_BITS; + out->r = + SHIFTFORDIV255(tmpr + (0x80 << PRECISION_BITS)) >> PRECISION_BITS; + out->g = + SHIFTFORDIV255(tmpg + (0x80 << PRECISION_BITS)) >> PRECISION_BITS; + out->b = + SHIFTFORDIV255(tmpb + (0x80 << PRECISION_BITS)) >> PRECISION_BITS; out->a = SHIFTFORDIV255(outa255 + 0x80); } - dst++; src++; out++; + dst++; + src++; + out++; } - } return imOut; diff --git a/src/libImaging/Bands.c b/src/libImaging/Bands.c index 7f86d497322..e1b16b34ac0 100644 --- a/src/libImaging/Bands.c +++ b/src/libImaging/Bands.c @@ -15,45 +15,45 @@ * See the README file for details on usage and redistribution. */ - #include "Imaging.h" - -#define CLIP(x) ((x) <= 0 ? 0 : (x) < 256 ? (x) : 255) - - Imaging -ImagingGetBand(Imaging imIn, int band) -{ +ImagingGetBand(Imaging imIn, int band) { Imaging imOut; int x, y; /* Check arguments */ - if (!imIn || imIn->type != IMAGING_TYPE_UINT8) - return (Imaging) ImagingError_ModeError(); + if (!imIn || imIn->type != IMAGING_TYPE_UINT8) { + return (Imaging)ImagingError_ModeError(); + } - if (band < 0 || band >= imIn->bands) - return (Imaging) ImagingError_ValueError("band index out of range"); + if (band < 0 || band >= imIn->bands) { + return (Imaging)ImagingError_ValueError("band index out of range"); + } /* Shortcuts */ - if (imIn->bands == 1) + if (imIn->bands == 1) { return ImagingCopy(imIn); + } /* Special case for LXXA etc */ - if (imIn->bands == 2 && band == 1) + if (imIn->bands == 2 && band == 1) { band = 3; + } imOut = ImagingNewDirty("L", imIn->xsize, imIn->ysize); - if (!imOut) + if (!imOut) { return NULL; + } /* Extract band from image */ for (y = 0; y < imIn->ysize; y++) { - UINT8* in = (UINT8*) imIn->image[y] + band; - UINT8* out = imOut->image8[y]; + UINT8 *in = (UINT8 *)imIn->image[y] + band; + UINT8 *out = imOut->image8[y]; x = 0; for (; x < imIn->xsize - 3; x += 4) { - *((UINT32*) (out + x)) = MAKE_UINT32(in[0], in[4], in[8], in[12]); + UINT32 v = MAKE_UINT32(in[0], in[4], in[8], in[12]); + memcpy(out + x, &v, sizeof(v)); in += 16; } for (; x < imIn->xsize; x++) { @@ -65,15 +65,13 @@ ImagingGetBand(Imaging imIn, int band) return imOut; } - int -ImagingSplit(Imaging imIn, Imaging bands[4]) -{ +ImagingSplit(Imaging imIn, Imaging bands[4]) { int i, j, x, y; /* Check arguments */ if (!imIn || imIn->type != IMAGING_TYPE_UINT8) { - (void) ImagingError_ModeError(); + (void)ImagingError_ModeError(); return 0; } @@ -85,7 +83,7 @@ ImagingSplit(Imaging imIn, Imaging bands[4]) for (i = 0; i < imIn->bands; i++) { bands[i] = ImagingNewDirty("L", imIn->xsize, imIn->ysize); - if ( ! bands[i]) { + if (!bands[i]) { for (j = 0; j < i; ++j) { ImagingDelete(bands[j]); } @@ -96,13 +94,15 @@ ImagingSplit(Imaging imIn, Imaging bands[4]) /* Extract bands from image */ if (imIn->bands == 2) { for (y = 0; y < imIn->ysize; y++) { - UINT8* in = (UINT8*) imIn->image[y]; - UINT8* out0 = bands[0]->image8[y]; - UINT8* out1 = bands[1]->image8[y]; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out0 = bands[0]->image8[y]; + UINT8 *out1 = bands[1]->image8[y]; x = 0; for (; x < imIn->xsize - 3; x += 4) { - *((UINT32*) (out0 + x)) = MAKE_UINT32(in[0], in[4], in[8], in[12]); - *((UINT32*) (out1 + x)) = MAKE_UINT32(in[0+3], in[4+3], in[8+3], in[12+3]); + UINT32 v = MAKE_UINT32(in[0], in[4], in[8], in[12]); + memcpy(out0 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0 + 3], in[4 + 3], in[8 + 3], in[12 + 3]); + memcpy(out1 + x, &v, sizeof(v)); in += 16; } for (; x < imIn->xsize; x++) { @@ -113,15 +113,18 @@ ImagingSplit(Imaging imIn, Imaging bands[4]) } } else if (imIn->bands == 3) { for (y = 0; y < imIn->ysize; y++) { - UINT8* in = (UINT8*) imIn->image[y]; - UINT8* out0 = bands[0]->image8[y]; - UINT8* out1 = bands[1]->image8[y]; - UINT8* out2 = bands[2]->image8[y]; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out0 = bands[0]->image8[y]; + UINT8 *out1 = bands[1]->image8[y]; + UINT8 *out2 = bands[2]->image8[y]; x = 0; for (; x < imIn->xsize - 3; x += 4) { - *((UINT32*) (out0 + x)) = MAKE_UINT32(in[0], in[4], in[8], in[12]); - *((UINT32*) (out1 + x)) = MAKE_UINT32(in[0+1], in[4+1], in[8+1], in[12+1]); - *((UINT32*) (out2 + x)) = MAKE_UINT32(in[0+2], in[4+2], in[8+2], in[12+2]); + UINT32 v = MAKE_UINT32(in[0], in[4], in[8], in[12]); + memcpy(out0 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0 + 1], in[4 + 1], in[8 + 1], in[12 + 1]); + memcpy(out1 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0 + 2], in[4 + 2], in[8 + 2], in[12 + 2]); + memcpy(out2 + x, &v, sizeof(v)); in += 16; } for (; x < imIn->xsize; x++) { @@ -133,17 +136,21 @@ ImagingSplit(Imaging imIn, Imaging bands[4]) } } else { for (y = 0; y < imIn->ysize; y++) { - UINT8* in = (UINT8*) imIn->image[y]; - UINT8* out0 = bands[0]->image8[y]; - UINT8* out1 = bands[1]->image8[y]; - UINT8* out2 = bands[2]->image8[y]; - UINT8* out3 = bands[3]->image8[y]; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out0 = bands[0]->image8[y]; + UINT8 *out1 = bands[1]->image8[y]; + UINT8 *out2 = bands[2]->image8[y]; + UINT8 *out3 = bands[3]->image8[y]; x = 0; for (; x < imIn->xsize - 3; x += 4) { - *((UINT32*) (out0 + x)) = MAKE_UINT32(in[0], in[4], in[8], in[12]); - *((UINT32*) (out1 + x)) = MAKE_UINT32(in[0+1], in[4+1], in[8+1], in[12+1]); - *((UINT32*) (out2 + x)) = MAKE_UINT32(in[0+2], in[4+2], in[8+2], in[12+2]); - *((UINT32*) (out3 + x)) = MAKE_UINT32(in[0+3], in[4+3], in[8+3], in[12+3]); + UINT32 v = MAKE_UINT32(in[0], in[4], in[8], in[12]); + memcpy(out0 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0 + 1], in[4 + 1], in[8 + 1], in[12 + 1]); + memcpy(out1 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0 + 2], in[4 + 2], in[8 + 2], in[12 + 2]); + memcpy(out2 + x, &v, sizeof(v)); + v = MAKE_UINT32(in[0 + 3], in[4 + 3], in[8 + 3], in[12 + 3]); + memcpy(out3 + x, &v, sizeof(v)); in += 16; } for (; x < imIn->xsize; x++) { @@ -159,36 +166,38 @@ ImagingSplit(Imaging imIn, Imaging bands[4]) return imIn->bands; } - Imaging -ImagingPutBand(Imaging imOut, Imaging imIn, int band) -{ +ImagingPutBand(Imaging imOut, Imaging imIn, int band) { int x, y; /* Check arguments */ - if (!imIn || imIn->bands != 1 || !imOut) - return (Imaging) ImagingError_ModeError(); + if (!imIn || imIn->bands != 1 || !imOut) { + return (Imaging)ImagingError_ModeError(); + } - if (band < 0 || band >= imOut->bands) - return (Imaging) ImagingError_ValueError("band index out of range"); + if (band < 0 || band >= imOut->bands) { + return (Imaging)ImagingError_ValueError("band index out of range"); + } - if (imIn->type != imOut->type || - imIn->xsize != imOut->xsize || - imIn->ysize != imOut->ysize) - return (Imaging) ImagingError_Mismatch(); + if (imIn->type != imOut->type || imIn->xsize != imOut->xsize || + imIn->ysize != imOut->ysize) { + return (Imaging)ImagingError_Mismatch(); + } /* Shortcuts */ - if (imOut->bands == 1) + if (imOut->bands == 1) { return ImagingCopy2(imOut, imIn); + } /* Special case for LXXA etc */ - if (imOut->bands == 2 && band == 1) + if (imOut->bands == 2 && band == 1) { band = 3; + } /* Insert band into image */ for (y = 0; y < imIn->ysize; y++) { - UINT8* in = imIn->image8[y]; - UINT8* out = (UINT8*) imOut->image[y] + band; + UINT8 *in = imIn->image8[y]; + UINT8 *out = (UINT8 *)imOut->image[y] + band; for (x = 0; x < imIn->xsize; x++) { *out = in[x]; out += 4; @@ -199,28 +208,30 @@ ImagingPutBand(Imaging imOut, Imaging imIn, int band) } Imaging -ImagingFillBand(Imaging imOut, int band, int color) -{ +ImagingFillBand(Imaging imOut, int band, int color) { int x, y; /* Check arguments */ - if (!imOut || imOut->type != IMAGING_TYPE_UINT8) - return (Imaging) ImagingError_ModeError(); + if (!imOut || imOut->type != IMAGING_TYPE_UINT8) { + return (Imaging)ImagingError_ModeError(); + } - if (band < 0 || band >= imOut->bands) - return (Imaging) ImagingError_ValueError("band index out of range"); + if (band < 0 || band >= imOut->bands) { + return (Imaging)ImagingError_ValueError("band index out of range"); + } /* Special case for LXXA etc */ - if (imOut->bands == 2 && band == 1) + if (imOut->bands == 2 && band == 1) { band = 3; + } - color = CLIP(color); + color = CLIP8(color); /* Insert color into image */ for (y = 0; y < imOut->ysize; y++) { - UINT8* out = (UINT8*) imOut->image[y] + band; + UINT8 *out = (UINT8 *)imOut->image[y] + band; for (x = 0; x < imOut->xsize; x++) { - *out = (UINT8) color; + *out = (UINT8)color; out += 4; } } @@ -229,70 +240,71 @@ ImagingFillBand(Imaging imOut, int band, int color) } Imaging -ImagingMerge(const char* mode, Imaging bands[4]) -{ +ImagingMerge(const char *mode, Imaging bands[4]) { int i, x, y; int bandsCount = 0; Imaging imOut; Imaging firstBand; firstBand = bands[0]; - if ( ! firstBand) { - return (Imaging) ImagingError_ValueError("wrong number of bands"); + if (!firstBand) { + return (Imaging)ImagingError_ValueError("wrong number of bands"); } for (i = 0; i < 4; ++i) { - if ( ! bands[i]) { + if (!bands[i]) { break; } if (bands[i]->bands != 1) { - return (Imaging) ImagingError_ModeError(); + return (Imaging)ImagingError_ModeError(); } - if (bands[i]->xsize != firstBand->xsize - || bands[i]->ysize != firstBand->ysize) { - return (Imaging) ImagingError_Mismatch(); + if (bands[i]->xsize != firstBand->xsize || + bands[i]->ysize != firstBand->ysize) { + return (Imaging)ImagingError_Mismatch(); } } bandsCount = i; imOut = ImagingNewDirty(mode, firstBand->xsize, firstBand->ysize); - if ( ! imOut) + if (!imOut) { return NULL; + } if (imOut->bands != bandsCount) { ImagingDelete(imOut); - return (Imaging) ImagingError_ValueError("wrong number of bands"); + return (Imaging)ImagingError_ValueError("wrong number of bands"); } - if (imOut->bands == 1) + if (imOut->bands == 1) { return ImagingCopy2(imOut, firstBand); + } if (imOut->bands == 2) { for (y = 0; y < imOut->ysize; y++) { - UINT8* in0 = bands[0]->image8[y]; - UINT8* in1 = bands[1]->image8[y]; - UINT32* out = (UINT32*) imOut->image32[y]; + UINT8 *in0 = bands[0]->image8[y]; + UINT8 *in1 = bands[1]->image8[y]; + UINT32 *out = (UINT32 *)imOut->image32[y]; for (x = 0; x < imOut->xsize; x++) { out[x] = MAKE_UINT32(in0[x], 0, 0, in1[x]); } } } else if (imOut->bands == 3) { for (y = 0; y < imOut->ysize; y++) { - UINT8* in0 = bands[0]->image8[y]; - UINT8* in1 = bands[1]->image8[y]; - UINT8* in2 = bands[2]->image8[y]; - UINT32* out = (UINT32*) imOut->image32[y]; + UINT8 *in0 = bands[0]->image8[y]; + UINT8 *in1 = bands[1]->image8[y]; + UINT8 *in2 = bands[2]->image8[y]; + UINT32 *out = (UINT32 *)imOut->image32[y]; for (x = 0; x < imOut->xsize; x++) { out[x] = MAKE_UINT32(in0[x], in1[x], in2[x], 0); } } } else if (imOut->bands == 4) { for (y = 0; y < imOut->ysize; y++) { - UINT8* in0 = bands[0]->image8[y]; - UINT8* in1 = bands[1]->image8[y]; - UINT8* in2 = bands[2]->image8[y]; - UINT8* in3 = bands[3]->image8[y]; - UINT32* out = (UINT32*) imOut->image32[y]; + UINT8 *in0 = bands[0]->image8[y]; + UINT8 *in1 = bands[1]->image8[y]; + UINT8 *in2 = bands[2]->image8[y]; + UINT8 *in3 = bands[3]->image8[y]; + UINT32 *out = (UINT32 *)imOut->image32[y]; for (x = 0; x < imOut->xsize; x++) { out[x] = MAKE_UINT32(in0[x], in1[x], in2[x], in3[x]); } diff --git a/src/libImaging/Bcn.h b/src/libImaging/Bcn.h new file mode 100644 index 00000000000..1a6fbee4576 --- /dev/null +++ b/src/libImaging/Bcn.h @@ -0,0 +1,3 @@ +typedef struct { + char *pixel_format; +} BCNSTATE; diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c index 58d3ecc1068..a57b74b61ac 100644 --- a/src/libImaging/BcnDecode.c +++ b/src/libImaging/BcnDecode.c @@ -4,853 +4,888 @@ * decoder for DXTn-compressed data * * Format documentation: - * http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt + * https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt * * The contents of this file are in the public domain (CC0) * Full text of the CC0 license: * https://creativecommons.org/publicdomain/zero/1.0/ */ - #include "Imaging.h" +#include "Bcn.h" typedef struct { - UINT8 r, g, b, a; + UINT8 r, g, b, a; } rgba; typedef struct { - UINT8 l; + UINT8 l; } lum; typedef struct { - FLOAT32 r, g, b; -} rgb32f; - -typedef struct { - UINT16 c0, c1; - UINT32 lut; + UINT16 c0, c1; + UINT32 lut; } bc1_color; typedef struct { - UINT8 a0, a1; - UINT8 lut[6]; + UINT8 a0, a1; + UINT8 lut[6]; } bc3_alpha; -#define LOAD16(p) \ - (p)[0] | ((p)[1] << 8) +typedef struct { + INT8 a0, a1; + UINT8 lut[6]; +} bc5s_alpha; -#define LOAD32(p) \ - (p)[0] | ((p)[1] << 8) | ((p)[2] << 16) | ((p)[3] << 24) +#define LOAD16(p) (p)[0] | ((p)[1] << 8) -static void bc1_color_load(bc1_color *dst, const UINT8 *src) { - dst->c0 = LOAD16(src); - dst->c1 = LOAD16(src + 2); - dst->lut = LOAD32(src + 4); -} +#define LOAD32(p) (p)[0] | ((p)[1] << 8) | ((p)[2] << 16) | ((p)[3] << 24) -static void bc3_alpha_load(bc3_alpha *dst, const UINT8 *src) { - memcpy(dst, src, sizeof(bc3_alpha)); +static void +bc1_color_load(bc1_color *dst, const UINT8 *src) { + dst->c0 = LOAD16(src); + dst->c1 = LOAD16(src + 2); + dst->lut = LOAD32(src + 4); } -static rgba decode_565(UINT16 x) { - rgba c; - int r, g, b; - r = (x & 0xf800) >> 8; - r |= r >> 5; - c.r = r; - g = (x & 0x7e0) >> 3; - g |= g >> 6; - c.g = g; - b = (x & 0x1f) << 3; - b |= b >> 5; - c.b = b; - c.a = 0xff; - return c; +static rgba +decode_565(UINT16 x) { + rgba c; + int r, g, b; + r = (x & 0xf800) >> 8; + r |= r >> 5; + c.r = r; + g = (x & 0x7e0) >> 3; + g |= g >> 6; + c.g = g; + b = (x & 0x1f) << 3; + b |= b >> 5; + c.b = b; + c.a = 0xff; + return c; } -static void decode_bc1_color(rgba *dst, const UINT8 *src) { - bc1_color col; - rgba p[4]; - int n, cw; - UINT16 r0, g0, b0, r1, g1, b1; - bc1_color_load(&col, src); - - p[0] = decode_565(col.c0); - r0 = p[0].r; - g0 = p[0].g; - b0 = p[0].b; - p[1] = decode_565(col.c1); - r1 = p[1].r; - g1 = p[1].g; - b1 = p[1].b; - if (col.c0 > col.c1) { - p[2].r = (2*r0 + 1*r1) / 3; - p[2].g = (2*g0 + 1*g1) / 3; - p[2].b = (2*b0 + 1*b1) / 3; - p[2].a = 0xff; - p[3].r = (1*r0 + 2*r1) / 3; - p[3].g = (1*g0 + 2*g1) / 3; - p[3].b = (1*b0 + 2*b1) / 3; - p[3].a = 0xff; - } else { - p[2].r = (r0 + r1) / 2; - p[2].g = (g0 + g1) / 2; - p[2].b = (b0 + b1) / 2; - p[2].a = 0xff; - p[3].r = 0; - p[3].g = 0; - p[3].b = 0; - p[3].a = 0; - } - for (n = 0; n < 16; n++) { - cw = 3 & (col.lut >> (2 * n)); - dst[n] = p[cw]; - } +static void +decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) { + bc1_color col; + rgba p[4]; + int n, cw; + UINT16 r0, g0, b0, r1, g1, b1; + bc1_color_load(&col, src); + + p[0] = decode_565(col.c0); + r0 = p[0].r; + g0 = p[0].g; + b0 = p[0].b; + p[1] = decode_565(col.c1); + r1 = p[1].r; + g1 = p[1].g; + b1 = p[1].b; + + + /* NOTE: BC2 and BC3 reuse BC1 color blocks but always act like c0 > c1 */ + if (col.c0 > col.c1 || separate_alpha) { + p[2].r = (2 * r0 + 1 * r1) / 3; + p[2].g = (2 * g0 + 1 * g1) / 3; + p[2].b = (2 * b0 + 1 * b1) / 3; + p[2].a = 0xff; + p[3].r = (1 * r0 + 2 * r1) / 3; + p[3].g = (1 * g0 + 2 * g1) / 3; + p[3].b = (1 * b0 + 2 * b1) / 3; + p[3].a = 0xff; + } else { + p[2].r = (r0 + r1) / 2; + p[2].g = (g0 + g1) / 2; + p[2].b = (b0 + b1) / 2; + p[2].a = 0xff; + p[3].r = 0; + p[3].g = 0; + p[3].b = 0; + p[3].a = 0; + } + for (n = 0; n < 16; n++) { + cw = 3 & (col.lut >> (2 * n)); + dst[n] = p[cw]; + } } -static void decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o) { - bc3_alpha b; - UINT16 a0, a1; - UINT8 a[8]; - int n, lut, aw; - bc3_alpha_load(&b, src); - - a0 = b.a0; - a1 = b.a1; - a[0] = (UINT8)a0; - a[1] = (UINT8)a1; - if (a0 > a1) { - a[2] = (6*a0 + 1*a1) / 7; - a[3] = (5*a0 + 2*a1) / 7; - a[4] = (4*a0 + 3*a1) / 7; - a[5] = (3*a0 + 4*a1) / 7; - a[6] = (2*a0 + 5*a1) / 7; - a[7] = (1*a0 + 6*a1) / 7; - } else { - a[2] = (4*a0 + 1*a1) / 5; - a[3] = (3*a0 + 2*a1) / 5; - a[4] = (2*a0 + 3*a1) / 5; - a[5] = (1*a0 + 4*a1) / 5; - a[6] = 0; - a[7] = 0xff; - } - lut = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16); - for (n = 0; n < 8; n++) { - aw = 7 & (lut >> (3 * n)); - dst[stride * n + o] = a[aw]; - } - lut = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16); - for (n = 0; n < 8; n++) { - aw = 7 & (lut >> (3 * n)); - dst[stride * (8+n) + o] = a[aw]; - } +static void +decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o, int sign) { + UINT16 a0, a1; + UINT8 a[8]; + int n, lut1, lut2, aw; + if (sign == 1) { + bc5s_alpha b; + memcpy(&b, src, sizeof(bc5s_alpha)); + a0 = (b.a0 + 255) / 2; + a1 = (b.a1 + 255) / 2; + lut1 = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16); + lut2 = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16); + } else { + bc3_alpha b; + memcpy(&b, src, sizeof(bc3_alpha)); + a0 = b.a0; + a1 = b.a1; + lut1 = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16); + lut2 = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16); + } + + a[0] = (UINT8)a0; + a[1] = (UINT8)a1; + if (a0 > a1) { + a[2] = (6 * a0 + 1 * a1) / 7; + a[3] = (5 * a0 + 2 * a1) / 7; + a[4] = (4 * a0 + 3 * a1) / 7; + a[5] = (3 * a0 + 4 * a1) / 7; + a[6] = (2 * a0 + 5 * a1) / 7; + a[7] = (1 * a0 + 6 * a1) / 7; + } else { + a[2] = (4 * a0 + 1 * a1) / 5; + a[3] = (3 * a0 + 2 * a1) / 5; + a[4] = (2 * a0 + 3 * a1) / 5; + a[5] = (1 * a0 + 4 * a1) / 5; + a[6] = 0; + a[7] = 0xff; + } + for (n = 0; n < 8; n++) { + aw = 7 & (lut1 >> (3 * n)); + dst[stride * n + o] = a[aw]; + } + for (n = 0; n < 8; n++) { + aw = 7 & (lut2 >> (3 * n)); + dst[stride * (8 + n) + o] = a[aw]; + } } -static void decode_bc1_block(rgba *col, const UINT8* src) { - decode_bc1_color(col, src); +static void +decode_bc1_block(rgba *col, const UINT8 *src) { + decode_bc1_color(col, src, 0); } -static void decode_bc2_block(rgba *col, const UINT8* src) { - int n, bitI, byI, av; - decode_bc1_color(col, src + 8); - for (n = 0; n < 16; n++) { - bitI = n * 4; - byI = bitI >> 3; - av = 0xf & (src[byI] >> (bitI & 7)); - av = (av << 4) | av; - col[n].a = av; - } +static void +decode_bc2_block(rgba *col, const UINT8 *src) { + int n, bitI, byI, av; + decode_bc1_color(col, src + 8, 1); + for (n = 0; n < 16; n++) { + bitI = n * 4; + byI = bitI >> 3; + av = 0xf & (src[byI] >> (bitI & 7)); + av = (av << 4) | av; + col[n].a = av; + } } -static void decode_bc3_block(rgba *col, const UINT8* src) { - decode_bc1_color(col, src + 8); - decode_bc3_alpha((char *)col, src, sizeof(col[0]), 3); +static void +decode_bc3_block(rgba *col, const UINT8 *src) { + decode_bc1_color(col, src + 8, 1); + decode_bc3_alpha((char *)col, src, sizeof(col[0]), 3, 0); } -static void decode_bc4_block(lum *col, const UINT8* src) { - decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0); +static void +decode_bc4_block(lum *col, const UINT8 *src) { + decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0, 0); } -static void decode_bc5_block(rgba *col, const UINT8* src) { - decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0); - decode_bc3_alpha((char *)col, src + 8, sizeof(col[0]), 1); +static void +decode_bc5_block(rgba *col, const UINT8 *src, int sign) { + decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0, sign); + decode_bc3_alpha((char *)col, src + 8, sizeof(col[0]), 1, sign); } /* BC6 and BC7 are described here: - https://www.opengl.org/registry/specs/ARB/texture_compression_bptc.txt */ + https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_texture_compression_bptc.txt + */ -static UINT8 get_bit(const UINT8* src, int bit) { - int by = bit >> 3; - bit &= 7; - return (src[by] >> bit) & 1; +static UINT8 +get_bit(const UINT8 *src, int bit) { + int by = bit >> 3; + bit &= 7; + return (src[by] >> bit) & 1; } -static UINT8 get_bits(const UINT8* src, int bit, int count) { - UINT8 v; - int x; - int by = bit >> 3; - bit &= 7; - if (!count) { - return 0; - } - if (bit + count <= 8) { - v = (src[by] >> bit) & ((1 << count) - 1); - } else { - x = src[by] | (src[by+1] << 8); - v = (x >> bit) & ((1 << count) - 1); - } - return v; +static UINT8 +get_bits(const UINT8 *src, int bit, int count) { + UINT8 v; + int x; + int by = bit >> 3; + bit &= 7; + if (!count) { + return 0; + } + if (bit + count <= 8) { + v = (src[by] >> bit) & ((1 << count) - 1); + } else { + x = src[by] | (src[by + 1] << 8); + v = (x >> bit) & ((1 << count) - 1); + } + return v; } /* BC7 */ typedef struct { - char ns; - char pb; - char rb; - char isb; - char cb; - char ab; - char epb; - char spb; - char ib; - char ib2; + char ns; + char pb; + char rb; + char isb; + char cb; + char ab; + char epb; + char spb; + char ib; + char ib2; } bc7_mode_info; static const bc7_mode_info bc7_modes[] = { - {3, 4, 0, 0, 4, 0, 1, 0, 3, 0}, - {2, 6, 0, 0, 6, 0, 0, 1, 3, 0}, - {3, 6, 0, 0, 5, 0, 0, 0, 2, 0}, - {2, 6, 0, 0, 7, 0, 1, 0, 2, 0}, - {1, 0, 2, 1, 5, 6, 0, 0, 2, 3}, - {1, 0, 2, 0, 7, 8, 0, 0, 2, 2}, - {1, 0, 0, 0, 7, 7, 1, 0, 4, 0}, - {2, 6, 0, 0, 5, 5, 1, 0, 2, 0} -}; + {3, 4, 0, 0, 4, 0, 1, 0, 3, 0}, + {2, 6, 0, 0, 6, 0, 0, 1, 3, 0}, + {3, 6, 0, 0, 5, 0, 0, 0, 2, 0}, + {2, 6, 0, 0, 7, 0, 1, 0, 2, 0}, + {1, 0, 2, 1, 5, 6, 0, 0, 2, 3}, + {1, 0, 2, 0, 7, 8, 0, 0, 2, 2}, + {1, 0, 0, 0, 7, 7, 1, 0, 4, 0}, + {2, 6, 0, 0, 5, 5, 1, 0, 2, 0}}; /* Subset indices: Table.P2, 1 bit per index */ static const UINT16 bc7_si2[] = { - 0xcccc, 0x8888, 0xeeee, 0xecc8, 0xc880, 0xfeec, 0xfec8, 0xec80, - 0xc800, 0xffec, 0xfe80, 0xe800, 0xffe8, 0xff00, 0xfff0, 0xf000, - 0xf710, 0x008e, 0x7100, 0x08ce, 0x008c, 0x7310, 0x3100, 0x8cce, - 0x088c, 0x3110, 0x6666, 0x366c, 0x17e8, 0x0ff0, 0x718e, 0x399c, - 0xaaaa, 0xf0f0, 0x5a5a, 0x33cc, 0x3c3c, 0x55aa, 0x9696, 0xa55a, - 0x73ce, 0x13c8, 0x324c, 0x3bdc, 0x6996, 0xc33c, 0x9966, 0x0660, - 0x0272, 0x04e4, 0x4e40, 0x2720, 0xc936, 0x936c, 0x39c6, 0x639c, - 0x9336, 0x9cc6, 0x817e, 0xe718, 0xccf0, 0x0fcc, 0x7744, 0xee22}; + 0xcccc, 0x8888, 0xeeee, 0xecc8, 0xc880, 0xfeec, 0xfec8, 0xec80, 0xc800, 0xffec, + 0xfe80, 0xe800, 0xffe8, 0xff00, 0xfff0, 0xf000, 0xf710, 0x008e, 0x7100, 0x08ce, + 0x008c, 0x7310, 0x3100, 0x8cce, 0x088c, 0x3110, 0x6666, 0x366c, 0x17e8, 0x0ff0, + 0x718e, 0x399c, 0xaaaa, 0xf0f0, 0x5a5a, 0x33cc, 0x3c3c, 0x55aa, 0x9696, 0xa55a, + 0x73ce, 0x13c8, 0x324c, 0x3bdc, 0x6996, 0xc33c, 0x9966, 0x0660, 0x0272, 0x04e4, + 0x4e40, 0x2720, 0xc936, 0x936c, 0x39c6, 0x639c, 0x9336, 0x9cc6, 0x817e, 0xe718, + 0xccf0, 0x0fcc, 0x7744, 0xee22}; /* Table.P3, 2 bits per index */ static const UINT32 bc7_si3[] = { - 0xaa685050, 0x6a5a5040, 0x5a5a4200, 0x5450a0a8, - 0xa5a50000, 0xa0a05050, 0x5555a0a0, 0x5a5a5050, - 0xaa550000, 0xaa555500, 0xaaaa5500, 0x90909090, - 0x94949494, 0xa4a4a4a4, 0xa9a59450, 0x2a0a4250, - 0xa5945040, 0x0a425054, 0xa5a5a500, 0x55a0a0a0, - 0xa8a85454, 0x6a6a4040, 0xa4a45000, 0x1a1a0500, - 0x0050a4a4, 0xaaa59090, 0x14696914, 0x69691400, - 0xa08585a0, 0xaa821414, 0x50a4a450, 0x6a5a0200, - 0xa9a58000, 0x5090a0a8, 0xa8a09050, 0x24242424, - 0x00aa5500, 0x24924924, 0x24499224, 0x50a50a50, - 0x500aa550, 0xaaaa4444, 0x66660000, 0xa5a0a5a0, - 0x50a050a0, 0x69286928, 0x44aaaa44, 0x66666600, - 0xaa444444, 0x54a854a8, 0x95809580, 0x96969600, - 0xa85454a8, 0x80959580, 0xaa141414, 0x96960000, - 0xaaaa1414, 0xa05050a0, 0xa0a5a5a0, 0x96000000, - 0x40804080, 0xa9a8a9a8, 0xaaaaaa44, 0x2a4a5254}; + 0xaa685050, 0x6a5a5040, 0x5a5a4200, 0x5450a0a8, 0xa5a50000, 0xa0a05050, 0x5555a0a0, + 0x5a5a5050, 0xaa550000, 0xaa555500, 0xaaaa5500, 0x90909090, 0x94949494, 0xa4a4a4a4, + 0xa9a59450, 0x2a0a4250, 0xa5945040, 0x0a425054, 0xa5a5a500, 0x55a0a0a0, 0xa8a85454, + 0x6a6a4040, 0xa4a45000, 0x1a1a0500, 0x0050a4a4, 0xaaa59090, 0x14696914, 0x69691400, + 0xa08585a0, 0xaa821414, 0x50a4a450, 0x6a5a0200, 0xa9a58000, 0x5090a0a8, 0xa8a09050, + 0x24242424, 0x00aa5500, 0x24924924, 0x24499224, 0x50a50a50, 0x500aa550, 0xaaaa4444, + 0x66660000, 0xa5a0a5a0, 0x50a050a0, 0x69286928, 0x44aaaa44, 0x66666600, 0xaa444444, + 0x54a854a8, 0x95809580, 0x96969600, 0xa85454a8, 0x80959580, 0xaa141414, 0x96960000, + 0xaaaa1414, 0xa05050a0, 0xa0a5a5a0, 0x96000000, 0x40804080, 0xa9a8a9a8, 0xaaaaaa44, + 0x2a4a5254}; /* Anchor indices: Table.A2 */ static const char bc7_ai0[] = { - 15,15,15,15,15,15,15,15, - 15,15,15,15,15,15,15,15, - 15, 2, 8, 2, 2, 8, 8,15, - 2, 8, 2, 2, 8, 8, 2, 2, - 15,15, 6, 8, 2, 8,15,15, - 2, 8, 2, 2, 2,15,15, 6, - 6, 2, 6, 8,15,15, 2, 2, - 15,15,15,15,15, 2, 2,15}; + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 2, 8, 2, 2, 8, + 8, 15, 2, 8, 2, 2, 8, 8, 2, 2, 15, 15, 6, 8, 2, 8, 15, 15, 2, 8, 2, 2, + 2, 15, 15, 6, 6, 2, 6, 8, 15, 15, 2, 2, 15, 15, 15, 15, 15, 2, 2, 15}; /* Table.A3a */ static const char bc7_ai1[] = { - 3, 3,15,15, 8, 3,15,15, - 8, 8, 6, 6, 6, 5, 3, 3, - 3, 3, 8,15, 3, 3, 6,10, - 5, 8, 8, 6, 8, 5,15,15, - 8,15, 3, 5, 6,10, 8,15, - 15, 3,15, 5,15,15,15,15, - 3,15, 5, 5, 5, 8, 5,10, - 5,10, 8,13,15,12, 3, 3}; + 3, 3, 15, 15, 8, 3, 15, 15, 8, 8, 6, 6, 6, 5, 3, 3, 3, 3, 8, 15, 3, 3, + 6, 10, 5, 8, 8, 6, 8, 5, 15, 15, 8, 15, 3, 5, 6, 10, 8, 15, 15, 3, 15, 5, + 15, 15, 15, 15, 3, 15, 5, 5, 5, 8, 5, 10, 5, 10, 8, 13, 15, 12, 3, 3}; /* Table.A3b */ -static const char bc7_ai2[] = { - 15, 8, 8, 3,15,15, 3, 8, - 15,15,15,15,15,15,15, 8, - 15, 8,15, 3,15, 8,15, 8, - 3,15, 6,10,15,15,10, 8, - 15, 3,15,10,10, 8, 9,10, - 6,15, 8,15, 3, 6, 6, 8, - 15, 3,15,15,15,15,15,15, - 15,15,15,15, 3,15,15, 8}; +static const char bc7_ai2[] = {15, 8, 8, 3, 15, 15, 3, 8, 15, 15, 15, 15, 15, + 15, 15, 8, 15, 8, 15, 3, 15, 8, 15, 8, 3, 15, + 6, 10, 15, 15, 10, 8, 15, 3, 15, 10, 10, 8, 9, + 10, 6, 15, 8, 15, 3, 6, 6, 8, 15, 3, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 3, 15, 15, 8}; /* Interpolation weights */ static const char bc7_weights2[] = {0, 21, 43, 64}; static const char bc7_weights3[] = {0, 9, 18, 27, 37, 46, 55, 64}; static const char bc7_weights4[] = { - 0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64}; - -static const char *bc7_get_weights(int n) { - if (n == 2) { - return bc7_weights2; - } - if (n == 3) { - return bc7_weights3; - } - return bc7_weights4; + 0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64}; + +static const char * +bc7_get_weights(int n) { + if (n == 2) { + return bc7_weights2; + } + if (n == 3) { + return bc7_weights3; + } + return bc7_weights4; } -static int bc7_get_subset(int ns, int partition, int n) { - if (ns == 2) { - return 1 & (bc7_si2[partition] >> n); - } - if (ns == 3) { - return 3 & (bc7_si3[partition] >> (2 * n)); - } - return 0; +static int +bc7_get_subset(int ns, int partition, int n) { + if (ns == 2) { + return 1 & (bc7_si2[partition] >> n); + } + if (ns == 3) { + return 3 & (bc7_si3[partition] >> (2 * n)); + } + return 0; } -static UINT8 expand_quantized(UINT8 v, int bits) { - v = v << (8 - bits); - return v | (v >> bits); +static UINT8 +expand_quantized(UINT8 v, int bits) { + v = v << (8 - bits); + return v | (v >> bits); } -static void bc7_lerp(rgba *dst, const rgba *e, int s0, int s1) { - int t0 = 64 - s0; - int t1 = 64 - s1; - dst->r = (UINT8)((t0 * e[0].r + s0 * e[1].r + 32) >> 6); - dst->g = (UINT8)((t0 * e[0].g + s0 * e[1].g + 32) >> 6); - dst->b = (UINT8)((t0 * e[0].b + s0 * e[1].b + 32) >> 6); - dst->a = (UINT8)((t1 * e[0].a + s1 * e[1].a + 32) >> 6); +static void +bc7_lerp(rgba *dst, const rgba *e, int s0, int s1) { + int t0 = 64 - s0; + int t1 = 64 - s1; + dst->r = (UINT8)((t0 * e[0].r + s0 * e[1].r + 32) >> 6); + dst->g = (UINT8)((t0 * e[0].g + s0 * e[1].g + 32) >> 6); + dst->b = (UINT8)((t0 * e[0].b + s0 * e[1].b + 32) >> 6); + dst->a = (UINT8)((t1 * e[0].a + s1 * e[1].a + 32) >> 6); } -static void decode_bc7_block(rgba *col, const UINT8* src) { - rgba endpoints[6]; - int bit = 0, cibit, aibit; - int mode = src[0]; - int i, j; - int numep, cb, ab, ib, ib2, i0, i1, s; - UINT8 index_sel, partition, rotation, val; - const char *cw, *aw; - const bc7_mode_info *info; - - /* mode is the number of unset bits before the first set bit: */ - if (!mode) { - /* degenerate case when no bits set */ - for (i = 0; i < 16; i++) { - col[i].r = col[i].g = col[i].b = 0; - col[i].a = 255; - } - return; - } - while (!(mode & (1 << bit++))) ; - mode = bit - 1; - info = &bc7_modes[mode]; - /* color selection bits: {subset}{endpoint} */ - cb = info->cb; - ab = info->ab; - cw = bc7_get_weights(info->ib); - aw = bc7_get_weights((ab && info->ib2) ? info->ib2 : info->ib); - -#define LOAD(DST, N) \ - DST = get_bits(src, bit, N); \ - bit += N; - LOAD(partition, info->pb); - LOAD(rotation, info->rb); - LOAD(index_sel, info->isb); - numep = info->ns << 1; - - /* red */ - for (i = 0; i < numep; i++) { - LOAD(val, cb); - endpoints[i].r = val; - } - - /* green */ - for (i = 0; i < numep; i++) { - LOAD(val, cb); - endpoints[i].g = val; - } - - /* blue */ - for (i = 0; i < numep; i++) { - LOAD(val, cb); - endpoints[i].b = val; - } - - /* alpha */ - for (i = 0; i < numep; i++) { - if (ab) { - LOAD(val, ab); - } else { - val = 255; - } - endpoints[i].a = val; - } - - /* p-bits */ +static void +decode_bc7_block(rgba *col, const UINT8 *src) { + rgba endpoints[6]; + int bit = 0, cibit, aibit; + int mode = src[0]; + int i, j; + int numep, cb, ab, ib, ib2, i0, i1, s; + UINT8 index_sel, partition, rotation, val; + const char *cw, *aw; + const bc7_mode_info *info; + + /* mode is the number of unset bits before the first set bit: */ + if (!mode) { + /* degenerate case when no bits set */ + for (i = 0; i < 16; i++) { + col[i].r = col[i].g = col[i].b = 0; + col[i].a = 255; + } + return; + } + while (!(mode & (1 << bit++))) + ; + mode = bit - 1; + info = &bc7_modes[mode]; + /* color selection bits: {subset}{endpoint} */ + cb = info->cb; + ab = info->ab; + cw = bc7_get_weights(info->ib); + aw = bc7_get_weights((ab && info->ib2) ? info->ib2 : info->ib); + +#define LOAD(DST, N) \ + DST = get_bits(src, bit, N); \ + bit += N; + LOAD(partition, info->pb); + LOAD(rotation, info->rb); + LOAD(index_sel, info->isb); + numep = info->ns << 1; + + /* red */ + for (i = 0; i < numep; i++) { + LOAD(val, cb); + endpoints[i].r = val; + } + + /* green */ + for (i = 0; i < numep; i++) { + LOAD(val, cb); + endpoints[i].g = val; + } + + /* blue */ + for (i = 0; i < numep; i++) { + LOAD(val, cb); + endpoints[i].b = val; + } + + /* alpha */ + for (i = 0; i < numep; i++) { + if (ab) { + LOAD(val, ab); + } else { + val = 255; + } + endpoints[i].a = val; + } + + /* p-bits */ #define ASSIGN_P(x) x = (x << 1) | val - if (info->epb) { - /* per endpoint */ - cb++; - if (ab) { - ab++; - } - for (i = 0; i < numep; i++) { - LOAD(val, 1); - ASSIGN_P(endpoints[i].r); - ASSIGN_P(endpoints[i].g); - ASSIGN_P(endpoints[i].b); - if (ab) { - ASSIGN_P(endpoints[i].a); - } - } - } - if (info->spb) { - /* per subset */ - cb++; - if (ab) { - ab++; - } - for (i = 0; i < numep; i+=2) { - LOAD(val, 1); - for (j = 0; j < 2; j++) { - ASSIGN_P(endpoints[i+j].r); - ASSIGN_P(endpoints[i+j].g); - ASSIGN_P(endpoints[i+j].b); - if (ab) { - ASSIGN_P(endpoints[i+j].a); - } - } - } - } + if (info->epb) { + /* per endpoint */ + cb++; + if (ab) { + ab++; + } + for (i = 0; i < numep; i++) { + LOAD(val, 1); + ASSIGN_P(endpoints[i].r); + ASSIGN_P(endpoints[i].g); + ASSIGN_P(endpoints[i].b); + if (ab) { + ASSIGN_P(endpoints[i].a); + } + } + } + if (info->spb) { + /* per subset */ + cb++; + if (ab) { + ab++; + } + for (i = 0; i < numep; i += 2) { + LOAD(val, 1); + for (j = 0; j < 2; j++) { + ASSIGN_P(endpoints[i + j].r); + ASSIGN_P(endpoints[i + j].g); + ASSIGN_P(endpoints[i + j].b); + if (ab) { + ASSIGN_P(endpoints[i + j].a); + } + } + } + } #undef ASSIGN_P #define EXPAND(x, b) x = expand_quantized(x, b) - for (i = 0; i < numep; i++) { - EXPAND(endpoints[i].r, cb); - EXPAND(endpoints[i].g, cb); - EXPAND(endpoints[i].b, cb); - if (ab) { - EXPAND(endpoints[i].a, ab); - } - } + for (i = 0; i < numep; i++) { + EXPAND(endpoints[i].r, cb); + EXPAND(endpoints[i].g, cb); + EXPAND(endpoints[i].b, cb); + if (ab) { + EXPAND(endpoints[i].a, ab); + } + } #undef EXPAND #undef LOAD - cibit = bit; - aibit = cibit + 16 * info->ib - info->ns; - for (i = 0; i < 16; i++) { - s = bc7_get_subset(info->ns, partition, i) << 1; - ib = info->ib; - if (i == 0) { - ib--; - } else if (info->ns == 2) { - if (i == bc7_ai0[partition]) { - ib--; - } - } else if (info->ns == 3) { - if (i == bc7_ai1[partition]) { - ib--; - } else if (i == bc7_ai2[partition]) { - ib--; - } - } - i0 = get_bits(src, cibit, ib); - cibit += ib; - - if (ab && info->ib2) { - ib2 = info->ib2; - if (ib2 && i == 0) { - ib2--; - } - i1 = get_bits(src, aibit, ib2); - aibit += ib2; - if (index_sel) { - bc7_lerp(&col[i], &endpoints[s], aw[i1], cw[i0]); - } else { - bc7_lerp(&col[i], &endpoints[s], cw[i0], aw[i1]); - } - } else { - bc7_lerp(&col[i], &endpoints[s], cw[i0], cw[i0]); - } + cibit = bit; + aibit = cibit + 16 * info->ib - info->ns; + for (i = 0; i < 16; i++) { + s = bc7_get_subset(info->ns, partition, i) << 1; + ib = info->ib; + if (i == 0) { + ib--; + } else if (info->ns == 2) { + if (i == bc7_ai0[partition]) { + ib--; + } + } else if (info->ns == 3) { + if (i == bc7_ai1[partition]) { + ib--; + } else if (i == bc7_ai2[partition]) { + ib--; + } + } + i0 = get_bits(src, cibit, ib); + cibit += ib; + + if (ab && info->ib2) { + ib2 = info->ib2; + if (ib2 && i == 0) { + ib2--; + } + i1 = get_bits(src, aibit, ib2); + aibit += ib2; + if (index_sel) { + bc7_lerp(&col[i], &endpoints[s], aw[i1], cw[i0]); + } else { + bc7_lerp(&col[i], &endpoints[s], cw[i0], aw[i1]); + } + } else { + bc7_lerp(&col[i], &endpoints[s], cw[i0], cw[i0]); + } #define ROTATE(x, y) \ - val = x; \ - x = y; \ - y = val - if (rotation == 1) { - ROTATE(col[i].r, col[i].a); - } else if (rotation == 2) { - ROTATE(col[i].g, col[i].a); - } else if (rotation == 3) { - ROTATE(col[i].b, col[i].a); - } + val = x; \ + x = y; \ + y = val + if (rotation == 1) { + ROTATE(col[i].r, col[i].a); + } else if (rotation == 2) { + ROTATE(col[i].g, col[i].a); + } else if (rotation == 3) { + ROTATE(col[i].b, col[i].a); + } #undef ROTATE - } + } } /* BC6 */ typedef struct { - char ns; /* number of subsets (also called regions) */ - char tr; /* whether endpoints are delta-compressed */ - char pb; /* partition bits */ - char epb; /* endpoint bits */ - char rb; /* red bits (delta) */ - char gb; /* green bits (delta) */ - char bb; /* blue bits (delta) */ + char ns; /* number of subsets (also called regions) */ + char tr; /* whether endpoints are delta-compressed */ + char pb; /* partition bits */ + char epb; /* endpoint bits */ + char rb; /* red bits (delta) */ + char gb; /* green bits (delta) */ + char bb; /* blue bits (delta) */ } bc6_mode_info; static const bc6_mode_info bc6_modes[] = { - // 00 - {2, 1, 5, 10, 5, 5, 5}, - // 01 - {2, 1, 5, 7, 6, 6, 6}, - // 10 - {2, 1, 5, 11, 5, 4, 4}, - {2, 1, 5, 11, 4, 5, 4}, - {2, 1, 5, 11, 4, 4, 5}, - {2, 1, 5, 9, 5, 5, 5}, - {2, 1, 5, 8, 6, 5, 5}, - {2, 1, 5, 8, 5, 6, 5}, - {2, 1, 5, 8, 5, 5, 6}, - {2, 0, 5, 6, 6, 6, 6}, - // 11 - {1, 0, 0, 10, 10, 10, 10}, - {1, 1, 0, 11, 9, 9, 9}, - {1, 1, 0, 12, 8, 8, 8}, - {1, 1, 0, 16, 4, 4, 4} -}; + // 00 + {2, 1, 5, 10, 5, 5, 5}, + // 01 + {2, 1, 5, 7, 6, 6, 6}, + // 10 + {2, 1, 5, 11, 5, 4, 4}, + {2, 1, 5, 11, 4, 5, 4}, + {2, 1, 5, 11, 4, 4, 5}, + {2, 1, 5, 9, 5, 5, 5}, + {2, 1, 5, 8, 6, 5, 5}, + {2, 1, 5, 8, 5, 6, 5}, + {2, 1, 5, 8, 5, 5, 6}, + {2, 0, 5, 6, 6, 6, 6}, + // 11 + {1, 0, 0, 10, 10, 10, 10}, + {1, 1, 0, 11, 9, 9, 9}, + {1, 1, 0, 12, 8, 8, 8}, + {1, 1, 0, 16, 4, 4, 4}}; /* Table.F, encoded as a sequence of bit indices */ static const UINT8 bc6_bit_packings[][75] = { - {116, 132, 176, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, - 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, - 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, 172, 160, 161, 162, 163, 80, - 81, 82, 83, 84, 173, 128, 129, 130, 131, 96, 97, 98, 99, 100, 174, 144, - 145, 146, 147, 148, 175}, - {117, 164, 165, 0, 1, 2, 3, 4, 5, 6, 172, 173, 132, 16, 17, 18, 19, 20, 21, - 22, 133, 174, 116, 32, 33, 34, 35, 36, 37, 38, 175, 177, 176, 48, 49, 50, - 51, 52, 53, 112, 113, 114, 115, 64, 65, 66, 67, 68, 69, 160, 161, 162, 163, - 80, 81, 82, 83, 84, 85, 128, 129, 130, 131, 96, 97, 98, 99, 100, 101, 144, - 145, 146, 147, 148, 149}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, - 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 10, 112, 113, 114, - 115, 64, 65, 66, 67, 26, 172, 160, 161, 162, 163, 80, 81, 82, 83, 42, 173, - 128, 129, 130, 131, 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, - 175}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, - 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 10, 164, 112, 113, 114, - 115, 64, 65, 66, 67, 68, 26, 160, 161, 162, 163, 80, 81, 82, 83, 42, 173, - 128, 129, 130, 131, 96, 97, 98, 99, 172, 174, 144, 145, 146, 147, 116, - 175}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, - 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 10, 132, 112, 113, 114, - 115, 64, 65, 66, 67, 26, 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 42, - 128, 129, 130, 131, 96, 97, 98, 99, 173, 174, 144, 145, 146, 147, 176, - 175}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 132, 16, 17, 18, 19, 20, 21, 22, 23, 24, 116, - 32, 33, 34, 35, 36, 37, 38, 39, 40, 176, 48, 49, 50, 51, 52, 164, 112, 113, - 114, 115, 64, 65, 66, 67, 68, 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, - 173, 128, 129, 130, 131, 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, - 175}, - {0, 1, 2, 3, 4, 5, 6, 7, 164, 132, 16, 17, 18, 19, 20, 21, 22, 23, 174, 116, - 32, 33, 34, 35, 36, 37, 38, 39, 175, 176, 48, 49, 50, 51, 52, 53, 112, 113, - 114, 115, 64, 65, 66, 67, 68, 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, - 173, 128, 129, 130, 131, 96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, - 149}, - {0, 1, 2, 3, 4, 5, 6, 7, 172, 132, 16, 17, 18, 19, 20, 21, 22, 23, 117, 116, - 32, 33, 34, 35, 36, 37, 38, 39, 165, 176, 48, 49, 50, 51, 52, 164, 112, - 113, 114, 115, 64, 65, 66, 67, 68, 69, 160, 161, 162, 163, 80, 81, 82, 83, - 84, 173, 128, 129, 130, 131, 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, - 148, 175}, - {0, 1, 2, 3, 4, 5, 6, 7, 173, 132, 16, 17, 18, 19, 20, 21, 22, 23, 133, 116, - 32, 33, 34, 35, 36, 37, 38, 39, 177, 176, 48, 49, 50, 51, 52, 164, 112, - 113, 114, 115, 64, 65, 66, 67, 68, 172, 160, 161, 162, 163, 80, 81, 82, 83, - 84, 85, 128, 129, 130, 131, 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, - 148, 175}, - {0, 1, 2, 3, 4, 5, 164, 172, 173, 132, 16, 17, 18, 19, 20, 21, 117, 133, - 174, 116, 32, 33, 34, 35, 36, 37, 165, 175, 177, 176, 48, 49, 50, 51, 52, - 53, 112, 113, 114, 115, 64, 65, 66, 67, 68, 69, 160, 161, 162, 163, 80, 81, - 82, 83, 84, 85, 128, 129, 130, 131, 96, 97, 98, 99, 100, 101, 144, 145, - 146, 147, 148, 149}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, - 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, - 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 80, 81, 82, 83, 84, 85, 86, 87, 88, - 89}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, - 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 56, 10, - 64, 65, 66, 67, 68, 69, 70, 71, 72, 26, 80, 81, 82, 83, 84, 85, 86, 87, 88, - 42}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, - 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 11, 10, - 64, 65, 66, 67, 68, 69, 70, 71, 27, 26, 80, 81, 82, 83, 84, 85, 86, 87, 43, - 42}, - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, - 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 15, 14, 13, 12, 11, 10, - 64, 65, 66, 67, 31, 30, 29, 28, 27, 26, 80, 81, 82, 83, 47, 46, 45, 44, 43, - 42}}; - -static void bc6_sign_extend(UINT16 *v, int prec) { - int x = *v; - if (x & (1 << (prec - 1))) { - x |= -1 << prec; - } - *v = (UINT16)x; + {116, 132, 180, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, + 66, 67, 68, 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, + 129, 130, 131, 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, + {117, 164, 165, 0, 1, 2, 3, 4, 5, 6, 176, 177, 132, 16, 17, + 18, 19, 20, 21, 22, 133, 178, 116, 32, 33, 34, 35, 36, 37, 38, + 179, 181, 180, 48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65, + 66, 67, 68, 69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, + 129, 130, 131, 96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, + 48, 49, 50, 51, 52, 10, 112, 113, 114, 115, 64, 65, 66, 67, 26, + 176, 160, 161, 162, 163, 80, 81, 82, 83, 42, 177, 128, 129, 130, 131, + 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, + 48, 49, 50, 51, 10, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, + 26, 160, 161, 162, 163, 80, 81, 82, 83, 42, 177, 128, 129, 130, 131, + 96, 97, 98, 99, 176, 178, 144, 145, 146, 147, 116, 179}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, + 48, 49, 50, 51, 10, 132, 112, 113, 114, 115, 64, 65, 66, 67, 26, + 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 42, 128, 129, 130, 131, + 96, 97, 98, 99, 177, 178, 144, 145, 146, 147, 180, 179}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 132, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 116, 32, 33, 34, 35, 36, 37, 38, 39, 40, 180, + 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, + 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131, + 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, + {0, 1, 2, 3, 4, 5, 6, 7, 164, 132, 16, 17, 18, 19, 20, + 21, 22, 23, 178, 116, 32, 33, 34, 35, 36, 37, 38, 39, 179, 180, + 48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65, 66, 67, 68, + 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131, + 96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149}, + {0, 1, 2, 3, 4, 5, 6, 7, 176, 132, 16, 17, 18, 19, 20, + 21, 22, 23, 117, 116, 32, 33, 34, 35, 36, 37, 38, 39, 165, 180, + 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, + 69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131, + 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, + {0, 1, 2, 3, 4, 5, 6, 7, 177, 132, 16, 17, 18, 19, 20, + 21, 22, 23, 133, 116, 32, 33, 34, 35, 36, 37, 38, 39, 181, 180, + 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68, + 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, 129, 130, 131, + 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179}, + {0, 1, 2, 3, 4, 5, 164, 176, 177, 132, 16, 17, 18, 19, 20, + 21, 117, 133, 178, 116, 32, 33, 34, 35, 36, 37, 165, 179, 181, 180, + 48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65, 66, 67, 68, + 69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, 129, 130, 131, + 96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 56, 10, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 26, 80, 81, 82, 83, 84, 85, 86, 87, 88, 42}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 11, 10, + 64, 65, 66, 67, 68, 69, 70, 71, 27, 26, 80, 81, 82, 83, 84, 85, 86, 87, 43, 42}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 15, 14, 13, 12, 11, 10, + 64, 65, 66, 67, 31, 30, 29, 28, 27, 26, 80, 81, 82, 83, 47, 46, 45, 44, 43, 42}}; + +static void +bc6_sign_extend(UINT16 *v, int prec) { + int x = *v; + if (x & (1 << (prec - 1))) { + x |= -1 << prec; + } + *v = (UINT16)x; +} + +static int +bc6_unquantize(UINT16 v, int prec, int sign) { + int s = 0; + int x; + if (!sign) { + x = v; + if (prec >= 15) { + return x; + } + if (x == 0) { + return 0; + } + if (x == ((1 << prec) - 1)) { + return 0xffff; + } + return ((x << 15) + 0x4000) >> (prec - 1); + } else { + x = (INT16)v; + if (prec >= 16) { + return x; + } + if (x < 0) { + s = 1; + x = -x; + } + + if (x != 0) { + if (x >= ((1 << (prec - 1)) - 1)) { + x = 0x7fff; + } else { + x = ((x << 15) + 0x4000) >> (prec - 1); + } + } + + if (s) { + return -x; + } + return x; + } } -static int bc6_unquantize(UINT16 v, int prec, int sign) { - int s = 0; - int x; - if (!sign) { - x = v; - if (prec >= 15) return x; - if (x == 0) return 0; - if (x == ((1 << prec) - 1)) { - return 0xffff; - } - return ((x << 15) + 0x4000) >> (prec - 1); - } else { - x = (INT16)v; - if (prec >= 16) return x; - if (x < 0) { - s = 1; - x = -x; - } - - if (x != 0) { - if (x >= ((1 << (prec - 1)) - 1)) { - x = 0x7fff; - } else { - x = ((x << 15) + 0x4000) >> (prec - 1); - } - } - - if (s) { - return -x; - } - return x; - } +static float +half_to_float(UINT16 h) { + /* https://gist.github.com/rygorous/2144712 */ + union { + UINT32 u; + float f; + } o, m; + m.u = 0x77800000; + o.u = (h & 0x7fff) << 13; + o.f *= m.f; + m.u = 0x47800000; + if (o.f >= m.f) { + o.u |= 255 << 23; + } + o.u |= (h & 0x8000) << 16; + return o.f; } -static float half_to_float(UINT16 h) { - /* https://gist.github.com/rygorous/2144712 */ - union { - UINT32 u; - float f; - } o, m; - m.u = 0x77800000; - o.u = (h & 0x7fff) << 13; - o.f *= m.f; - m.u = 0x47800000; - if (o.f >= m.f) { - o.u |= 255 << 23; - } - o.u |= (h & 0x8000) << 16; - return o.f; +static float +bc6_finalize(int v, int sign) { + if (sign) { + if (v < 0) { + v = ((-v) * 31) / 32; + return half_to_float((UINT16)(0x8000 | v)); + } else { + return half_to_float((UINT16)((v * 31) / 32)); + } + } else { + return half_to_float((UINT16)((v * 31) / 64)); + } } -static float bc6_finalize(int v, int sign) { - if (sign) { - if (v < 0) { - v = ((-v) * 31) / 32; - return half_to_float((UINT16)(0x8000 | v)); - } else { - return half_to_float((UINT16)((v * 31) / 32)); - } - } else { - return half_to_float((UINT16)((v * 31) / 64)); - } +static UINT8 +bc6_clamp(float value) { + if (value < 0.0f) { + return 0; + } else if (value > 1.0f) { + return 255; + } else { + return (UINT8) (value * 255.0f); + } } -static void bc6_lerp(rgb32f *col, int *e0, int *e1, int s, int sign) { - int r, g, b; - int t = 64 - s; - r = (e0[0] * t + e1[0] * s) >> 6; - g = (e0[1] * t + e1[1] * s) >> 6; - b = (e0[2] * t + e1[2] * s) >> 6; - col->r = bc6_finalize(r, sign); - col->g = bc6_finalize(g, sign); - col->b = bc6_finalize(b, sign); +static void +bc6_lerp(rgba *col, int *e0, int *e1, int s, int sign) { + int r, g, b; + int t = 64 - s; + r = (e0[0] * t + e1[0] * s) >> 6; + g = (e0[1] * t + e1[1] * s) >> 6; + b = (e0[2] * t + e1[2] * s) >> 6; + col->r = bc6_clamp(bc6_finalize(r, sign)); + col->g = bc6_clamp(bc6_finalize(g, sign)); + col->b = bc6_clamp(bc6_finalize(b, sign)); } -static void decode_bc6_block(rgb32f *col, const UINT8* src, int sign) { - UINT16 endpoints[12]; /* storage for r0, g0, b0, r1, ... */ - int ueps[12]; - int i, i0, ib2, di, dw, mask, numep, s; - UINT8 partition; - const bc6_mode_info *info; - const char *cw; - int bit = 5; - int epbits = 75; - int ib = 3; - int mode = src[0] & 0x1f; - if ((mode & 3) == 0 || (mode & 3) == 1) { - mode &= 3; - bit = 2; - } else if ((mode & 3) == 2) { - mode = 2 + (mode >> 2); - epbits = 72; - } else { - mode = 10 + (mode >> 2); - epbits = 60; - ib = 4; - } - if (mode >= 14) { - /* invalid block */ - memset(col, 0, 16 * sizeof(col[0])); - return; - } - info = &bc6_modes[mode]; - cw = bc7_get_weights(ib); - numep = info->ns == 2 ? 12 : 6; - for (i = 0; i < 12; i++) { - endpoints[i] = 0; - } - for (i = 0; i < epbits; i++) { - di = bc6_bit_packings[mode][i]; - dw = di >> 4; - di &= 15; - endpoints[dw] |= (UINT16)get_bit(src, bit + i) << di; - } - bit += epbits; - partition = get_bits(src, bit, info->pb); - bit += info->pb; - mask = (1 << info->epb) - 1; - if (sign) { /* sign-extend e0 if signed */ - bc6_sign_extend(&endpoints[0], info->epb); - bc6_sign_extend(&endpoints[1], info->epb); - bc6_sign_extend(&endpoints[2], info->epb); - } - if (sign || info->tr) { /* sign-extend e1,2,3 if signed or deltas */ - for (i = 3; i < numep; i += 3) { - bc6_sign_extend(&endpoints[i+0], info->rb); - bc6_sign_extend(&endpoints[i+1], info->gb); - bc6_sign_extend(&endpoints[i+2], info->bb); - } - } - if (info->tr) { /* apply deltas */ - for (i = 3; i < numep; i++) { - endpoints[i] = (endpoints[i] + endpoints[0]) & mask; - } - if (sign) { - for (i = 3; i < numep; i += 3) { - bc6_sign_extend(&endpoints[i+0], info->rb); - bc6_sign_extend(&endpoints[i+1], info->gb); - bc6_sign_extend(&endpoints[i+2], info->bb); - } - } - } - for (i = 0; i < numep; i++) { - ueps[i] = bc6_unquantize(endpoints[i], info->epb, sign); - } - for (i = 0; i < 16; i++) { - s = bc7_get_subset(info->ns, partition, i) * 6; - ib2 = ib; - if (i == 0) { - ib2--; - } else if (info->ns == 2) { - if (i == bc7_ai0[partition]) { - ib2--; - } - } - i0 = get_bits(src, bit, ib2); - bit += ib2; - - bc6_lerp(&col[i], &ueps[s], &ueps[s+3], cw[i0], sign); - } +static void +decode_bc6_block(rgba *col, const UINT8 *src, int sign) { + UINT16 endpoints[12]; /* storage for r0, g0, b0, r1, ... */ + int ueps[12]; + int i, i0, ib2, di, dw, mask, numep, s; + UINT8 partition; + const bc6_mode_info *info; + const char *cw; + int bit = 5; + int epbits = 75; + int ib = 3; + int mode = src[0] & 0x1f; + if ((mode & 3) == 0 || (mode & 3) == 1) { + mode &= 3; + bit = 2; + } else if ((mode & 3) == 2) { + mode = 2 + (mode >> 2); + epbits = 72; + } else { + mode = 10 + (mode >> 2); + epbits = 60; + ib = 4; + } + if (mode >= 14) { + /* invalid block */ + memset(col, 0, 16 * sizeof(col[0])); + return; + } + info = &bc6_modes[mode]; + cw = bc7_get_weights(ib); + numep = info->ns == 2 ? 12 : 6; + for (i = 0; i < 12; i++) { + endpoints[i] = 0; + } + for (i = 0; i < epbits; i++) { + di = bc6_bit_packings[mode][i]; + dw = di >> 4; + di &= 15; + endpoints[dw] |= (UINT16)get_bit(src, bit + i) << di; + } + bit += epbits; + partition = get_bits(src, bit, info->pb); + bit += info->pb; + mask = (1 << info->epb) - 1; + if (sign) { /* sign-extend e0 if signed */ + bc6_sign_extend(&endpoints[0], info->epb); + bc6_sign_extend(&endpoints[1], info->epb); + bc6_sign_extend(&endpoints[2], info->epb); + } + if (sign || info->tr) { /* sign-extend e1,2,3 if signed or deltas */ + for (i = 3; i < numep; i += 3) { + bc6_sign_extend(&endpoints[i], info->rb); + bc6_sign_extend(&endpoints[i + 1], info->gb); + bc6_sign_extend(&endpoints[i + 2], info->bb); + } + } + if (info->tr) { /* apply deltas */ + for (i = 3; i < numep; i += 3) { + endpoints[i] = (endpoints[i] + endpoints[0]) & mask; + endpoints[i + 1] = (endpoints[i + 1] + endpoints[1]) & mask; + endpoints[i + 2] = (endpoints[i + 2] + endpoints[2]) & mask; + } + } + for (i = 0; i < numep; i++) { + ueps[i] = bc6_unquantize(endpoints[i], info->epb, sign); + } + for (i = 0; i < 16; i++) { + s = bc7_get_subset(info->ns, partition, i) * 6; + ib2 = ib; + if (i == 0) { + ib2--; + } else if (info->ns == 2) { + if (i == bc7_ai0[partition]) { + ib2--; + } + } + i0 = get_bits(src, bit, ib2); + bit += ib2; + + bc6_lerp(&col[i], &ueps[s], &ueps[s + 3], cw[i0], sign); + } } -static void put_block(Imaging im, ImagingCodecState state, const char *col, int sz, int C) { - int width = state->xsize; - int height = state->ysize; - int xmax = width + state->xoff; - int ymax = height + state->yoff; - int j, i, y, x; - char *dst; - for (j = 0; j < 4; j++) { - y = state->y + j; - if (C) { - if (y >= height) { - continue; - } - if (state->ystep < 0) { - y = state->yoff + ymax - y - 1; - } - dst = im->image[y]; - for (i = 0; i < 4; i++) { - x = state->x + i; - if (x >= width) { - continue; - } - memcpy(dst + sz*x, col + sz*(j*4 + i), sz); - } - } else { - if (state->ystep < 0) { - y = state->yoff + ymax - y - 1; - } - x = state->x; - dst = im->image[y] + sz*x; - memcpy(dst, col + sz*(j*4), 4 * sz); - } - } - state->x += 4; - if (state->x >= xmax) { - state->y += 4; - state->x = state->xoff; - } +static void +put_block(Imaging im, ImagingCodecState state, const char *col, int sz, int C) { + int width = state->xsize; + int height = state->ysize; + int xmax = width + state->xoff; + int ymax = height + state->yoff; + int j, i, y, x; + char *dst; + for (j = 0; j < 4; j++) { + y = state->y + j; + if (C) { + if (y >= height) { + continue; + } + if (state->ystep < 0) { + y = state->yoff + ymax - y - 1; + } + dst = im->image[y]; + for (i = 0; i < 4; i++) { + x = state->x + i; + if (x >= width) { + continue; + } + memcpy(dst + sz * x, col + sz * (j * 4 + i), sz); + } + } else { + if (state->ystep < 0) { + y = state->yoff + ymax - y - 1; + } + x = state->x; + dst = im->image[y] + sz * x; + memcpy(dst, col + sz * (j * 4), 4 * sz); + } + } + state->x += 4; + if (state->x >= xmax) { + state->y += 4; + state->x = state->xoff; + } } -static int decode_bcn(Imaging im, ImagingCodecState state, const UINT8* src, int bytes, int N, int C) { - int ymax = state->ysize + state->yoff; - const UINT8 *ptr = src; - switch (N) { -#define DECODE_LOOP(NN, SZ, TY, ...) \ - case NN: \ - while (bytes >= SZ) { \ - TY col[16]; \ - memset(col, 0, 16 * sizeof(col[0])); \ - decode_bc##NN##_block(col, ptr); \ - put_block(im, state, (const char *)col, sizeof(col[0]), C); \ - ptr += SZ; \ - bytes -= SZ; \ - if (state->y >= ymax) return -1; \ - } \ - break - DECODE_LOOP(1, 8, rgba); - DECODE_LOOP(2, 16, rgba); - DECODE_LOOP(3, 16, rgba); - DECODE_LOOP(4, 8, lum); - DECODE_LOOP(5, 16, rgba); - case 6: - while (bytes >= 16) { - rgb32f col[16]; - decode_bc6_block(col, ptr, (state->state >> 4) & 1); - put_block(im, state, (const char *)col, sizeof(col[0]), C); - ptr += 16; - bytes -= 16; - if (state->y >= ymax) return -1; \ - } - break; - DECODE_LOOP(7, 16, rgba); +static int +decode_bcn( + Imaging im, ImagingCodecState state, const UINT8 *src, int bytes, int N, int C, char *pixel_format) { + int ymax = state->ysize + state->yoff; + const UINT8 *ptr = src; + switch (N) { +#define DECODE_LOOP(NN, SZ, TY, ...) \ + case NN: \ + while (bytes >= SZ) { \ + TY col[16]; \ + memset(col, 0, 16 * sizeof(col[0])); \ + decode_bc##NN##_block(col, ptr); \ + put_block(im, state, (const char *)col, sizeof(col[0]), C); \ + ptr += SZ; \ + bytes -= SZ; \ + if (state->y >= ymax) { \ + return -1; \ + } \ + } \ + break + + DECODE_LOOP(1, 8, rgba); + DECODE_LOOP(2, 16, rgba); + DECODE_LOOP(3, 16, rgba); + DECODE_LOOP(4, 8, lum); + case 5: + while (bytes >= 16) { + rgba col[16]; + memset(col, 0, 16 * sizeof(col[0])); + decode_bc5_block(col, ptr, strcmp(pixel_format, "BC5S") == 0 ? 1 : 0); + put_block(im, state, (const char *)col, sizeof(col[0]), C); + ptr += 16; + bytes -= 16; + if (state->y >= ymax) { + return -1; + } + } + break; + case 6: + while (bytes >= 16) { + rgba col[16]; + decode_bc6_block(col, ptr, strcmp(pixel_format, "BC6HS") == 0 ? 1 : 0); + put_block(im, state, (const char *)col, sizeof(col[0]), C); + ptr += 16; + bytes -= 16; + if (state->y >= ymax) { + return -1; + } + } + break; + DECODE_LOOP(7, 16, rgba); #undef DECODE_LOOP - } - return (int)(ptr - src); + } + return (int)(ptr - src); } -int ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) { - int N = state->state & 0xf; - int width = state->xsize; - int height = state->ysize; - if ((width & 3) | (height & 3)) { - return decode_bcn(im, state, buf, bytes, N, 1); - } else { - return decode_bcn(im, state, buf, bytes, N, 0); - } +int +ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + int N = state->state & 0xf; + int width = state->xsize; + int height = state->ysize; + int C = (width & 3) | (height & 3) ? 1 : 0; + char *pixel_format = ((BCNSTATE *)state->context)->pixel_format; + return decode_bcn(im, state, buf, bytes, N, C, pixel_format); } diff --git a/src/libImaging/Bit.h b/src/libImaging/Bit.h index 56e3a17d2e7..f64bfb46990 100644 --- a/src/libImaging/Bit.h +++ b/src/libImaging/Bit.h @@ -1,7 +1,6 @@ /* Bit.h */ typedef struct { - /* CONFIGURATION */ /* Number of bits per pixel */ @@ -19,7 +18,7 @@ typedef struct { /* Lookup table (not implemented) */ unsigned long lutsize; - FLOAT32* lut; + FLOAT32 *lut; /* INTERNAL */ unsigned long mask; diff --git a/src/libImaging/BitDecode.c b/src/libImaging/BitDecode.c index a78183542d2..28baa8b7ea8 100644 --- a/src/libImaging/BitDecode.c +++ b/src/libImaging/BitDecode.c @@ -5,7 +5,7 @@ * decoder for packed bitfields (converts to floating point) * * history: - * 97-05-31 fl created (much more than originally intended) + * 97-05-31 fl created (much more than originally intended) * * Copyright (c) Fredrik Lundh 1997. * Copyright (c) Secret Labs AB 1997. @@ -13,113 +13,112 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" #include "Bit.h" - int -ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ - BITSTATE* bitstate = state->context; - UINT8* ptr; +ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + BITSTATE *bitstate = state->context; + UINT8 *ptr; if (state->state == 0) { - - /* Initialize context variables */ + /* Initialize context variables */ /* this decoder only works for float32 image buffers */ if (im->type != IMAGING_TYPE_FLOAT32) { - state->errcode = IMAGING_CODEC_CONFIG; - return -1; + state->errcode = IMAGING_CODEC_CONFIG; + return -1; } /* sanity check */ if (bitstate->bits < 1 || bitstate->bits >= 32) { - state->errcode = IMAGING_CODEC_CONFIG; - return -1; + state->errcode = IMAGING_CODEC_CONFIG; + return -1; } - bitstate->mask = (1<bits)-1; - - if (bitstate->sign) - bitstate->signmask = (1<<(bitstate->bits-1)); + bitstate->mask = (1 << bitstate->bits) - 1; - /* check image orientation */ - if (state->ystep < 0) { - state->y = state->ysize-1; - state->ystep = -1; - } else - state->ystep = 1; + if (bitstate->sign) { + bitstate->signmask = (1 << (bitstate->bits - 1)); + } - state->state = 1; + /* check image orientation */ + if (state->ystep < 0) { + state->y = state->ysize - 1; + state->ystep = -1; + } else { + state->ystep = 1; + } + state->state = 1; } ptr = buf; while (bytes > 0) { - UINT8 byte = *ptr; ptr++; bytes--; /* get a byte from the input stream and insert in the bit buffer */ - if (bitstate->fill&1) + if (bitstate->fill & 1) { /* fill MSB first */ - bitstate->bitbuffer |= (unsigned long) byte << bitstate->bitcount; - else + bitstate->bitbuffer |= (unsigned long)byte << bitstate->bitcount; + } else { /* fill LSB first */ bitstate->bitbuffer = (bitstate->bitbuffer << 8) | byte; + } bitstate->bitcount += 8; while (bitstate->bitcount >= bitstate->bits) { - /* get a pixel from the bit buffer */ unsigned long data; FLOAT32 pixel; - if (bitstate->fill&2) { + if (bitstate->fill & 2) { /* store LSB first */ data = bitstate->bitbuffer & bitstate->mask; - if (bitstate->bitcount > 32) + if (bitstate->bitcount > 32) { /* bitbuffer overflow; restore it from last input byte */ - bitstate->bitbuffer = byte >> (8 - (bitstate->bitcount - - bitstate->bits)); - else + bitstate->bitbuffer = + byte >> (8 - (bitstate->bitcount - bitstate->bits)); + } else { bitstate->bitbuffer >>= bitstate->bits; - } else + } + } else { /* store MSB first */ - data = (bitstate->bitbuffer >> (bitstate->bitcount - - bitstate->bits)) - & bitstate->mask; + data = (bitstate->bitbuffer >> (bitstate->bitcount - bitstate->bits)) & + bitstate->mask; + } bitstate->bitcount -= bitstate->bits; if (bitstate->lutsize > 0) { /* map through lookup table */ - if (data <= 0) + if (data <= 0) { pixel = bitstate->lut[0]; - else if (data >= bitstate->lutsize) - pixel = bitstate->lut[bitstate->lutsize-1]; - else + } else if (data >= bitstate->lutsize) { + pixel = bitstate->lut[bitstate->lutsize - 1]; + } else { pixel = bitstate->lut[data]; + } } else { /* convert */ - if (data & bitstate->signmask) + if (data & bitstate->signmask) { /* image memory contains signed data */ - pixel = (FLOAT32) (INT32) (data | ~bitstate->mask); - else - pixel = (FLOAT32) data; + pixel = (FLOAT32)(INT32)(data | ~bitstate->mask); + } else { + pixel = (FLOAT32)data; + } } - *(FLOAT32*)(&im->image32[state->y][state->x]) = pixel; + *(FLOAT32 *)(&im->image32[state->y][state->x]) = pixel; /* step forward */ - if (++state->x >= state->xsize) { + if (++state->x >= state->xsize) { /* new line */ state->y += state->ystep; if (state->y < 0 || state->y >= state->ysize) { @@ -128,8 +127,9 @@ ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) } state->x = 0; /* reset bit buffer */ - if (bitstate->pad > 0) + if (bitstate->pad > 0) { bitstate->bitcount = 0; + } } } } diff --git a/src/libImaging/Blend.c b/src/libImaging/Blend.c index 19a080d6d5b..a53ae0fad53 100644 --- a/src/libImaging/Blend.c +++ b/src/libImaging/Blend.c @@ -5,9 +5,9 @@ * interpolate between two existing images * * history: - * 96-03-20 fl Created - * 96-05-18 fl Simplified blend expression - * 96-10-05 fl Fixed expression bug, special case for interpolation + * 96-03-20 fl Created + * 96-05-18 fl Simplified blend expression + * 96-10-05 fl Fixed expression bug, special case for interpolation * * Copyright (c) Fredrik Lundh 1996. * Copyright (c) Secret Labs AB 1997. @@ -15,65 +15,64 @@ * See the README file for details on usage and redistribution. */ - #include "Imaging.h" - Imaging -ImagingBlend(Imaging imIn1, Imaging imIn2, float alpha) -{ +ImagingBlend(Imaging imIn1, Imaging imIn2, float alpha) { Imaging imOut; int x, y; /* Check arguments */ - if (!imIn1 || !imIn2 || imIn1->type != IMAGING_TYPE_UINT8 - || imIn1->palette || strcmp(imIn1->mode, "1") == 0 - || imIn2->palette || strcmp(imIn2->mode, "1") == 0) + if (!imIn1 || !imIn2 || imIn1->type != IMAGING_TYPE_UINT8 || imIn1->palette || + strcmp(imIn1->mode, "1") == 0 || imIn2->palette || + strcmp(imIn2->mode, "1") == 0) { return ImagingError_ModeError(); + } - if (imIn1->type != imIn2->type || - imIn1->bands != imIn2->bands || - imIn1->xsize != imIn2->xsize || - imIn1->ysize != imIn2->ysize) - return ImagingError_Mismatch(); + if (imIn1->type != imIn2->type || imIn1->bands != imIn2->bands || + imIn1->xsize != imIn2->xsize || imIn1->ysize != imIn2->ysize) { + return ImagingError_Mismatch(); + } /* Shortcuts */ - if (alpha == 0.0) - return ImagingCopy(imIn1); - else if (alpha == 1.0) - return ImagingCopy(imIn2); + if (alpha == 0.0) { + return ImagingCopy(imIn1); + } else if (alpha == 1.0) { + return ImagingCopy(imIn2); + } imOut = ImagingNewDirty(imIn1->mode, imIn1->xsize, imIn1->ysize); - if (!imOut) - return NULL; + if (!imOut) { + return NULL; + } if (alpha >= 0 && alpha <= 1.0) { - /* Interpolate between bands */ - for (y = 0; y < imIn1->ysize; y++) { - UINT8* in1 = (UINT8*) imIn1->image[y]; - UINT8* in2 = (UINT8*) imIn2->image[y]; - UINT8* out = (UINT8*) imOut->image[y]; - for (x = 0; x < imIn1->linesize; x++) - out[x] = (UINT8) - ((int) in1[x] + alpha * ((int) in2[x] - (int) in1[x])); - } + /* Interpolate between bands */ + for (y = 0; y < imIn1->ysize; y++) { + UINT8 *in1 = (UINT8 *)imIn1->image[y]; + UINT8 *in2 = (UINT8 *)imIn2->image[y]; + UINT8 *out = (UINT8 *)imOut->image[y]; + for (x = 0; x < imIn1->linesize; x++) { + out[x] = (UINT8)((int)in1[x] + alpha * ((int)in2[x] - (int)in1[x])); + } + } } else { - /* Extrapolation; must make sure to clip resulting values */ - for (y = 0; y < imIn1->ysize; y++) { - UINT8* in1 = (UINT8*) imIn1->image[y]; - UINT8* in2 = (UINT8*) imIn2->image[y]; - UINT8* out = (UINT8*) imOut->image[y]; - for (x = 0; x < imIn1->linesize; x++) { - float temp = (float) - ((int) in1[x] + alpha * ((int) in2[x] - (int) in1[x])); - if (temp <= 0.0) - out[x] = 0; - else if (temp >= 255.0) - out[x] = 255; - else - out[x] = (UINT8) temp; - } - } + /* Extrapolation; must make sure to clip resulting values */ + for (y = 0; y < imIn1->ysize; y++) { + UINT8 *in1 = (UINT8 *)imIn1->image[y]; + UINT8 *in2 = (UINT8 *)imIn2->image[y]; + UINT8 *out = (UINT8 *)imOut->image[y]; + for (x = 0; x < imIn1->linesize; x++) { + float temp = (float)((int)in1[x] + alpha * ((int)in2[x] - (int)in1[x])); + if (temp <= 0.0) { + out[x] = 0; + } else if (temp >= 255.0) { + out[x] = 255; + } else { + out[x] = (UINT8)temp; + } + } + } } return imOut; diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 1a415ed1603..2e45a33587c 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -1,38 +1,40 @@ -#include "Python.h" #include "Imaging.h" - #define MAX(x, y) (((x) > (y)) ? (x) : (y)) #define MIN(x, y) (((x) < (y)) ? (x) : (y)) - typedef UINT8 pixel[4]; -void static inline -ImagingLineBoxBlur32(pixel *lineOut, pixel *lineIn, int lastx, int radius, int edgeA, - int edgeB, UINT32 ww, UINT32 fw) -{ +void static inline ImagingLineBoxBlur32( + pixel *lineOut, + pixel *lineIn, + int lastx, + int radius, + int edgeA, + int edgeB, + UINT32 ww, + UINT32 fw) { int x; UINT32 acc[4]; UINT32 bulk[4]; - #define MOVE_ACC(acc, subtract, add) \ - acc[0] += lineIn[add][0] - lineIn[subtract][0]; \ - acc[1] += lineIn[add][1] - lineIn[subtract][1]; \ - acc[2] += lineIn[add][2] - lineIn[subtract][2]; \ - acc[3] += lineIn[add][3] - lineIn[subtract][3]; +#define MOVE_ACC(acc, subtract, add) \ + acc[0] += lineIn[add][0] - lineIn[subtract][0]; \ + acc[1] += lineIn[add][1] - lineIn[subtract][1]; \ + acc[2] += lineIn[add][2] - lineIn[subtract][2]; \ + acc[3] += lineIn[add][3] - lineIn[subtract][3]; - #define ADD_FAR(bulk, acc, left, right) \ - bulk[0] = (acc[0] * ww) + (lineIn[left][0] + lineIn[right][0]) * fw; \ - bulk[1] = (acc[1] * ww) + (lineIn[left][1] + lineIn[right][1]) * fw; \ - bulk[2] = (acc[2] * ww) + (lineIn[left][2] + lineIn[right][2]) * fw; \ - bulk[3] = (acc[3] * ww) + (lineIn[left][3] + lineIn[right][3]) * fw; +#define ADD_FAR(bulk, acc, left, right) \ + bulk[0] = (acc[0] * ww) + (lineIn[left][0] + lineIn[right][0]) * fw; \ + bulk[1] = (acc[1] * ww) + (lineIn[left][1] + lineIn[right][1]) * fw; \ + bulk[2] = (acc[2] * ww) + (lineIn[left][2] + lineIn[right][2]) * fw; \ + bulk[3] = (acc[3] * ww) + (lineIn[left][3] + lineIn[right][3]) * fw; - #define SAVE(x, bulk) \ - lineOut[x][0] = (UINT8)((bulk[0] + (1 << 23)) >> 24); \ - lineOut[x][1] = (UINT8)((bulk[1] + (1 << 23)) >> 24); \ - lineOut[x][2] = (UINT8)((bulk[2] + (1 << 23)) >> 24); \ - lineOut[x][3] = (UINT8)((bulk[3] + (1 << 23)) >> 24); +#define SAVE(x, bulk) \ + lineOut[x][0] = (UINT8)((bulk[0] + (1 << 23)) >> 24); \ + lineOut[x][1] = (UINT8)((bulk[1] + (1 << 23)) >> 24); \ + lineOut[x][2] = (UINT8)((bulk[2] + (1 << 23)) >> 24); \ + lineOut[x][3] = (UINT8)((bulk[3] + (1 << 23)) >> 24); /* Compute acc for -1 pixel (outside of image): From "-radius-1" to "-1" get first pixel, @@ -54,8 +56,7 @@ ImagingLineBoxBlur32(pixel *lineOut, pixel *lineIn, int lastx, int radius, int e acc[2] += lineIn[lastx][2] * (radius - edgeA + 1); acc[3] += lineIn[lastx][3] * (radius - edgeA + 1); - if (edgeA <= edgeB) - { + if (edgeA <= edgeB) { /* Subtract pixel from left ("0"). Add pixels from radius. */ for (x = 0; x < edgeA; x++) { @@ -77,9 +78,7 @@ ImagingLineBoxBlur32(pixel *lineOut, pixel *lineIn, int lastx, int radius, int e ADD_FAR(bulk, acc, x - radius - 1, lastx); SAVE(x, bulk); } - } - else - { + } else { for (x = 0; x < edgeB; x++) { MOVE_ACC(acc, 0, x + radius); ADD_FAR(bulk, acc, 0, x + radius + 1); @@ -97,28 +96,30 @@ ImagingLineBoxBlur32(pixel *lineOut, pixel *lineIn, int lastx, int radius, int e } } - #undef MOVE_ACC - #undef ADD_FAR - #undef SAVE +#undef MOVE_ACC +#undef ADD_FAR +#undef SAVE } - -void static inline -ImagingLineBoxBlur8(UINT8 *lineOut, UINT8 *lineIn, int lastx, int radius, int edgeA, - int edgeB, UINT32 ww, UINT32 fw) -{ +void static inline ImagingLineBoxBlur8( + UINT8 *lineOut, + UINT8 *lineIn, + int lastx, + int radius, + int edgeA, + int edgeB, + UINT32 ww, + UINT32 fw) { int x; UINT32 acc; UINT32 bulk; - #define MOVE_ACC(acc, subtract, add) \ - acc += lineIn[add] - lineIn[subtract]; +#define MOVE_ACC(acc, subtract, add) acc += lineIn[add] - lineIn[subtract]; - #define ADD_FAR(bulk, acc, left, right) \ - bulk = (acc * ww) + (lineIn[left] + lineIn[right]) * fw; +#define ADD_FAR(bulk, acc, left, right) \ + bulk = (acc * ww) + (lineIn[left] + lineIn[right]) * fw; - #define SAVE(x, bulk) \ - lineOut[x] = (UINT8)((bulk + (1 << 23)) >> 24) +#define SAVE(x, bulk) lineOut[x] = (UINT8)((bulk + (1 << 23)) >> 24) acc = lineIn[0] * (radius + 1); for (x = 0; x < edgeA - 1; x++) { @@ -126,8 +127,7 @@ ImagingLineBoxBlur8(UINT8 *lineOut, UINT8 *lineIn, int lastx, int radius, int ed } acc += lineIn[lastx] * (radius - edgeA + 1); - if (edgeA <= edgeB) - { + if (edgeA <= edgeB) { for (x = 0; x < edgeA; x++) { MOVE_ACC(acc, 0, x + radius); ADD_FAR(bulk, acc, 0, x + radius + 1); @@ -143,9 +143,7 @@ ImagingLineBoxBlur8(UINT8 *lineOut, UINT8 *lineIn, int lastx, int radius, int ed ADD_FAR(bulk, acc, x - radius - 1, lastx); SAVE(x, bulk); } - } - else - { + } else { for (x = 0; x < edgeB; x++) { MOVE_ACC(acc, 0, x + radius); ADD_FAR(bulk, acc, 0, x + radius + 1); @@ -163,61 +161,60 @@ ImagingLineBoxBlur8(UINT8 *lineOut, UINT8 *lineIn, int lastx, int radius, int ed } } - #undef MOVE_ACC - #undef ADD_FAR - #undef SAVE +#undef MOVE_ACC +#undef ADD_FAR +#undef SAVE } - - Imaging -ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) -{ +ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) { ImagingSectionCookie cookie; int y; - int radius = (int) floatRadius; - UINT32 ww = (UINT32) (1 << 24) / (floatRadius * 2 + 1); + int radius = (int)floatRadius; + UINT32 ww = (UINT32)(1 << 24) / (floatRadius * 2 + 1); UINT32 fw = ((1 << 24) - (radius * 2 + 1) * ww) / 2; int edgeA = MIN(radius + 1, imIn->xsize); int edgeB = MAX(imIn->xsize - radius - 1, 0); UINT32 *lineOut = calloc(imIn->xsize, sizeof(UINT32)); - if (lineOut == NULL) + if (lineOut == NULL) { return ImagingError_MemoryError(); + } // printf(">>> %d %d %d\n", radius, ww, fw); ImagingSectionEnter(&cookie); - if (imIn->image8) - { + if (imIn->image8) { for (y = 0; y < imIn->ysize; y++) { ImagingLineBoxBlur8( - (imIn == imOut ? (UINT8 *) lineOut : imOut->image8[y]), + (imIn == imOut ? (UINT8 *)lineOut : imOut->image8[y]), imIn->image8[y], imIn->xsize - 1, - radius, edgeA, edgeB, - ww, fw - ); + radius, + edgeA, + edgeB, + ww, + fw); if (imIn == imOut) { // Commit. memcpy(imOut->image8[y], lineOut, imIn->xsize); } } - } - else - { + } else { for (y = 0; y < imIn->ysize; y++) { ImagingLineBoxBlur32( - imIn == imOut ? (pixel *) lineOut : (pixel *) imOut->image32[y], - (pixel *) imIn->image32[y], + imIn == imOut ? (pixel *)lineOut : (pixel *)imOut->image32[y], + (pixel *)imIn->image32[y], imIn->xsize - 1, - radius, edgeA, edgeB, - ww, fw - ); + radius, + edgeA, + edgeB, + ww, + fw); if (imIn == imOut) { // Commit. memcpy(imOut->image32[y], lineOut, imIn->xsize * 4); @@ -232,55 +229,49 @@ ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) return imOut; } - Imaging -ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) -{ +ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) { int i; Imaging imTransposed; if (n < 1) { - return ImagingError_ValueError( - "number of passes must be greater than zero" - ); + return ImagingError_ValueError("number of passes must be greater than zero"); } - if (strcmp(imIn->mode, imOut->mode) || - imIn->type != imOut->type || - imIn->bands != imOut->bands || - imIn->xsize != imOut->xsize || - imIn->ysize != imOut->ysize) + if (strcmp(imIn->mode, imOut->mode) || imIn->type != imOut->type || + imIn->bands != imOut->bands || imIn->xsize != imOut->xsize || + imIn->ysize != imOut->ysize) { return ImagingError_Mismatch(); + } - if (imIn->type != IMAGING_TYPE_UINT8) + if (imIn->type != IMAGING_TYPE_UINT8) { return ImagingError_ModeError(); + } - if (!(strcmp(imIn->mode, "RGB") == 0 || - strcmp(imIn->mode, "RGBA") == 0 || - strcmp(imIn->mode, "RGBa") == 0 || - strcmp(imIn->mode, "RGBX") == 0 || - strcmp(imIn->mode, "CMYK") == 0 || - strcmp(imIn->mode, "L") == 0 || - strcmp(imIn->mode, "LA") == 0 || - strcmp(imIn->mode, "La") == 0)) + if (!(strcmp(imIn->mode, "RGB") == 0 || strcmp(imIn->mode, "RGBA") == 0 || + strcmp(imIn->mode, "RGBa") == 0 || strcmp(imIn->mode, "RGBX") == 0 || + strcmp(imIn->mode, "CMYK") == 0 || strcmp(imIn->mode, "L") == 0 || + strcmp(imIn->mode, "LA") == 0 || strcmp(imIn->mode, "La") == 0)) { return ImagingError_ModeError(); + } imTransposed = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize); - if (!imTransposed) + if (!imTransposed) { return NULL; + } /* Apply blur in one dimension. Use imOut as a destination at first pass, then use imOut as a source too. */ ImagingHorizontalBoxBlur(imOut, imIn, radius); - for (i = 1; i < n; i ++) { + for (i = 1; i < n; i++) { ImagingHorizontalBoxBlur(imOut, imOut, radius); } /* Transpose result for blur in another direction. */ ImagingTranspose(imTransposed, imOut); /* Reuse imTransposed as a source and destination there. */ - for (i = 0; i < n; i ++) { + for (i = 0; i < n; i++) { ImagingHorizontalBoxBlur(imTransposed, imTransposed, radius); } /* Restore original orientation. */ @@ -291,14 +282,12 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) return imOut; } - -Imaging ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, - int passes) -{ +Imaging +ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes) { float sigma2, L, l, a; sigma2 = radius * radius / passes; - // from http://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf + // from https://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf // [7] Box length. L = sqrt(12.0 * sigma2 + 1.0); // [11] Integer part of box radius. diff --git a/src/libImaging/Chops.c b/src/libImaging/Chops.c index 8059b6ffbd6..f9c005efe3a 100644 --- a/src/libImaging/Chops.c +++ b/src/libImaging/Chops.c @@ -16,58 +16,60 @@ * See the README file for details on usage and redistribution. */ - #include "Imaging.h" -#define CHOP(operation, mode)\ - int x, y;\ - Imaging imOut;\ - imOut = create(imIn1, imIn2, mode);\ - if (!imOut)\ - return NULL;\ - for (y = 0; y < imOut->ysize; y++) {\ - UINT8* out = (UINT8*) imOut->image[y];\ - UINT8* in1 = (UINT8*) imIn1->image[y];\ - UINT8* in2 = (UINT8*) imIn2->image[y];\ - for (x = 0; x < imOut->linesize; x++) {\ - int temp = operation;\ - if (temp <= 0)\ - out[x] = 0;\ - else if (temp >= 255)\ - out[x] = 255;\ - else\ - out[x] = temp;\ - }\ - }\ +#define CHOP(operation) \ + int x, y; \ + Imaging imOut; \ + imOut = create(imIn1, imIn2, NULL); \ + if (!imOut) { \ + return NULL; \ + } \ + for (y = 0; y < imOut->ysize; y++) { \ + UINT8 *out = (UINT8 *)imOut->image[y]; \ + UINT8 *in1 = (UINT8 *)imIn1->image[y]; \ + UINT8 *in2 = (UINT8 *)imIn2->image[y]; \ + for (x = 0; x < imOut->linesize; x++) { \ + int temp = operation; \ + if (temp <= 0) { \ + out[x] = 0; \ + } else if (temp >= 255) { \ + out[x] = 255; \ + } else { \ + out[x] = temp; \ + } \ + } \ + } \ return imOut; -#define CHOP2(operation, mode)\ - int x, y;\ - Imaging imOut;\ - imOut = create(imIn1, imIn2, mode);\ - if (!imOut)\ - return NULL;\ - for (y = 0; y < imOut->ysize; y++) {\ - UINT8* out = (UINT8*) imOut->image[y];\ - UINT8* in1 = (UINT8*) imIn1->image[y];\ - UINT8* in2 = (UINT8*) imIn2->image[y];\ - for (x = 0; x < imOut->linesize; x++) {\ - out[x] = operation;\ - }\ - }\ +#define CHOP2(operation, mode) \ + int x, y; \ + Imaging imOut; \ + imOut = create(imIn1, imIn2, mode); \ + if (!imOut) { \ + return NULL; \ + } \ + for (y = 0; y < imOut->ysize; y++) { \ + UINT8 *out = (UINT8 *)imOut->image[y]; \ + UINT8 *in1 = (UINT8 *)imIn1->image[y]; \ + UINT8 *in2 = (UINT8 *)imIn2->image[y]; \ + for (x = 0; x < imOut->linesize; x++) { \ + out[x] = operation; \ + } \ + } \ return imOut; static Imaging -create(Imaging im1, Imaging im2, char* mode) -{ +create(Imaging im1, Imaging im2, char *mode) { int xsize, ysize; if (!im1 || !im2 || im1->type != IMAGING_TYPE_UINT8 || - (mode != NULL && (strcmp(im1->mode, "1") || strcmp(im2->mode, "1")))) - return (Imaging) ImagingError_ModeError(); - if (im1->type != im2->type || - im1->bands != im2->bands) - return (Imaging) ImagingError_Mismatch(); + (mode != NULL && (strcmp(im1->mode, "1") || strcmp(im2->mode, "1")))) { + return (Imaging)ImagingError_ModeError(); + } + if (im1->type != im2->type || im1->bands != im2->bands) { + return (Imaging)ImagingError_Mismatch(); + } xsize = (im1->xsize < im2->xsize) ? im1->xsize : im2->xsize; ysize = (im1->ysize < im2->ysize) ? im1->ysize : im2->ysize; @@ -76,73 +78,85 @@ create(Imaging im1, Imaging im2, char* mode) } Imaging -ImagingChopLighter(Imaging imIn1, Imaging imIn2) -{ - CHOP((in1[x] > in2[x]) ? in1[x] : in2[x], NULL); +ImagingChopLighter(Imaging imIn1, Imaging imIn2) { + CHOP((in1[x] > in2[x]) ? in1[x] : in2[x]); } Imaging -ImagingChopDarker(Imaging imIn1, Imaging imIn2) -{ - CHOP((in1[x] < in2[x]) ? in1[x] : in2[x], NULL); +ImagingChopDarker(Imaging imIn1, Imaging imIn2) { + CHOP((in1[x] < in2[x]) ? in1[x] : in2[x]); } Imaging -ImagingChopDifference(Imaging imIn1, Imaging imIn2) -{ - CHOP(abs((int) in1[x] - (int) in2[x]), NULL); +ImagingChopDifference(Imaging imIn1, Imaging imIn2) { + CHOP(abs((int)in1[x] - (int)in2[x])); } Imaging -ImagingChopMultiply(Imaging imIn1, Imaging imIn2) -{ - CHOP((int) in1[x] * (int) in2[x] / 255, NULL); +ImagingChopMultiply(Imaging imIn1, Imaging imIn2) { + CHOP((int)in1[x] * (int)in2[x] / 255); } Imaging -ImagingChopScreen(Imaging imIn1, Imaging imIn2) -{ - CHOP(255 - ((int) (255 - in1[x]) * (int) (255 - in2[x])) / 255, NULL); +ImagingChopScreen(Imaging imIn1, Imaging imIn2) { + CHOP(255 - ((int)(255 - in1[x]) * (int)(255 - in2[x])) / 255); } Imaging -ImagingChopAdd(Imaging imIn1, Imaging imIn2, float scale, int offset) -{ - CHOP(((int) in1[x] + (int) in2[x]) / scale + offset, NULL); +ImagingChopAdd(Imaging imIn1, Imaging imIn2, float scale, int offset) { + CHOP(((int)in1[x] + (int)in2[x]) / scale + offset); } Imaging -ImagingChopSubtract(Imaging imIn1, Imaging imIn2, float scale, int offset) -{ - CHOP(((int) in1[x] - (int) in2[x]) / scale + offset, NULL); +ImagingChopSubtract(Imaging imIn1, Imaging imIn2, float scale, int offset) { + CHOP(((int)in1[x] - (int)in2[x]) / scale + offset); } Imaging -ImagingChopAnd(Imaging imIn1, Imaging imIn2) -{ +ImagingChopAnd(Imaging imIn1, Imaging imIn2) { CHOP2((in1[x] && in2[x]) ? 255 : 0, "1"); } Imaging -ImagingChopOr(Imaging imIn1, Imaging imIn2) -{ +ImagingChopOr(Imaging imIn1, Imaging imIn2) { CHOP2((in1[x] || in2[x]) ? 255 : 0, "1"); } Imaging -ImagingChopXor(Imaging imIn1, Imaging imIn2) -{ +ImagingChopXor(Imaging imIn1, Imaging imIn2) { CHOP2(((in1[x] != 0) ^ (in2[x] != 0)) ? 255 : 0, "1"); } Imaging -ImagingChopAddModulo(Imaging imIn1, Imaging imIn2) -{ +ImagingChopAddModulo(Imaging imIn1, Imaging imIn2) { CHOP2(in1[x] + in2[x], NULL); } Imaging -ImagingChopSubtractModulo(Imaging imIn1, Imaging imIn2) -{ +ImagingChopSubtractModulo(Imaging imIn1, Imaging imIn2) { CHOP2(in1[x] - in2[x], NULL); } + +Imaging +ImagingChopSoftLight(Imaging imIn1, Imaging imIn2) { + CHOP2( + (((255 - in1[x]) * (in1[x] * in2[x])) / 65536) + + (in1[x] * (255 - ((255 - in1[x]) * (255 - in2[x]) / 255))) / 255, + NULL); +} + +Imaging +ImagingChopHardLight(Imaging imIn1, Imaging imIn2) { + CHOP2( + (in2[x] < 128) ? ((in1[x] * in2[x]) / 127) + : 255 - (((255 - in2[x]) * (255 - in1[x])) / 127), + NULL); +} + +Imaging +ImagingOverlay(Imaging imIn1, Imaging imIn2) { + CHOP2( + (in1[x] < 128) ? ((in1[x] * in2[x]) / 127) + : 255 - (((255 - in1[x]) * (255 - in2[x])) / 127), + NULL); +} diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c new file mode 100644 index 00000000000..aee7cda067d --- /dev/null +++ b/src/libImaging/ColorLUT.c @@ -0,0 +1,187 @@ +#include "Imaging.h" +#include + +/* 8 bits for result. Table can overflow [0, 1.0] range, + so we need extra bits for overflow and negative values. + NOTE: This value should be the same as in _imaging/_prepare_lut_table() */ +#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 + (max size is 65, but index 64 is not reachable) */ +#define SCALE_BITS (32 - 8 - 6) +#define SCALE_MASK ((1 << SCALE_BITS) - 1) + +#define SHIFT_BITS (16 - 1) + +static inline UINT8 +clip8(int in) { + return clip8_lookups[(in + PRECISION_ROUNDING) >> PRECISION_BITS]; +} + +static inline void +interpolate3(INT16 out[3], const INT16 a[3], const INT16 b[3], INT16 shift) { + out[0] = (a[0] * ((1 << SHIFT_BITS) - shift) + b[0] * shift) >> SHIFT_BITS; + out[1] = (a[1] * ((1 << SHIFT_BITS) - shift) + b[1] * shift) >> SHIFT_BITS; + out[2] = (a[2] * ((1 << SHIFT_BITS) - shift) + b[2] * shift) >> SHIFT_BITS; +} + +static inline void +interpolate4(INT16 out[4], const INT16 a[4], const INT16 b[4], INT16 shift) { + out[0] = (a[0] * ((1 << SHIFT_BITS) - shift) + b[0] * shift) >> SHIFT_BITS; + out[1] = (a[1] * ((1 << SHIFT_BITS) - shift) + b[1] * shift) >> SHIFT_BITS; + out[2] = (a[2] * ((1 << SHIFT_BITS) - shift) + b[2] * shift) >> SHIFT_BITS; + out[3] = (a[3] * ((1 << SHIFT_BITS) - shift) + b[3] * shift) >> SHIFT_BITS; +} + +static inline int +table_index3D(int index1D, int index2D, int index3D, int size1D, int size1D_2D) { + return index1D + index2D * size1D + index3D * 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. + Should have 3 or 4 channels. + 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. + Each element is signed 16-bit int where 0 is lowest output value + and 255 << PRECISION_BITS (16320) is highest value. +*/ +Imaging +ImagingColorLUT3D_linear( + Imaging imOut, + Imaging imIn, + int table_channels, + int size1D, + int size2D, + int size3D, + INT16 *table) { + /* This float to int conversion doesn't have rounding + error compensation (+0.5) for two reasons: + 1. As we don't hit the highest value, + we can use one extra bit for precision. + 2. For every pixel, we interpolate 8 elements from the table: + current and +1 for every dimension and their combinations. + If we hit the upper cells from the table, + +1 cells will be outside of the table. + With this compensation we never hit the upper cells + but this also doesn't introduce any noticeable difference. */ + UINT32 scale1D = (size1D - 1) / 255.0 * (1 << SCALE_BITS); + UINT32 scale2D = (size2D - 1) / 255.0 * (1 << SCALE_BITS); + UINT32 scale3D = (size3D - 1) / 255.0 * (1 << SCALE_BITS); + int size1D_2D = size1D * size2D; + int x, y; + ImagingSectionCookie cookie; + + if (table_channels < 3 || table_channels > 4) { + PyErr_SetString(PyExc_ValueError, "table_channels could be 3 or 4"); + return NULL; + } + + if (imIn->type != IMAGING_TYPE_UINT8 || imOut->type != IMAGING_TYPE_UINT8 || + imIn->bands < 3 || imOut->bands < table_channels) { + return (Imaging)ImagingError_ModeError(); + } + + /* In case we have one extra band in imOut and don't have in imIn.*/ + if (imOut->bands > table_channels && imOut->bands > imIn->bands) { + return (Imaging)ImagingError_ModeError(); + } + + ImagingSectionEnter(&cookie); + for (y = 0; y < imOut->ysize; y++) { + UINT8 *rowIn = (UINT8 *)imIn->image[y]; + char *rowOut = (char *)imOut->image[y]; + for (x = 0; x < imOut->xsize; x++) { + UINT32 index1D = rowIn[x * 4 + 0] * scale1D; + UINT32 index2D = rowIn[x * 4 + 1] * scale2D; + UINT32 index3D = rowIn[x * 4 + 2] * scale3D; + INT16 shift1D = (SCALE_MASK & index1D) >> (SCALE_BITS - SHIFT_BITS); + INT16 shift2D = (SCALE_MASK & index2D) >> (SCALE_BITS - SHIFT_BITS); + INT16 shift3D = (SCALE_MASK & index3D) >> (SCALE_BITS - SHIFT_BITS); + int idx = table_channels * table_index3D( + index1D >> SCALE_BITS, + index2D >> SCALE_BITS, + index3D >> SCALE_BITS, + size1D, + size1D_2D); + INT16 result[4], left[4], right[4]; + INT16 leftleft[4], leftright[4], rightleft[4], rightright[4]; + + if (table_channels == 3) { + UINT32 v; + interpolate3(leftleft, &table[idx + 0], &table[idx + 3], shift1D); + interpolate3( + leftright, + &table[idx + size1D * 3], + &table[idx + size1D * 3 + 3], + shift1D); + interpolate3(left, leftleft, leftright, shift2D); + + interpolate3( + rightleft, + &table[idx + size1D_2D * 3], + &table[idx + size1D_2D * 3 + 3], + shift1D); + interpolate3( + rightright, + &table[idx + size1D_2D * 3 + size1D * 3], + &table[idx + size1D_2D * 3 + size1D * 3 + 3], + shift1D); + interpolate3(right, rightleft, rightright, shift2D); + + interpolate3(result, left, right, shift3D); + + v = MAKE_UINT32( + clip8(result[0]), + clip8(result[1]), + clip8(result[2]), + rowIn[x * 4 + 3]); + memcpy(rowOut + x * sizeof(v), &v, sizeof(v)); + } + + if (table_channels == 4) { + UINT32 v; + interpolate4(leftleft, &table[idx + 0], &table[idx + 4], shift1D); + interpolate4( + leftright, + &table[idx + size1D * 4], + &table[idx + size1D * 4 + 4], + shift1D); + interpolate4(left, leftleft, leftright, shift2D); + + interpolate4( + rightleft, + &table[idx + size1D_2D * 4], + &table[idx + size1D_2D * 4 + 4], + shift1D); + interpolate4( + rightright, + &table[idx + size1D_2D * 4 + size1D * 4], + &table[idx + size1D_2D * 4 + size1D * 4 + 4], + shift1D); + interpolate4(right, rightleft, rightright, shift2D); + + interpolate4(result, left, right, shift3D); + + v = MAKE_UINT32( + clip8(result[0]), + clip8(result[1]), + clip8(result[2]), + clip8(result[3])); + memcpy(rowOut + x * sizeof(v), &v, sizeof(v)); + } + } + } + ImagingSectionLeave(&cookie); + + return imOut; +} diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index b3e48e52b08..2b45d0cc4a8 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -32,42 +32,29 @@ * See the README file for details on usage and redistribution. */ - #include "Imaging.h" -#define MAX(a, b) (a)>(b) ? (a) : (b) -#define MIN(a, b) (a)<(b) ? (a) : (b) +#define MAX(a, b) (a) > (b) ? (a) : (b) +#define MIN(a, b) (a) < (b) ? (a) : (b) -#define CLIP(v) ((v) <= 0 ? 0 : (v) >= 255 ? 255 : (v)) -#define CLIP16(v) ((v) <= -32768 ? -32768 : (v) >= 32767 ? 32767 : (v)) +#define CLIP16(v) ((v) <= 0 ? 0 : (v) >= 65535 ? 65535 : (v)) /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ -#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) - -#ifndef round -double round(double x) { - return floor(x+0.5); -} -#endif +#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) /* ------------------- */ /* 1 (bit) conversions */ /* ------------------- */ static void -bit2l(UINT8* out, const UINT8* in, int xsize) -{ +bit2l(UINT8 *out, const UINT8 *in, int xsize) { int x; - for (x = 0; x < xsize; x++) - *out++ = (*in++ != 0) ? 255 : 0; + for (x = 0; x < xsize; x++) *out++ = (*in++ != 0) ? 255 : 0; } static void -bit2rgb(UINT8* out, const UINT8* in, int xsize) -{ +bit2rgb(UINT8 *out, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++) { UINT8 v = (*in++ != 0) ? 255 : 0; @@ -79,8 +66,7 @@ bit2rgb(UINT8* out, const UINT8* in, int xsize) } static void -bit2cmyk(UINT8* out, const UINT8* in, int xsize) -{ +bit2cmyk(UINT8 *out, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++) { *out++ = 0; @@ -91,8 +77,7 @@ bit2cmyk(UINT8* out, const UINT8* in, int xsize) } static void -bit2ycbcr(UINT8* out, const UINT8* in, int xsize) -{ +bit2ycbcr(UINT8 *out, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++) { *out++ = (*in++ != 0) ? 255 : 0; @@ -102,57 +87,66 @@ bit2ycbcr(UINT8* out, const UINT8* in, int xsize) } } +static void +bit2hsv(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, out += 4) { + UINT8 v = (*in++ != 0) ? 255 : 0; + out[0] = 0; + out[1] = 0; + out[2] = v; + out[3] = 255; + } +} + /* ----------------- */ /* RGB/L conversions */ /* ----------------- */ static void -l2bit(UINT8* out, const UINT8* in, int xsize) -{ +l2bit(UINT8 *out, const UINT8 *in, int xsize) { int x; - for (x = 0; x < xsize; x++) + for (x = 0; x < xsize; x++) { *out++ = (*in++ >= 128) ? 255 : 0; + } } static void -lA2la(UINT8* out, const UINT8* in, int xsize) -{ +lA2la(UINT8 *out, const UINT8 *in, int xsize) { int x; unsigned int alpha, pixel, tmp; for (x = 0; x < xsize; x++, in += 4) { alpha = in[3]; pixel = MULDIV255(in[0], alpha, tmp); - *out++ = (UINT8) pixel; - *out++ = (UINT8) pixel; - *out++ = (UINT8) pixel; - *out++ = (UINT8) alpha; + *out++ = (UINT8)pixel; + *out++ = (UINT8)pixel; + *out++ = (UINT8)pixel; + *out++ = (UINT8)alpha; } } /* RGBa -> RGBA conversion to remove premultiplication Needed for correct transforms/resizing on RGBA images */ static void -la2lA(UINT8* out, const UINT8* in, int xsize) -{ +la2lA(UINT8 *out, const UINT8 *in, int xsize) { int x; unsigned int alpha, pixel; - for (x = 0; x < xsize; x++, in+=4) { + for (x = 0; x < xsize; x++, in += 4) { alpha = in[3]; if (alpha == 255 || alpha == 0) { pixel = in[0]; } else { - pixel = CLIP((255 * in[0]) / alpha); + pixel = CLIP8((255 * in[0]) / alpha); } - *out++ = (UINT8) pixel; - *out++ = (UINT8) pixel; - *out++ = (UINT8) pixel; - *out++ = (UINT8) alpha; + *out++ = (UINT8)pixel; + *out++ = (UINT8)pixel; + *out++ = (UINT8)pixel; + *out++ = (UINT8)alpha; } } static void -l2la(UINT8* out, const UINT8* in, int xsize) -{ +l2la(UINT8 *out, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++) { UINT8 v = *in++; @@ -164,8 +158,7 @@ l2la(UINT8* out, const UINT8* in, int xsize) } static void -l2rgb(UINT8* out, const UINT8* in, int xsize) -{ +l2rgb(UINT8 *out, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++) { UINT8 v = *in++; @@ -177,16 +170,27 @@ l2rgb(UINT8* out, const UINT8* in, int xsize) } static void -la2l(UINT8* out, const UINT8* in, int xsize) -{ +l2hsv(UINT8 *out, const UINT8 *in, int xsize) { int x; - for (x = 0; x < xsize; x++, in += 4) + for (x = 0; x < xsize; x++, out += 4) { + UINT8 v = *in++; + out[0] = 0; + out[1] = 0; + out[2] = v; + out[3] = 255; + } +} + +static void +la2l(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4) { *out++ = in[0]; + } } static void -la2rgb(UINT8* out, const UINT8* in, int xsize) -{ +la2rgb(UINT8 *out, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++, in += 4) { UINT8 v = in[0]; @@ -198,26 +202,37 @@ la2rgb(UINT8* out, const UINT8* in, int xsize) } static void -rgb2bit(UINT8* out, const UINT8* in, int xsize) -{ +la2hsv(UINT8 *out, const UINT8 *in, int xsize) { int x; - for (x = 0; x < xsize; x++, in += 4) + for (x = 0; x < xsize; x++, in += 4, out += 4) { + UINT8 v = in[0]; + out[0] = 0; + out[1] = 0; + out[2] = v; + out[3] = in[3]; + } +} + +static void +rgb2bit(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4) { /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ *out++ = (L(in) >= 128000) ? 255 : 0; + } } static void -rgb2l(UINT8* out, const UINT8* in, int xsize) -{ +rgb2l(UINT8 *out, const UINT8 *in, int xsize) { int x; - for (x = 0; x < xsize; x++, in += 4) + for (x = 0; x < xsize; x++, in += 4) { /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ *out++ = L24(in) >> 16; + } } static void -rgb2la(UINT8* out, const UINT8* in, int xsize) -{ +rgb2la(UINT8 *out, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++, in += 4, out += 4) { /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ @@ -227,50 +242,47 @@ rgb2la(UINT8* out, const UINT8* in, int xsize) } static void -rgb2i(UINT8* out_, const UINT8* in, int xsize) -{ +rgb2i(UINT8 *out_, const UINT8 *in, int xsize) { int x; - INT32* out = (INT32*) out_; - for (x = 0; x < xsize; x++, in += 4) - *out++ = L24(in) >> 16; + for (x = 0; x < xsize; x++, in += 4, out_ += 4) { + INT32 v = L24(in) >> 16; + memcpy(out_, &v, sizeof(v)); + } } static void -rgb2f(UINT8* out_, const UINT8* in, int xsize) -{ +rgb2f(UINT8 *out_, const UINT8 *in, int xsize) { int x; - FLOAT32* out = (FLOAT32*) out_; - for (x = 0; x < xsize; x++, in += 4) - *out++ = (float) L(in) / 1000.0F; + for (x = 0; x < xsize; x++, in += 4, out_ += 4) { + FLOAT32 v = (float)L(in) / 1000.0F; + memcpy(out_, &v, sizeof(v)); + } } static void -rgb2bgr15(UINT8* out_, const UINT8* in, int xsize) -{ +rgb2bgr15(UINT8 *out_, const UINT8 *in, int xsize) { int x; - UINT16* out = (UINT16*) out_; - for (x = 0; x < xsize; x++, in += 4) - *out++ = - ((((UINT16)in[0])<<7)&0x7c00) + - ((((UINT16)in[1])<<2)&0x03e0) + - ((((UINT16)in[2])>>3)&0x001f); + for (x = 0; x < xsize; x++, in += 4, out_ += 2) { + UINT16 v = ((((UINT16)in[0]) << 7) & 0x7c00) + + ((((UINT16)in[1]) << 2) & 0x03e0) + + ((((UINT16)in[2]) >> 3) & 0x001f); + memcpy(out_, &v, sizeof(v)); + } } static void -rgb2bgr16(UINT8* out_, const UINT8* in, int xsize) -{ +rgb2bgr16(UINT8 *out_, const UINT8 *in, int xsize) { int x; - UINT16* out = (UINT16*) out_; - for (x = 0; x < xsize; x++, in += 4) - *out++ = - ((((UINT16)in[0])<<8)&0xf800) + - ((((UINT16)in[1])<<3)&0x07e0) + - ((((UINT16)in[2])>>3)&0x001f); + for (x = 0; x < xsize; x++, in += 4, out_ += 2) { + UINT16 v = ((((UINT16)in[0]) << 8) & 0xf800) + + ((((UINT16)in[1]) << 3) & 0x07e0) + + ((((UINT16)in[2]) >> 3) & 0x001f); + memcpy(out_, &v, sizeof(v)); + } } static void -rgb2bgr24(UINT8* out, const UINT8* in, int xsize) -{ +rgb2bgr24(UINT8 *out, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++, in += 4) { *out++ = in[2]; @@ -280,144 +292,139 @@ rgb2bgr24(UINT8* out, const UINT8* in, int xsize) } static void -rgb2hsv(UINT8* out, const UINT8* in, int xsize) -{ // following colorsys.py - float h,s,rc,gc,bc,cr; - UINT8 maxc,minc; +rgb2hsv_row(UINT8 *out, const UINT8 *in) { // following colorsys.py + float h, s, rc, gc, bc, cr; + UINT8 maxc, minc; UINT8 r, g, b; - UINT8 uh,us,uv; - int x; - - for (x = 0; x < xsize; x++, in += 4) { - r = in[0]; - g = in[1]; - b = in[2]; - - maxc = MAX(r,MAX(g,b)); - minc = MIN(r,MIN(g,b)); - uv = maxc; - if (minc == maxc){ - *out++ = 0; - *out++ = 0; - *out++ = uv; + UINT8 uh, us, uv; + + r = in[0]; + g = in[1]; + b = in[2]; + maxc = MAX(r, MAX(g, b)); + minc = MIN(r, MIN(g, b)); + uv = maxc; + if (minc == maxc) { + uh = 0; + us = 0; + } else { + cr = (float)(maxc - minc); + s = cr / (float)maxc; + rc = ((float)(maxc - r)) / cr; + gc = ((float)(maxc - g)) / cr; + bc = ((float)(maxc - b)) / cr; + if (r == maxc) { + h = bc - gc; + } else if (g == maxc) { + h = 2.0 + rc - bc; } else { - cr = (float)(maxc-minc); - s = cr/(float)maxc; - rc = ((float)(maxc-r))/cr; - gc = ((float)(maxc-g))/cr; - bc = ((float)(maxc-b))/cr; - if (r == maxc) { - h = bc-gc; - } else if (g == maxc) { - h = 2.0 + rc-bc; - } else { - h = 4.0 + gc-rc; - } - // incorrect hue happens if h/6 is negative. - h = fmod((h/6.0 + 1.0), 1.0); - - uh = (UINT8)CLIP((int)(h*255.0)); - us = (UINT8)CLIP((int)(s*255.0)); + h = 4.0 + gc - rc; + } + // incorrect hue happens if h/6 is negative. + h = fmod((h / 6.0 + 1.0), 1.0); - *out++ = uh; - *out++ = us; - *out++ = uv; + uh = (UINT8)CLIP8((int)(h * 255.0)); + us = (UINT8)CLIP8((int)(s * 255.0)); + } + out[0] = uh; + out[1] = us; + out[2] = uv; +} - } - *out++ = in[3]; +static void +rgb2hsv(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + rgb2hsv_row(out, in); + out[3] = in[3]; } } static void -hsv2rgb(UINT8* out, const UINT8* in, int xsize) -{ // following colorsys.py +hsv2rgb(UINT8 *out, const UINT8 *in, int xsize) { // following colorsys.py - int p,q,t; - UINT8 up,uq,ut; + int p, q, t; + UINT8 up, uq, ut; int i, x; float f, fs; - UINT8 h,s,v; + UINT8 h, s, v; for (x = 0; x < xsize; x++, in += 4) { h = in[0]; s = in[1]; v = in[2]; - if (s==0){ + if (s == 0) { *out++ = v; *out++ = v; *out++ = v; } else { - i = floor((float)h * 6.0 / 255.0); // 0 - 6 - f = (float)h * 6.0 / 255.0 - (float)i; // 0-1 : remainder. - fs = ((float)s)/255.0; - - p = round((float)v * (1.0-fs)); - q = round((float)v * (1.0-fs*f)); - t = round((float)v * (1.0-fs*(1.0-f))); - up = (UINT8)CLIP(p); - uq = (UINT8)CLIP(q); - ut = (UINT8)CLIP(t); - - switch (i%6) { - case 0: - *out++ = v; - *out++ = ut; - *out++ = up; - break; - case 1: - *out++ = uq; - *out++ = v; - *out++ = up; - break; - case 2: - *out++ = up; - *out++ = v; - *out++ = ut; - break; - case 3: - *out++ = up; - *out++ = uq; - *out++ = v; - break; - case 4: - *out++ = ut; - *out++ = up; - *out++ = v; - break; - case 5: - *out++ = v; - *out++ = up; - *out++ = uq; - break; - + i = floor((float)h * 6.0 / 255.0); // 0 - 6 + f = (float)h * 6.0 / 255.0 - (float)i; // 0-1 : remainder. + fs = ((float)s) / 255.0; + + p = round((float)v * (1.0 - fs)); + q = round((float)v * (1.0 - fs * f)); + t = round((float)v * (1.0 - fs * (1.0 - f))); + up = (UINT8)CLIP8(p); + uq = (UINT8)CLIP8(q); + ut = (UINT8)CLIP8(t); + + switch (i % 6) { + case 0: + *out++ = v; + *out++ = ut; + *out++ = up; + break; + case 1: + *out++ = uq; + *out++ = v; + *out++ = up; + break; + case 2: + *out++ = up; + *out++ = v; + *out++ = ut; + break; + case 3: + *out++ = up; + *out++ = uq; + *out++ = v; + break; + case 4: + *out++ = ut; + *out++ = up; + *out++ = v; + break; + case 5: + *out++ = v; + *out++ = up; + *out++ = uq; + break; } } *out++ = in[3]; } } - - /* ---------------- */ /* RGBA conversions */ /* ---------------- */ static void -rgb2rgba(UINT8* out, const UINT8* in, int xsize) -{ +rgb2rgba(UINT8 *out, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++) { *out++ = *in++; *out++ = *in++; *out++ = *in++; - *out++ = 255; in++; + *out++ = 255; + in++; } } static void -rgba2la(UINT8* out, const UINT8* in, int xsize) -{ +rgba2la(UINT8 *out, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++, in += 4, out += 4) { /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ @@ -427,20 +434,19 @@ rgba2la(UINT8* out, const UINT8* in, int xsize) } static void -rgba2rgb(UINT8* out, const UINT8* in, int xsize) -{ +rgba2rgb(UINT8 *out, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++) { *out++ = *in++; *out++ = *in++; *out++ = *in++; - *out++ = 255; in++; + *out++ = 255; + in++; } } static void -rgbA2rgba(UINT8* out, const UINT8* in, int xsize) -{ +rgbA2rgba(UINT8 *out, const UINT8 *in, int xsize) { int x; unsigned int alpha, tmp; for (x = 0; x < xsize; x++) { @@ -455,20 +461,19 @@ rgbA2rgba(UINT8* out, const UINT8* in, int xsize) /* RGBa -> RGBA conversion to remove premultiplication Needed for correct transforms/resizing on RGBA images */ static void -rgba2rgbA(UINT8* out, const UINT8* in, int xsize) -{ +rgba2rgbA(UINT8 *out, const UINT8 *in, int xsize) { int x; unsigned int alpha; - for (x = 0; x < xsize; x++, in+=4) { + 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++ = CLIP((255 * in[0]) / alpha); - *out++ = CLIP((255 * in[1]) / alpha); - *out++ = CLIP((255 * in[2]) / alpha); + *out++ = CLIP8((255 * in[0]) / alpha); + *out++ = CLIP8((255 * in[1]) / alpha); + *out++ = CLIP8((255 * in[2]) / alpha); } *out++ = in[3]; } @@ -481,34 +486,32 @@ rgba2rgbA(UINT8* out, const UINT8* in, int xsize) */ static void -rgbT2rgba(UINT8* out, int xsize, int r, int g, int b) -{ +rgbT2rgba(UINT8 *out, int xsize, int r, int g, int b) { #ifdef WORDS_BIGENDIAN - UINT32 trns = ((r & 0xff)<<24) | ((g & 0xff)<<16) | ((b & 0xff)<<8) | 0xff; + UINT32 trns = ((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | 0xff; UINT32 repl = trns & 0xffffff00; #else - UINT32 trns = (0xff <<24) | ((b & 0xff)<<16) | ((g & 0xff)<<8) | (r & 0xff); + UINT32 trns = (0xff << 24) | ((b & 0xff) << 16) | ((g & 0xff) << 8) | (r & 0xff); UINT32 repl = trns & 0x00ffffff; #endif - UINT32* tmp = (UINT32 *)out; int i; - for (i=0; i < xsize; i++ ,tmp++) { - if (tmp[0]==trns) { - tmp[0]=repl; + for (i = 0; i < xsize; i++, out += sizeof(trns)) { + UINT32 v; + memcpy(&v, out, sizeof(v)); + if (v == trns) { + memcpy(out, &repl, sizeof(repl)); } } } - /* ---------------- */ /* CMYK conversions */ /* ---------------- */ static void -l2cmyk(UINT8* out, const UINT8* in, int xsize) -{ +l2cmyk(UINT8 *out, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++) { *out++ = 0; @@ -519,27 +522,52 @@ l2cmyk(UINT8* out, const UINT8* in, int xsize) } static void -rgb2cmyk(UINT8* out, const UINT8* in, int xsize) -{ +la2cmyk(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = ~(in[0]); + } +} + +static void +rgb2cmyk(UINT8 *out, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++) { /* Note: no undercolour removal */ *out++ = ~(*in++); *out++ = ~(*in++); *out++ = ~(*in++); - *out++ = 0; in++; + *out++ = 0; + in++; } } static void -cmyk2rgb(UINT8* out, const UINT8* in, int xsize) -{ +cmyk2rgb(UINT8 *out, const UINT8 *in, int xsize) { int x, nk, tmp; for (x = 0; x < xsize; x++) { nk = 255 - in[3]; - out[0] = CLIP(nk - MULDIV255(in[0], nk, tmp)); - out[1] = CLIP(nk - MULDIV255(in[1], nk, tmp)); - out[2] = CLIP(nk - MULDIV255(in[2], nk, tmp)); + out[0] = CLIP8(nk - MULDIV255(in[0], nk, tmp)); + out[1] = CLIP8(nk - MULDIV255(in[1], nk, tmp)); + out[2] = CLIP8(nk - MULDIV255(in[2], nk, tmp)); + out[3] = 255; + out += 4; + in += 4; + } +} + +static void +cmyk2hsv(UINT8 *out, const UINT8 *in, int xsize) { + int x, nk, tmp; + for (x = 0; x < xsize; x++) { + nk = 255 - in[3]; + out[0] = CLIP8(nk - MULDIV255(in[0], nk, tmp)); + out[1] = CLIP8(nk - MULDIV255(in[1], nk, tmp)); + out[2] = CLIP8(nk - MULDIV255(in[2], nk, tmp)); + rgb2hsv_row(out, out); out[3] = 255; out += 4; in += 4; @@ -551,46 +579,83 @@ cmyk2rgb(UINT8* out, const UINT8* in, int xsize) /* ------------- */ static void -bit2i(UINT8* out_, const UINT8* in, int xsize) -{ +bit2i(UINT8 *out_, const UINT8 *in, int xsize) { int x; - INT32* out = (INT32*) out_; - for (x = 0; x < xsize; x++) - *out++ = (*in++ != 0) ? 255 : 0; + for (x = 0; x < xsize; x++, out_ += 4) { + INT32 v = (*in++ != 0) ? 255 : 0; + memcpy(out_, &v, sizeof(v)); + } } static void -l2i(UINT8* out_, const UINT8* in, int xsize) -{ +l2i(UINT8 *out_, const UINT8 *in, int xsize) { int x; - INT32* out = (INT32*) out_; - for (x = 0; x < xsize; x++) - *out++ = (INT32) *in++; + for (x = 0; x < xsize; x++, out_ += 4) { + INT32 v = *in++; + memcpy(out_, &v, sizeof(v)); + } } static void -i2l(UINT8* out, const UINT8* in_, int xsize) -{ +i2l(UINT8 *out, const UINT8 *in_, int xsize) { int x; - INT32* in = (INT32*) in_; - for (x = 0; x < xsize; x++, in++, out++) { - if (*in <= 0) + for (x = 0; x < xsize; x++, out++, in_ += 4) { + INT32 v; + memcpy(&v, in_, sizeof(v)); + if (v <= 0) { *out = 0; - else if (*in >= 255) + } else if (v >= 255) { *out = 255; - else - *out = (UINT8) *in; + } else { + *out = (UINT8)v; + } + } +} + +static void +i2f(UINT8 *out_, const UINT8 *in_, int xsize) { + int x; + for (x = 0; x < xsize; x++, in_ += 4, out_ += 4) { + INT32 i; + FLOAT32 f; + memcpy(&i, in_, sizeof(i)); + f = i; + memcpy(out_, &f, sizeof(f)); + } +} + +static void +i2rgb(UINT8 *out, const UINT8 *in_, int xsize) { + int x; + INT32 *in = (INT32 *)in_; + for (x = 0; x < xsize; x++, in++, out += 4) { + if (*in <= 0) { + out[0] = out[1] = out[2] = 0; + } else if (*in >= 255) { + out[0] = out[1] = out[2] = 255; + } else { + out[0] = out[1] = out[2] = (UINT8)*in; + } + out[3] = 255; } } static void -i2f(UINT8* out_, const UINT8* in_, int xsize) -{ +i2hsv(UINT8 *out, const UINT8 *in_, int xsize) { int x; - INT32* in = (INT32*) in_; - FLOAT32* out = (FLOAT32*) out_; - for (x = 0; x < xsize; x++) - *out++ = (FLOAT32) *in++; + INT32 *in = (INT32 *)in_; + for (x = 0; x < xsize; x++, in++, out += 4) { + out[0] = 0; + out[1] = 0; + if (*in <= 0) { + out[2] = 0; + } else if (*in >= 255) { + out[2] = 255; + } else { + out[2] = (UINT8)*in; + } + out[3] = 255; + } } /* ------------- */ @@ -598,46 +663,49 @@ i2f(UINT8* out_, const UINT8* in_, int xsize) /* ------------- */ static void -bit2f(UINT8* out_, const UINT8* in, int xsize) -{ +bit2f(UINT8 *out_, const UINT8 *in, int xsize) { int x; - FLOAT32* out = (FLOAT32*) out_; - for (x = 0; x < xsize; x++) - *out++ = (*in++ != 0) ? 255.0F : 0.0F; + for (x = 0; x < xsize; x++, out_ += 4) { + FLOAT32 f = (*in++ != 0) ? 255.0F : 0.0F; + memcpy(out_, &f, sizeof(f)); + } } static void -l2f(UINT8* out_, const UINT8* in, int xsize) -{ +l2f(UINT8 *out_, const UINT8 *in, int xsize) { int x; - FLOAT32* out = (FLOAT32*) out_; - for (x = 0; x < xsize; x++) - *out++ = (FLOAT32) *in++; + for (x = 0; x < xsize; x++, out_ += 4) { + FLOAT32 f = (FLOAT32)*in++; + memcpy(out_, &f, sizeof(f)); + } } static void -f2l(UINT8* out, const UINT8* in_, int xsize) -{ +f2l(UINT8 *out, const UINT8 *in_, int xsize) { int x; - FLOAT32* in = (FLOAT32*) in_; - for (x = 0; x < xsize; x++, in++, out++) { - if (*in <= 0.0) + for (x = 0; x < xsize; x++, out++, in_ += 4) { + FLOAT32 v; + memcpy(&v, in_, sizeof(v)); + if (v <= 0.0) { *out = 0; - else if (*in >= 255.0) + } else if (v >= 255.0) { *out = 255; - else - *out = (UINT8) *in; + } else { + *out = (UINT8)v; + } } } static void -f2i(UINT8* out_, const UINT8* in_, int xsize) -{ +f2i(UINT8 *out_, const UINT8 *in_, int xsize) { int x; - FLOAT32* in = (FLOAT32*) in_; - INT32* out = (INT32*) out_; - for (x = 0; x < xsize; x++) - *out++ = (INT32) *in++; + for (x = 0; x < xsize; x++, in_ += 4, out_ += 4) { + FLOAT32 f; + INT32 i; + memcpy(&f, in_, sizeof(f)); + i = f; + memcpy(out_, &i, sizeof(i)); + } } /* ----------------- */ @@ -647,8 +715,7 @@ f2i(UINT8* out_, const UINT8* in_, int xsize) /* See ConvertYCbCr.c for RGB/YCbCr tables */ static void -l2ycbcr(UINT8* out, const UINT8* in, int xsize) -{ +l2ycbcr(UINT8 *out, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++) { *out++ = *in++; @@ -659,11 +726,31 @@ l2ycbcr(UINT8* out, const UINT8* in, int xsize) } static void -ycbcr2l(UINT8* out, const UINT8* in, int xsize) -{ +la2ycbcr(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = in[0]; + *out++ = 128; + *out++ = 128; + *out++ = 255; + } +} + +static void +ycbcr2l(UINT8 *out, const UINT8 *in, int xsize) { int x; - for (x = 0; x < xsize; x++, in += 4) + for (x = 0; x < xsize; x++, in += 4) { *out++ = in[0]; + } +} + +static void +ycbcr2la(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + out[0] = out[1] = out[2] = in[0]; + out[3] = 255; + } } /* ------------------------- */ @@ -671,71 +758,67 @@ ycbcr2l(UINT8* out, const UINT8* in, int xsize) /* ------------------------- */ static void -I_I16L(UINT8* out, const UINT8* in_, int xsize) -{ +I_I16L(UINT8 *out, const UINT8 *in_, int xsize) { int x, v; - INT32* in = (INT32*) in_; - for (x = 0; x < xsize; x++, in++) { - v = CLIP16(*in); - *out++ = (UINT8) v; - *out++ = (UINT8) (v >> 8); + for (x = 0; x < xsize; x++, in_ += 4) { + INT32 i; + memcpy(&i, in_, sizeof(i)); + v = CLIP16(i); + *out++ = (UINT8)v; + *out++ = (UINT8)(v >> 8); } } static void -I_I16B(UINT8* out, const UINT8* in_, int xsize) -{ +I_I16B(UINT8 *out, const UINT8 *in_, int xsize) { int x, v; - INT32* in = (INT32*) in_; - for (x = 0; x < xsize; x++, in++) { - v = CLIP16(*in); - *out++ = (UINT8) (v >> 8); - *out++ = (UINT8) v; + for (x = 0; x < xsize; x++, in_ += 4) { + INT32 i; + memcpy(&i, in_, sizeof(i)); + v = CLIP16(i); + *out++ = (UINT8)(v >> 8); + *out++ = (UINT8)v; } } - static void -I16L_I(UINT8* out_, const UINT8* in, int xsize) -{ +I16L_I(UINT8 *out_, const UINT8 *in, int xsize) { int x; - INT32* out = (INT32*) out_; - for (x = 0; x < xsize; x++, in += 2) - *out++ = in[0] + ((int) in[1] << 8); + for (x = 0; x < xsize; x++, in += 2, out_ += 4) { + INT32 v = in[0] + ((int)in[1] << 8); + memcpy(out_, &v, sizeof(v)); + } } - static void -I16B_I(UINT8* out_, const UINT8* in, int xsize) -{ +I16B_I(UINT8 *out_, const UINT8 *in, int xsize) { int x; - INT32* out = (INT32*) out_; - for (x = 0; x < xsize; x++, in += 2) - *out++ = in[1] + ((int) in[0] << 8); + for (x = 0; x < xsize; x++, in += 2, out_ += 4) { + INT32 v = in[1] + ((int)in[0] << 8); + memcpy(out_, &v, sizeof(v)); + } } static void -I16L_F(UINT8* out_, const UINT8* in, int xsize) -{ +I16L_F(UINT8 *out_, const UINT8 *in, int xsize) { int x; - FLOAT32* out = (FLOAT32*) out_; - for (x = 0; x < xsize; x++, in += 2) - *out++ = (FLOAT32) (in[0] + ((int) in[1] << 8)); + for (x = 0; x < xsize; x++, in += 2, out_ += 4) { + FLOAT32 v = in[0] + ((int)in[1] << 8); + memcpy(out_, &v, sizeof(v)); + } } - static void -I16B_F(UINT8* out_, const UINT8* in, int xsize) -{ +I16B_F(UINT8 *out_, const UINT8 *in, int xsize) { int x; - FLOAT32* out = (FLOAT32*) out_; - for (x = 0; x < xsize; x++, in += 2) - *out++ = (FLOAT32) (in[1] + ((int) in[0] << 8)); + for (x = 0; x < xsize; x++, in += 2, out_ += 4) { + FLOAT32 v = in[1] + ((int)in[0] << 8); + memcpy(out_, &v, sizeof(v)); + } } static void -L_I16L(UINT8* out, const UINT8* in, int xsize) -{ +L_I16L(UINT8 *out, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++, in++) { *out++ = *in; @@ -744,8 +827,7 @@ L_I16L(UINT8* out, const UINT8* in, int xsize) } static void -L_I16B(UINT8* out, const UINT8* in, int xsize) -{ +L_I16B(UINT8 *out, const UINT8 *in, int xsize) { int x; for (x = 0; x < xsize; x++, in++) { *out++ = 0; @@ -754,131 +836,146 @@ L_I16B(UINT8* out, const UINT8* in, int xsize) } static void -I16L_L(UINT8* out, const UINT8* in, int xsize) -{ +I16L_L(UINT8 *out, const UINT8 *in, int xsize) { int x; - for (x = 0; x < xsize; x++, in += 2) - if (in[1] != 0) + for (x = 0; x < xsize; x++, in += 2) { + if (in[1] != 0) { *out++ = 255; - else + } else { *out++ = in[0]; + } + } } static void -I16B_L(UINT8* out, const UINT8* in, int xsize) -{ +I16B_L(UINT8 *out, const UINT8 *in, int xsize) { int x; - for (x = 0; x < xsize; x++, in += 2) - if (in[0] != 0) + for (x = 0; x < xsize; x++, in += 2) { + if (in[0] != 0) { *out++ = 255; - else + } else { *out++ = in[1]; + } + } } static struct { - const char* from; - const char* to; + const char *from; + const char *to; ImagingShuffler convert; } converters[] = { - { "1", "L", bit2l }, - { "1", "I", bit2i }, - { "1", "F", bit2f }, - { "1", "RGB", bit2rgb }, - { "1", "RGBA", bit2rgb }, - { "1", "RGBX", bit2rgb }, - { "1", "CMYK", bit2cmyk }, - { "1", "YCbCr", bit2ycbcr }, - - { "L", "1", l2bit }, - { "L", "LA", l2la }, - { "L", "I", l2i }, - { "L", "F", l2f }, - { "L", "RGB", l2rgb }, - { "L", "RGBA", l2rgb }, - { "L", "RGBX", l2rgb }, - { "L", "CMYK", l2cmyk }, - { "L", "YCbCr", l2ycbcr }, - - { "LA", "L", la2l }, - { "LA", "La", lA2la }, - { "LA", "RGB", la2rgb }, - { "LA", "RGBX", la2rgb }, - { "LA", "RGBA", la2rgb }, - - { "La", "LA", la2lA }, - - { "I", "L", i2l }, - { "I", "F", i2f }, - - { "F", "L", f2l }, - { "F", "I", f2i }, - - { "RGB", "1", rgb2bit }, - { "RGB", "L", rgb2l }, - { "RGB", "LA", rgb2la }, - { "RGB", "I", rgb2i }, - { "RGB", "F", rgb2f }, - { "RGB", "BGR;15", rgb2bgr15 }, - { "RGB", "BGR;16", rgb2bgr16 }, - { "RGB", "BGR;24", rgb2bgr24 }, - { "RGB", "RGBA", rgb2rgba }, - { "RGB", "RGBX", rgb2rgba }, - { "RGB", "CMYK", rgb2cmyk }, - { "RGB", "YCbCr", ImagingConvertRGB2YCbCr }, - { "RGB", "HSV", rgb2hsv }, - - { "RGBA", "1", rgb2bit }, - { "RGBA", "L", rgb2l }, - { "RGBA", "LA", rgba2la }, - { "RGBA", "I", rgb2i }, - { "RGBA", "F", rgb2f }, - { "RGBA", "RGB", rgba2rgb }, - { "RGBA", "RGBa", rgbA2rgba }, - { "RGBA", "RGBX", rgb2rgba }, - { "RGBA", "CMYK", rgb2cmyk }, - { "RGBA", "YCbCr", ImagingConvertRGB2YCbCr }, - - { "RGBa", "RGBA", rgba2rgbA }, - - { "RGBX", "1", rgb2bit }, - { "RGBX", "L", rgb2l }, - { "RGBA", "I", rgb2i }, - { "RGBA", "F", rgb2f }, - { "RGBX", "RGB", rgba2rgb }, - { "RGBX", "CMYK", rgb2cmyk }, - { "RGBX", "YCbCr", ImagingConvertRGB2YCbCr }, - - { "CMYK", "RGB", cmyk2rgb }, - { "CMYK", "RGBA", cmyk2rgb }, - { "CMYK", "RGBX", cmyk2rgb }, - - { "YCbCr", "L", ycbcr2l }, - { "YCbCr", "RGB", ImagingConvertYCbCr2RGB }, - - { "HSV", "RGB", hsv2rgb }, - - { "I", "I;16", I_I16L }, - { "I;16", "I", I16L_I }, - { "L", "I;16", L_I16L }, - { "I;16", "L", I16L_L }, - - { "I", "I;16L", I_I16L }, - { "I;16L", "I", I16L_I }, - { "I", "I;16B", I_I16B }, - { "I;16B", "I", I16B_I }, - - { "L", "I;16L", L_I16L }, - { "I;16L", "L", I16L_L }, - { "L", "I;16B", L_I16B }, - { "I;16B", "L", I16B_L }, - - { "I;16", "F", I16L_F }, - { "I;16L", "F", I16L_F }, - { "I;16B", "F", I16B_F }, - - { NULL } -}; + {"1", "L", bit2l}, + {"1", "I", bit2i}, + {"1", "F", bit2f}, + {"1", "RGB", bit2rgb}, + {"1", "RGBA", bit2rgb}, + {"1", "RGBX", bit2rgb}, + {"1", "CMYK", bit2cmyk}, + {"1", "YCbCr", bit2ycbcr}, + {"1", "HSV", bit2hsv}, + + {"L", "1", l2bit}, + {"L", "LA", l2la}, + {"L", "I", l2i}, + {"L", "F", l2f}, + {"L", "RGB", l2rgb}, + {"L", "RGBA", l2rgb}, + {"L", "RGBX", l2rgb}, + {"L", "CMYK", l2cmyk}, + {"L", "YCbCr", l2ycbcr}, + {"L", "HSV", l2hsv}, + + {"LA", "L", la2l}, + {"LA", "La", lA2la}, + {"LA", "RGB", la2rgb}, + {"LA", "RGBA", la2rgb}, + {"LA", "RGBX", la2rgb}, + {"LA", "CMYK", la2cmyk}, + {"LA", "YCbCr", la2ycbcr}, + {"LA", "HSV", la2hsv}, + + {"La", "LA", la2lA}, + + {"I", "L", i2l}, + {"I", "F", i2f}, + {"I", "RGB", i2rgb}, + {"I", "RGBA", i2rgb}, + {"I", "RGBX", i2rgb}, + {"I", "HSV", i2hsv}, + + {"F", "L", f2l}, + {"F", "I", f2i}, + + {"RGB", "1", rgb2bit}, + {"RGB", "L", rgb2l}, + {"RGB", "LA", rgb2la}, + {"RGB", "I", rgb2i}, + {"RGB", "F", rgb2f}, + {"RGB", "BGR;15", rgb2bgr15}, + {"RGB", "BGR;16", rgb2bgr16}, + {"RGB", "BGR;24", rgb2bgr24}, + {"RGB", "RGBA", rgb2rgba}, + {"RGB", "RGBX", rgb2rgba}, + {"RGB", "CMYK", rgb2cmyk}, + {"RGB", "YCbCr", ImagingConvertRGB2YCbCr}, + {"RGB", "HSV", rgb2hsv}, + + {"RGBA", "1", rgb2bit}, + {"RGBA", "L", rgb2l}, + {"RGBA", "LA", rgba2la}, + {"RGBA", "I", rgb2i}, + {"RGBA", "F", rgb2f}, + {"RGBA", "RGB", rgba2rgb}, + {"RGBA", "RGBa", rgbA2rgba}, + {"RGBA", "RGBX", rgb2rgba}, + {"RGBA", "CMYK", rgb2cmyk}, + {"RGBA", "YCbCr", ImagingConvertRGB2YCbCr}, + {"RGBA", "HSV", rgb2hsv}, + + {"RGBa", "RGBA", rgba2rgbA}, + + {"RGBX", "1", rgb2bit}, + {"RGBX", "L", rgb2l}, + {"RGBX", "LA", rgb2la}, + {"RGBX", "I", rgb2i}, + {"RGBX", "F", rgb2f}, + {"RGBX", "RGB", rgba2rgb}, + {"RGBX", "CMYK", rgb2cmyk}, + {"RGBX", "YCbCr", ImagingConvertRGB2YCbCr}, + {"RGBX", "HSV", rgb2hsv}, + + {"CMYK", "RGB", cmyk2rgb}, + {"CMYK", "RGBA", cmyk2rgb}, + {"CMYK", "RGBX", cmyk2rgb}, + {"CMYK", "HSV", cmyk2hsv}, + + {"YCbCr", "L", ycbcr2l}, + {"YCbCr", "LA", ycbcr2la}, + {"YCbCr", "RGB", ImagingConvertYCbCr2RGB}, + + {"HSV", "RGB", hsv2rgb}, + + {"I", "I;16", I_I16L}, + {"I;16", "I", I16L_I}, + {"L", "I;16", L_I16L}, + {"I;16", "L", I16L_L}, + + {"I", "I;16L", I_I16L}, + {"I;16L", "I", I16L_I}, + {"I", "I;16B", I_I16B}, + {"I;16B", "I", I16B_I}, + + {"L", "I;16L", L_I16L}, + {"I;16L", "L", I16L_L}, + {"L", "I;16B", L_I16B}, + {"I;16B", "L", I16B_L}, + + {"I;16", "F", I16L_F}, + {"I;16L", "F", I16L_F}, + {"I;16B", "F", I16B_F}, + + {NULL}}; /* FIXME: translate indexed versions to pointer versions below this line */ @@ -887,70 +984,124 @@ static struct { /* ------------------- */ static void -p2bit(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) -{ +p2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + /* FIXME: precalculate greyscale palette? */ + for (x = 0; x < xsize; x++) { + *out++ = (L(&palette->palette[in[x] * 4]) >= 128000) ? 255 : 0; + } +} + +static void +pa2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; /* FIXME: precalculate greyscale palette? */ - for (x = 0; x < xsize; x++) - *out++ = (L(&palette[in[x]*4]) >= 128000) ? 255 : 0; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = (L(&palette->palette[in[0] * 4]) >= 128000) ? 255 : 0; + } } static void -p2l(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) -{ +p2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; /* FIXME: precalculate greyscale palette? */ - for (x = 0; x < xsize; x++) - *out++ = L(&palette[in[x]*4]) / 1000; + for (x = 0; x < xsize; x++) { + *out++ = L24(&palette->palette[in[x] * 4]) >> 16; + } } static void -p2la(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) -{ +pa2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; /* FIXME: precalculate greyscale palette? */ - for (x = 0; x < xsize; x++, out+=4) { - const UINT8* rgba = &palette[*in++ * 4]; - out[0] = out[1] = out[2] = L(rgba) / 1000; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = L24(&palette->palette[in[0] * 4]) >> 16; + } +} + +static void +pa2p(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = in[0]; + } +} + +static void +p2pa(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + int rgb = strcmp(palette->mode, "RGB"); + for (x = 0; x < xsize; x++, in++) { + const UINT8 *rgba = &palette->palette[in[0] * 4]; + *out++ = in[0]; + *out++ = in[0]; + *out++ = in[0]; + *out++ = rgb == 0 ? 255 : rgba[3]; + } +} + +static void +p2la(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + /* FIXME: precalculate greyscale palette? */ + for (x = 0; x < xsize; x++, out += 4) { + const UINT8 *rgba = &palette->palette[*in++ * 4]; + out[0] = out[1] = out[2] = L24(rgba) >> 16; out[3] = rgba[3]; } } static void -pa2la(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) -{ +pa2la(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; /* FIXME: precalculate greyscale palette? */ - for (x = 0; x < xsize; x++, in += 2) { - *out++ = L(&palette[in[0]*4]) / 1000; - *out++ = in[1]; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + out[0] = out[1] = out[2] = L24(&palette->palette[in[0] * 4]) >> 16; + out[3] = in[3]; } } static void -p2i(UINT8* out_, const UINT8* in, int xsize, const UINT8* palette) -{ +p2i(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) { int x; - INT32* out = (INT32*) out_; - for (x = 0; x < xsize; x++) - *out++ = L(&palette[in[x]*4]) / 1000; + for (x = 0; x < xsize; x++, out_ += 4) { + INT32 v = L24(&palette->palette[in[x] * 4]) >> 16; + memcpy(out_, &v, sizeof(v)); + } } static void -p2f(UINT8* out_, const UINT8* in, int xsize, const UINT8* palette) -{ +pa2i(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) { int x; - FLOAT32* out = (FLOAT32*) out_; - for (x = 0; x < xsize; x++) - *out++ = (float) L(&palette[in[x]*4]) / 1000.0F; + INT32 *out = (INT32 *)out_; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = L24(&palette->palette[in[0] * 4]) >> 16; + } } static void -p2rgb(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) -{ +p2f(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + for (x = 0; x < xsize; x++, out_ += 4) { + FLOAT32 v = L(&palette->palette[in[x] * 4]) / 1000.0F; + memcpy(out_, &v, sizeof(v)); + } +} + +static void +pa2f(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + FLOAT32 *out = (FLOAT32 *)out_; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = (float)L(&palette->palette[in[0] * 4]) / 1000.0F; + } +} + +static void +p2rgb(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; for (x = 0; x < xsize; x++) { - const UINT8* rgb = &palette[*in++ * 4]; + const UINT8 *rgb = &palette->palette[*in++ * 4]; *out++ = rgb[0]; *out++ = rgb[1]; *out++ = rgb[2]; @@ -959,11 +1110,42 @@ p2rgb(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) } static void -p2rgba(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) -{ +pa2rgb(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + const UINT8 *rgb = &palette->palette[in[0] * 4]; + *out++ = rgb[0]; + *out++ = rgb[1]; + *out++ = rgb[2]; + *out++ = 255; + } +} + +static void +p2hsv(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + for (x = 0; x < xsize; x++, out += 4) { + const UINT8 *rgb = &palette->palette[*in++ * 4]; + rgb2hsv_row(out, rgb); + out[3] = 255; + } +} + +static void +pa2hsv(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + const UINT8 *rgb = &palette->palette[in[0] * 4]; + rgb2hsv_row(out, rgb); + out[3] = 255; + } +} + +static void +p2rgba(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; for (x = 0; x < xsize; x++) { - const UINT8* rgba = &palette[*in++ * 4]; + const UINT8 *rgba = &palette->palette[*in++ * 4]; *out++ = rgba[0]; *out++ = rgba[1]; *out++ = rgba[2]; @@ -972,11 +1154,10 @@ p2rgba(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) } static void -pa2rgba(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) -{ +pa2rgba(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; for (x = 0; x < xsize; x++, in += 4) { - const UINT8* rgb = &palette[in[0] * 4]; + const UINT8 *rgb = &palette->palette[in[0] * 4]; *out++ = rgb[0]; *out++ = rgb[1]; *out++ = rgb[2]; @@ -985,65 +1166,89 @@ pa2rgba(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) } static void -p2cmyk(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) -{ +p2cmyk(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { p2rgb(out, in, xsize, palette); rgb2cmyk(out, out, xsize); } static void -p2ycbcr(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) -{ +pa2cmyk(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + pa2rgb(out, in, xsize, palette); + rgb2cmyk(out, out, xsize); +} + +static void +p2ycbcr(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { p2rgb(out, in, xsize, palette); ImagingConvertRGB2YCbCr(out, out, xsize); } +static void +pa2ycbcr(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + pa2rgb(out, in, xsize, palette); + ImagingConvertRGB2YCbCr(out, out, xsize); +} + static Imaging -frompalette(Imaging imOut, Imaging imIn, const char *mode) -{ +frompalette(Imaging imOut, Imaging imIn, const char *mode) { ImagingSectionCookie cookie; int alpha; int y; - void (*convert)(UINT8*, const UINT8*, int, const UINT8*); + void (*convert)(UINT8 *, const UINT8 *, int, ImagingPalette); /* Map palette image to L, RGB, RGBA, or CMYK */ - if (!imIn->palette) - return (Imaging) ImagingError_ValueError("no palette"); + if (!imIn->palette) { + return (Imaging)ImagingError_ValueError("no palette"); + } alpha = !strcmp(imIn->mode, "PA"); - if (strcmp(mode, "1") == 0) - convert = p2bit; - else if (strcmp(mode, "L") == 0) - convert = p2l; - else if (strcmp(mode, "LA") == 0) - convert = (alpha) ? pa2la : p2la; - else if (strcmp(mode, "I") == 0) - convert = p2i; - else if (strcmp(mode, "F") == 0) - convert = p2f; - else if (strcmp(mode, "RGB") == 0) - convert = p2rgb; - else if (strcmp(mode, "RGBA") == 0) - convert = (alpha) ? pa2rgba : p2rgba; - else if (strcmp(mode, "RGBX") == 0) - convert = p2rgba; - else if (strcmp(mode, "CMYK") == 0) - convert = p2cmyk; - else if (strcmp(mode, "YCbCr") == 0) - convert = p2ycbcr; - else - return (Imaging) ImagingError_ValueError("conversion not supported"); + if (strcmp(mode, "1") == 0) { + convert = alpha ? pa2bit : p2bit; + } else if (strcmp(mode, "L") == 0) { + convert = alpha ? pa2l : p2l; + } else if (strcmp(mode, "LA") == 0) { + convert = alpha ? pa2la : p2la; + } else if (strcmp(mode, "P") == 0) { + convert = pa2p; + } else if (strcmp(mode, "PA") == 0) { + convert = p2pa; + } else if (strcmp(mode, "I") == 0) { + convert = alpha ? pa2i : p2i; + } else if (strcmp(mode, "F") == 0) { + convert = alpha ? pa2f : p2f; + } else if (strcmp(mode, "RGB") == 0) { + convert = alpha ? pa2rgb : p2rgb; + } else if (strcmp(mode, "RGBA") == 0 || strcmp(mode, "RGBX") == 0) { + convert = alpha ? pa2rgba : p2rgba; + } else if (strcmp(mode, "CMYK") == 0) { + convert = alpha ? pa2cmyk : p2cmyk; + } else if (strcmp(mode, "YCbCr") == 0) { + convert = alpha ? pa2ycbcr : p2ycbcr; + } else if (strcmp(mode, "HSV") == 0) { + convert = alpha ? pa2hsv : p2hsv; + } else { + return (Imaging)ImagingError_ValueError("conversion not supported"); + } imOut = ImagingNew2Dirty(mode, imOut, imIn); - if (!imOut) + if (!imOut) { return NULL; + } + if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) { + ImagingPaletteDelete(imOut->palette); + imOut->palette = ImagingPaletteDuplicate(imIn->palette); + } ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) - (*convert)((UINT8*) imOut->image[y], (UINT8*) imIn->image[y], - imIn->xsize, imIn->palette->palette); + for (y = 0; y < imIn->ysize; y++) { + (*convert)( + (UINT8 *)imOut->image[y], + (UINT8 *)imIn->image[y], + imIn->xsize, + imIn->palette); + } ImagingSectionLeave(&cookie); return imOut; @@ -1053,32 +1258,44 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) #pragma optimize("", off) #endif static Imaging -topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) -{ +topalette( + Imaging imOut, + Imaging imIn, + const char *mode, + ImagingPalette inpalette, + int dither) { ImagingSectionCookie cookie; + int alpha; int x, y; - ImagingPalette palette = inpalette;; + ImagingPalette palette = inpalette; + ; /* Map L or RGB/RGBX/RGBA to palette image */ - if (strcmp(imIn->mode, "L") != 0 && strncmp(imIn->mode, "RGB", 3) != 0) - return (Imaging) ImagingError_ValueError("conversion not supported"); + if (strcmp(imIn->mode, "L") != 0 && strncmp(imIn->mode, "RGB", 3) != 0) { + return (Imaging)ImagingError_ValueError("conversion not supported"); + } + + alpha = !strcmp(mode, "PA"); if (palette == NULL) { - /* FIXME: make user configurable */ - if (imIn->bands == 1) - palette = ImagingPaletteNew("RGB"); /* Initialised to grey ramp */ - else - palette = ImagingPaletteNewBrowser(); /* Standard colour cube */ + /* FIXME: make user configurable */ + if (imIn->bands == 1) { + palette = ImagingPaletteNew("RGB"); /* Initialised to grey ramp */ + } else { + palette = ImagingPaletteNewBrowser(); /* Standard colour cube */ + } } - if (!palette) - return (Imaging) ImagingError_ValueError("no palette"); + if (!palette) { + return (Imaging)ImagingError_ValueError("no palette"); + } - imOut = ImagingNew2Dirty("P", imOut, imIn); + imOut = ImagingNew2Dirty(mode, imOut, imIn); if (!imOut) { - if (palette != inpalette) - ImagingPaletteDelete(palette); - return NULL; + if (palette != inpalette) { + ImagingPaletteDelete(palette); + } + return NULL; } ImagingPaletteDelete(imOut->palette); @@ -1089,8 +1306,13 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) /* Greyscale palette: copy data as is */ ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) - memcpy(imOut->image[y], imIn->image[y], imIn->linesize); + for (y = 0; y < imIn->ysize; y++) { + if (alpha) { + l2la((UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize); + } else { + memcpy(imOut->image[y], imIn->image[y], imIn->linesize); + } + } ImagingSectionLeave(&cookie); } else { @@ -1099,15 +1321,16 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) /* Create mapping cache */ if (ImagingPaletteCachePrepare(palette) < 0) { ImagingDelete(imOut); - if (palette != inpalette) - ImagingPaletteDelete(palette); + if (palette != inpalette) { + ImagingPaletteDelete(palette); + } return NULL; } if (dither) { /* floyd-steinberg dither */ - int* errors; + int *errors; errors = calloc(imIn->xsize + 1, sizeof(int) * 3); if (!errors) { ImagingDelete(imOut); @@ -1120,9 +1343,9 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) int r, r0, r1, r2; int g, g0, g1, g2; int b, b0, b1, b2; - UINT8* in = (UINT8*) imIn->image[y]; - UINT8* out = imOut->image8[y]; - int* e = errors; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; + int *e = errors; r = r0 = r1 = 0; g = g0 = g1 = 0; @@ -1130,91 +1353,121 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) for (x = 0; x < imIn->xsize; x++, in += 4) { int d2; - INT16* cache; + INT16 *cache; - r = CLIP(in[0] + (r + e[3+0])/16); - g = CLIP(in[1] + (g + e[3+1])/16); - b = CLIP(in[2] + (b + e[3+2])/16); + r = CLIP8(in[0] + (r + e[3 + 0]) / 16); + g = CLIP8(in[1] + (g + e[3 + 1]) / 16); + b = CLIP8(in[2] + (b + e[3 + 2]) / 16); /* get closest colour */ cache = &ImagingPaletteCache(palette, r, g, b); - if (cache[0] == 0x100) + if (cache[0] == 0x100) { ImagingPaletteCacheUpdate(palette, r, g, b); - out[x] = (UINT8) cache[0]; - - r -= (int) palette->palette[cache[0]*4]; - g -= (int) palette->palette[cache[0]*4+1]; - b -= (int) palette->palette[cache[0]*4+2]; + } + if (alpha) { + out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)cache[0]; + out[x * 4 + 3] = 255; + } else { + out[x] = (UINT8)cache[0]; + } + + r -= (int)palette->palette[cache[0] * 4]; + g -= (int)palette->palette[cache[0] * 4 + 1]; + b -= (int)palette->palette[cache[0] * 4 + 2]; /* propagate errors (don't ask ;-) */ - r2 = r; d2 = r + r; r += d2; e[0] = r + r0; - r += d2; r0 = r + r1; r1 = r2; r += d2; - g2 = g; d2 = g + g; g += d2; e[1] = g + g0; - g += d2; g0 = g + g1; g1 = g2; g += d2; - b2 = b; d2 = b + b; b += d2; e[2] = b + b0; - b += d2; b0 = b + b1; b1 = b2; b += d2; + r2 = r; + d2 = r + r; + r += d2; + e[0] = r + r0; + r += d2; + r0 = r + r1; + r1 = r2; + r += d2; + g2 = g; + d2 = g + g; + g += d2; + e[1] = g + g0; + g += d2; + g0 = g + g1; + g1 = g2; + g += d2; + b2 = b; + d2 = b + b; + b += d2; + e[2] = b + b0; + b += d2; + b0 = b + b1; + b1 = b2; + b += d2; e += 3; - } e[0] = b0; e[1] = b1; e[2] = b2; - } ImagingSectionLeave(&cookie); free(errors); } else { - /* closest colour */ ImagingSectionEnter(&cookie); for (y = 0; y < imIn->ysize; y++) { int r, g, b; - UINT8* in = (UINT8*) imIn->image[y]; - UINT8* out = imOut->image8[y]; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = alpha ? (UINT8 *)imOut->image32[y] : imOut->image8[y]; for (x = 0; x < imIn->xsize; x++, in += 4) { - INT16* cache; + INT16 *cache; - r = in[0]; g = in[1]; b = in[2]; + r = in[0]; + g = in[1]; + b = in[2]; /* get closest colour */ cache = &ImagingPaletteCache(palette, r, g, b); - if (cache[0] == 0x100) + if (cache[0] == 0x100) { ImagingPaletteCacheUpdate(palette, r, g, b); - out[x] = (UINT8) cache[0]; - + } + if (alpha) { + out[x * 4] = out[x * 4 + 1] = out[x * 4 + 2] = (UINT8)cache[0]; + out[x * 4 + 3] = 255; + } else { + out[x] = (UINT8)cache[0]; + } } } ImagingSectionLeave(&cookie); - } - if (inpalette != palette) - ImagingPaletteCacheDelete(palette); + if (inpalette != palette) { + ImagingPaletteCacheDelete(palette); + } } - if (inpalette != palette) - ImagingPaletteDelete(palette); + if (inpalette != palette) { + ImagingPaletteDelete(palette); + } return imOut; } static Imaging -tobilevel(Imaging imOut, Imaging imIn, int dither) -{ +tobilevel(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int x, y; - int* errors; + int *errors; /* Map L or RGB to dithered 1 image */ - if (strcmp(imIn->mode, "L") != 0 && strcmp(imIn->mode, "RGB") != 0) - return (Imaging) ImagingError_ValueError("conversion not supported"); + if (strcmp(imIn->mode, "L") != 0 && strcmp(imIn->mode, "RGB") != 0) { + return (Imaging)ImagingError_ValueError("conversion not supported"); + } imOut = ImagingNew2Dirty("1", imOut, imIn); - if (!imOut) + if (!imOut) { return NULL; + } errors = calloc(imIn->xsize + 1, sizeof(int)); if (!errors) { @@ -1223,59 +1476,64 @@ tobilevel(Imaging imOut, Imaging imIn, int dither) } if (imIn->bands == 1) { - /* map each pixel to black or white, using error diffusion */ ImagingSectionEnter(&cookie); for (y = 0; y < imIn->ysize; y++) { int l, l0, l1, l2, d2; - UINT8* in = (UINT8*) imIn->image[y]; - UINT8* out = imOut->image8[y]; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = imOut->image8[y]; l = l0 = l1 = 0; for (x = 0; x < imIn->xsize; x++) { - /* pick closest colour */ - l = CLIP(in[x] + (l + errors[x+1])/16); + l = CLIP8(in[x] + (l + errors[x + 1]) / 16); out[x] = (l > 128) ? 255 : 0; /* propagate errors */ - l -= (int) out[x]; - l2 = l; d2 = l + l; l += d2; errors[x] = l + l0; - l += d2; l0 = l + l1; l1 = l2; l += d2; + l -= (int)out[x]; + l2 = l; + d2 = l + l; + l += d2; + errors[x] = l + l0; + l += d2; + l0 = l + l1; + l1 = l2; + l += d2; } errors[x] = l0; - } ImagingSectionLeave(&cookie); } else { - /* map each pixel to black or white, using error diffusion */ ImagingSectionEnter(&cookie); for (y = 0; y < imIn->ysize; y++) { int l, l0, l1, l2, d2; - UINT8* in = (UINT8*) imIn->image[y]; - UINT8* out = imOut->image8[y]; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = imOut->image8[y]; l = l0 = l1 = 0; for (x = 0; x < imIn->xsize; x++, in += 4) { - /* pick closest colour */ - l = CLIP(L(in)/1000 + (l + errors[x+1])/16); + l = CLIP8(L(in) / 1000 + (l + errors[x + 1]) / 16); out[x] = (l > 128) ? 255 : 0; /* propagate errors */ - l -= (int) out[x]; - l2 = l; d2 = l + l; l += d2; errors[x] = l + l0; - l += d2; l0 = l + l1; l1 = l2; l += d2; - + l -= (int)out[x]; + l2 = l; + d2 = l + l; + l += d2; + errors[x] = l + l0; + l += d2; + l0 = l + l1; + l1 = l2; + l += d2; } errors[x] = l0; - } ImagingSectionLeave(&cookie); } @@ -1289,161 +1547,162 @@ tobilevel(Imaging imOut, Imaging imIn, int dither) #endif static Imaging -convert(Imaging imOut, Imaging imIn, const char *mode, - ImagingPalette palette, int dither) -{ +convert( + Imaging imOut, Imaging imIn, const char *mode, ImagingPalette palette, int dither) { ImagingSectionCookie cookie; ImagingShuffler convert; int y; - if (!imIn) - return (Imaging) ImagingError_ModeError(); + if (!imIn) { + return (Imaging)ImagingError_ModeError(); + } if (!mode) { /* Map palette image to full depth */ - if (!imIn->palette) - return (Imaging) ImagingError_ModeError(); + if (!imIn->palette) { + return (Imaging)ImagingError_ModeError(); + } mode = imIn->palette->mode; - } else + } else { /* Same mode? */ - if (!strcmp(imIn->mode, mode)) + if (!strcmp(imIn->mode, mode)) { return ImagingCopy2(imOut, imIn); - + } + } /* test for special conversions */ - if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "PA") == 0) + if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "PA") == 0) { return frompalette(imOut, imIn, mode); + } - if (strcmp(mode, "P") == 0) - return topalette(imOut, imIn, palette, dither); - - if (dither && strcmp(mode, "1") == 0) - return tobilevel(imOut, imIn, dither); + if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) { + return topalette(imOut, imIn, mode, palette, dither); + } + if (dither && strcmp(mode, "1") == 0) { + return tobilevel(imOut, imIn); + } /* standard conversion machinery */ convert = NULL; - for (y = 0; converters[y].from; y++) + for (y = 0; converters[y].from; y++) { if (!strcmp(imIn->mode, converters[y].from) && !strcmp(mode, converters[y].to)) { convert = converters[y].convert; break; } + } - if (!convert) + if (!convert) { #ifdef notdef - return (Imaging) ImagingError_ValueError("conversion not supported"); + return (Imaging)ImagingError_ValueError("conversion not supported"); #else - { - static char buf[256]; - /* FIXME: may overflow if mode is too large */ - sprintf(buf, "conversion from %s to %s not supported", imIn->mode, mode); - return (Imaging) ImagingError_ValueError(buf); - } + static char buf[100]; + snprintf(buf, 100, "conversion from %.10s to %.10s not supported", imIn->mode, mode); + return (Imaging)ImagingError_ValueError(buf); #endif + } imOut = ImagingNew2Dirty(mode, imOut, imIn); - if (!imOut) + if (!imOut) { return NULL; + } ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) - (*convert)((UINT8*) imOut->image[y], (UINT8*) imIn->image[y], - imIn->xsize); + for (y = 0; y < imIn->ysize; y++) { + (*convert)((UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize); + } ImagingSectionLeave(&cookie); return imOut; } Imaging -ImagingConvert(Imaging imIn, const char *mode, - ImagingPalette palette, int dither) -{ +ImagingConvert(Imaging imIn, const char *mode, ImagingPalette palette, int dither) { return convert(NULL, imIn, mode, palette, dither); } Imaging -ImagingConvert2(Imaging imOut, Imaging imIn) -{ +ImagingConvert2(Imaging imOut, Imaging imIn) { return convert(imOut, imIn, imOut->mode, NULL, 0); } - Imaging -ImagingConvertTransparent(Imaging imIn, const char *mode, - int r, int g, int b) -{ +ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { ImagingSectionCookie cookie; ImagingShuffler convert; Imaging imOut = NULL; int y; - if (!imIn){ - return (Imaging) ImagingError_ModeError(); - } - - if (!((strcmp(imIn->mode, "RGB") == 0 || - strcmp(imIn->mode, "L") == 0) - && strcmp(mode, "RGBA") == 0)) -#ifdef notdef - { - return (Imaging) ImagingError_ValueError("conversion not supported"); - } -#else - { - static char buf[256]; - /* FIXME: may overflow if mode is too large */ - sprintf(buf, "conversion from %s to %s not supported in convert_transparent", imIn->mode, mode); - return (Imaging) ImagingError_ValueError(buf); + if (!imIn) { + return (Imaging)ImagingError_ModeError(); } -#endif - if (strcmp(imIn->mode, "RGB") == 0) { + if (strcmp(imIn->mode, "RGB") == 0 && strcmp(mode, "RGBA") == 0) { convert = rgb2rgba; - } else { - convert = l2rgb; + } else if ((strcmp(imIn->mode, "1") == 0 || + strcmp(imIn->mode, "I") == 0 || + strcmp(imIn->mode, "L") == 0 + ) && ( + strcmp(mode, "RGBA") == 0 || + strcmp(mode, "LA") == 0 + )) { + if (strcmp(imIn->mode, "1") == 0) { + convert = bit2rgb; + } else if (strcmp(imIn->mode, "I") == 0) { + convert = i2rgb; + } else { + convert = l2rgb; + } g = b = r; + } else { + static char buf[100]; + snprintf( + buf, + 100, + "conversion from %.10s to %.10s not supported in convert_transparent", + imIn->mode, + mode); + return (Imaging)ImagingError_ValueError(buf); } imOut = ImagingNew2Dirty(mode, imOut, imIn); - if (!imOut){ + if (!imOut) { return NULL; } ImagingSectionEnter(&cookie); for (y = 0; y < imIn->ysize; y++) { - (*convert)((UINT8*) imOut->image[y], (UINT8*) imIn->image[y], - imIn->xsize); - rgbT2rgba((UINT8*) imOut->image[y], imIn->xsize, r, g, b); + (*convert)((UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize); + rgbT2rgba((UINT8 *)imOut->image[y], imIn->xsize, r, g, b); } ImagingSectionLeave(&cookie); return imOut; - } Imaging -ImagingConvertInPlace(Imaging imIn, const char* mode) -{ +ImagingConvertInPlace(Imaging imIn, const char *mode) { ImagingSectionCookie cookie; ImagingShuffler convert; int y; /* limited support for inplace conversion */ - if (strcmp(imIn->mode, "L") == 0 && strcmp(mode, "1") == 0) + if (strcmp(imIn->mode, "L") == 0 && strcmp(mode, "1") == 0) { convert = l2bit; - else if (strcmp(imIn->mode, "1") == 0 && strcmp(mode, "L") == 0) + } else if (strcmp(imIn->mode, "1") == 0 && strcmp(mode, "L") == 0) { convert = bit2l; - else + } else { return ImagingError_ModeError(); + } ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) - (*convert)((UINT8*) imIn->image[y], (UINT8*) imIn->image[y], - imIn->xsize); + for (y = 0; y < imIn->ysize; y++) { + (*convert)((UINT8 *)imIn->image[y], (UINT8 *)imIn->image[y], imIn->xsize); + } ImagingSectionLeave(&cookie); return imIn; diff --git a/src/libImaging/ConvertYCbCr.c b/src/libImaging/ConvertYCbCr.c index 6ce549111e4..142f065e57d 100644 --- a/src/libImaging/ConvertYCbCr.c +++ b/src/libImaging/ConvertYCbCr.c @@ -5,7 +5,7 @@ * code to convert YCbCr data * * history: - * 98-07-01 hk Created + * 98-07-01 hk Created * * Copyright (c) Secret Labs AB 1998 * @@ -28,356 +28,332 @@ #define SCALE 6 /* bits */ -static INT16 Y_R[] = { 0, 19, 38, 57, 77, 96, 115, 134, 153, 172, 191, -210, 230, 249, 268, 287, 306, 325, 344, 364, 383, 402, 421, 440, 459, -478, 498, 517, 536, 555, 574, 593, 612, 631, 651, 670, 689, 708, 727, -746, 765, 785, 804, 823, 842, 861, 880, 899, 919, 938, 957, 976, 995, -1014, 1033, 1052, 1072, 1091, 1110, 1129, 1148, 1167, 1186, 1206, -1225, 1244, 1263, 1282, 1301, 1320, 1340, 1359, 1378, 1397, 1416, -1435, 1454, 1473, 1493, 1512, 1531, 1550, 1569, 1588, 1607, 1627, -1646, 1665, 1684, 1703, 1722, 1741, 1761, 1780, 1799, 1818, 1837, -1856, 1875, 1894, 1914, 1933, 1952, 1971, 1990, 2009, 2028, 2048, -2067, 2086, 2105, 2124, 2143, 2162, 2182, 2201, 2220, 2239, 2258, -2277, 2296, 2315, 2335, 2354, 2373, 2392, 2411, 2430, 2449, 2469, -2488, 2507, 2526, 2545, 2564, 2583, 2602, 2622, 2641, 2660, 2679, -2698, 2717, 2736, 2756, 2775, 2794, 2813, 2832, 2851, 2870, 2890, -2909, 2928, 2947, 2966, 2985, 3004, 3023, 3043, 3062, 3081, 3100, -3119, 3138, 3157, 3177, 3196, 3215, 3234, 3253, 3272, 3291, 3311, -3330, 3349, 3368, 3387, 3406, 3425, 3444, 3464, 3483, 3502, 3521, -3540, 3559, 3578, 3598, 3617, 3636, 3655, 3674, 3693, 3712, 3732, -3751, 3770, 3789, 3808, 3827, 3846, 3865, 3885, 3904, 3923, 3942, -3961, 3980, 3999, 4019, 4038, 4057, 4076, 4095, 4114, 4133, 4153, -4172, 4191, 4210, 4229, 4248, 4267, 4286, 4306, 4325, 4344, 4363, -4382, 4401, 4420, 4440, 4459, 4478, 4497, 4516, 4535, 4554, 4574, -4593, 4612, 4631, 4650, 4669, 4688, 4707, 4727, 4746, 4765, 4784, -4803, 4822, 4841, 4861, 4880 }; +static INT16 Y_R[] = { + 0, 19, 38, 57, 77, 96, 115, 134, 153, 172, 191, 210, 230, 249, + 268, 287, 306, 325, 344, 364, 383, 402, 421, 440, 459, 478, 498, 517, + 536, 555, 574, 593, 612, 631, 651, 670, 689, 708, 727, 746, 765, 785, + 804, 823, 842, 861, 880, 899, 919, 938, 957, 976, 995, 1014, 1033, 1052, + 1072, 1091, 1110, 1129, 1148, 1167, 1186, 1206, 1225, 1244, 1263, 1282, 1301, 1320, + 1340, 1359, 1378, 1397, 1416, 1435, 1454, 1473, 1493, 1512, 1531, 1550, 1569, 1588, + 1607, 1627, 1646, 1665, 1684, 1703, 1722, 1741, 1761, 1780, 1799, 1818, 1837, 1856, + 1875, 1894, 1914, 1933, 1952, 1971, 1990, 2009, 2028, 2048, 2067, 2086, 2105, 2124, + 2143, 2162, 2182, 2201, 2220, 2239, 2258, 2277, 2296, 2315, 2335, 2354, 2373, 2392, + 2411, 2430, 2449, 2469, 2488, 2507, 2526, 2545, 2564, 2583, 2602, 2622, 2641, 2660, + 2679, 2698, 2717, 2736, 2756, 2775, 2794, 2813, 2832, 2851, 2870, 2890, 2909, 2928, + 2947, 2966, 2985, 3004, 3023, 3043, 3062, 3081, 3100, 3119, 3138, 3157, 3177, 3196, + 3215, 3234, 3253, 3272, 3291, 3311, 3330, 3349, 3368, 3387, 3406, 3425, 3444, 3464, + 3483, 3502, 3521, 3540, 3559, 3578, 3598, 3617, 3636, 3655, 3674, 3693, 3712, 3732, + 3751, 3770, 3789, 3808, 3827, 3846, 3865, 3885, 3904, 3923, 3942, 3961, 3980, 3999, + 4019, 4038, 4057, 4076, 4095, 4114, 4133, 4153, 4172, 4191, 4210, 4229, 4248, 4267, + 4286, 4306, 4325, 4344, 4363, 4382, 4401, 4420, 4440, 4459, 4478, 4497, 4516, 4535, + 4554, 4574, 4593, 4612, 4631, 4650, 4669, 4688, 4707, 4727, 4746, 4765, 4784, 4803, + 4822, 4841, 4861, 4880}; -static INT16 Y_G[] = { 0, 38, 75, 113, 150, 188, 225, 263, 301, 338, -376, 413, 451, 488, 526, 564, 601, 639, 676, 714, 751, 789, 826, 864, -902, 939, 977, 1014, 1052, 1089, 1127, 1165, 1202, 1240, 1277, 1315, -1352, 1390, 1428, 1465, 1503, 1540, 1578, 1615, 1653, 1691, 1728, -1766, 1803, 1841, 1878, 1916, 1954, 1991, 2029, 2066, 2104, 2141, -2179, 2217, 2254, 2292, 2329, 2367, 2404, 2442, 2479, 2517, 2555, -2592, 2630, 2667, 2705, 2742, 2780, 2818, 2855, 2893, 2930, 2968, -3005, 3043, 3081, 3118, 3156, 3193, 3231, 3268, 3306, 3344, 3381, -3419, 3456, 3494, 3531, 3569, 3607, 3644, 3682, 3719, 3757, 3794, -3832, 3870, 3907, 3945, 3982, 4020, 4057, 4095, 4132, 4170, 4208, -4245, 4283, 4320, 4358, 4395, 4433, 4471, 4508, 4546, 4583, 4621, -4658, 4696, 4734, 4771, 4809, 4846, 4884, 4921, 4959, 4997, 5034, -5072, 5109, 5147, 5184, 5222, 5260, 5297, 5335, 5372, 5410, 5447, -5485, 5522, 5560, 5598, 5635, 5673, 5710, 5748, 5785, 5823, 5861, -5898, 5936, 5973, 6011, 6048, 6086, 6124, 6161, 6199, 6236, 6274, -6311, 6349, 6387, 6424, 6462, 6499, 6537, 6574, 6612, 6650, 6687, -6725, 6762, 6800, 6837, 6875, 6913, 6950, 6988, 7025, 7063, 7100, -7138, 7175, 7213, 7251, 7288, 7326, 7363, 7401, 7438, 7476, 7514, -7551, 7589, 7626, 7664, 7701, 7739, 7777, 7814, 7852, 7889, 7927, -7964, 8002, 8040, 8077, 8115, 8152, 8190, 8227, 8265, 8303, 8340, -8378, 8415, 8453, 8490, 8528, 8566, 8603, 8641, 8678, 8716, 8753, -8791, 8828, 8866, 8904, 8941, 8979, 9016, 9054, 9091, 9129, 9167, -9204, 9242, 9279, 9317, 9354, 9392, 9430, 9467, 9505, 9542, 9580 }; +static INT16 Y_G[] = { + 0, 38, 75, 113, 150, 188, 225, 263, 301, 338, 376, 413, 451, 488, + 526, 564, 601, 639, 676, 714, 751, 789, 826, 864, 902, 939, 977, 1014, + 1052, 1089, 1127, 1165, 1202, 1240, 1277, 1315, 1352, 1390, 1428, 1465, 1503, 1540, + 1578, 1615, 1653, 1691, 1728, 1766, 1803, 1841, 1878, 1916, 1954, 1991, 2029, 2066, + 2104, 2141, 2179, 2217, 2254, 2292, 2329, 2367, 2404, 2442, 2479, 2517, 2555, 2592, + 2630, 2667, 2705, 2742, 2780, 2818, 2855, 2893, 2930, 2968, 3005, 3043, 3081, 3118, + 3156, 3193, 3231, 3268, 3306, 3344, 3381, 3419, 3456, 3494, 3531, 3569, 3607, 3644, + 3682, 3719, 3757, 3794, 3832, 3870, 3907, 3945, 3982, 4020, 4057, 4095, 4132, 4170, + 4208, 4245, 4283, 4320, 4358, 4395, 4433, 4471, 4508, 4546, 4583, 4621, 4658, 4696, + 4734, 4771, 4809, 4846, 4884, 4921, 4959, 4997, 5034, 5072, 5109, 5147, 5184, 5222, + 5260, 5297, 5335, 5372, 5410, 5447, 5485, 5522, 5560, 5598, 5635, 5673, 5710, 5748, + 5785, 5823, 5861, 5898, 5936, 5973, 6011, 6048, 6086, 6124, 6161, 6199, 6236, 6274, + 6311, 6349, 6387, 6424, 6462, 6499, 6537, 6574, 6612, 6650, 6687, 6725, 6762, 6800, + 6837, 6875, 6913, 6950, 6988, 7025, 7063, 7100, 7138, 7175, 7213, 7251, 7288, 7326, + 7363, 7401, 7438, 7476, 7514, 7551, 7589, 7626, 7664, 7701, 7739, 7777, 7814, 7852, + 7889, 7927, 7964, 8002, 8040, 8077, 8115, 8152, 8190, 8227, 8265, 8303, 8340, 8378, + 8415, 8453, 8490, 8528, 8566, 8603, 8641, 8678, 8716, 8753, 8791, 8828, 8866, 8904, + 8941, 8979, 9016, 9054, 9091, 9129, 9167, 9204, 9242, 9279, 9317, 9354, 9392, 9430, + 9467, 9505, 9542, 9580}; -static INT16 Y_B[] = { 0, 7, 15, 22, 29, 36, 44, 51, 58, 66, 73, 80, -88, 95, 102, 109, 117, 124, 131, 139, 146, 153, 161, 168, 175, 182, -190, 197, 204, 212, 219, 226, 233, 241, 248, 255, 263, 270, 277, 285, -292, 299, 306, 314, 321, 328, 336, 343, 350, 358, 365, 372, 379, 387, -394, 401, 409, 416, 423, 430, 438, 445, 452, 460, 467, 474, 482, 489, -496, 503, 511, 518, 525, 533, 540, 547, 554, 562, 569, 576, 584, 591, -598, 606, 613, 620, 627, 635, 642, 649, 657, 664, 671, 679, 686, 693, -700, 708, 715, 722, 730, 737, 744, 751, 759, 766, 773, 781, 788, 795, -803, 810, 817, 824, 832, 839, 846, 854, 861, 868, 876, 883, 890, 897, -905, 912, 919, 927, 934, 941, 948, 956, 963, 970, 978, 985, 992, 1000, -1007, 1014, 1021, 1029, 1036, 1043, 1051, 1058, 1065, 1073, 1080, -1087, 1094, 1102, 1109, 1116, 1124, 1131, 1138, 1145, 1153, 1160, -1167, 1175, 1182, 1189, 1197, 1204, 1211, 1218, 1226, 1233, 1240, -1248, 1255, 1262, 1270, 1277, 1284, 1291, 1299, 1306, 1313, 1321, -1328, 1335, 1342, 1350, 1357, 1364, 1372, 1379, 1386, 1394, 1401, -1408, 1415, 1423, 1430, 1437, 1445, 1452, 1459, 1466, 1474, 1481, -1488, 1496, 1503, 1510, 1518, 1525, 1532, 1539, 1547, 1554, 1561, -1569, 1576, 1583, 1591, 1598, 1605, 1612, 1620, 1627, 1634, 1642, -1649, 1656, 1663, 1671, 1678, 1685, 1693, 1700, 1707, 1715, 1722, -1729, 1736, 1744, 1751, 1758, 1766, 1773, 1780, 1788, 1795, 1802, -1809, 1817, 1824, 1831, 1839, 1846, 1853, 1860 }; +static INT16 Y_B[] = { + 0, 7, 15, 22, 29, 36, 44, 51, 58, 66, 73, 80, 88, 95, + 102, 109, 117, 124, 131, 139, 146, 153, 161, 168, 175, 182, 190, 197, + 204, 212, 219, 226, 233, 241, 248, 255, 263, 270, 277, 285, 292, 299, + 306, 314, 321, 328, 336, 343, 350, 358, 365, 372, 379, 387, 394, 401, + 409, 416, 423, 430, 438, 445, 452, 460, 467, 474, 482, 489, 496, 503, + 511, 518, 525, 533, 540, 547, 554, 562, 569, 576, 584, 591, 598, 606, + 613, 620, 627, 635, 642, 649, 657, 664, 671, 679, 686, 693, 700, 708, + 715, 722, 730, 737, 744, 751, 759, 766, 773, 781, 788, 795, 803, 810, + 817, 824, 832, 839, 846, 854, 861, 868, 876, 883, 890, 897, 905, 912, + 919, 927, 934, 941, 948, 956, 963, 970, 978, 985, 992, 1000, 1007, 1014, + 1021, 1029, 1036, 1043, 1051, 1058, 1065, 1073, 1080, 1087, 1094, 1102, 1109, 1116, + 1124, 1131, 1138, 1145, 1153, 1160, 1167, 1175, 1182, 1189, 1197, 1204, 1211, 1218, + 1226, 1233, 1240, 1248, 1255, 1262, 1270, 1277, 1284, 1291, 1299, 1306, 1313, 1321, + 1328, 1335, 1342, 1350, 1357, 1364, 1372, 1379, 1386, 1394, 1401, 1408, 1415, 1423, + 1430, 1437, 1445, 1452, 1459, 1466, 1474, 1481, 1488, 1496, 1503, 1510, 1518, 1525, + 1532, 1539, 1547, 1554, 1561, 1569, 1576, 1583, 1591, 1598, 1605, 1612, 1620, 1627, + 1634, 1642, 1649, 1656, 1663, 1671, 1678, 1685, 1693, 1700, 1707, 1715, 1722, 1729, + 1736, 1744, 1751, 1758, 1766, 1773, 1780, 1788, 1795, 1802, 1809, 1817, 1824, 1831, + 1839, 1846, 1853, 1860}; -static INT16 Cb_R[] = { 0, -10, -21, -31, -42, -53, -64, -75, -85, --96, -107, -118, -129, -139, -150, -161, -172, -183, -193, -204, -215, --226, -237, -247, -258, -269, -280, -291, -301, -312, -323, -334, --345, -355, -366, -377, -388, -399, -409, -420, -431, -442, -453, --463, -474, -485, -496, -507, -517, -528, -539, -550, -561, -571, --582, -593, -604, -615, -625, -636, -647, -658, -669, -679, -690, --701, -712, -723, -733, -744, -755, -766, -777, -787, -798, -809, --820, -831, -841, -852, -863, -874, -885, -895, -906, -917, -928, --939, -949, -960, -971, -982, -993, -1003, -1014, -1025, -1036, -1047, --1057, -1068, -1079, -1090, -1101, -1111, -1122, -1133, -1144, -1155, --1165, -1176, -1187, -1198, -1209, -1219, -1230, -1241, -1252, -1263, --1273, -1284, -1295, -1306, -1317, -1327, -1338, -1349, -1360, -1371, --1381, -1392, -1403, -1414, -1425, -1435, -1446, -1457, -1468, -1479, --1489, -1500, -1511, -1522, -1533, -1543, -1554, -1565, -1576, -1587, --1597, -1608, -1619, -1630, -1641, -1651, -1662, -1673, -1684, -1694, --1705, -1716, -1727, -1738, -1748, -1759, -1770, -1781, -1792, -1802, --1813, -1824, -1835, -1846, -1856, -1867, -1878, -1889, -1900, -1910, --1921, -1932, -1943, -1954, -1964, -1975, -1986, -1997, -2008, -2018, --2029, -2040, -2051, -2062, -2072, -2083, -2094, -2105, -2116, -2126, --2137, -2148, -2159, -2170, -2180, -2191, -2202, -2213, -2224, -2234, --2245, -2256, -2267, -2278, -2288, -2299, -2310, -2321, -2332, -2342, --2353, -2364, -2375, -2386, -2396, -2407, -2418, -2429, -2440, -2450, --2461, -2472, -2483, -2494, -2504, -2515, -2526, -2537, -2548, -2558, --2569, -2580, -2591, -2602, -2612, -2623, -2634, -2645, -2656, -2666, --2677, -2688, -2699, -2710, -2720, -2731, -2742, -2753 }; +static INT16 Cb_R[] = { + 0, -10, -21, -31, -42, -53, -64, -75, -85, -96, -107, -118, + -129, -139, -150, -161, -172, -183, -193, -204, -215, -226, -237, -247, + -258, -269, -280, -291, -301, -312, -323, -334, -345, -355, -366, -377, + -388, -399, -409, -420, -431, -442, -453, -463, -474, -485, -496, -507, + -517, -528, -539, -550, -561, -571, -582, -593, -604, -615, -625, -636, + -647, -658, -669, -679, -690, -701, -712, -723, -733, -744, -755, -766, + -777, -787, -798, -809, -820, -831, -841, -852, -863, -874, -885, -895, + -906, -917, -928, -939, -949, -960, -971, -982, -993, -1003, -1014, -1025, + -1036, -1047, -1057, -1068, -1079, -1090, -1101, -1111, -1122, -1133, -1144, -1155, + -1165, -1176, -1187, -1198, -1209, -1219, -1230, -1241, -1252, -1263, -1273, -1284, + -1295, -1306, -1317, -1327, -1338, -1349, -1360, -1371, -1381, -1392, -1403, -1414, + -1425, -1435, -1446, -1457, -1468, -1479, -1489, -1500, -1511, -1522, -1533, -1543, + -1554, -1565, -1576, -1587, -1597, -1608, -1619, -1630, -1641, -1651, -1662, -1673, + -1684, -1694, -1705, -1716, -1727, -1738, -1748, -1759, -1770, -1781, -1792, -1802, + -1813, -1824, -1835, -1846, -1856, -1867, -1878, -1889, -1900, -1910, -1921, -1932, + -1943, -1954, -1964, -1975, -1986, -1997, -2008, -2018, -2029, -2040, -2051, -2062, + -2072, -2083, -2094, -2105, -2116, -2126, -2137, -2148, -2159, -2170, -2180, -2191, + -2202, -2213, -2224, -2234, -2245, -2256, -2267, -2278, -2288, -2299, -2310, -2321, + -2332, -2342, -2353, -2364, -2375, -2386, -2396, -2407, -2418, -2429, -2440, -2450, + -2461, -2472, -2483, -2494, -2504, -2515, -2526, -2537, -2548, -2558, -2569, -2580, + -2591, -2602, -2612, -2623, -2634, -2645, -2656, -2666, -2677, -2688, -2699, -2710, + -2720, -2731, -2742, -2753}; -static INT16 Cb_G[] = { 0, -20, -41, -63, -84, -105, -126, -147, -169, --190, -211, -232, -253, -275, -296, -317, -338, -359, -381, -402, --423, -444, -465, -487, -508, -529, -550, -571, -593, -614, -635, --656, -677, -699, -720, -741, -762, -783, -805, -826, -847, -868, --889, -911, -932, -953, -974, -995, -1017, -1038, -1059, -1080, -1101, --1123, -1144, -1165, -1186, -1207, -1229, -1250, -1271, -1292, -1313, --1335, -1356, -1377, -1398, -1419, -1441, -1462, -1483, -1504, -1525, --1547, -1568, -1589, -1610, -1631, -1653, -1674, -1695, -1716, -1737, --1759, -1780, -1801, -1822, -1843, -1865, -1886, -1907, -1928, -1949, --1971, -1992, -2013, -2034, -2055, -2077, -2098, -2119, -2140, -2161, --2183, -2204, -2225, -2246, -2267, -2289, -2310, -2331, -2352, -2373, --2395, -2416, -2437, -2458, -2479, -2501, -2522, -2543, -2564, -2585, --2607, -2628, -2649, -2670, -2691, -2713, -2734, -2755, -2776, -2797, --2819, -2840, -2861, -2882, -2903, -2925, -2946, -2967, -2988, -3009, --3031, -3052, -3073, -3094, -3115, -3137, -3158, -3179, -3200, -3221, --3243, -3264, -3285, -3306, -3328, -3349, -3370, -3391, -3412, -3434, --3455, -3476, -3497, -3518, -3540, -3561, -3582, -3603, -3624, -3646, --3667, -3688, -3709, -3730, -3752, -3773, -3794, -3815, -3836, -3858, --3879, -3900, -3921, -3942, -3964, -3985, -4006, -4027, -4048, -4070, --4091, -4112, -4133, -4154, -4176, -4197, -4218, -4239, -4260, -4282, --4303, -4324, -4345, -4366, -4388, -4409, -4430, -4451, -4472, -4494, --4515, -4536, -4557, -4578, -4600, -4621, -4642, -4663, -4684, -4706, --4727, -4748, -4769, -4790, -4812, -4833, -4854, -4875, -4896, -4918, --4939, -4960, -4981, -5002, -5024, -5045, -5066, -5087, -5108, -5130, --5151, -5172, -5193, -5214, -5236, -5257, -5278, -5299, -5320, -5342, --5363, -5384, -5405 }; +static INT16 Cb_G[] = { + 0, -20, -41, -63, -84, -105, -126, -147, -169, -190, -211, -232, + -253, -275, -296, -317, -338, -359, -381, -402, -423, -444, -465, -487, + -508, -529, -550, -571, -593, -614, -635, -656, -677, -699, -720, -741, + -762, -783, -805, -826, -847, -868, -889, -911, -932, -953, -974, -995, + -1017, -1038, -1059, -1080, -1101, -1123, -1144, -1165, -1186, -1207, -1229, -1250, + -1271, -1292, -1313, -1335, -1356, -1377, -1398, -1419, -1441, -1462, -1483, -1504, + -1525, -1547, -1568, -1589, -1610, -1631, -1653, -1674, -1695, -1716, -1737, -1759, + -1780, -1801, -1822, -1843, -1865, -1886, -1907, -1928, -1949, -1971, -1992, -2013, + -2034, -2055, -2077, -2098, -2119, -2140, -2161, -2183, -2204, -2225, -2246, -2267, + -2289, -2310, -2331, -2352, -2373, -2395, -2416, -2437, -2458, -2479, -2501, -2522, + -2543, -2564, -2585, -2607, -2628, -2649, -2670, -2691, -2713, -2734, -2755, -2776, + -2797, -2819, -2840, -2861, -2882, -2903, -2925, -2946, -2967, -2988, -3009, -3031, + -3052, -3073, -3094, -3115, -3137, -3158, -3179, -3200, -3221, -3243, -3264, -3285, + -3306, -3328, -3349, -3370, -3391, -3412, -3434, -3455, -3476, -3497, -3518, -3540, + -3561, -3582, -3603, -3624, -3646, -3667, -3688, -3709, -3730, -3752, -3773, -3794, + -3815, -3836, -3858, -3879, -3900, -3921, -3942, -3964, -3985, -4006, -4027, -4048, + -4070, -4091, -4112, -4133, -4154, -4176, -4197, -4218, -4239, -4260, -4282, -4303, + -4324, -4345, -4366, -4388, -4409, -4430, -4451, -4472, -4494, -4515, -4536, -4557, + -4578, -4600, -4621, -4642, -4663, -4684, -4706, -4727, -4748, -4769, -4790, -4812, + -4833, -4854, -4875, -4896, -4918, -4939, -4960, -4981, -5002, -5024, -5045, -5066, + -5087, -5108, -5130, -5151, -5172, -5193, -5214, -5236, -5257, -5278, -5299, -5320, + -5342, -5363, -5384, -5405}; -static INT16 Cb_B[] = { 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, -320, 352, 384, 416, 448, 480, 512, 544, 576, 608, 640, 672, 704, 736, -768, 800, 832, 864, 896, 928, 960, 992, 1024, 1056, 1088, 1120, 1152, -1184, 1216, 1248, 1280, 1312, 1344, 1376, 1408, 1440, 1472, 1504, -1536, 1568, 1600, 1632, 1664, 1696, 1728, 1760, 1792, 1824, 1856, -1888, 1920, 1952, 1984, 2016, 2048, 2080, 2112, 2144, 2176, 2208, -2240, 2272, 2304, 2336, 2368, 2400, 2432, 2464, 2496, 2528, 2560, -2592, 2624, 2656, 2688, 2720, 2752, 2784, 2816, 2848, 2880, 2912, -2944, 2976, 3008, 3040, 3072, 3104, 3136, 3168, 3200, 3232, 3264, -3296, 3328, 3360, 3392, 3424, 3456, 3488, 3520, 3552, 3584, 3616, -3648, 3680, 3712, 3744, 3776, 3808, 3840, 3872, 3904, 3936, 3968, -4000, 4032, 4064, 4096, 4128, 4160, 4192, 4224, 4256, 4288, 4320, -4352, 4384, 4416, 4448, 4480, 4512, 4544, 4576, 4608, 4640, 4672, -4704, 4736, 4768, 4800, 4832, 4864, 4896, 4928, 4960, 4992, 5024, -5056, 5088, 5120, 5152, 5184, 5216, 5248, 5280, 5312, 5344, 5376, -5408, 5440, 5472, 5504, 5536, 5568, 5600, 5632, 5664, 5696, 5728, -5760, 5792, 5824, 5856, 5888, 5920, 5952, 5984, 6016, 6048, 6080, -6112, 6144, 6176, 6208, 6240, 6272, 6304, 6336, 6368, 6400, 6432, -6464, 6496, 6528, 6560, 6592, 6624, 6656, 6688, 6720, 6752, 6784, -6816, 6848, 6880, 6912, 6944, 6976, 7008, 7040, 7072, 7104, 7136, -7168, 7200, 7232, 7264, 7296, 7328, 7360, 7392, 7424, 7456, 7488, -7520, 7552, 7584, 7616, 7648, 7680, 7712, 7744, 7776, 7808, 7840, -7872, 7904, 7936, 7968, 8000, 8032, 8064, 8096, 8128, 8160 }; +static INT16 Cb_B[] = { + 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, + 448, 480, 512, 544, 576, 608, 640, 672, 704, 736, 768, 800, 832, 864, + 896, 928, 960, 992, 1024, 1056, 1088, 1120, 1152, 1184, 1216, 1248, 1280, 1312, + 1344, 1376, 1408, 1440, 1472, 1504, 1536, 1568, 1600, 1632, 1664, 1696, 1728, 1760, + 1792, 1824, 1856, 1888, 1920, 1952, 1984, 2016, 2048, 2080, 2112, 2144, 2176, 2208, + 2240, 2272, 2304, 2336, 2368, 2400, 2432, 2464, 2496, 2528, 2560, 2592, 2624, 2656, + 2688, 2720, 2752, 2784, 2816, 2848, 2880, 2912, 2944, 2976, 3008, 3040, 3072, 3104, + 3136, 3168, 3200, 3232, 3264, 3296, 3328, 3360, 3392, 3424, 3456, 3488, 3520, 3552, + 3584, 3616, 3648, 3680, 3712, 3744, 3776, 3808, 3840, 3872, 3904, 3936, 3968, 4000, + 4032, 4064, 4096, 4128, 4160, 4192, 4224, 4256, 4288, 4320, 4352, 4384, 4416, 4448, + 4480, 4512, 4544, 4576, 4608, 4640, 4672, 4704, 4736, 4768, 4800, 4832, 4864, 4896, + 4928, 4960, 4992, 5024, 5056, 5088, 5120, 5152, 5184, 5216, 5248, 5280, 5312, 5344, + 5376, 5408, 5440, 5472, 5504, 5536, 5568, 5600, 5632, 5664, 5696, 5728, 5760, 5792, + 5824, 5856, 5888, 5920, 5952, 5984, 6016, 6048, 6080, 6112, 6144, 6176, 6208, 6240, + 6272, 6304, 6336, 6368, 6400, 6432, 6464, 6496, 6528, 6560, 6592, 6624, 6656, 6688, + 6720, 6752, 6784, 6816, 6848, 6880, 6912, 6944, 6976, 7008, 7040, 7072, 7104, 7136, + 7168, 7200, 7232, 7264, 7296, 7328, 7360, 7392, 7424, 7456, 7488, 7520, 7552, 7584, + 7616, 7648, 7680, 7712, 7744, 7776, 7808, 7840, 7872, 7904, 7936, 7968, 8000, 8032, + 8064, 8096, 8128, 8160}; #define Cr_R Cb_B -static INT16 Cr_G[] = { 0, -26, -53, -79, -106, -133, -160, -187, --213, -240, -267, -294, -321, -347, -374, -401, -428, -455, -481, --508, -535, -562, -589, -615, -642, -669, -696, -722, -749, -776, --803, -830, -856, -883, -910, -937, -964, -990, -1017, -1044, -1071, --1098, -1124, -1151, -1178, -1205, -1232, -1258, -1285, -1312, -1339, --1366, -1392, -1419, -1446, -1473, -1500, -1526, -1553, -1580, -1607, --1634, -1660, -1687, -1714, -1741, -1768, -1794, -1821, -1848, -1875, --1902, -1928, -1955, -1982, -2009, -2036, -2062, -2089, -2116, -2143, --2169, -2196, -2223, -2250, -2277, -2303, -2330, -2357, -2384, -2411, --2437, -2464, -2491, -2518, -2545, -2571, -2598, -2625, -2652, -2679, --2705, -2732, -2759, -2786, -2813, -2839, -2866, -2893, -2920, -2947, --2973, -3000, -3027, -3054, -3081, -3107, -3134, -3161, -3188, -3215, --3241, -3268, -3295, -3322, -3349, -3375, -3402, -3429, -3456, -3483, --3509, -3536, -3563, -3590, -3616, -3643, -3670, -3697, -3724, -3750, --3777, -3804, -3831, -3858, -3884, -3911, -3938, -3965, -3992, -4018, --4045, -4072, -4099, -4126, -4152, -4179, -4206, -4233, -4260, -4286, --4313, -4340, -4367, -4394, -4420, -4447, -4474, -4501, -4528, -4554, --4581, -4608, -4635, -4662, -4688, -4715, -4742, -4769, -4796, -4822, --4849, -4876, -4903, -4929, -4956, -4983, -5010, -5037, -5063, -5090, --5117, -5144, -5171, -5197, -5224, -5251, -5278, -5305, -5331, -5358, --5385, -5412, -5439, -5465, -5492, -5519, -5546, -5573, -5599, -5626, --5653, -5680, -5707, -5733, -5760, -5787, -5814, -5841, -5867, -5894, --5921, -5948, -5975, -6001, -6028, -6055, -6082, -6109, -6135, -6162, --6189, -6216, -6243, -6269, -6296, -6323, -6350, -6376, -6403, -6430, --6457, -6484, -6510, -6537, -6564, -6591, -6618, -6644, -6671, -6698, --6725, -6752, -6778, -6805, -6832 }; +static INT16 Cr_G[] = { + 0, -26, -53, -79, -106, -133, -160, -187, -213, -240, -267, -294, + -321, -347, -374, -401, -428, -455, -481, -508, -535, -562, -589, -615, + -642, -669, -696, -722, -749, -776, -803, -830, -856, -883, -910, -937, + -964, -990, -1017, -1044, -1071, -1098, -1124, -1151, -1178, -1205, -1232, -1258, + -1285, -1312, -1339, -1366, -1392, -1419, -1446, -1473, -1500, -1526, -1553, -1580, + -1607, -1634, -1660, -1687, -1714, -1741, -1768, -1794, -1821, -1848, -1875, -1902, + -1928, -1955, -1982, -2009, -2036, -2062, -2089, -2116, -2143, -2169, -2196, -2223, + -2250, -2277, -2303, -2330, -2357, -2384, -2411, -2437, -2464, -2491, -2518, -2545, + -2571, -2598, -2625, -2652, -2679, -2705, -2732, -2759, -2786, -2813, -2839, -2866, + -2893, -2920, -2947, -2973, -3000, -3027, -3054, -3081, -3107, -3134, -3161, -3188, + -3215, -3241, -3268, -3295, -3322, -3349, -3375, -3402, -3429, -3456, -3483, -3509, + -3536, -3563, -3590, -3616, -3643, -3670, -3697, -3724, -3750, -3777, -3804, -3831, + -3858, -3884, -3911, -3938, -3965, -3992, -4018, -4045, -4072, -4099, -4126, -4152, + -4179, -4206, -4233, -4260, -4286, -4313, -4340, -4367, -4394, -4420, -4447, -4474, + -4501, -4528, -4554, -4581, -4608, -4635, -4662, -4688, -4715, -4742, -4769, -4796, + -4822, -4849, -4876, -4903, -4929, -4956, -4983, -5010, -5037, -5063, -5090, -5117, + -5144, -5171, -5197, -5224, -5251, -5278, -5305, -5331, -5358, -5385, -5412, -5439, + -5465, -5492, -5519, -5546, -5573, -5599, -5626, -5653, -5680, -5707, -5733, -5760, + -5787, -5814, -5841, -5867, -5894, -5921, -5948, -5975, -6001, -6028, -6055, -6082, + -6109, -6135, -6162, -6189, -6216, -6243, -6269, -6296, -6323, -6350, -6376, -6403, + -6430, -6457, -6484, -6510, -6537, -6564, -6591, -6618, -6644, -6671, -6698, -6725, + -6752, -6778, -6805, -6832}; -static INT16 Cr_B[] = { 0, -4, -9, -15, -20, -25, -30, -35, -41, -46, --51, -56, -61, -67, -72, -77, -82, -87, -93, -98, -103, -108, -113, --119, -124, -129, -134, -140, -145, -150, -155, -160, -166, -171, --176, -181, -186, -192, -197, -202, -207, -212, -218, -223, -228, --233, -238, -244, -249, -254, -259, -264, -270, -275, -280, -285, --290, -296, -301, -306, -311, -316, -322, -327, -332, -337, -342, --348, -353, -358, -363, -368, -374, -379, -384, -389, -394, -400, --405, -410, -415, -421, -426, -431, -436, -441, -447, -452, -457, --462, -467, -473, -478, -483, -488, -493, -499, -504, -509, -514, --519, -525, -530, -535, -540, -545, -551, -556, -561, -566, -571, --577, -582, -587, -592, -597, -603, -608, -613, -618, -623, -629, --634, -639, -644, -649, -655, -660, -665, -670, -675, -681, -686, --691, -696, -702, -707, -712, -717, -722, -728, -733, -738, -743, --748, -754, -759, -764, -769, -774, -780, -785, -790, -795, -800, --806, -811, -816, -821, -826, -832, -837, -842, -847, -852, -858, --863, -868, -873, -878, -884, -889, -894, -899, -904, -910, -915, --920, -925, -930, -936, -941, -946, -951, -957, -962, -967, -972, --977, -983, -988, -993, -998, -1003, -1009, -1014, -1019, -1024, --1029, -1035, -1040, -1045, -1050, -1055, -1061, -1066, -1071, -1076, --1081, -1087, -1092, -1097, -1102, -1107, -1113, -1118, -1123, -1128, --1133, -1139, -1144, -1149, -1154, -1159, -1165, -1170, -1175, -1180, --1185, -1191, -1196, -1201, -1206, -1211, -1217, -1222, -1227, -1232, --1238, -1243, -1248, -1253, -1258, -1264, -1269, -1274, -1279, -1284, --1290, -1295, -1300, -1305, -1310, -1316, -1321, -1326 }; +static INT16 Cr_B[] = { + 0, -4, -9, -15, -20, -25, -30, -35, -41, -46, -51, -56, + -61, -67, -72, -77, -82, -87, -93, -98, -103, -108, -113, -119, + -124, -129, -134, -140, -145, -150, -155, -160, -166, -171, -176, -181, + -186, -192, -197, -202, -207, -212, -218, -223, -228, -233, -238, -244, + -249, -254, -259, -264, -270, -275, -280, -285, -290, -296, -301, -306, + -311, -316, -322, -327, -332, -337, -342, -348, -353, -358, -363, -368, + -374, -379, -384, -389, -394, -400, -405, -410, -415, -421, -426, -431, + -436, -441, -447, -452, -457, -462, -467, -473, -478, -483, -488, -493, + -499, -504, -509, -514, -519, -525, -530, -535, -540, -545, -551, -556, + -561, -566, -571, -577, -582, -587, -592, -597, -603, -608, -613, -618, + -623, -629, -634, -639, -644, -649, -655, -660, -665, -670, -675, -681, + -686, -691, -696, -702, -707, -712, -717, -722, -728, -733, -738, -743, + -748, -754, -759, -764, -769, -774, -780, -785, -790, -795, -800, -806, + -811, -816, -821, -826, -832, -837, -842, -847, -852, -858, -863, -868, + -873, -878, -884, -889, -894, -899, -904, -910, -915, -920, -925, -930, + -936, -941, -946, -951, -957, -962, -967, -972, -977, -983, -988, -993, + -998, -1003, -1009, -1014, -1019, -1024, -1029, -1035, -1040, -1045, -1050, -1055, + -1061, -1066, -1071, -1076, -1081, -1087, -1092, -1097, -1102, -1107, -1113, -1118, + -1123, -1128, -1133, -1139, -1144, -1149, -1154, -1159, -1165, -1170, -1175, -1180, + -1185, -1191, -1196, -1201, -1206, -1211, -1217, -1222, -1227, -1232, -1238, -1243, + -1248, -1253, -1258, -1264, -1269, -1274, -1279, -1284, -1290, -1295, -1300, -1305, + -1310, -1316, -1321, -1326}; -static INT16 R_Cr[] = { -11484, -11394, -11305, -11215, -11125, --11036, -10946, -10856, -10766, -10677, -10587, -10497, -10407, --10318, -10228, -10138, -10049, -9959, -9869, -9779, -9690, -9600, --9510, -9420, -9331, -9241, -9151, -9062, -8972, -8882, -8792, -8703, --8613, -8523, -8433, -8344, -8254, -8164, -8075, -7985, -7895, -7805, --7716, -7626, -7536, -7446, -7357, -7267, -7177, -7088, -6998, -6908, --6818, -6729, -6639, -6549, -6459, -6370, -6280, -6190, -6101, -6011, --5921, -5831, -5742, -5652, -5562, -5472, -5383, -5293, -5203, -5113, --5024, -4934, -4844, -4755, -4665, -4575, -4485, -4396, -4306, -4216, --4126, -4037, -3947, -3857, -3768, -3678, -3588, -3498, -3409, -3319, --3229, -3139, -3050, -2960, -2870, -2781, -2691, -2601, -2511, -2422, --2332, -2242, -2152, -2063, -1973, -1883, -1794, -1704, -1614, -1524, --1435, -1345, -1255, -1165, -1076, -986, -896, -807, -717, -627, -537, --448, -358, -268, -178, -89, 0, 90, 179, 269, 359, 449, 538, 628, 718, -808, 897, 987, 1077, 1166, 1256, 1346, 1436, 1525, 1615, 1705, 1795, -1884, 1974, 2064, 2153, 2243, 2333, 2423, 2512, 2602, 2692, 2782, -2871, 2961, 3051, 3140, 3230, 3320, 3410, 3499, 3589, 3679, 3769, -3858, 3948, 4038, 4127, 4217, 4307, 4397, 4486, 4576, 4666, 4756, -4845, 4935, 5025, 5114, 5204, 5294, 5384, 5473, 5563, 5653, 5743, -5832, 5922, 6012, 6102, 6191, 6281, 6371, 6460, 6550, 6640, 6730, -6819, 6909, 6999, 7089, 7178, 7268, 7358, 7447, 7537, 7627, 7717, -7806, 7896, 7986, 8076, 8165, 8255, 8345, 8434, 8524, 8614, 8704, -8793, 8883, 8973, 9063, 9152, 9242, 9332, 9421, 9511, 9601, 9691, -9780, 9870, 9960, 10050, 10139, 10229, 10319, 10408, 10498, 10588, -10678, 10767, 10857, 10947, 11037, 11126, 11216, 11306, 11395 }; +static INT16 R_Cr[] = { + -11484, -11394, -11305, -11215, -11125, -11036, -10946, -10856, -10766, -10677, + -10587, -10497, -10407, -10318, -10228, -10138, -10049, -9959, -9869, -9779, + -9690, -9600, -9510, -9420, -9331, -9241, -9151, -9062, -8972, -8882, + -8792, -8703, -8613, -8523, -8433, -8344, -8254, -8164, -8075, -7985, + -7895, -7805, -7716, -7626, -7536, -7446, -7357, -7267, -7177, -7088, + -6998, -6908, -6818, -6729, -6639, -6549, -6459, -6370, -6280, -6190, + -6101, -6011, -5921, -5831, -5742, -5652, -5562, -5472, -5383, -5293, + -5203, -5113, -5024, -4934, -4844, -4755, -4665, -4575, -4485, -4396, + -4306, -4216, -4126, -4037, -3947, -3857, -3768, -3678, -3588, -3498, + -3409, -3319, -3229, -3139, -3050, -2960, -2870, -2781, -2691, -2601, + -2511, -2422, -2332, -2242, -2152, -2063, -1973, -1883, -1794, -1704, + -1614, -1524, -1435, -1345, -1255, -1165, -1076, -986, -896, -807, + -717, -627, -537, -448, -358, -268, -178, -89, 0, 90, + 179, 269, 359, 449, 538, 628, 718, 808, 897, 987, + 1077, 1166, 1256, 1346, 1436, 1525, 1615, 1705, 1795, 1884, + 1974, 2064, 2153, 2243, 2333, 2423, 2512, 2602, 2692, 2782, + 2871, 2961, 3051, 3140, 3230, 3320, 3410, 3499, 3589, 3679, + 3769, 3858, 3948, 4038, 4127, 4217, 4307, 4397, 4486, 4576, + 4666, 4756, 4845, 4935, 5025, 5114, 5204, 5294, 5384, 5473, + 5563, 5653, 5743, 5832, 5922, 6012, 6102, 6191, 6281, 6371, + 6460, 6550, 6640, 6730, 6819, 6909, 6999, 7089, 7178, 7268, + 7358, 7447, 7537, 7627, 7717, 7806, 7896, 7986, 8076, 8165, + 8255, 8345, 8434, 8524, 8614, 8704, 8793, 8883, 8973, 9063, + 9152, 9242, 9332, 9421, 9511, 9601, 9691, 9780, 9870, 9960, + 10050, 10139, 10229, 10319, 10408, 10498, 10588, 10678, 10767, 10857, + 10947, 11037, 11126, 11216, 11306, 11395}; -static INT16 G_Cb[] = { 2819, 2797, 2775, 2753, 2731, 2709, 2687, -2665, 2643, 2621, 2599, 2577, 2555, 2533, 2511, 2489, 2467, 2445, -2423, 2401, 2379, 2357, 2335, 2313, 2291, 2269, 2247, 2225, 2202, -2180, 2158, 2136, 2114, 2092, 2070, 2048, 2026, 2004, 1982, 1960, -1938, 1916, 1894, 1872, 1850, 1828, 1806, 1784, 1762, 1740, 1718, -1696, 1674, 1652, 1630, 1608, 1586, 1564, 1542, 1520, 1498, 1476, -1454, 1432, 1410, 1388, 1366, 1344, 1321, 1299, 1277, 1255, 1233, -1211, 1189, 1167, 1145, 1123, 1101, 1079, 1057, 1035, 1013, 991, 969, -947, 925, 903, 881, 859, 837, 815, 793, 771, 749, 727, 705, 683, 661, -639, 617, 595, 573, 551, 529, 507, 485, 463, 440, 418, 396, 374, 352, -330, 308, 286, 264, 242, 220, 198, 176, 154, 132, 110, 88, 66, 44, 22, -0, -21, -43, -65, -87, -109, -131, -153, -175, -197, -219, -241, -263, --285, -307, -329, -351, -373, -395, -417, -439, -462, -484, -506, --528, -550, -572, -594, -616, -638, -660, -682, -704, -726, -748, --770, -792, -814, -836, -858, -880, -902, -924, -946, -968, -990, --1012, -1034, -1056, -1078, -1100, -1122, -1144, -1166, -1188, -1210, --1232, -1254, -1276, -1298, -1320, -1343, -1365, -1387, -1409, -1431, --1453, -1475, -1497, -1519, -1541, -1563, -1585, -1607, -1629, -1651, --1673, -1695, -1717, -1739, -1761, -1783, -1805, -1827, -1849, -1871, --1893, -1915, -1937, -1959, -1981, -2003, -2025, -2047, -2069, -2091, --2113, -2135, -2157, -2179, -2201, -2224, -2246, -2268, -2290, -2312, --2334, -2356, -2378, -2400, -2422, -2444, -2466, -2488, -2510, -2532, --2554, -2576, -2598, -2620, -2642, -2664, -2686, -2708, -2730, -2752, --2774, -2796 }; +static INT16 G_Cb[] = { + 2819, 2797, 2775, 2753, 2731, 2709, 2687, 2665, 2643, 2621, 2599, 2577, + 2555, 2533, 2511, 2489, 2467, 2445, 2423, 2401, 2379, 2357, 2335, 2313, + 2291, 2269, 2247, 2225, 2202, 2180, 2158, 2136, 2114, 2092, 2070, 2048, + 2026, 2004, 1982, 1960, 1938, 1916, 1894, 1872, 1850, 1828, 1806, 1784, + 1762, 1740, 1718, 1696, 1674, 1652, 1630, 1608, 1586, 1564, 1542, 1520, + 1498, 1476, 1454, 1432, 1410, 1388, 1366, 1344, 1321, 1299, 1277, 1255, + 1233, 1211, 1189, 1167, 1145, 1123, 1101, 1079, 1057, 1035, 1013, 991, + 969, 947, 925, 903, 881, 859, 837, 815, 793, 771, 749, 727, + 705, 683, 661, 639, 617, 595, 573, 551, 529, 507, 485, 463, + 440, 418, 396, 374, 352, 330, 308, 286, 264, 242, 220, 198, + 176, 154, 132, 110, 88, 66, 44, 22, 0, -21, -43, -65, + -87, -109, -131, -153, -175, -197, -219, -241, -263, -285, -307, -329, + -351, -373, -395, -417, -439, -462, -484, -506, -528, -550, -572, -594, + -616, -638, -660, -682, -704, -726, -748, -770, -792, -814, -836, -858, + -880, -902, -924, -946, -968, -990, -1012, -1034, -1056, -1078, -1100, -1122, + -1144, -1166, -1188, -1210, -1232, -1254, -1276, -1298, -1320, -1343, -1365, -1387, + -1409, -1431, -1453, -1475, -1497, -1519, -1541, -1563, -1585, -1607, -1629, -1651, + -1673, -1695, -1717, -1739, -1761, -1783, -1805, -1827, -1849, -1871, -1893, -1915, + -1937, -1959, -1981, -2003, -2025, -2047, -2069, -2091, -2113, -2135, -2157, -2179, + -2201, -2224, -2246, -2268, -2290, -2312, -2334, -2356, -2378, -2400, -2422, -2444, + -2466, -2488, -2510, -2532, -2554, -2576, -2598, -2620, -2642, -2664, -2686, -2708, + -2730, -2752, -2774, -2796}; -static INT16 G_Cr[] = { 5850, 5805, 5759, 5713, 5667, 5622, 5576, -5530, 5485, 5439, 5393, 5347, 5302, 5256, 5210, 5165, 5119, 5073, -5028, 4982, 4936, 4890, 4845, 4799, 4753, 4708, 4662, 4616, 4570, -4525, 4479, 4433, 4388, 4342, 4296, 4251, 4205, 4159, 4113, 4068, -4022, 3976, 3931, 3885, 3839, 3794, 3748, 3702, 3656, 3611, 3565, -3519, 3474, 3428, 3382, 3336, 3291, 3245, 3199, 3154, 3108, 3062, -3017, 2971, 2925, 2879, 2834, 2788, 2742, 2697, 2651, 2605, 2559, -2514, 2468, 2422, 2377, 2331, 2285, 2240, 2194, 2148, 2102, 2057, -2011, 1965, 1920, 1874, 1828, 1782, 1737, 1691, 1645, 1600, 1554, -1508, 1463, 1417, 1371, 1325, 1280, 1234, 1188, 1143, 1097, 1051, -1006, 960, 914, 868, 823, 777, 731, 686, 640, 594, 548, 503, 457, 411, -366, 320, 274, 229, 183, 137, 91, 46, 0, -45, -90, -136, -182, -228, --273, -319, -365, -410, -456, -502, -547, -593, -639, -685, -730, --776, -822, -867, -913, -959, -1005, -1050, -1096, -1142, -1187, --1233, -1279, -1324, -1370, -1416, -1462, -1507, -1553, -1599, -1644, --1690, -1736, -1781, -1827, -1873, -1919, -1964, -2010, -2056, -2101, --2147, -2193, -2239, -2284, -2330, -2376, -2421, -2467, -2513, -2558, --2604, -2650, -2696, -2741, -2787, -2833, -2878, -2924, -2970, -3016, --3061, -3107, -3153, -3198, -3244, -3290, -3335, -3381, -3427, -3473, --3518, -3564, -3610, -3655, -3701, -3747, -3793, -3838, -3884, -3930, --3975, -4021, -4067, -4112, -4158, -4204, -4250, -4295, -4341, -4387, --4432, -4478, -4524, -4569, -4615, -4661, -4707, -4752, -4798, -4844, --4889, -4935, -4981, -5027, -5072, -5118, -5164, -5209, -5255, -5301, --5346, -5392, -5438, -5484, -5529, -5575, -5621, -5666, -5712, -5758, --5804 }; - -static INT16 B_Cb[] = { -14515, -14402, -14288, -14175, -14062, --13948, -13835, -13721, -13608, -13495, -13381, -13268, -13154, --13041, -12928, -12814, -12701, -12587, -12474, -12360, -12247, --12134, -12020, -11907, -11793, -11680, -11567, -11453, -11340, --11226, -11113, -11000, -10886, -10773, -10659, -10546, -10433, --10319, -10206, -10092, -9979, -9865, -9752, -9639, -9525, -9412, --9298, -9185, -9072, -8958, -8845, -8731, -8618, -8505, -8391, -8278, --8164, -8051, -7938, -7824, -7711, -7597, -7484, -7371, -7257, -7144, --7030, -6917, -6803, -6690, -6577, -6463, -6350, -6236, -6123, -6010, --5896, -5783, -5669, -5556, -5443, -5329, -5216, -5102, -4989, -4876, --4762, -4649, -4535, -4422, -4309, -4195, -4082, -3968, -3855, -3741, --3628, -3515, -3401, -3288, -3174, -3061, -2948, -2834, -2721, -2607, --2494, -2381, -2267, -2154, -2040, -1927, -1814, -1700, -1587, -1473, --1360, -1246, -1133, -1020, -906, -793, -679, -566, -453, -339, -226, --112, 0, 113, 227, 340, 454, 567, 680, 794, 907, 1021, 1134, 1247, -1361, 1474, 1588, 1701, 1815, 1928, 2041, 2155, 2268, 2382, 2495, -2608, 2722, 2835, 2949, 3062, 3175, 3289, 3402, 3516, 3629, 3742, -3856, 3969, 4083, 4196, 4310, 4423, 4536, 4650, 4763, 4877, 4990, -5103, 5217, 5330, 5444, 5557, 5670, 5784, 5897, 6011, 6124, 6237, -6351, 6464, 6578, 6691, 6804, 6918, 7031, 7145, 7258, 7372, 7485, -7598, 7712, 7825, 7939, 8052, 8165, 8279, 8392, 8506, 8619, 8732, -8846, 8959, 9073, 9186, 9299, 9413, 9526, 9640, 9753, 9866, 9980, -10093, 10207, 10320, 10434, 10547, 10660, 10774, 10887, 11001, 11114, -11227, 11341, 11454, 11568, 11681, 11794, 11908, 12021, 12135, 12248, -12361, 12475, 12588, 12702, 12815, 12929, 13042, 13155, 13269, 13382, -13496, 13609, 13722, 13836, 13949, 14063, 14176, 14289, 14403 }; +static INT16 G_Cr[] = { + 5850, 5805, 5759, 5713, 5667, 5622, 5576, 5530, 5485, 5439, 5393, 5347, + 5302, 5256, 5210, 5165, 5119, 5073, 5028, 4982, 4936, 4890, 4845, 4799, + 4753, 4708, 4662, 4616, 4570, 4525, 4479, 4433, 4388, 4342, 4296, 4251, + 4205, 4159, 4113, 4068, 4022, 3976, 3931, 3885, 3839, 3794, 3748, 3702, + 3656, 3611, 3565, 3519, 3474, 3428, 3382, 3336, 3291, 3245, 3199, 3154, + 3108, 3062, 3017, 2971, 2925, 2879, 2834, 2788, 2742, 2697, 2651, 2605, + 2559, 2514, 2468, 2422, 2377, 2331, 2285, 2240, 2194, 2148, 2102, 2057, + 2011, 1965, 1920, 1874, 1828, 1782, 1737, 1691, 1645, 1600, 1554, 1508, + 1463, 1417, 1371, 1325, 1280, 1234, 1188, 1143, 1097, 1051, 1006, 960, + 914, 868, 823, 777, 731, 686, 640, 594, 548, 503, 457, 411, + 366, 320, 274, 229, 183, 137, 91, 46, 0, -45, -90, -136, + -182, -228, -273, -319, -365, -410, -456, -502, -547, -593, -639, -685, + -730, -776, -822, -867, -913, -959, -1005, -1050, -1096, -1142, -1187, -1233, + -1279, -1324, -1370, -1416, -1462, -1507, -1553, -1599, -1644, -1690, -1736, -1781, + -1827, -1873, -1919, -1964, -2010, -2056, -2101, -2147, -2193, -2239, -2284, -2330, + -2376, -2421, -2467, -2513, -2558, -2604, -2650, -2696, -2741, -2787, -2833, -2878, + -2924, -2970, -3016, -3061, -3107, -3153, -3198, -3244, -3290, -3335, -3381, -3427, + -3473, -3518, -3564, -3610, -3655, -3701, -3747, -3793, -3838, -3884, -3930, -3975, + -4021, -4067, -4112, -4158, -4204, -4250, -4295, -4341, -4387, -4432, -4478, -4524, + -4569, -4615, -4661, -4707, -4752, -4798, -4844, -4889, -4935, -4981, -5027, -5072, + -5118, -5164, -5209, -5255, -5301, -5346, -5392, -5438, -5484, -5529, -5575, -5621, + -5666, -5712, -5758, -5804}; +static INT16 B_Cb[] = { + -14515, -14402, -14288, -14175, -14062, -13948, -13835, -13721, -13608, -13495, + -13381, -13268, -13154, -13041, -12928, -12814, -12701, -12587, -12474, -12360, + -12247, -12134, -12020, -11907, -11793, -11680, -11567, -11453, -11340, -11226, + -11113, -11000, -10886, -10773, -10659, -10546, -10433, -10319, -10206, -10092, + -9979, -9865, -9752, -9639, -9525, -9412, -9298, -9185, -9072, -8958, + -8845, -8731, -8618, -8505, -8391, -8278, -8164, -8051, -7938, -7824, + -7711, -7597, -7484, -7371, -7257, -7144, -7030, -6917, -6803, -6690, + -6577, -6463, -6350, -6236, -6123, -6010, -5896, -5783, -5669, -5556, + -5443, -5329, -5216, -5102, -4989, -4876, -4762, -4649, -4535, -4422, + -4309, -4195, -4082, -3968, -3855, -3741, -3628, -3515, -3401, -3288, + -3174, -3061, -2948, -2834, -2721, -2607, -2494, -2381, -2267, -2154, + -2040, -1927, -1814, -1700, -1587, -1473, -1360, -1246, -1133, -1020, + -906, -793, -679, -566, -453, -339, -226, -112, 0, 113, + 227, 340, 454, 567, 680, 794, 907, 1021, 1134, 1247, + 1361, 1474, 1588, 1701, 1815, 1928, 2041, 2155, 2268, 2382, + 2495, 2608, 2722, 2835, 2949, 3062, 3175, 3289, 3402, 3516, + 3629, 3742, 3856, 3969, 4083, 4196, 4310, 4423, 4536, 4650, + 4763, 4877, 4990, 5103, 5217, 5330, 5444, 5557, 5670, 5784, + 5897, 6011, 6124, 6237, 6351, 6464, 6578, 6691, 6804, 6918, + 7031, 7145, 7258, 7372, 7485, 7598, 7712, 7825, 7939, 8052, + 8165, 8279, 8392, 8506, 8619, 8732, 8846, 8959, 9073, 9186, + 9299, 9413, 9526, 9640, 9753, 9866, 9980, 10093, 10207, 10320, + 10434, 10547, 10660, 10774, 10887, 11001, 11114, 11227, 11341, 11454, + 11568, 11681, 11794, 11908, 12021, 12135, 12248, 12361, 12475, 12588, + 12702, 12815, 12929, 13042, 13155, 13269, 13382, 13496, 13609, 13722, + 13836, 13949, 14063, 14176, 14289, 14403}; void -ImagingConvertRGB2YCbCr(UINT8* out, const UINT8* in, int pixels) -{ +ImagingConvertRGB2YCbCr(UINT8 *out, const UINT8 *in, int pixels) { int x; UINT8 a; int r, g, b; int y, cr, cb; - for (x = 0; x < pixels; x++, in +=4, out += 4) { - + for (x = 0; x < pixels; x++, in += 4, out += 4) { r = in[0]; g = in[1]; b = in[2]; a = in[3]; - y = (Y_R[r] + Y_G[g] + Y_B[b]) >> SCALE; + y = (Y_R[r] + Y_G[g] + Y_B[b]) >> SCALE; cb = ((Cb_R[r] + Cb_G[g] + Cb_B[b]) >> SCALE) + 128; cr = ((Cr_R[r] + Cr_G[g] + Cr_B[b]) >> SCALE) + 128; - out[0] = (UINT8) y; - out[1] = (UINT8) cb; - out[2] = (UINT8) cr; + out[0] = (UINT8)y; + out[1] = (UINT8)cb; + out[2] = (UINT8)cr; out[3] = a; } } void -ImagingConvertYCbCr2RGB(UINT8* out, const UINT8* in, int pixels) -{ +ImagingConvertYCbCr2RGB(UINT8 *out, const UINT8 *in, int pixels) { int x; UINT8 a; int r, g, b; int y, cr, cb; for (x = 0; x < pixels; x++, in += 4, out += 4) { - y = in[0]; cb = in[1]; cr = in[2]; a = in[3]; - r = y + (( R_Cr[cr]) >> SCALE); + r = y + ((R_Cr[cr]) >> SCALE); g = y + ((G_Cb[cb] + G_Cr[cr]) >> SCALE); - b = y + ((B_Cb[cb] ) >> SCALE); + b = y + ((B_Cb[cb]) >> SCALE); out[0] = (r <= 0) ? 0 : (r >= 255) ? 255 : r; out[1] = (g <= 0) ? 0 : (g >= 255) ? 255 : g; diff --git a/src/libImaging/Copy.c b/src/libImaging/Copy.c index 1bc9b1a709a..571133e14b6 100644 --- a/src/libImaging/Copy.c +++ b/src/libImaging/Copy.c @@ -15,44 +15,43 @@ * See the README file for details on usage and redistribution. */ - #include "Imaging.h" - static Imaging -_copy(Imaging imOut, Imaging imIn) -{ +_copy(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int y; - if (!imIn) - return (Imaging) ImagingError_ValueError(NULL); + if (!imIn) { + return (Imaging)ImagingError_ValueError(NULL); + } imOut = ImagingNew2Dirty(imIn->mode, imOut, imIn); - if (!imOut) + if (!imOut) { return NULL; + } ImagingCopyPalette(imOut, imIn); ImagingSectionEnter(&cookie); - if (imIn->block != NULL && imOut->block != NULL) - memcpy(imOut->block, imIn->block, imIn->ysize * imIn->linesize); - else - for (y = 0; y < imIn->ysize; y++) + if (imIn->block != NULL && imOut->block != NULL) { + memcpy(imOut->block, imIn->block, imIn->ysize * imIn->linesize); + } else { + for (y = 0; y < imIn->ysize; y++) { memcpy(imOut->image[y], imIn->image[y], imIn->linesize); + } + } ImagingSectionLeave(&cookie); return imOut; } Imaging -ImagingCopy(Imaging imIn) -{ +ImagingCopy(Imaging imIn) { return _copy(NULL, imIn); } Imaging -ImagingCopy2(Imaging imOut, Imaging imIn) -{ +ImagingCopy2(Imaging imOut, Imaging imIn) { return _copy(imOut, imIn); } diff --git a/src/libImaging/Crc32.c b/src/libImaging/Crc32.c deleted file mode 100644 index d83d0fbacb0..00000000000 --- a/src/libImaging/Crc32.c +++ /dev/null @@ -1,87 +0,0 @@ -/* - * The Python Imaging Library - * $Id$ - * - * calculate ISO 3307 checksum - * - * history: - * 96-12-10 fl: Created (based on example in the PNG spec) - * - * Copyright (c) Fredrik Lundh 1996. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - - -#include "Imaging.h" - - -/* Precalculated CRC values (created by makecrctable.py) */ - -static UINT32 crc32table[] = { 0x0, 0x77073096L, 0xEE0E612CL, -0x990951BAL, 0x76DC419L, 0x706AF48FL, 0xE963A535L, 0x9E6495A3L, -0xEDB8832L, 0x79DCB8A4L, 0xE0D5E91EL, 0x97D2D988L, 0x9B64C2BL, -0x7EB17CBDL, 0xE7B82D07L, 0x90BF1D91L, 0x1DB71064L, 0x6AB020F2L, -0xF3B97148L, 0x84BE41DEL, 0x1ADAD47DL, 0x6DDDE4EBL, 0xF4D4B551L, -0x83D385C7L, 0x136C9856L, 0x646BA8C0L, 0xFD62F97AL, 0x8A65C9ECL, -0x14015C4FL, 0x63066CD9L, 0xFA0F3D63L, 0x8D080DF5L, 0x3B6E20C8L, -0x4C69105EL, 0xD56041E4L, 0xA2677172L, 0x3C03E4D1L, 0x4B04D447L, -0xD20D85FDL, 0xA50AB56BL, 0x35B5A8FAL, 0x42B2986CL, 0xDBBBC9D6L, -0xACBCF940L, 0x32D86CE3L, 0x45DF5C75L, 0xDCD60DCFL, 0xABD13D59L, -0x26D930ACL, 0x51DE003AL, 0xC8D75180L, 0xBFD06116L, 0x21B4F4B5L, -0x56B3C423L, 0xCFBA9599L, 0xB8BDA50FL, 0x2802B89EL, 0x5F058808L, -0xC60CD9B2L, 0xB10BE924L, 0x2F6F7C87L, 0x58684C11L, 0xC1611DABL, -0xB6662D3DL, 0x76DC4190L, 0x1DB7106L, 0x98D220BCL, 0xEFD5102AL, -0x71B18589L, 0x6B6B51FL, 0x9FBFE4A5L, 0xE8B8D433L, 0x7807C9A2L, -0xF00F934L, 0x9609A88EL, 0xE10E9818L, 0x7F6A0DBBL, 0x86D3D2DL, -0x91646C97L, 0xE6635C01L, 0x6B6B51F4L, 0x1C6C6162L, 0x856530D8L, -0xF262004EL, 0x6C0695EDL, 0x1B01A57BL, 0x8208F4C1L, 0xF50FC457L, -0x65B0D9C6L, 0x12B7E950L, 0x8BBEB8EAL, 0xFCB9887CL, 0x62DD1DDFL, -0x15DA2D49L, 0x8CD37CF3L, 0xFBD44C65L, 0x4DB26158L, 0x3AB551CEL, -0xA3BC0074L, 0xD4BB30E2L, 0x4ADFA541L, 0x3DD895D7L, 0xA4D1C46DL, -0xD3D6F4FBL, 0x4369E96AL, 0x346ED9FCL, 0xAD678846L, 0xDA60B8D0L, -0x44042D73L, 0x33031DE5L, 0xAA0A4C5FL, 0xDD0D7CC9L, 0x5005713CL, -0x270241AAL, 0xBE0B1010L, 0xC90C2086L, 0x5768B525L, 0x206F85B3L, -0xB966D409L, 0xCE61E49FL, 0x5EDEF90EL, 0x29D9C998L, 0xB0D09822L, -0xC7D7A8B4L, 0x59B33D17L, 0x2EB40D81L, 0xB7BD5C3BL, 0xC0BA6CADL, -0xEDB88320L, 0x9ABFB3B6L, 0x3B6E20CL, 0x74B1D29AL, 0xEAD54739L, -0x9DD277AFL, 0x4DB2615L, 0x73DC1683L, 0xE3630B12L, 0x94643B84L, -0xD6D6A3EL, 0x7A6A5AA8L, 0xE40ECF0BL, 0x9309FF9DL, 0xA00AE27L, -0x7D079EB1L, 0xF00F9344L, 0x8708A3D2L, 0x1E01F268L, 0x6906C2FEL, -0xF762575DL, 0x806567CBL, 0x196C3671L, 0x6E6B06E7L, 0xFED41B76L, -0x89D32BE0L, 0x10DA7A5AL, 0x67DD4ACCL, 0xF9B9DF6FL, 0x8EBEEFF9L, -0x17B7BE43L, 0x60B08ED5L, 0xD6D6A3E8L, 0xA1D1937EL, 0x38D8C2C4L, -0x4FDFF252L, 0xD1BB67F1L, 0xA6BC5767L, 0x3FB506DDL, 0x48B2364BL, -0xD80D2BDAL, 0xAF0A1B4CL, 0x36034AF6L, 0x41047A60L, 0xDF60EFC3L, -0xA867DF55L, 0x316E8EEFL, 0x4669BE79L, 0xCB61B38CL, 0xBC66831AL, -0x256FD2A0L, 0x5268E236L, 0xCC0C7795L, 0xBB0B4703L, 0x220216B9L, -0x5505262FL, 0xC5BA3BBEL, 0xB2BD0B28L, 0x2BB45A92L, 0x5CB36A04L, -0xC2D7FFA7L, 0xB5D0CF31L, 0x2CD99E8BL, 0x5BDEAE1DL, 0x9B64C2B0L, -0xEC63F226L, 0x756AA39CL, 0x26D930AL, 0x9C0906A9L, 0xEB0E363FL, -0x72076785L, 0x5005713L, 0x95BF4A82L, 0xE2B87A14L, 0x7BB12BAEL, -0xCB61B38L, 0x92D28E9BL, 0xE5D5BE0DL, 0x7CDCEFB7L, 0xBDBDF21L, -0x86D3D2D4L, 0xF1D4E242L, 0x68DDB3F8L, 0x1FDA836EL, 0x81BE16CDL, -0xF6B9265BL, 0x6FB077E1L, 0x18B74777L, 0x88085AE6L, 0xFF0F6A70L, -0x66063BCAL, 0x11010B5CL, 0x8F659EFFL, 0xF862AE69L, 0x616BFFD3L, -0x166CCF45L, 0xA00AE278L, 0xD70DD2EEL, 0x4E048354L, 0x3903B3C2L, -0xA7672661L, 0xD06016F7L, 0x4969474DL, 0x3E6E77DBL, 0xAED16A4AL, -0xD9D65ADCL, 0x40DF0B66L, 0x37D83BF0L, 0xA9BCAE53L, 0xDEBB9EC5L, -0x47B2CF7FL, 0x30B5FFE9L, 0xBDBDF21CL, 0xCABAC28AL, 0x53B39330L, -0x24B4A3A6L, 0xBAD03605L, 0xCDD70693L, 0x54DE5729L, 0x23D967BFL, -0xB3667A2EL, 0xC4614AB8L, 0x5D681B02L, 0x2A6F2B94L, 0xB40BBE37L, -0xC30C8EA1L, 0x5A05DF1BL, 0x2D02EF8DL }; - - -UINT32 -ImagingCRC32(UINT32 crc, UINT8* buffer, int bytes) -{ - int i; - - crc ^= 0xFFFFFFFFL; - - for (i = 0; i < bytes; i++) - crc = crc32table[(UINT8) crc ^ buffer[i]] ^ (crc >> 8); - - return crc ^ 0xFFFFFFFFL; -} diff --git a/src/libImaging/Crop.c b/src/libImaging/Crop.c index 4407c1b1d2d..2425b4cd589 100644 --- a/src/libImaging/Crop.c +++ b/src/libImaging/Crop.c @@ -5,9 +5,9 @@ * cut region from image * * history: - * 95-11-27 fl Created - * 98-07-10 fl Fixed "null result" error - * 99-02-05 fl Rewritten to use Paste primitive + * 95-11-27 fl Created + * 98-07-10 fl Fixed "null result" error + * 99-02-05 fl Rewritten to use Paste primitive * * Copyright (c) Secret Labs AB 1997-99. * Copyright (c) Fredrik Lundh 1995. @@ -15,36 +15,38 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" - Imaging -ImagingCrop(Imaging imIn, int sx0, int sy0, int sx1, int sy1) -{ +ImagingCrop(Imaging imIn, int sx0, int sy0, int sx1, int sy1) { Imaging imOut; int xsize, ysize; int dx0, dy0, dx1, dy1; INT32 zero = 0; - if (!imIn) - return (Imaging) ImagingError_ModeError(); + if (!imIn) { + return (Imaging)ImagingError_ModeError(); + } xsize = sx1 - sx0; - if (xsize < 0) + if (xsize < 0) { xsize = 0; + } ysize = sy1 - sy0; - if (ysize < 0) + if (ysize < 0) { ysize = 0; + } imOut = ImagingNewDirty(imIn->mode, xsize, ysize); - if (!imOut) - return NULL; + if (!imOut) { + return NULL; + } ImagingCopyPalette(imOut, imIn); - if (sx0 < 0 || sy0 < 0 || sx1 > imIn->xsize || sy1 > imIn->ysize) - (void) ImagingFill(imOut, &zero); + if (sx0 < 0 || sy0 < 0 || sx1 > imIn->xsize || sy1 > imIn->ysize) { + (void)ImagingFill(imOut, &zero); + } dx0 = -sx0; dy0 = -sy0; diff --git a/src/libImaging/Dib.c b/src/libImaging/Dib.c index 5042902316d..f8a2901b8c7 100644 --- a/src/libImaging/Dib.c +++ b/src/libImaging/Dib.c @@ -19,29 +19,27 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" #ifdef _WIN32 #include "ImDib.h" - -char* -ImagingGetModeDIB(int size_out[2]) -{ +char * +ImagingGetModeDIB(int size_out[2]) { /* Get device characteristics */ HDC dc; - char* mode; + char *mode; dc = CreateCompatibleDC(NULL); mode = "P"; if (!(GetDeviceCaps(dc, RASTERCAPS) & RC_PALETTE)) { mode = "RGB"; - if (GetDeviceCaps(dc, BITSPIXEL) == 1) + if (GetDeviceCaps(dc, BITSPIXEL) == 1) { mode = "1"; + } } if (size_out) { @@ -54,10 +52,8 @@ ImagingGetModeDIB(int size_out[2]) return mode; } - ImagingDIB -ImagingNewDIB(const char *mode, int xsize, int ysize) -{ +ImagingNewDIB(const char *mode, int xsize, int ysize) { /* Create a Windows bitmap */ ImagingDIB dib; @@ -65,21 +61,21 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) int i; /* Check mode */ - if (strcmp(mode, "1") != 0 && strcmp(mode, "L") != 0 && - strcmp(mode, "RGB") != 0) - return (ImagingDIB) ImagingError_ModeError(); + if (strcmp(mode, "1") != 0 && strcmp(mode, "L") != 0 && strcmp(mode, "RGB") != 0) { + return (ImagingDIB)ImagingError_ModeError(); + } /* Create DIB context and info header */ /* malloc check ok, small constant allocation */ - dib = (ImagingDIB) malloc(sizeof(*dib)); - if (!dib) - return (ImagingDIB) ImagingError_MemoryError(); + dib = (ImagingDIB)malloc(sizeof(*dib)); + if (!dib) { + return (ImagingDIB)ImagingError_MemoryError(); + } /* malloc check ok, small constant allocation */ - dib->info = (BITMAPINFO*) malloc(sizeof(BITMAPINFOHEADER) + - 256 * sizeof(RGBQUAD)); + dib->info = (BITMAPINFO *)malloc(sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD)); if (!dib->info) { free(dib); - return (ImagingDIB) ImagingError_MemoryError(); + return (ImagingDIB)ImagingError_MemoryError(); } memset(dib->info, 0, sizeof(BITMAPINFOHEADER)); @@ -87,7 +83,7 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) dib->info->bmiHeader.biWidth = xsize; dib->info->bmiHeader.biHeight = ysize; dib->info->bmiHeader.biPlanes = 1; - dib->info->bmiHeader.biBitCount = strlen(mode)*8; + dib->info->bmiHeader.biBitCount = strlen(mode) * 8; dib->info->bmiHeader.biCompression = BI_RGB; /* Create DIB */ @@ -95,15 +91,15 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) if (!dib->dc) { free(dib->info); free(dib); - return (ImagingDIB) ImagingError_MemoryError(); + return (ImagingDIB)ImagingError_MemoryError(); } - dib->bitmap = CreateDIBSection(dib->dc, dib->info, DIB_RGB_COLORS, - &dib->bits, NULL, 0); + dib->bitmap = + CreateDIBSection(dib->dc, dib->info, DIB_RGB_COLORS, &dib->bits, NULL, 0); if (!dib->bitmap) { free(dib->info); free(dib); - return (ImagingDIB) ImagingError_MemoryError(); + return (ImagingDIB)ImagingError_MemoryError(); } strcpy(dib->mode, mode); @@ -113,9 +109,9 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) dib->pixelsize = strlen(mode); dib->linesize = (xsize * dib->pixelsize + 3) & -4; - if (dib->pixelsize == 1) - dib->pack = dib->unpack = (ImagingShuffler) memcpy; - else { + if (dib->pixelsize == 1) { + dib->pack = dib->unpack = (ImagingShuffler)memcpy; + } else { dib->pack = ImagingPackBGR; dib->unpack = ImagingPackBGR; } @@ -128,9 +124,7 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) /* Bind a palette to it as well (only required for 8-bit DIBs) */ if (dib->pixelsize == 1) { for (i = 0; i < 256; i++) { - palette[i].rgbRed = - palette[i].rgbGreen = - palette[i].rgbBlue = i; + palette[i].rgbRed = palette[i].rgbGreen = palette[i].rgbBlue = i; palette[i].rgbReserved = 0; } SetDIBColorTable(dib->dc, 0, 256, palette); @@ -138,9 +132,8 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) /* Create an associated palette (for 8-bit displays only) */ if (strcmp(ImagingGetModeDIB(NULL), "P") == 0) { - - char palbuf[sizeof(LOGPALETTE)+256*sizeof(PALETTEENTRY)]; - LPLOGPALETTE pal = (LPLOGPALETTE) palbuf; + char palbuf[sizeof(LOGPALETTE) + 256 * sizeof(PALETTEENTRY)]; + LPLOGPALETTE pal = (LPLOGPALETTE)palbuf; int i, r, g, b; /* Load system palette */ @@ -149,7 +142,6 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) GetSystemPaletteEntries(dib->dc, 0, 256, pal->palPalEntry); if (strcmp(mode, "L") == 0) { - /* Greyscale DIB. Fill all 236 slots with a greyscale ramp * (this is usually overkill on Windows since VGA only offers * 6 bits greyscale resolution). Ignore the slots already @@ -157,16 +149,14 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) i = 10; for (r = 0; r < 236; r++) { - pal->palPalEntry[i].peRed = - pal->palPalEntry[i].peGreen = - pal->palPalEntry[i].peBlue = i; + pal->palPalEntry[i].peRed = pal->palPalEntry[i].peGreen = + pal->palPalEntry[i].peBlue = i; i++; } dib->palette = CreatePalette(pal); } else if (strcmp(mode, "RGB") == 0) { - #ifdef CUBE216 /* Colour DIB. Create a 6x6x6 colour cube (216 entries) and @@ -174,19 +164,20 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) * images. */ i = 10; - for (r = 0; r < 256; r += 51) - for (g = 0; g < 256; g += 51) + for (r = 0; r < 256; r += 51) { + for (g = 0; g < 256; g += 51) { for (b = 0; b < 256; b += 51) { pal->palPalEntry[i].peRed = r; pal->palPalEntry[i].peGreen = g; pal->palPalEntry[i].peBlue = b; i++; } - for (r = 1; r < 22-1; r++) { + } + } + for (r = 1; r < 22 - 1; r++) { /* Black and white are already provided by the cube. */ - pal->palPalEntry[i].peRed = - pal->palPalEntry[i].peGreen = - pal->palPalEntry[i].peBlue = r * 255 / (22-1); + pal->palPalEntry[i].peRed = pal->palPalEntry[i].peGreen = + pal->palPalEntry[i].peBlue = r * 255 / (22 - 1); i++; } @@ -195,105 +186,127 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) /* Colour DIB. Alternate palette. */ i = 10; - for (r = 0; r < 256; r += 37) - for (g = 0; g < 256; g += 32) + for (r = 0; r < 256; r += 37) { + for (g = 0; g < 256; g += 32) { for (b = 0; b < 256; b += 64) { pal->palPalEntry[i].peRed = r; pal->palPalEntry[i].peGreen = g; pal->palPalEntry[i].peBlue = b; i++; } + } + } #endif dib->palette = CreatePalette(pal); - } - } return dib; } void -ImagingPasteDIB(ImagingDIB dib, Imaging im, int xy[4]) -{ +ImagingPasteDIB(ImagingDIB dib, Imaging im, int xy[4]) { /* Paste image data into a bitmap */ /* FIXME: check size! */ int y; - for (y = 0; y < im->ysize; y++) - dib->pack(dib->bits + dib->linesize*(dib->ysize-(xy[1]+y)-1) + - xy[0]*dib->pixelsize, im->image[y], im->xsize); - + for (y = 0; y < im->ysize; y++) { + dib->pack( + dib->bits + dib->linesize * (dib->ysize - (xy[1] + y) - 1) + + xy[0] * dib->pixelsize, + im->image[y], + im->xsize); + } } void -ImagingExposeDIB(ImagingDIB dib, void *dc) -{ +ImagingExposeDIB(ImagingDIB dib, void *dc) { /* Copy bitmap to display */ - if (dib->palette != 0) - SelectPalette((HDC) dc, dib->palette, FALSE); - BitBlt((HDC) dc, 0, 0, dib->xsize, dib->ysize, dib->dc, 0, 0, SRCCOPY); + if (dib->palette != 0) { + SelectPalette((HDC)dc, dib->palette, FALSE); + } + BitBlt((HDC)dc, 0, 0, dib->xsize, dib->ysize, dib->dc, 0, 0, SRCCOPY); } void -ImagingDrawDIB(ImagingDIB dib, void *dc, int dst[4], int src[4]) -{ +ImagingDrawDIB(ImagingDIB dib, void *dc, int dst[4], int src[4]) { /* Copy bitmap to printer/display */ - if (GetDeviceCaps((HDC) dc, RASTERCAPS) & RC_STRETCHDIB) { + if (GetDeviceCaps((HDC)dc, RASTERCAPS) & RC_STRETCHDIB) { /* stretchdib (printers) */ - StretchDIBits((HDC) dc, dst[0], dst[1], dst[2]-dst[0], dst[3]-dst[1], - src[0], src[1], src[2]-src[0], src[3]-src[1], dib->bits, - dib->info, DIB_RGB_COLORS, SRCCOPY); + StretchDIBits( + (HDC)dc, + dst[0], + dst[1], + dst[2] - dst[0], + dst[3] - dst[1], + src[0], + src[1], + src[2] - src[0], + src[3] - src[1], + dib->bits, + dib->info, + DIB_RGB_COLORS, + SRCCOPY); } else { /* stretchblt (displays) */ - if (dib->palette != 0) - SelectPalette((HDC) dc, dib->palette, FALSE); - StretchBlt((HDC) dc, dst[0], dst[1], dst[2]-dst[0], dst[3]-dst[1], - dib->dc, src[0], src[1], src[2]-src[0], src[3]-src[1], - SRCCOPY); + if (dib->palette != 0) { + SelectPalette((HDC)dc, dib->palette, FALSE); + } + StretchBlt( + (HDC)dc, + dst[0], + dst[1], + dst[2] - dst[0], + dst[3] - dst[1], + dib->dc, + src[0], + src[1], + src[2] - src[0], + src[3] - src[1], + SRCCOPY); } } int -ImagingQueryPaletteDIB(ImagingDIB dib, void *dc) -{ +ImagingQueryPaletteDIB(ImagingDIB dib, void *dc) { /* Install bitmap palette */ int n; if (dib->palette != 0) { - /* Realize associated palette */ - HPALETTE now = SelectPalette((HDC) dc, dib->palette, FALSE); - n = RealizePalette((HDC) dc); + HPALETTE now = SelectPalette((HDC)dc, dib->palette, FALSE); + n = RealizePalette((HDC)dc); /* Restore palette */ - SelectPalette((HDC) dc, now, FALSE); + SelectPalette((HDC)dc, now, FALSE); - } else + } else { n = 0; + } return n; /* number of colours that was changed */ } void -ImagingDeleteDIB(ImagingDIB dib) -{ +ImagingDeleteDIB(ImagingDIB dib) { /* Clean up */ - if (dib->palette) + if (dib->palette) { DeleteObject(dib->palette); + } if (dib->bitmap) { SelectObject(dib->dc, dib->old_bitmap); DeleteObject(dib->bitmap); } - if (dib->dc) + if (dib->dc) { DeleteDC(dib->dc); + } free(dib->info); } diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 3fae3d931e5..77343e583d4 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -35,19 +35,19 @@ #include "Imaging.h" #include +#include -#define CEIL(v) (int) ceil(v) -#define FLOOR(v) ((v) >= 0.0 ? (int) (v) : (int) floor(v)) +#define CEIL(v) (int)ceil(v) +#define FLOOR(v) ((v) >= 0.0 ? (int)(v) : (int)floor(v)) -#define INK8(ink) (*(UINT8*)ink) -#define INK32(ink) (*(INT32*)ink) +#define INK8(ink) (*(UINT8 *)ink) /* - * Rounds around zero (up=away from zero, down=torwards zero) + * Rounds around zero (up=away from zero, down=towards zero) * This guarantees that ROUND_UP|DOWN(f) == -ROUND_UP|DOWN(-f) */ -#define ROUND_UP(f) ((int) ((f) >= 0.0 ? floor((f) + 0.5F) : -floor(fabs(f) + 0.5F))) -#define ROUND_DOWN(f) ((int) ((f) >= 0.0 ? ceil((f) - 0.5F) : -ceil(fabs(f) - 0.5F))) +#define ROUND_UP(f) ((int)((f) >= 0.0 ? floor((f) + 0.5F) : -floor(fabs(f) + 0.5F))) +#define ROUND_DOWN(f) ((int)((f) >= 0.0 ? ceil((f)-0.5F) : -ceil(fabs(f) - 0.5F))) /* -------------------------------------------------------------------- */ /* Primitives */ @@ -65,27 +65,31 @@ typedef struct { typedef void (*hline_handler)(Imaging, int, int, int, int); static inline void -point8(Imaging im, int x, int y, int ink) -{ - if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) - im->image8[y][x] = (UINT8) ink; +point8(Imaging im, int x, int y, int ink) { + if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { + if (strncmp(im->mode, "I;16", 4) == 0) { + im->image8[y][x * 2] = (UINT8)ink; + im->image8[y][x * 2 + 1] = (UINT8)ink; + } else { + im->image8[y][x] = (UINT8)ink; + } + } } static inline void -point32(Imaging im, int x, int y, int ink) -{ - if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) +point32(Imaging im, int x, int y, int ink) { + if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { im->image32[y][x] = ink; + } } static inline void -point32rgba(Imaging im, int x, int y, int ink) -{ +point32rgba(Imaging im, int x, int y, int ink) { unsigned int tmp1; if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { - UINT8* out = (UINT8*) im->image[y]+x*4; - UINT8* in = (UINT8*) &ink; + UINT8 *out = (UINT8 *)im->image[y] + x * 4; + UINT8 *in = (UINT8 *)&ink; out[0] = BLEND(in[3], out[0], in[0], tmp1); out[1] = BLEND(in[3], out[1], in[1], tmp1); out[2] = BLEND(in[3], out[2], in[2], tmp1); @@ -93,118 +97,129 @@ point32rgba(Imaging im, int x, int y, int ink) } static inline void -hline8(Imaging im, int x0, int y0, int x1, int ink) -{ - int tmp; +hline8(Imaging im, int x0, int y0, int x1, int ink) { + int tmp, pixelwidth; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) + if (x0 > x1) { tmp = x0, x0 = x1, x1 = tmp; - if (x0 < 0) + } + if (x0 < 0) { x0 = 0; - else if (x0 >= im->xsize) + } else if (x0 >= im->xsize) { return; - if (x1 < 0) + } + if (x1 < 0) { return; - else if (x1 >= im->xsize) - x1 = im->xsize-1; - if (x0 <= x1) - memset(im->image8[y0] + x0, (UINT8) ink, x1 - x0 + 1); + } else if (x1 >= im->xsize) { + x1 = im->xsize - 1; + } + if (x0 <= x1) { + pixelwidth = strncmp(im->mode, "I;16", 4) == 0 ? 2 : 1; + memset( + im->image8[y0] + x0 * pixelwidth, + (UINT8)ink, + (x1 - x0 + 1) * pixelwidth); + } } } static inline void -hline32(Imaging im, int x0, int y0, int x1, int ink) -{ +hline32(Imaging im, int x0, int y0, int x1, int ink) { int tmp; - INT32* p; + INT32 *p; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) + if (x0 > x1) { tmp = x0, x0 = x1, x1 = tmp; - if (x0 < 0) + } + if (x0 < 0) { x0 = 0; - else if (x0 >= im->xsize) + } else if (x0 >= im->xsize) { return; - if (x1 < 0) + } + if (x1 < 0) { return; - else if (x1 >= im->xsize) - x1 = im->xsize-1; + } else if (x1 >= im->xsize) { + x1 = im->xsize - 1; + } p = im->image32[y0]; - while (x0 <= x1) + while (x0 <= x1) { p[x0++] = ink; + } } } static inline void -hline32rgba(Imaging im, int x0, int y0, int x1, int ink) -{ +hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { int tmp; unsigned int tmp1; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) + if (x0 > x1) { tmp = x0, x0 = x1, x1 = tmp; - if (x0 < 0) + } + if (x0 < 0) { x0 = 0; - else if (x0 >= im->xsize) + } else if (x0 >= im->xsize) { return; - if (x1 < 0) + } + if (x1 < 0) { return; - else if (x1 >= im->xsize) - x1 = im->xsize-1; + } else if (x1 >= im->xsize) { + x1 = im->xsize - 1; + } if (x0 <= x1) { - UINT8* out = (UINT8*) im->image[y0]+x0*4; - UINT8* in = (UINT8*) &ink; + UINT8 *out = (UINT8 *)im->image[y0] + x0 * 4; + UINT8 *in = (UINT8 *)&ink; while (x0 <= x1) { out[0] = BLEND(in[3], out[0], in[0], tmp1); out[1] = BLEND(in[3], out[1], in[1], tmp1); out[2] = BLEND(in[3], out[2], in[2], tmp1); - x0++; out += 4; + x0++; + out += 4; } } } } static inline void -line8(Imaging im, int x0, int y0, int x1, int y1, int ink) -{ +line8(Imaging im, int x0, int y0, int x1, int y1, int ink) { int i, n, e; int dx, dy; int xs, ys; /* normalize coordinates */ - dx = x1-x0; - if (dx < 0) + dx = x1 - x0; + if (dx < 0) { dx = -dx, xs = -1; - else + } else { xs = 1; - dy = y1-y0; - if (dy < 0) + } + dy = y1 - y0; + if (dy < 0) { dy = -dy, ys = -1; - else + } else { ys = 1; + } n = (dx > dy) ? dx : dy; - if (dx == 0) - + if (dx == 0) { /* vertical */ for (i = 0; i < dy; i++) { point8(im, x0, y0, ink); y0 += ys; } - else if (dy == 0) - + } else if (dy == 0) { /* horizontal */ for (i = 0; i < dx; i++) { point8(im, x0, y0, ink); x0 += xs; } - else if (dx > dy) { - + } else if (dx > dy) { /* bresenham, horizontal slope */ n = dx; dy += dy; @@ -222,7 +237,6 @@ line8(Imaging im, int x0, int y0, int x1, int y1, int ink) } } else { - /* bresenham, vertical slope */ n = dy; dx += dx; @@ -238,49 +252,46 @@ line8(Imaging im, int x0, int y0, int x1, int y1, int ink) e += dx; y0 += ys; } - } } static inline void -line32(Imaging im, int x0, int y0, int x1, int y1, int ink) -{ +line32(Imaging im, int x0, int y0, int x1, int y1, int ink) { int i, n, e; int dx, dy; int xs, ys; /* normalize coordinates */ - dx = x1-x0; - if (dx < 0) + dx = x1 - x0; + if (dx < 0) { dx = -dx, xs = -1; - else + } else { xs = 1; - dy = y1-y0; - if (dy < 0) + } + dy = y1 - y0; + if (dy < 0) { dy = -dy, ys = -1; - else + } else { ys = 1; + } n = (dx > dy) ? dx : dy; - if (dx == 0) - + if (dx == 0) { /* vertical */ for (i = 0; i < dy; i++) { point32(im, x0, y0, ink); y0 += ys; } - else if (dy == 0) - + } else if (dy == 0) { /* horizontal */ for (i = 0; i < dx; i++) { point32(im, x0, y0, ink); x0 += xs; } - else if (dx > dy) { - + } else if (dx > dy) { /* bresenham, horizontal slope */ n = dx; dy += dy; @@ -298,7 +309,6 @@ line32(Imaging im, int x0, int y0, int x1, int y1, int ink) } } else { - /* bresenham, vertical slope */ n = dy; dx += dx; @@ -314,49 +324,46 @@ line32(Imaging im, int x0, int y0, int x1, int y1, int ink) e += dx; y0 += ys; } - } } static inline void -line32rgba(Imaging im, int x0, int y0, int x1, int y1, int ink) -{ +line32rgba(Imaging im, int x0, int y0, int x1, int y1, int ink) { int i, n, e; int dx, dy; int xs, ys; /* normalize coordinates */ - dx = x1-x0; - if (dx < 0) + dx = x1 - x0; + if (dx < 0) { dx = -dx, xs = -1; - else + } else { xs = 1; - dy = y1-y0; - if (dy < 0) + } + dy = y1 - y0; + if (dy < 0) { dy = -dy, ys = -1; - else + } else { ys = 1; + } n = (dx > dy) ? dx : dy; - if (dx == 0) - + if (dx == 0) { /* vertical */ for (i = 0; i < dy; i++) { point32rgba(im, x0, y0, ink); y0 += ys; } - else if (dy == 0) - + } else if (dy == 0) { /* horizontal */ for (i = 0; i < dx; i++) { point32rgba(im, x0, y0, ink); x0 += xs; } - else if (dx > dy) { - + } else if (dx > dy) { /* bresenham, horizontal slope */ n = dx; dy += dy; @@ -374,7 +381,6 @@ line32rgba(Imaging im, int x0, int y0, int x1, int y1, int ink) } } else { - /* bresenham, vertical slope */ n = dy; dx += dx; @@ -390,37 +396,62 @@ line32rgba(Imaging im, int x0, int y0, int x1, int y1, int ink) e += dx; y0 += ys; } - } } static int -x_cmp(const void *x0, const void *x1) -{ - float diff = *((float*)x0) - *((float*)x1); - if (diff < 0) +x_cmp(const void *x0, const void *x1) { + float diff = *((float *)x0) - *((float *)x1); + if (diff < 0) { return -1; - else if (diff > 0) + } else if (diff > 0) { return 1; - else + } else { return 0; + } } +static void +draw_horizontal_lines( + Imaging im, int n, Edge *e, int ink, int *x_pos, int y, hline_handler hline) { + int i; + for (i = 0; i < n; i++) { + if (e[i].ymin == y && e[i].ymin == e[i].ymax) { + int xmax; + int xmin = e[i].xmin; + if (*x_pos != -1 && *x_pos < xmin) { + // Line would be after the current position + continue; + } + + xmax = e[i].xmax; + if (*x_pos > xmin) { + // Line would be partway through x_pos, so increase the starting point + xmin = *x_pos; + if (xmax < xmin) { + // Line would now end before it started + continue; + } + } + + (*hline)(im, xmin, e[i].ymin, xmax, ink); + *x_pos = xmax + 1; + } + } +} /* * Filled polygon draw function using scan line algorithm. */ static inline int -polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, - hline_handler hline) -{ - - Edge** edge_table; - float* xx; +polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, int hasAlpha) { + Edge **edge_table; + float *xx; int edge_count = 0; int ymin = im->ysize - 1; int ymax = 0; - int i; + int i, j, k; + float adjacent_line_x, adjacent_line_x_other_edge; if (n <= 0) { return 0; @@ -428,24 +459,24 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, /* Initialize the edge table and find polygon boundaries */ /* malloc check ok, using calloc */ - edge_table = calloc(n, sizeof(Edge*)); + edge_table = calloc(n, sizeof(Edge *)); if (!edge_table) { return -1; } for (i = 0; i < n; i++) { - /* This causes that the pixels of horizontal edges are drawn twice :( - * but without it there are inconsistencies in ellipses */ - if (e[i].ymin == e[i].ymax) { - (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink); - continue; - } if (ymin > e[i].ymin) { ymin = e[i].ymin; } if (ymax < e[i].ymax) { ymax = e[i].ymax; } + if (e[i].ymin == e[i].ymax) { + if (hasAlpha != 1) { + (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink); + } + continue; + } edge_table[edge_count++] = (e + i); } if (ymin < 0) { @@ -463,21 +494,84 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, return -1; } for (; ymin <= ymax; ymin++) { - int j = 0; + j = 0; for (i = 0; i < edge_count; i++) { - Edge* current = edge_table[i]; + Edge *current = edge_table[i]; if (ymin >= current->ymin && ymin <= current->ymax) { xx[j++] = (ymin - current->y0) * current->dx + current->x0; - } - /* Needed to draw consistent polygons */ - if (ymin == current->ymax && ymin < ymax) { - xx[j] = xx[j - 1]; - j++; + + if (ymin == current->ymax && ymin < ymax) { + // Needed to draw consistent polygons + xx[j] = xx[j - 1]; + j++; + } else if (current->dx != 0 && roundf(xx[j-1]) == xx[j-1]) { + // Connect discontiguous corners + for (k = 0; k < i; k++) { + Edge *other_edge = edge_table[k]; + if ((current->dx > 0 && other_edge->dx <= 0) || + (current->dx < 0 && other_edge->dx >= 0)) { + continue; + } + // Check if the two edges join to make a corner + if (((ymin == current->ymin && ymin == other_edge->ymin) || + (ymin == current->ymax && ymin == other_edge->ymax)) && + xx[j-1] == (ymin - other_edge->y0) * other_edge->dx + other_edge->x0) { + // Determine points from the edges on the next row + // Or if this is the last row, check the previous row + int offset = ymin == ymax ? -1 : 1; + adjacent_line_x = (ymin + offset - current->y0) * current->dx + current->x0; + adjacent_line_x_other_edge = (ymin + offset - other_edge->y0) * other_edge->dx + other_edge->x0; + if (ymin == current->ymax) { + if (current->dx > 0) { + xx[k] = fmax(adjacent_line_x, adjacent_line_x_other_edge) + 1; + } else { + xx[k] = fmin(adjacent_line_x, adjacent_line_x_other_edge) - 1; + } + } else { + if (current->dx > 0) { + xx[k] = fmin(adjacent_line_x, adjacent_line_x_other_edge); + } else { + xx[k] = fmax(adjacent_line_x, adjacent_line_x_other_edge) + 1; + } + } + break; + } + } + } } } qsort(xx, j, sizeof(float), x_cmp); - for (i = 1; i < j; i += 2) { - (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink); + if (hasAlpha == 1) { + int x_pos = j == 0 ? -1 : 0; + for (i = 1; i < j; i += 2) { + int x_end = ROUND_DOWN(xx[i]); + if (x_end < x_pos) { + // Line would be before the current position + continue; + } + draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); + if (x_end < x_pos) { + // Line would be before the current position + continue; + } + + int x_start = ROUND_UP(xx[i - 1]); + if (x_pos > x_start) { + // Line would be partway through x_pos, so increase the starting point + x_start = x_pos; + if (x_end < x_start) { + // Line would now end before it started + continue; + } + } + (*hline)(im, x_start, ymin, x_end, ink); + x_pos = x_end + 1; + } + draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); + } else { + for (i = 1; i < j; i += 2) { + (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink); + } } } @@ -487,47 +581,46 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, } static inline int -polygon8(Imaging im, int n, Edge *e, int ink, int eofill) -{ - return polygon_generic(im, n, e, ink, eofill, hline8); +polygon8(Imaging im, int n, Edge *e, int ink, int eofill) { + return polygon_generic(im, n, e, ink, eofill, hline8, 0); } static inline int -polygon32(Imaging im, int n, Edge *e, int ink, int eofill) -{ - return polygon_generic(im, n, e, ink, eofill, hline32); +polygon32(Imaging im, int n, Edge *e, int ink, int eofill) { + return polygon_generic(im, n, e, ink, eofill, hline32, 0); } static inline int -polygon32rgba(Imaging im, int n, Edge *e, int ink, int eofill) -{ - return polygon_generic(im, n, e, ink, eofill, hline32rgba); +polygon32rgba(Imaging im, int n, Edge *e, int ink, int eofill) { + return polygon_generic(im, n, e, ink, eofill, hline32rgba, 1); } static inline void -add_edge(Edge *e, int x0, int y0, int x1, int y1) -{ +add_edge(Edge *e, int x0, int y0, int x1, int y1) { /* printf("edge %d %d %d %d\n", x0, y0, x1, y1); */ - if (x0 <= x1) + if (x0 <= x1) { e->xmin = x0, e->xmax = x1; - else + } else { e->xmin = x1, e->xmax = x0; + } - if (y0 <= y1) + if (y0 <= y1) { e->ymin = y0, e->ymax = y1; - else + } else { e->ymin = y1, e->ymax = y0; + } if (y0 == y1) { e->d = 0; e->dx = 0.0; } else { - e->dx = ((float)(x1-x0)) / (y1-y0); - if (y0 == e->ymin) + e->dx = ((float)(x1 - x0)) / (y1 - y0); + if (y0 == e->ymin) { e->d = 1; - else + } else { e->d = -1; + } } e->x0 = x0; @@ -541,27 +634,26 @@ typedef struct { int (*polygon)(Imaging im, int n, Edge *e, int ink, int eofill); } DRAW; -DRAW draw8 = { point8, hline8, line8, polygon8 }; -DRAW draw32 = { point32, hline32, line32, polygon32 }; -DRAW draw32rgba = { point32rgba, hline32rgba, line32rgba, polygon32rgba }; +DRAW draw8 = {point8, hline8, line8, polygon8}; +DRAW draw32 = {point32, hline32, line32, polygon32}; +DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba, polygon32rgba}; /* -------------------------------------------------------------------- */ /* Interface */ /* -------------------------------------------------------------------- */ -#define DRAWINIT()\ - if (im->image8) {\ - draw = &draw8;\ - ink = INK8(ink_);\ - } else {\ - draw = (op) ? &draw32rgba : &draw32; \ - ink = INK32(ink_);\ +#define DRAWINIT() \ + if (im->image8) { \ + draw = &draw8; \ + ink = INK8(ink_); \ + } else { \ + draw = (op) ? &draw32rgba : &draw32; \ + memcpy(&ink, ink_, sizeof(ink)); \ } int -ImagingDrawPoint(Imaging im, int x0, int y0, const void* ink_, int op) -{ - DRAW* draw; +ImagingDrawPoint(Imaging im, int x0, int y0, const void *ink_, int op) { + DRAW *draw; INT32 ink; DRAWINIT(); @@ -572,10 +664,8 @@ ImagingDrawPoint(Imaging im, int x0, int y0, const void* ink_, int op) } int -ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void* ink_, - int op) -{ - DRAW* draw; +ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink_, int op) { + DRAW *draw; INT32 ink; DRAWINIT(); @@ -586,10 +676,9 @@ ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void* ink_, } int -ImagingDrawWideLine(Imaging im, int x0, int y0, int x1, int y1, - const void* ink_, int width, int op) -{ - DRAW* draw; +ImagingDrawWideLine( + Imaging im, int x0, int y0, int x1, int y1, const void *ink_, int width, int op) { + DRAW *draw; INT32 ink; int dx, dy; double big_hypotenuse, small_hypotenuse, ratio_max, ratio_min; @@ -598,14 +687,14 @@ ImagingDrawWideLine(Imaging im, int x0, int y0, int x1, int y1, DRAWINIT(); - dx = x1-x0; - dy = y1-y0; + dx = x1 - x0; + dy = y1 - y0; if (dx == 0 && dy == 0) { draw->point(im, x0, y0, ink); return 0; } - big_hypotenuse = sqrt((double) (dx*dx + dy*dy)); + big_hypotenuse = hypot(dx, dy); small_hypotenuse = (width - 1) / 2.0; ratio_max = ROUND_UP(small_hypotenuse) / big_hypotenuse; ratio_min = ROUND_DOWN(small_hypotenuse) / big_hypotenuse; @@ -619,13 +708,12 @@ ImagingDrawWideLine(Imaging im, int x0, int y0, int x1, int y1, {x0 - dxmin, y0 + dymax}, {x1 - dxmin, y1 + dymax}, {x1 + dxmax, y1 - dymin}, - {x0 + dxmax, y0 - dymin} - }; + {x0 + dxmax, y0 - dymin}}; - add_edge(e+0, vertices[0][0], vertices[0][1], vertices[1][0], vertices[1][1]); - add_edge(e+1, vertices[1][0], vertices[1][1], vertices[2][0], vertices[2][1]); - add_edge(e+2, vertices[2][0], vertices[2][1], vertices[3][0], vertices[3][1]); - add_edge(e+3, vertices[3][0], vertices[3][1], vertices[0][0], vertices[0][1]); + add_edge(e + 0, vertices[0][0], vertices[0][1], vertices[1][0], vertices[1][1]); + add_edge(e + 1, vertices[1][0], vertices[1][1], vertices[2][0], vertices[2][1]); + add_edge(e + 2, vertices[2][0], vertices[2][1], vertices[3][0], vertices[3][1]); + add_edge(e + 3, vertices[3][0], vertices[3][1], vertices[0][0], vertices[0][1]); draw->polygon(im, 4, e, ink, 0); } @@ -633,236 +721,995 @@ ImagingDrawWideLine(Imaging im, int x0, int y0, int x1, int y1, } int -ImagingDrawRectangle(Imaging im, int x0, int y0, int x1, int y1, - const void* ink_, int fill, int op) -{ +ImagingDrawRectangle( + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink_, + int fill, + int width, + int op) { + int i; int y; int tmp; - DRAW* draw; + DRAW *draw; INT32 ink; DRAWINIT(); - if (y0 > y1) + if (y0 > y1) { tmp = y0, y0 = y1, y1 = tmp; + } if (fill) { - - if (y0 < 0) + if (y0 < 0) { y0 = 0; - else if (y0 >= im->ysize) + } else if (y0 >= im->ysize) { return 0; + } - if (y1 < 0) + if (y1 < 0) { return 0; - else if (y1 > im->ysize) + } else if (y1 > im->ysize) { y1 = im->ysize; + } - for (y = y0; y <= y1; y++) + for (y = y0; y <= y1; y++) { draw->hline(im, x0, y, x1, ink); + } } else { - /* outline */ - draw->line(im, x0, y0, x1, y0, ink); - draw->line(im, x1, y0, x1, y1, ink); - draw->line(im, x1, y1, x0, y1, ink); - draw->line(im, x0, y1, x0, y0, ink); - + if (width == 0) { + width = 1; + } + for (i = 0; i < width; i++) { + draw->hline(im, x0, y0 + i, x1, ink); + draw->hline(im, x0, y1 - i, x1, ink); + draw->line(im, x1 - i, y0 + width, x1 - i, y1 - width + 1, ink); + draw->line(im, x0 + i, y0 + width, x0 + i, y1 - width + 1, ink); + } } return 0; } int -ImagingDrawPolygon(Imaging im, int count, int* xy, const void* ink_, - int fill, int op) -{ - int i, n; - DRAW* draw; +ImagingDrawPolygon(Imaging im, int count, int *xy, const void *ink_, int fill, int width, int op) { + int i, n, x0, y0, x1, y1; + DRAW *draw; INT32 ink; - if (count <= 0) + if (count <= 0) { return 0; + } DRAWINIT(); if (fill) { - /* Build edge list */ /* malloc check ok, using calloc */ - Edge* e = calloc(count, sizeof(Edge)); + Edge *e = calloc(count, sizeof(Edge)); if (!e) { - (void) ImagingError_MemoryError(); + (void)ImagingError_MemoryError(); return -1; } - for (i = n = 0; i < count-1; i++) - add_edge(&e[n++], xy[i+i], xy[i+i+1], xy[i+i+2], xy[i+i+3]); - if (xy[i+i] != xy[0] || xy[i+i+1] != xy[1]) - add_edge(&e[n++], xy[i+i], xy[i+i+1], xy[0], xy[1]); + for (i = n = 0; i < count - 1; i++) { + x0 = xy[i * 2]; + y0 = xy[i * 2 + 1]; + x1 = xy[i * 2 + 2]; + y1 = xy[i * 2 + 3]; + if (y0 == y1 && i != 0 && y0 == xy[i * 2 - 1]) { + // This is a horizontal line, + // that immediately follows another horizontal line + Edge *last_e = &e[n-1]; + if (x1 > x0 && x0 > xy[i * 2 - 2]) { + // They are both increasing in x + last_e->xmax = x1; + continue; + } else if (x1 < x0 && x0 < xy[i * 2 - 2]) { + // They are both decreasing in x + last_e->xmin = x1; + continue; + } + } + add_edge(&e[n++], x0, y0, x1, y1); + } + if (xy[i * 2] != xy[0] || xy[i * 2 + 1] != xy[1]) { + add_edge(&e[n++], xy[i * 2], xy[i * 2 + 1], xy[0], xy[1]); + } draw->polygon(im, n, e, ink, 0); free(e); } else { - /* Outline */ - for (i = 0; i < count-1; i++) - draw->line(im, xy[i+i], xy[i+i+1], xy[i+i+2], xy[i+i+3], ink); - draw->line(im, xy[i+i], xy[i+i+1], xy[0], xy[1], ink); - + if (width == 1) { + for (i = 0; i < count - 1; i++) { + draw->line(im, xy[i * 2], xy[i * 2 + 1], xy[i * 2 + 2], xy[i * 2 + 3], ink); + } + draw->line(im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink); + } else { + for (i = 0; i < count - 1; i++) { + ImagingDrawWideLine(im, xy[i * 2], xy[i * 2 + 1], xy[i * 2 + 2], xy[i * 2 + 3], ink_, width, op); + } + ImagingDrawWideLine(im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op); + } } return 0; } int -ImagingDrawBitmap(Imaging im, int x0, int y0, Imaging bitmap, const void* ink, - int op) -{ +ImagingDrawBitmap(Imaging im, int x0, int y0, Imaging bitmap, const void *ink, int op) { return ImagingFill2( - im, ink, bitmap, - x0, y0, x0 + bitmap->xsize, y0 + bitmap->ysize - ); + im, ink, bitmap, x0, y0, x0 + bitmap->xsize, y0 + bitmap->ysize); } /* -------------------------------------------------------------------- */ /* standard shapes */ -#define ARC 0 -#define CHORD 1 -#define PIESLICE 2 +// Imagine 2D plane and ellipse with center in (0, 0) and semi-major axes a and b. +// Then quarter_* stuff approximates its top right quarter (x, y >= 0) with integer +// points from set {(2x+x0, 2y+y0) | x,y in Z} where x0, y0 are from {0, 1} and +// are such that point (a, b) is in the set. -static int -ellipse(Imaging im, int x0, int y0, int x1, int y1, - float start, float end, const void* ink_, int fill, - int mode, int op) -{ - float i; - int n; - int cx, cy; - int w, h; - int x = 0, y = 0; - int lx = 0, ly = 0; - int sx = 0, sy = 0; - DRAW* draw; - INT32 ink; +typedef struct { + int32_t a, b, cx, cy, ex, ey; + int64_t a2, b2, a2b2; + int8_t finished; +} quarter_state; - w = x1 - x0; - h = y1 - y0; - if (w < 0 || h < 0) - return 0; +void +quarter_init(quarter_state *s, int32_t a, int32_t b) { + if (a < 0 || b < 0) { + s->finished = 1; + } else { + s->a = a; + s->b = b; + s->cx = a; + s->cy = b % 2; + s->ex = a % 2; + s->ey = b; + s->a2 = a * a; + s->b2 = b * b; + s->a2b2 = s->a2 * s->b2; + s->finished = 0; + } +} - DRAWINIT(); +// deviation of the point from ellipse curve, basically a substitution +// of the point into the ellipse equation +int64_t +quarter_delta(quarter_state *s, int64_t x, int64_t y) { + return llabs(s->a2 * y * y + s->b2 * x * x - s->a2b2); +} + +int8_t +quarter_next(quarter_state *s, int32_t *ret_x, int32_t *ret_y) { + if (s->finished) { + return -1; + } + *ret_x = s->cx; + *ret_y = s->cy; + if (s->cx == s->ex && s->cy == s->ey) { + s->finished = 1; + } else { + // Bresenham's algorithm, possible optimization: only consider 2 of 3 + // next points depending on current slope + int32_t nx = s->cx; + int32_t ny = s->cy + 2; + int64_t ndelta = quarter_delta(s, nx, ny); + if (nx > 1) { + int64_t newdelta = quarter_delta(s, s->cx - 2, s->cy + 2); + if (ndelta > newdelta) { + nx = s->cx - 2; + ny = s->cy + 2; + ndelta = newdelta; + } + newdelta = quarter_delta(s, s->cx - 2, s->cy); + if (ndelta > newdelta) { + nx = s->cx - 2; + ny = s->cy; + } + } + s->cx = nx; + s->cy = ny; + } + return 0; +} - cx = (x0 + x1) / 2; - cy = (y0 + y1) / 2; +// quarter_* stuff can "draw" a quarter of an ellipse with thickness 1, great. +// Now we use ellipse_* stuff to join all four quarters of two different sized +// ellipses and receive horizontal segments of a complete ellipse with +// specified thickness. +// +// Still using integer grid with step 2 at this point (like in quarter_*) +// to ease angle clipping in future. - while (end < start) - end += 360; +typedef struct { + quarter_state st_o, st_i; + int32_t py, pl, pr; + int32_t cy[4], cl[4], cr[4]; + int8_t bufcnt; + int8_t finished; + int8_t leftmost; +} ellipse_state; - if (end - start > 360) { - /* no need to go in loops */ - end = start + 361; +void +ellipse_init(ellipse_state *s, int32_t a, int32_t b, int32_t w) { + s->bufcnt = 0; + s->leftmost = a % 2; + quarter_init(&s->st_o, a, b); + if (w < 1 || quarter_next(&s->st_o, &s->pr, &s->py) == -1) { + s->finished = 1; + } else { + s->finished = 0; + quarter_init(&s->st_i, a - 2 * (w - 1), b - 2 * (w - 1)); + s->pl = s->leftmost; } +} - if (mode != ARC && fill) { - - /* Build edge list */ - /* malloc check UNDONE, FLOAT? */ - Edge* e = calloc((end - start + 3), sizeof(Edge)); - if (!e) { - ImagingError_MemoryError(); +int8_t +ellipse_next(ellipse_state *s, int32_t *ret_x0, int32_t *ret_y, int32_t *ret_x1) { + if (s->bufcnt == 0) { + if (s->finished) { return -1; } + int32_t y = s->py; + int32_t l = s->pl; + int32_t r = s->pr; + int32_t cx = 0, cy = 0; + int8_t next_ret; + while ((next_ret = quarter_next(&s->st_o, &cx, &cy)) != -1 && cy <= y) { + } + if (next_ret == -1) { + s->finished = 1; + } else { + s->pr = cx; + s->py = cy; + } + while ((next_ret = quarter_next(&s->st_i, &cx, &cy)) != -1 && cy <= y) { + l = cx; + } + s->pl = next_ret == -1 ? s->leftmost : cx; - n = 0; + if ((l > 0 || l < r) && y > 0) { + s->cl[s->bufcnt] = l == 0 ? 2 : l; + s->cy[s->bufcnt] = y; + s->cr[s->bufcnt] = r; + ++s->bufcnt; + } + if (y > 0) { + s->cl[s->bufcnt] = -r; + s->cy[s->bufcnt] = y; + s->cr[s->bufcnt] = -l; + ++s->bufcnt; + } + if (l > 0 || l < r) { + s->cl[s->bufcnt] = l == 0 ? 2 : l; + s->cy[s->bufcnt] = -y; + s->cr[s->bufcnt] = r; + ++s->bufcnt; + } + s->cl[s->bufcnt] = -r; + s->cy[s->bufcnt] = -y; + s->cr[s->bufcnt] = -l; + ++s->bufcnt; + } + --s->bufcnt; + *ret_x0 = s->cl[s->bufcnt]; + *ret_y = s->cy[s->bufcnt]; + *ret_x1 = s->cr[s->bufcnt]; + return 0; +} - for (i = start; i < end+1; i++) { - if (i > end) { - i = end; +// Clipping tree consists of half-plane clipping nodes and combining nodes. +// We can throw a horizontal segment in such a tree and collect an ordered set +// of resulting disjoint clipped segments organized into a sorted linked list +// of their end points. +typedef enum { + CT_AND, // intersection + CT_OR, // union + CT_CLIP // half-plane clipping +} clip_type; + +typedef struct clip_node { + clip_type type; + double a, b, c; // half-plane coeffs, only used in clipping nodes + struct clip_node *l; // child pointers, are only non-NULL in combining nodes + struct clip_node *r; +} clip_node; + +// Linked list for the ends of the clipped horizontal segments. +// Since the segment is always horizontal, we don't need to store Y coordinate. +typedef struct event_list { + int32_t x; + int8_t type; // used internally, 1 for the left end (smaller X), -1 for the + // right end; pointless in output since the output segments + // are disjoint, therefore the types would always come in pairs + // and interchange (1 -1 1 -1 ...) + struct event_list *next; +} event_list; + +// Mirrors all the clipping nodes of the tree relative to the y = x line. +void +clip_tree_transpose(clip_node *root) { + if (root != NULL) { + if (root->type == CT_CLIP) { + double t = root->a; + root->a = root->b; + root->b = t; + } + clip_tree_transpose(root->l); + clip_tree_transpose(root->r); + } +} + +// Outputs a sequence of open-close events (types -1 and 1) for +// non-intersecting segments sorted by X coordinate. +// Combining nodes (AND, OR) may also accept sequences for intersecting +// segments, i.e. something like correct bracket sequences. +int +clip_tree_do_clip( + clip_node *root, int32_t x0, int32_t y, int32_t x1, event_list **ret) { + if (root == NULL) { + event_list *start = malloc(sizeof(event_list)); + if (!start) { + ImagingError_MemoryError(); + return -1; + } + event_list *end = malloc(sizeof(event_list)); + if (!end) { + free(start); + ImagingError_MemoryError(); + return -1; + } + start->x = x0; + start->type = 1; + start->next = end; + end->x = x1; + end->type = -1; + end->next = NULL; + *ret = start; + return 0; + } + if (root->type == CT_CLIP) { + double eps = 1e-9; + double A = root->a; + double B = root->b; + double C = root->c; + if (fabs(A) < eps) { + if (B * y + C < -eps) { + x0 = 1; + x1 = 0; + } + } else { + // X of intersection + double ix = -(B * y + C) / A; + if (A * x0 + B * y + C < eps) { + x0 = lround(fmax(x0, ix)); + } + if (A * x1 + B * y + C < eps) { + x1 = lround(fmin(x1, ix)); + } + } + if (x0 <= x1) { + event_list *start = malloc(sizeof(event_list)); + if (!start) { + ImagingError_MemoryError(); + return -1; + } + event_list *end = malloc(sizeof(event_list)); + if (!end) { + free(start); + ImagingError_MemoryError(); + return -1; + } + start->x = x0; + start->type = 1; + start->next = end; + end->x = x1; + end->type = -1; + end->next = NULL; + *ret = start; + } else { + *ret = NULL; + } + return 0; + } + if (root->type == CT_OR || root->type == CT_AND) { + event_list *l1; + event_list *l2; + if (clip_tree_do_clip(root->l, x0, y, x1, &l1) < 0) { + return -1; + } + if (clip_tree_do_clip(root->r, x0, y, x1, &l2) < 0) { + while (l1) { + l2 = l1->next; + free(l1); + l1 = l2; } - x = FLOOR((cos(i*M_PI/180) * w/2) + cx + 0.5); - y = FLOOR((sin(i*M_PI/180) * h/2) + cy + 0.5); - if (i != start) - add_edge(&e[n++], lx, ly, x, y); - else - sx = x, sy = y; - lx = x, ly = y; - } - - if (n > 0) { - /* close and draw polygon */ - if (mode == PIESLICE) { - if (x != cx || y != cy) { - add_edge(&e[n++], x, y, cx, cy); - add_edge(&e[n++], cx, cy, sx, sy); + return -1; + } + *ret = NULL; + event_list *tail = NULL; + int32_t k1 = 0; + int32_t k2 = 0; + while (l1 != NULL || l2 != NULL) { + event_list *t; + if (l2 == NULL || + (l1 != NULL && + (l1->x < l2->x || (l1->x == l2->x && l1->type > l2->type)))) { + t = l1; + k1 += t->type; + assert(k1 >= 0); + l1 = l1->next; + } else { + t = l2; + k2 += t->type; + assert(k2 >= 0); + l2 = l2->next; + } + t->next = NULL; + if ((root->type == CT_OR && + ((t->type == 1 && (tail == NULL || tail->type == -1)) || + (t->type == -1 && k1 == 0 && k2 == 0))) || + (root->type == CT_AND && + ((t->type == 1 && (tail == NULL || tail->type == -1) && k1 > 0 && + k2 > 0) || + (t->type == -1 && tail != NULL && tail->type == 1 && + (k1 == 0 || k2 == 0))))) { + if (tail == NULL) { + *ret = t; + } else { + tail->next = t; } + tail = t; } else { - if (x != sx || y != sy) - add_edge(&e[n++], x, y, sx, sy); + free(t); } - draw->polygon(im, n, e, ink, 0); } + return 0; + } + *ret = NULL; + return 0; +} - free(e); +// One more layer of processing on top of the regular ellipse. +// Uses the clipping tree. +// Used for producing ellipse derivatives such as arc, chord, pie, etc. +typedef struct { + ellipse_state st; + clip_node *root; + clip_node nodes[7]; + int32_t node_count; + event_list *head; + int32_t y; +} clip_ellipse_state; +typedef void (*clip_ellipse_init)( + clip_ellipse_state *, int32_t, int32_t, int32_t, float, float); + +void +debug_clip_tree(clip_node *root, int space) { + if (root == NULL) { + return; + } + if (root->type == CT_CLIP) { + int t = space; + while (t--) { + fputc(' ', stderr); + } + fprintf(stderr, "clip %+fx%+fy%+f > 0\n", root->a, root->b, root->c); } else { + debug_clip_tree(root->l, space + 2); + int t = space; + while (t--) { + fputc(' ', stderr); + } + fprintf(stderr, "%s\n", root->type == CT_AND ? "and" : "or"); + debug_clip_tree(root->r, space + 2); + } + if (space == 0) { + fputc('\n', stderr); + } +} - for (i = start; i < end+1; i++) { - if (i > end) { - i = end; - } - x = FLOOR((cos(i*M_PI/180) * w/2) + cx + 0.5); - y = FLOOR((sin(i*M_PI/180) * h/2) + cy + 0.5); - if (i != start) - draw->line(im, lx, ly, x, y, ink); - else - sx = x, sy = y; - lx = x, ly = y; - } - - if (i != start) { - if (mode == PIESLICE) { - if (x != cx || y != cy) { - draw->line(im, x, y, cx, cy, ink); - draw->line(im, cx, cy, sx, sy, ink); - } - } else if (mode == CHORD) { - if (x != sx || y != sy) - draw->line(im, x, y, sx, sy, ink); +// Resulting angles will satisfy 0 <= al < 360, al <= ar <= al + 360 +void +normalize_angles(float *al, float *ar) { + if (*ar - *al >= 360) { + *al = 0; + *ar = 360; + } else { + *al = fmod(*al < 0 ? 360 - (fmod(-*al, 360)) : *al, 360); + *ar = *al + fmod(*ar < *al ? 360 - fmod(*al - *ar, 360) : *ar - *al, 360); + } +} + +// An arc with caps orthogonal to the ellipse curve. +void +arc_init(clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float ar) { + if (a < b) { + // transpose the coordinate system + arc_init(s, b, a, w, 90 - ar, 90 - al); + ellipse_init(&s->st, a, b, w); + clip_tree_transpose(s->root); + } else { + // a >= b, based on "wide" ellipse + ellipse_init(&s->st, a, b, w); + + s->head = NULL; + s->node_count = 0; + normalize_angles(&al, &ar); + + // building clipping tree, a lot of different cases + if (ar == al + 360) { + s->root = NULL; + } else { + clip_node *lc = s->nodes + s->node_count++; + clip_node *rc = s->nodes + s->node_count++; + lc->l = lc->r = rc->l = rc->r = NULL; + lc->type = rc->type = CT_CLIP; + lc->a = -a * sin(al * M_PI / 180.0); + lc->b = b * cos(al * M_PI / 180.0); + lc->c = (a * a - b * b) * sin(al * M_PI / 90.0) / 2.0; + rc->a = a * sin(ar * M_PI / 180.0); + rc->b = -b * cos(ar * M_PI / 180.0); + rc->c = (b * b - a * a) * sin(ar * M_PI / 90.0) / 2.0; + if (fmod(al, 180) == 0 || fmod(ar, 180) == 0) { + s->root = s->nodes + s->node_count++; + s->root->l = lc; + s->root->r = rc; + s->root->type = ar - al < 180 ? CT_AND : CT_OR; + } else if (((int)(al / 180) + (int)(ar / 180)) % 2 == 1) { + s->root = s->nodes + s->node_count++; + s->root->l = s->nodes + s->node_count++; + s->root->l->l = s->nodes + s->node_count++; + s->root->l->r = lc; + s->root->r = s->nodes + s->node_count++; + s->root->r->l = s->nodes + s->node_count++; + s->root->r->r = rc; + s->root->type = CT_OR; + s->root->l->type = CT_AND; + s->root->r->type = CT_AND; + s->root->l->l->type = CT_CLIP; + s->root->r->l->type = CT_CLIP; + s->root->l->l->l = s->root->l->l->r = NULL; + s->root->r->l->l = s->root->r->l->r = NULL; + s->root->l->l->a = s->root->l->l->c = 0; + s->root->r->l->a = s->root->r->l->c = 0; + s->root->l->l->b = (int)(al / 180) % 2 == 0 ? 1 : -1; + s->root->r->l->b = (int)(ar / 180) % 2 == 0 ? 1 : -1; + } else { + s->root = s->nodes + s->node_count++; + s->root->l = s->nodes + s->node_count++; + s->root->r = s->nodes + s->node_count++; + s->root->type = s->root->l->type = ar - al < 180 ? CT_AND : CT_OR; + s->root->l->l = lc; + s->root->l->r = rc; + s->root->r->type = CT_CLIP; + s->root->r->l = s->root->r->r = NULL; + s->root->r->a = s->root->r->c = 0; + s->root->r->b = ar < 180 || ar > 540 ? 1 : -1; } } } +} + +// A chord line. +void +chord_line_init( + clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float ar) { + ellipse_init(&s->st, a, b, a + b + 1); + + s->head = NULL; + s->node_count = 0; + + // line equation for chord + double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); + double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); + s->root = s->nodes + s->node_count++; + s->root->l = s->nodes + s->node_count++; + s->root->r = s->nodes + s->node_count++; + s->root->type = CT_AND; + s->root->l->type = s->root->r->type = CT_CLIP; + s->root->l->l = s->root->l->r = s->root->r->l = s->root->r->r = NULL; + s->root->l->a = yr - yl; + s->root->l->b = xl - xr; + s->root->l->c = -(s->root->l->a * xl + s->root->l->b * yl); + s->root->r->a = -s->root->l->a; + s->root->r->b = -s->root->l->b; + s->root->r->c = + 2 * w * sqrt(pow(s->root->l->a, 2.0) + pow(s->root->l->b, 2.0)) - s->root->l->c; +} + +// Pie side. +void +pie_side_init( + clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float _) { + ellipse_init(&s->st, a, b, a + b + 1); + + s->head = NULL; + s->node_count = 0; + + double xl = a * cos(al * M_PI / 180.0); + double yl = b * sin(al * M_PI / 180.0); + double a1 = -yl; + double b1 = xl; + double c1 = w * sqrt(a1 * a1 + b1 * b1); + + s->root = s->nodes + s->node_count++; + s->root->type = CT_AND; + s->root->l = s->nodes + s->node_count++; + s->root->l->type = CT_AND; + + clip_node *cnode; + cnode = s->nodes + s->node_count++; + cnode->l = cnode->r = NULL; + cnode->type = CT_CLIP; + cnode->a = a1; + cnode->b = b1; + cnode->c = c1; + s->root->l->l = cnode; + cnode = s->nodes + s->node_count++; + cnode->l = cnode->r = NULL; + cnode->type = CT_CLIP; + cnode->a = -a1; + cnode->b = -b1; + cnode->c = c1; + s->root->l->r = cnode; + cnode = s->nodes + s->node_count++; + cnode->l = cnode->r = NULL; + cnode->type = CT_CLIP; + cnode->a = b1; + cnode->b = -a1; + cnode->c = 0; + s->root->r = cnode; +} + +// A chord. +void +chord_init(clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float ar) { + ellipse_init(&s->st, a, b, w); + + s->head = NULL; + s->node_count = 0; + + // line equation for chord + double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); + double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); + s->root = s->nodes + s->node_count++; + s->root->l = s->root->r = NULL; + s->root->type = CT_CLIP; + s->root->a = yr - yl; + s->root->b = xl - xr; + s->root->c = -(s->root->a * xl + s->root->b * yl); +} + +// A pie. Can also be used to draw an arc with ugly sharp caps. +void +pie_init(clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float ar) { + ellipse_init(&s->st, a, b, w); + + s->head = NULL; + s->node_count = 0; + + // line equations for pie sides + double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); + double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); + + clip_node *lc = s->nodes + s->node_count++; + clip_node *rc = s->nodes + s->node_count++; + lc->l = lc->r = rc->l = rc->r = NULL; + lc->type = rc->type = CT_CLIP; + lc->a = -yl; + lc->b = xl; + lc->c = 0; + rc->a = yr; + rc->b = -xr; + rc->c = 0; + + s->root = s->nodes + s->node_count++; + s->root->l = lc; + s->root->r = rc; + s->root->type = ar - al < 180 ? CT_AND : CT_OR; + + // add one more semiplane to avoid spikes + if (ar - al < 90) { + clip_node *old_root = s->root; + clip_node *spike_clipper = s->nodes + s->node_count++; + s->root = s->nodes + s->node_count++; + s->root->l = old_root; + s->root->r = spike_clipper; + s->root->type = CT_AND; + + spike_clipper->l = spike_clipper->r = NULL; + spike_clipper->type = CT_CLIP; + spike_clipper->a = (xl + xr) / 2.0; + spike_clipper->b = (yl + yr) / 2.0; + spike_clipper->c = 0; + } +} + +void +clip_ellipse_free(clip_ellipse_state *s) { + while (s->head != NULL) { + event_list *t = s->head; + s->head = s->head->next; + free(t); + } +} + +int8_t +clip_ellipse_next( + clip_ellipse_state *s, int32_t *ret_x0, int32_t *ret_y, int32_t *ret_x1) { + int32_t x0, y, x1; + while (s->head == NULL && ellipse_next(&s->st, &x0, &y, &x1) >= 0) { + if (clip_tree_do_clip(s->root, x0, y, x1, &s->head) < 0) { + return -2; + } + s->y = y; + } + if (s->head != NULL) { + *ret_y = s->y; + event_list *t = s->head; + s->head = s->head->next; + *ret_x0 = t->x; + free(t); + t = s->head; + assert(t != NULL); + s->head = s->head->next; + *ret_x1 = t->x; + free(t); + return 0; + } + return -1; +} + +static int +ellipseNew( + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink_, + int fill, + int width, + int op) { + DRAW *draw; + INT32 ink; + DRAWINIT(); + + int a = x1 - x0; + int b = y1 - y0; + if (a < 0 || b < 0) { + return 0; + } + if (fill) { + width = a + b; + } + ellipse_state st; + ellipse_init(&st, a, b, width); + int32_t X0, Y, X1; + while (ellipse_next(&st, &X0, &Y, &X1) != -1) { + draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); + } return 0; } +static int +clipEllipseNew( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink_, + int width, + int op, + clip_ellipse_init init) { + DRAW *draw; + INT32 ink; + DRAWINIT(); + + int a = x1 - x0; + int b = y1 - y0; + if (a < 0 || b < 0) { + return 0; + } + + clip_ellipse_state st; + init(&st, a, b, width, start, end); + // debug_clip_tree(st.root, 0); + int32_t X0, Y, X1; + int next_code; + while ((next_code = clip_ellipse_next(&st, &X0, &Y, &X1)) >= 0) { + draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); + } + clip_ellipse_free(&st); + return next_code == -1 ? 0 : -1; +} +static int +arcNew( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink_, + int width, + int op) { + return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, arc_init); +} + +static int +chordNew( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink_, + int width, + int op) { + return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, chord_init); +} + +static int +chordLineNew( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink_, + int width, + int op) { + return clipEllipseNew( + im, x0, y0, x1, y1, start, end, ink_, width, op, chord_line_init); +} + +static int +pieNew( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink_, + int width, + int op) { + return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, pie_init); +} + +static int +pieSideNew( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + const void *ink_, + int width, + int op) { + return clipEllipseNew(im, x0, y0, x1, y1, start, 0, ink_, width, op, pie_side_init); +} + int -ImagingDrawArc(Imaging im, int x0, int y0, int x1, int y1, - float start, float end, const void* ink, int op) -{ - return ellipse(im, x0, y0, x1, y1, start, end, ink, 0, ARC, op); +ImagingDrawEllipse( + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink, + int fill, + int width, + int op) { + return ellipseNew(im, x0, y0, x1, y1, ink, fill, width, op); } int -ImagingDrawChord(Imaging im, int x0, int y0, int x1, int y1, - float start, float end, const void* ink, int fill, int op) -{ - return ellipse(im, x0, y0, x1, y1, start, end, ink, fill, CHORD, op); +ImagingDrawArc( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink, + int width, + int op) { + normalize_angles(&start, &end); + if (start + 360 == end) { + return ImagingDrawEllipse(im, x0, y0, x1, y1, ink, 0, width, op); + } + if (start == end) { + return 0; + } + return arcNew(im, x0, y0, x1, y1, start, end, ink, width, op); } int -ImagingDrawEllipse(Imaging im, int x0, int y0, int x1, int y1, - const void* ink, int fill, int op) -{ - return ellipse(im, x0, y0, x1, y1, 0, 360, ink, fill, CHORD, op); +ImagingDrawChord( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink, + int fill, + int width, + int op) { + normalize_angles(&start, &end); + if (start + 360 == end) { + return ImagingDrawEllipse(im, x0, y0, x1, y1, ink, fill, width, op); + } + if (start == end) { + return 0; + } + if (fill) { + return chordNew(im, x0, y0, x1, y1, start, end, ink, x1 - x0 + y1 - y0 + 1, op); + } else { + if (chordLineNew(im, x0, y0, x1, y1, start, end, ink, width, op)) { + return -1; + } + return chordNew(im, x0, y0, x1, y1, start, end, ink, width, op); + } } int -ImagingDrawPieslice(Imaging im, int x0, int y0, int x1, int y1, - float start, float end, const void* ink, int fill, int op) -{ - return ellipse(im, x0, y0, x1, y1, start, end, ink, fill, PIESLICE, op); +ImagingDrawPieslice( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink, + int fill, + int width, + int op) { + normalize_angles(&start, &end); + if (start + 360 == end) { + return ellipseNew(im, x0, y0, x1, y1, ink, fill, width, op); + } + if (start == end) { + return 0; + } + if (fill) { + return pieNew(im, x0, y0, x1, y1, start, end, ink, x1 + y1 - x0 - y0, op); + } else { + if (pieSideNew(im, x0, y0, x1, y1, start, ink, width, op)) { + return -1; + } + if (pieSideNew(im, x0, y0, x1, y1, end, ink, width, op)) { + return -1; + } + int xc = lround((x0 + x1 - width) / 2.0), yc = lround((y0 + y1 - width) / 2.0); + ellipseNew(im, xc, yc, xc + width - 1, yc + width - 1, ink, 1, 0, op); + return pieNew(im, x0, y0, x1, y1, start, end, ink, width, op); + } } /* -------------------------------------------------------------------- */ @@ -872,10 +1719,7 @@ ImagingDrawPieslice(Imaging im, int x0, int y0, int x1, int y1, semantics are ok, except that "curve" flattens the bezier curves by itself */ -#if 1 /* ARROW_GRAPHICS */ - struct ImagingOutlineInstance { - float x0, y0; float x, y; @@ -884,18 +1728,16 @@ struct ImagingOutlineInstance { Edge *edges; int size; - }; - ImagingOutline -ImagingOutlineNew(void) -{ +ImagingOutlineNew(void) { ImagingOutline outline; outline = calloc(1, sizeof(struct ImagingOutlineInstance)); - if (!outline) - return (ImagingOutline) ImagingError_MemoryError(); + if (!outline) { + return (ImagingOutline)ImagingError_MemoryError(); + } outline->edges = NULL; outline->count = outline->size = 0; @@ -906,22 +1748,21 @@ ImagingOutlineNew(void) } void -ImagingOutlineDelete(ImagingOutline outline) -{ - if (!outline) +ImagingOutlineDelete(ImagingOutline outline) { + if (!outline) { return; + } - if (outline->edges) + if (outline->edges) { free(outline->edges); + } free(outline); } - -static Edge* -allocate(ImagingOutline outline, int extra) -{ - Edge* e; +static Edge * +allocate(ImagingOutline outline, int extra) { + Edge *e; if (outline->count + extra > outline->size) { /* expand outline buffer */ @@ -930,14 +1771,15 @@ allocate(ImagingOutline outline, int extra) /* malloc check ok, uses calloc for overflow */ e = calloc(outline->size, sizeof(Edge)); } else { - if (outline->size > INT_MAX / sizeof(Edge)) { + if (outline->size > INT_MAX / (int)sizeof(Edge)) { return NULL; } /* malloc check ok, overflow checked above */ e = realloc(outline->edges, outline->size * sizeof(Edge)); } - if (!e) + if (!e) { return NULL; + } outline->edges = e; } @@ -949,8 +1791,7 @@ allocate(ImagingOutline outline, int extra) } int -ImagingOutlineMove(ImagingOutline outline, float x0, float y0) -{ +ImagingOutlineMove(ImagingOutline outline, float x0, float y0) { outline->x = outline->x0 = x0; outline->y = outline->y0 = y0; @@ -958,15 +1799,15 @@ ImagingOutlineMove(ImagingOutline outline, float x0, float y0) } int -ImagingOutlineLine(ImagingOutline outline, float x1, float y1) -{ - Edge* e; +ImagingOutlineLine(ImagingOutline outline, float x1, float y1) { + Edge *e; e = allocate(outline, 1); - if (!e) + if (!e) { return -1; /* out of memory */ + } - add_edge(e, (int) outline->x, (int) outline->y, (int) x1, (int) y1); + add_edge(e, (int)outline->x, (int)outline->y, (int)x1, (int)y1); outline->x = x1; outline->y = y1; @@ -975,18 +1816,24 @@ ImagingOutlineLine(ImagingOutline outline, float x1, float y1) } int -ImagingOutlineCurve(ImagingOutline outline, float x1, float y1, - float x2, float y2, float x3, float y3) -{ - Edge* e; +ImagingOutlineCurve( + ImagingOutline outline, + float x1, + float y1, + float x2, + float y2, + float x3, + float y3) { + Edge *e; int i; float xo, yo; #define STEPS 32 e = allocate(outline, STEPS); - if (!e) + if (!e) { return -1; /* out of memory */ + } xo = outline->x; yo = outline->y; @@ -994,22 +1841,20 @@ ImagingOutlineCurve(ImagingOutline outline, float x1, float y1, /* flatten the bezier segment */ for (i = 1; i <= STEPS; i++) { - - float t = ((float) i) / STEPS; - float t2 = t*t; - float t3 = t2*t; + float t = ((float)i) / STEPS; + float t2 = t * t; + float t3 = t2 * t; float u = 1.0F - t; - float u2 = u*u; - float u3 = u2*u; + float u2 = u * u; + float u3 = u2 * u; - float x = outline->x*u3 + 3*(x1*t*u2 + x2*t2*u) + x3*t3 + 0.5; - float y = outline->y*u3 + 3*(y1*t*u2 + y2*t2*u) + y3*t3 + 0.5; + float x = outline->x * u3 + 3 * (x1 * t * u2 + x2 * t2 * u) + x3 * t3 + 0.5; + float y = outline->y * u3 + 3 * (y1 * t * u2 + y2 * t2 * u) + y3 * t3 + 0.5; - add_edge(e++, xo, yo, (int) x, (int) y); + add_edge(e++, xo, yo, (int)x, (int)y); xo = x, yo = y; - } outline->x = xo; @@ -1019,81 +1864,81 @@ ImagingOutlineCurve(ImagingOutline outline, float x1, float y1, } int -ImagingOutlineClose(ImagingOutline outline) -{ - if (outline->x == outline->x0 && outline->y == outline->y0) +ImagingOutlineClose(ImagingOutline outline) { + if (outline->x == outline->x0 && outline->y == outline->y0) { return 0; + } return ImagingOutlineLine(outline, outline->x0, outline->y0); } int -ImagingOutlineTransform(ImagingOutline outline, double a[6]) -{ +ImagingOutlineTransform(ImagingOutline outline, double a[6]) { Edge *eIn; Edge *eOut; int i, n; int x0, y0, x1, y1; int X0, Y0, X1, Y1; - double a0 = a[0]; double a1 = a[1]; double a2 = a[2]; - double a3 = a[3]; double a4 = a[4]; double a5 = a[5]; + double a0 = a[0]; + double a1 = a[1]; + double a2 = a[2]; + double a3 = a[3]; + double a4 = a[4]; + double a5 = a[5]; eIn = outline->edges; n = outline->count; - /* FIXME: ugly! */ - outline->edges = NULL; - outline->count = outline->size = 0; - eOut = allocate(outline, n); if (!eOut) { - outline->edges = eIn; - outline->count = outline->size = n; ImagingError_MemoryError(); return -1; } for (i = 0; i < n; i++) { - x0 = eIn->x0; y0 = eIn->y0; /* FIXME: ouch! */ - if (eIn->x0 == eIn->xmin) + if (eIn->x0 == eIn->xmin) { x1 = eIn->xmax; - else + } else { x1 = eIn->xmin; - if (eIn->y0 == eIn->ymin) + } + if (eIn->y0 == eIn->ymin) { y1 = eIn->ymax; - else + } else { y1 = eIn->ymin; + } /* full moon tonight! if this doesn't work, you may need to upgrade your compiler (make sure you have the right service pack) */ - X0 = (int) (a0*x0 + a1*y0 + a2); - Y0 = (int) (a3*x0 + a4*y0 + a5); - X1 = (int) (a0*x1 + a1*y1 + a2); - Y1 = (int) (a3*x1 + a4*y1 + a5); + X0 = (int)(a0 * x0 + a1 * y0 + a2); + Y0 = (int)(a3 * x0 + a4 * y0 + a5); + X1 = (int)(a0 * x1 + a1 * y1 + a2); + Y1 = (int)(a3 * x1 + a4 * y1 + a5); add_edge(eOut, X0, Y0, X1, Y1); eIn++; eOut++; - } - free(eIn); + free(outline->edges); + + /* FIXME: ugly! */ + outline->edges = NULL; + outline->count = outline->size = 0; return 0; } int -ImagingDrawOutline(Imaging im, ImagingOutline outline, const void* ink_, - int fill, int op) -{ - DRAW* draw; +ImagingDrawOutline( + Imaging im, ImagingOutline outline, const void *ink_, int fill, int op) { + DRAW *draw; INT32 ink; DRAWINIT(); @@ -1102,5 +1947,3 @@ ImagingDrawOutline(Imaging im, ImagingOutline outline, const void* ink_, return 0; } - -#endif diff --git a/src/libImaging/Effects.c b/src/libImaging/Effects.c index 26b063a11f9..93e7af0bce9 100644 --- a/src/libImaging/Effects.c +++ b/src/libImaging/Effects.c @@ -15,14 +15,12 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" #include Imaging -ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality) -{ +ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality) { /* Generate a Mandelbrot set covering the given extent */ Imaging im; @@ -32,33 +30,35 @@ ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality) double dr, di; /* Check arguments */ - width = extent[2] - extent[0]; + width = extent[2] - extent[0]; height = extent[3] - extent[1]; - if (width < 0.0 || height < 0.0 || quality < 2) - return (Imaging) ImagingError_ValueError(NULL); + if (width < 0.0 || height < 0.0 || quality < 2) { + return (Imaging)ImagingError_ValueError(NULL); + } im = ImagingNewDirty("L", xsize, ysize); - if (!im) + if (!im) { return NULL; + } - dr = width/(xsize-1); - di = height/(ysize-1); + dr = width / (xsize - 1); + di = height / (ysize - 1); radius = 100.0; for (y = 0; y < ysize; y++) { - UINT8* buf = im->image8[y]; + UINT8 *buf = im->image8[y]; for (x = 0; x < xsize; x++) { x1 = y1 = xi2 = yi2 = 0.0; - cr = x*dr + extent[0]; - ci = y*di + extent[1]; + cr = x * dr + extent[0]; + ci = y * di + extent[1]; for (k = 1;; k++) { - y1 = 2*x1*y1 + ci; + y1 = 2 * x1 * y1 + ci; x1 = xi2 - yi2 + cr; - xi2 = x1*x1; - yi2 = y1*y1; + xi2 = x1 * x1; + yi2 = y1 * y1; if ((xi2 + yi2) > radius) { - buf[x] = k*255/quality; + buf[x] = k * 255 / quality; break; } if (k > quality) { @@ -72,8 +72,7 @@ ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality) } Imaging -ImagingEffectNoise(int xsize, int ysize, float sigma) -{ +ImagingEffectNoise(int xsize, int ysize, float sigma) { /* Generate Gaussian noise centered around 128 */ Imaging imOut; @@ -82,14 +81,15 @@ ImagingEffectNoise(int xsize, int ysize, float sigma) double this, next; imOut = ImagingNewDirty("L", xsize, ysize); - if (!imOut) + if (!imOut) { return NULL; + } next = 0.0; nextok = 0; for (y = 0; y < imOut->ysize; y++) { - UINT8* out = imOut->image8[y]; + UINT8 *out = imOut->image8[y]; for (x = 0; x < imOut->xsize; x++) { if (nextok) { this = next; @@ -98,15 +98,15 @@ ImagingEffectNoise(int xsize, int ysize, float sigma) /* after numerical recipes */ double v1, v2, radius, factor; do { - v1 = rand()*(2.0/RAND_MAX) - 1.0; - v2 = rand()*(2.0/RAND_MAX) - 1.0; - radius= v1*v1 + v2*v2; + v1 = rand() * (2.0 / RAND_MAX) - 1.0; + v2 = rand() * (2.0 / RAND_MAX) - 1.0; + radius = v1 * v1 + v2 * v2; } while (radius >= 1.0); - factor = sqrt(-2.0*log(radius)/radius); + factor = sqrt(-2.0 * log(radius) / radius); this = factor * v1; next = factor * v2; } - out[x] = (unsigned char) (128 + sigma * this); + out[x] = CLIP8(128 + sigma * this); } } @@ -114,8 +114,7 @@ ImagingEffectNoise(int xsize, int ysize, float sigma) } Imaging -ImagingEffectSpread(Imaging imIn, int distance) -{ +ImagingEffectSpread(Imaging imIn, int distance) { /* Randomly spread pixels in an image */ Imaging imOut; @@ -123,20 +122,31 @@ ImagingEffectSpread(Imaging imIn, int distance) imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); - if (!imOut) + if (!imOut) { return NULL; + } -#define SPREAD(type, image)\ - for (y = 0; y < imOut->ysize; y++)\ - for (x = 0; x < imOut->xsize; x++) {\ - int xx = x + (rand() % distance) - distance/2;\ - int yy = y + (rand() % distance) - distance/2;\ - if (xx >= 0 && xx < imIn->xsize && yy >= 0 && yy < imIn->ysize) {\ - imOut->image[yy][xx] = imIn->image[y][x];\ - imOut->image[y][x] = imIn->image[yy][xx];\ - } else\ - imOut->image[y][x] = imIn->image[y][x];\ - } +#define SPREAD(type, image) \ + if (distance == 0) { \ + for (y = 0; y < imOut->ysize; y++) { \ + for (x = 0; x < imOut->xsize; x++) { \ + imOut->image[y][x] = imIn->image[y][x]; \ + } \ + } \ + } else { \ + for (y = 0; y < imOut->ysize; y++) { \ + for (x = 0; x < imOut->xsize; x++) { \ + int xx = x + (rand() % distance) - distance / 2; \ + int yy = y + (rand() % distance) - distance / 2; \ + if (xx >= 0 && xx < imIn->xsize && yy >= 0 && yy < imIn->ysize) { \ + imOut->image[yy][xx] = imIn->image[y][x]; \ + imOut->image[y][x] = imIn->image[yy][xx]; \ + } else { \ + imOut->image[y][x] = imIn->image[y][x]; \ + } \ + } \ + } \ + } if (imIn->image8) { SPREAD(UINT8, image8); diff --git a/src/libImaging/EpsEncode.c b/src/libImaging/EpsEncode.c index 45fab0a6ed4..3f2cb33b2d2 100644 --- a/src/libImaging/EpsEncode.c +++ b/src/libImaging/EpsEncode.c @@ -5,11 +5,11 @@ * encoder for EPS hex data * * history: - * 96-04-19 fl created - * 96-06-27 fl don't drop last block of encoded data + * 96-04-19 fl created + * 96-06-27 fl don't drop last block of encoded data * * notes: - * FIXME: rename to HexEncode.c ?? + * FIXME: rename to HexEncode.c ?? * * Copyright (c) Fredrik Lundh 1996. * Copyright (c) Secret Labs AB 1997. @@ -17,64 +17,61 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" - int -ImagingEpsEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ - enum { HEXBYTE=1, NEWLINE }; +ImagingEpsEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + enum { HEXBYTE = 1, NEWLINE }; const char *hex = "0123456789abcdef"; - UINT8* ptr = buf; - UINT8* in, i; + UINT8 *ptr = buf; + UINT8 *in, i; if (!state->state) { - state->state = HEXBYTE; - state->xsize *= im->pixelsize; /* Hack! */ + state->state = HEXBYTE; + state->xsize *= im->pixelsize; /* Hack! */ } - in = (UINT8*) im->image[state->y]; + in = (UINT8 *)im->image[state->y]; for (;;) { - - if (state->state == NEWLINE) { - if (bytes < 1) - break; - *ptr++ = '\n'; - bytes--; - state->state = HEXBYTE; - } - - if (bytes < 2) - break; - - i = in[state->x++]; - *ptr++ = hex[(i>>4)&15]; - *ptr++ = hex[i&15]; - bytes -= 2; - - /* Skip junk bytes */ - if (im->bands == 3 && (state->x & 3) == 3) - state->x++; - - if (++state->count >= 79/2) { - state->state = NEWLINE; - state->count = 0; - } - - if (state->x >= state->xsize) { - state->x = 0; - if (++state->y >= state->ysize) { - state->errcode = IMAGING_CODEC_END; - break; - } - in = (UINT8*) im->image[state->y]; - } - + if (state->state == NEWLINE) { + if (bytes < 1) { + break; + } + *ptr++ = '\n'; + bytes--; + state->state = HEXBYTE; + } + + if (bytes < 2) { + break; + } + + i = in[state->x++]; + *ptr++ = hex[(i >> 4) & 15]; + *ptr++ = hex[i & 15]; + bytes -= 2; + + /* Skip junk bytes */ + if (im->bands == 3 && (state->x & 3) == 3) { + state->x++; + } + + if (++state->count >= 79 / 2) { + state->state = NEWLINE; + state->count = 0; + } + + if (state->x >= state->xsize) { + state->x = 0; + if (++state->y >= state->ysize) { + state->errcode = IMAGING_CODEC_END; + break; + } + in = (UINT8 *)im->image[state->y]; + } } return ptr - buf; - } diff --git a/src/libImaging/Except.c b/src/libImaging/Except.c index 515d85d1fe9..f42ff9aec9e 100644 --- a/src/libImaging/Except.c +++ b/src/libImaging/Except.c @@ -19,65 +19,54 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" - /* exception state */ void * -ImagingError_IOError(void) -{ +ImagingError_OSError(void) { fprintf(stderr, "*** exception: file access error\n"); return NULL; } void * -ImagingError_MemoryError(void) -{ +ImagingError_MemoryError(void) { fprintf(stderr, "*** exception: out of memory\n"); return NULL; } void * -ImagingError_ModeError(void) -{ +ImagingError_ModeError(void) { return ImagingError_ValueError("bad image mode"); - return NULL; } void * -ImagingError_Mismatch(void) -{ +ImagingError_Mismatch(void) { return ImagingError_ValueError("images don't match"); - return NULL; } void * -ImagingError_ValueError(const char *message) -{ - if (!message) - message = "exception: bad argument to function"; +ImagingError_ValueError(const char *message) { + if (!message) { + message = "exception: bad argument to function"; + } fprintf(stderr, "*** %s\n", message); return NULL; } void -ImagingError_Clear(void) -{ +ImagingError_Clear(void) { /* nop */; } /* thread state */ void -ImagingSectionEnter(ImagingSectionCookie* cookie) -{ +ImagingSectionEnter(ImagingSectionCookie *cookie) { /* pass */ } void -ImagingSectionLeave(ImagingSectionCookie* cookie) -{ +ImagingSectionLeave(ImagingSectionCookie *cookie) { /* pass */ } diff --git a/src/libImaging/File.c b/src/libImaging/File.c index d67bcabde94..76d0abccc4f 100644 --- a/src/libImaging/File.c +++ b/src/libImaging/File.c @@ -15,51 +15,46 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" #include - int -ImagingSaveRaw(Imaging im, FILE* fp) -{ +ImagingSaveRaw(Imaging im, FILE *fp) { int x, y, i; if (strcmp(im->mode, "1") == 0 || strcmp(im->mode, "L") == 0) { - /* @PIL227: FIXME: for mode "1", map != 0 to 255 */ /* PGM "L" */ - for (y = 0; y < im->ysize; y++) + for (y = 0; y < im->ysize; y++) { fwrite(im->image[y], 1, im->xsize, fp); + } } else { - /* PPM "RGB" or other internal format */ - for (y = 0; y < im->ysize; y++) - for (x = i = 0; x < im->xsize; x++, i += im->pixelsize) - fwrite(im->image[y]+i, 1, im->bands, fp); - + for (y = 0; y < im->ysize; y++) { + for (x = i = 0; x < im->xsize; x++, i += im->pixelsize) { + fwrite(im->image[y] + i, 1, im->bands, fp); + } + } } return 1; } - int -ImagingSavePPM(Imaging im, const char* outfile) -{ - FILE* fp; +ImagingSavePPM(Imaging im, const char *outfile) { + FILE *fp; if (!im) { - (void) ImagingError_ValueError(NULL); + (void)ImagingError_ValueError(NULL); return 0; } fp = fopen(outfile, "wb"); if (!fp) { - (void) ImagingError_IOError(); + (void)ImagingError_OSError(); return 0; } @@ -70,7 +65,8 @@ ImagingSavePPM(Imaging im, const char* outfile) /* Write "PPM" */ fprintf(fp, "P6\n%d %d\n255\n", im->xsize, im->ysize); } else { - (void) ImagingError_ModeError(); + fclose(fp); + (void)ImagingError_ModeError(); return 0; } @@ -80,4 +76,3 @@ ImagingSavePPM(Imaging im, const char* outfile) return 1; } - diff --git a/src/libImaging/Fill.c b/src/libImaging/Fill.c index d641a59962a..f7206022843 100644 --- a/src/libImaging/Fill.c +++ b/src/libImaging/Fill.c @@ -5,9 +5,9 @@ * fill image with constant pixel value * * history: - * 95-11-26 fl moved from Imaging.c - * 96-05-17 fl added radial fill, renamed wedge to linear - * 98-06-23 fl changed ImageFill signature + * 95-11-26 fl moved from Imaging.c + * 96-05-17 fl added radial fill, renamed wedge to linear + * 98-06-23 fl changed ImageFill signature * * Copyright (c) Secret Labs AB 1997-98. All rights reserved. * Copyright (c) Fredrik Lundh 1995-96. @@ -15,14 +15,12 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" #include "math.h" Imaging -ImagingFill(Imaging im, const void* colour) -{ +ImagingFill(Imaging im, const void *colour) { int x, y; ImagingSectionCookie cookie; @@ -30,27 +28,33 @@ ImagingFill(Imaging im, const void* colour) /* use generic API */ ImagingAccess access = ImagingAccessNew(im); if (access) { - for (y = 0; y < im->ysize; y++) - for (x = 0; x < im->xsize; x++) + for (y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++) { access->put_pixel(im, x, y, colour); + } + } ImagingAccessDelete(im, access); } else { /* wipe the image */ - for (y = 0; y < im->ysize; y++) + for (y = 0; y < im->ysize; y++) { memset(im->image[y], 0, im->linesize); + } } } else { INT32 c = 0L; ImagingSectionEnter(&cookie); memcpy(&c, colour, im->pixelsize); if (im->image32 && c != 0L) { - for (y = 0; y < im->ysize; y++) - for (x = 0; x < im->xsize; x++) + for (y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++) { im->image32[y][x] = c; + } + } } else { - unsigned char cc = (unsigned char) *(UINT8*) colour; - for (y = 0; y < im->ysize; y++) + unsigned char cc = (unsigned char)*(UINT8 *)colour; + for (y = 0; y < im->ysize; y++) { memset(im->image[y], cc, im->linesize); + } } ImagingSectionLeave(&cookie); } @@ -59,13 +63,12 @@ ImagingFill(Imaging im, const void* colour) } Imaging -ImagingFillLinearGradient(const char *mode) -{ +ImagingFillLinearGradient(const char *mode) { Imaging im; int y; if (strlen(mode) != 1) { - return (Imaging) ImagingError_ModeError(); + return (Imaging)ImagingError_ModeError(); } im = ImagingNewDirty(mode, 256, 256); @@ -73,22 +76,34 @@ ImagingFillLinearGradient(const char *mode) return NULL; } - for (y = 0; y < 256; y++) { - memset(im->image8[y], (unsigned char) y, 256); + if (im->image8) { + for (y = 0; y < 256; y++) { + memset(im->image8[y], (unsigned char)y, 256); + } + } else { + int x; + for (y = 0; y < 256; y++) { + for (x = 0; x < 256; x++) { + if (im->type == IMAGING_TYPE_FLOAT32) { + IMAGING_PIXEL_FLOAT32(im, x, y) = y; + } else { + IMAGING_PIXEL_INT32(im, x, y) = y; + } + } + } } return im; } Imaging -ImagingFillRadialGradient(const char *mode) -{ +ImagingFillRadialGradient(const char *mode) { Imaging im; int x, y; int d; if (strlen(mode) != 1) { - return (Imaging) ImagingError_ModeError(); + return (Imaging)ImagingError_ModeError(); } im = ImagingNewDirty(mode, 256, 256); @@ -98,11 +113,19 @@ ImagingFillRadialGradient(const char *mode) for (y = 0; y < 256; y++) { for (x = 0; x < 256; x++) { - d = (int) sqrt((double) ((x-128)*(x-128) + (y-128)*(y-128)) * 2.0); + d = (int)sqrt( + (double)((x - 128) * (x - 128) + (y - 128) * (y - 128)) * 2.0); if (d >= 255) { - im->image8[y][x] = 255; - } else { + d = 255; + } + if (im->image8) { im->image8[y][x] = d; + } else { + if (im->type == IMAGING_TYPE_FLOAT32) { + IMAGING_PIXEL_FLOAT32(im, x, y) = d; + } else { + IMAGING_PIXEL_INT32(im, x, y) = d; + } } } } diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index 6e4a0050171..fab3b494819 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -26,48 +26,58 @@ #include "Imaging.h" - -static inline UINT8 clip8(float in) -{ - if (in <= 0.0) +static inline UINT8 +clip8(float in) { + if (in <= 0.0) { return 0; - if (in >= 255.0) + } + if (in >= 255.0) { return 255; - return (UINT8) in; + } + return (UINT8)in; } Imaging -ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) -{ +ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { Imaging imOut; int x, y; ImagingSectionCookie cookie; - if (xmargin < 0 && ymargin < 0) - return (Imaging) ImagingError_ValueError("bad kernel size"); + if (xmargin < 0 && ymargin < 0) { + return (Imaging)ImagingError_ValueError("bad kernel size"); + } imOut = ImagingNewDirty( - imIn->mode, imIn->xsize+2*xmargin, imIn->ysize+2*ymargin); - if (!imOut) + imIn->mode, imIn->xsize + 2 * xmargin, imIn->ysize + 2 * ymargin); + if (!imOut) { return NULL; + } -#define EXPAND_LINE(type, image, yin, yout) {\ - for (x = 0; x < xmargin; x++)\ - imOut->image[yout][x] = imIn->image[yin][0];\ - for (x = 0; x < imIn->xsize; x++)\ - imOut->image[yout][x+xmargin] = imIn->image[yin][x];\ - for (x = 0; x < xmargin; x++)\ - imOut->image[yout][xmargin+imIn->xsize+x] =\ - imIn->image[yin][imIn->xsize-1];\ +#define EXPAND_LINE(type, image, yin, yout) \ + { \ + for (x = 0; x < xmargin; x++) { \ + imOut->image[yout][x] = imIn->image[yin][0]; \ + } \ + for (x = 0; x < imIn->xsize; x++) { \ + imOut->image[yout][x + xmargin] = imIn->image[yin][x]; \ + } \ + for (x = 0; x < xmargin; x++) { \ + imOut->image[yout][xmargin + imIn->xsize + x] = \ + imIn->image[yin][imIn->xsize - 1]; \ + } \ } -#define EXPAND(type, image) {\ - for (y = 0; y < ymargin; y++)\ - EXPAND_LINE(type, image, 0, y);\ - for (y = 0; y < imIn->ysize; y++)\ - EXPAND_LINE(type, image, y, y+ymargin);\ - for (y = 0; y < ymargin; y++)\ - EXPAND_LINE(type, image, imIn->ysize-1, ymargin+imIn->ysize+y);\ +#define EXPAND(type, image) \ + { \ + for (y = 0; y < ymargin; y++) { \ + EXPAND_LINE(type, image, 0, y); \ + } \ + for (y = 0; y < imIn->ysize; y++) { \ + EXPAND_LINE(type, image, y, y + ymargin); \ + } \ + for (y = 0; y < ymargin; y++) { \ + EXPAND_LINE(type, image, imIn->ysize - 1, ymargin + imIn->ysize + y); \ + } \ } ImagingSectionEnter(&cookie); @@ -83,15 +93,11 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) return imOut; } - void -ImagingFilter3x3(Imaging imOut, Imaging im, const float* kernel, - float offset) -{ -#define KERNEL1x3(in0, x, kernel, d) ( \ - _i2f((UINT8) in0[x-d]) * (kernel)[0] + \ - _i2f((UINT8) in0[x]) * (kernel)[1] + \ - _i2f((UINT8) in0[x+d]) * (kernel)[2]) +ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { +#define KERNEL1x3(in0, x, kernel, d) \ + (_i2f((UINT8)in0[x - d]) * (kernel)[0] + _i2f((UINT8)in0[x]) * (kernel)[1] + \ + _i2f((UINT8)in0[x + d]) * (kernel)[2]) int x = 0, y = 0; @@ -99,100 +105,100 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float* kernel, if (im->bands == 1) { // Add one time for rounding offset += 0.5; - for (y = 1; y < im->ysize-1; y++) { - UINT8* in_1 = (UINT8*) im->image[y-1]; - UINT8* in0 = (UINT8*) im->image[y]; - UINT8* in1 = (UINT8*) im->image[y+1]; - UINT8* out = (UINT8*) imOut->image[y]; + for (y = 1; y < im->ysize - 1; y++) { + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *out = (UINT8 *)imOut->image[y]; out[0] = in0[0]; - for (x = 1; x < im->xsize-1; x++) { + for (x = 1; x < im->xsize - 1; x++) { float ss = offset; ss += KERNEL1x3(in1, x, &kernel[0], 1); ss += KERNEL1x3(in0, x, &kernel[3], 1); ss += KERNEL1x3(in_1, x, &kernel[6], 1); out[x] = clip8(ss); - } + } out[x] = in0[x]; } } else { // Add one time for rounding offset += 0.5; - for (y = 1; y < im->ysize-1; y++) { - UINT8* in_1 = (UINT8*) im->image[y-1]; - UINT8* in0 = (UINT8*) im->image[y]; - UINT8* in1 = (UINT8*) im->image[y+1]; - UINT32* out = (UINT32*) imOut->image[y]; + for (y = 1; y < im->ysize - 1; y++) { + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *out = (UINT8 *)imOut->image[y]; - out[0] = ((UINT32*) in0)[0]; + memcpy(out, in0, sizeof(UINT32)); if (im->bands == 2) { - for (x = 1; x < im->xsize-1; x++) { + for (x = 1; x < im->xsize - 1; x++) { float ss0 = offset; float ss3 = offset; - ss0 += KERNEL1x3(in1, x*4+0, &kernel[0], 4); - ss3 += KERNEL1x3(in1, x*4+3, &kernel[0], 4); - ss0 += KERNEL1x3(in0, x*4+0, &kernel[3], 4); - ss3 += KERNEL1x3(in0, x*4+3, &kernel[3], 4); - ss0 += KERNEL1x3(in_1, x*4+0, &kernel[6], 4); - ss3 += KERNEL1x3(in_1, x*4+3, &kernel[6], 4); - out[x] = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); + UINT32 v; + ss0 += KERNEL1x3(in1, x * 4 + 0, &kernel[0], 4); + ss3 += KERNEL1x3(in1, x * 4 + 3, &kernel[0], 4); + ss0 += KERNEL1x3(in0, x * 4 + 0, &kernel[3], 4); + ss3 += KERNEL1x3(in0, x * 4 + 3, &kernel[3], 4); + ss0 += KERNEL1x3(in_1, x * 4 + 0, &kernel[6], 4); + ss3 += KERNEL1x3(in_1, x * 4 + 3, &kernel[6], 4); + v = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); + memcpy(out + x * sizeof(v), &v, sizeof(v)); } } else if (im->bands == 3) { - for (x = 1; x < im->xsize-1; x++) { + for (x = 1; x < im->xsize - 1; x++) { float ss0 = offset; float ss1 = offset; float ss2 = offset; - ss0 += KERNEL1x3(in1, x*4+0, &kernel[0], 4); - ss1 += KERNEL1x3(in1, x*4+1, &kernel[0], 4); - ss2 += KERNEL1x3(in1, x*4+2, &kernel[0], 4); - ss0 += KERNEL1x3(in0, x*4+0, &kernel[3], 4); - ss1 += KERNEL1x3(in0, x*4+1, &kernel[3], 4); - ss2 += KERNEL1x3(in0, x*4+2, &kernel[3], 4); - ss0 += KERNEL1x3(in_1, x*4+0, &kernel[6], 4); - ss1 += KERNEL1x3(in_1, x*4+1, &kernel[6], 4); - ss2 += KERNEL1x3(in_1, x*4+2, &kernel[6], 4); - out[x] = MAKE_UINT32( - clip8(ss0), clip8(ss1), clip8(ss2), 0); + UINT32 v; + ss0 += KERNEL1x3(in1, x * 4 + 0, &kernel[0], 4); + ss1 += KERNEL1x3(in1, x * 4 + 1, &kernel[0], 4); + ss2 += KERNEL1x3(in1, x * 4 + 2, &kernel[0], 4); + ss0 += KERNEL1x3(in0, x * 4 + 0, &kernel[3], 4); + ss1 += KERNEL1x3(in0, x * 4 + 1, &kernel[3], 4); + ss2 += KERNEL1x3(in0, x * 4 + 2, &kernel[3], 4); + ss0 += KERNEL1x3(in_1, x * 4 + 0, &kernel[6], 4); + ss1 += KERNEL1x3(in_1, x * 4 + 1, &kernel[6], 4); + ss2 += KERNEL1x3(in_1, x * 4 + 2, &kernel[6], 4); + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), 0); + memcpy(out + x * sizeof(v), &v, sizeof(v)); } } else if (im->bands == 4) { - for (x = 1; x < im->xsize-1; x++) { + for (x = 1; x < im->xsize - 1; x++) { float ss0 = offset; float ss1 = offset; float ss2 = offset; float ss3 = offset; - ss0 += KERNEL1x3(in1, x*4+0, &kernel[0], 4); - ss1 += KERNEL1x3(in1, x*4+1, &kernel[0], 4); - ss2 += KERNEL1x3(in1, x*4+2, &kernel[0], 4); - ss3 += KERNEL1x3(in1, x*4+3, &kernel[0], 4); - ss0 += KERNEL1x3(in0, x*4+0, &kernel[3], 4); - ss1 += KERNEL1x3(in0, x*4+1, &kernel[3], 4); - ss2 += KERNEL1x3(in0, x*4+2, &kernel[3], 4); - ss3 += KERNEL1x3(in0, x*4+3, &kernel[3], 4); - ss0 += KERNEL1x3(in_1, x*4+0, &kernel[6], 4); - ss1 += KERNEL1x3(in_1, x*4+1, &kernel[6], 4); - ss2 += KERNEL1x3(in_1, x*4+2, &kernel[6], 4); - ss3 += KERNEL1x3(in_1, x*4+3, &kernel[6], 4); - out[x] = MAKE_UINT32( - clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + UINT32 v; + ss0 += KERNEL1x3(in1, x * 4 + 0, &kernel[0], 4); + ss1 += KERNEL1x3(in1, x * 4 + 1, &kernel[0], 4); + ss2 += KERNEL1x3(in1, x * 4 + 2, &kernel[0], 4); + ss3 += KERNEL1x3(in1, x * 4 + 3, &kernel[0], 4); + ss0 += KERNEL1x3(in0, x * 4 + 0, &kernel[3], 4); + ss1 += KERNEL1x3(in0, x * 4 + 1, &kernel[3], 4); + ss2 += KERNEL1x3(in0, x * 4 + 2, &kernel[3], 4); + ss3 += KERNEL1x3(in0, x * 4 + 3, &kernel[3], 4); + ss0 += KERNEL1x3(in_1, x * 4 + 0, &kernel[6], 4); + ss1 += KERNEL1x3(in_1, x * 4 + 1, &kernel[6], 4); + ss2 += KERNEL1x3(in_1, x * 4 + 2, &kernel[6], 4); + ss3 += KERNEL1x3(in_1, x * 4 + 3, &kernel[6], 4); + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + memcpy(out + x * sizeof(v), &v, sizeof(v)); } } - out[x] = ((UINT32*) in0)[x]; + memcpy(out + x * sizeof(UINT32), in0 + x * sizeof(UINT32), sizeof(UINT32)); } } memcpy(imOut->image[y], im->image[y], im->linesize); } - void -ImagingFilter5x5(Imaging imOut, Imaging im, const float* kernel, - float offset) -{ -#define KERNEL1x5(in0, x, kernel, d) ( \ - _i2f((UINT8) in0[x-d-d]) * (kernel)[0] + \ - _i2f((UINT8) in0[x-d]) * (kernel)[1] + \ - _i2f((UINT8) in0[x]) * (kernel)[2] + \ - _i2f((UINT8) in0[x+d]) * (kernel)[3] + \ - _i2f((UINT8) in0[x+d+d]) * (kernel)[4]) +ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { +#define KERNEL1x5(in0, x, kernel, d) \ + (_i2f((UINT8)in0[x - d - d]) * (kernel)[0] + \ + _i2f((UINT8)in0[x - d]) * (kernel)[1] + _i2f((UINT8)in0[x]) * (kernel)[2] + \ + _i2f((UINT8)in0[x + d]) * (kernel)[3] + \ + _i2f((UINT8)in0[x + d + d]) * (kernel)[4]) int x = 0, y = 0; @@ -201,17 +207,17 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float* kernel, if (im->bands == 1) { // Add one time for rounding offset += 0.5; - for (y = 2; y < im->ysize-2; y++) { - UINT8* in_2 = (UINT8*) im->image[y-2]; - UINT8* in_1 = (UINT8*) im->image[y-1]; - UINT8* in0 = (UINT8*) im->image[y]; - UINT8* in1 = (UINT8*) im->image[y+1]; - UINT8* in2 = (UINT8*) im->image[y+2]; - UINT8* out = (UINT8*) imOut->image[y]; + for (y = 2; y < im->ysize - 2; y++) { + UINT8 *in_2 = (UINT8 *)im->image[y - 2]; + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *in2 = (UINT8 *)im->image[y + 2]; + UINT8 *out = (UINT8 *)imOut->image[y]; out[0] = in0[0]; out[1] = in0[1]; - for (x = 2; x < im->xsize-2; x++) { + for (x = 2; x < im->xsize - 2; x++) { float ss = offset; ss += KERNEL1x5(in2, x, &kernel[0], 1); ss += KERNEL1x5(in1, x, &kernel[5], 1); @@ -220,118 +226,123 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float* kernel, ss += KERNEL1x5(in_2, x, &kernel[20], 1); out[x] = clip8(ss); } - out[x+0] = in0[x+0]; - out[x+1] = in0[x+1]; + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; } } else { // Add one time for rounding offset += 0.5; - for (y = 2; y < im->ysize-2; y++) { - UINT8* in_2 = (UINT8*) im->image[y-2]; - UINT8* in_1 = (UINT8*) im->image[y-1]; - UINT8* in0 = (UINT8*) im->image[y]; - UINT8* in1 = (UINT8*) im->image[y+1]; - UINT8* in2 = (UINT8*) im->image[y+2]; - UINT32* out = (UINT32*) imOut->image[y]; + for (y = 2; y < im->ysize - 2; y++) { + UINT8 *in_2 = (UINT8 *)im->image[y - 2]; + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *in2 = (UINT8 *)im->image[y + 2]; + UINT8 *out = (UINT8 *)imOut->image[y]; - out[0] = ((UINT32*) in0)[0]; - out[1] = ((UINT32*) in0)[1]; + memcpy(out, in0, sizeof(UINT32) * 2); if (im->bands == 2) { - for (x = 2; x < im->xsize-2; x++) { + for (x = 2; x < im->xsize - 2; x++) { float ss0 = offset; float ss3 = offset; - ss0 += KERNEL1x5(in2, x*4+0, &kernel[0], 4); - ss3 += KERNEL1x5(in2, x*4+3, &kernel[0], 4); - ss0 += KERNEL1x5(in1, x*4+0, &kernel[5], 4); - ss3 += KERNEL1x5(in1, x*4+3, &kernel[5], 4); - ss0 += KERNEL1x5(in0, x*4+0, &kernel[10], 4); - ss3 += KERNEL1x5(in0, x*4+3, &kernel[10], 4); - ss0 += KERNEL1x5(in_1, x*4+0, &kernel[15], 4); - ss3 += KERNEL1x5(in_1, x*4+3, &kernel[15], 4); - ss0 += KERNEL1x5(in_2, x*4+0, &kernel[20], 4); - ss3 += KERNEL1x5(in_2, x*4+3, &kernel[20], 4); - out[x] = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); + UINT32 v; + ss0 += KERNEL1x5(in2, x * 4 + 0, &kernel[0], 4); + ss3 += KERNEL1x5(in2, x * 4 + 3, &kernel[0], 4); + ss0 += KERNEL1x5(in1, x * 4 + 0, &kernel[5], 4); + ss3 += KERNEL1x5(in1, x * 4 + 3, &kernel[5], 4); + ss0 += KERNEL1x5(in0, x * 4 + 0, &kernel[10], 4); + ss3 += KERNEL1x5(in0, x * 4 + 3, &kernel[10], 4); + ss0 += KERNEL1x5(in_1, x * 4 + 0, &kernel[15], 4); + ss3 += KERNEL1x5(in_1, x * 4 + 3, &kernel[15], 4); + ss0 += KERNEL1x5(in_2, x * 4 + 0, &kernel[20], 4); + ss3 += KERNEL1x5(in_2, x * 4 + 3, &kernel[20], 4); + v = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); + memcpy(out + x * sizeof(v), &v, sizeof(v)); } } else if (im->bands == 3) { - for (x = 2; x < im->xsize-2; x++) { + for (x = 2; x < im->xsize - 2; x++) { float ss0 = offset; float ss1 = offset; float ss2 = offset; - ss0 += KERNEL1x5(in2, x*4+0, &kernel[0], 4); - ss1 += KERNEL1x5(in2, x*4+1, &kernel[0], 4); - ss2 += KERNEL1x5(in2, x*4+2, &kernel[0], 4); - ss0 += KERNEL1x5(in1, x*4+0, &kernel[5], 4); - ss1 += KERNEL1x5(in1, x*4+1, &kernel[5], 4); - ss2 += KERNEL1x5(in1, x*4+2, &kernel[5], 4); - ss0 += KERNEL1x5(in0, x*4+0, &kernel[10], 4); - ss1 += KERNEL1x5(in0, x*4+1, &kernel[10], 4); - ss2 += KERNEL1x5(in0, x*4+2, &kernel[10], 4); - ss0 += KERNEL1x5(in_1, x*4+0, &kernel[15], 4); - ss1 += KERNEL1x5(in_1, x*4+1, &kernel[15], 4); - ss2 += KERNEL1x5(in_1, x*4+2, &kernel[15], 4); - ss0 += KERNEL1x5(in_2, x*4+0, &kernel[20], 4); - ss1 += KERNEL1x5(in_2, x*4+1, &kernel[20], 4); - ss2 += KERNEL1x5(in_2, x*4+2, &kernel[20], 4); - out[x] = MAKE_UINT32( - clip8(ss0), clip8(ss1), clip8(ss2), 0); + UINT32 v; + ss0 += KERNEL1x5(in2, x * 4 + 0, &kernel[0], 4); + ss1 += KERNEL1x5(in2, x * 4 + 1, &kernel[0], 4); + ss2 += KERNEL1x5(in2, x * 4 + 2, &kernel[0], 4); + ss0 += KERNEL1x5(in1, x * 4 + 0, &kernel[5], 4); + ss1 += KERNEL1x5(in1, x * 4 + 1, &kernel[5], 4); + ss2 += KERNEL1x5(in1, x * 4 + 2, &kernel[5], 4); + ss0 += KERNEL1x5(in0, x * 4 + 0, &kernel[10], 4); + ss1 += KERNEL1x5(in0, x * 4 + 1, &kernel[10], 4); + ss2 += KERNEL1x5(in0, x * 4 + 2, &kernel[10], 4); + ss0 += KERNEL1x5(in_1, x * 4 + 0, &kernel[15], 4); + ss1 += KERNEL1x5(in_1, x * 4 + 1, &kernel[15], 4); + ss2 += KERNEL1x5(in_1, x * 4 + 2, &kernel[15], 4); + ss0 += KERNEL1x5(in_2, x * 4 + 0, &kernel[20], 4); + ss1 += KERNEL1x5(in_2, x * 4 + 1, &kernel[20], 4); + ss2 += KERNEL1x5(in_2, x * 4 + 2, &kernel[20], 4); + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), 0); + memcpy(out + x * sizeof(v), &v, sizeof(v)); } } else if (im->bands == 4) { - for (x = 2; x < im->xsize-2; x++) { + for (x = 2; x < im->xsize - 2; x++) { float ss0 = offset; float ss1 = offset; float ss2 = offset; float ss3 = offset; - ss0 += KERNEL1x5(in2, x*4+0, &kernel[0], 4); - ss1 += KERNEL1x5(in2, x*4+1, &kernel[0], 4); - ss2 += KERNEL1x5(in2, x*4+2, &kernel[0], 4); - ss3 += KERNEL1x5(in2, x*4+3, &kernel[0], 4); - ss0 += KERNEL1x5(in1, x*4+0, &kernel[5], 4); - ss1 += KERNEL1x5(in1, x*4+1, &kernel[5], 4); - ss2 += KERNEL1x5(in1, x*4+2, &kernel[5], 4); - ss3 += KERNEL1x5(in1, x*4+3, &kernel[5], 4); - ss0 += KERNEL1x5(in0, x*4+0, &kernel[10], 4); - ss1 += KERNEL1x5(in0, x*4+1, &kernel[10], 4); - ss2 += KERNEL1x5(in0, x*4+2, &kernel[10], 4); - ss3 += KERNEL1x5(in0, x*4+3, &kernel[10], 4); - ss0 += KERNEL1x5(in_1, x*4+0, &kernel[15], 4); - ss1 += KERNEL1x5(in_1, x*4+1, &kernel[15], 4); - ss2 += KERNEL1x5(in_1, x*4+2, &kernel[15], 4); - ss3 += KERNEL1x5(in_1, x*4+3, &kernel[15], 4); - ss0 += KERNEL1x5(in_2, x*4+0, &kernel[20], 4); - ss1 += KERNEL1x5(in_2, x*4+1, &kernel[20], 4); - ss2 += KERNEL1x5(in_2, x*4+2, &kernel[20], 4); - ss3 += KERNEL1x5(in_2, x*4+3, &kernel[20], 4); - out[x] = MAKE_UINT32( - clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + UINT32 v; + ss0 += KERNEL1x5(in2, x * 4 + 0, &kernel[0], 4); + ss1 += KERNEL1x5(in2, x * 4 + 1, &kernel[0], 4); + ss2 += KERNEL1x5(in2, x * 4 + 2, &kernel[0], 4); + ss3 += KERNEL1x5(in2, x * 4 + 3, &kernel[0], 4); + ss0 += KERNEL1x5(in1, x * 4 + 0, &kernel[5], 4); + ss1 += KERNEL1x5(in1, x * 4 + 1, &kernel[5], 4); + ss2 += KERNEL1x5(in1, x * 4 + 2, &kernel[5], 4); + ss3 += KERNEL1x5(in1, x * 4 + 3, &kernel[5], 4); + ss0 += KERNEL1x5(in0, x * 4 + 0, &kernel[10], 4); + ss1 += KERNEL1x5(in0, x * 4 + 1, &kernel[10], 4); + ss2 += KERNEL1x5(in0, x * 4 + 2, &kernel[10], 4); + ss3 += KERNEL1x5(in0, x * 4 + 3, &kernel[10], 4); + ss0 += KERNEL1x5(in_1, x * 4 + 0, &kernel[15], 4); + ss1 += KERNEL1x5(in_1, x * 4 + 1, &kernel[15], 4); + ss2 += KERNEL1x5(in_1, x * 4 + 2, &kernel[15], 4); + ss3 += KERNEL1x5(in_1, x * 4 + 3, &kernel[15], 4); + ss0 += KERNEL1x5(in_2, x * 4 + 0, &kernel[20], 4); + ss1 += KERNEL1x5(in_2, x * 4 + 1, &kernel[20], 4); + ss2 += KERNEL1x5(in_2, x * 4 + 2, &kernel[20], 4); + ss3 += KERNEL1x5(in_2, x * 4 + 3, &kernel[20], 4); + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + memcpy(out + x * sizeof(v), &v, sizeof(v)); } } - out[x] = ((UINT32*) in0)[x]; - out[x+1] = ((UINT32*) in0)[x+1]; + memcpy( + out + x * sizeof(UINT32), in0 + x * sizeof(UINT32), sizeof(UINT32) * 2); } } memcpy(imOut->image[y], im->image[y], im->linesize); - memcpy(imOut->image[y+1], im->image[y+1], im->linesize); + memcpy(imOut->image[y + 1], im->image[y + 1], im->linesize); } Imaging -ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32* kernel, - FLOAT32 offset) -{ +ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 offset) { Imaging imOut; ImagingSectionCookie cookie; - if ( ! im || im->type != IMAGING_TYPE_UINT8) - return (Imaging) ImagingError_ModeError(); + if (!im || im->type != IMAGING_TYPE_UINT8) { + return (Imaging)ImagingError_ModeError(); + } - if (im->xsize < xsize || im->ysize < ysize) + if (im->xsize < xsize || im->ysize < ysize) { return ImagingCopy(im); + } - if ((xsize != 3 && xsize != 5) || xsize != ysize) - return (Imaging) ImagingError_ValueError("bad kernel size"); + if ((xsize != 3 && xsize != 5) || xsize != ysize) { + return (Imaging)ImagingError_ValueError("bad kernel size"); + } imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); - if (!imOut) + if (!imOut) { return NULL; + } ImagingSectionEnter(&cookie); if (xsize == 3) { @@ -344,4 +355,3 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32* kernel, ImagingSectionLeave(&cookie); return imOut; } - diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c index 6d22c6c4ea8..d6e4ea0ff9d 100644 --- a/src/libImaging/FliDecode.c +++ b/src/libImaging/FliDecode.c @@ -5,8 +5,8 @@ * decoder for Autodesk Animator FLI/FLC animations * * history: - * 97-01-03 fl Created - * 97-01-17 fl Added SS2 support (FLC) + * 97-01-03 fl Created + * 97-01-17 fl Added SS2 support (FLC) * * Copyright (c) Fredrik Lundh 1997. * Copyright (c) Secret Labs AB 1997. @@ -14,191 +14,254 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" +#define I16(ptr) ((ptr)[0] + ((ptr)[1] << 8)) -#define I16(ptr)\ - ((ptr)[0] + ((ptr)[1] << 8)) - -#define I32(ptr)\ - ((ptr)[0] + ((ptr)[1] << 8) + ((ptr)[2] << 16) + ((ptr)[3] << 24)) +#define I32(ptr) ((ptr)[0] + ((ptr)[1] << 8) + ((ptr)[2] << 16) + ((ptr)[3] << 24)) +#define ERR_IF_DATA_OOB(offset) \ + if ((data + (offset)) > ptr + bytes) { \ + state->errcode = IMAGING_CODEC_OVERRUN; \ + return -1; \ + } int -ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ - UINT8* ptr; +ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + UINT8 *ptr; int framesize; - int c, chunks; + int c, chunks, advance; int l, lines; int i, j, x = 0, y, ymax; /* If not even the chunk size is present, we'd better leave */ - if (bytes < 4) - return 0; + if (bytes < 4) { + return 0; + } /* We don't decode anything unless we have a full chunk in the - input buffer (on the other hand, the Python part of the driver - makes sure this is always the case) */ + input buffer */ ptr = buf; framesize = I32(ptr); - if (framesize < I32(ptr)) - return 0; + // there can be one pad byte in the framesize + if (bytes + (bytes % 2) < framesize) { + return 0; + } /* Make sure this is a frame chunk. The Python driver takes case of other chunk types. */ - if (I16(ptr+4) != 0xF1FA) { - state->errcode = IMAGING_CODEC_UNKNOWN; - return -1; + if (bytes < 8) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + if (I16(ptr + 4) != 0xF1FA) { + state->errcode = IMAGING_CODEC_UNKNOWN; + return -1; } - chunks = I16(ptr+6); + chunks = I16(ptr + 6); ptr += 16; + bytes -= 16; /* Process subchunks */ for (c = 0; c < chunks; c++) { - UINT8 *data = ptr + 6; - switch (I16(ptr+4)) { - case 4: case 11: - /* FLI COLOR chunk */ - break; /* ignored; handled by Python code */ - case 7: - /* FLI SS2 chunk (word delta) */ - lines = I16(data); data += 2; - for (l = y = 0; l < lines && y < state->ysize; l++, y++) { - UINT8* buf = (UINT8*) im->image[y]; - int p, packets; - packets = I16(data); data += 2; - while (packets & 0x8000) { - /* flag word */ - if (packets & 0x4000) { - y += 65536 - packets; /* skip lines */ - if (y >= state->ysize) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - buf = (UINT8*) im->image[y]; - } else { - /* store last byte (used if line width is odd) */ - buf[state->xsize-1] = (UINT8) packets; - } - packets = I16(data); data += 2; - } - for (p = x = 0; p < packets; p++) { - x += data[0]; /* pixel skip */ - if (data[1] >= 128) { - i = 256-data[1]; /* run */ - if (x + i + i > state->xsize) - break; - for (j = 0; j < i; j++) { - buf[x++] = data[2]; - buf[x++] = data[3]; - } - data += 2 + 2; - } else { - i = 2 * (int) data[1]; /* chunk */ - if (x + i > state->xsize) - break; - memcpy(buf + x, data + 2, i); - data += 2 + i; - x += i; - } - } - if (p < packets) - break; /* didn't process all packets */ - } - if (l < lines) { - /* didn't process all lines */ - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - break; - case 12: - /* FLI LC chunk (byte delta) */ - y = I16(data); ymax = y + I16(data+2); data += 4; - for (; y < ymax && y < state->ysize; y++) { - UINT8* out = (UINT8*) im->image[y]; - int p, packets = *data++; - for (p = x = 0; p < packets; p++, x += i) { - x += data[0]; /* skip pixels */ - if (data[1] & 0x80) { - i = 256-data[1]; /* run */ - if (x + i > state->xsize) - break; - memset(out + x, data[2], i); - data += 3; - } else { - i = data[1]; /* chunk */ - if (x + i > state->xsize) - break; - memcpy(out + x, data + 2, i); - data += i + 2; - } - } - if (p < packets) - break; /* didn't process all packets */ - } - if (y < ymax) { - /* didn't process all lines */ - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - break; - case 13: - /* FLI BLACK chunk */ - for (y = 0; y < state->ysize; y++) - memset(im->image[y], 0, state->xsize); - break; - case 15: - /* FLI BRUN chunk */ - for (y = 0; y < state->ysize; y++) { - UINT8* out = (UINT8*) im->image[y]; - data += 1; /* ignore packetcount byte */ - for (x = 0; x < state->xsize; x += i) { - if (data[0] & 0x80) { - i = 256 - data[0]; - if (x + i > state->xsize) - break; /* safety first */ - memcpy(out + x, data + 1, i); - data += i + 1; - } else { - i = data[0]; - if (x + i > state->xsize) - break; /* safety first */ - memset(out + x, data[1], i); - data += 2; - } - } - if (x != state->xsize) { - /* didn't unpack whole line */ - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - } - break; - case 16: - /* COPY chunk */ - for (y = 0; y < state->ysize; y++) { - UINT8* buf = (UINT8*) im->image[y]; - memcpy(buf, data, state->xsize); - data += state->xsize; - } - break; - case 18: - /* PSTAMP chunk */ - break; /* ignored */ - default: - /* unknown chunk */ - /* printf("unknown FLI/FLC chunk: %d\n", I16(ptr+4)); */ - state->errcode = IMAGING_CODEC_UNKNOWN; - return -1; - } - ptr += I32(ptr); + UINT8 *data; + if (bytes < 10) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + data = ptr + 6; + switch (I16(ptr + 4)) { + case 4: + case 11: + /* FLI COLOR chunk */ + break; /* ignored; handled by Python code */ + case 7: + /* FLI SS2 chunk (word delta) */ + /* OOB ok, we've got 4 bytes min on entry */ + lines = I16(data); + data += 2; + for (l = y = 0; l < lines && y < state->ysize; l++, y++) { + UINT8 *local_buf = (UINT8 *)im->image[y]; + int p, packets; + ERR_IF_DATA_OOB(2) + packets = I16(data); + data += 2; + while (packets & 0x8000) { + /* flag word */ + if (packets & 0x4000) { + y += 65536 - packets; /* skip lines */ + if (y >= state->ysize) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + local_buf = (UINT8 *)im->image[y]; + } else { + /* store last byte (used if line width is odd) */ + local_buf[state->xsize - 1] = (UINT8)packets; + } + ERR_IF_DATA_OOB(2) + packets = I16(data); + data += 2; + } + for (p = x = 0; p < packets; p++) { + ERR_IF_DATA_OOB(2) + x += data[0]; /* pixel skip */ + if (data[1] >= 128) { + ERR_IF_DATA_OOB(4) + i = 256 - data[1]; /* run */ + if (x + i + i > state->xsize) { + break; + } + for (j = 0; j < i; j++) { + local_buf[x++] = data[2]; + local_buf[x++] = data[3]; + } + data += 2 + 2; + } else { + i = 2 * (int)data[1]; /* chunk */ + if (x + i > state->xsize) { + break; + } + ERR_IF_DATA_OOB(2 + i) + memcpy(local_buf + x, data + 2, i); + data += 2 + i; + x += i; + } + } + if (p < packets) { + break; /* didn't process all packets */ + } + } + if (l < lines) { + /* didn't process all lines */ + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + break; + case 12: + /* FLI LC chunk (byte delta) */ + /* OOB Check ok, we have 4 bytes min here */ + y = I16(data); + ymax = y + I16(data + 2); + data += 4; + for (; y < ymax && y < state->ysize; y++) { + UINT8 *out = (UINT8 *)im->image[y]; + ERR_IF_DATA_OOB(1) + int p, packets = *data++; + for (p = x = 0; p < packets; p++, x += i) { + ERR_IF_DATA_OOB(2) + x += data[0]; /* skip pixels */ + if (data[1] & 0x80) { + i = 256 - data[1]; /* run */ + if (x + i > state->xsize) { + break; + } + ERR_IF_DATA_OOB(3) + memset(out + x, data[2], i); + data += 3; + } else { + i = data[1]; /* chunk */ + if (x + i > state->xsize) { + break; + } + ERR_IF_DATA_OOB(2 + i) + memcpy(out + x, data + 2, i); + data += i + 2; + } + } + if (p < packets) { + break; /* didn't process all packets */ + } + } + if (y < ymax) { + /* didn't process all lines */ + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + break; + case 13: + /* FLI BLACK chunk */ + for (y = 0; y < state->ysize; y++) { + memset(im->image[y], 0, state->xsize); + } + break; + case 15: + /* FLI BRUN chunk */ + /* OOB, ok, we've got 4 bytes min on entry */ + for (y = 0; y < state->ysize; y++) { + UINT8 *out = (UINT8 *)im->image[y]; + data += 1; /* ignore packetcount byte */ + for (x = 0; x < state->xsize; x += i) { + ERR_IF_DATA_OOB(2) + if (data[0] & 0x80) { + i = 256 - data[0]; + if (x + i > state->xsize) { + break; /* safety first */ + } + ERR_IF_DATA_OOB(i + 1) + memcpy(out + x, data + 1, i); + data += i + 1; + } else { + i = data[0]; + if (x + i > state->xsize) { + break; /* safety first */ + } + memset(out + x, data[1], i); + data += 2; + } + } + if (x != state->xsize) { + /* didn't unpack whole line */ + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + } + break; + case 16: + /* COPY chunk */ + if (INT32_MAX / state->xsize < state->ysize) { + /* Integer overflow, bail */ + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + /* Note, have to check Data + size, not just ptr + size) */ + if (data + (state->xsize * state->ysize) > ptr + bytes) { + /* not enough data for frame */ + /* UNDONE Unclear that we're actually going to leave the buffer at the right place. */ + return ptr - buf; /* bytes consumed */ + } + for (y = 0; y < state->ysize; y++) { + UINT8 *local_buf = (UINT8 *)im->image[y]; + memcpy(local_buf, data, state->xsize); + data += state->xsize; + } + break; + case 18: + /* PSTAMP chunk */ + break; /* ignored */ + default: + /* unknown chunk */ + /* printf("unknown FLI/FLC chunk: %d\n", I16(ptr+4)); */ + state->errcode = IMAGING_CODEC_UNKNOWN; + return -1; + } + advance = I32(ptr); + if (advance == 0 ) { + // If there's no advance, we're in an infinite loop + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + if (advance < 0 || advance > bytes) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + ptr += advance; + bytes -= advance; } return -1; /* end of frame */ diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index 1d08728dad5..0c591579217 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -15,31 +15,37 @@ /* Transpose operations */ Imaging -ImagingFlipLeftRight(Imaging imOut, Imaging imIn) -{ +ImagingFlipLeftRight(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int x, y, xr; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); - if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) - return (Imaging) ImagingError_Mismatch(); + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { + return (Imaging)ImagingError_Mismatch(); + } ImagingCopyPalette(imOut, imIn); -#define FLIP_LEFT_RIGHT(INT, image) \ - for (y = 0; y < imIn->ysize; y++) { \ - INT* in = imIn->image[y]; \ - INT* out = imOut->image[y]; \ - xr = imIn->xsize-1; \ - for (x = 0; x < imIn->xsize; x++, xr--) \ - out[xr] = in[x]; \ +#define FLIP_LEFT_RIGHT(INT, image) \ + for (y = 0; y < imIn->ysize; y++) { \ + INT *in = (INT *)imIn->image[y]; \ + INT *out = (INT *)imOut->image[y]; \ + xr = imIn->xsize - 1; \ + for (x = 0; x < imIn->xsize; x++, xr--) { \ + out[xr] = in[x]; \ + } \ } ImagingSectionEnter(&cookie); if (imIn->image8) { - FLIP_LEFT_RIGHT(UINT8, image8) + if (strncmp(imIn->mode, "I;16", 4) == 0) { + FLIP_LEFT_RIGHT(UINT16, image8) + } else { + FLIP_LEFT_RIGHT(UINT8, image8) + } } else { FLIP_LEFT_RIGHT(INT32, image32) } @@ -51,73 +57,84 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn) return imOut; } - Imaging -ImagingFlipTopBottom(Imaging imOut, Imaging imIn) -{ +ImagingFlipTopBottom(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int y, yr; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); - if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) - return (Imaging) ImagingError_Mismatch(); + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { + return (Imaging)ImagingError_Mismatch(); + } ImagingCopyPalette(imOut, imIn); ImagingSectionEnter(&cookie); yr = imIn->ysize - 1; - for (y = 0; y < imIn->ysize; y++, yr--) + for (y = 0; y < imIn->ysize; y++, yr--) { memcpy(imOut->image[yr], imIn->image[y], imIn->linesize); + } ImagingSectionLeave(&cookie); return imOut; } - Imaging -ImagingRotate90(Imaging imOut, Imaging imIn) -{ +ImagingRotate90(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int x, y, xx, yy, xr, xxsize, yysize; int xxx, yyy, xxxsize, yyysize; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); - if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) - return (Imaging) ImagingError_Mismatch(); + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { + return (Imaging)ImagingError_Mismatch(); + } ImagingCopyPalette(imOut, imIn); -#define ROTATE_90(INT, image) \ - for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ - for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ +#define ROTATE_90(INT, image) \ + for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ + for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ - for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ - for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ - yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize ? yy + ROTATE_SMALL_CHUNK : imIn->ysize; \ - xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize ? xx + ROTATE_SMALL_CHUNK : imIn->xsize; \ - for (yyy = yy; yyy < yyysize; yyy++) { \ - INT* in = imIn->image[yyy]; \ - xr = imIn->xsize - 1 - xx; \ - for (xxx = xx; xxx < xxxsize; xxx++, xr--) { \ - imOut->image[xr][yyy] = in[xxx]; \ - } \ - } \ - } \ - } \ - } \ + for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ + for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ + yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize \ + ? yy + ROTATE_SMALL_CHUNK \ + : imIn->ysize; \ + xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize \ + ? xx + ROTATE_SMALL_CHUNK \ + : imIn->xsize; \ + for (yyy = yy; yyy < yyysize; yyy++) { \ + INT *in = (INT *)imIn->image[yyy]; \ + xr = imIn->xsize - 1 - xx; \ + for (xxx = xx; xxx < xxxsize; xxx++, xr--) { \ + INT *out = (INT *)imOut->image[xr]; \ + out[yyy] = in[xxx]; \ + } \ + } \ + } \ + } \ + } \ } ImagingSectionEnter(&cookie); - if (imIn->image8) - ROTATE_90(UINT8, image8) - else - ROTATE_90(INT32, image32) + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + ROTATE_90(UINT16, image8); + } else { + ROTATE_90(UINT8, image8); + } + } else { + ROTATE_90(INT32, image32); + } ImagingSectionLeave(&cookie); @@ -126,47 +143,57 @@ ImagingRotate90(Imaging imOut, Imaging imIn) return imOut; } - Imaging -ImagingTranspose(Imaging imOut, Imaging imIn) -{ +ImagingTranspose(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int x, y, xx, yy, xxsize, yysize; int xxx, yyy, xxxsize, yyysize; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); - if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) - return (Imaging) ImagingError_Mismatch(); + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { + return (Imaging)ImagingError_Mismatch(); + } ImagingCopyPalette(imOut, imIn); -#define TRANSPOSE(INT, image) \ - for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ - for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ +#define TRANSPOSE(INT, image) \ + for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ + for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ - for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ - for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ - yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize ? yy + ROTATE_SMALL_CHUNK : imIn->ysize; \ - xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize ? xx + ROTATE_SMALL_CHUNK : imIn->xsize; \ - for (yyy = yy; yyy < yyysize; yyy++) { \ - INT* in = imIn->image[yyy]; \ - for (xxx = xx; xxx < xxxsize; xxx++) { \ - imOut->image[xxx][yyy] = in[xxx]; \ - } \ - } \ - } \ - } \ - } \ + for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ + for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ + yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize \ + ? yy + ROTATE_SMALL_CHUNK \ + : imIn->ysize; \ + xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize \ + ? xx + ROTATE_SMALL_CHUNK \ + : imIn->xsize; \ + for (yyy = yy; yyy < yyysize; yyy++) { \ + INT *in = (INT *)imIn->image[yyy]; \ + for (xxx = xx; xxx < xxxsize; xxx++) { \ + INT *out = (INT *)imOut->image[xxx]; \ + out[yyy] = in[xxx]; \ + } \ + } \ + } \ + } \ + } \ } ImagingSectionEnter(&cookie); - if (imIn->image8) - TRANSPOSE(UINT8, image8) - else - TRANSPOSE(INT32, image32) + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + TRANSPOSE(UINT16, image8); + } else { + TRANSPOSE(UINT8, image8); + } + } else { + TRANSPOSE(INT32, image32); + } ImagingSectionLeave(&cookie); @@ -175,49 +202,59 @@ ImagingTranspose(Imaging imOut, Imaging imIn) return imOut; } - Imaging -ImagingTransverse(Imaging imOut, Imaging imIn) -{ +ImagingTransverse(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int x, y, xr, yr, xx, yy, xxsize, yysize; int xxx, yyy, xxxsize, yyysize; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); - if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) - return (Imaging) ImagingError_Mismatch(); + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { + return (Imaging)ImagingError_Mismatch(); + } ImagingCopyPalette(imOut, imIn); -#define TRANSVERSE(INT, image) \ - for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ - for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ +#define TRANSVERSE(INT, image) \ + for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ + for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ - for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ - for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ - yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize ? yy + ROTATE_SMALL_CHUNK : imIn->ysize; \ - xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize ? xx + ROTATE_SMALL_CHUNK : imIn->xsize; \ - yr = imIn->ysize - 1 - yy; \ - for (yyy = yy; yyy < yyysize; yyy++, yr--) { \ - INT* in = imIn->image[yyy]; \ - xr = imIn->xsize - 1 - xx; \ - for (xxx = xx; xxx < xxxsize; xxx++, xr--) { \ - imOut->image[xr][yr] = in[xxx]; \ - } \ - } \ - } \ - } \ - } \ + for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ + for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ + yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize \ + ? yy + ROTATE_SMALL_CHUNK \ + : imIn->ysize; \ + xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize \ + ? xx + ROTATE_SMALL_CHUNK \ + : imIn->xsize; \ + yr = imIn->ysize - 1 - yy; \ + for (yyy = yy; yyy < yyysize; yyy++, yr--) { \ + INT *in = (INT *)imIn->image[yyy]; \ + xr = imIn->xsize - 1 - xx; \ + for (xxx = xx; xxx < xxxsize; xxx++, xr--) { \ + INT *out = (INT *)imOut->image[xr]; \ + out[yr] = in[xxx]; \ + } \ + } \ + } \ + } \ + } \ } ImagingSectionEnter(&cookie); - if (imIn->image8) - TRANSVERSE(UINT8, image8) - else - TRANSVERSE(INT32, image32) + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + TRANSVERSE(UINT16, image8); + } else { + TRANSVERSE(UINT8, image8); + } + } else { + TRANSVERSE(INT32, image32); + } ImagingSectionLeave(&cookie); @@ -226,34 +263,39 @@ ImagingTransverse(Imaging imOut, Imaging imIn) return imOut; } - Imaging -ImagingRotate180(Imaging imOut, Imaging imIn) -{ +ImagingRotate180(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int x, y, xr, yr; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); - if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) - return (Imaging) ImagingError_Mismatch(); + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { + return (Imaging)ImagingError_Mismatch(); + } ImagingCopyPalette(imOut, imIn); -#define ROTATE_180(INT, image) \ - for (y = 0; y < imIn->ysize; y++, yr--) { \ - INT* in = imIn->image[y]; \ - INT* out = imOut->image[yr]; \ - xr = imIn->xsize-1; \ - for (x = 0; x < imIn->xsize; x++, xr--) \ - out[xr] = in[x]; \ +#define ROTATE_180(INT, image) \ + for (y = 0; y < imIn->ysize; y++, yr--) { \ + INT *in = (INT *)imIn->image[y]; \ + INT *out = (INT *)imOut->image[yr]; \ + xr = imIn->xsize - 1; \ + for (x = 0; x < imIn->xsize; x++, xr--) { \ + out[xr] = in[x]; \ + } \ } ImagingSectionEnter(&cookie); - yr = imIn->ysize-1; + yr = imIn->ysize - 1; if (imIn->image8) { - ROTATE_180(UINT8, image8) + if (strncmp(imIn->mode, "I;16", 4) == 0) { + ROTATE_180(UINT16, image8) + } else { + ROTATE_180(UINT8, image8) + } } else { ROTATE_180(INT32, image32) } @@ -265,48 +307,58 @@ ImagingRotate180(Imaging imOut, Imaging imIn) return imOut; } - Imaging -ImagingRotate270(Imaging imOut, Imaging imIn) -{ +ImagingRotate270(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int x, y, xx, yy, yr, xxsize, yysize; int xxx, yyy, xxxsize, yyysize; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); - if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) - return (Imaging) ImagingError_Mismatch(); + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } + if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { + return (Imaging)ImagingError_Mismatch(); + } ImagingCopyPalette(imOut, imIn); -#define ROTATE_270(INT, image) \ - for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ - for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ +#define ROTATE_270(INT, image) \ + for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ + for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ - for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ - for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ - yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize ? yy + ROTATE_SMALL_CHUNK : imIn->ysize; \ - xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize ? xx + ROTATE_SMALL_CHUNK : imIn->xsize; \ - yr = imIn->ysize - 1 - yy; \ - for (yyy = yy; yyy < yyysize; yyy++, yr--) { \ - INT* in = imIn->image[yyy]; \ - for (xxx = xx; xxx < xxxsize; xxx++) { \ - imOut->image[xxx][yr] = in[xxx]; \ - } \ - } \ - } \ - } \ - } \ + for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ + for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ + yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize \ + ? yy + ROTATE_SMALL_CHUNK \ + : imIn->ysize; \ + xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize \ + ? xx + ROTATE_SMALL_CHUNK \ + : imIn->xsize; \ + yr = imIn->ysize - 1 - yy; \ + for (yyy = yy; yyy < yyysize; yyy++, yr--) { \ + INT *in = (INT *)imIn->image[yyy]; \ + for (xxx = xx; xxx < xxxsize; xxx++) { \ + INT *out = (INT *)imOut->image[xxx]; \ + out[yr] = in[xxx]; \ + } \ + } \ + } \ + } \ + } \ } ImagingSectionEnter(&cookie); - if (imIn->image8) - ROTATE_270(UINT8, image8) - else - ROTATE_270(INT32, image32) + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + ROTATE_270(UINT16, image8); + } else { + ROTATE_270(UINT8, image8); + } + } else { + ROTATE_270(INT32, image32); + } ImagingSectionLeave(&cookie); @@ -315,63 +367,74 @@ ImagingRotate270(Imaging imOut, Imaging imIn) return imOut; } - /* -------------------------------------------------------------------- */ /* Transforms */ /* transform primitives (ImagingTransformMap) */ static int -affine_transform(double* xout, double* yout, int x, int y, void* data) -{ +affine_transform(double *xout, double *yout, int x, int y, void *data) { /* full moon tonight. your compiler will generate bogus code for simple expressions, unless you reorganize the code, or install Service Pack 3 */ - double* a = (double*) data; - double a0 = a[0]; double a1 = a[1]; double a2 = a[2]; - double a3 = a[3]; double a4 = a[4]; double a5 = a[5]; + double *a = (double *)data; + double a0 = a[0]; + double a1 = a[1]; + double a2 = a[2]; + double a3 = a[3]; + double a4 = a[4]; + double a5 = a[5]; double xin = x + 0.5; double yin = y + 0.5; - xout[0] = a0*xin + a1*yin + a2; - yout[0] = a3*xin + a4*yin + a5; + xout[0] = a0 * xin + a1 * yin + a2; + yout[0] = a3 * xin + a4 * yin + a5; return 1; } static int -perspective_transform(double* xout, double* yout, int x, int y, void* data) -{ - double* a = (double*) data; - double a0 = a[0]; double a1 = a[1]; double a2 = a[2]; - double a3 = a[3]; double a4 = a[4]; double a5 = a[5]; - double a6 = a[6]; double a7 = a[7]; +perspective_transform(double *xout, double *yout, int x, int y, void *data) { + double *a = (double *)data; + double a0 = a[0]; + double a1 = a[1]; + double a2 = a[2]; + double a3 = a[3]; + double a4 = a[4]; + double a5 = a[5]; + double a6 = a[6]; + double a7 = a[7]; double xin = x + 0.5; double yin = y + 0.5; - xout[0] = (a0*xin + a1*yin + a2) / (a6*xin + a7*yin + 1); - yout[0] = (a3*xin + a4*yin + a5) / (a6*xin + a7*yin + 1); + xout[0] = (a0 * xin + a1 * yin + a2) / (a6 * xin + a7 * yin + 1); + yout[0] = (a3 * xin + a4 * yin + a5) / (a6 * xin + a7 * yin + 1); return 1; } static int -quad_transform(double* xout, double* yout, int x, int y, void* data) -{ +quad_transform(double *xout, double *yout, int x, int y, void *data) { /* quad warp: map quadrilateral to rectangle */ - double* a = (double*) data; - double a0 = a[0]; double a1 = a[1]; double a2 = a[2]; double a3 = a[3]; - double a4 = a[4]; double a5 = a[5]; double a6 = a[6]; double a7 = a[7]; + double *a = (double *)data; + double a0 = a[0]; + double a1 = a[1]; + double a2 = a[2]; + double a3 = a[3]; + double a4 = a[4]; + double a5 = a[5]; + double a6 = a[6]; + double a7 = a[7]; double xin = x + 0.5; double yin = y + 0.5; - xout[0] = a0 + a1*xin + a2*yin + a3*xin*yin; - yout[0] = a4 + a5*xin + a6*yin + a7*xin*yin; + xout[0] = a0 + a1 * xin + a2 * yin + a3 * xin * yin; + yout[0] = a4 + a5 * xin + a6 * yin + a7 * xin * yin; return 1; } @@ -379,120 +442,121 @@ quad_transform(double* xout, double* yout, int x, int y, void* data) /* transform filters (ImagingTransformFilter) */ static int -nearest_filter8(void* out, Imaging im, double xin, double yin) -{ +nearest_filter8(void *out, Imaging im, double xin, double yin) { int x = COORD(xin); int y = COORD(yin); - if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) + if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { return 0; - ((UINT8*)out)[0] = im->image8[y][x]; + } + ((UINT8 *)out)[0] = im->image8[y][x]; return 1; } static int -nearest_filter16(void* out, Imaging im, double xin, double yin) -{ +nearest_filter16(void *out, Imaging im, double xin, double yin) { int x = COORD(xin); int y = COORD(yin); - if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) + if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { return 0; - ((INT16*)out)[0] = ((INT16*)(im->image8[y]))[x]; + } + memcpy(out, im->image8[y] + x * sizeof(INT16), sizeof(INT16)); return 1; } static int -nearest_filter32(void* out, Imaging im, double xin, double yin) -{ +nearest_filter32(void *out, Imaging im, double xin, double yin) { int x = COORD(xin); int y = COORD(yin); - if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) + if (x < 0 || x >= im->xsize || y < 0 || y >= im->ysize) { return 0; - ((INT32*)out)[0] = im->image32[y][x]; + } + memcpy(out, &im->image32[y][x], sizeof(INT32)); return 1; } -#define XCLIP(im, x) ( ((x) < 0) ? 0 : ((x) < im->xsize) ? (x) : im->xsize-1 ) -#define YCLIP(im, y) ( ((y) < 0) ? 0 : ((y) < im->ysize) ? (y) : im->ysize-1 ) - -#define BILINEAR(v, a, b, d)\ - (v = (a) + ( (b) - (a) ) * (d)) - -#define BILINEAR_HEAD(type)\ - int x, y;\ - int x0, x1;\ - double v1, v2;\ - double dx, dy;\ - type* in;\ - if (xin < 0.0 || xin >= im->xsize || yin < 0.0 || yin >= im->ysize)\ - return 0;\ - xin -= 0.5;\ - yin -= 0.5;\ - x = FLOOR(xin);\ - y = FLOOR(yin);\ - dx = xin - x;\ +#define XCLIP(im, x) (((x) < 0) ? 0 : ((x) < im->xsize) ? (x) : im->xsize - 1) +#define YCLIP(im, y) (((y) < 0) ? 0 : ((y) < im->ysize) ? (y) : im->ysize - 1) + +#define BILINEAR(v, a, b, d) (v = (a) + ((b) - (a)) * (d)) + +#define BILINEAR_HEAD(type) \ + int x, y; \ + int x0, x1; \ + double v1, v2; \ + double dx, dy; \ + type *in; \ + if (xin < 0.0 || xin >= im->xsize || yin < 0.0 || yin >= im->ysize) { \ + return 0; \ + } \ + xin -= 0.5; \ + yin -= 0.5; \ + x = FLOOR(xin); \ + y = FLOOR(yin); \ + dx = xin - x; \ dy = yin - y; -#define BILINEAR_BODY(type, image, step, offset) {\ - in = (type*) ((image)[YCLIP(im, y)] + offset);\ - x0 = XCLIP(im, x+0)*step;\ - x1 = XCLIP(im, x+1)*step;\ - BILINEAR(v1, in[x0], in[x1], dx);\ - if (y+1 >= 0 && y+1 < im->ysize) {\ - in = (type*) ((image)[y+1] + offset);\ - BILINEAR(v2, in[x0], in[x1], dx);\ - } else\ - v2 = v1;\ - BILINEAR(v1, v1, v2, dy);\ -} +#define BILINEAR_BODY(type, image, step, offset) \ + { \ + in = (type *)((image)[YCLIP(im, y)] + offset); \ + x0 = XCLIP(im, x + 0) * step; \ + x1 = XCLIP(im, x + 1) * step; \ + BILINEAR(v1, in[x0], in[x1], dx); \ + if (y + 1 >= 0 && y + 1 < im->ysize) { \ + in = (type *)((image)[y + 1] + offset); \ + BILINEAR(v2, in[x0], in[x1], dx); \ + } else { \ + v2 = v1; \ + } \ + BILINEAR(v1, v1, v2, dy); \ + } static int -bilinear_filter8(void* out, Imaging im, double xin, double yin) -{ +bilinear_filter8(void *out, Imaging im, double xin, double yin) { BILINEAR_HEAD(UINT8); BILINEAR_BODY(UINT8, im->image8, 1, 0); - ((UINT8*)out)[0] = (UINT8) v1; + ((UINT8 *)out)[0] = (UINT8)v1; return 1; } static int -bilinear_filter32I(void* out, Imaging im, double xin, double yin) -{ +bilinear_filter32I(void *out, Imaging im, double xin, double yin) { + INT32 k; BILINEAR_HEAD(INT32); BILINEAR_BODY(INT32, im->image32, 1, 0); - ((INT32*)out)[0] = (INT32) v1; + k = v1; + memcpy(out, &k, sizeof(k)); return 1; } static int -bilinear_filter32F(void* out, Imaging im, double xin, double yin) -{ +bilinear_filter32F(void *out, Imaging im, double xin, double yin) { + FLOAT32 k; BILINEAR_HEAD(FLOAT32); BILINEAR_BODY(FLOAT32, im->image32, 1, 0); - ((FLOAT32*)out)[0] = (FLOAT32) v1; + k = v1; + memcpy(out, &k, sizeof(k)); return 1; } static int -bilinear_filter32LA(void* out, Imaging im, double xin, double yin) -{ +bilinear_filter32LA(void *out, Imaging im, double xin, double yin) { BILINEAR_HEAD(UINT8); BILINEAR_BODY(UINT8, im->image, 4, 0); - ((UINT8*)out)[0] = (UINT8) v1; - ((UINT8*)out)[1] = (UINT8) v1; - ((UINT8*)out)[2] = (UINT8) v1; + ((UINT8 *)out)[0] = (UINT8)v1; + ((UINT8 *)out)[1] = (UINT8)v1; + ((UINT8 *)out)[2] = (UINT8)v1; BILINEAR_BODY(UINT8, im->image, 4, 3); - ((UINT8*)out)[3] = (UINT8) v1; + ((UINT8 *)out)[3] = (UINT8)v1; return 1; } static int -bilinear_filter32RGB(void* out, Imaging im, double xin, double yin) -{ +bilinear_filter32RGB(void *out, Imaging im, double xin, double yin) { int b; BILINEAR_HEAD(UINT8); for (b = 0; b < im->bands; b++) { BILINEAR_BODY(UINT8, im->image, 4, b); - ((UINT8*)out)[b] = (UINT8) v1; + ((UINT8 *)out)[b] = (UINT8)v1; } return 1; } @@ -501,130 +565,138 @@ bilinear_filter32RGB(void* out, Imaging im, double xin, double yin) #undef BILINEAR_HEAD #undef BILINEAR_BODY -#define BICUBIC(v, v1, v2, v3, v4, d) {\ - double p1 = v2;\ - double p2 = -v1 + v3;\ - double p3 = 2*(v1 - v2) + v3 - v4;\ - double p4 = -v1 + v2 - v3 + v4;\ - v = p1 + (d)*(p2 + (d)*(p3 + (d)*p4));\ -} - -#define BICUBIC_HEAD(type)\ - int x = FLOOR(xin);\ - int y = FLOOR(yin);\ - int x0, x1, x2, x3;\ - double v1, v2, v3, v4;\ - double dx, dy;\ - type* in;\ - if (xin < 0.0 || xin >= im->xsize || yin < 0.0 || yin >= im->ysize)\ - return 0;\ - xin -= 0.5;\ - yin -= 0.5;\ - x = FLOOR(xin);\ - y = FLOOR(yin);\ - dx = xin - x;\ - dy = yin - y;\ - x--; y--; - -#define BICUBIC_BODY(type, image, step, offset) {\ - in = (type*) ((image)[YCLIP(im, y)] + offset);\ - x0 = XCLIP(im, x+0)*step;\ - x1 = XCLIP(im, x+1)*step;\ - x2 = XCLIP(im, x+2)*step;\ - x3 = XCLIP(im, x+3)*step;\ - BICUBIC(v1, in[x0], in[x1], in[x2], in[x3], dx);\ - if (y+1 >= 0 && y+1 < im->ysize) {\ - in = (type*) ((image)[y+1] + offset);\ - BICUBIC(v2, in[x0], in[x1], in[x2], in[x3], dx);\ - } else\ - v2 = v1;\ - if (y+2 >= 0 && y+2 < im->ysize) {\ - in = (type*) ((image)[y+2] + offset);\ - BICUBIC(v3, in[x0], in[x1], in[x2], in[x3], dx);\ - } else\ - v3 = v2;\ - if (y+3 >= 0 && y+3 < im->ysize) {\ - in = (type*) ((image)[y+3] + offset);\ - BICUBIC(v4, in[x0], in[x1], in[x2], in[x3], dx);\ - } else\ - v4 = v3;\ - BICUBIC(v1, v1, v2, v3, v4, dy);\ -} +#define BICUBIC(v, v1, v2, v3, v4, d) \ + { \ + double p1 = v2; \ + double p2 = -v1 + v3; \ + double p3 = 2 * (v1 - v2) + v3 - v4; \ + double p4 = -v1 + v2 - v3 + v4; \ + v = p1 + (d) * (p2 + (d) * (p3 + (d)*p4)); \ + } +#define BICUBIC_HEAD(type) \ + int x = FLOOR(xin); \ + int y = FLOOR(yin); \ + int x0, x1, x2, x3; \ + double v1, v2, v3, v4; \ + double dx, dy; \ + type *in; \ + if (xin < 0.0 || xin >= im->xsize || yin < 0.0 || yin >= im->ysize) { \ + return 0; \ + } \ + xin -= 0.5; \ + yin -= 0.5; \ + x = FLOOR(xin); \ + y = FLOOR(yin); \ + dx = xin - x; \ + dy = yin - y; \ + x--; \ + y--; + +#define BICUBIC_BODY(type, image, step, offset) \ + { \ + in = (type *)((image)[YCLIP(im, y)] + offset); \ + x0 = XCLIP(im, x + 0) * step; \ + x1 = XCLIP(im, x + 1) * step; \ + x2 = XCLIP(im, x + 2) * step; \ + x3 = XCLIP(im, x + 3) * step; \ + BICUBIC(v1, in[x0], in[x1], in[x2], in[x3], dx); \ + if (y + 1 >= 0 && y + 1 < im->ysize) { \ + in = (type *)((image)[y + 1] + offset); \ + BICUBIC(v2, in[x0], in[x1], in[x2], in[x3], dx); \ + } else { \ + v2 = v1; \ + } \ + if (y + 2 >= 0 && y + 2 < im->ysize) { \ + in = (type *)((image)[y + 2] + offset); \ + BICUBIC(v3, in[x0], in[x1], in[x2], in[x3], dx); \ + } else { \ + v3 = v2; \ + } \ + if (y + 3 >= 0 && y + 3 < im->ysize) { \ + in = (type *)((image)[y + 3] + offset); \ + BICUBIC(v4, in[x0], in[x1], in[x2], in[x3], dx); \ + } else { \ + v4 = v3; \ + } \ + BICUBIC(v1, v1, v2, v3, v4, dy); \ + } static int -bicubic_filter8(void* out, Imaging im, double xin, double yin) -{ +bicubic_filter8(void *out, Imaging im, double xin, double yin) { BICUBIC_HEAD(UINT8); BICUBIC_BODY(UINT8, im->image8, 1, 0); - if (v1 <= 0.0) - ((UINT8*)out)[0] = 0; - else if (v1 >= 255.0) - ((UINT8*)out)[0] = 255; - else - ((UINT8*)out)[0] = (UINT8) v1; + if (v1 <= 0.0) { + ((UINT8 *)out)[0] = 0; + } else if (v1 >= 255.0) { + ((UINT8 *)out)[0] = 255; + } else { + ((UINT8 *)out)[0] = (UINT8)v1; + } return 1; } static int -bicubic_filter32I(void* out, Imaging im, double xin, double yin) -{ +bicubic_filter32I(void *out, Imaging im, double xin, double yin) { + INT32 k; BICUBIC_HEAD(INT32); BICUBIC_BODY(INT32, im->image32, 1, 0); - ((INT32*)out)[0] = (INT32) v1; + k = v1; + memcpy(out, &k, sizeof(k)); return 1; } static int -bicubic_filter32F(void* out, Imaging im, double xin, double yin) -{ +bicubic_filter32F(void *out, Imaging im, double xin, double yin) { + FLOAT32 k; BICUBIC_HEAD(FLOAT32); BICUBIC_BODY(FLOAT32, im->image32, 1, 0); - ((FLOAT32*)out)[0] = (FLOAT32) v1; + k = v1; + memcpy(out, &k, sizeof(k)); return 1; } static int -bicubic_filter32LA(void* out, Imaging im, double xin, double yin) -{ +bicubic_filter32LA(void *out, Imaging im, double xin, double yin) { BICUBIC_HEAD(UINT8); BICUBIC_BODY(UINT8, im->image, 4, 0); if (v1 <= 0.0) { - ((UINT8*)out)[0] = 0; - ((UINT8*)out)[1] = 0; - ((UINT8*)out)[2] = 0; + ((UINT8 *)out)[0] = 0; + ((UINT8 *)out)[1] = 0; + ((UINT8 *)out)[2] = 0; } else if (v1 >= 255.0) { - ((UINT8*)out)[0] = 255; - ((UINT8*)out)[1] = 255; - ((UINT8*)out)[2] = 255; + ((UINT8 *)out)[0] = 255; + ((UINT8 *)out)[1] = 255; + ((UINT8 *)out)[2] = 255; } else { - ((UINT8*)out)[0] = (UINT8) v1; - ((UINT8*)out)[1] = (UINT8) v1; - ((UINT8*)out)[2] = (UINT8) v1; + ((UINT8 *)out)[0] = (UINT8)v1; + ((UINT8 *)out)[1] = (UINT8)v1; + ((UINT8 *)out)[2] = (UINT8)v1; } BICUBIC_BODY(UINT8, im->image, 4, 3); - if (v1 <= 0.0) - ((UINT8*)out)[3] = 0; - else if (v1 >= 255.0) - ((UINT8*)out)[3] = 255; - else - ((UINT8*)out)[3] = (UINT8) v1; + if (v1 <= 0.0) { + ((UINT8 *)out)[3] = 0; + } else if (v1 >= 255.0) { + ((UINT8 *)out)[3] = 255; + } else { + ((UINT8 *)out)[3] = (UINT8)v1; + } return 1; } static int -bicubic_filter32RGB(void* out, Imaging im, double xin, double yin) -{ +bicubic_filter32RGB(void *out, Imaging im, double xin, double yin) { int b; BICUBIC_HEAD(UINT8); for (b = 0; b < im->bands; b++) { BICUBIC_BODY(UINT8, im->image, 4, b); - if (v1 <= 0.0) - ((UINT8*)out)[b] = 0; - else if (v1 >= 255.0) - ((UINT8*)out)[b] = 255; - else - ((UINT8*)out)[b] = (UINT8) v1; + if (v1 <= 0.0) { + ((UINT8 *)out)[b] = 0; + } else if (v1 >= 255.0) { + ((UINT8 *)out)[b] = 255; + } else { + ((UINT8 *)out)[b] = (UINT8)v1; + } } return 1; } @@ -634,61 +706,63 @@ bicubic_filter32RGB(void* out, Imaging im, double xin, double yin) #undef BICUBIC_BODY static ImagingTransformFilter -getfilter(Imaging im, int filterid) -{ +getfilter(Imaging im, int filterid) { switch (filterid) { - case IMAGING_TRANSFORM_NEAREST: - if (im->image8) - switch (im->type) { - case IMAGING_TYPE_UINT8: - return nearest_filter8; - case IMAGING_TYPE_SPECIAL: - switch (im->pixelsize) { - case 1: - return nearest_filter8; - case 2: - return nearest_filter16; - case 4: - return nearest_filter32; + case IMAGING_TRANSFORM_NEAREST: + if (im->image8) { + switch (im->type) { + case IMAGING_TYPE_UINT8: + return nearest_filter8; + case IMAGING_TYPE_SPECIAL: + switch (im->pixelsize) { + case 1: + return nearest_filter8; + case 2: + return nearest_filter16; + case 4: + return nearest_filter32; + } } + } else { + return nearest_filter32; } - else - return nearest_filter32; - break; - case IMAGING_TRANSFORM_BILINEAR: - if (im->image8) - return bilinear_filter8; - else if (im->image32) { - switch (im->type) { - case IMAGING_TYPE_UINT8: - if (im->bands == 2) - return bilinear_filter32LA; - else - return bilinear_filter32RGB; - case IMAGING_TYPE_INT32: - return bilinear_filter32I; - case IMAGING_TYPE_FLOAT32: - return bilinear_filter32F; + break; + case IMAGING_TRANSFORM_BILINEAR: + if (im->image8) { + return bilinear_filter8; + } else if (im->image32) { + switch (im->type) { + case IMAGING_TYPE_UINT8: + if (im->bands == 2) { + return bilinear_filter32LA; + } else { + return bilinear_filter32RGB; + } + case IMAGING_TYPE_INT32: + return bilinear_filter32I; + case IMAGING_TYPE_FLOAT32: + return bilinear_filter32F; + } } - } - break; - case IMAGING_TRANSFORM_BICUBIC: - if (im->image8) - return bicubic_filter8; - else if (im->image32) { - switch (im->type) { - case IMAGING_TYPE_UINT8: - if (im->bands == 2) - return bicubic_filter32LA; - else - return bicubic_filter32RGB; - case IMAGING_TYPE_INT32: - return bicubic_filter32I; - case IMAGING_TYPE_FLOAT32: - return bicubic_filter32F; + break; + case IMAGING_TRANSFORM_BICUBIC: + if (im->image8) { + return bicubic_filter8; + } else if (im->image32) { + switch (im->type) { + case IMAGING_TYPE_UINT8: + if (im->bands == 2) { + return bicubic_filter32LA; + } else { + return bicubic_filter32RGB; + } + case IMAGING_TYPE_INT32: + return bicubic_filter32I; + case IMAGING_TYPE_FLOAT32: + return bicubic_filter32F; + } } - } - break; + break; } /* no such filter */ return NULL; @@ -698,10 +772,16 @@ getfilter(Imaging im, int filterid) Imaging ImagingGenericTransform( - Imaging imOut, Imaging imIn, int x0, int y0, int x1, int y1, - ImagingTransformMap transform, void* transform_data, - int filterid, int fill) -{ + Imaging imOut, + Imaging imIn, + int x0, + int y0, + int x1, + int y1, + ImagingTransformMap transform, + void *transform_data, + int filterid, + int fill) { /* slow generic transformation. use ImagingTransformAffine or ImagingScaleAffine where possible. */ @@ -711,32 +791,39 @@ ImagingGenericTransform( double xx, yy; ImagingTransformFilter filter = getfilter(imIn, filterid); - if (!filter) - return (Imaging) ImagingError_ValueError("bad filter number"); + if (!filter) { + return (Imaging)ImagingError_ValueError("bad filter number"); + } - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } ImagingCopyPalette(imOut, imIn); ImagingSectionEnter(&cookie); - if (x0 < 0) + if (x0 < 0) { x0 = 0; - if (y0 < 0) + } + if (y0 < 0) { y0 = 0; - if (x1 > imOut->xsize) + } + if (x1 > imOut->xsize) { x1 = imOut->xsize; - if (y1 > imOut->ysize) + } + if (y1 > imOut->ysize) { y1 = imOut->ysize; + } for (y = y0; y < y1; y++) { - out = imOut->image[y] + x0*imOut->pixelsize; + out = imOut->image[y] + x0 * imOut->pixelsize; for (x = x0; x < x1; x++) { - if ( ! transform(&xx, &yy, x-x0, y-y0, transform_data) || - ! filter(out, imIn, xx, yy)) { - if (fill) + if (!transform(&xx, &yy, x - x0, y - y0, transform_data) || + !filter(out, imIn, xx, yy)) { + if (fill) { memset(out, 0, imOut->pixelsize); + } } out += imOut->pixelsize; } @@ -748,10 +835,15 @@ ImagingGenericTransform( } static Imaging -ImagingScaleAffine(Imaging imOut, Imaging imIn, - int x0, int y0, int x1, int y1, - double a[6], int fill) -{ +ImagingScaleAffine( + Imaging imOut, + Imaging imIn, + int x0, + int y0, + int x1, + int y1, + double a[6], + int fill) { /* scale, nearest neighbour resampling */ ImagingSectionCookie cookie; @@ -761,25 +853,30 @@ ImagingScaleAffine(Imaging imOut, Imaging imIn, int xmin, xmax; int *xintab; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } ImagingCopyPalette(imOut, imIn); - if (x0 < 0) + if (x0 < 0) { x0 = 0; - if (y0 < 0) + } + if (y0 < 0) { y0 = 0; - if (x1 > imOut->xsize) + } + if (x1 > imOut->xsize) { x1 = imOut->xsize; - if (y1 > imOut->ysize) + } + if (y1 > imOut->ysize) { y1 = imOut->ysize; + } /* malloc check ok, uses calloc for overflow */ - xintab = (int*) calloc(imOut->xsize, sizeof(int)); + xintab = (int *)calloc(imOut->xsize, sizeof(int)); if (!xintab) { ImagingDelete(imOut); - return (Imaging) ImagingError_MemoryError(); + return (Imaging)ImagingError_MemoryError(); } xo = a[2] + a[0] * 0.5; @@ -791,28 +888,31 @@ ImagingScaleAffine(Imaging imOut, Imaging imIn, /* Pretabulate horizontal pixel positions */ for (x = x0; x < x1; x++) { xin = COORD(xo); - if (xin >= 0 && xin < (int) imIn->xsize) { - xmax = x+1; - if (x < xmin) + if (xin >= 0 && xin < (int)imIn->xsize) { + xmax = x + 1; + if (x < xmin) { xmin = x; + } xintab[x] = xin; } xo += a[0]; } -#define AFFINE_SCALE(pixel, image)\ - for (y = y0; y < y1; y++) {\ - int yi = COORD(yo);\ - pixel *in, *out;\ - out = imOut->image[y];\ - if (fill && x1 > x0)\ - memset(out+x0, 0, (x1-x0)*sizeof(pixel));\ - if (yi >= 0 && yi < imIn->ysize) {\ - in = imIn->image[yi];\ - for (x = xmin; x < xmax; x++)\ - out[x] = in[xintab[x]];\ - }\ - yo += a[4];\ +#define AFFINE_SCALE(pixel, image) \ + for (y = y0; y < y1; y++) { \ + int yi = COORD(yo); \ + pixel *in, *out; \ + out = imOut->image[y]; \ + if (fill && x1 > x0) { \ + memset(out + x0, 0, (x1 - x0) * sizeof(pixel)); \ + } \ + if (yi >= 0 && yi < imIn->ysize) { \ + in = imIn->image[yi]; \ + for (x = xmin; x < xmax; x++) { \ + out[x] = in[xintab[x]]; \ + } \ + } \ + yo += a[4]; \ } ImagingSectionEnter(&cookie); @@ -833,17 +933,23 @@ ImagingScaleAffine(Imaging imOut, Imaging imIn, } static inline int -check_fixed(double a[6], int x, int y) -{ - return (fabs(x*a[0] + y*a[1] + a[2]) < 32768.0 && - fabs(x*a[3] + y*a[4] + a[5]) < 32768.0); +check_fixed(double a[6], int x, int y) { + return ( + fabs(x * a[0] + y * a[1] + a[2]) < 32768.0 && + fabs(x * a[3] + y * a[4] + a[5]) < 32768.0); } static inline Imaging -affine_fixed(Imaging imOut, Imaging imIn, - int x0, int y0, int x1, int y1, - double a[6], int filterid, int fill) -{ +affine_fixed( + Imaging imOut, + Imaging imIn, + int x0, + int y0, + int x1, + int y1, + double a[6], + int filterid, + int fill) { /* affine transform, nearest neighbour resampling, fixed point arithmetics */ @@ -856,47 +962,52 @@ affine_fixed(Imaging imOut, Imaging imIn, ImagingCopyPalette(imOut, imIn); - xsize = (int) imIn->xsize; - ysize = (int) imIn->ysize; + xsize = (int)imIn->xsize; + ysize = (int)imIn->ysize; /* use 16.16 fixed point arithmetics */ #define FIX(v) FLOOR((v)*65536.0 + 0.5) - a0 = FIX(a[0]); a1 = FIX(a[1]); - a3 = FIX(a[3]); a4 = FIX(a[4]); + a0 = FIX(a[0]); + a1 = FIX(a[1]); + a3 = FIX(a[3]); + a4 = FIX(a[4]); a2 = FIX(a[2] + a[0] * 0.5 + a[1] * 0.5); a5 = FIX(a[5] + a[3] * 0.5 + a[4] * 0.5); #undef FIX -#define AFFINE_TRANSFORM_FIXED(pixel, image)\ - for (y = y0; y < y1; y++) {\ - pixel *out;\ - xx = a2;\ - yy = a5;\ - out = imOut->image[y];\ - if (fill && x1 > x0)\ - memset(out+x0, 0, (x1-x0)*sizeof(pixel));\ - for (x = x0; x < x1; x++, out++) {\ - xin = xx >> 16;\ - if (xin >= 0 && xin < xsize) {\ - yin = yy >> 16;\ - if (yin >= 0 && yin < ysize)\ - *out = imIn->image[yin][xin];\ - }\ - xx += a0;\ - yy += a3;\ - }\ - a2 += a1;\ - a5 += a4;\ +#define AFFINE_TRANSFORM_FIXED(pixel, image) \ + for (y = y0; y < y1; y++) { \ + pixel *out; \ + xx = a2; \ + yy = a5; \ + out = imOut->image[y]; \ + if (fill && x1 > x0) { \ + memset(out + x0, 0, (x1 - x0) * sizeof(pixel)); \ + } \ + for (x = x0; x < x1; x++, out++) { \ + xin = xx >> 16; \ + if (xin >= 0 && xin < xsize) { \ + yin = yy >> 16; \ + if (yin >= 0 && yin < ysize) { \ + *out = imIn->image[yin][xin]; \ + } \ + } \ + xx += a0; \ + yy += a3; \ + } \ + a2 += a1; \ + a5 += a4; \ } ImagingSectionEnter(&cookie); - if (imIn->image8) + if (imIn->image8) { AFFINE_TRANSFORM_FIXED(UINT8, image8) - else + } else { AFFINE_TRANSFORM_FIXED(INT32, image32) + } ImagingSectionLeave(&cookie); @@ -906,10 +1017,16 @@ affine_fixed(Imaging imOut, Imaging imIn, } Imaging -ImagingTransformAffine(Imaging imOut, Imaging imIn, - int x0, int y0, int x1, int y1, - double a[6], int filterid, int fill) -{ +ImagingTransformAffine( + Imaging imOut, + Imaging imIn, + int x0, + int y0, + int x1, + int y1, + double a[6], + int filterid, + int fill) { /* affine transform, nearest neighbour resampling, floating point arithmetics*/ @@ -922,10 +1039,7 @@ ImagingTransformAffine(Imaging imOut, Imaging imIn, if (filterid || imIn->type == IMAGING_TYPE_SPECIAL) { return ImagingGenericTransform( - imOut, imIn, - x0, y0, x1, y1, - affine_transform, a, - filterid, fill); + imOut, imIn, x0, y0, x1, y1, affine_transform, a, filterid, fill); } if (a[1] == 0 && a[3] == 0) { @@ -933,24 +1047,30 @@ ImagingTransformAffine(Imaging imOut, Imaging imIn, return ImagingScaleAffine(imOut, imIn, x0, y0, x1, y1, a, fill); } - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + return (Imaging)ImagingError_ModeError(); + } - if (x0 < 0) + if (x0 < 0) { x0 = 0; - if (y0 < 0) + } + if (y0 < 0) { y0 = 0; - if (x1 > imOut->xsize) + } + if (x1 > imOut->xsize) { x1 = imOut->xsize; - if (y1 > imOut->ysize) + } + if (y1 > imOut->ysize) { y1 = imOut->ysize; + } /* translate all four corners to check if they are within the range that can be represented by the fixed point arithmetics */ - if (check_fixed(a, 0, 0) && check_fixed(a, x1-x0, y1-y0) && - check_fixed(a, 0, y1-y0) && check_fixed(a, x1-x0, 0)) + if (check_fixed(a, 0, 0) && check_fixed(a, x1 - x0, y1 - y0) && + check_fixed(a, 0, y1 - y0) && check_fixed(a, x1 - x0, 0)) { return affine_fixed(imOut, imIn, x0, y0, x1, y1, a, filterid, fill); + } /* FIXME: cannot really think of any reasonable case when the following code is used. maybe we should fall back on the slow @@ -958,40 +1078,43 @@ ImagingTransformAffine(Imaging imOut, Imaging imIn, ImagingCopyPalette(imOut, imIn); - xsize = (int) imIn->xsize; - ysize = (int) imIn->ysize; + xsize = (int)imIn->xsize; + ysize = (int)imIn->ysize; xo = a[2] + a[1] * 0.5 + a[0] * 0.5; yo = a[5] + a[4] * 0.5 + a[3] * 0.5; -#define AFFINE_TRANSFORM(pixel, image)\ - for (y = y0; y < y1; y++) {\ - pixel *out;\ - xx = xo;\ - yy = yo;\ - out = imOut->image[y];\ - if (fill && x1 > x0)\ - memset(out+x0, 0, (x1-x0)*sizeof(pixel));\ - for (x = x0; x < x1; x++, out++) {\ - xin = COORD(xx);\ - if (xin >= 0 && xin < xsize) {\ - yin = COORD(yy);\ - if (yin >= 0 && yin < ysize)\ - *out = imIn->image[yin][xin];\ - }\ - xx += a[0];\ - yy += a[3];\ - }\ - xo += a[1];\ - yo += a[4];\ +#define AFFINE_TRANSFORM(pixel, image) \ + for (y = y0; y < y1; y++) { \ + pixel *out; \ + xx = xo; \ + yy = yo; \ + out = imOut->image[y]; \ + if (fill && x1 > x0) { \ + memset(out + x0, 0, (x1 - x0) * sizeof(pixel)); \ + } \ + for (x = x0; x < x1; x++, out++) { \ + xin = COORD(xx); \ + if (xin >= 0 && xin < xsize) { \ + yin = COORD(yy); \ + if (yin >= 0 && yin < ysize) { \ + *out = imIn->image[yin][xin]; \ + } \ + } \ + xx += a[0]; \ + yy += a[3]; \ + } \ + xo += a[1]; \ + yo += a[4]; \ } ImagingSectionEnter(&cookie); - if (imIn->image8) + if (imIn->image8) { AFFINE_TRANSFORM(UINT8, image8) - else + } else { AFFINE_TRANSFORM(INT32, image32) + } ImagingSectionLeave(&cookie); @@ -1001,29 +1124,34 @@ ImagingTransformAffine(Imaging imOut, Imaging imIn, } Imaging -ImagingTransform(Imaging imOut, Imaging imIn, int method, - int x0, int y0, int x1, int y1, - double a[8], int filterid, int fill) -{ +ImagingTransform( + Imaging imOut, + Imaging imIn, + int method, + int x0, + int y0, + int x1, + int y1, + double a[8], + int filterid, + int fill) { ImagingTransformMap transform; - switch(method) { - case IMAGING_TRANSFORM_AFFINE: - return ImagingTransformAffine( - imOut, imIn, x0, y0, x1, y1, a, filterid, fill); - break; - case IMAGING_TRANSFORM_PERSPECTIVE: - transform = perspective_transform; - break; - case IMAGING_TRANSFORM_QUAD: - transform = quad_transform; - break; - default: - return (Imaging) ImagingError_ValueError("bad transform method"); + switch (method) { + case IMAGING_TRANSFORM_AFFINE: + return ImagingTransformAffine( + imOut, imIn, x0, y0, x1, y1, a, filterid, fill); + break; + case IMAGING_TRANSFORM_PERSPECTIVE: + transform = perspective_transform; + break; + case IMAGING_TRANSFORM_QUAD: + transform = quad_transform; + break; + default: + return (Imaging)ImagingError_ValueError("bad transform method"); } return ImagingGenericTransform( - imOut, imIn, - x0, y0, x1, y1, - transform, a, filterid, fill); + imOut, imIn, x0, y0, x1, y1, transform, a, filterid, fill); } diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 3cfa42c480f..e73153600d0 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -16,13 +16,10 @@ * See the README file for details on usage and redistribution. */ - #include "Imaging.h" - int -ImagingGetBBox(Imaging im, int bbox[4]) -{ +ImagingGetBBox(Imaging im, int bbox[4]) { /* Get the bounding box for any non-zero data in the image.*/ int x, y; @@ -33,44 +30,57 @@ ImagingGetBBox(Imaging im, int bbox[4]) bbox[1] = -1; bbox[2] = bbox[3] = 0; -#define GETBBOX(image, mask)\ - for (y = 0; y < im->ysize; y++) {\ - has_data = 0;\ - for (x = 0; x < im->xsize; x++)\ - if (im->image[y][x] & mask) {\ - has_data = 1;\ - if (x < bbox[0])\ - bbox[0] = x;\ - if (x >= bbox[2])\ - bbox[2] = x+1;\ - }\ - if (has_data) {\ - if (bbox[1] < 0)\ - bbox[1] = y;\ - bbox[3] = y+1;\ - }\ +#define GETBBOX(image, mask) \ + for (y = 0; y < im->ysize; y++) { \ + has_data = 0; \ + for (x = 0; x < im->xsize; x++) { \ + if (im->image[y][x] & mask) { \ + has_data = 1; \ + if (x < bbox[0]) { \ + bbox[0] = x; \ + } \ + if (x >= bbox[2]) { \ + bbox[2] = x + 1; \ + } \ + } \ + } \ + if (has_data) { \ + if (bbox[1] < 0) { \ + bbox[1] = y; \ + } \ + bbox[3] = y + 1; \ + } \ } if (im->image8) { - GETBBOX(image8, 0xff); + GETBBOX(image8, 0xff); } else { - INT32 mask = 0xffffffff; - if (im->bands == 3) - ((UINT8*) &mask)[3] = 0; - GETBBOX(image32, mask); + INT32 mask = 0xffffffff; + if (im->bands == 3) { + ((UINT8 *)&mask)[3] = 0; + } else if ( + strcmp(im->mode, "RGBa") == 0 || strcmp(im->mode, "RGBA") == 0 || + strcmp(im->mode, "La") == 0 || strcmp(im->mode, "LA") == 0 || + strcmp(im->mode, "PA") == 0) { +#ifdef WORDS_BIGENDIAN + mask = 0x000000ff; +#else + mask = 0xff000000; +#endif + } + GETBBOX(image32, mask); } /* Check that we got a box */ - if (bbox[1] < 0) - return 0; /* no data */ + if (bbox[1] < 0) { + return 0; /* no data */ + } return 1; /* ok */ } - int -ImagingGetProjection(Imaging im, UINT8* xproj, UINT8* yproj) -{ +ImagingGetProjection(Imaging im, UINT8 *xproj, UINT8 *yproj) { /* Get projection arrays for non-zero data in the image.*/ int x, y; @@ -80,134 +90,152 @@ ImagingGetProjection(Imaging im, UINT8* xproj, UINT8* yproj) memset(xproj, 0, im->xsize); memset(yproj, 0, im->ysize); -#define GETPROJ(image, mask)\ - for (y = 0; y < im->ysize; y++) {\ - has_data = 0;\ - for (x = 0; x < im->xsize; x++)\ - if (im->image[y][x] & mask) {\ - has_data = 1;\ - xproj[x] = 1;\ - }\ - if (has_data)\ - yproj[y] = 1;\ +#define GETPROJ(image, mask) \ + for (y = 0; y < im->ysize; y++) { \ + has_data = 0; \ + for (x = 0; x < im->xsize; x++) { \ + if (im->image[y][x] & mask) { \ + has_data = 1; \ + xproj[x] = 1; \ + } \ + } \ + if (has_data) { \ + yproj[y] = 1; \ + } \ } if (im->image8) { - GETPROJ(image8, 0xff); + GETPROJ(image8, 0xff); } else { - INT32 mask = 0xffffffff; - if (im->bands == 3) - ((UINT8*) &mask)[3] = 0; - GETPROJ(image32, mask); + INT32 mask = 0xffffffff; + if (im->bands == 3) { + ((UINT8 *)&mask)[3] = 0; + } + GETPROJ(image32, mask); } return 1; /* ok */ } - int -ImagingGetExtrema(Imaging im, void *extrema) -{ +ImagingGetExtrema(Imaging im, void *extrema) { int x, y; INT32 imin, imax; FLOAT32 fmin, fmax; if (im->bands != 1) { - (void) ImagingError_ModeError(); + (void)ImagingError_ModeError(); return -1; /* mismatch */ } - if (!im->xsize || !im->ysize) + if (!im->xsize || !im->ysize) { return 0; /* zero size */ + } switch (im->type) { - case IMAGING_TYPE_UINT8: - imin = imax = im->image8[0][0]; - for (y = 0; y < im->ysize; y++) { - UINT8* in = im->image8[y]; - for (x = 0; x < im->xsize; x++) { - if (imin > in[x]) - imin = in[x]; - else if (imax < in[x]) - imax = in[x]; + case IMAGING_TYPE_UINT8: + imin = imax = im->image8[0][0]; + for (y = 0; y < im->ysize; y++) { + UINT8 *in = im->image8[y]; + for (x = 0; x < im->xsize; x++) { + if (imin > in[x]) { + imin = in[x]; + } else if (imax < in[x]) { + imax = in[x]; + } + } } - } - ((UINT8*) extrema)[0] = (UINT8) imin; - ((UINT8*) extrema)[1] = (UINT8) imax; - break; - case IMAGING_TYPE_INT32: - imin = imax = im->image32[0][0]; - for (y = 0; y < im->ysize; y++) { - INT32* in = im->image32[y]; - for (x = 0; x < im->xsize; x++) { - if (imin > in[x]) - imin = in[x]; - else if (imax < in[x]) - imax = in[x]; + ((UINT8 *)extrema)[0] = (UINT8)imin; + ((UINT8 *)extrema)[1] = (UINT8)imax; + break; + case IMAGING_TYPE_INT32: + imin = imax = im->image32[0][0]; + for (y = 0; y < im->ysize; y++) { + INT32 *in = im->image32[y]; + for (x = 0; x < im->xsize; x++) { + if (imin > in[x]) { + imin = in[x]; + } else if (imax < in[x]) { + imax = in[x]; + } + } } - } - ((INT32*) extrema)[0] = imin; - ((INT32*) extrema)[1] = imax; - break; - case IMAGING_TYPE_FLOAT32: - fmin = fmax = ((FLOAT32*) im->image32[0])[0]; - for (y = 0; y < im->ysize; y++) { - FLOAT32* in = (FLOAT32*) im->image32[y]; - for (x = 0; x < im->xsize; x++) { - if (fmin > in[x]) - fmin = in[x]; - else if (fmax < in[x]) - fmax = in[x]; + memcpy(extrema, &imin, sizeof(imin)); + memcpy(((char *)extrema) + sizeof(imin), &imax, sizeof(imax)); + break; + case IMAGING_TYPE_FLOAT32: + fmin = fmax = ((FLOAT32 *)im->image32[0])[0]; + for (y = 0; y < im->ysize; y++) { + FLOAT32 *in = (FLOAT32 *)im->image32[y]; + for (x = 0; x < im->xsize; x++) { + if (fmin > in[x]) { + fmin = in[x]; + } else if (fmax < in[x]) { + fmax = in[x]; + } + } } - } - ((FLOAT32*) extrema)[0] = fmin; - ((FLOAT32*) extrema)[1] = fmax; - break; - case IMAGING_TYPE_SPECIAL: - if (strcmp(im->mode, "I;16") == 0) { - imin = imax = ((UINT16*) im->image8[0])[0]; - for (y = 0; y < im->ysize; y++) { - UINT16* in = (UINT16 *) im->image[y]; - for (x = 0; x < im->xsize; x++) { - if (imin > in[x]) - imin = in[x]; - else if (imax < in[x]) - imax = in[x]; - } - } - ((UINT16*) extrema)[0] = (UINT16) imin; - ((UINT16*) extrema)[1] = (UINT16) imax; - break; - } - /* FALL THROUGH */ - default: - (void) ImagingError_ModeError(); - return -1; + memcpy(extrema, &fmin, sizeof(fmin)); + memcpy(((char *)extrema) + sizeof(fmin), &fmax, sizeof(fmax)); + break; + case IMAGING_TYPE_SPECIAL: + if (strcmp(im->mode, "I;16") == 0) { + UINT16 v; + UINT8 *pixel = *im->image8; +#ifdef WORDS_BIGENDIAN + v = pixel[0] + (pixel[1] << 8); +#else + memcpy(&v, pixel, sizeof(v)); +#endif + imin = imax = v; + for (y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++) { + pixel = (UINT8 *)im->image[y] + x * sizeof(v); +#ifdef WORDS_BIGENDIAN + v = pixel[0] + (pixel[1] << 8); +#else + memcpy(&v, pixel, sizeof(v)); +#endif + if (imin > v) { + imin = v; + } else if (imax < v) { + imax = v; + } + } + } + v = (UINT16)imin; + memcpy(extrema, &v, sizeof(v)); + v = (UINT16)imax; + memcpy(((char *)extrema) + sizeof(v), &v, sizeof(v)); + break; + } + /* FALL THROUGH */ + default: + (void)ImagingError_ModeError(); + return -1; } return 1; /* ok */ } - /* static ImagingColorItem* getcolors8(Imaging im, int maxcolors, int* size);*/ -static ImagingColorItem* getcolors32(Imaging im, int maxcolors, int* size); +static ImagingColorItem * +getcolors32(Imaging im, int maxcolors, int *size); -ImagingColorItem* -ImagingGetColors(Imaging im, int maxcolors, int* size) -{ +ImagingColorItem * +ImagingGetColors(Imaging im, int maxcolors, int *size) { /* FIXME: add support for 8-bit images */ return getcolors32(im, maxcolors, size); } -static ImagingColorItem* -getcolors32(Imaging im, int maxcolors, int* size) -{ +static ImagingColorItem * +getcolors32(Imaging im, int maxcolors, int *size) { unsigned int h; unsigned int i, incr; int colors; INT32 pixel_mask; int x, y; - ImagingColorItem* table; - ImagingColorItem* v; + ImagingColorItem *table; + ImagingColorItem *v; unsigned int code_size; unsigned int code_poly; @@ -218,19 +246,19 @@ getcolors32(Imaging im, int maxcolors, int* size) Python's Unicode property database (written by yours truly) /F */ static int SIZES[] = { - 4,3, 8,3, 16,3, 32,5, 64,3, 128,3, 256,29, 512,17, 1024,9, 2048,5, - 4096,83, 8192,27, 16384,43, 32768,3, 65536,45, 131072,9, 262144,39, - 524288,39, 1048576,9, 2097152,5, 4194304,3, 8388608,33, 16777216,27, - 33554432,9, 67108864,71, 134217728,39, 268435456,9, 536870912,5, - 1073741824,83, 0 - }; + 4, 3, 8, 3, 16, 3, 32, 5, 64, 3, + 128, 3, 256, 29, 512, 17, 1024, 9, 2048, 5, + 4096, 83, 8192, 27, 16384, 43, 32768, 3, 65536, 45, + 131072, 9, 262144, 39, 524288, 39, 1048576, 9, 2097152, 5, + 4194304, 3, 8388608, 33, 16777216, 27, 33554432, 9, 67108864, 71, + 134217728, 39, 268435456, 9, 536870912, 5, 1073741824, 83, 0}; code_size = code_poly = code_mask = 0; for (i = 0; SIZES[i]; i += 2) { if (SIZES[i] > maxcolors) { code_size = SIZES[i]; - code_poly = SIZES[i+1]; + code_poly = SIZES[i + 1]; code_mask = code_size - 1; break; } @@ -239,24 +267,28 @@ getcolors32(Imaging im, int maxcolors, int* size) /* printf("code_size=%d\n", code_size); */ /* printf("code_poly=%d\n", code_poly); */ - if (!code_size) - return ImagingError_MemoryError(); /* just give up */ + if (!code_size) { + return ImagingError_MemoryError(); /* just give up */ + } - if (!im->image32) - return ImagingError_ModeError(); + if (!im->image32) { + return ImagingError_ModeError(); + } table = calloc(code_size + 1, sizeof(ImagingColorItem)); - if (!table) - return ImagingError_MemoryError(); + if (!table) { + return ImagingError_MemoryError(); + } pixel_mask = 0xffffffff; - if (im->bands == 3) - ((UINT8*) &pixel_mask)[3] = 0; + if (im->bands == 3) { + ((UINT8 *)&pixel_mask)[3] = 0; + } colors = 0; for (y = 0; y < im->ysize; y++) { - INT32* p = im->image32[y]; + INT32 *p = im->image32[y]; for (x = 0; x < im->xsize; x++) { INT32 pixel = p[x] & pixel_mask; h = (pixel); /* null hashing */ @@ -264,9 +296,11 @@ getcolors32(Imaging im, int maxcolors, int* size) v = &table[i]; if (!v->count) { /* add to table */ - if (colors++ == maxcolors) + if (colors++ == maxcolors) { goto overflow; - v->x = x; v->y = y; + } + v->x = x; + v->y = y; v->pixel = pixel; v->count = 1; continue; @@ -275,16 +309,19 @@ getcolors32(Imaging im, int maxcolors, int* size) continue; } incr = (h ^ (h >> 3)) & code_mask; - if (!incr) + if (!incr) { incr = code_mask; + } for (;;) { i = (i + incr) & code_mask; v = &table[i]; if (!v->count) { /* add to table */ - if (colors++ == maxcolors) + if (colors++ == maxcolors) { goto overflow; - v->x = x; v->y = y; + } + v->x = x; + v->y = y; v->pixel = pixel; v->count = 1; break; @@ -293,8 +330,9 @@ getcolors32(Imaging im, int maxcolors, int* size) break; } incr = incr << 1; - if (incr > code_mask) + if (incr > code_mask) { incr = incr ^ code_poly; + } } } } @@ -302,10 +340,11 @@ getcolors32(Imaging im, int maxcolors, int* size) overflow: /* pack the table */ - for (x = y = 0; x < (int) code_size; x++) + for (x = y = 0; x < (int)code_size; x++) if (table[x].count) { - if (x != y) + if (x != y) { table[y] = table[x]; + } y++; } table[y].count = 0; /* mark end of table */ diff --git a/src/libImaging/Gif.h b/src/libImaging/Gif.h index 2cb95efd289..5d7e2bdaa96 100644 --- a/src/libImaging/Gif.h +++ b/src/libImaging/Gif.h @@ -7,17 +7,14 @@ * Copyright (c) Fredrik Lundh 1995-96. */ - /* Max size for a LZW code word. */ -#define GIFBITS 12 - -#define GIFTABLE (1< -#include /* memcpy() */ +#include /* memcpy() */ #include "Gif.h" - -#define NEWLINE(state, context) {\ - state->x = 0;\ - state->y += context->step;\ - while (state->y >= state->ysize)\ - switch (context->interlace) {\ - case 1:\ - context->repeat = state->y = 4;\ - context->interlace = 2;\ - break;\ - case 2:\ - context->step = 4;\ - context->repeat = state->y = 2;\ - context->interlace = 3;\ - break;\ - case 3:\ - context->step = 2;\ - context->repeat = state->y = 1;\ - context->interlace = 0;\ - break;\ - default:\ - return -1;\ - }\ - if (state->y < state->ysize)\ - out = im->image8[state->y + state->yoff] + state->xoff;\ -} - +#define NEWLINE(state, context) \ + { \ + state->x = 0; \ + state->y += context->step; \ + while (state->y >= state->ysize) switch (context->interlace) { \ + case 1: \ + context->repeat = state->y = 4; \ + context->interlace = 2; \ + break; \ + case 2: \ + context->step = 4; \ + context->repeat = state->y = 2; \ + context->interlace = 3; \ + break; \ + case 3: \ + context->step = 2; \ + context->repeat = state->y = 1; \ + context->interlace = 0; \ + break; \ + default: \ + return -1; \ + } \ + if (state->y < state->ysize) { \ + out = im->image8[state->y + state->yoff] + state->xoff; \ + } \ + } int -ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes) -{ - UINT8* p; - UINT8* out; +ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes) { + UINT8 *p; + UINT8 *out; int c, i; int thiscode; - GIFDECODERSTATE *context = (GIFDECODERSTATE*) state->context; + GIFDECODERSTATE *context = (GIFDECODERSTATE *)state->context; UINT8 *ptr = buffer; if (!state->state) { - - /* Initialise state */ - if (context->bits < 0 || context->bits > 12) { - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - } - - /* Clear code */ - context->clear = 1 << context->bits; - - /* End code */ - context->end = context->clear + 1; - - /* Interlace */ - if (context->interlace) { - context->interlace = 1; - context->step = context->repeat = 8; - } else - context->step = 1; - - state->state = 1; + /* Initialise state */ + if (context->bits < 0 || context->bits > 12) { + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + + /* Clear code */ + context->clear = 1 << context->bits; + + /* End code */ + context->end = context->clear + 1; + + /* Interlace */ + if (context->interlace) { + context->interlace = 1; + context->step = context->repeat = 8; + } else { + context->step = 1; + } + + state->state = 1; } out = im->image8[state->y + state->yoff] + state->xoff + state->x; for (;;) { + if (state->state == 1) { + /* First free entry in table */ + context->next = context->clear + 2; + + /* Initial code size */ + context->codesize = context->bits + 1; + context->codemask = (1 << context->codesize) - 1; + + /* Buffer pointer. We fill the buffer from right, which + allows us to return all of it in one operation. */ + context->bufferindex = GIFBUFFER; - if (state->state == 1) { - - /* First free entry in table */ - context->next = context->clear + 2; - - /* Initial code size */ - context->codesize = context->bits + 1; - context->codemask = (1 << context->codesize) - 1; - - /* Buffer pointer. We fill the buffer from right, which - allows us to return all of it in one operation. */ - context->bufferindex = GIFBUFFER; - - state->state = 2; - } - - if (context->bufferindex < GIFBUFFER) { - - /* Return whole buffer in one chunk */ - i = GIFBUFFER - context->bufferindex; - p = &context->buffer[context->bufferindex]; - - context->bufferindex = GIFBUFFER; - - } else { - - /* Get current symbol */ - - while (context->bitcount < context->codesize) { - - if (context->blocksize > 0) { - - /* Read next byte */ - c = *ptr++; bytes--; - - context->blocksize--; - - /* New bits are shifted in from from the left. */ - context->bitbuffer |= (INT32) c << context->bitcount; - context->bitcount += 8; - - } else { - - /* New GIF block */ - - /* We don't start decoding unless we have a full block */ - if (bytes < 1) - return ptr - buffer; - c = *ptr; - if (bytes < c+1) - return ptr - buffer; - - context->blocksize = c; - - ptr++; bytes--; - - } - } - - /* Extract current symbol from bit buffer. */ - c = (int) context->bitbuffer & context->codemask; - - /* Adjust buffer */ - context->bitbuffer >>= context->codesize; - context->bitcount -= context->codesize; - - /* If c is less than "clear", it's a data byte. Otherwise, - it's either clear/end or a code symbol which should be - expanded. */ - - if (c == context->clear) { - if (state->state != 2) - state->state = 1; - continue; - } - - if (c == context->end) - break; - - i = 1; - p = &context->lastdata; - - if (state->state == 2) { - - /* First valid symbol after clear; use as is */ - if (c > context->clear) { - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - context->lastdata = context->lastcode = c; - state->state = 3; - - } else { - - thiscode = c; - - if (c > context->next) { - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - if (c == context->next) { - - /* c == next is allowed. not sure why. */ - - if (context->bufferindex <= 0) { - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - context->buffer[--context->bufferindex] = - context->lastdata; - - c = context->lastcode; - - } - - while (c >= context->clear) { - - /* Copy data string to buffer (beginning from right) */ - - if (context->bufferindex <= 0 || c >= GIFTABLE) { - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - context->buffer[--context->bufferindex] = - context->data[c]; - - c = context->link[c]; - } - - context->lastdata = c; - - if (context->next < GIFTABLE) { + state->state = 2; + } - /* We'll only add this symbol if we have room - for it (take advise, Netscape!) */ - context->data[context->next] = c; - context->link[context->next] = context->lastcode; + if (context->bufferindex < GIFBUFFER) { + /* Return whole buffer in one chunk */ + i = GIFBUFFER - context->bufferindex; + p = &context->buffer[context->bufferindex]; - if (context->next == context->codemask && - context->codesize < GIFBITS) { + context->bufferindex = GIFBUFFER; - /* Expand code size */ - context->codesize++; - context->codemask = (1 << context->codesize) - 1; - } + } else { + /* Get current symbol */ - context->next++; + while (context->bitcount < context->codesize) { + if (context->blocksize > 0) { + /* Read next byte */ + c = *ptr++; + bytes--; - } + context->blocksize--; - context->lastcode = thiscode; + /* New bits are shifted in from the left. */ + context->bitbuffer |= (INT32)c << context->bitcount; + context->bitcount += 8; - } - } + } else { + /* New GIF block */ - /* Copy the bytes into the image */ - if (state->y >= state->ysize) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } + /* We don't start decoding unless we have a full block */ + if (bytes < 1) { + return ptr - buffer; + } + c = *ptr; + if (bytes < c + 1) { + return ptr - buffer; + } - /* To squeeze some extra pixels out of this loop, we test for - some common cases and handle them separately. */ + context->blocksize = c; - /* FIXME: should we handle the transparency index in here??? */ + ptr++; + bytes--; + } + } - if (i == 1) { - if (state->x < state->xsize-1) { - /* Single pixel, not at the end of the line. */ - *out++ = p[0]; - state->x++; - continue; - } - } else if (state->x + i <= state->xsize) { - /* This string fits into current line. */ - memcpy(out, p, i); - out += i; - state->x += i; - if (state->x == state->xsize) { - NEWLINE(state, context); - } - continue; - } + /* Extract current symbol from bit buffer. */ + c = (int)context->bitbuffer & context->codemask; - /* No shortcut, copy pixel by pixel */ - for (c = 0; c < i; c++) { - *out++ = p[c]; - if (++state->x >= state->xsize) { - NEWLINE(state, context); - } - } + /* Adjust buffer */ + context->bitbuffer >>= context->codesize; + context->bitcount -= context->codesize; + + /* If c is less than "clear", it's a data byte. Otherwise, + it's either clear/end or a code symbol which should be + expanded. */ + + if (c == context->clear) { + if (state->state != 2) { + state->state = 1; + } + continue; + } + + if (c == context->end) { + break; + } + + i = 1; + p = &context->lastdata; + + if (state->state == 2) { + /* First valid symbol after clear; use as is */ + if (c > context->clear) { + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + context->lastdata = context->lastcode = c; + state->state = 3; + + } else { + thiscode = c; + + if (c > context->next) { + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + if (c == context->next) { + /* c == next is allowed. not sure why. */ + + if (context->bufferindex <= 0) { + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + context->buffer[--context->bufferindex] = context->lastdata; + + c = context->lastcode; + } + + while (c >= context->clear) { + /* Copy data string to buffer (beginning from right) */ + + if (context->bufferindex <= 0 || c >= GIFTABLE) { + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + context->buffer[--context->bufferindex] = context->data[c]; + + c = context->link[c]; + } + + context->lastdata = c; + + if (context->next < GIFTABLE) { + /* We'll only add this symbol if we have room + for it (take the advice, Netscape!) */ + context->data[context->next] = c; + context->link[context->next] = context->lastcode; + + if (context->next == context->codemask && + context->codesize < GIFBITS) { + /* Expand code size */ + context->codesize++; + context->codemask = (1 << context->codesize) - 1; + } + + context->next++; + } + + context->lastcode = thiscode; + } + } + + /* Copy the bytes into the image */ + if (state->y >= state->ysize) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + + /* To squeeze some extra pixels out of this loop, we test for + some common cases and handle them separately. */ + + /* This cannot be used if there is transparency */ + if (context->transparency == -1) { + if (i == 1) { + if (state->x < state->xsize - 1) { + /* Single pixel, not at the end of the line. */ + *out++ = p[0]; + state->x++; + continue; + } + } else if (state->x + i <= state->xsize) { + /* This string fits into current line. */ + memcpy(out, p, i); + out += i; + state->x += i; + if (state->x == state->xsize) { + NEWLINE(state, context); + } + continue; + } + } + + /* No shortcut, copy pixel by pixel */ + for (c = 0; c < i; c++) { + if (p[c] != context->transparency) { + *out = p[c]; + } + out++; + if (++state->x >= state->xsize) { + NEWLINE(state, context); + } + } } return ptr - buffer; diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c index f211814ed03..f232454052a 100644 --- a/src/libImaging/GifEncode.c +++ b/src/libImaging/GifEncode.c @@ -5,11 +5,12 @@ * encoder for uncompressed GIF data * * history: - * 97-01-05 fl created (writes uncompressed data) - * 97-08-27 fl fixed off-by-one error in buffer size test - * 98-07-09 fl added interlace write support - * 99-02-07 fl rewritten, now uses a run-length encoding strategy - * 99-02-08 fl improved run-length encoding for long runs + * 97-01-05 fl created (writes uncompressed data) + * 97-08-27 fl fixed off-by-one error in buffer size test + * 98-07-09 fl added interlace write support + * 99-02-07 fl rewritten, now uses a run-length encoding strategy + * 99-02-08 fl improved run-length encoding for long runs + * 2020-12-12 rdg Reworked for LZW compression. * * Copyright (c) Secret Labs AB 1997-99. * Copyright (c) Fredrik Lundh 1997. @@ -21,193 +22,294 @@ #include "Gif.h" -/* codes from 0 to 255 are literals */ -#define CLEAR_CODE 256 -#define EOF_CODE 257 -#define FIRST_CODE 258 -#define LAST_CODE 511 - -enum { INIT, ENCODE, ENCODE_EOF, FLUSH, EXIT }; - -/* to make things a little less complicated, we use a simple output - queue to hold completed blocks. the following inlined function - adds a byte to the current block. it allocates a new block if - necessary. */ - -static inline int -emit(GIFENCODERSTATE *context, int byte) -{ - /* write a byte to the output buffer */ - - if (!context->block || context->block->size == 255) { - GIFENCODERBLOCK* block; - - /* no room in the current block (or no current block); - allocate a new one */ - - /* add current block to end of flush queue */ - if (context->block) { - block = context->flush; - while (block && block->next) - block = block->next; - if (block) - block->next = context->block; - else - context->flush = context->block; - } +enum { INIT, ENCODE, FINISH }; - /* get a new block */ - if (context->free) { - block = context->free; - context->free = NULL; - } else { - /* malloc check ok, small constant allocation */ - block = malloc(sizeof(GIFENCODERBLOCK)); - if (!block) - return 0; - } +/* GIF LZW encoder by Raymond Gardner. */ +/* Released here under PIL license. */ - block->size = 0; - block->next = NULL; +/* This LZW encoder conforms to the GIF LZW format specified in the original + * Compuserve GIF 87a and GIF 89a specifications (see e.g. + * https://www.w3.org/Graphics/GIF/spec-gif87.txt Appendix C and + * https://www.w3.org/Graphics/GIF/spec-gif89a.txt Appendix F). + */ - context->block = block; +/* Return values */ +#define GLZW_OK 0 +#define GLZW_NO_INPUT_AVAIL 1 +#define GLZW_NO_OUTPUT_AVAIL 2 +#define GLZW_INTERNAL_ERROR 3 - } +#define CODE_LIMIT 4096 - /* write new byte to block */ - context->block->data[context->block->size++] = byte; +/* Values of entry_state */ +enum { LZW_INITIAL, LZW_TRY_IN1, LZW_TRY_IN2, LZW_TRY_OUT1, LZW_TRY_OUT2, + LZW_FINISHED }; - return 1; -} +/* Values of control_state */ +enum { PUT_HEAD, PUT_INIT_CLEAR, PUT_CLEAR, PUT_LAST_HEAD, PUT_END }; -/* write a code word to the current block. this is a macro to make - sure it's inlined on all platforms */ - -#define EMIT(code) {\ - context->bitbuffer |= ((INT32) (code)) << context->bitcount;\ - context->bitcount += 9;\ - while (context->bitcount >= 8) {\ - if (!emit(context, (UINT8) context->bitbuffer)) {\ - state->errcode = IMAGING_CODEC_MEMORY;\ - return 0;\ - }\ - context->bitbuffer >>= 8;\ - context->bitcount -= 8;\ - }\ +static void glzwe_reset(GIFENCODERSTATE *st) { + st->next_code = st->end_code + 1; + st->max_code = 2 * st->clear_code - 1; + st->code_width = st->bits + 1; + memset(st->codes, 0, sizeof(st->codes)); } -/* write a run. we use a combination of literals and combinations of - literals. this can give quite decent compression for images with - long stretches of identical pixels. but remember: if you want - really good compression, use another file format. */ - -#define EMIT_RUN(label) {\ -label:\ - while (context->count > 0) {\ - int run = 2;\ - EMIT(context->last);\ - context->count--;\ - if (state->count++ == LAST_CODE) {\ - EMIT(CLEAR_CODE);\ - state->count = FIRST_CODE;\ - goto label;\ - }\ - while (context->count >= run) {\ - EMIT(state->count - 1);\ - context->count -= run;\ - run++;\ - if (state->count++ == LAST_CODE) {\ - EMIT(CLEAR_CODE);\ - state->count = FIRST_CODE;\ - goto label;\ - }\ - }\ - if (context->count > 1) {\ - EMIT(state->count - 1 - (run - context->count));\ - context->count = 0;\ - if (state->count++ == LAST_CODE) {\ - EMIT(CLEAR_CODE);\ - state->count = FIRST_CODE;\ - }\ - break;\ - }\ - }\ +static void glzwe_init(GIFENCODERSTATE *st) { + st->clear_code = 1 << st->bits; + st->end_code = st->clear_code + 1; + glzwe_reset(st); + st->entry_state = LZW_INITIAL; + st->buf_bits_left = 8; + st->code_buffer = 0; } -int -ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ - UINT8* ptr; - int this; - - GIFENCODERBLOCK* block; - GIFENCODERSTATE *context = (GIFENCODERSTATE*) state->context; - - if (!state->state) { +static int glzwe(GIFENCODERSTATE *st, const UINT8 *in_ptr, UINT8 *out_ptr, + UINT32 *in_avail, UINT32 *out_avail, + UINT32 end_of_data) { + switch (st->entry_state) { - /* place a clear code in the output buffer */ - context->bitbuffer = CLEAR_CODE; - context->bitcount = 9; - - state->count = FIRST_CODE; - - if (context->interlace) { - context->interlace = 1; - context->step = 8; - } else - context->step = 1; + case LZW_TRY_IN1: +get_first_byte: + if (!*in_avail) { + if (end_of_data) { + goto end_of_data; + } + st->entry_state = LZW_TRY_IN1; + return GLZW_NO_INPUT_AVAIL; + } + st->head = *in_ptr++; + (*in_avail)--; + + case LZW_TRY_IN2: +encode_loop: + if (!*in_avail) { + if (end_of_data) { + st->code = st->head; + st->put_state = PUT_LAST_HEAD; + goto put_code; + } + st->entry_state = LZW_TRY_IN2; + return GLZW_NO_INPUT_AVAIL; + } + st->tail = *in_ptr++; + (*in_avail)--; + + /* Knuth TAOCP vol 3 sec. 6.4 algorithm D. */ + /* Hash found experimentally to be pretty good. */ + /* This works ONLY with TABLE_SIZE a power of 2. */ + st->probe = ((st->head ^ (st->tail << 6)) * 31) & (TABLE_SIZE - 1); + while (st->codes[st->probe]) { + if ((st->codes[st->probe] & 0xFFFFF) == + ((st->head << 8) | st->tail)) { + st->head = st->codes[st->probe] >> 20; + goto encode_loop; + } else { + /* Reprobe decrement must be nonzero and relatively prime to table + * size. So, any odd positive number for power-of-2 size. */ + if ((st->probe -= ((st->tail << 2) | 1)) < 0) { + st->probe += TABLE_SIZE; + } + } + } + /* Key not found, probe is at empty slot. */ + st->code = st->head; + st->put_state = PUT_HEAD; + goto put_code; +insert_code_or_clear: /* jump here after put_code */ + if (st->next_code < CODE_LIMIT) { + st->codes[st->probe] = (st->next_code << 20) | + (st->head << 8) | st->tail; + if (st->next_code > st->max_code) { + st->max_code = st->max_code * 2 + 1; + st->code_width++; + } + st->next_code++; + } else { + st->code = st->clear_code; + st->put_state = PUT_CLEAR; + goto put_code; +reset_after_clear: /* jump here after put_code */ + glzwe_reset(st); + } + st->head = st->tail; + goto encode_loop; + + case LZW_INITIAL: + glzwe_reset(st); + st->code = st->clear_code; + st->put_state = PUT_INIT_CLEAR; +put_code: + st->code_bits_left = st->code_width; +check_buf_bits: + if (!st->buf_bits_left) { /* out buffer full */ + + case LZW_TRY_OUT1: + if (!*out_avail) { + st->entry_state = LZW_TRY_OUT1; + return GLZW_NO_OUTPUT_AVAIL; + } + *out_ptr++ = st->code_buffer; + (*out_avail)--; + st->code_buffer = 0; + st->buf_bits_left = 8; + } + /* code bits to pack */ + UINT32 n = st->buf_bits_left < st->code_bits_left + ? st->buf_bits_left : st->code_bits_left; + st->code_buffer |= + (st->code & ((1 << n) - 1)) << (8 - st->buf_bits_left); + st->code >>= n; + st->buf_bits_left -= n; + st->code_bits_left -= n; + if (st->code_bits_left) { + goto check_buf_bits; + } + switch (st->put_state) { + case PUT_INIT_CLEAR: + goto get_first_byte; + case PUT_HEAD: + goto insert_code_or_clear; + case PUT_CLEAR: + goto reset_after_clear; + case PUT_LAST_HEAD: + goto end_of_data; + case PUT_END: + goto flush_code_buffer; + default: + return GLZW_INTERNAL_ERROR; + } - context->last = -1; +end_of_data: + st->code = st->end_code; + st->put_state = PUT_END; + goto put_code; +flush_code_buffer: /* jump here after put_code */ + if (st->buf_bits_left < 8) { + + case LZW_TRY_OUT2: + if (!*out_avail) { + st->entry_state = LZW_TRY_OUT2; + return GLZW_NO_OUTPUT_AVAIL; + } + *out_ptr++ = st->code_buffer; + (*out_avail)--; + } + st->entry_state = LZW_FINISHED; + return GLZW_OK; - /* sanity check */ - if (state->xsize <= 0 || state->ysize <= 0) - state->state = ENCODE_EOF; + case LZW_FINISHED: + return GLZW_OK; + default: + return GLZW_INTERNAL_ERROR; } +} +/* -END- GIF LZW encoder. */ - ptr = buf; - - for (;;) - - switch (state->state) { - - case INIT: - case ENCODE: +int +ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) { + UINT8* ptr; + UINT8* sub_block_ptr; + UINT8* sub_block_limit; + UINT8* buf_limit; + GIFENCODERSTATE *context = (GIFENCODERSTATE*) state->context; + int r; - /* identify and store a run of pixels */ + UINT32 in_avail, in_used; + UINT32 out_avail, out_used; - if (state->x == 0 || state->x >= state->xsize) { + if (state->state == INIT) { + state->state = ENCODE; + glzwe_init(context); - if (!context->interlace && state->y >= state->ysize) { - state->state = ENCODE_EOF; - break; - } + if (context->interlace) { + context->interlace = 1; + context->step = 8; + } else { + context->step = 1; + } - if (context->flush) { - state->state = FLUSH; - break; + /* Need at least 2 bytes for data sub-block; 5 for empty image */ + if (bytes < 5) { + state->errcode = IMAGING_CODEC_CONFIG; + return 0; + } + /* sanity check */ + if (state->xsize <= 0 || state->ysize <= 0) { + /* Is this better than an error return? */ + /* This will handle any legal "LZW Minimum Code Size" */ + memset(buf, 0, 5); + in_avail = 0; + out_avail = 5; + r = glzwe(context, (const UINT8 *)"", buf + 1, &in_avail, &out_avail, 1); + if (r == GLZW_OK) { + r = 5 - out_avail; + if (r < 1 || r > 3) { + state->errcode = IMAGING_CODEC_BROKEN; + return 0; } + buf[0] = r; + state->errcode = IMAGING_CODEC_END; + return r + 2; + } else { + /* Should not be possible unless something external to this + * routine messes with our state data */ + state->errcode = IMAGING_CODEC_BROKEN; + return 0; + } + } + /* Init state->x to make if() below true the first time through. */ + state->x = state->xsize; + } - /* get another line of data */ - state->shuffle( - state->buffer, - (UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->xsize - ); - - state->x = 0; + buf_limit = buf + bytes; + sub_block_limit = sub_block_ptr = ptr = buf; + + /* On entry, buf is output buffer, bytes is space available in buf. + * Loop here getting input until buf is full or image is all encoded. */ + for (;;) { + /* Set up sub-block ptr and limit. sub_block_ptr stays at beginning + * of sub-block until it is full. ptr will advance when any data is + * placed in buf. + */ + if (ptr >= sub_block_limit) { + if (buf_limit - ptr < 2) { /* Need at least 2 for data sub-block */ + return ptr - buf; + } + sub_block_ptr = ptr; + sub_block_limit = sub_block_ptr + + (256 < buf_limit - sub_block_ptr ? + 256 : buf_limit - sub_block_ptr); + *ptr++ = 0; + } - if (state->state == INIT) { - /* preload the run-length buffer and get going */ - context->last = state->buffer[0]; - context->count = state->x = 1; - state->state = ENCODE; - } + /* Get next row of pixels. */ + /* This if() originally tested state->x==0 for the first time through. + * This no longer works, as the loop will not advance state->x if + * glzwe() does not consume any input; this would advance the row + * spuriously. Now pre-init state->x above for first time, and avoid + * entering if() when state->state is FINISH, or it will loop + * infinitely. + */ + if (state->x >= state->xsize && state->state == ENCODE) { + if (!context->interlace && state->y >= state->ysize) { + state->state = FINISH; + continue; + } - /* step forward, according to the interlace settings */ - state->y += context->step; - while (context->interlace && state->y >= state->ysize) - switch (context->interlace) { + /* get another line of data */ + state->shuffle( + state->buffer, + (UINT8*) im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, state->xsize + ); + state->x = 0; + + /* step forward, according to the interlace settings */ + state->y += context->step; + while (context->interlace && state->y >= state->ysize) { + switch (context->interlace) { case 1: state->y = 4; context->interlace = 2; @@ -225,96 +327,35 @@ ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) default: /* just make sure we don't loop forever */ context->interlace = 0; - } - - } - - this = state->buffer[state->x++]; - - if (this == context->last) - context->count++; - else { - EMIT_RUN(label1); - context->last = this; - context->count = 1; - } - break; - - - case ENCODE_EOF: - - /* write the final run */ - EMIT_RUN(label2); - - /* write an end of image marker */ - EMIT(EOF_CODE); - - /* empty the bit buffer */ - while (context->bitcount > 0) { - if (!emit(context, (UINT8) context->bitbuffer)) { - state->errcode = IMAGING_CODEC_MEMORY; - return 0; } - context->bitbuffer >>= 8; - context->bitcount -= 8; - } - - /* flush the last block, and exit */ - if (context->block) { - GIFENCODERBLOCK* block; - block = context->flush; - while (block && block->next) - block = block->next; - if (block) - block->next = context->block; - else - context->flush = context->block; - context->block = NULL; - } - - state->state = EXIT; - - /* fall through... */ - - case EXIT: - case FLUSH: - - while (context->flush) { - - /* get a block from the flush queue */ - block = context->flush; - - if (block->size > 0) { - - /* make sure it fits into the output buffer */ - if (bytes < block->size+1) - return ptr - buf; - - ptr[0] = block->size; - memcpy(ptr+1, block->data, block->size); - - ptr += block->size+1; - bytes -= block->size+1; - - } - - context->flush = block->next; - - if (context->free) - free(context->free); - context->free = block; - - } - - if (state->state == EXIT) { - /* this was the last block! */ - if (context->free) - free(context->free); - state->errcode = IMAGING_CODEC_END; - return ptr - buf; } + } - state->state = ENCODE; - break; + in_avail = state->xsize - state->x; /* bytes left in line */ + out_avail = sub_block_limit - ptr; /* bytes left in sub-block */ + r = glzwe(context, &state->buffer[state->x], ptr, &in_avail, + &out_avail, state->state == FINISH); + out_used = sub_block_limit - ptr - out_avail; + *sub_block_ptr += out_used; + ptr += out_used; + in_used = state->xsize - state->x - in_avail; + state->x += in_used; + + if (r == GLZW_OK) { + /* Should not be possible when end-of-data flag is false. */ + state->errcode = IMAGING_CODEC_END; + return ptr - buf; + } else if (r == GLZW_NO_INPUT_AVAIL) { + /* Used all the input line; get another line */ + continue; + } else if (r == GLZW_NO_OUTPUT_AVAIL) { + /* subblock is full */ + continue; + } else { + /* Should not be possible unless something external to this + * routine messes with our state data */ + state->errcode = IMAGING_CODEC_BROKEN; + return 0; } + } } diff --git a/src/libImaging/HexDecode.c b/src/libImaging/HexDecode.c index 14f5241dc5e..bd16cdbe1da 100644 --- a/src/libImaging/HexDecode.c +++ b/src/libImaging/HexDecode.c @@ -5,7 +5,7 @@ * decoder for hex encoded image data * * history: - * 96-05-16 fl Created + * 96-05-16 fl Created * * Copyright (c) Fredrik Lundh 1996. * Copyright (c) Secret Labs AB 1997. @@ -13,55 +13,51 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" -#define HEX(v) ((v >= '0' && v <= '9') ? v - '0' :\ - (v >= 'a' && v <= 'f') ? v - 'a' + 10 :\ - (v >= 'A' && v <= 'F') ? v - 'A' + 10 : -1) +#define HEX(v) \ + ((v >= '0' && v <= '9') ? v - '0' \ + : (v >= 'a' && v <= 'f') ? v - 'a' + 10 \ + : (v >= 'A' && v <= 'F') ? v - 'A' + 10 \ + : -1) int -ImagingHexDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ - UINT8* ptr; +ImagingHexDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + UINT8 *ptr; int a, b; ptr = buf; for (;;) { + if (bytes < 2) { + return ptr - buf; + } - if (bytes < 2) - return ptr - buf; - - a = HEX(ptr[0]); - b = HEX(ptr[1]); - - if (a < 0 || b < 0) { - - ptr++; - bytes--; - - } else { - - ptr += 2; - bytes -= 2; + a = HEX(ptr[0]); + b = HEX(ptr[1]); - state->buffer[state->x] = (a<<4) + b; + if (a < 0 || b < 0) { + ptr++; + bytes--; - if (++state->x >= state->bytes) { + } else { + ptr += 2; + bytes -= 2; - /* Got a full line, unpack it */ - state->shuffle((UINT8*) im->image[state->y], state->buffer, - state->xsize); + state->buffer[state->x] = (a << 4) + b; - state->x = 0; + if (++state->x >= state->bytes) { + /* Got a full line, unpack it */ + state->shuffle( + (UINT8 *)im->image[state->y], state->buffer, state->xsize); - if (++state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } - } + state->x = 0; - } + if (++state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } + } + } } } diff --git a/src/libImaging/Histo.c b/src/libImaging/Histo.c index 0bfc8dfe954..c5a547a647b 100644 --- a/src/libImaging/Histo.c +++ b/src/libImaging/Histo.c @@ -16,10 +16,8 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" - /* HISTOGRAM */ /* -------------------------------------------------------------------- * Take a histogram of an image. Returns a histogram object containing @@ -27,140 +25,174 @@ */ void -ImagingHistogramDelete(ImagingHistogram h) -{ - if (h->histogram) - free(h->histogram); - free(h); +ImagingHistogramDelete(ImagingHistogram h) { + if (h) { + if (h->histogram) { + free(h->histogram); + } + free(h); + } } ImagingHistogram -ImagingHistogramNew(Imaging im) -{ +ImagingHistogramNew(Imaging im) { ImagingHistogram h; /* Create histogram descriptor */ h = calloc(1, sizeof(struct ImagingHistogramInstance)); - strncpy(h->mode, im->mode, IMAGING_MODE_LENGTH); + if (!h) { + return (ImagingHistogram)ImagingError_MemoryError(); + } + strncpy(h->mode, im->mode, IMAGING_MODE_LENGTH - 1); + h->mode[IMAGING_MODE_LENGTH - 1] = 0; + h->bands = im->bands; h->histogram = calloc(im->pixelsize, 256 * sizeof(long)); + if (!h->histogram) { + free(h); + return (ImagingHistogram)ImagingError_MemoryError(); + } return h; } ImagingHistogram -ImagingGetHistogram(Imaging im, Imaging imMask, void* minmax) -{ +ImagingGetHistogram(Imaging im, Imaging imMask, void *minmax) { ImagingSectionCookie cookie; int x, y, i; ImagingHistogram h; INT32 imin, imax; FLOAT32 fmin, fmax, scale; - if (!im) - return ImagingError_ModeError(); + if (!im) { + return ImagingError_ModeError(); + } if (imMask) { - /* Validate mask */ - if (im->xsize != imMask->xsize || im->ysize != imMask->ysize) - return ImagingError_Mismatch(); - if (strcmp(imMask->mode, "1") != 0 && strcmp(imMask->mode, "L") != 0) - return ImagingError_ValueError("bad transparency mask"); + /* Validate mask */ + if (im->xsize != imMask->xsize || im->ysize != imMask->ysize) { + return ImagingError_Mismatch(); + } + if (strcmp(imMask->mode, "1") != 0 && strcmp(imMask->mode, "L") != 0) { + return ImagingError_ValueError("bad transparency mask"); + } } h = ImagingHistogramNew(im); + if (!h) { + return NULL; + } if (imMask) { - /* mask */ - if (im->image8) { + /* mask */ + if (im->image8) { ImagingSectionEnter(&cookie); - for (y = 0; y < im->ysize; y++) - for (x = 0; x < im->xsize; x++) - if (imMask->image8[y][x] != 0) - h->histogram[im->image8[y][x]]++; + for (y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++) { + if (imMask->image8[y][x] != 0) { + h->histogram[im->image8[y][x]]++; + } + } + } ImagingSectionLeave(&cookie); - } else { /* yes, we need the braces. C isn't Python! */ - if (im->type != IMAGING_TYPE_UINT8) + } else { /* yes, we need the braces. C isn't Python! */ + if (im->type != IMAGING_TYPE_UINT8) { + ImagingHistogramDelete(h); return ImagingError_ModeError(); + } ImagingSectionEnter(&cookie); - for (y = 0; y < im->ysize; y++) { - UINT8* in = (UINT8*) im->image32[y]; - for (x = 0; x < im->xsize; x++) - if (imMask->image8[y][x] != 0) { - h->histogram[(*in++)]++; - h->histogram[(*in++)+256]++; - h->histogram[(*in++)+512]++; - h->histogram[(*in++)+768]++; - } else - in += 4; - } + for (y = 0; y < im->ysize; y++) { + UINT8 *in = (UINT8 *)im->image32[y]; + for (x = 0; x < im->xsize; x++) { + if (imMask->image8[y][x] != 0) { + h->histogram[(*in++)]++; + h->histogram[(*in++) + 256]++; + h->histogram[(*in++) + 512]++; + h->histogram[(*in++) + 768]++; + } else { + in += 4; + } + } + } ImagingSectionLeave(&cookie); - } + } } else { - /* mask not given; process pixels in image */ - if (im->image8) { + /* mask not given; process pixels in image */ + if (im->image8) { ImagingSectionEnter(&cookie); - for (y = 0; y < im->ysize; y++) - for (x = 0; x < im->xsize; x++) - h->histogram[im->image8[y][x]]++; + for (y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++) { + h->histogram[im->image8[y][x]]++; + } + } ImagingSectionLeave(&cookie); - } else { + } else { switch (im->type) { - case IMAGING_TYPE_UINT8: - ImagingSectionEnter(&cookie); - for (y = 0; y < im->ysize; y++) { - UINT8* in = (UINT8*) im->image[y]; - for (x = 0; x < im->xsize; x++) { - h->histogram[(*in++)]++; - h->histogram[(*in++)+256]++; - h->histogram[(*in++)+512]++; - h->histogram[(*in++)+768]++; + case IMAGING_TYPE_UINT8: + ImagingSectionEnter(&cookie); + for (y = 0; y < im->ysize; y++) { + UINT8 *in = (UINT8 *)im->image[y]; + for (x = 0; x < im->xsize; x++) { + h->histogram[(*in++)]++; + h->histogram[(*in++) + 256]++; + h->histogram[(*in++) + 512]++; + h->histogram[(*in++) + 768]++; + } } - } - ImagingSectionLeave(&cookie); - break; - case IMAGING_TYPE_INT32: - if (!minmax) - return ImagingError_ValueError("min/max not given"); - if (!im->xsize || !im->ysize) - break; - imin = ((INT32*) minmax)[0]; - imax = ((INT32*) minmax)[1]; - if (imin >= imax) + ImagingSectionLeave(&cookie); break; - ImagingSectionEnter(&cookie); - scale = 255.0F / (imax - imin); - for (y = 0; y < im->ysize; y++) { - INT32* in = im->image32[y]; - for (x = 0; x < im->xsize; x++) { - i = (int) (((*in++)-imin)*scale); - if (i >= 0 && i < 256) - h->histogram[i]++; + case IMAGING_TYPE_INT32: + if (!minmax) { + ImagingHistogramDelete(h); + return ImagingError_ValueError("min/max not given"); } - } - ImagingSectionLeave(&cookie); - break; - case IMAGING_TYPE_FLOAT32: - if (!minmax) - return ImagingError_ValueError("min/max not given"); - if (!im->xsize || !im->ysize) - break; - fmin = ((FLOAT32*) minmax)[0]; - fmax = ((FLOAT32*) minmax)[1]; - if (fmin >= fmax) + if (!im->xsize || !im->ysize) { + break; + } + memcpy(&imin, minmax, sizeof(imin)); + memcpy(&imax, ((char *)minmax) + sizeof(imin), sizeof(imax)); + if (imin >= imax) { + break; + } + ImagingSectionEnter(&cookie); + scale = 255.0F / (imax - imin); + for (y = 0; y < im->ysize; y++) { + INT32 *in = im->image32[y]; + for (x = 0; x < im->xsize; x++) { + i = (int)(((*in++) - imin) * scale); + if (i >= 0 && i < 256) { + h->histogram[i]++; + } + } + } + ImagingSectionLeave(&cookie); break; - ImagingSectionEnter(&cookie); - scale = 255.0F / (fmax - fmin); - for (y = 0; y < im->ysize; y++) { - FLOAT32* in = (FLOAT32*) im->image32[y]; - for (x = 0; x < im->xsize; x++) { - i = (int) (((*in++)-fmin)*scale); - if (i >= 0 && i < 256) - h->histogram[i]++; + case IMAGING_TYPE_FLOAT32: + if (!minmax) { + ImagingHistogramDelete(h); + return ImagingError_ValueError("min/max not given"); } - } - ImagingSectionLeave(&cookie); - break; + if (!im->xsize || !im->ysize) { + break; + } + memcpy(&fmin, minmax, sizeof(fmin)); + memcpy(&fmax, ((char *)minmax) + sizeof(fmin), sizeof(fmax)); + if (fmin >= fmax) { + break; + } + ImagingSectionEnter(&cookie); + scale = 255.0F / (fmax - fmin); + for (y = 0; y < im->ysize; y++) { + FLOAT32 *in = (FLOAT32 *)im->image32[y]; + for (x = 0; x < im->xsize; x++) { + i = (int)(((*in++) - fmin) * scale); + if (i >= 0 && i < 256) { + h->histogram[i]++; + } + } + } + ImagingSectionLeave(&cookie); + break; } } } diff --git a/src/libImaging/ImDib.h b/src/libImaging/ImDib.h index e5a2cc0f639..91ff3f322ff 100644 --- a/src/libImaging/ImDib.h +++ b/src/libImaging/ImDib.h @@ -35,20 +35,27 @@ struct ImagingDIBInstance { ImagingShuffler unpack; }; -typedef struct ImagingDIBInstance* ImagingDIB; +typedef struct ImagingDIBInstance *ImagingDIB; -extern char* ImagingGetModeDIB(int size_out[2]); +extern char * +ImagingGetModeDIB(int size_out[2]); -extern ImagingDIB ImagingNewDIB(const char *mode, int xsize, int ysize); +extern ImagingDIB +ImagingNewDIB(const char *mode, int xsize, int ysize); -extern void ImagingDeleteDIB(ImagingDIB im); +extern void +ImagingDeleteDIB(ImagingDIB im); -extern void ImagingDrawDIB(ImagingDIB dib, void *dc, int dst[4], int src[4]); -extern void ImagingExposeDIB(ImagingDIB dib, void *dc); +extern void +ImagingDrawDIB(ImagingDIB dib, void *dc, int dst[4], int src[4]); +extern void +ImagingExposeDIB(ImagingDIB dib, void *dc); -extern int ImagingQueryPaletteDIB(ImagingDIB dib, void *dc); +extern int +ImagingQueryPaletteDIB(ImagingDIB dib, void *dc); -extern void ImagingPasteDIB(ImagingDIB dib, Imaging im, int xy[4]); +extern void +ImagingPasteDIB(ImagingDIB dib, Imaging im, int xy[4]); #if defined(__cplusplus) } diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 7b42510d495..af9996ca98c 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -9,11 +9,6 @@ #include "Python.h" -/* Workaround issue #2479 */ -#if PY_VERSION_HEX < 0x03070000 && defined(PySlice_GetIndicesEx) && !defined(PYPY_VERSION) -#undef PySlice_GetIndicesEx -#endif - /* Check that we have an ANSI compliant compiler */ #ifndef HAVE_PROTOTYPES #error Sorry, this library requires support for ANSI prototypes. @@ -30,56 +25,62 @@ #endif #endif -#ifdef _WIN32 +#if defined(_WIN32) || defined(__CYGWIN__) #define WIN32_LEAN_AND_MEAN #include +#ifdef __CYGWIN__ +#undef _WIN64 +#undef _WIN32 +#undef __WIN32__ +#undef WIN32 +#endif + #else /* For System that are not Windows, we'll need to define these. */ #if SIZEOF_SHORT == 2 -#define INT16 short +#define INT16 short #elif SIZEOF_INT == 2 -#define INT16 int +#define INT16 int #else -#define INT16 short /* most things works just fine anyway... */ +#define INT16 short /* most things works just fine anyway... */ #endif #if SIZEOF_SHORT == 4 -#define INT32 short +#define INT32 short #elif SIZEOF_INT == 4 -#define INT32 int +#define INT32 int #elif SIZEOF_LONG == 4 -#define INT32 long +#define INT32 long #else #error Cannot find required 32-bit integer type #endif #if SIZEOF_LONG == 8 -#define INT64 long +#define INT64 long #elif SIZEOF_LONG_LONG == 8 -#define INT64 long +#define INT64 long #endif -#define INT8 signed char -#define UINT8 unsigned char +#define INT8 signed char +#define UINT8 unsigned char -#define UINT16 unsigned INT16 -#define UINT32 unsigned INT32 +#define UINT16 unsigned INT16 +#define UINT32 unsigned INT32 #endif /* assume IEEE; tweak if necessary (patches are welcome) */ -#define FLOAT32 float -#define FLOAT64 double +#define FLOAT16 UINT16 +#define FLOAT32 float +#define FLOAT64 double #ifdef _MSC_VER -typedef signed __int64 int64_t; +typedef signed __int64 int64_t; #endif #ifdef __GNUC__ - #define GCC_VERSION (__GNUC__ * 10000 \ - + __GNUC_MINOR__ * 100 \ - + __GNUC_PATCHLEVEL__) +#define GCC_VERSION (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) #endif diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index aa59fe18c39..b65f8eadd51 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -10,20 +10,16 @@ * See the README file for information on usage and redistribution. */ - #include "ImPlatform.h" - #if defined(__cplusplus) extern "C" { #endif - #ifndef M_PI -#define M_PI 3.1415926535897932384626433832795 +#define M_PI 3.1415926535897932384626433832795 #endif - /* -------------------------------------------------------------------- */ /* @@ -57,12 +53,12 @@ extern "C" { /* Handles */ -typedef struct ImagingMemoryInstance* Imaging; +typedef struct ImagingMemoryInstance *Imaging; -typedef struct ImagingAccessInstance* ImagingAccess; -typedef struct ImagingHistogramInstance* ImagingHistogram; -typedef struct ImagingOutlineInstance* ImagingOutline; -typedef struct ImagingPaletteInstance* ImagingPalette; +typedef struct ImagingAccessInstance *ImagingAccess; +typedef struct ImagingHistogramInstance *ImagingHistogram; +typedef struct ImagingOutlineInstance *ImagingOutline; +typedef struct ImagingPaletteInstance *ImagingPalette; /* handle magics (used with PyCObject). */ #define IMAGING_MAGIC "PIL Imaging" @@ -73,7 +69,8 @@ typedef struct ImagingPaletteInstance* ImagingPalette; #define IMAGING_TYPE_FLOAT32 2 #define IMAGING_TYPE_SPECIAL 3 /* check mode for details */ -#define IMAGING_MODE_LENGTH 6+1 /* Band names ("1", "L", "P", "RGB", "RGBA", "CMYK", "YCbCr", "BGR;xy") */ +#define IMAGING_MODE_LENGTH \ + 6 + 1 /* Band names ("1", "L", "P", "RGB", "RGBA", "CMYK", "YCbCr", "BGR;xy") */ typedef struct { char *ptr; @@ -81,160 +78,179 @@ typedef struct { } ImagingMemoryBlock; struct ImagingMemoryInstance { - /* Format */ - char mode[IMAGING_MODE_LENGTH]; /* Band names ("1", "L", "P", "RGB", "RGBA", "CMYK", "YCbCr", "BGR;xy") */ - int type; /* Data type (IMAGING_TYPE_*) */ - int depth; /* Depth (ignored in this version) */ - int bands; /* Number of bands (1, 2, 3, or 4) */ - int xsize; /* Image dimension. */ + char mode[IMAGING_MODE_LENGTH]; /* Band names ("1", "L", "P", "RGB", "RGBA", "CMYK", + "YCbCr", "BGR;xy") */ + int type; /* Data type (IMAGING_TYPE_*) */ + int depth; /* Depth (ignored in this version) */ + int bands; /* Number of bands (1, 2, 3, or 4) */ + int xsize; /* Image dimension. */ int ysize; /* Colour palette (for "P" images only) */ ImagingPalette palette; /* Data pointers */ - UINT8 **image8; /* Set for 8-bit images (pixelsize=1). */ - INT32 **image32; /* Set for 32-bit images (pixelsize=4). */ + UINT8 **image8; /* Set for 8-bit images (pixelsize=1). */ + INT32 **image32; /* Set for 32-bit images (pixelsize=4). */ /* Internals */ - char **image; /* Actual raster data. */ - char *block; /* Set if data is allocated in a single block. */ - ImagingMemoryBlock *blocks; /* Memory blocks for pixel storage */ + char **image; /* Actual raster data. */ + char *block; /* Set if data is allocated in a single block. */ + ImagingMemoryBlock *blocks; /* Memory blocks for pixel storage */ - int pixelsize; /* Size of a pixel, in bytes (1, 2 or 4) */ - int linesize; /* Size of a line, in bytes (xsize * pixelsize) */ + int pixelsize; /* Size of a pixel, in bytes (1, 2 or 4) */ + int linesize; /* Size of a line, in bytes (xsize * pixelsize) */ /* Virtual methods */ void (*destroy)(Imaging im); }; - -#define IMAGING_PIXEL_1(im,x,y) ((im)->image8[(y)][(x)]) -#define IMAGING_PIXEL_L(im,x,y) ((im)->image8[(y)][(x)]) -#define IMAGING_PIXEL_LA(im,x,y) ((im)->image[(y)][(x)*4]) -#define IMAGING_PIXEL_P(im,x,y) ((im)->image8[(y)][(x)]) -#define IMAGING_PIXEL_PA(im,x,y) ((im)->image[(y)][(x)*4]) -#define IMAGING_PIXEL_I(im,x,y) ((im)->image32[(y)][(x)]) -#define IMAGING_PIXEL_F(im,x,y) (((FLOAT32*)(im)->image32[y])[x]) -#define IMAGING_PIXEL_RGB(im,x,y) ((im)->image[(y)][(x)*4]) -#define IMAGING_PIXEL_RGBA(im,x,y) ((im)->image[(y)][(x)*4]) -#define IMAGING_PIXEL_CMYK(im,x,y) ((im)->image[(y)][(x)*4]) -#define IMAGING_PIXEL_YCbCr(im,x,y) ((im)->image[(y)][(x)*4]) - -#define IMAGING_PIXEL_UINT8(im,x,y) ((im)->image8[(y)][(x)]) -#define IMAGING_PIXEL_INT32(im,x,y) ((im)->image32[(y)][(x)]) -#define IMAGING_PIXEL_FLOAT32(im,x,y) (((FLOAT32*)(im)->image32[y])[x]) +#define IMAGING_PIXEL_1(im, x, y) ((im)->image8[(y)][(x)]) +#define IMAGING_PIXEL_L(im, x, y) ((im)->image8[(y)][(x)]) +#define IMAGING_PIXEL_LA(im, x, y) ((im)->image[(y)][(x)*4]) +#define IMAGING_PIXEL_P(im, x, y) ((im)->image8[(y)][(x)]) +#define IMAGING_PIXEL_PA(im, x, y) ((im)->image[(y)][(x)*4]) +#define IMAGING_PIXEL_I(im, x, y) ((im)->image32[(y)][(x)]) +#define IMAGING_PIXEL_F(im, x, y) (((FLOAT32 *)(im)->image32[y])[x]) +#define IMAGING_PIXEL_RGB(im, x, y) ((im)->image[(y)][(x)*4]) +#define IMAGING_PIXEL_RGBA(im, x, y) ((im)->image[(y)][(x)*4]) +#define IMAGING_PIXEL_CMYK(im, x, y) ((im)->image[(y)][(x)*4]) +#define IMAGING_PIXEL_YCbCr(im, x, y) ((im)->image[(y)][(x)*4]) + +#define IMAGING_PIXEL_UINT8(im, x, y) ((im)->image8[(y)][(x)]) +#define IMAGING_PIXEL_INT32(im, x, y) ((im)->image32[(y)][(x)]) +#define IMAGING_PIXEL_FLOAT32(im, x, y) (((FLOAT32 *)(im)->image32[y])[x]) 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); + 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); }; struct ImagingHistogramInstance { - /* Format */ - char mode[IMAGING_MODE_LENGTH]; /* Band names (of corresponding source image) */ - int bands; /* Number of bands (1, 3, or 4) */ + char mode[IMAGING_MODE_LENGTH]; /* Band names (of corresponding source image) */ + int bands; /* Number of bands (1, 3, or 4) */ /* Data */ - long *histogram; /* Histogram (bands*256 longs) */ - + long *histogram; /* Histogram (bands*256 longs) */ }; - struct ImagingPaletteInstance { - /* Format */ - char mode[IMAGING_MODE_LENGTH]; /* Band names */ + char mode[IMAGING_MODE_LENGTH]; /* Band names */ /* Data */ - UINT8 palette[1024];/* Palette data (same format as image data) */ - - INT16* cache; /* Palette cache (used for predefined palettes) */ - int keep_cache; /* This palette will be reused; keep cache */ + int size; + UINT8 palette[1024]; /* Palette data (same format as image data) */ + INT16 *cache; /* Palette cache (used for predefined palettes) */ + int keep_cache; /* This palette will be reused; keep cache */ }; typedef struct ImagingMemoryArena { - int alignment; /* Alignment in memory of each line of an image */ - int block_size; /* Preferred block size, bytes */ - int blocks_max; /* Maximum number of cached blocks */ - int blocks_cached; /* Current number of blocks not associated with images */ + int alignment; /* Alignment in memory of each line of an image */ + int block_size; /* Preferred block size, bytes */ + int blocks_max; /* Maximum number of cached blocks */ + int blocks_cached; /* Current number of blocks not associated with images */ ImagingMemoryBlock *blocks_pool; - int stats_new_count; /* Number of new allocated images */ - int stats_allocated_blocks; /* Number of allocated blocks */ - int stats_reused_blocks; /* Number of blocks which were retrieved from a pool */ - int stats_reallocated_blocks; /* Number of blocks which were actually reallocated after retrieving */ - int stats_freed_blocks; /* Number of freed blocks */ -} *ImagingMemoryArena; - + int stats_new_count; /* Number of new allocated images */ + int stats_allocated_blocks; /* Number of allocated blocks */ + int stats_reused_blocks; /* Number of blocks which were retrieved from a pool */ + int stats_reallocated_blocks; /* Number of blocks which were actually reallocated + after retrieving */ + int stats_freed_blocks; /* Number of freed blocks */ +} * ImagingMemoryArena; /* Objects */ /* ------- */ extern struct ImagingMemoryArena ImagingDefaultArena; -extern int ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max); -extern void ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size); - -extern Imaging ImagingNew(const char* mode, int xsize, int ysize); -extern Imaging ImagingNewDirty(const char* mode, int xsize, int ysize); -extern Imaging ImagingNew2Dirty(const char* mode, Imaging imOut, Imaging imIn); -extern void ImagingDelete(Imaging im); - -extern Imaging ImagingNewBlock(const char* mode, int xsize, int ysize); -extern Imaging ImagingNewMap(const char* filename, int readonly, - const char* mode, int xsize, int ysize); - -extern Imaging ImagingNewPrologue(const char *mode, - int xsize, int ysize); -extern Imaging ImagingNewPrologueSubtype(const char *mode, - int xsize, int ysize, - int structure_size); - -extern void ImagingCopyPalette(Imaging destination, Imaging source); - -extern void ImagingHistogramDelete(ImagingHistogram histogram); - -extern void ImagingAccessInit(void); -extern ImagingAccess ImagingAccessNew(Imaging im); -extern void _ImagingAccessDelete(Imaging im, ImagingAccess access); +extern int +ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max); +extern void +ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size); + +extern Imaging +ImagingNew(const char *mode, int xsize, int ysize); +extern Imaging +ImagingNewDirty(const char *mode, int xsize, int ysize); +extern Imaging +ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn); +extern void +ImagingDelete(Imaging im); + +extern Imaging +ImagingNewBlock(const char *mode, int xsize, int ysize); + +extern Imaging +ImagingNewPrologue(const char *mode, int xsize, int ysize); +extern Imaging +ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int structure_size); + +extern void +ImagingCopyPalette(Imaging destination, Imaging source); + +extern void +ImagingHistogramDelete(ImagingHistogram histogram); + +extern void +ImagingAccessInit(void); +extern ImagingAccess +ImagingAccessNew(Imaging im); +extern void +_ImagingAccessDelete(Imaging im, ImagingAccess access); #define ImagingAccessDelete(im, access) /* nop, for now */ -extern ImagingPalette ImagingPaletteNew(const char *mode); -extern ImagingPalette ImagingPaletteNewBrowser(void); -extern ImagingPalette ImagingPaletteDuplicate(ImagingPalette palette); -extern void ImagingPaletteDelete(ImagingPalette palette); +extern ImagingPalette +ImagingPaletteNew(const char *mode); +extern ImagingPalette +ImagingPaletteNewBrowser(void); +extern ImagingPalette +ImagingPaletteDuplicate(ImagingPalette palette); +extern void +ImagingPaletteDelete(ImagingPalette palette); -extern int ImagingPaletteCachePrepare(ImagingPalette palette); -extern void ImagingPaletteCacheUpdate(ImagingPalette palette, - int r, int g, int b); -extern void ImagingPaletteCacheDelete(ImagingPalette palette); +extern int +ImagingPaletteCachePrepare(ImagingPalette palette); +extern void +ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b); +extern void +ImagingPaletteCacheDelete(ImagingPalette palette); -#define ImagingPaletteCache(p, r, g, b)\ - p->cache[(r>>2) + (g>>2)*64 + (b>>2)*64*64] +#define ImagingPaletteCache(p, r, g, b) \ + p->cache[(r >> 2) + (g >> 2) * 64 + (b >> 2) * 64 * 64] -extern Imaging ImagingQuantize(Imaging im, int colours, int mode, int kmeans); +extern Imaging +ImagingQuantize(Imaging im, int colours, int mode, int kmeans); /* Threading */ /* --------- */ -typedef void* ImagingSectionCookie; +typedef void *ImagingSectionCookie; -extern void ImagingSectionEnter(ImagingSectionCookie* cookie); -extern void ImagingSectionLeave(ImagingSectionCookie* cookie); +extern void +ImagingSectionEnter(ImagingSectionCookie *cookie); +extern void +ImagingSectionLeave(ImagingSectionCookie *cookie); /* Exceptions */ /* ---------- */ -extern void* ImagingError_IOError(void); -extern void* ImagingError_MemoryError(void); -extern void* ImagingError_ModeError(void); /* maps to ValueError by default */ -extern void* ImagingError_Mismatch(void); /* maps to ValueError by default */ -extern void* ImagingError_ValueError(const char* message); -extern void ImagingError_Clear(void); +extern void * +ImagingError_OSError(void); +extern void * +ImagingError_MemoryError(void); +extern void * +ImagingError_ModeError(void); /* maps to ValueError by default */ +extern void * +ImagingError_Mismatch(void); /* maps to ValueError by default */ +extern void * +ImagingError_ValueError(const char *message); +extern void +ImagingError_Clear(void); /* Transform callbacks */ /* ------------------- */ @@ -244,7 +260,6 @@ extern void ImagingError_Clear(void); #define IMAGING_TRANSFORM_PERSPECTIVE 2 #define IMAGING_TRANSFORM_QUAD 3 - /* standard filters */ #define IMAGING_TRANSFORM_NEAREST 0 #define IMAGING_TRANSFORM_BOX 4 @@ -253,251 +268,391 @@ extern void ImagingError_Clear(void); #define IMAGING_TRANSFORM_BICUBIC 3 #define IMAGING_TRANSFORM_LANCZOS 1 -typedef int (*ImagingTransformMap)(double* X, double* Y, - int x, int y, void* data); -typedef int (*ImagingTransformFilter)(void* out, Imaging im, - double x, double y); +typedef int (*ImagingTransformMap)(double *X, double *Y, int x, int y, void *data); +typedef int (*ImagingTransformFilter)(void *out, Imaging im, double x, double y); /* Image Manipulation Methods */ /* -------------------------- */ -extern Imaging ImagingAlphaComposite(Imaging imIn1, Imaging imIn2); -extern Imaging ImagingBlend(Imaging imIn1, Imaging imIn2, float alpha); -extern Imaging ImagingCopy(Imaging im); -extern Imaging ImagingConvert(Imaging im, const char* mode, ImagingPalette palette, int dither); -extern Imaging ImagingConvertInPlace(Imaging im, const char* mode); -extern Imaging ImagingConvertMatrix(Imaging im, const char *mode, float m[]); -extern Imaging ImagingConvertTransparent(Imaging im, const char *mode, int r, int g, int b); -extern Imaging ImagingCrop(Imaging im, int x0, int y0, int x1, int y1); -extern Imaging ImagingExpand(Imaging im, int x, int y, int mode); -extern Imaging ImagingFill(Imaging im, const void* ink); -extern int ImagingFill2( - Imaging into, const void* ink, Imaging mask, - int x0, int y0, int x1, int y1); -extern Imaging ImagingFillBand(Imaging im, int band, int color); -extern Imaging ImagingFillLinearGradient(const char* mode); -extern Imaging ImagingFillRadialGradient(const char* mode); -extern Imaging ImagingFilter( - Imaging im, int xsize, int ysize, const FLOAT32* kernel, - FLOAT32 offset); -extern Imaging ImagingFlipLeftRight(Imaging imOut, Imaging imIn); -extern Imaging ImagingFlipTopBottom(Imaging imOut, Imaging imIn); -extern Imaging ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, - int passes); -extern Imaging ImagingGetBand(Imaging im, int band); -extern Imaging ImagingMerge(const char* mode, Imaging bands[4]); -extern int ImagingSplit(Imaging im, Imaging bands[4]); -extern int ImagingGetBBox(Imaging im, int bbox[4]); -typedef struct { int x, y; INT32 count; INT32 pixel; } ImagingColorItem; -extern ImagingColorItem* ImagingGetColors(Imaging im, int maxcolors, - int *colors); -extern int ImagingGetExtrema(Imaging im, void *extrema); -extern int ImagingGetProjection(Imaging im, UINT8* xproj, UINT8* yproj); -extern ImagingHistogram ImagingGetHistogram( - Imaging im, Imaging mask, void *extrema); -extern Imaging ImagingModeFilter(Imaging im, int size); -extern Imaging ImagingNegative(Imaging im); -extern Imaging ImagingOffset(Imaging im, int xoffset, int yoffset); -extern int ImagingPaste( - Imaging into, Imaging im, Imaging mask, - int x0, int y0, int x1, int y1); -extern Imaging ImagingPoint( - Imaging im, const char* tablemode, const void* table); -extern Imaging ImagingPointTransform( - Imaging imIn, double scale, double offset); -extern Imaging ImagingPutBand(Imaging im, Imaging imIn, int band); -extern Imaging ImagingRankFilter(Imaging im, int size, int rank); -extern Imaging ImagingRotate90(Imaging imOut, Imaging imIn); -extern Imaging ImagingRotate180(Imaging imOut, Imaging imIn); -extern Imaging ImagingRotate270(Imaging imOut, Imaging imIn); -extern Imaging ImagingTranspose(Imaging imOut, Imaging imIn); -extern Imaging ImagingTransverse(Imaging imOut, Imaging imIn); -extern Imaging ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]); -extern Imaging ImagingTransform( - Imaging imOut, Imaging imIn, int method, int x0, int y0, int x1, int y1, - double *a, int filter, int fill); -extern Imaging ImagingUnsharpMask( - Imaging imOut, Imaging im, float radius, int percent, int threshold); -extern Imaging ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n); - -extern Imaging ImagingCopy2(Imaging imOut, Imaging imIn); -extern Imaging ImagingConvert2(Imaging imOut, Imaging imIn); +extern Imaging +ImagingAlphaComposite(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingBlend(Imaging imIn1, Imaging imIn2, float alpha); +extern Imaging +ImagingCopy(Imaging im); +extern Imaging +ImagingConvert(Imaging im, const char *mode, ImagingPalette palette, int dither); +extern Imaging +ImagingConvertInPlace(Imaging im, const char *mode); +extern Imaging +ImagingConvertMatrix(Imaging im, const char *mode, float m[]); +extern Imaging +ImagingConvertTransparent(Imaging im, const char *mode, int r, int g, int b); +extern Imaging +ImagingCrop(Imaging im, int x0, int y0, int x1, int y1); +extern Imaging +ImagingExpand(Imaging im, int x, int y, int mode); +extern Imaging +ImagingFill(Imaging im, const void *ink); +extern int +ImagingFill2( + Imaging into, const void *ink, Imaging mask, int x0, int y0, int x1, int y1); +extern Imaging +ImagingFillBand(Imaging im, int band, int color); +extern Imaging +ImagingFillLinearGradient(const char *mode); +extern Imaging +ImagingFillRadialGradient(const char *mode); +extern Imaging +ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 offset); +extern Imaging +ImagingFlipLeftRight(Imaging imOut, Imaging imIn); +extern Imaging +ImagingFlipTopBottom(Imaging imOut, Imaging imIn); +extern Imaging +ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes); +extern Imaging +ImagingGetBand(Imaging im, int band); +extern Imaging +ImagingMerge(const char *mode, Imaging bands[4]); +extern int +ImagingSplit(Imaging im, Imaging bands[4]); +extern int +ImagingGetBBox(Imaging im, int bbox[4]); +typedef struct { + int x, y; + INT32 count; + INT32 pixel; +} ImagingColorItem; +extern ImagingColorItem * +ImagingGetColors(Imaging im, int maxcolors, int *colors); +extern int +ImagingGetExtrema(Imaging im, void *extrema); +extern int +ImagingGetProjection(Imaging im, UINT8 *xproj, UINT8 *yproj); +extern ImagingHistogram +ImagingGetHistogram(Imaging im, Imaging mask, void *extrema); +extern Imaging +ImagingModeFilter(Imaging im, int size); +extern Imaging +ImagingNegative(Imaging im); +extern Imaging +ImagingOffset(Imaging im, int xoffset, int yoffset); +extern int +ImagingPaste(Imaging into, Imaging im, Imaging mask, int x0, int y0, int x1, int y1); +extern Imaging +ImagingPoint(Imaging im, const char *tablemode, const void *table); +extern Imaging +ImagingPointTransform(Imaging imIn, double scale, double offset); +extern Imaging +ImagingPutBand(Imaging im, Imaging imIn, int band); +extern Imaging +ImagingRankFilter(Imaging im, int size, int rank); +extern Imaging +ImagingRotate90(Imaging imOut, Imaging imIn); +extern Imaging +ImagingRotate180(Imaging imOut, Imaging imIn); +extern Imaging +ImagingRotate270(Imaging imOut, Imaging imIn); +extern Imaging +ImagingTranspose(Imaging imOut, Imaging imIn); +extern Imaging +ImagingTransverse(Imaging imOut, Imaging imIn); +extern Imaging +ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]); +extern Imaging +ImagingReduce(Imaging imIn, int xscale, int yscale, int box[4]); +extern Imaging +ImagingTransform( + Imaging imOut, + Imaging imIn, + int method, + int x0, + int y0, + int x1, + int y1, + double a[8], + int filter, + int fill); +extern Imaging +ImagingUnsharpMask(Imaging imOut, Imaging im, float radius, int percent, int threshold); +extern Imaging +ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n); +extern Imaging +ImagingColorLUT3D_linear( + Imaging imOut, + Imaging imIn, + int table_channels, + int size1D, + int size2D, + int size3D, + INT16 *table); + +extern Imaging +ImagingCopy2(Imaging imOut, Imaging imIn); +extern Imaging +ImagingConvert2(Imaging imOut, Imaging imIn); /* Channel operations */ /* any mode, except "F" */ -extern Imaging ImagingChopLighter(Imaging imIn1, Imaging imIn2); -extern Imaging ImagingChopDarker(Imaging imIn1, Imaging imIn2); -extern Imaging ImagingChopDifference(Imaging imIn1, Imaging imIn2); -extern Imaging ImagingChopMultiply(Imaging imIn1, Imaging imIn2); -extern Imaging ImagingChopScreen(Imaging imIn1, Imaging imIn2); -extern Imaging ImagingChopAdd( - Imaging imIn1, Imaging imIn2, float scale, int offset); -extern Imaging ImagingChopSubtract( - Imaging imIn1, Imaging imIn2, float scale, int offset); -extern Imaging ImagingChopAddModulo(Imaging imIn1, Imaging imIn2); -extern Imaging ImagingChopSubtractModulo(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopLighter(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopDarker(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopDifference(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopMultiply(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopScreen(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopAdd(Imaging imIn1, Imaging imIn2, float scale, int offset); +extern Imaging +ImagingChopSubtract(Imaging imIn1, Imaging imIn2, float scale, int offset); +extern Imaging +ImagingChopAddModulo(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopSubtractModulo(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopSoftLight(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopHardLight(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingOverlay(Imaging imIn1, Imaging imIn2); /* "1" images only */ -extern Imaging ImagingChopAnd(Imaging imIn1, Imaging imIn2); -extern Imaging ImagingChopOr(Imaging imIn1, Imaging imIn2); -extern Imaging ImagingChopXor(Imaging imIn1, Imaging imIn2); - -/* Image measurement */ -extern void ImagingCrack(Imaging im, int x0, int y0); +extern Imaging +ImagingChopAnd(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopOr(Imaging imIn1, Imaging imIn2); +extern Imaging +ImagingChopXor(Imaging imIn1, Imaging imIn2); /* Graphics */ -extern int ImagingDrawArc(Imaging im, int x0, int y0, int x1, int y1, - float start, float end, const void* ink, int op); -extern int ImagingDrawBitmap(Imaging im, int x0, int y0, Imaging bitmap, - const void* ink, int op); -extern int ImagingDrawChord(Imaging im, int x0, int y0, int x1, int y1, - float start, float end, const void* ink, int fill, - int op); -extern int ImagingDrawEllipse(Imaging im, int x0, int y0, int x1, int y1, - const void* ink, int fill, int op); -extern int ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, - const void* ink, int op); -extern int ImagingDrawWideLine(Imaging im, int x0, int y0, int x1, int y1, - const void* ink, int width, int op); -extern int ImagingDrawPieslice(Imaging im, int x0, int y0, int x1, int y1, - float start, float end, const void* ink, int fill, - int op); -extern int ImagingDrawPoint(Imaging im, int x, int y, const void* ink, int op); -extern int ImagingDrawPolygon(Imaging im, int points, int *xy, - const void* ink, int fill, int op); -extern int ImagingDrawRectangle(Imaging im, int x0, int y0, int x1, int y1, - const void* ink, int fill, int op); +extern int +ImagingDrawArc( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink, + int width, + int op); +extern int +ImagingDrawBitmap(Imaging im, int x0, int y0, Imaging bitmap, const void *ink, int op); +extern int +ImagingDrawChord( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink, + int fill, + int width, + int op); +extern int +ImagingDrawEllipse( + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink, + int fill, + int width, + int op); +extern int +ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink, int op); +extern int +ImagingDrawWideLine( + Imaging im, int x0, int y0, int x1, int y1, const void *ink, int width, int op); +extern int +ImagingDrawPieslice( + Imaging im, + int x0, + int y0, + int x1, + int y1, + float start, + float end, + const void *ink, + int fill, + int width, + int op); +extern int +ImagingDrawPoint(Imaging im, int x, int y, const void *ink, int op); +extern int +ImagingDrawPolygon(Imaging im, int points, int *xy, const void *ink, int fill, int width, int op); +extern int +ImagingDrawRectangle( + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink, + int fill, + int width, + int op); /* Level 2 graphics (WORK IN PROGRESS) */ -extern ImagingOutline ImagingOutlineNew(void); -extern void ImagingOutlineDelete(ImagingOutline outline); - -extern int ImagingDrawOutline(Imaging im, ImagingOutline outline, - const void* ink, int fill, int op); - -extern int ImagingOutlineMove(ImagingOutline outline, float x, float y); -extern int ImagingOutlineLine(ImagingOutline outline, float x, float y); -extern int ImagingOutlineCurve(ImagingOutline outline, float x1, float y1, - float x2, float y2, float x3, float y3); -extern int ImagingOutlineTransform(ImagingOutline outline, double a[6]); - -extern int ImagingOutlineClose(ImagingOutline outline); +extern ImagingOutline +ImagingOutlineNew(void); +extern void +ImagingOutlineDelete(ImagingOutline outline); + +extern int +ImagingDrawOutline( + Imaging im, ImagingOutline outline, const void *ink, int fill, int op); + +extern int +ImagingOutlineMove(ImagingOutline outline, float x, float y); +extern int +ImagingOutlineLine(ImagingOutline outline, float x, float y); +extern int +ImagingOutlineCurve( + ImagingOutline outline, float x1, float y1, float x2, float y2, float x3, float y3); +extern int +ImagingOutlineTransform(ImagingOutline outline, double a[6]); + +extern int +ImagingOutlineClose(ImagingOutline outline); /* Special effects */ -extern Imaging ImagingEffectSpread(Imaging imIn, int distance); -extern Imaging ImagingEffectNoise(int xsize, int ysize, float sigma); -extern Imaging ImagingEffectMandelbrot(int xsize, int ysize, - double extent[4], int quality); - -/* Obsolete */ -extern int ImagingToString(Imaging im, int orientation, char *buffer); -extern int ImagingFromString(Imaging im, int orientation, char *buffer); - +extern Imaging +ImagingEffectSpread(Imaging imIn, int distance); +extern Imaging +ImagingEffectNoise(int xsize, int ysize, float sigma); +extern Imaging +ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality); /* File I/O */ /* -------- */ /* Built-in drivers */ -extern Imaging ImagingOpenPPM(const char* filename); -extern int ImagingSavePPM(Imaging im, const char* filename); - -/* Utility functions */ -extern UINT32 ImagingCRC32(UINT32 crc, UINT8* buffer, int bytes); +extern Imaging +ImagingOpenPPM(const char *filename); +extern int +ImagingSavePPM(Imaging im, const char *filename); /* Codecs */ typedef struct ImagingCodecStateInstance *ImagingCodecState; -typedef int (*ImagingCodec)(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); - -extern int ImagingBcnDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingBitDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingEpsEncode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingFliDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingGifDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingGifEncode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingHexDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -#ifdef HAVE_LIBJPEG -extern int ImagingJpegDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingJpegDecodeCleanup(ImagingCodecState state); -extern int ImagingJpegUseJCSExtensions(void); - -extern int ImagingJpegEncode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); +typedef int (*ImagingCodec)( + Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); + +extern int +ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingEpsEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +extern int +ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +extern int +ImagingHexDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +#ifdef HAVE_LIBJPEG +extern int +ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingJpegDecodeCleanup(ImagingCodecState state); +extern int +ImagingJpegUseJCSExtensions(void); + +extern int +ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); #endif #ifdef HAVE_OPENJPEG -extern int ImagingJpeg2KDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingJpeg2KDecodeCleanup(ImagingCodecState state); -extern int ImagingJpeg2KEncode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingJpeg2KEncodeCleanup(ImagingCodecState state); +extern int +ImagingJpeg2KDecode( + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingJpeg2KDecodeCleanup(ImagingCodecState state); +extern int +ImagingJpeg2KEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +extern int +ImagingJpeg2KEncodeCleanup(ImagingCodecState state); #endif -#ifdef HAVE_LIBTIFF -extern int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingLibTiffEncode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); +#ifdef HAVE_LIBTIFF +extern int +ImagingLibTiffDecode( + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); #endif -#ifdef HAVE_LIBMPEG -extern int ImagingMpegDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); +#ifdef HAVE_LIBMPEG +extern int +ImagingMpegDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); #endif -extern int ImagingMspDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingPackbitsDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingPcdDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingPcxDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingPcxEncode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingRawDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingRawEncode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingSgiRleDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingSgiRleDecodeCleanup(ImagingCodecState state); -extern int ImagingSunRleDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingTgaRleDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingXbmDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingXbmEncode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -#ifdef HAVE_LIBZ -extern int ImagingZipDecode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingZipDecodeCleanup(ImagingCodecState state); -extern int ImagingZipEncode(Imaging im, ImagingCodecState state, - UINT8* buffer, int bytes); -extern int ImagingZipEncodeCleanup(ImagingCodecState state); +extern int +ImagingMspDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingPackbitsDecode( + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingPcdDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingPcxEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +extern int +ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingRawEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +extern int +ImagingSgiRleDecode( + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingSunRleDecode( + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingTgaRleDecode( + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +extern int +ImagingXbmDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingXbmEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +#ifdef HAVE_LIBZ +extern int +ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); +extern int +ImagingZipDecodeCleanup(ImagingCodecState state); +extern int +ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +extern int +ImagingZipEncodeCleanup(ImagingCodecState state); #endif -typedef void (*ImagingShuffler)(UINT8* out, const UINT8* in, int pixels); +typedef void (*ImagingShuffler)(UINT8 *out, const UINT8 *in, int pixels); /* Public shufflers */ -extern void ImagingPackBGR(UINT8* out, const UINT8* in, int pixels); -extern void ImagingUnpackYCC(UINT8* out, const UINT8* in, int pixels); -extern void ImagingUnpackYCCA(UINT8* out, const UINT8* in, int pixels); - -extern void ImagingConvertRGB2YCbCr(UINT8* out, const UINT8* in, int pixels); -extern void ImagingConvertYCbCr2RGB(UINT8* out, const UINT8* in, int pixels); - -extern ImagingShuffler ImagingFindUnpacker(const char* mode, - const char* rawmode, int* bits_out); -extern ImagingShuffler ImagingFindPacker(const char* mode, - const char* rawmode, int* bits_out); +extern void +ImagingPackBGR(UINT8 *out, const UINT8 *in, int pixels); +extern void +ImagingUnpackYCC(UINT8 *out, const UINT8 *in, int pixels); +extern void +ImagingUnpackYCCA(UINT8 *out, const UINT8 *in, int pixels); + +extern void +ImagingConvertRGB2YCbCr(UINT8 *out, const UINT8 *in, int pixels); +extern void +ImagingConvertYCbCr2RGB(UINT8 *out, const UINT8 *in, int pixels); + +extern ImagingShuffler +ImagingFindUnpacker(const char *mode, const char *rawmode, int *bits_out); +extern ImagingShuffler +ImagingFindPacker(const char *mode, const char *rawmode, int *bits_out); struct ImagingCodecStateInstance { int count; @@ -513,27 +668,26 @@ struct ImagingCodecStateInstance { PyObject *fd; }; - - /* Codec read/write python fd */ -extern Py_ssize_t _imaging_read_pyFd(PyObject *fd, char* dest, Py_ssize_t bytes); -extern Py_ssize_t _imaging_write_pyFd(PyObject *fd, char* src, Py_ssize_t bytes); -extern int _imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence); -extern Py_ssize_t _imaging_tell_pyFd(PyObject *fd); - - +extern Py_ssize_t +_imaging_read_pyFd(PyObject *fd, char *dest, Py_ssize_t bytes); +extern Py_ssize_t +_imaging_write_pyFd(PyObject *fd, char *src, Py_ssize_t bytes); +extern int +_imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence); +extern Py_ssize_t +_imaging_tell_pyFd(PyObject *fd); /* Errcodes */ -#define IMAGING_CODEC_END 1 -#define IMAGING_CODEC_OVERRUN -1 -#define IMAGING_CODEC_BROKEN -2 -#define IMAGING_CODEC_UNKNOWN -3 -#define IMAGING_CODEC_CONFIG -8 -#define IMAGING_CODEC_MEMORY -9 - - +#define IMAGING_CODEC_END 1 +#define IMAGING_CODEC_OVERRUN -1 +#define IMAGING_CODEC_BROKEN -2 +#define IMAGING_CODEC_UNKNOWN -3 +#define IMAGING_CODEC_CONFIG -8 +#define IMAGING_CODEC_MEMORY -9 #include "ImagingUtils.h" +extern UINT8 *clip8_lookups; #if defined(__cplusplus) } diff --git a/src/libImaging/ImagingUtils.h b/src/libImaging/ImagingUtils.h index b040fc303f4..0c0c1eda917 100644 --- a/src/libImaging/ImagingUtils.h +++ b/src/libImaging/ImagingUtils.h @@ -1,45 +1,42 @@ #ifdef WORDS_BIGENDIAN - #define MAKE_UINT32(u0, u1, u2, u3) (u3 | (u2<<8) | (u1<<16) | (u0<<24)) - #define MASK_UINT32_CHANNEL_0 0xff000000 - #define MASK_UINT32_CHANNEL_1 0x00ff0000 - #define MASK_UINT32_CHANNEL_2 0x0000ff00 - #define MASK_UINT32_CHANNEL_3 0x000000ff +#define MAKE_UINT32(u0, u1, u2, u3) \ + ((UINT32)(u3) | ((UINT32)(u2) << 8) | ((UINT32)(u1) << 16) | ((UINT32)(u0) << 24)) +#define MASK_UINT32_CHANNEL_0 0xff000000 +#define MASK_UINT32_CHANNEL_1 0x00ff0000 +#define MASK_UINT32_CHANNEL_2 0x0000ff00 +#define MASK_UINT32_CHANNEL_3 0x000000ff #else - #define MAKE_UINT32(u0, u1, u2, u3) (u0 | (u1<<8) | (u2<<16) | (u3<<24)) - #define MASK_UINT32_CHANNEL_0 0x000000ff - #define MASK_UINT32_CHANNEL_1 0x0000ff00 - #define MASK_UINT32_CHANNEL_2 0x00ff0000 - #define MASK_UINT32_CHANNEL_3 0xff000000 +#define MAKE_UINT32(u0, u1, u2, u3) \ + ((UINT32)(u0) | ((UINT32)(u1) << 8) | ((UINT32)(u2) << 16) | ((UINT32)(u3) << 24)) +#define MASK_UINT32_CHANNEL_0 0x000000ff +#define MASK_UINT32_CHANNEL_1 0x0000ff00 +#define MASK_UINT32_CHANNEL_2 0x00ff0000 +#define MASK_UINT32_CHANNEL_3 0xff000000 #endif - -#define SHIFTFORDIV255(a)\ - ((((a) >> 8) + a) >> 8) +#define SHIFTFORDIV255(a) ((((a) >> 8) + a) >> 8) /* like (a * b + 127) / 255), but much faster on most platforms */ -#define MULDIV255(a, b, tmp)\ - (tmp = (a) * (b) + 128, SHIFTFORDIV255(tmp)) +#define MULDIV255(a, b, tmp) (tmp = (a) * (b) + 128, SHIFTFORDIV255(tmp)) -#define DIV255(a, tmp)\ - (tmp = (a) + 128, SHIFTFORDIV255(tmp)) +#define DIV255(a, tmp) (tmp = (a) + 128, SHIFTFORDIV255(tmp)) -#define BLEND(mask, in1, in2, tmp1)\ - DIV255(in1 * (255 - mask) + in2 * mask, tmp1) +#define BLEND(mask, in1, in2, tmp1) DIV255(in1 *(255 - mask) + in2 * mask, tmp1) -#define PREBLEND(mask, in1, in2, tmp1)\ - (MULDIV255(in1, (255 - mask), tmp1) + in2) +#define PREBLEND(mask, in1, in2, tmp1) (MULDIV255(in1, (255 - mask), tmp1) + in2) +#define CLIP8(v) ((v) <= 0 ? 0 : (v) < 256 ? (v) : 255) /* This is to work around a bug in GCC prior 4.9 in 64 bit mode. GCC generates code with partial dependency which is 3 times slower. - See: http://stackoverflow.com/a/26588074/253146 */ -#if defined(__x86_64__) && defined(__SSE__) && ! defined(__NO_INLINE__) && \ - ! defined(__clang__) && defined(GCC_VERSION) && (GCC_VERSION < 40900) + See: https://stackoverflow.com/a/26588074/253146 */ +#if defined(__x86_64__) && defined(__SSE__) && !defined(__NO_INLINE__) && \ + !defined(__clang__) && defined(GCC_VERSION) && (GCC_VERSION < 40900) static float __attribute__((always_inline)) inline _i2f(int v) { float x; - __asm__("xorps %0, %0; cvtsi2ss %1, %0" : "=X"(x) : "r"(v) ); + __asm__("xorps %0, %0; cvtsi2ss %1, %0" : "=x"(x) : "r"(v)); return x; } #else -static float inline _i2f(int v) { return (float) v; } +static float inline _i2f(int v) { return (float)v; } #endif diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index 82e1b449fec..a876d3bb6d9 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -12,15 +12,13 @@ #include - typedef struct { - struct jpeg_error_mgr pub; /* "public" fields */ - jmp_buf setjmp_buffer; /* for return to caller */ + struct jpeg_error_mgr pub; /* "public" fields */ + jmp_buf setjmp_buffer; /* for return to caller */ } JPEGERROR; - /* -------------------------------------------------------------------- */ -/* Decoder */ +/* Decoder */ typedef struct { struct jpeg_source_mgr pub; @@ -28,15 +26,14 @@ typedef struct { } JPEGSOURCE; typedef struct { - /* CONFIGURATION */ /* Jpeg file mode (empty if not known) */ - char jpegmode[8+1]; + char jpegmode[8 + 1]; /* Converter output mode (input to the shuffler). If empty, convert conversions are disabled */ - char rawmode[8+1]; + char rawmode[8 + 1]; /* If set, trade quality for speed */ int draft; @@ -54,9 +51,8 @@ typedef struct { } JPEGSTATE; - /* -------------------------------------------------------------------- */ -/* Encoder */ +/* Encoder */ typedef struct { struct jpeg_destination_mgr pub; @@ -64,10 +60,9 @@ typedef struct { } JPEGDESTINATION; typedef struct { - /* CONFIGURATION */ - /* Quality (1-100, 0 means default) */ + /* Quality (0-100, -1 means default) */ int quality; /* Progressive mode */ @@ -89,7 +84,7 @@ typedef struct { int subsampling; /* Converter input mode (input to the shuffler) */ - char rawmode[8+1]; + char rawmode[8 + 1]; /* Custom quantization tables () */ unsigned int *qtables; @@ -98,7 +93,8 @@ typedef struct { int qtablesLen; /* Extra data (to be injected after header) */ - char* extra; int extra_size; + char *extra; + int extra_size; /* PRIVATE CONTEXT (set by encoder) */ @@ -110,7 +106,7 @@ typedef struct { int extra_offset; - int rawExifLen; /* EXIF data length */ - char* rawExif; /* EXIF buffer pointer */ + size_t rawExifLen; /* EXIF data length */ + char *rawExif; /* EXIF buffer pointer */ } JPEGENCODERSTATE; diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index 7bb14eb2c0d..d030b0c439e 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -14,7 +14,7 @@ #define BUFFER_SIZE OPJ_J2K_STREAM_CHUNK_SIZE /* -------------------------------------------------------------------- */ -/* Decoder */ +/* Decoder */ /* -------------------------------------------------------------------- */ typedef struct { @@ -24,7 +24,7 @@ typedef struct { int fd; /* File pointer, when opened */ - FILE * pfile; + FILE *pfile; /* Length of data, if available; otherwise, -1 */ off_t length; @@ -33,54 +33,57 @@ typedef struct { OPJ_CODEC_FORMAT format; /* Set to divide image resolution by 2**reduce. */ - int reduce; + int reduce; /* Set to limit the number of quality layers to decode (0 = all layers) */ - int layers; + int layers; /* PRIVATE CONTEXT (set by decoder) */ - const char *error_msg; + const char *error_msg; } JPEG2KDECODESTATE; /* -------------------------------------------------------------------- */ -/* Encoder */ +/* Encoder */ /* -------------------------------------------------------------------- */ typedef struct { /* CONFIGURATION */ /* File descriptor, if available; otherwise, -1 */ - int fd; + int fd; /* File pointer, when opened */ - FILE * pfile; + FILE *pfile; /* Specify the desired format */ OPJ_CODEC_FORMAT format; /* Image offset */ - int offset_x, offset_y; + int offset_x, offset_y; /* Tile information */ - int tile_offset_x, tile_offset_y; - int tile_size_x, tile_size_y; + int tile_offset_x, tile_offset_y; + int tile_size_x, tile_size_y; /* Quality layers (a sequence of numbers giving *either* rates or dB) */ - int quality_is_in_db; - PyObject *quality_layers; + int quality_is_in_db; + PyObject *quality_layers; /* Number of resolutions (DWT decompositions + 1 */ - int num_resolutions; + int num_resolutions; /* Code block size */ - int cblk_width, cblk_height; + int cblk_width, cblk_height; /* Precinct size */ - int precinct_width, precinct_height; + int precinct_width, precinct_height; /* Compression style */ - int irreversible; + int irreversible; + + /* Set multiple component transformation */ + char mct; /* Progression order (LRCP/RLCP/RPCL/PCRL/CPRL) */ OPJ_PROG_ORDER progression; @@ -89,8 +92,7 @@ typedef struct { OPJ_CINEMA_MODE cinema_mode; /* PRIVATE CONTEXT (set by decoder) */ - const char *error_msg; - + const char *error_msg; } JPEG2KENCODESTATE; diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index 9140e007415..cff30e2d0bf 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -23,7 +23,7 @@ typedef struct { OPJ_UINT32 tile_index; OPJ_UINT32 data_size; - OPJ_INT32 x0, y0, x1, y1; + OPJ_INT32 x0, y0, x1, y1; OPJ_UINT32 nb_comps; } JPEG2KTILEINFO; @@ -32,9 +32,8 @@ typedef struct { /* -------------------------------------------------------------------- */ static void -j2k_error(const char *msg, void *client_data) -{ - JPEG2KDECODESTATE *state = (JPEG2KDECODESTATE *) client_data; +j2k_error(const char *msg, void *client_data) { + JPEG2KDECODESTATE *state = (JPEG2KDECODESTATE *)client_data; free((void *)state->error_msg); state->error_msg = strdup(msg); } @@ -44,8 +43,7 @@ j2k_error(const char *msg, void *client_data) /* -------------------------------------------------------------------- */ static OPJ_SIZE_T -j2k_read(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data) -{ +j2k_read(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data) { ImagingCodecState state = (ImagingCodecState)p_user_data; size_t len = _imaging_read_pyFd(state->fd, p_buffer, p_nb_bytes); @@ -54,8 +52,7 @@ j2k_read(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data) } static OPJ_OFF_T -j2k_skip(OPJ_OFF_T p_nb_bytes, void *p_user_data) -{ +j2k_skip(OPJ_OFF_T p_nb_bytes, void *p_user_data) { off_t pos; ImagingCodecState state = (ImagingCodecState)p_user_data; @@ -69,31 +66,33 @@ j2k_skip(OPJ_OFF_T p_nb_bytes, void *p_user_data) /* Unpackers */ /* -------------------------------------------------------------------- */ -typedef void (*j2k_unpacker_t)(opj_image_t *in, - const JPEG2KTILEINFO *tileInfo, - const UINT8 *data, - Imaging im); +typedef void (*j2k_unpacker_t)( + opj_image_t *in, const JPEG2KTILEINFO *tileInfo, const UINT8 *data, Imaging im); struct j2k_decode_unpacker { - const char *mode; - OPJ_COLOR_SPACE color_space; - unsigned components; - j2k_unpacker_t unpacker; + const char *mode; + OPJ_COLOR_SPACE color_space; + unsigned components; + /* bool indicating if unpacker supports subsampling */ + int subsampling; + j2k_unpacker_t unpacker; }; -static inline -unsigned j2ku_shift(unsigned x, int n) -{ - if (n < 0) +static inline unsigned +j2ku_shift(unsigned x, int n) { + if (n < 0) { return x >> -n; - else + } else { return x << n; + } } static void -j2ku_gray_l(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, Imaging im) -{ +j2ku_gray_l( + opj_image_t *in, + const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, + Imaging im) { unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; unsigned w = tileinfo->x1 - tileinfo->x0; unsigned h = tileinfo->y1 - tileinfo->y0; @@ -104,45 +103,52 @@ j2ku_gray_l(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, unsigned x, y; - if (csiz == 3) + if (csiz == 3) { csiz = 4; + } - if (shift < 0) + if (shift < 0) { offset += 1 << (-shift - 1); + } + /* csiz*h*w + offset = tileinfo.datasize */ switch (csiz) { - case 1: - for (y = 0; y < h; ++y) { - const UINT8 *data = &tiledata[y * w]; - UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) - *row++ = j2ku_shift(offset + *data++, shift); - } - break; - case 2: - for (y = 0; y < h; ++y) { - const UINT16 *data = (const UINT16 *)&tiledata[2 * y * w]; - UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) - *row++ = j2ku_shift(offset + *data++, shift); - } - break; - case 4: - for (y = 0; y < h; ++y) { - const UINT32 *data = (const UINT32 *)&tiledata[4 * y * w]; - UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) - *row++ = j2ku_shift(offset + *data++, shift); - } - break; + case 1: + for (y = 0; y < h; ++y) { + const UINT8 *data = &tiledata[y * w]; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + *row++ = j2ku_shift(offset + *data++, shift); + } + } + break; + case 2: + for (y = 0; y < h; ++y) { + const UINT16 *data = (const UINT16 *)&tiledata[2 * y * w]; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + *row++ = j2ku_shift(offset + *data++, shift); + } + } + break; + case 4: + for (y = 0; y < h; ++y) { + const UINT32 *data = (const UINT32 *)&tiledata[4 * y * w]; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + *row++ = j2ku_shift(offset + *data++, shift); + } + } + break; } } - static void -j2ku_gray_i(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, Imaging im) -{ +j2ku_gray_i( + opj_image_t *in, + const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, + Imaging im) { unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; unsigned w = tileinfo->x1 - tileinfo->x0; unsigned h = tileinfo->y1 - tileinfo->y0; @@ -153,45 +159,55 @@ j2ku_gray_i(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, unsigned x, y; - if (csiz == 3) + if (csiz == 3) { csiz = 4; + } - if (shift < 0) + if (shift < 0) { offset += 1 << (-shift - 1); + } switch (csiz) { - case 1: - for (y = 0; y < h; ++y) { - const UINT8 *data = &tiledata[y * w]; - UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) - *row++ = j2ku_shift(offset + *data++, shift); - } - break; - case 2: - for (y = 0; y < h; ++y) { - const UINT16 *data = (const UINT16 *)&tiledata[2 * y * w]; - UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) - *row++ = j2ku_shift(offset + *data++, shift); - } - break; - case 4: - for (y = 0; y < h; ++y) { - const UINT32 *data = (const UINT32 *)&tiledata[4 * y * w]; - UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) - *row++ = j2ku_shift(offset + *data++, shift); - } - break; + case 1: + for (y = 0; y < h; ++y) { + const UINT8 *data = &tiledata[y * w]; + UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + *row++ = j2ku_shift(offset + *data++, shift); + } + } + break; + case 2: + for (y = 0; y < h; ++y) { + const UINT16 *data = (const UINT16 *)&tiledata[2 * y * w]; + UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + UINT16 pixel = j2ku_shift(offset + *data++, shift); + #ifdef WORDS_BIGENDIAN + pixel = (pixel >> 8) | (pixel << 8); + #endif + *row++ = pixel; + } + } + break; + case 4: + for (y = 0; y < h; ++y) { + const UINT32 *data = (const UINT32 *)&tiledata[4 * y * w]; + UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + *row++ = j2ku_shift(offset + *data++, shift); + } + } + break; } } - static void -j2ku_gray_rgb(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, Imaging im) -{ +j2ku_gray_rgb( + opj_image_t *in, + const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, + Imaging im) { unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; unsigned w = tileinfo->x1 - tileinfo->x0; unsigned h = tileinfo->y1 - tileinfo->y0; @@ -202,56 +218,60 @@ j2ku_gray_rgb(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, unsigned x, y; - if (shift < 0) + if (shift < 0) { offset += 1 << (-shift - 1); + } - if (csiz == 3) + if (csiz == 3) { csiz = 4; + } switch (csiz) { - case 1: - for (y = 0; y < h; ++y) { - const UINT8 *data = &tiledata[y * w]; - UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) { - UINT8 byte = j2ku_shift(offset + *data++, shift); - row[0] = row[1] = row[2] = byte; - row[3] = 0xff; - row += 4; + case 1: + for (y = 0; y < h; ++y) { + const UINT8 *data = &tiledata[y * w]; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + UINT8 byte = j2ku_shift(offset + *data++, shift); + row[0] = row[1] = row[2] = byte; + row[3] = 0xff; + row += 4; + } } - } - break; - case 2: - for (y = 0; y < h; ++y) { - const UINT16 *data = (UINT16 *)&tiledata[2 * y * w]; - UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) { - UINT8 byte = j2ku_shift(offset + *data++, shift); - row[0] = row[1] = row[2] = byte; - row[3] = 0xff; - row += 4; + break; + case 2: + for (y = 0; y < h; ++y) { + const UINT16 *data = (UINT16 *)&tiledata[2 * y * w]; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + UINT8 byte = j2ku_shift(offset + *data++, shift); + row[0] = row[1] = row[2] = byte; + row[3] = 0xff; + row += 4; + } } - } - break; - case 4: - for (y = 0; y < h; ++y) { - const UINT32 *data = (UINT32 *)&tiledata[4 * y * w]; - UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; - for (x = 0; x < w; ++x) { - UINT8 byte = j2ku_shift(offset + *data++, shift); - row[0] = row[1] = row[2] = byte; - row[3] = 0xff; - row += 4; + break; + case 4: + for (y = 0; y < h; ++y) { + const UINT32 *data = (UINT32 *)&tiledata[4 * y * w]; + UINT8 *row = (UINT8 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) { + UINT8 byte = j2ku_shift(offset + *data++, shift); + row[0] = row[1] = row[2] = byte; + row[3] = 0xff; + row += 4; + } } - } - break; + break; } } static void -j2ku_graya_la(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, Imaging im) -{ +j2ku_graya_la( + opj_image_t *in, + const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, + Imaging im) { unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; unsigned w = tileinfo->x1 - tileinfo->x0; unsigned h = tileinfo->y1 - tileinfo->y0; @@ -266,15 +286,19 @@ j2ku_graya_la(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, unsigned x, y; - if (csiz == 3) + if (csiz == 3) { csiz = 4; - if (acsiz == 3) + } + if (acsiz == 3) { acsiz = 4; + } - if (shift < 0) + if (shift < 0) { offset += 1 << (-shift - 1); - if (ashift < 0) + } + if (ashift < 0) { aoffset += 1 << (-ashift - 1); + } atiledata = tiledata + csiz * w * h; @@ -286,15 +310,31 @@ j2ku_graya_la(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, UINT32 word = 0, aword = 0, byte; switch (csiz) { - case 1: word = *data++; break; - case 2: word = *(const UINT16 *)data; data += 2; break; - case 4: word = *(const UINT32 *)data; data += 4; break; + case 1: + word = *data++; + break; + case 2: + word = *(const UINT16 *)data; + data += 2; + break; + case 4: + word = *(const UINT32 *)data; + data += 4; + break; } switch (acsiz) { - case 1: aword = *adata++; break; - case 2: aword = *(const UINT16 *)adata; adata += 2; break; - case 4: aword = *(const UINT32 *)adata; adata += 4; break; + case 1: + aword = *adata++; + break; + case 2: + aword = *(const UINT16 *)adata; + adata += 2; + break; + case 4: + aword = *(const UINT32 *)adata; + adata += 4; + break; } byte = j2ku_shift(offset + word, shift); @@ -306,14 +346,17 @@ j2ku_graya_la(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, } static void -j2ku_srgb_rgb(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, Imaging im) -{ +j2ku_srgb_rgb( + opj_image_t *in, + const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, + Imaging im) { unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; unsigned w = tileinfo->x1 - tileinfo->x0; unsigned h = tileinfo->y1 - tileinfo->y0; int shifts[3], offsets[3], csiz[3]; + unsigned dx[3], dy[3]; const UINT8 *cdata[3]; const UINT8 *cptr = tiledata; unsigned n, x, y; @@ -323,30 +366,41 @@ j2ku_srgb_rgb(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, shifts[n] = 8 - in->comps[n].prec; offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; csiz[n] = (in->comps[n].prec + 7) >> 3; + dx[n] = (in->comps[n].dx); + dy[n] = (in->comps[n].dy); - if (csiz[n] == 3) + if (csiz[n] == 3) { csiz[n] = 4; + } - if (shifts[n] < 0) + if (shifts[n] < 0) { offsets[n] += 1 << (-shifts[n] - 1); + } - cptr += csiz[n] * w * h; + cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); } for (y = 0; y < h; ++y) { const UINT8 *data[3]; UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; - for (n = 0; n < 3; ++n) - data[n] = &cdata[n][csiz[n] * y * w]; + for (n = 0; n < 3; ++n) { + data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; + } for (x = 0; x < w; ++x) { for (n = 0; n < 3; ++n) { UINT32 word = 0; switch (csiz[n]) { - case 1: word = *data[n]++; break; - case 2: word = *(const UINT16 *)data[n]; data[n] += 2; break; - case 4: word = *(const UINT32 *)data[n]; data[n] += 4; break; + case 1: + word = data[n][x / dx[n]]; + break; + case 2: + word = ((const UINT16 *)data[n])[x / dx[n]]; + break; + case 4: + word = ((const UINT32 *)data[n])[x / dx[n]]; + break; } row[n] = j2ku_shift(offsets[n] + word, shifts[n]); @@ -358,14 +412,17 @@ j2ku_srgb_rgb(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, } static void -j2ku_sycc_rgb(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, Imaging im) -{ +j2ku_sycc_rgb( + opj_image_t *in, + const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, + Imaging im) { unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; unsigned w = tileinfo->x1 - tileinfo->x0; unsigned h = tileinfo->y1 - tileinfo->y0; int shifts[3], offsets[3], csiz[3]; + unsigned dx[3], dy[3]; const UINT8 *cdata[3]; const UINT8 *cptr = tiledata; unsigned n, x, y; @@ -375,31 +432,42 @@ j2ku_sycc_rgb(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, shifts[n] = 8 - in->comps[n].prec; offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; csiz[n] = (in->comps[n].prec + 7) >> 3; + dx[n] = (in->comps[n].dx); + dy[n] = (in->comps[n].dy); - if (csiz[n] == 3) + if (csiz[n] == 3) { csiz[n] = 4; + } - if (shifts[n] < 0) + if (shifts[n] < 0) { offsets[n] += 1 << (-shifts[n] - 1); + } - cptr += csiz[n] * w * h; + cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); } for (y = 0; y < h; ++y) { const UINT8 *data[3]; UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; UINT8 *row_start = row; - for (n = 0; n < 3; ++n) - data[n] = &cdata[n][csiz[n] * y * w]; + for (n = 0; n < 3; ++n) { + data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; + } for (x = 0; x < w; ++x) { for (n = 0; n < 3; ++n) { UINT32 word = 0; switch (csiz[n]) { - case 1: word = *data[n]++; break; - case 2: word = *(const UINT16 *)data[n]; data[n] += 2; break; - case 4: word = *(const UINT32 *)data[n]; data[n] += 4; break; + case 1: + word = data[n][x / dx[n]]; + break; + case 2: + word = ((const UINT16 *)data[n])[x / dx[n]]; + break; + case 4: + word = ((const UINT32 *)data[n])[x / dx[n]]; + break; } row[n] = j2ku_shift(offsets[n] + word, shifts[n]); @@ -413,14 +481,17 @@ j2ku_sycc_rgb(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, } static void -j2ku_srgba_rgba(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, Imaging im) -{ +j2ku_srgba_rgba( + opj_image_t *in, + const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, + Imaging im) { unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; unsigned w = tileinfo->x1 - tileinfo->x0; unsigned h = tileinfo->y1 - tileinfo->y0; int shifts[4], offsets[4], csiz[4]; + unsigned dx[4], dy[4]; const UINT8 *cdata[4]; const UINT8 *cptr = tiledata; unsigned n, x, y; @@ -430,30 +501,41 @@ j2ku_srgba_rgba(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, shifts[n] = 8 - in->comps[n].prec; offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; csiz[n] = (in->comps[n].prec + 7) >> 3; + dx[n] = (in->comps[n].dx); + dy[n] = (in->comps[n].dy); - if (csiz[n] == 3) + if (csiz[n] == 3) { csiz[n] = 4; + } - if (shifts[n] < 0) + if (shifts[n] < 0) { offsets[n] += 1 << (-shifts[n] - 1); + } - cptr += csiz[n] * w * h; + cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); } for (y = 0; y < h; ++y) { const UINT8 *data[4]; UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; - for (n = 0; n < 4; ++n) - data[n] = &cdata[n][csiz[n] * y * w]; + for (n = 0; n < 4; ++n) { + data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; + } for (x = 0; x < w; ++x) { for (n = 0; n < 4; ++n) { UINT32 word = 0; switch (csiz[n]) { - case 1: word = *data[n]++; break; - case 2: word = *(const UINT16 *)data[n]; data[n] += 2; break; - case 4: word = *(const UINT32 *)data[n]; data[n] += 4; break; + case 1: + word = data[n][x / dx[n]]; + break; + case 2: + word = ((const UINT16 *)data[n])[x / dx[n]]; + break; + case 4: + word = ((const UINT32 *)data[n])[x / dx[n]]; + break; } row[n] = j2ku_shift(offsets[n] + word, shifts[n]); @@ -464,14 +546,17 @@ j2ku_srgba_rgba(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, } static void -j2ku_sycca_rgba(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, Imaging im) -{ +j2ku_sycca_rgba( + opj_image_t *in, + const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, + Imaging im) { unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; unsigned w = tileinfo->x1 - tileinfo->x0; unsigned h = tileinfo->y1 - tileinfo->y0; int shifts[4], offsets[4], csiz[4]; + unsigned dx[4], dy[4]; const UINT8 *cdata[4]; const UINT8 *cptr = tiledata; unsigned n, x, y; @@ -481,31 +566,42 @@ j2ku_sycca_rgba(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, shifts[n] = 8 - in->comps[n].prec; offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; csiz[n] = (in->comps[n].prec + 7) >> 3; + dx[n] = (in->comps[n].dx); + dy[n] = (in->comps[n].dy); - if (csiz[n] == 3) + if (csiz[n] == 3) { csiz[n] = 4; + } - if (shifts[n] < 0) + if (shifts[n] < 0) { offsets[n] += 1 << (-shifts[n] - 1); + } - cptr += csiz[n] * w * h; + cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); } for (y = 0; y < h; ++y) { const UINT8 *data[4]; UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; UINT8 *row_start = row; - for (n = 0; n < 4; ++n) - data[n] = &cdata[n][csiz[n] * y * w]; + for (n = 0; n < 4; ++n) { + data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; + } for (x = 0; x < w; ++x) { for (n = 0; n < 4; ++n) { UINT32 word = 0; switch (csiz[n]) { - case 1: word = *data[n]++; break; - case 2: word = *(const UINT16 *)data[n]; data[n] += 2; break; - case 4: word = *(const UINT32 *)data[n]; data[n] += 4; break; + case 1: + word = data[n][x / dx[n]]; + break; + case 2: + word = ((const UINT16 *)data[n])[x / dx[n]]; + break; + case 4: + word = ((const UINT32 *)data[n])[x / dx[n]]; + break; } row[n] = j2ku_shift(offsets[n] + word, shifts[n]); @@ -518,22 +614,22 @@ j2ku_sycca_rgba(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, } static const struct j2k_decode_unpacker j2k_unpackers[] = { - { "L", OPJ_CLRSPC_GRAY, 1, j2ku_gray_l }, - { "I;16", OPJ_CLRSPC_GRAY, 1, j2ku_gray_i }, - { "I;16B", OPJ_CLRSPC_GRAY, 1, j2ku_gray_i }, - { "LA", OPJ_CLRSPC_GRAY, 2, j2ku_graya_la }, - { "RGB", OPJ_CLRSPC_GRAY, 1, j2ku_gray_rgb }, - { "RGB", OPJ_CLRSPC_GRAY, 2, j2ku_gray_rgb }, - { "RGB", OPJ_CLRSPC_SRGB, 3, j2ku_srgb_rgb }, - { "RGB", OPJ_CLRSPC_SYCC, 3, j2ku_sycc_rgb }, - { "RGB", OPJ_CLRSPC_SRGB, 4, j2ku_srgb_rgb }, - { "RGB", OPJ_CLRSPC_SYCC, 4, j2ku_sycc_rgb }, - { "RGBA", OPJ_CLRSPC_GRAY, 1, j2ku_gray_rgb }, - { "RGBA", OPJ_CLRSPC_GRAY, 2, j2ku_graya_la }, - { "RGBA", OPJ_CLRSPC_SRGB, 3, j2ku_srgb_rgb }, - { "RGBA", OPJ_CLRSPC_SYCC, 3, j2ku_sycc_rgb }, - { "RGBA", OPJ_CLRSPC_SRGB, 4, j2ku_srgba_rgba }, - { "RGBA", OPJ_CLRSPC_SYCC, 4, j2ku_sycca_rgba }, + {"L", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_l}, + {"I;16", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, + {"I;16B", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, + {"LA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la}, + {"RGB", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb}, + {"RGB", OPJ_CLRSPC_GRAY, 2, 0, j2ku_gray_rgb}, + {"RGB", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb}, + {"RGB", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, + {"RGB", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgb_rgb}, + {"RGB", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycc_rgb}, + {"RGBA", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb}, + {"RGBA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la}, + {"RGBA", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb}, + {"RGBA", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, + {"RGBA", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgba_rgba}, + {"RGBA", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycca_rgba}, }; /* -------------------------------------------------------------------- */ @@ -548,17 +644,18 @@ enum { }; static int -j2k_decode_entry(Imaging im, ImagingCodecState state) -{ - JPEG2KDECODESTATE *context = (JPEG2KDECODESTATE *) state->context; +j2k_decode_entry(Imaging im, ImagingCodecState state) { + JPEG2KDECODESTATE *context = (JPEG2KDECODESTATE *)state->context; opj_stream_t *stream = NULL; opj_image_t *image = NULL; opj_codec_t *codec = NULL; opj_dparameters_t params; OPJ_COLOR_SPACE color_space; j2k_unpacker_t unpack = NULL; - size_t buffer_size = 0; - unsigned n; + size_t buffer_size = 0, tile_bytes = 0; + unsigned n, tile_height, tile_width; + int subsampling; + int total_component_width = 0; stream = opj_stream_create(BUFFER_SIZE, OPJ_TRUE); @@ -581,10 +678,11 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) possibly support is 4GB. We can't go larger than this, because OpenJPEG truncates this value for the final box in the file, and the box lengths in OpenJPEG are currently 32 bit. */ - if (context->length < 0) + if (context->length < 0) { opj_stream_set_user_data_length(stream, 0xffffffff); - else + } else { opj_stream_set_user_data_length(stream, context->length); + } #endif /* Setup decompression context */ @@ -612,18 +710,23 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) } /* Check that this image is something we can handle */ - if (image->numcomps < 1 || image->numcomps > 4 - || image->color_space == OPJ_CLRSPC_UNKNOWN) { + if (image->numcomps < 1 || image->numcomps > 4 || + image->color_space == OPJ_CLRSPC_UNKNOWN) { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; goto quick_exit; } - for (n = 1; n < image->numcomps; ++n) { + /* + * Find first component with subsampling. + * + * This is a heuristic to determine the colorspace if unspecified. + */ + subsampling = -1; + for (n = 0; n < image->numcomps; ++n) { if (image->comps[n].dx != 1 || image->comps[n].dy != 1) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; + subsampling = n; + break; } } @@ -639,12 +742,14 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) If colorspace is unspecified, we assume: - Number of components Colorspace - ----------------------------------------- - 1 gray - 2 gray (+ alpha) - 3 sRGB - 4 sRGB (+ alpha) + Number of components Subsampling Colorspace + ------------------------------------------------------- + 1 Any gray + 2 Any gray (+ alpha) + 3 -1, 0 sRGB + 3 1, 2 YCbCr + 4 -1, 0, 3 sRGB (+ alpha) + 4 1, 2 YCbCr (+ alpha) */ @@ -653,15 +758,32 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) if (color_space == OPJ_CLRSPC_UNSPECIFIED) { switch (image->numcomps) { - case 1: case 2: color_space = OPJ_CLRSPC_GRAY; break; - case 3: case 4: color_space = OPJ_CLRSPC_SRGB; break; + case 1: + case 2: + color_space = OPJ_CLRSPC_GRAY; + break; + case 3: + case 4: + switch (subsampling) { + case -1: + case 0: + case 3: + color_space = OPJ_CLRSPC_SRGB; + break; + case 1: + case 2: + color_space = OPJ_CLRSPC_SYCC; + break; + } + break; } } - for (n = 0; n < sizeof(j2k_unpackers) / sizeof (j2k_unpackers[0]); ++n) { - if (color_space == j2k_unpackers[n].color_space - && image->numcomps == j2k_unpackers[n].components - && strcmp (im->mode, j2k_unpackers[n].mode) == 0) { + for (n = 0; n < sizeof(j2k_unpackers) / sizeof(j2k_unpackers[0]); ++n) { + if (color_space == j2k_unpackers[n].color_space && + image->numcomps == j2k_unpackers[n].components && + (j2k_unpackers[n].subsampling || (subsampling == -1)) && + strcmp(im->mode, j2k_unpackers[n].mode) == 0) { unpack = j2k_unpackers[n].unpacker; break; } @@ -680,21 +802,25 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) OPJ_BOOL should_continue; unsigned correction = (1 << params.cp_reduce) - 1; - if (!opj_read_tile_header(codec, - stream, - &tile_info.tile_index, - &tile_info.data_size, - &tile_info.x0, &tile_info.y0, - &tile_info.x1, &tile_info.y1, - &tile_info.nb_comps, - &should_continue)) { + if (!opj_read_tile_header( + codec, + stream, + &tile_info.tile_index, + &tile_info.data_size, + &tile_info.x0, + &tile_info.y0, + &tile_info.x1, + &tile_info.y1, + &tile_info.nb_comps, + &should_continue)) { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; goto quick_exit; } - if (!should_continue) + if (!should_continue) { break; + } /* Adjust the tile co-ordinates based on the reduction (OpenJPEG doesn't do this for us) */ @@ -703,37 +829,81 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) tile_info.x1 = (tile_info.x1 + correction) >> context->reduce; tile_info.y1 = (tile_info.y1 + correction) >> context->reduce; + /* Check the tile bounds; if the tile is outside the image area, + or if it has a negative width or height (i.e. the coordinates are + swapped), bail. */ + if (tile_info.x0 >= tile_info.x1 || tile_info.y0 >= tile_info.y1 || + tile_info.x0 < 0 || tile_info.y0 < 0 || + (OPJ_UINT32)tile_info.x0 < image->x0 || + (OPJ_UINT32)tile_info.y0 < image->y0 || + (OPJ_INT32)(tile_info.x1 - image->x0) > im->xsize || + (OPJ_INT32)(tile_info.y1 - image->y0) > im->ysize) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + if (tile_info.nb_comps != image->numcomps) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + /* Sometimes the tile_info.datasize we get back from openjpeg + is less than sum(comp_bytes)*w*h, and we overflow in the + shuffle stage */ + + tile_width = tile_info.x1 - tile_info.x0; + tile_height = tile_info.y1 - tile_info.y0; + + /* Total component width = sum (component_width) e.g, it's + legal for an la file to have a 1 byte width for l, and 4 for + a, and then a malicious file could have a smaller tile_bytes + */ + + for (n=0; n < tile_info.nb_comps; n++) { + // see csize /acsize calcs + int csize = (image->comps[n].prec + 7) >> 3; + csize = (csize == 3) ? 4 : csize; + total_component_width += csize; + } + if ((tile_width > UINT_MAX / total_component_width) || + (tile_height > UINT_MAX / total_component_width) || + (tile_width > UINT_MAX / (tile_height * total_component_width)) || + (tile_height > UINT_MAX / (tile_width * total_component_width))) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + tile_bytes = tile_width * tile_height * total_component_width; + + if (tile_bytes > tile_info.data_size) { + tile_info.data_size = tile_bytes; + } + if (buffer_size < tile_info.data_size) { - /* malloc check ok, tile_info.data_size from openjpeg */ - UINT8 *new = realloc (state->buffer, tile_info.data_size); + /* malloc check ok, overflow and tile size sanity check above */ + UINT8 *new = realloc(state->buffer, tile_info.data_size); if (!new) { state->errcode = IMAGING_CODEC_MEMORY; state->state = J2K_STATE_FAILED; goto quick_exit; } + /* Undefined behavior, sometimes decode_tile_data doesn't + fill the buffer and we do things with it later, leading + to valgrind errors. */ + memset(new, 0, tile_info.data_size); state->buffer = new; buffer_size = tile_info.data_size; } - if (!opj_decode_tile_data(codec, - tile_info.tile_index, - (OPJ_BYTE *)state->buffer, - tile_info.data_size, - stream)) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; - } - - /* Check the tile bounds; if the tile is outside the image area, - or if it has a negative width or height (i.e. the coordinates are - swapped), bail. */ - if (tile_info.x0 >= tile_info.x1 - || tile_info.y0 >= tile_info.y1 - || tile_info.x0 < image->x0 - || tile_info.y0 < image->y0 - || tile_info.x1 - image->x0 > im->xsize - || tile_info.y1 - image->y0 > im->ysize) { + if (!opj_decode_tile_data( + codec, + tile_info.tile_index, + (OPJ_BYTE *)state->buffer, + tile_info.data_size, + stream)) { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; goto quick_exit; @@ -752,34 +922,36 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) state->errcode = IMAGING_CODEC_END; if (context->pfile) { - if(fclose(context->pfile)){ + if (fclose(context->pfile)) { context->pfile = NULL; } } - quick_exit: - if (codec) +quick_exit: + if (codec) { opj_destroy_codec(codec); - if (image) + } + if (image) { opj_image_destroy(image); - if (stream) + } + if (stream) { opj_stream_destroy(stream); + } return -1; } int -ImagingJpeg2KDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ - - if (bytes){ +ImagingJpeg2KDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + if (bytes) { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; return -1; } - if (state->state == J2K_STATE_DONE || state->state == J2K_STATE_FAILED) + if (state->state == J2K_STATE_DONE || state->state == J2K_STATE_FAILED) { return -1; + } if (state->state == J2K_STATE_START) { state->state = J2K_STATE_DECODING; @@ -804,7 +976,7 @@ ImagingJpeg2KDecodeCleanup(ImagingCodecState state) { JPEG2KDECODESTATE *context = (JPEG2KDECODESTATE *)state->context; if (context->error_msg) { - free ((void *)context->error_msg); + free((void *)context->error_msg); } context->error_msg = NULL; @@ -813,8 +985,7 @@ ImagingJpeg2KDecodeCleanup(ImagingCodecState state) { } const char * -ImagingJpeg2KVersion(void) -{ +ImagingJpeg2KVersion(void) { return opj_version(); } diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 92e55e190fd..fe5511ba5cb 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -19,26 +19,24 @@ #include "Jpeg2K.h" -#define CINEMA_24_CS_LENGTH 1302083 -#define CINEMA_48_CS_LENGTH 651041 +#define CINEMA_24_CS_LENGTH 1302083 +#define CINEMA_48_CS_LENGTH 651041 #define COMP_24_CS_MAX_LENGTH 1041666 -#define COMP_48_CS_MAX_LENGTH 520833 +#define COMP_48_CS_MAX_LENGTH 520833 /* -------------------------------------------------------------------- */ /* Error handler */ /* -------------------------------------------------------------------- */ static void -j2k_error(const char *msg, void *client_data) -{ - JPEG2KENCODESTATE *state = (JPEG2KENCODESTATE *) client_data; +j2k_error(const char *msg, void *client_data) { + JPEG2KENCODESTATE *state = (JPEG2KENCODESTATE *)client_data; free((void *)state->error_msg); state->error_msg = strdup(msg); } static void -j2k_warn(const char *msg, void *client_data) -{ +j2k_warn(const char *msg, void *client_data) { // Null handler } @@ -47,26 +45,23 @@ j2k_warn(const char *msg, void *client_data) /* -------------------------------------------------------------------- */ static OPJ_SIZE_T -j2k_write(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data) -{ +j2k_write(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data) { ImagingCodecState state = (ImagingCodecState)p_user_data; - int result; + unsigned int result; result = _imaging_write_pyFd(state->fd, p_buffer, p_nb_bytes); return result ? result : (OPJ_SIZE_T)-1; } - static OPJ_OFF_T -j2k_skip(OPJ_OFF_T p_nb_bytes, void *p_user_data) -{ +j2k_skip(OPJ_OFF_T p_nb_bytes, void *p_user_data) { ImagingCodecState state = (ImagingCodecState)p_user_data; char *buffer; int result; /* Explicitly write zeros */ - buffer = calloc(p_nb_bytes,1); + buffer = calloc(p_nb_bytes, 1); if (!buffer) { return (OPJ_OFF_T)-1; } @@ -79,8 +74,7 @@ j2k_skip(OPJ_OFF_T p_nb_bytes, void *p_user_data) } static OPJ_BOOL -j2k_seek(OPJ_OFF_T p_nb_bytes, void *p_user_data) -{ +j2k_seek(OPJ_OFF_T p_nb_bytes, void *p_user_data) { ImagingCodecState state = (ImagingCodecState)p_user_data; off_t pos = 0; @@ -94,46 +88,46 @@ j2k_seek(OPJ_OFF_T p_nb_bytes, void *p_user_data) /* Encoder */ /* -------------------------------------------------------------------- */ -typedef void (*j2k_pack_tile_t)(Imaging im, UINT8 *buf, - unsigned x0, unsigned y0, - unsigned w, unsigned h); +typedef void (*j2k_pack_tile_t)( + Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h); static void -j2k_pack_l(Imaging im, UINT8 *buf, - unsigned x0, unsigned y0, unsigned w, unsigned h) -{ +j2k_pack_l(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { UINT8 *ptr = buf; - unsigned x,y; + unsigned x, y; for (y = 0; y < h; ++y) { UINT8 *data = (UINT8 *)(im->image[y + y0] + x0); - for (x = 0; x < w; ++x) + for (x = 0; x < w; ++x) { *ptr++ = *data++; + } } } static void -j2k_pack_i16(Imaging im, UINT8 *buf, - unsigned x0, unsigned y0, unsigned w, unsigned h) -{ +j2k_pack_i16(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { UINT8 *ptr = buf; - unsigned x,y; + unsigned x, y; for (y = 0; y < h; ++y) { UINT8 *data = (UINT8 *)(im->image[y + y0] + x0); for (x = 0; x < w; ++x) { - *ptr++ = *data++; - *ptr++ = *data++; +#ifdef WORDS_BIGENDIAN + ptr[0] = data[1]; + ptr[1] = data[0]; +#else + ptr[0] = data[0]; + ptr[1] = data[1]; +#endif + ptr += 2; + data += 2; } } } - static void -j2k_pack_la(Imaging im, UINT8 *buf, - unsigned x0, unsigned y0, unsigned w, unsigned h) -{ +j2k_pack_la(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { UINT8 *ptr = buf; UINT8 *ptra = buf + w * h; - unsigned x,y; + unsigned x, y; for (y = 0; y < h; ++y) { UINT8 *data = (UINT8 *)(im->image[y + y0] + 4 * x0); for (x = 0; x < w; ++x) { @@ -145,13 +139,11 @@ j2k_pack_la(Imaging im, UINT8 *buf, } static void -j2k_pack_rgb(Imaging im, UINT8 *buf, - unsigned x0, unsigned y0, unsigned w, unsigned h) -{ +j2k_pack_rgb(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { UINT8 *pr = buf; UINT8 *pg = pr + w * h; UINT8 *pb = pg + w * h; - unsigned x,y; + unsigned x, y; for (y = 0; y < h; ++y) { UINT8 *data = (UINT8 *)(im->image[y + y0] + 4 * x0); for (x = 0; x < w; ++x) { @@ -164,14 +156,13 @@ j2k_pack_rgb(Imaging im, UINT8 *buf, } static void -j2k_pack_rgba(Imaging im, UINT8 *buf, - unsigned x0, unsigned y0, unsigned w, unsigned h) -{ +j2k_pack_rgba( + Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { UINT8 *pr = buf; UINT8 *pg = pr + w * h; UINT8 *pb = pg + w * h; UINT8 *pa = pb + w * h; - unsigned x,y; + unsigned x, y; for (y = 0; y < h; ++y) { UINT8 *data = (UINT8 *)(im->image[y + y0] + 4 * x0); for (x = 0; x < w; ++x) { @@ -191,10 +182,9 @@ enum { }; static void -j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) -{ +j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) { float rate; - unsigned n; + int n; /* These settings have been copied from opj_compress in the OpenJPEG sources. */ @@ -214,8 +204,9 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) params->irreversible = 1; if (params->cp_cinema == OPJ_CINEMA4K_24) { - float max_rate = ((float)(components * im->xsize * im->ysize * 8) - / (CINEMA_24_CS_LENGTH * 8)); + float max_rate = + ((float)(components * im->xsize * im->ysize * 8) / + (CINEMA_24_CS_LENGTH * 8)); params->POC[0].tile = 1; params->POC[0].resno0 = 0; @@ -238,27 +229,32 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) if (params->tcp_rates[0] == 0) { params->tcp_rates[n] = max_rate; } else { - rate = ((float)(components * im->xsize * im->ysize * 8) - / (params->tcp_rates[n] * 8)); - if (rate > CINEMA_24_CS_LENGTH) + rate = + ((float)(components * im->xsize * im->ysize * 8) / + (params->tcp_rates[n] * 8)); + if (rate > CINEMA_24_CS_LENGTH) { params->tcp_rates[n] = max_rate; + } } } params->max_comp_size = COMP_24_CS_MAX_LENGTH; } else { - float max_rate = ((float)(components * im->xsize * im->ysize * 8) - / (CINEMA_48_CS_LENGTH * 8)); + float max_rate = + ((float)(components * im->xsize * im->ysize * 8) / + (CINEMA_48_CS_LENGTH * 8)); for (n = 0; n < params->tcp_numlayers; ++n) { rate = 0; if (params->tcp_rates[0] == 0) { params->tcp_rates[n] = max_rate; } else { - rate = ((float)(components * im->xsize * im->ysize * 8) - / (params->tcp_rates[n] * 8)); - if (rate > CINEMA_48_CS_LENGTH) + rate = + ((float)(components * im->xsize * im->ysize * 8) / + (params->tcp_rates[n] * 8)); + if (rate > CINEMA_48_CS_LENGTH) { params->tcp_rates[n] = max_rate; + } } } @@ -267,8 +263,7 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) } static int -j2k_encode_entry(Imaging im, ImagingCodecState state) -{ +j2k_encode_entry(Imaging im, ImagingCodecState state) { JPEG2KENCODESTATE *context = (JPEG2KENCODESTATE *)state->context; opj_stream_t *stream = NULL; opj_image_t *image = NULL; @@ -309,35 +304,29 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) #endif /* Setup an opj_image */ - if (strcmp (im->mode, "L") == 0) { + if (strcmp(im->mode, "L") == 0) { components = 1; color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_l; - } else if (strcmp (im->mode, "I;16") == 0){ + } else if (strcmp(im->mode, "I;16") == 0 || strcmp(im->mode, "I;16B") == 0) { components = 1; color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_i16; prec = 16; bpp = 12; - } else if (strcmp (im->mode, "I;16B") == 0){ - components = 1; - color_space = OPJ_CLRSPC_GRAY; - pack = j2k_pack_i16; - prec = 16; - bpp = 12; - } else if (strcmp (im->mode, "LA") == 0) { + } else if (strcmp(im->mode, "LA") == 0) { components = 2; color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_la; - } else if (strcmp (im->mode, "RGB") == 0) { + } else if (strcmp(im->mode, "RGB") == 0) { components = 3; color_space = OPJ_CLRSPC_SRGB; pack = j2k_pack_rgb; - } else if (strcmp (im->mode, "YCbCr") == 0) { + } else if (strcmp(im->mode, "YCbCr") == 0) { components = 3; color_space = OPJ_CLRSPC_SYCC; pack = j2k_pack_rgb; - } else if (strcmp (im->mode, "RGBA") == 0) { + } else if (strcmp(im->mode, "RGBA") == 0) { components = 4; color_space = OPJ_CLRSPC_SRGB; pack = j2k_pack_rgba; @@ -396,9 +385,11 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) Py_ssize_t n; float *pq; - if (len) { - if (len > sizeof(params.tcp_rates) / sizeof(params.tcp_rates[0])) - len = sizeof(params.tcp_rates)/sizeof(params.tcp_rates[0]); + if (len > 0) { + if ((size_t)len > + sizeof(params.tcp_rates) / sizeof(params.tcp_rates[0])) { + len = sizeof(params.tcp_rates) / sizeof(params.tcp_rates[0]); + } params.tcp_numlayers = (int)len; @@ -423,19 +414,20 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) params.cp_disto_alloc = 1; } - if (context->num_resolutions) + if (context->num_resolutions) { params.numresolution = context->num_resolutions; + } - if (context->cblk_width >= 4 && context->cblk_width <= 1024 - && context->cblk_height >= 4 && context->cblk_height <= 1024 - && context->cblk_width * context->cblk_height <= 4096) { + if (context->cblk_width >= 4 && context->cblk_width <= 1024 && + context->cblk_height >= 4 && context->cblk_height <= 1024 && + context->cblk_width * context->cblk_height <= 4096) { params.cblockw_init = context->cblk_width; params.cblockh_init = context->cblk_height; } - if (context->precinct_width >= 4 && context->precinct_height >= 4 - && context->precinct_width >= context->cblk_width - && context->precinct_height > context->cblk_height) { + if (context->precinct_width >= 4 && context->precinct_height >= 4 && + context->precinct_width >= context->cblk_width && + context->precinct_height > context->cblk_height) { params.prcw_init[0] = context->precinct_width; params.prch_init[0] = context->precinct_height; params.res_spec = 1; @@ -443,30 +435,42 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) } params.irreversible = context->irreversible; + if (components == 3) { + params.tcp_mct = context->mct; + } params.prog_order = context->progression; params.cp_cinema = context->cinema_mode; switch (params.cp_cinema) { - case OPJ_OFF: - params.cp_rsiz = OPJ_STD_RSIZ; - break; - case OPJ_CINEMA2K_24: - case OPJ_CINEMA2K_48: - params.cp_rsiz = OPJ_CINEMA2K; - if (params.numresolution > 6) - params.numresolution = 6; - break; - case OPJ_CINEMA4K_24: - params.cp_rsiz = OPJ_CINEMA4K; - if (params.numresolution > 7) - params.numresolution = 7; - break; - } - - if (context->cinema_mode != OPJ_OFF) + case OPJ_OFF: + params.cp_rsiz = OPJ_STD_RSIZ; + break; + case OPJ_CINEMA2K_24: + case OPJ_CINEMA2K_48: + params.cp_rsiz = OPJ_CINEMA2K; + if (params.numresolution > 6) { + params.numresolution = 6; + } + break; + case OPJ_CINEMA4K_24: + params.cp_rsiz = OPJ_CINEMA4K; + if (params.numresolution > 7) { + params.numresolution = 7; + } + break; + } + + if (!context->num_resolutions) { + while (tile_width < (1 << (params.numresolution - 1U)) || tile_height < (1 << (params.numresolution - 1U))) { + params.numresolution -= 1; + } + } + + if (context->cinema_mode != OPJ_OFF) { j2k_set_cinema_params(im, components, ¶ms); + } /* Set up the reference grid in the image */ image->x0 = params.image_offset_x0; @@ -496,24 +500,24 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) } /* Write each tile */ - tiles_x = (im->xsize + (params.image_offset_x0 - params.cp_tx0) - + tile_width - 1) / tile_width; - tiles_y = (im->ysize + (params.image_offset_y0 - params.cp_ty0) - + tile_height - 1) / tile_height; + tiles_x = (im->xsize + (params.image_offset_x0 - params.cp_tx0) + tile_width - 1) / + tile_width; + tiles_y = (im->ysize + (params.image_offset_y0 - params.cp_ty0) + tile_height - 1) / + tile_height; /* check for integer overflow for the malloc line, checking any expression that may multiply either tile_width or tile_height */ _overflow_scale_factor = components * prec; - if (( tile_width > UINT_MAX / _overflow_scale_factor ) || - ( tile_height > UINT_MAX / _overflow_scale_factor ) || - ( tile_width > UINT_MAX / (tile_height * _overflow_scale_factor )) || - ( tile_height > UINT_MAX / (tile_width * _overflow_scale_factor ))) { + if ((tile_width > UINT_MAX / _overflow_scale_factor) || + (tile_height > UINT_MAX / _overflow_scale_factor) || + (tile_width > UINT_MAX / (tile_height * _overflow_scale_factor)) || + (tile_height > UINT_MAX / (tile_width * _overflow_scale_factor))) { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; goto quick_exit; } /* malloc check ok, checked for overflow above */ - state->buffer = malloc (tile_width * tile_height * components * prec / 8); + state->buffer = malloc(tile_width * tile_height * components * prec / 8); if (!state->buffer) { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; @@ -522,28 +526,32 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) tile_ndx = 0; for (y = 0; y < tiles_y; ++y) { - unsigned ty0 = params.cp_ty0 + y * tile_height; + int ty0 = params.cp_ty0 + y * tile_height; unsigned ty1 = ty0 + tile_height; unsigned pixy, pixh; - if (ty0 < params.image_offset_y0) + if (ty0 < params.image_offset_y0) { ty0 = params.image_offset_y0; - if (ty1 > ysiz) + } + if (ty1 > ysiz) { ty1 = ysiz; + } pixy = ty0 - params.image_offset_y0; pixh = ty1 - ty0; for (x = 0; x < tiles_x; ++x) { - unsigned tx0 = params.cp_tx0 + x * tile_width; + int tx0 = params.cp_tx0 + x * tile_width; unsigned tx1 = tx0 + tile_width; unsigned pixx, pixw; unsigned data_size; - if (tx0 < params.image_offset_x0) + if (tx0 < params.image_offset_x0) { tx0 = params.image_offset_x0; - if (tx1 > xsiz) + } + if (tx1 > xsiz) { tx1 = xsiz; + } pixx = tx0 - params.image_offset_x0; pixw = tx1 - tx0; @@ -552,8 +560,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) data_size = pixw * pixh * components * prec / 8; - if (!opj_write_tile(codec, tile_ndx++, state->buffer, - data_size, stream)) { + if (!opj_write_tile(codec, tile_ndx++, state->buffer, data_size, stream)) { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; goto quick_exit; @@ -571,25 +578,27 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) state->state = J2K_STATE_DONE; ret = -1; - quick_exit: - if (codec) +quick_exit: + if (codec) { opj_destroy_codec(codec); - if (image) + } + if (image) { opj_image_destroy(image); - if (stream) + } + if (stream) { opj_stream_destroy(stream); + } return ret; } int -ImagingJpeg2KEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) -{ - if (state->state == J2K_STATE_FAILED) +ImagingJpeg2KEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + if (state->state == J2K_STATE_FAILED) { return -1; + } if (state->state == J2K_STATE_START) { - state->state = J2K_STATE_ENCODING; return j2k_encode_entry(im, state); @@ -611,12 +620,12 @@ ImagingJpeg2KEncodeCleanup(ImagingCodecState state) { context->quality_layers = NULL; } - if (context->error_msg) - free ((void *)context->error_msg); + if (context->error_msg) { + free((void *)context->error_msg); + } context->error_msg = NULL; - return -1; } diff --git a/src/libImaging/JpegDecode.c b/src/libImaging/JpegDecode.c index 33cc5d0955b..55d10a81aec 100644 --- a/src/libImaging/JpegDecode.c +++ b/src/libImaging/JpegDecode.c @@ -21,10 +21,9 @@ * See the README file for details on usage and redistribution. */ - #include "Imaging.h" -#ifdef HAVE_LIBJPEG +#ifdef HAVE_LIBJPEG #undef HAVE_PROTOTYPES #undef HAVE_STDLIB_H @@ -37,7 +36,6 @@ #include "Jpeg.h" - #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) @@ -50,20 +48,19 @@ char *libjpeg_turbo_version = NULL; #endif int -ImagingJpegUseJCSExtensions() -{ +ImagingJpegUseJCSExtensions() { int use_jcs_extensions = 0; - #ifdef JCS_EXTENSIONS - #if defined(LIBJPEG_TURBO_VERSION_NUMBER) - #if LIBJPEG_TURBO_VERSION_NUMBER >= 1002010 - use_jcs_extensions = 1; - #endif - #else - if (libjpeg_turbo_version) { - use_jcs_extensions = strcmp(libjpeg_turbo_version, "1.2.1") >= 0; - } - #endif - #endif +#ifdef JCS_EXTENSIONS +#if defined(LIBJPEG_TURBO_VERSION_NUMBER) +#if LIBJPEG_TURBO_VERSION_NUMBER >= 1002010 + use_jcs_extensions = 1; +#endif +#else + if (libjpeg_turbo_version) { + use_jcs_extensions = strcmp(libjpeg_turbo_version, "1.2.1") >= 0; + } +#endif +#endif return use_jcs_extensions; } @@ -72,24 +69,19 @@ ImagingJpegUseJCSExtensions() /* -------------------------------------------------------------------- */ METHODDEF(void) -stub(j_decompress_ptr cinfo) -{ - /* empty */ -} +stub(j_decompress_ptr cinfo) { /* empty */ } METHODDEF(boolean) -fill_input_buffer(j_decompress_ptr cinfo) -{ +fill_input_buffer(j_decompress_ptr cinfo) { /* Suspension */ return FALSE; } METHODDEF(void) -skip_input_data(j_decompress_ptr cinfo, long num_bytes) -{ - JPEGSOURCE* source = (JPEGSOURCE*) cinfo->src; +skip_input_data(j_decompress_ptr cinfo, long num_bytes) { + JPEGSOURCE *source = (JPEGSOURCE *)cinfo->src; - if (num_bytes > (long) source->pub.bytes_in_buffer) { + if (num_bytes > (long)source->pub.bytes_in_buffer) { /* We need to skip more data than we have in the buffer. This will force the JPEG library to suspend decoding. */ source->skip = num_bytes - source->pub.bytes_in_buffer; @@ -103,50 +95,42 @@ skip_input_data(j_decompress_ptr cinfo, long num_bytes) } } - GLOBAL(void) -jpeg_buffer_src(j_decompress_ptr cinfo, JPEGSOURCE* source) -{ - cinfo->src = (void*) source; - - /* Prepare for suspending reader */ - source->pub.init_source = stub; - source->pub.fill_input_buffer = fill_input_buffer; - source->pub.skip_input_data = skip_input_data; - source->pub.resync_to_restart = jpeg_resync_to_restart; - source->pub.term_source = stub; - source->pub.bytes_in_buffer = 0; /* forces fill_input_buffer on first read */ - - source->skip = 0; +jpeg_buffer_src(j_decompress_ptr cinfo, JPEGSOURCE *source) { + cinfo->src = (void *)source; + + /* Prepare for suspending reader */ + source->pub.init_source = stub; + source->pub.fill_input_buffer = fill_input_buffer; + source->pub.skip_input_data = skip_input_data; + source->pub.resync_to_restart = jpeg_resync_to_restart; + source->pub.term_source = stub; + source->pub.bytes_in_buffer = 0; /* forces fill_input_buffer on first read */ + + source->skip = 0; } - /* -------------------------------------------------------------------- */ /* Error handler */ /* -------------------------------------------------------------------- */ METHODDEF(void) -error(j_common_ptr cinfo) -{ - JPEGERROR* error; - error = (JPEGERROR*) cinfo->err; - longjmp(error->setjmp_buffer, 1); +error(j_common_ptr cinfo) { + JPEGERROR *error; + error = (JPEGERROR *)cinfo->err; + longjmp(error->setjmp_buffer, 1); } METHODDEF(void) -output(j_common_ptr cinfo) -{ - /* nothing */ -} +output(j_common_ptr cinfo) { /* nothing */ } /* -------------------------------------------------------------------- */ /* Decoder */ /* -------------------------------------------------------------------- */ int -ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ - JPEGSTATE* context = (JPEGSTATE*) state->context; +ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + JPEGSTATE *context = (JPEGSTATE *)state->context; int ok; if (setjmp(context->error.setjmp_buffer)) { @@ -157,7 +141,6 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) } if (!state->state) { - /* Setup decompression context */ context->cinfo.err = jpeg_std_error(&context->error.pub); context->error.pub.error_exit = error; @@ -167,7 +150,6 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) /* Ready to decode */ state->state = 1; - } /* Load the source buffer */ @@ -176,140 +158,147 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) if (context->source.skip > 0) { skip_input_data(&context->cinfo, context->source.skip); - if (context->source.skip > 0) + if (context->source.skip > 0) { return context->source.pub.next_input_byte - buf; + } } switch (state->state) { + case 1: - case 1: - - /* Read JPEG header, until we find an image body. */ - do { - - /* Note that we cannot return unless we have decoded - as much data as possible. */ - ok = jpeg_read_header(&context->cinfo, FALSE); - - } while (ok == JPEG_HEADER_TABLES_ONLY); + /* Read JPEG header, until we find an image body. */ + do { + /* Note that we cannot return unless we have decoded + as much data as possible. */ + ok = jpeg_read_header(&context->cinfo, FALSE); - if (ok == JPEG_SUSPENDED) - break; + } while (ok == JPEG_HEADER_TABLES_ONLY); - /* Decoder settings */ + if (ok == JPEG_SUSPENDED) { + break; + } - /* jpegmode indicates whats in the file; if not set, we'll - trust the decoder */ - if (strcmp(context->jpegmode, "L") == 0) - context->cinfo.jpeg_color_space = JCS_GRAYSCALE; - else if (strcmp(context->jpegmode, "RGB") == 0) - context->cinfo.jpeg_color_space = JCS_RGB; - else if (strcmp(context->jpegmode, "CMYK") == 0) - context->cinfo.jpeg_color_space = JCS_CMYK; - else if (strcmp(context->jpegmode, "YCbCr") == 0) - context->cinfo.jpeg_color_space = JCS_YCbCr; - else if (strcmp(context->jpegmode, "YCbCrK") == 0) { - context->cinfo.jpeg_color_space = JCS_YCCK; - } + /* Decoder settings */ + + /* jpegmode indicates whats in the file; if not set, we'll + trust the decoder */ + if (strcmp(context->jpegmode, "L") == 0) { + context->cinfo.jpeg_color_space = JCS_GRAYSCALE; + } else if (strcmp(context->jpegmode, "RGB") == 0) { + context->cinfo.jpeg_color_space = JCS_RGB; + } else if (strcmp(context->jpegmode, "CMYK") == 0) { + context->cinfo.jpeg_color_space = JCS_CMYK; + } else if (strcmp(context->jpegmode, "YCbCr") == 0) { + context->cinfo.jpeg_color_space = JCS_YCbCr; + } else if (strcmp(context->jpegmode, "YCbCrK") == 0) { + context->cinfo.jpeg_color_space = JCS_YCCK; + } - /* rawmode indicates what we want from the decoder. if not - set, conversions are disabled */ - if (strcmp(context->rawmode, "L") == 0) - context->cinfo.out_color_space = JCS_GRAYSCALE; - else if (strcmp(context->rawmode, "RGB") == 0) - context->cinfo.out_color_space = JCS_RGB; - #ifdef JCS_EXTENSIONS - else if (strcmp(context->rawmode, "RGBX") == 0) + /* rawmode indicates what we want from the decoder. if not + set, conversions are disabled */ + if (strcmp(context->rawmode, "L") == 0) { + context->cinfo.out_color_space = JCS_GRAYSCALE; + } else if (strcmp(context->rawmode, "RGB") == 0) { + context->cinfo.out_color_space = JCS_RGB; + } +#ifdef JCS_EXTENSIONS + else if (strcmp(context->rawmode, "RGBX") == 0) { context->cinfo.out_color_space = JCS_EXT_RGBX; - #endif - else if (strcmp(context->rawmode, "CMYK") == 0 || - strcmp(context->rawmode, "CMYK;I") == 0) - context->cinfo.out_color_space = JCS_CMYK; - else if (strcmp(context->rawmode, "YCbCr") == 0) - context->cinfo.out_color_space = JCS_YCbCr; - else if (strcmp(context->rawmode, "YCbCrK") == 0) - context->cinfo.out_color_space = JCS_YCCK; - else { - /* Disable decoder conversions */ - context->cinfo.jpeg_color_space = JCS_UNKNOWN; - context->cinfo.out_color_space = JCS_UNKNOWN; - } - - if (context->scale > 1) { - context->cinfo.scale_num = 1; - context->cinfo.scale_denom = context->scale; - } - if (context->draft) { - context->cinfo.do_fancy_upsampling = FALSE; - context->cinfo.dct_method = JDCT_FASTEST; - } - - state->state++; - /* fall through */ - - case 2: + } +#endif + else if ( + strcmp(context->rawmode, "CMYK") == 0 || + strcmp(context->rawmode, "CMYK;I") == 0) { + context->cinfo.out_color_space = JCS_CMYK; + } else if (strcmp(context->rawmode, "YCbCr") == 0) { + context->cinfo.out_color_space = JCS_YCbCr; + } else if (strcmp(context->rawmode, "YCbCrK") == 0) { + context->cinfo.out_color_space = JCS_YCCK; + } else { + /* Disable decoder conversions */ + context->cinfo.jpeg_color_space = JCS_UNKNOWN; + context->cinfo.out_color_space = JCS_UNKNOWN; + } - /* Set things up for decompression (this processes the entire - file if necessary to return data line by line) */ - if (!jpeg_start_decompress(&context->cinfo)) - break; + if (context->scale > 1) { + context->cinfo.scale_num = 1; + context->cinfo.scale_denom = context->scale; + } + if (context->draft) { + context->cinfo.do_fancy_upsampling = FALSE; + context->cinfo.dct_method = JDCT_FASTEST; + } - state->state++; - /* fall through */ + state->state++; + /* fall through */ - case 3: + case 2: - /* Decompress a single line of data */ - ok = 1; - while (state->y < state->ysize) { - ok = jpeg_read_scanlines(&context->cinfo, &state->buffer, 1); - if (ok != 1) + /* Set things up for decompression (this processes the entire + file if necessary to return data line by line) */ + if (!jpeg_start_decompress(&context->cinfo)) { break; - state->shuffle((UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->buffer, - state->xsize); - state->y++; - } - if (ok != 1) - break; - state->state++; - /* fall through */ - - case 4: + } - /* Finish decompression */ - if (!jpeg_finish_decompress(&context->cinfo)) { - /* FIXME: add strictness mode test */ - if (state->y < state->ysize) + state->state++; + /* fall through */ + + case 3: + + /* Decompress a single line of data */ + ok = 1; + while (state->y < state->ysize) { + ok = jpeg_read_scanlines(&context->cinfo, &state->buffer, 1); + if (ok != 1) { + break; + } + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->buffer, + state->xsize); + state->y++; + } + if (ok != 1) { break; - } + } + state->state++; + /* fall through */ - /* Clean up */ - jpeg_destroy_decompress(&context->cinfo); - /* if (jerr.pub.num_warnings) return BROKEN; */ - return -1; + case 4: + /* Finish decompression */ + if (!jpeg_finish_decompress(&context->cinfo)) { + /* FIXME: add strictness mode test */ + if (state->y < state->ysize) { + break; + } + } + + /* Clean up */ + jpeg_destroy_decompress(&context->cinfo); + /* if (jerr.pub.num_warnings) return BROKEN; */ + return -1; } /* Return number of bytes consumed */ return context->source.pub.next_input_byte - buf; - } /* -------------------------------------------------------------------- */ /* Cleanup */ /* -------------------------------------------------------------------- */ -int ImagingJpegDecodeCleanup(ImagingCodecState state){ - /* called to free the decompression engine when the decode terminates - due to a corrupt or truncated image - */ - JPEGSTATE* context = (JPEGSTATE*) state->context; - - /* Clean up */ - jpeg_destroy_decompress(&context->cinfo); - return -1; +int +ImagingJpegDecodeCleanup(ImagingCodecState state) { + /* called to free the decompression engine when the decode terminates + due to a corrupt or truncated image + */ + JPEGSTATE *context = (JPEGSTATE *)state->context; + + /* Clean up */ + jpeg_destroy_decompress(&context->cinfo); + return -1; } #endif - diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 10ad886e042..a44debcafe8 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -19,10 +19,9 @@ * See the README file for details on usage and redistribution. */ - #include "Imaging.h" -#ifdef HAVE_LIBJPEG +#ifdef HAVE_LIBJPEG #undef HAVE_PROTOTYPES #undef HAVE_STDLIB_H @@ -40,73 +39,62 @@ /* -------------------------------------------------------------------- */ METHODDEF(void) -stub(j_compress_ptr cinfo) -{ - /* empty */ -} +stub(j_compress_ptr cinfo) { /* empty */ } METHODDEF(boolean) -empty_output_buffer (j_compress_ptr cinfo) -{ +empty_output_buffer(j_compress_ptr cinfo) { /* Suspension */ return FALSE; } GLOBAL(void) -jpeg_buffer_dest(j_compress_ptr cinfo, JPEGDESTINATION* destination) -{ - cinfo->dest = (void*) destination; +jpeg_buffer_dest(j_compress_ptr cinfo, JPEGDESTINATION *destination) { + cinfo->dest = (void *)destination; destination->pub.init_destination = stub; destination->pub.empty_output_buffer = empty_output_buffer; destination->pub.term_destination = stub; } - /* -------------------------------------------------------------------- */ /* Error handler */ /* -------------------------------------------------------------------- */ METHODDEF(void) -error(j_common_ptr cinfo) -{ - JPEGERROR* error; - error = (JPEGERROR*) cinfo->err; - (*cinfo->err->output_message) (cinfo); +error(j_common_ptr cinfo) { + JPEGERROR *error; + error = (JPEGERROR *)cinfo->err; + (*cinfo->err->output_message)(cinfo); longjmp(error->setjmp_buffer, 1); } - /* -------------------------------------------------------------------- */ -/* Encoder */ +/* Encoder */ /* -------------------------------------------------------------------- */ int -ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ - JPEGENCODERSTATE* context = (JPEGENCODERSTATE*) state->context; +ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + JPEGENCODERSTATE *context = (JPEGENCODERSTATE *)state->context; int ok; if (setjmp(context->error.setjmp_buffer)) { - /* JPEG error handler */ - jpeg_destroy_compress(&context->cinfo); - state->errcode = IMAGING_CODEC_BROKEN; - return -1; + /* JPEG error handler */ + jpeg_destroy_compress(&context->cinfo); + state->errcode = IMAGING_CODEC_BROKEN; + return -1; } if (!state->state) { - - /* Setup compression context (very similar to the decoder) */ - context->cinfo.err = jpeg_std_error(&context->error.pub); - context->error.pub.error_exit = error; - jpeg_create_compress(&context->cinfo); - jpeg_buffer_dest(&context->cinfo, &context->destination); + /* Setup compression context (very similar to the decoder) */ + context->cinfo.err = jpeg_std_error(&context->error.pub); + context->error.pub.error_exit = error; + jpeg_create_compress(&context->cinfo); + jpeg_buffer_dest(&context->cinfo, &context->destination); context->extra_offset = 0; - /* Ready to encode */ - state->state = 1; - + /* Ready to encode */ + state->state = 1; } /* Load the destination buffer */ @@ -114,224 +102,239 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) context->destination.pub.free_in_buffer = bytes; switch (state->state) { + case 1: + + context->cinfo.image_width = state->xsize; + context->cinfo.image_height = state->ysize; + + switch (state->bits) { + case 8: + context->cinfo.input_components = 1; + context->cinfo.in_color_space = JCS_GRAYSCALE; + break; + case 24: + context->cinfo.input_components = 3; + if (strcmp(im->mode, "YCbCr") == 0) { + context->cinfo.in_color_space = JCS_YCbCr; + } else { + context->cinfo.in_color_space = JCS_RGB; + } + break; + case 32: + context->cinfo.input_components = 4; + context->cinfo.in_color_space = JCS_CMYK; +#ifdef JCS_EXTENSIONS + if (strcmp(context->rawmode, "RGBX") == 0) { + context->cinfo.in_color_space = JCS_EXT_RGBX; + } +#endif + break; + default: + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + + /* Compressor configuration */ + jpeg_set_defaults(&context->cinfo); + + /* Use custom quantization tables */ + if (context->qtables) { + int i; + int quality = 100; + int last_q = 0; + if (context->quality != -1) { + quality = context->quality; + } + for (i = 0; i < context->qtablesLen; i++) { + jpeg_add_quant_table( + &context->cinfo, + i, + &context->qtables[i * DCTSIZE2], + quality, + FALSE); + context->cinfo.comp_info[i].quant_tbl_no = i; + last_q = i; + } + if (context->qtablesLen == 1) { + // jpeg_set_defaults created two qtables internally, but we only + // wanted one. + jpeg_add_quant_table( + &context->cinfo, 1, &context->qtables[0], quality, FALSE); + } + for (i = last_q; i < context->cinfo.num_components; i++) { + context->cinfo.comp_info[i].quant_tbl_no = last_q; + } + } else if (context->quality != -1) { + jpeg_set_quality(&context->cinfo, context->quality, TRUE); + } + + /* Set subsampling options */ + switch (context->subsampling) { + case 0: /* 1x1 1x1 1x1 (4:4:4) : None */ + { + context->cinfo.comp_info[0].h_samp_factor = 1; + context->cinfo.comp_info[0].v_samp_factor = 1; + context->cinfo.comp_info[1].h_samp_factor = 1; + context->cinfo.comp_info[1].v_samp_factor = 1; + context->cinfo.comp_info[2].h_samp_factor = 1; + context->cinfo.comp_info[2].v_samp_factor = 1; + break; + } + case 1: /* 2x1, 1x1, 1x1 (4:2:2) : Medium */ + { + context->cinfo.comp_info[0].h_samp_factor = 2; + context->cinfo.comp_info[0].v_samp_factor = 1; + context->cinfo.comp_info[1].h_samp_factor = 1; + context->cinfo.comp_info[1].v_samp_factor = 1; + context->cinfo.comp_info[2].h_samp_factor = 1; + context->cinfo.comp_info[2].v_samp_factor = 1; + break; + } + case 2: /* 2x2, 1x1, 1x1 (4:2:0) : High */ + { + context->cinfo.comp_info[0].h_samp_factor = 2; + context->cinfo.comp_info[0].v_samp_factor = 2; + context->cinfo.comp_info[1].h_samp_factor = 1; + context->cinfo.comp_info[1].v_samp_factor = 1; + context->cinfo.comp_info[2].h_samp_factor = 1; + context->cinfo.comp_info[2].v_samp_factor = 1; + break; + } + default: { + /* Use the lib's default */ + break; + } + } + if (context->progressive) { + jpeg_simple_progression(&context->cinfo); + } + context->cinfo.smoothing_factor = context->smooth; + context->cinfo.optimize_coding = (boolean)context->optimize; + if (context->xdpi > 0 && context->ydpi > 0) { + context->cinfo.write_JFIF_header = TRUE; + context->cinfo.density_unit = 1; /* dots per inch */ + context->cinfo.X_density = context->xdpi; + context->cinfo.Y_density = context->ydpi; + } + switch (context->streamtype) { + case 1: + /* tables only -- not yet implemented */ + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + case 2: + /* image only */ + jpeg_suppress_tables(&context->cinfo, TRUE); + jpeg_start_compress(&context->cinfo, FALSE); + /* suppress extra section */ + context->extra_offset = context->extra_size; + break; + default: + /* interchange stream */ + jpeg_start_compress(&context->cinfo, TRUE); + break; + } + state->state++; + /* fall through */ + + case 2: + // check for exif len + 'APP1' header bytes + if (context->rawExifLen + 5 > context->destination.pub.free_in_buffer) { + break; + } + // add exif header + if (context->rawExifLen > 0) { + jpeg_write_marker( + &context->cinfo, + JPEG_APP0 + 1, + (unsigned char *)context->rawExif, + context->rawExifLen); + } + + state->state++; + /* fall through */ + case 3: + + if (context->extra) { + /* copy extra buffer to output buffer */ + unsigned int n = context->extra_size - context->extra_offset; + if (n > context->destination.pub.free_in_buffer) { + n = context->destination.pub.free_in_buffer; + } + memcpy( + context->destination.pub.next_output_byte, + context->extra + context->extra_offset, + n); + context->destination.pub.next_output_byte += n; + context->destination.pub.free_in_buffer -= n; + context->extra_offset += n; + if (context->extra_offset >= context->extra_size) { + state->state++; + } else { + break; + } + } else { + state->state++; + } - case 1: + case 4: + if (1024 > context->destination.pub.free_in_buffer) { + break; + } + + ok = 1; + while (state->y < state->ysize) { + state->shuffle( + state->buffer, + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->xsize); + ok = jpeg_write_scanlines(&context->cinfo, &state->buffer, 1); + if (ok != 1) { + break; + } + state->y++; + } + + if (ok != 1) { + break; + } + state->state++; + /* fall through */ - context->cinfo.image_width = state->xsize; - context->cinfo.image_height = state->ysize; + case 5: - switch (state->bits) { - case 8: - context->cinfo.input_components = 1; - context->cinfo.in_color_space = JCS_GRAYSCALE; - break; - case 24: - context->cinfo.input_components = 3; - if (strcmp(im->mode, "YCbCr") == 0) - context->cinfo.in_color_space = JCS_YCbCr; - else - context->cinfo.in_color_space = JCS_RGB; - break; - case 32: - context->cinfo.input_components = 4; - context->cinfo.in_color_space = JCS_CMYK; - #ifdef JCS_EXTENSIONS - if (strcmp(context->rawmode, "RGBX") == 0) - context->cinfo.in_color_space = JCS_EXT_RGBX; - #endif - break; - default: - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - } - - /* Compressor configuration */ - jpeg_set_defaults(&context->cinfo); - - /* Use custom quantization tables */ - if (context->qtables) { - int i; - int quality = 100; - int last_q = 0; - if (context->quality > 0) { - quality = context->quality; - } - for (i = 0; i < context->qtablesLen; i++) { - // TODO: Should add support for none baseline - jpeg_add_quant_table(&context->cinfo, i, &context->qtables[i * DCTSIZE2], - quality, TRUE); - context->cinfo.comp_info[i].quant_tbl_no = i; - last_q = i; - } - if (context->qtablesLen == 1) { - // jpeg_set_defaults created two qtables internally, but we only wanted one. - jpeg_add_quant_table(&context->cinfo, 1, &context->qtables[0], - quality, TRUE); - } - for (i = last_q; i < context->cinfo.num_components; i++) { - context->cinfo.comp_info[i].quant_tbl_no = last_q; - } - } else if (context->quality > 0) { - jpeg_set_quality(&context->cinfo, context->quality, 1); - } - - /* Set subsampling options */ - switch (context->subsampling) - { - case 0: /* 1x1 1x1 1x1 (4:4:4) : None */ - { - context->cinfo.comp_info[0].h_samp_factor = 1; - context->cinfo.comp_info[0].v_samp_factor = 1; - context->cinfo.comp_info[1].h_samp_factor = 1; - context->cinfo.comp_info[1].v_samp_factor = 1; - context->cinfo.comp_info[2].h_samp_factor = 1; - context->cinfo.comp_info[2].v_samp_factor = 1; - break; - } - case 1: /* 2x1, 1x1, 1x1 (4:2:2) : Medium */ - { - context->cinfo.comp_info[0].h_samp_factor = 2; - context->cinfo.comp_info[0].v_samp_factor = 1; - context->cinfo.comp_info[1].h_samp_factor = 1; - context->cinfo.comp_info[1].v_samp_factor = 1; - context->cinfo.comp_info[2].h_samp_factor = 1; - context->cinfo.comp_info[2].v_samp_factor = 1; - break; - } - case 2: /* 2x2, 1x1, 1x1 (4:2:0) : High */ - { - context->cinfo.comp_info[0].h_samp_factor = 2; - context->cinfo.comp_info[0].v_samp_factor = 2; - context->cinfo.comp_info[1].h_samp_factor = 1; - context->cinfo.comp_info[1].v_samp_factor = 1; - context->cinfo.comp_info[2].h_samp_factor = 1; - context->cinfo.comp_info[2].v_samp_factor = 1; - break; - } - default: - { - /* Use the lib's default */ - break; - } - } - if (context->progressive) - jpeg_simple_progression(&context->cinfo); - context->cinfo.smoothing_factor = context->smooth; - context->cinfo.optimize_coding = (boolean) context->optimize; - if (context->xdpi > 0 && context->ydpi > 0) { - context->cinfo.density_unit = 1; /* dots per inch */ - context->cinfo.X_density = context->xdpi; - context->cinfo.Y_density = context->ydpi; - } - switch (context->streamtype) { - case 1: - /* tables only -- not yet implemented */ - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - case 2: - /* image only */ - jpeg_suppress_tables(&context->cinfo, TRUE); - jpeg_start_compress(&context->cinfo, FALSE); - /* suppress extra section */ - context->extra_offset = context->extra_size; - break; - default: - /* interchange stream */ - jpeg_start_compress(&context->cinfo, TRUE); - break; - } - state->state++; - /* fall through */ - - case 2: - // check for exif len + 'APP1' header bytes - if (context->rawExifLen + 5 > context->destination.pub.free_in_buffer){ - break; - } - //add exif header - if (context->rawExifLen > 0){ - jpeg_write_marker(&context->cinfo, JPEG_APP0+1, - (unsigned char*)context->rawExif, context->rawExifLen); - } - - state->state++; - /* fall through */ - case 3: - - if (context->extra) { - /* copy extra buffer to output buffer */ - unsigned int n = context->extra_size - context->extra_offset; - if (n > context->destination.pub.free_in_buffer) - n = context->destination.pub.free_in_buffer; - memcpy(context->destination.pub.next_output_byte, - context->extra + context->extra_offset, n); - context->destination.pub.next_output_byte += n; - context->destination.pub.free_in_buffer -= n; - context->extra_offset += n; - if (context->extra_offset >= context->extra_size) - state->state++; - else + /* Finish compression */ + if (context->destination.pub.free_in_buffer < 100) { break; - } else - state->state++; - - case 4: - if (1024 > context->destination.pub.free_in_buffer){ + } + jpeg_finish_compress(&context->cinfo); + + /* Clean up */ + if (context->extra) { + free(context->extra); + context->extra = NULL; + } + if (context->rawExif) { + free(context->rawExif); + context->rawExif = NULL; + } + if (context->qtables) { + free(context->qtables); + context->qtables = NULL; + } + + jpeg_destroy_compress(&context->cinfo); + /* if (jerr.pub.num_warnings) return BROKEN; */ + state->errcode = IMAGING_CODEC_END; break; - } - - ok = 1; - while (state->y < state->ysize) { - state->shuffle(state->buffer, - (UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->xsize); - ok = jpeg_write_scanlines(&context->cinfo, &state->buffer, 1); - if (ok != 1) - break; - state->y++; - } - - if (ok != 1) - break; - state->state++; - /* fall through */ - - case 5: - - /* Finish compression */ - if (context->destination.pub.free_in_buffer < 100) - break; - jpeg_finish_compress(&context->cinfo); - - /* Clean up */ - if (context->extra) { - free(context->extra); - context->extra = NULL; - } - if (context->rawExif) { - free(context->rawExif); - context->rawExif = NULL; - } - if (context->qtables) { - free(context->qtables); - context->qtables = NULL; - } - - jpeg_destroy_compress(&context->cinfo); - /* if (jerr.pub.num_warnings) return BROKEN; */ - state->errcode = IMAGING_CODEC_END; - break; - } /* Return number of bytes in output buffer */ return context->destination.pub.next_output_byte - buf; - } -const char* -ImagingJpegVersion(void) -{ +const char * +ImagingJpegVersion(void) { static char version[20]; sprintf(version, "%d.%d", JPEG_LIB_VERSION / 10, JPEG_LIB_VERSION % 10); return version; diff --git a/src/libImaging/Matrix.c b/src/libImaging/Matrix.c index 5cc7795a4be..182eb62a7e6 100644 --- a/src/libImaging/Matrix.c +++ b/src/libImaging/Matrix.c @@ -13,62 +13,66 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" - -#define CLIPF(v) ((v <= 0.0) ? 0 : (v >= 255.0F) ? 255 : (UINT8) v) - +#define CLIPF(v) ((v <= 0.0) ? 0 : (v >= 255.0F) ? 255 : (UINT8)v) Imaging -ImagingConvertMatrix(Imaging im, const char *mode, float m[]) -{ +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) - return (Imaging) ImagingError_ModeError(); + if (!im) { + return (Imaging)ImagingError_ModeError(); + } if (strcmp(mode, "L") == 0 && im->bands == 3) { - - imOut = ImagingNewDirty("L", im->xsize, im->ysize); - if (!imOut) - return NULL; - - for (y = 0; y < im->ysize; y++) { - UINT8* in = (UINT8*) im->image[y]; - UINT8* out = (UINT8*) imOut->image[y]; - - for (x = 0; x < im->xsize; x++) { - float v = m[0]*in[0] + m[1]*in[1] + m[2]*in[2] + m[3] + 0.5; - out[x] = CLIPF(v); - in += 4; - } - } + imOut = ImagingNewDirty("L", im->xsize, im->ysize); + if (!imOut) { + return NULL; + } + + ImagingSectionEnter(&cookie); + for (y = 0; y < im->ysize; y++) { + UINT8 *in = (UINT8 *)im->image[y]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + for (x = 0; x < im->xsize; x++) { + float v = m[0] * in[0] + m[1] * in[1] + m[2] * in[2] + m[3] + 0.5; + out[x] = CLIPF(v); + in += 4; + } + } + ImagingSectionLeave(&cookie); } else if (strlen(mode) == 3 && im->bands == 3) { - - imOut = ImagingNewDirty(mode, im->xsize, im->ysize); - if (!imOut) - return NULL; - - for (y = 0; y < im->ysize; y++) { - UINT8* in = (UINT8*) im->image[y]; - UINT8* out = (UINT8*) imOut->image[y]; - - 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; - float v2 = m[8]*in[0] + m[9]*in[1] + m[10]*in[2] + m[11] + 0.5; - out[0] = CLIPF(v0); - out[1] = CLIPF(v1); - out[2] = CLIPF(v2); - in += 4; out += 4; - } - } - } else - return (Imaging) ImagingError_ModeError(); + imOut = ImagingNewDirty(mode, im->xsize, im->ysize); + if (!imOut) { + return NULL; + } + + for (y = 0; y < im->ysize; y++) { + 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; + float v2 = m[8] * in[0] + m[9] * in[1] + m[10] * in[2] + m[11] + 0.5; + out[0] = CLIPF(v0); + out[1] = CLIPF(v1); + out[2] = CLIPF(v2); + in += 4; + out += 4; + } + ImagingSectionLeave(&cookie); + } + } else { + return (Imaging)ImagingError_ModeError(); + } return imOut; } diff --git a/src/libImaging/ModeFilter.c b/src/libImaging/ModeFilter.c index 5237d07328f..757cbc3fb86 100644 --- a/src/libImaging/ModeFilter.c +++ b/src/libImaging/ModeFilter.c @@ -16,8 +16,7 @@ #include "Imaging.h" Imaging -ImagingModeFilter(Imaging im, int size) -{ +ImagingModeFilter(Imaging im, int size) { Imaging imOut; int x, y, i; int xx, yy; @@ -25,19 +24,20 @@ ImagingModeFilter(Imaging im, int size) UINT8 maxpixel; int histogram[256]; - if (!im || im->bands != 1 || im->type != IMAGING_TYPE_UINT8) - return (Imaging) ImagingError_ModeError(); + if (!im || im->bands != 1 || im->type != IMAGING_TYPE_UINT8) { + return (Imaging)ImagingError_ModeError(); + } imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); - if (!imOut) + if (!imOut) { return NULL; + } size = size / 2; for (y = 0; y < imOut->ysize; y++) { - UINT8* out = &IMAGING_PIXEL_L(imOut, 0, y); + UINT8 *out = &IMAGING_PIXEL_L(imOut, 0, y); for (x = 0; x < imOut->xsize; x++) { - /* calculate histogram over current area */ /* FIXME: brute force! to improve, update the histogram @@ -46,30 +46,33 @@ ImagingModeFilter(Imaging im, int size) the added complexity... */ memset(histogram, 0, sizeof(histogram)); - for (yy = y - size; yy <= y + size; yy++) + for (yy = y - size; yy <= y + size; yy++) { if (yy >= 0 && yy < imOut->ysize) { - UINT8* in = &IMAGING_PIXEL_L(im, 0, yy); - for (xx = x - size; xx <= x + size; xx++) - if (xx >= 0 && xx < imOut->xsize) + UINT8 *in = &IMAGING_PIXEL_L(im, 0, yy); + for (xx = x - size; xx <= x + size; xx++) { + if (xx >= 0 && xx < imOut->xsize) { histogram[in[xx]]++; + } + } } + } /* find most frequent pixel value in this region */ maxpixel = 0; maxcount = histogram[maxpixel]; - for (i = 1; i < 256; i++) + for (i = 1; i < 256; i++) { if (histogram[i] > maxcount) { maxcount = histogram[i]; - maxpixel = (UINT8) i; + maxpixel = (UINT8)i; } + } - if (maxcount > 2) + if (maxcount > 2) { out[x] = maxpixel; - else + } else { out[x] = IMAGING_PIXEL_L(im, x, y); - + } } - } ImagingCopyPalette(imOut, im); diff --git a/src/libImaging/Negative.c b/src/libImaging/Negative.c index 4dedcb24517..70b96c39772 100644 --- a/src/libImaging/Negative.c +++ b/src/libImaging/Negative.c @@ -8,7 +8,7 @@ * FIXME: Maybe this should be implemented using ImagingPoint() * * history: - * 95-11-27 fl: Created + * 95-11-27 fl: Created * * Copyright (c) Fredrik Lundh 1995. * Copyright (c) Secret Labs AB 1997. @@ -16,27 +16,27 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" - Imaging -ImagingNegative(Imaging im) -{ +ImagingNegative(Imaging im) { Imaging imOut; int x, y; - if (!im) - return (Imaging) ImagingError_ModeError(); + if (!im) { + return (Imaging)ImagingError_ModeError(); + } imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); - if (!imOut) - return NULL; + if (!imOut) { + return NULL; + } - for (y = 0; y < im->ysize; y++) - for (x = 0; x < im->linesize; x++) - imOut->image[y][x] = ~im->image[y][x]; + for (y = 0; y < im->ysize; y++) { + for (x = 0; x < im->linesize; x++) { + imOut->image[y][x] = ~im->image[y][x]; + } + } return imOut; } - diff --git a/src/libImaging/Offset.c b/src/libImaging/Offset.c index b3d9425fb65..91ee91083cc 100644 --- a/src/libImaging/Offset.c +++ b/src/libImaging/Offset.c @@ -5,7 +5,7 @@ * offset an image in x and y directions * * history: - * 96-07-22 fl: Created + * 96-07-22 fl: Created * 98-11-01 cgw@pgt.com: Fixed negative-array index bug * * Copyright (c) Fredrik Lundh 1996. @@ -14,48 +14,51 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" - Imaging -ImagingOffset(Imaging im, int xoffset, int yoffset) -{ +ImagingOffset(Imaging im, int xoffset, int yoffset) { int x, y; Imaging imOut; - if (!im) - return (Imaging) ImagingError_ModeError(); + if (!im) { + return (Imaging)ImagingError_ModeError(); + } imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); - if (!imOut) - return NULL; + if (!imOut) { + return NULL; + } ImagingCopyPalette(imOut, im); /* make offsets positive to avoid negative coordinates */ xoffset %= im->xsize; xoffset = im->xsize - xoffset; - if (xoffset < 0) - xoffset += im->xsize; + if (xoffset < 0) { + xoffset += im->xsize; + } yoffset %= im->ysize; yoffset = im->ysize - yoffset; - if (yoffset < 0) - yoffset += im->ysize; - -#define OFFSET(image)\ - for (y = 0; y < im->ysize; y++)\ - for (x = 0; x < im->xsize; x++) {\ - int yi = (y + yoffset) % im->ysize;\ - int xi = (x + xoffset) % im->xsize;\ - imOut->image[y][x] = im->image[yi][xi];\ - } - - if (im->image8) - OFFSET(image8) - else - OFFSET(image32) + if (yoffset < 0) { + yoffset += im->ysize; + } + +#define OFFSET(image) \ + for (y = 0; y < im->ysize; y++) { \ + for (x = 0; x < im->xsize; x++) { \ + int yi = (y + yoffset) % im->ysize; \ + int xi = (x + xoffset) % im->xsize; \ + imOut->image[y][x] = im->image[yi][xi]; \ + } \ + } + + if (im->image8) { + OFFSET(image8) + } else { + OFFSET(image32) + } return imOut; } diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index 5c298c6c50f..01760e742be 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -1,4 +1,4 @@ - /* +/* * The Python Imaging Library. * $Id$ * @@ -25,7 +25,6 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" #define R 0 @@ -41,20 +40,28 @@ /* byte swapping macros */ -#define C16N\ - (out[0]=tmp[0], out[1]=tmp[1]); -#define C16S\ - (out[1]=tmp[0], out[0]=tmp[1]); -#define C32N\ - (out[0]=tmp[0], out[1]=tmp[1], out[2]=tmp[2], out[3]=tmp[3]); -#define C32S\ - (out[3]=tmp[0], out[2]=tmp[1], out[1]=tmp[2], out[0]=tmp[3]); -#define C64N\ - (out[0]=tmp[0], out[1]=tmp[1], out[2]=tmp[2], out[3]=tmp[3],\ - out[4]=tmp[4], out[5]=tmp[5], out[6]=tmp[6], out[7]=tmp[7]); -#define C64S\ - (out[7]=tmp[0], out[6]=tmp[1], out[5]=tmp[2], out[4]=tmp[3],\ - out[3]=tmp[4], out[2]=tmp[5], out[1]=tmp[6], out[0]=tmp[7]); +#define C16N (out[0] = tmp[0], out[1] = tmp[1]); +#define C16S (out[1] = tmp[0], out[0] = tmp[1]); +#define C32N (out[0] = tmp[0], out[1] = tmp[1], out[2] = tmp[2], out[3] = tmp[3]); +#define C32S (out[3] = tmp[0], out[2] = tmp[1], out[1] = tmp[2], out[0] = tmp[3]); +#define C64N \ + (out[0] = tmp[0], \ + out[1] = tmp[1], \ + out[2] = tmp[2], \ + out[3] = tmp[3], \ + out[4] = tmp[4], \ + out[5] = tmp[5], \ + out[6] = tmp[6], \ + out[7] = tmp[7]); +#define C64S \ + (out[7] = tmp[0], \ + out[6] = tmp[1], \ + out[5] = tmp[2], \ + out[4] = tmp[3], \ + out[3] = tmp[4], \ + out[2] = tmp[5], \ + out[1] = tmp[6], \ + out[0] = tmp[7]); #ifdef WORDS_BIGENDIAN #define C16B C16N @@ -72,134 +79,138 @@ #define C64L C64N #endif - static void -pack1(UINT8* out, const UINT8* in, int pixels) -{ +pack1(UINT8 *out, const UINT8 *in, int pixels) { int i, m, b; /* bilevel (black is 0) */ - b = 0; m = 128; + b = 0; + m = 128; for (i = 0; i < pixels; i++) { - if (in[i] != 0) + if (in[i] != 0) { b |= m; + } m >>= 1; if (m == 0) { *out++ = b; - b = 0; m = 128; + b = 0; + m = 128; } } - if (m != 128) + if (m != 128) { *out++ = b; + } } static void -pack1I(UINT8* out, const UINT8* in, int pixels) -{ +pack1I(UINT8 *out, const UINT8 *in, int pixels) { int i, m, b; /* bilevel (black is 1) */ - b = 0; m = 128; + b = 0; + m = 128; for (i = 0; i < pixels; i++) { - if (in[i] == 0) + if (in[i] == 0) { b |= m; + } m >>= 1; if (m == 0) { *out++ = b; - b = 0; m = 128; + b = 0; + m = 128; } } - if (m != 128) + if (m != 128) { *out++ = b; + } } static void -pack1R(UINT8* out, const UINT8* in, int pixels) -{ +pack1R(UINT8 *out, const UINT8 *in, int pixels) { int i, m, b; /* bilevel, lsb first (black is 0) */ - b = 0; m = 1; + b = 0; + m = 1; for (i = 0; i < pixels; i++) { - if (in[i] != 0) + if (in[i] != 0) { b |= m; + } m <<= 1; - if (m == 256){ + if (m == 256) { *out++ = b; - b = 0; m = 1; + b = 0; + m = 1; } } - if (m != 1) + if (m != 1) { *out++ = b; + } } static void -pack1IR(UINT8* out, const UINT8* in, int pixels) -{ +pack1IR(UINT8 *out, const UINT8 *in, int pixels) { int i, m, b; /* bilevel, lsb first (black is 1) */ - b = 0; m = 1; + b = 0; + m = 1; for (i = 0; i < pixels; i++) { - if (in[i] == 0) + if (in[i] == 0) { b |= m; + } m <<= 1; - if (m == 256){ + if (m == 256) { *out++ = b; - b = 0; m = 1; + b = 0; + m = 1; } } - if (m != 1) + if (m != 1) { *out++ = b; + } } static void -pack1L(UINT8* out, const UINT8* in, int pixels) -{ +pack1L(UINT8 *out, const UINT8 *in, int pixels) { int i; /* bilevel, stored as bytes */ - for (i = 0; i < pixels; i++) + for (i = 0; i < pixels; i++) { out[i] = (in[i] != 0) ? 255 : 0; + } } static void -packP4(UINT8* out, const UINT8* in, int pixels) -{ +packP4(UINT8 *out, const UINT8 *in, int pixels) { while (pixels >= 2) { - *out++ = (in[0] << 4) | - (in[1] & 15); - in += 2; pixels -= 2; + *out++ = (in[0] << 4) | (in[1] & 15); + in += 2; + pixels -= 2; } - if (pixels) + if (pixels) { out[0] = (in[0] << 4); + } } static void -packP2(UINT8* out, const UINT8* in, int pixels) -{ +packP2(UINT8 *out, const UINT8 *in, int pixels) { while (pixels >= 4) { - *out++ = (in[0] << 6) | - ((in[1] & 3) << 4) | - ((in[2] & 3) << 2) | - (in[3] & 3); - in += 4; pixels -= 4; + *out++ = (in[0] << 6) | ((in[1] & 3) << 4) | ((in[2] & 3) << 2) | (in[3] & 3); + in += 4; + pixels -= 4; } switch (pixels) { - case 3: - out[0] = (in[0] << 6) | - ((in[1] & 3) << 4) | - ((in[2] & 3) << 2); - break; - case 2: - out[0] = (in[0] << 6) | - ((in[1] & 3) << 4); - break; - case 1: - out[0] = (in[0] << 6); + case 3: + out[0] = (in[0] << 6) | ((in[1] & 3) << 4) | ((in[2] & 3) << 2); + break; + case 2: + out[0] = (in[0] << 6) | ((in[1] & 3) << 4); + break; + case 1: + out[0] = (in[0] << 6); } } static void -packL16(UINT8* out, const UINT8* in, int pixels) -{ +packL16(UINT8 *out, const UINT8 *in, int pixels) { int i; /* L -> L;16, e.g: \xff77 -> \x00\xff\x00\x77 */ for (i = 0; i < pixels; i++) { @@ -210,8 +221,7 @@ packL16(UINT8* out, const UINT8* in, int pixels) } static void -packL16B(UINT8* out, const UINT8* in, int pixels) -{ +packL16B(UINT8 *out, const UINT8 *in, int pixels) { int i; /* L -> L;16B, e.g: \xff77 -> \xff\x00\x77\x00 */ for (i = 0; i < pixels; i++) { @@ -221,51 +231,58 @@ packL16B(UINT8* out, const UINT8* in, int pixels) } } - static void -packLA(UINT8* out, const UINT8* in, int pixels) -{ +packLA(UINT8 *out, const UINT8 *in, int pixels) { int i; /* LA, pixel interleaved */ for (i = 0; i < pixels; i++) { out[0] = in[R]; out[1] = in[A]; - out += 2; in += 4; + out += 2; + in += 4; } } static void -packLAL(UINT8* out, const UINT8* in, int pixels) -{ +packLAL(UINT8 *out, const UINT8 *in, int pixels) { int i; /* LA, line interleaved */ for (i = 0; i < pixels; i++) { out[i] = in[R]; - out[i+pixels] = in[A]; + out[i + pixels] = in[A]; in += 4; } } void -ImagingPackRGB(UINT8* out, const UINT8* in, int pixels) -{ +ImagingPackRGB(UINT8 *out, const UINT8 *in, int pixels) { int i = 0; /* RGB triplets */ - for (; i < pixels-1; i++) { - ((UINT32*)out)[0] = ((UINT32*)in)[i]; +#ifdef __sparc + /* SPARC CPUs cannot read integers from nonaligned addresses. */ + for (; i < pixels; i++) { + out[0] = in[R]; + out[1] = in[G]; + out[2] = in[B]; + out += 3; + in += 4; + } +#else + for (; i < pixels - 1; i++) { + memcpy(out, in + i * 4, 4); out += 3; } for (; i < pixels; i++) { - out[0] = in[i*4+R]; - out[1] = in[i*4+G]; - out[2] = in[i*4+B]; + out[0] = in[i * 4 + R]; + out[1] = in[i * 4 + G]; + out[2] = in[i * 4 + B]; out += 3; } +#endif } void -ImagingPackXRGB(UINT8* out, const UINT8* in, int pixels) -{ +ImagingPackXRGB(UINT8 *out, const UINT8 *in, int pixels) { int i; /* XRGB, triplets with left padding */ for (i = 0; i < pixels; i++) { @@ -273,26 +290,26 @@ ImagingPackXRGB(UINT8* out, const UINT8* in, int pixels) out[1] = in[R]; out[2] = in[G]; out[3] = in[B]; - out += 4; in += 4; + out += 4; + in += 4; } } void -ImagingPackBGR(UINT8* out, const UINT8* in, int pixels) -{ +ImagingPackBGR(UINT8 *out, const UINT8 *in, int pixels) { int i; /* RGB, reversed bytes */ for (i = 0; i < pixels; i++) { out[0] = in[B]; out[1] = in[G]; out[2] = in[R]; - out += 3; in += 4; + out += 3; + in += 4; } } void -ImagingPackBGRX(UINT8* out, const UINT8* in, int pixels) -{ +ImagingPackBGRX(UINT8 *out, const UINT8 *in, int pixels) { int i; /* BGRX, reversed bytes with right padding */ for (i = 0; i < pixels; i++) { @@ -300,13 +317,13 @@ ImagingPackBGRX(UINT8* out, const UINT8* in, int pixels) out[1] = in[G]; out[2] = in[R]; out[3] = 0; - out += 4; in += 4; + out += 4; + in += 4; } } void -ImagingPackXBGR(UINT8* out, const UINT8* in, int pixels) -{ +ImagingPackXBGR(UINT8 *out, const UINT8 *in, int pixels) { int i; /* XBGR, reversed bytes with left padding */ for (i = 0; i < pixels; i++) { @@ -314,13 +331,13 @@ ImagingPackXBGR(UINT8* out, const UINT8* in, int pixels) out[1] = in[B]; out[2] = in[G]; out[3] = in[R]; - out += 4; in += 4; + out += 4; + in += 4; } } void -ImagingPackBGRA(UINT8* out, const UINT8* in, int pixels) -{ +ImagingPackBGRA(UINT8 *out, const UINT8 *in, int pixels) { int i; /* BGRX, reversed bytes with right padding */ for (i = 0; i < pixels; i++) { @@ -328,13 +345,13 @@ ImagingPackBGRA(UINT8* out, const UINT8* in, int pixels) out[1] = in[G]; out[2] = in[R]; out[3] = in[A]; - out += 4; in += 4; + out += 4; + in += 4; } } void -ImagingPackABGR(UINT8* out, const UINT8* in, int pixels) -{ +ImagingPackABGR(UINT8 *out, const UINT8 *in, int pixels) { int i; /* XBGR, reversed bytes with left padding */ for (i = 0; i < pixels; i++) { @@ -342,13 +359,13 @@ ImagingPackABGR(UINT8* out, const UINT8* in, int pixels) out[1] = in[B]; out[2] = in[G]; out[3] = in[R]; - out += 4; in += 4; + out += 4; + in += 4; } } void -ImagingPackBGRa(UINT8* out, const UINT8* in, int pixels) -{ +ImagingPackBGRa(UINT8 *out, const UINT8 *in, int pixels) { int i; /* BGRa, reversed bytes with premultiplied alpha */ for (i = 0; i < pixels; i++) { @@ -357,313 +374,319 @@ ImagingPackBGRa(UINT8* out, const UINT8* in, int pixels) out[0] = MULDIV255(in[B], alpha, tmp); out[1] = MULDIV255(in[G], alpha, tmp); out[2] = MULDIV255(in[R], alpha, tmp); - out += 4; in += 4; + out += 4; + in += 4; } } static void -packRGBL(UINT8* out, const UINT8* in, int pixels) -{ +packRGBL(UINT8 *out, const UINT8 *in, int pixels) { int i; /* RGB, line interleaved */ for (i = 0; i < pixels; i++) { out[i] = in[R]; - out[i+pixels] = in[G]; - out[i+pixels+pixels] = in[B]; + out[i + pixels] = in[G]; + out[i + pixels + pixels] = in[B]; in += 4; } } static void -packRGBXL(UINT8* out, const UINT8* in, int pixels) -{ +packRGBXL(UINT8 *out, const UINT8 *in, int pixels) { int i; /* RGBX, line interleaved */ for (i = 0; i < pixels; i++) { out[i] = in[R]; - out[i+pixels] = in[G]; - out[i+pixels+pixels] = in[B]; - out[i+pixels+pixels+pixels] = in[X]; + out[i + pixels] = in[G]; + out[i + pixels + pixels] = in[B]; + out[i + pixels + pixels + pixels] = in[X]; in += 4; } } static void -packI16B(UINT8* out, const UINT8* in_, int pixels) -{ +packI16B(UINT8 *out, const UINT8 *in_, int pixels) { int i; - INT32* in = (INT32*) in_; UINT16 tmp_; - UINT8* tmp = (UINT8*) &tmp_; + UINT8 *tmp = (UINT8 *)&tmp_; for (i = 0; i < pixels; i++) { - if (in[0] <= 0) + INT32 in; + memcpy(&in, in_, sizeof(in)); + if (in <= 0) { tmp_ = 0; - else if (in[0] > 65535) + } else if (in > 65535) { tmp_ = 65535; - else - tmp_ = in[0]; + } else { + tmp_ = in; + } C16B; - out += 2; in++; + out += 2; + in_ += sizeof(in); } } static void -packI16N_I16B(UINT8* out, const UINT8* in, int pixels){ +packI16N_I16B(UINT8 *out, const UINT8 *in, int pixels) { int i; - UINT8* tmp = (UINT8*) in; + UINT8 *tmp = (UINT8 *)in; for (i = 0; i < pixels; i++) { C16B; - out += 2; tmp += 2; + out += 2; + tmp += 2; } - } static void -packI16N_I16(UINT8* out, const UINT8* in, int pixels){ +packI16N_I16(UINT8 *out, const UINT8 *in, int pixels) { int i; - UINT8* tmp = (UINT8*) in; + UINT8 *tmp = (UINT8 *)in; for (i = 0; i < pixels; i++) { C16L; - out += 2; tmp += 2; + out += 2; + tmp += 2; } } - static void -packI32S(UINT8* out, const UINT8* in, int pixels) -{ +packI32S(UINT8 *out, const UINT8 *in, int pixels) { int i; - UINT8* tmp = (UINT8*) in; + UINT8 *tmp = (UINT8 *)in; for (i = 0; i < pixels; i++) { C32L; - out += 4; tmp += 4; + out += 4; + tmp += 4; } } void -ImagingPackLAB(UINT8* out, const UINT8* in, int pixels) -{ +ImagingPackLAB(UINT8 *out, const UINT8 *in, int pixels) { int i; /* LAB triplets */ for (i = 0; i < pixels; i++) { out[0] = in[0]; out[1] = in[1] ^ 128; /* signed in outside world */ out[2] = in[2] ^ 128; - out += 3; in += 4; + out += 3; + in += 4; } } static void -copy1(UINT8* out, const UINT8* in, int pixels) -{ +copy1(UINT8 *out, const UINT8 *in, int pixels) { /* L, P */ memcpy(out, in, pixels); } static void -copy2(UINT8* out, const UINT8* in, int pixels) -{ +copy2(UINT8 *out, const UINT8 *in, int pixels) { /* I;16, etc */ - memcpy(out, in, pixels*2); + memcpy(out, in, pixels * 2); } static void -copy3(UINT8* out, const UINT8* in, int pixels) -{ +copy3(UINT8 *out, const UINT8 *in, int pixels) { /* BGR;24, etc */ - memcpy(out, in, pixels*3); + memcpy(out, in, pixels * 3); } static void -copy4(UINT8* out, const UINT8* in, int pixels) -{ +copy4(UINT8 *out, const UINT8 *in, int pixels) { /* RGBA, CMYK quadruples */ - memcpy(out, in, 4*pixels); + memcpy(out, in, 4 * pixels); } static void -copy4I(UINT8* out, const UINT8* in, int pixels) -{ +copy4I(UINT8 *out, const UINT8 *in, int pixels) { /* RGBA, CMYK quadruples, inverted */ int i; - for (i = 0; i < pixels*4; i++) + for (i = 0; i < pixels * 4; i++) { out[i] = ~in[i]; + } } static void -band0(UINT8* out, const UINT8* in, int pixels) -{ +band0(UINT8 *out, const UINT8 *in, int pixels) { int i; - for (i = 0; i < pixels; i++, in += 4) + for (i = 0; i < pixels; i++, in += 4) { out[i] = in[0]; + } } static void -band1(UINT8* out, const UINT8* in, int pixels) -{ +band1(UINT8 *out, const UINT8 *in, int pixels) { int i; - for (i = 0; i < pixels; i++, in += 4) + for (i = 0; i < pixels; i++, in += 4) { out[i] = in[1]; + } } static void -band2(UINT8* out, const UINT8* in, int pixels) -{ +band2(UINT8 *out, const UINT8 *in, int pixels) { int i; - for (i = 0; i < pixels; i++, in += 4) + for (i = 0; i < pixels; i++, in += 4) { out[i] = in[2]; + } } static void -band3(UINT8* out, const UINT8* in, int pixels) -{ +band3(UINT8 *out, const UINT8 *in, int pixels) { int i; - for (i = 0; i < pixels; i++, in += 4) + for (i = 0; i < pixels; i++, in += 4) { out[i] = in[3]; + } } static struct { - const char* mode; - const char* rawmode; + const char *mode; + const char *rawmode; int bits; ImagingShuffler pack; } packers[] = { /* bilevel */ - {"1", "1", 1, pack1}, - {"1", "1;I", 1, pack1I}, - {"1", "1;R", 1, pack1R}, - {"1", "1;IR", 1, pack1IR}, - {"1", "L", 8, pack1L}, + {"1", "1", 1, pack1}, + {"1", "1;I", 1, pack1I}, + {"1", "1;R", 1, pack1R}, + {"1", "1;IR", 1, pack1IR}, + {"1", "L", 8, pack1L}, /* greyscale */ - {"L", "L", 8, copy1}, - {"L", "L;16", 16, packL16}, - {"L", "L;16B", 16, packL16B}, + {"L", "L", 8, copy1}, + {"L", "L;16", 16, packL16}, + {"L", "L;16B", 16, packL16B}, /* greyscale w. alpha */ - {"LA", "LA", 16, packLA}, - {"LA", "LA;L", 16, packLAL}, + {"LA", "LA", 16, packLA}, + {"LA", "LA;L", 16, packLAL}, + + /* greyscale w. alpha premultiplied */ + {"La", "La", 16, packLA}, /* palette */ - {"P", "P;1", 1, pack1}, - {"P", "P;2", 2, packP2}, - {"P", "P;4", 4, packP4}, - {"P", "P", 8, copy1}, + {"P", "P;1", 1, pack1}, + {"P", "P;2", 2, packP2}, + {"P", "P;4", 4, packP4}, + {"P", "P", 8, copy1}, /* palette w. alpha */ - {"PA", "PA", 16, packLA}, - {"PA", "PA;L", 16, packLAL}, + {"PA", "PA", 16, packLA}, + {"PA", "PA;L", 16, packLAL}, /* true colour */ - {"RGB", "RGB", 24, ImagingPackRGB}, - {"RGB", "RGBX", 32, copy4}, - {"RGB", "XRGB", 32, ImagingPackXRGB}, - {"RGB", "BGR", 24, ImagingPackBGR}, - {"RGB", "BGRX", 32, ImagingPackBGRX}, - {"RGB", "XBGR", 32, ImagingPackXBGR}, - {"RGB", "RGB;L", 24, packRGBL}, - {"RGB", "R", 8, band0}, - {"RGB", "G", 8, band1}, - {"RGB", "B", 8, band2}, + {"RGB", "RGB", 24, ImagingPackRGB}, + {"RGB", "RGBX", 32, copy4}, + {"RGB", "RGBA", 32, copy4}, + {"RGB", "XRGB", 32, ImagingPackXRGB}, + {"RGB", "BGR", 24, ImagingPackBGR}, + {"RGB", "BGRX", 32, ImagingPackBGRX}, + {"RGB", "XBGR", 32, ImagingPackXBGR}, + {"RGB", "RGB;L", 24, packRGBL}, + {"RGB", "R", 8, band0}, + {"RGB", "G", 8, band1}, + {"RGB", "B", 8, band2}, /* true colour w. alpha */ - {"RGBA", "RGBA", 32, copy4}, - {"RGBA", "RGBA;L", 32, packRGBXL}, - {"RGBA", "RGB", 24, ImagingPackRGB}, - {"RGBA", "BGR", 24, ImagingPackBGR}, - {"RGBA", "BGRA", 32, ImagingPackBGRA}, - {"RGBA", "ABGR", 32, ImagingPackABGR}, - {"RGBA", "BGRa", 32, ImagingPackBGRa}, - {"RGBA", "R", 8, band0}, - {"RGBA", "G", 8, band1}, - {"RGBA", "B", 8, band2}, - {"RGBA", "A", 8, band3}, + {"RGBA", "RGBA", 32, copy4}, + {"RGBA", "RGBA;L", 32, packRGBXL}, + {"RGBA", "RGB", 24, ImagingPackRGB}, + {"RGBA", "BGR", 24, ImagingPackBGR}, + {"RGBA", "BGRA", 32, ImagingPackBGRA}, + {"RGBA", "ABGR", 32, ImagingPackABGR}, + {"RGBA", "BGRa", 32, ImagingPackBGRa}, + {"RGBA", "R", 8, band0}, + {"RGBA", "G", 8, band1}, + {"RGBA", "B", 8, band2}, + {"RGBA", "A", 8, band3}, /* true colour w. alpha premultiplied */ - {"RGBa", "RGBa", 32, copy4}, - {"RGBa", "BGRa", 32, ImagingPackBGRA}, - {"RGBa", "aBGR", 32, ImagingPackABGR}, + {"RGBa", "RGBa", 32, copy4}, + {"RGBa", "BGRa", 32, ImagingPackBGRA}, + {"RGBa", "aBGR", 32, ImagingPackABGR}, /* true colour w. padding */ - {"RGBX", "RGBX", 32, copy4}, - {"RGBX", "RGBX;L", 32, packRGBXL}, - {"RGBX", "RGB", 24, ImagingPackRGB}, - {"RGBX", "BGR", 24, ImagingPackBGR}, - {"RGBX", "BGRX", 32, ImagingPackBGRX}, - {"RGBX", "XBGR", 32, ImagingPackXBGR}, - {"RGBX", "R", 8, band0}, - {"RGBX", "G", 8, band1}, - {"RGBX", "B", 8, band2}, - {"RGBX", "X", 8, band3}, + {"RGBX", "RGBX", 32, copy4}, + {"RGBX", "RGBX;L", 32, packRGBXL}, + {"RGBX", "RGB", 24, ImagingPackRGB}, + {"RGBX", "BGR", 24, ImagingPackBGR}, + {"RGBX", "BGRX", 32, ImagingPackBGRX}, + {"RGBX", "XBGR", 32, ImagingPackXBGR}, + {"RGBX", "R", 8, band0}, + {"RGBX", "G", 8, band1}, + {"RGBX", "B", 8, band2}, + {"RGBX", "X", 8, band3}, /* colour separation */ - {"CMYK", "CMYK", 32, copy4}, - {"CMYK", "CMYK;I", 32, copy4I}, - {"CMYK", "CMYK;L", 32, packRGBXL}, - {"CMYK", "C", 8, band0}, - {"CMYK", "M", 8, band1}, - {"CMYK", "Y", 8, band2}, - {"CMYK", "K", 8, band3}, + {"CMYK", "CMYK", 32, copy4}, + {"CMYK", "CMYK;I", 32, copy4I}, + {"CMYK", "CMYK;L", 32, packRGBXL}, + {"CMYK", "C", 8, band0}, + {"CMYK", "M", 8, band1}, + {"CMYK", "Y", 8, band2}, + {"CMYK", "K", 8, band3}, /* video (YCbCr) */ - {"YCbCr", "YCbCr", 24, ImagingPackRGB}, - {"YCbCr", "YCbCr;L", 24, packRGBL}, - {"YCbCr", "YCbCrX", 32, copy4}, - {"YCbCr", "YCbCrK", 32, copy4}, - {"YCbCr", "Y", 8, band0}, - {"YCbCr", "Cb", 8, band1}, - {"YCbCr", "Cr", 8, band2}, + {"YCbCr", "YCbCr", 24, ImagingPackRGB}, + {"YCbCr", "YCbCr;L", 24, packRGBL}, + {"YCbCr", "YCbCrX", 32, copy4}, + {"YCbCr", "YCbCrK", 32, copy4}, + {"YCbCr", "Y", 8, band0}, + {"YCbCr", "Cb", 8, band1}, + {"YCbCr", "Cr", 8, band2}, /* LAB Color */ - {"LAB", "LAB", 24, ImagingPackLAB}, - {"LAB", "L", 8, band0}, - {"LAB", "A", 8, band1}, - {"LAB", "B", 8, band2}, + {"LAB", "LAB", 24, ImagingPackLAB}, + {"LAB", "L", 8, band0}, + {"LAB", "A", 8, band1}, + {"LAB", "B", 8, band2}, /* HSV */ - {"HSV", "HSV", 24, ImagingPackRGB}, - {"HSV", "H", 8, band0}, - {"HSV", "S", 8, band1}, - {"HSV", "V", 8, band2}, + {"HSV", "HSV", 24, ImagingPackRGB}, + {"HSV", "H", 8, band0}, + {"HSV", "S", 8, band1}, + {"HSV", "V", 8, band2}, /* integer */ - {"I", "I", 32, copy4}, - {"I", "I;16B", 16, packI16B}, - {"I", "I;32S", 32, packI32S}, - {"I", "I;32NS", 32, copy4}, + {"I", "I", 32, copy4}, + {"I", "I;16B", 16, packI16B}, + {"I", "I;32S", 32, packI32S}, + {"I", "I;32NS", 32, copy4}, /* floating point */ - {"F", "F", 32, copy4}, - {"F", "F;32F", 32, packI32S}, - {"F", "F;32NF", 32, copy4}, + {"F", "F", 32, copy4}, + {"F", "F;32F", 32, packI32S}, + {"F", "F;32NF", 32, copy4}, /* storage modes */ - {"I;16", "I;16", 16, copy2}, - {"I;16B", "I;16B", 16, copy2}, - {"I;16L", "I;16L", 16, copy2}, - {"I;16", "I;16N", 16, packI16N_I16}, // LibTiff native->image endian. - {"I;16L", "I;16N", 16, packI16N_I16}, - {"I;16B", "I;16N", 16, packI16N_I16B}, - {"BGR;15", "BGR;15", 16, copy2}, - {"BGR;16", "BGR;16", 16, copy2}, - {"BGR;24", "BGR;24", 24, copy3}, + {"I;16", "I;16", 16, copy2}, +#ifdef WORDS_BIGENDIAN + {"I;16", "I;16B", 16, packI16N_I16}, +#else + {"I;16", "I;16B", 16, packI16N_I16B}, +#endif + {"I;16B", "I;16B", 16, copy2}, + {"I;16L", "I;16L", 16, copy2}, + {"I;16", "I;16N", 16, packI16N_I16}, // LibTiff native->image endian. + {"I;16L", "I;16N", 16, packI16N_I16}, + {"I;16B", "I;16N", 16, packI16N_I16B}, + {"BGR;15", "BGR;15", 16, copy2}, + {"BGR;16", "BGR;16", 16, copy2}, + {"BGR;24", "BGR;24", 24, copy3}, {NULL} /* sentinel */ }; - ImagingShuffler -ImagingFindPacker(const char* mode, const char* rawmode, int* bits_out) -{ +ImagingFindPacker(const char *mode, const char *rawmode, int *bits_out) { int i; /* find a suitable pixel packer */ - for (i = 0; packers[i].rawmode; i++) + for (i = 0; packers[i].rawmode; i++) { if (strcmp(packers[i].mode, mode) == 0 && strcmp(packers[i].rawmode, rawmode) == 0) { - if (bits_out) + if (bits_out) { *bits_out = packers[i].bits; + } return packers[i].pack; } + } return NULL; } diff --git a/src/libImaging/PackDecode.c b/src/libImaging/PackDecode.c index aea8f04e301..7dd432b91c2 100644 --- a/src/libImaging/PackDecode.c +++ b/src/libImaging/PackDecode.c @@ -5,7 +5,7 @@ * decoder for PackBits image data. * * history: - * 96-04-19 fl Created + * 96-04-19 fl Created * * Copyright (c) Fredrik Lundh 1996. * Copyright (c) Secret Labs AB 1997. @@ -13,80 +13,80 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" int -ImagingPackbitsDecode(Imaging im, ImagingCodecState state, - UINT8* buf, int bytes) -{ +ImagingPackbitsDecode( + Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { UINT8 n; - UINT8* ptr; + UINT8 *ptr; int i; ptr = buf; for (;;) { - - if (bytes < 1) - return ptr - buf; - - if (ptr[0] & 0x80) { - - if (ptr[0] == 0x80) { - /* Nop */ - ptr++; bytes--; - continue; - } - - /* Run */ - if (bytes < 2) - return ptr - buf; - - for (n = 257 - ptr[0]; n > 0; n--) { - if (state->x >= state->bytes) { - /* state->errcode = IMAGING_CODEC_OVERRUN; */ - break; - } - state->buffer[state->x++] = ptr[1]; - } - - ptr += 2; bytes -= 2; - - } else { - - /* Literal */ - n = ptr[0]+2; - - if (bytes < n) - return ptr - buf; - - for (i = 1; i < n; i++) { - if (state->x >= state->bytes) { - /* state->errcode = IMAGING_CODEC_OVERRUN; */ - break; - } - state->buffer[state->x++] = ptr[i]; - } - - ptr += n; bytes -= n; - - } - - if (state->x >= state->bytes) { - - /* Got a full line, unpack it */ - state->shuffle((UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->buffer, - state->xsize); - - state->x = 0; - - if (++state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } - } - + if (bytes < 1) { + return ptr - buf; + } + + if (ptr[0] & 0x80) { + if (ptr[0] == 0x80) { + /* Nop */ + ptr++; + bytes--; + continue; + } + + /* Run */ + if (bytes < 2) { + return ptr - buf; + } + + for (n = 257 - ptr[0]; n > 0; n--) { + if (state->x >= state->bytes) { + /* state->errcode = IMAGING_CODEC_OVERRUN; */ + break; + } + state->buffer[state->x++] = ptr[1]; + } + + ptr += 2; + bytes -= 2; + + } else { + /* Literal */ + n = ptr[0] + 2; + + if (bytes < n) { + return ptr - buf; + } + + for (i = 1; i < n; i++) { + if (state->x >= state->bytes) { + /* state->errcode = IMAGING_CODEC_OVERRUN; */ + break; + } + state->buffer[state->x++] = ptr[i]; + } + + ptr += n; + bytes -= n; + } + + if (state->x >= state->bytes) { + /* Got a full line, unpack it */ + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->buffer, + state->xsize); + + state->x = 0; + + if (++state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } + } } } diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c index 31c2c0245ca..71a095c2cb8 100644 --- a/src/libImaging/Palette.c +++ b/src/libImaging/Palette.c @@ -16,97 +16,98 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" #include - ImagingPalette -ImagingPaletteNew(const char* mode) -{ +ImagingPaletteNew(const char *mode) { /* Create a palette object */ int i; ImagingPalette palette; - if (strcmp(mode, "RGB") && strcmp(mode, "RGBA")) - return (ImagingPalette) ImagingError_ModeError(); + if (strcmp(mode, "RGB") && strcmp(mode, "RGBA")) { + return (ImagingPalette)ImagingError_ModeError(); + } palette = calloc(1, sizeof(struct ImagingPaletteInstance)); - if (!palette) - return (ImagingPalette) ImagingError_MemoryError(); + if (!palette) { + return (ImagingPalette)ImagingError_MemoryError(); + } - strncpy(palette->mode, mode, IMAGING_MODE_LENGTH); + strncpy(palette->mode, mode, IMAGING_MODE_LENGTH - 1); + palette->mode[IMAGING_MODE_LENGTH - 1] = 0; /* Initialize to ramp */ + palette->size = 256; for (i = 0; i < 256; i++) { - palette->palette[i*4+0] = - palette->palette[i*4+1] = - palette->palette[i*4+2] = (UINT8) i; - palette->palette[i*4+3] = 255; /* opaque */ + palette->palette[i * 4 + 0] = palette->palette[i * 4 + 1] = + palette->palette[i * 4 + 2] = (UINT8)i; + palette->palette[i * 4 + 3] = 255; /* opaque */ } return palette; } ImagingPalette -ImagingPaletteNewBrowser(void) -{ +ImagingPaletteNewBrowser(void) { /* Create a standard "browser" palette object */ int i, r, g, b; ImagingPalette palette; palette = ImagingPaletteNew("RGB"); - if (!palette) + if (!palette) { return NULL; + } /* Blank out unused entries */ /* FIXME: Add 10-level windows palette here? */ for (i = 0; i < 10; i++) { - palette->palette[i*4+0] = - palette->palette[i*4+1] = - palette->palette[i*4+2] = 0; + palette->palette[i * 4 + 0] = palette->palette[i * 4 + 1] = + palette->palette[i * 4 + 2] = 0; } /* Simple 6x6x6 colour cube */ - for (b = 0; b < 256; b += 51) - for (g = 0; g < 256; g += 51) + for (b = 0; b < 256; b += 51) { + for (g = 0; g < 256; g += 51) { for (r = 0; r < 256; r += 51) { - palette->palette[i*4+0] = r; - palette->palette[i*4+1] = g; - palette->palette[i*4+2] = b; + palette->palette[i * 4 + 0] = r; + palette->palette[i * 4 + 1] = g; + palette->palette[i * 4 + 2] = b; i++; } + } + } /* Blank out unused entries */ /* FIXME: add 30-level greyscale wedge here? */ for (; i < 256; i++) { - palette->palette[i*4+0] = - palette->palette[i*4+1] = - palette->palette[i*4+2] = 0; + palette->palette[i * 4 + 0] = palette->palette[i * 4 + 1] = + palette->palette[i * 4 + 2] = 0; } return palette; } ImagingPalette -ImagingPaletteDuplicate(ImagingPalette palette) -{ +ImagingPaletteDuplicate(ImagingPalette palette) { /* Duplicate palette descriptor */ ImagingPalette new_palette; - if (!palette) + if (!palette) { return NULL; + } /* malloc check ok, small constant allocation */ new_palette = malloc(sizeof(struct ImagingPaletteInstance)); - if (!new_palette) - return (ImagingPalette) ImagingError_MemoryError(); + if (!new_palette) { + return (ImagingPalette)ImagingError_MemoryError(); + } memcpy(new_palette, palette, sizeof(struct ImagingPaletteInstance)); @@ -117,18 +118,17 @@ ImagingPaletteDuplicate(ImagingPalette palette) } void -ImagingPaletteDelete(ImagingPalette palette) -{ +ImagingPaletteDelete(ImagingPalette palette) { /* Destroy palette object */ if (palette) { - if (palette->cache) + if (palette->cache) { free(palette->cache); + } free(palette); } } - /* -------------------------------------------------------------------- */ /* Colour mapping */ /* -------------------------------------------------------------------- */ @@ -146,27 +146,26 @@ ImagingPaletteDelete(ImagingPalette palette) #define DIST(a, b, s) (a - b) * (a - b) * s /* Colour weights (no scaling, for now) */ -#define RSCALE 1 -#define GSCALE 1 -#define BSCALE 1 +#define RSCALE 1 +#define GSCALE 1 +#define BSCALE 1 /* Calculated scaled distances */ -#define RDIST(a, b) DIST(a, b, RSCALE*RSCALE) -#define GDIST(a, b) DIST(a, b, GSCALE*GSCALE) -#define BDIST(a, b) DIST(a, b, BSCALE*BSCALE) +#define RDIST(a, b) DIST(a, b, RSCALE *RSCALE) +#define GDIST(a, b) DIST(a, b, GSCALE *GSCALE) +#define BDIST(a, b) DIST(a, b, BSCALE *BSCALE) /* Incremental steps */ -#define RSTEP (4 * RSCALE) -#define GSTEP (4 * GSCALE) -#define BSTEP (4 * BSCALE) +#define RSTEP (4 * RSCALE) +#define GSTEP (4 * GSCALE) +#define BSTEP (4 * BSCALE) -#define BOX 8 +#define BOX 8 -#define BOXVOLUME BOX*BOX*BOX +#define BOXVOLUME BOX *BOX *BOX void -ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) -{ +ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) { int i, j; unsigned int dmin[256], dmax; int r0, g0, b0; @@ -178,39 +177,44 @@ ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) /* Get box boundaries for the given (r,g,b)-triplet. Each box covers eight cache slots (32 colour values, that is). */ - r0 = r & 0xe0; r1 = r0 + 0x1f; rc = (r0 + r1) / 2; - g0 = g & 0xe0; g1 = g0 + 0x1f; gc = (g0 + g1) / 2; - b0 = b & 0xe0; b1 = b0 + 0x1f; bc = (b0 + b1) / 2; + r0 = r & 0xe0; + r1 = r0 + 0x1f; + rc = (r0 + r1) / 2; + g0 = g & 0xe0; + g1 = g0 + 0x1f; + gc = (g0 + g1) / 2; + b0 = b & 0xe0; + b1 = b0 + 0x1f; + bc = (b0 + b1) / 2; /* Step 1 -- Select relevant palette entries (after Heckbert) */ /* For each palette entry, calculate the min and max distances to * any position in the box given by the colour we're looking for. */ - dmax = (unsigned int) ~0; - - for (i = 0; i < 256; i++) { + dmax = (unsigned int)~0; + for (i = 0; i < palette->size; i++) { int r, g, b; unsigned int tmin, tmax; /* Find min and max distances to any point in the box */ - r = palette->palette[i*4+0]; - tmin = (r < r0) ? RDIST(r, r1) : (r > r1) ? RDIST(r, r0) : 0; + r = palette->palette[i * 4 + 0]; + tmin = (r < r0) ? RDIST(r, r0) : (r > r1) ? RDIST(r, r1) : 0; tmax = (r <= rc) ? RDIST(r, r1) : RDIST(r, r0); - g = palette->palette[i*4+1]; - tmin += (g < g0) ? GDIST(g, g1) : (g > g1) ? GDIST(g, g0) : 0; + g = palette->palette[i * 4 + 1]; + tmin += (g < g0) ? GDIST(g, g0) : (g > g1) ? GDIST(g, g1) : 0; tmax += (g <= gc) ? GDIST(g, g1) : GDIST(g, g0); - b = palette->palette[i*4+2]; - tmin += (b < b0) ? BDIST(b, b1) : (b > b1) ? BDIST(b, b0) : 0; + b = palette->palette[i * 4 + 2]; + tmin += (b < b0) ? BDIST(b, b0) : (b > b1) ? BDIST(b, b1) : 0; tmax += (b <= bc) ? BDIST(b, b1) : BDIST(b, b0); dmin[i] = tmin; - if (tmax < dmax) + if (tmax < dmax) { dmax = tmax; /* keep the smallest max distance only */ - + } } /* Step 2 -- Incrementally update cache slot (after Thomas) */ @@ -219,22 +223,21 @@ ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) * all slots in that box. We only check boxes for which the min * distance is less than or equal the smallest max distance */ - for (i = 0; i < BOXVOLUME; i++) - d[i] = (unsigned int) ~0; - - for (i = 0; i < 256; i++) + for (i = 0; i < BOXVOLUME; i++) { + d[i] = (unsigned int)~0; + } + for (i = 0; i < palette->size; i++) { if (dmin[i] <= dmax) { - int rd, gd, bd; int ri, gi, bi; int rx, gx, bx; - ri = (r0 - palette->palette[i*4+0]) * RSCALE; - gi = (g0 - palette->palette[i*4+1]) * GSCALE; - bi = (b0 - palette->palette[i*4+2]) * BSCALE; + ri = (r0 - palette->palette[i * 4 + 0]) * RSCALE; + gi = (g0 - palette->palette[i * 4 + 1]) * GSCALE; + bi = (b0 - palette->palette[i * 4 + 2]) * BSCALE; - rd = ri*ri + gi*gi + bi*bi; + rd = ri * ri + gi * gi + bi * bi; ri = ri * (2 * RSTEP) + RSTEP * RSTEP; gi = gi * (2 * GSTEP) + GSTEP * GSTEP; @@ -242,13 +245,15 @@ ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) rx = ri; for (r = j = 0; r < BOX; r++) { - gd = rd; gx = gi; + gd = rd; + gx = gi; for (g = 0; g < BOX; g++) { - bd = gd; bx = bi; + bd = gd; + bx = bi; for (b = 0; b < BOX; b++) { - if ((unsigned int) bd < d[j]) { + if ((unsigned int)bd < d[j]) { d[j] = bd; - c[j] = (UINT8) i; + c[j] = (UINT8)i; } bd += bx; bx += 2 * BSTEP * BSTEP; @@ -261,6 +266,7 @@ ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) rx += 2 * RSTEP * RSTEP; } } + } /* Step 3 -- Update cache */ @@ -268,46 +274,44 @@ ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) * cache slot in the box. Update the cache. */ j = 0; - for (r = r0; r < r1; r+=4) - for (g = g0; g < g1; g+=4) - for (b = b0; b < b1; b+=4) + for (r = r0; r < r1; r += 4) { + for (g = g0; g < g1; g += 4) { + for (b = b0; b < b1; b += 4) { ImagingPaletteCache(palette, r, g, b) = c[j++]; + } + } + } } - int -ImagingPaletteCachePrepare(ImagingPalette palette) -{ +ImagingPaletteCachePrepare(ImagingPalette palette) { /* Add a colour cache to a palette */ int i; - int entries = 64*64*64; + int entries = 64 * 64 * 64; if (palette->cache == NULL) { - /* The cache is 512k. It might be a good idea to break it up into a pointer array (e.g. an 8-bit image?) */ /* malloc check ok, small constant allocation */ - palette->cache = (INT16*) malloc(entries * sizeof(INT16)); + palette->cache = (INT16 *)malloc(entries * sizeof(INT16)); if (!palette->cache) { - (void) ImagingError_MemoryError(); + (void)ImagingError_MemoryError(); return -1; } /* Mark all entries as empty */ - for (i = 0; i < entries; i++) + for (i = 0; i < entries; i++) { palette->cache[i] = 0x100; - + } } return 0; } - void -ImagingPaletteCacheDelete(ImagingPalette palette) -{ +ImagingPaletteCacheDelete(ImagingPalette palette) { /* Release the colour cache, if any */ if (palette && palette->cache) { diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index 0bda25739ae..acf5202e50c 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -23,11 +23,17 @@ #include "Imaging.h" - static inline void -paste(Imaging imOut, Imaging imIn, int dx, int dy, int sx, int sy, - int xsize, int ysize, int pixelsize) -{ +paste( + Imaging imOut, + Imaging imIn, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { /* paste opaque region */ int y; @@ -37,41 +43,49 @@ paste(Imaging imOut, Imaging imIn, int dx, int dy, int sx, int sy, xsize *= pixelsize; - for (y = 0; y < ysize; y++) - memcpy(imOut->image[y+dy]+dx, imIn->image[y+sy]+sx, xsize); + for (y = 0; y < ysize; y++) { + memcpy(imOut->image[y + dy] + dx, imIn->image[y + sy] + sx, xsize); + } } static inline void -paste_mask_1(Imaging imOut, Imaging imIn, Imaging imMask, - int dx, int dy, int sx, int sy, - int xsize, int ysize, int pixelsize) -{ +paste_mask_1( + Imaging imOut, + Imaging imIn, + Imaging imMask, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { /* paste with mode "1" mask */ int x, y; if (imOut->image8) { - for (y = 0; y < ysize; y++) { - UINT8* out = imOut->image8[y+dy]+dx; - UINT8* in = imIn->image8[y+sy]+sx; - UINT8* mask = imMask->image8[y+sy]+sx; + UINT8 *out = imOut->image8[y + dy] + dx; + UINT8 *in = imIn->image8[y + sy] + sx; + UINT8 *mask = imMask->image8[y + sy] + sx; for (x = 0; x < xsize; x++) { - if (*mask++) + if (*mask++) { *out = *in; + } out++, in++; } } } else { - for (y = 0; y < ysize; y++) { - INT32* out = imOut->image32[y+dy]+dx; - INT32* in = imIn->image32[y+sy]+sx; - UINT8* mask = imMask->image8[y+sy]+sx; + INT32 *out = imOut->image32[y + dy] + dx; + INT32 *in = imIn->image32[y + sy] + sx; + UINT8 *mask = imMask->image8[y + sy] + sx; for (x = 0; x < xsize; x++) { - if (*mask++) + if (*mask++) { *out = *in; + } out++, in++; } } @@ -79,21 +93,27 @@ paste_mask_1(Imaging imOut, Imaging imIn, Imaging imMask, } static inline void -paste_mask_L(Imaging imOut, Imaging imIn, Imaging imMask, - int dx, int dy, int sx, int sy, - int xsize, int ysize, int pixelsize) -{ +paste_mask_L( + Imaging imOut, + Imaging imIn, + Imaging imMask, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { /* paste with mode "L" matte */ int x, y; unsigned int tmp1; if (imOut->image8) { - for (y = 0; y < ysize; y++) { - UINT8* out = imOut->image8[y+dy]+dx; - UINT8* in = imIn->image8[y+sy]+sx; - UINT8* mask = imMask->image8[y+sy]+sx; + UINT8 *out = imOut->image8[y + dy] + dx; + UINT8 *in = imIn->image8[y + sy] + sx; + UINT8 *mask = imMask->image8[y + sy] + sx; for (x = 0; x < xsize; x++) { *out = BLEND(*mask, *out, *in, tmp1); out++, in++, mask++; @@ -101,39 +121,46 @@ paste_mask_L(Imaging imOut, Imaging imIn, Imaging imMask, } } else { - for (y = 0; y < ysize; y++) { - UINT8* out = (UINT8*) (imOut->image32[y + dy] + dx); - UINT8* in = (UINT8*) (imIn->image32[y + sy] + sx); - UINT8* mask = (UINT8*) (imMask->image8[y+sy] + sx); + UINT8 *out = (UINT8 *)(imOut->image32[y + dy] + dx); + UINT8 *in = (UINT8 *)(imIn->image32[y + sy] + sx); + UINT8 *mask = (UINT8 *)(imMask->image8[y + sy] + sx); for (x = 0; x < xsize; x++) { UINT8 a = mask[0]; out[0] = BLEND(a, out[0], in[0], tmp1); out[1] = BLEND(a, out[1], in[1], tmp1); out[2] = BLEND(a, out[2], in[2], tmp1); out[3] = BLEND(a, out[3], in[3], tmp1); - out += 4; in += 4; mask ++; + out += 4; + in += 4; + mask++; } } } } static inline void -paste_mask_RGBA(Imaging imOut, Imaging imIn, Imaging imMask, - int dx, int dy, int sx, int sy, - int xsize, int ysize, int pixelsize) -{ +paste_mask_RGBA( + Imaging imOut, + Imaging imIn, + Imaging imMask, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { /* paste with mode "RGBA" matte */ int x, y; unsigned int tmp1; if (imOut->image8) { - for (y = 0; y < ysize; y++) { - UINT8* out = imOut->image8[y+dy]+dx; - UINT8* in = imIn->image8[y+sy]+sx; - UINT8* mask = (UINT8*) imMask->image[y+sy]+sx*4+3; + UINT8 *out = imOut->image8[y + dy] + dx; + UINT8 *in = imIn->image8[y + sy] + sx; + UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx * 4 + 3; for (x = 0; x < xsize; x++) { *out = BLEND(*mask, *out, *in, tmp1); out++, in++, mask += 4; @@ -141,40 +168,46 @@ paste_mask_RGBA(Imaging imOut, Imaging imIn, Imaging imMask, } } else { - for (y = 0; y < ysize; y++) { - UINT8* out = (UINT8*) (imOut->image32[y + dy] + dx); - UINT8* in = (UINT8*) (imIn->image32[y + sy] + sx); - UINT8* mask = (UINT8*) (imMask->image32[y+sy] + sx); + UINT8 *out = (UINT8 *)(imOut->image32[y + dy] + dx); + UINT8 *in = (UINT8 *)(imIn->image32[y + sy] + sx); + UINT8 *mask = (UINT8 *)(imMask->image32[y + sy] + sx); for (x = 0; x < xsize; x++) { UINT8 a = mask[3]; out[0] = BLEND(a, out[0], in[0], tmp1); out[1] = BLEND(a, out[1], in[1], tmp1); out[2] = BLEND(a, out[2], in[2], tmp1); out[3] = BLEND(a, out[3], in[3], tmp1); - out += 4; in += 4; mask += 4; + out += 4; + in += 4; + mask += 4; } } } } - static inline void -paste_mask_RGBa(Imaging imOut, Imaging imIn, Imaging imMask, - int dx, int dy, int sx, int sy, - int xsize, int ysize, int pixelsize) -{ +paste_mask_RGBa( + Imaging imOut, + Imaging imIn, + Imaging imMask, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { /* paste with mode "RGBa" matte */ int x, y; unsigned int tmp1; if (imOut->image8) { - for (y = 0; y < ysize; y++) { - UINT8* out = imOut->image8[y+dy]+dx; - UINT8* in = imIn->image8[y+sy]+sx; - UINT8* mask = (UINT8*) imMask->image[y+sy]+sx*4+3; + UINT8 *out = imOut->image8[y + dy] + dx; + UINT8 *in = imIn->image8[y + sy] + sx; + UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx * 4 + 3; for (x = 0; x < xsize; x++) { *out = PREBLEND(*mask, *out, *in, tmp1); out++, in++, mask += 4; @@ -182,34 +215,34 @@ paste_mask_RGBa(Imaging imOut, Imaging imIn, Imaging imMask, } } else { - for (y = 0; y < ysize; y++) { - UINT8* out = (UINT8*) (imOut->image32[y + dy] + dx); - UINT8* in = (UINT8*) (imIn->image32[y + sy] + sx); - UINT8* mask = (UINT8*) (imMask->image32[y+sy] + sx); + UINT8 *out = (UINT8 *)(imOut->image32[y + dy] + dx); + UINT8 *in = (UINT8 *)(imIn->image32[y + sy] + sx); + UINT8 *mask = (UINT8 *)(imMask->image32[y + sy] + sx); for (x = 0; x < xsize; x++) { UINT8 a = mask[3]; out[0] = PREBLEND(a, out[0], in[0], tmp1); out[1] = PREBLEND(a, out[1], in[1], tmp1); out[2] = PREBLEND(a, out[2], in[2], tmp1); out[3] = PREBLEND(a, out[3], in[3], tmp1); - out += 4; in += 4; mask += 4; + out += 4; + in += 4; + mask += 4; } } } } int -ImagingPaste(Imaging imOut, Imaging imIn, Imaging imMask, - int dx0, int dy0, int dx1, int dy1) -{ +ImagingPaste( + Imaging imOut, Imaging imIn, Imaging imMask, int dx0, int dy0, int dx1, int dy1) { int xsize, ysize; int pixelsize; int sx0, sy0; ImagingSectionCookie cookie; if (!imOut || !imIn) { - (void) ImagingError_ModeError(); + (void)ImagingError_ModeError(); return -1; } @@ -218,30 +251,34 @@ ImagingPaste(Imaging imOut, Imaging imIn, Imaging imMask, xsize = dx1 - dx0; ysize = dy1 - dy0; - if (xsize != imIn->xsize || ysize != imIn->ysize || - pixelsize != imIn->pixelsize) { - (void) ImagingError_Mismatch(); + if (xsize != imIn->xsize || ysize != imIn->ysize || pixelsize != imIn->pixelsize) { + (void)ImagingError_Mismatch(); return -1; } if (imMask && (xsize != imMask->xsize || ysize != imMask->ysize)) { - (void) ImagingError_Mismatch(); + (void)ImagingError_Mismatch(); return -1; } /* Determine which region to copy */ sx0 = sy0 = 0; - if (dx0 < 0) + if (dx0 < 0) { xsize += dx0, sx0 = -dx0, dx0 = 0; - if (dx0 + xsize > imOut->xsize) + } + if (dx0 + xsize > imOut->xsize) { xsize = imOut->xsize - dx0; - if (dy0 < 0) + } + if (dy0 < 0) { ysize += dy0, sy0 = -dy0, dy0 = 0; - if (dy0 + ysize > imOut->ysize) + } + if (dy0 + ysize > imOut->ysize) { ysize = imOut->ysize - dy0; + } - if (xsize <= 0 || ysize <= 0) + if (xsize <= 0 || ysize <= 0) { return 0; + } if (!imMask) { ImagingSectionEnter(&cookie); @@ -250,30 +287,28 @@ ImagingPaste(Imaging imOut, Imaging imIn, Imaging imMask, } else if (strcmp(imMask->mode, "1") == 0) { ImagingSectionEnter(&cookie); - paste_mask_1(imOut, imIn, imMask, dx0, dy0, sx0, sy0, - xsize, ysize, pixelsize); + paste_mask_1(imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); } else if (strcmp(imMask->mode, "L") == 0) { ImagingSectionEnter(&cookie); - paste_mask_L(imOut, imIn, imMask, dx0, dy0, sx0, sy0, - xsize, ysize, pixelsize); + paste_mask_L(imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - } else if (strcmp(imMask->mode, "RGBA") == 0) { + } else if (strcmp(imMask->mode, "LA") == 0 || strcmp(imMask->mode, "RGBA") == 0) { ImagingSectionEnter(&cookie); - paste_mask_RGBA(imOut, imIn, imMask, dx0, dy0, sx0, sy0, - xsize, ysize, pixelsize); + paste_mask_RGBA( + imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); } else if (strcmp(imMask->mode, "RGBa") == 0) { ImagingSectionEnter(&cookie); - paste_mask_RGBa(imOut, imIn, imMask, dx0, dy0, sx0, sy0, - xsize, ysize, pixelsize); + paste_mask_RGBa( + imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); } else { - (void) ImagingError_ValueError("bad transparency mask"); + (void)ImagingError_ValueError("bad transparency mask"); return -1; } @@ -281,9 +316,14 @@ ImagingPaste(Imaging imOut, Imaging imIn, Imaging imMask, } static inline void -fill(Imaging imOut, const void* ink_, int dx, int dy, - int xsize, int ysize, int pixelsize) -{ +fill( + Imaging imOut, + const void *ink_, + int dx, + int dy, + int xsize, + int ysize, + int pixelsize) { /* fill opaque region */ int x, y; @@ -294,28 +334,34 @@ fill(Imaging imOut, const void* ink_, int dx, int dy, memcpy(&ink8, ink_, sizeof(ink8)); if (imOut->image8 || ink32 == 0L) { - dx *= pixelsize; xsize *= pixelsize; - for (y = 0; y < ysize; y++) - memset(imOut->image[y+dy]+dx, ink8, xsize); + for (y = 0; y < ysize; y++) { + memset(imOut->image[y + dy] + dx, ink8, xsize); + } } else { - for (y = 0; y < ysize; y++) { - INT32* out = imOut->image32[y+dy]+dx; - for (x = 0; x < xsize; x++) + INT32 *out = imOut->image32[y + dy] + dx; + for (x = 0; x < xsize; x++) { out[x] = ink32; + } } - } } static inline void -fill_mask_1(Imaging imOut, const void* ink_, Imaging imMask, - int dx, int dy, int sx, int sy, - int xsize, int ysize, int pixelsize) -{ +fill_mask_1( + Imaging imOut, + const void *ink_, + Imaging imMask, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { /* fill with mode "1" mask */ int x, y; @@ -326,25 +372,25 @@ fill_mask_1(Imaging imOut, const void* ink_, Imaging imMask, memcpy(&ink8, ink_, sizeof(ink8)); if (imOut->image8) { - for (y = 0; y < ysize; y++) { - UINT8* out = imOut->image8[y+dy]+dx; - UINT8* mask = imMask->image8[y+sy]+sx; + UINT8 *out = imOut->image8[y + dy] + dx; + UINT8 *mask = imMask->image8[y + sy] + sx; for (x = 0; x < xsize; x++) { - if (*mask++) + if (*mask++) { *out = ink8; + } out++; } } } else { - for (y = 0; y < ysize; y++) { - INT32* out = imOut->image32[y+dy]+dx; - UINT8* mask = imMask->image8[y+sy]+sx; + INT32 *out = imOut->image32[y + dy] + dx; + UINT8 *mask = imMask->image8[y + sy] + sx; for (x = 0; x < xsize; x++) { - if (*mask++) + if (*mask++) { *out = ink32; + } out++; } } @@ -352,36 +398,58 @@ fill_mask_1(Imaging imOut, const void* ink_, Imaging imMask, } static inline void -fill_mask_L(Imaging imOut, const UINT8* ink, Imaging imMask, - int dx, int dy, int sx, int sy, - int xsize, int ysize, int pixelsize) -{ +fill_mask_L( + Imaging imOut, + const UINT8 *ink, + Imaging imMask, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { /* fill with mode "L" matte */ int x, y, i; unsigned int tmp1; if (imOut->image8) { - for (y = 0; y < ysize; y++) { - UINT8* out = imOut->image8[y+dy]+dx; - UINT8* mask = imMask->image8[y+sy]+sx; + UINT8 *out = imOut->image8[y + dy] + dx; + if (strncmp(imOut->mode, "I;16", 4) == 0) { + out += dx; + } + UINT8 *mask = imMask->image8[y + sy] + sx; for (x = 0; x < xsize; x++) { *out = BLEND(*mask, *out, ink[0], tmp1); + if (strncmp(imOut->mode, "I;16", 4) == 0) { + out++; + *out = BLEND(*mask, *out, ink[0], tmp1); + } out++, mask++; } } } else { - + int alpha_channel = strcmp(imOut->mode, "RGBa") == 0 || + strcmp(imOut->mode, "RGBA") == 0 || + strcmp(imOut->mode, "La") == 0 || + strcmp(imOut->mode, "LA") == 0 || + strcmp(imOut->mode, "PA") == 0; for (y = 0; y < ysize; y++) { - UINT8* out = (UINT8*) imOut->image[y+dy]+dx*pixelsize; - UINT8* mask = (UINT8*) imMask->image[y+sy]+sx; + UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx * pixelsize; + UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; for (x = 0; x < xsize; x++) { for (i = 0; i < pixelsize; i++) { - *out = BLEND(*mask, *out, ink[i], tmp1); - out++; + UINT8 channel_mask = *mask; + if (alpha_channel && i != 3 && channel_mask != 0) { + channel_mask = + 255 - (255 - channel_mask) * (1 - (255 - out[3]) / 255); + } + out[i] = BLEND(channel_mask, out[i], ink[i], tmp1); } + out += pixelsize; mask++; } } @@ -389,21 +457,27 @@ fill_mask_L(Imaging imOut, const UINT8* ink, Imaging imMask, } static inline void -fill_mask_RGBA(Imaging imOut, const UINT8* ink, Imaging imMask, - int dx, int dy, int sx, int sy, - int xsize, int ysize, int pixelsize) -{ +fill_mask_RGBA( + Imaging imOut, + const UINT8 *ink, + Imaging imMask, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { /* fill with mode "RGBA" matte */ int x, y, i; unsigned int tmp1; if (imOut->image8) { - - sx = sx*4+3; + sx = sx * 4 + 3; for (y = 0; y < ysize; y++) { - UINT8* out = imOut->image8[y+dy]+dx; - UINT8* mask = (UINT8*) imMask->image[y+sy]+sx; + UINT8 *out = imOut->image8[y + dy] + dx; + UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; for (x = 0; x < xsize; x++) { *out = BLEND(*mask, *out, ink[0], tmp1); out++, mask += 4; @@ -411,12 +485,11 @@ fill_mask_RGBA(Imaging imOut, const UINT8* ink, Imaging imMask, } } else { - dx *= pixelsize; - sx = sx*4 + 3; + sx = sx * 4 + 3; for (y = 0; y < ysize; y++) { - UINT8* out = (UINT8*) imOut->image[y+dy]+dx; - UINT8* mask = (UINT8*) imMask->image[y+sy]+sx; + UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx; + UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; for (x = 0; x < xsize; x++) { for (i = 0; i < pixelsize; i++) { *out = BLEND(*mask, *out, ink[i], tmp1); @@ -429,21 +502,27 @@ fill_mask_RGBA(Imaging imOut, const UINT8* ink, Imaging imMask, } static inline void -fill_mask_RGBa(Imaging imOut, const UINT8* ink, Imaging imMask, - int dx, int dy, int sx, int sy, - int xsize, int ysize, int pixelsize) -{ +fill_mask_RGBa( + Imaging imOut, + const UINT8 *ink, + Imaging imMask, + int dx, + int dy, + int sx, + int sy, + int xsize, + int ysize, + int pixelsize) { /* fill with mode "RGBa" matte */ int x, y, i; unsigned int tmp1; if (imOut->image8) { - - sx = sx*4 + 3; + sx = sx * 4 + 3; for (y = 0; y < ysize; y++) { - UINT8* out = imOut->image8[y+dy]+dx; - UINT8* mask = (UINT8*) imMask->image[y+sy]+sx; + UINT8 *out = imOut->image8[y + dy] + dx; + UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; for (x = 0; x < xsize; x++) { *out = PREBLEND(*mask, *out, ink[0], tmp1); out++, mask += 4; @@ -451,12 +530,11 @@ fill_mask_RGBa(Imaging imOut, const UINT8* ink, Imaging imMask, } } else { - dx *= pixelsize; - sx = sx*4 + 3; + sx = sx * 4 + 3; for (y = 0; y < ysize; y++) { - UINT8* out = (UINT8*) imOut->image[y+dy]+dx; - UINT8* mask = (UINT8*) imMask->image[y+sy]+sx; + UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx; + UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; for (x = 0; x < xsize; x++) { for (i = 0; i < pixelsize; i++) { *out = PREBLEND(*mask, *out, ink[i], tmp1); @@ -469,16 +547,21 @@ fill_mask_RGBa(Imaging imOut, const UINT8* ink, Imaging imMask, } int -ImagingFill2(Imaging imOut, const void* ink, Imaging imMask, - int dx0, int dy0, int dx1, int dy1) -{ +ImagingFill2( + Imaging imOut, + const void *ink, + Imaging imMask, + int dx0, + int dy0, + int dx1, + int dy1) { ImagingSectionCookie cookie; int xsize, ysize; int pixelsize; int sx0, sy0; if (!imOut || !ink) { - (void) ImagingError_ModeError(); + (void)ImagingError_ModeError(); return -1; } @@ -488,23 +571,28 @@ ImagingFill2(Imaging imOut, const void* ink, Imaging imMask, ysize = dy1 - dy0; if (imMask && (xsize != imMask->xsize || ysize != imMask->ysize)) { - (void) ImagingError_Mismatch(); + (void)ImagingError_Mismatch(); return -1; } /* Determine which region to fill */ sx0 = sy0 = 0; - if (dx0 < 0) + if (dx0 < 0) { xsize += dx0, sx0 = -dx0, dx0 = 0; - if (dx0 + xsize > imOut->xsize) + } + if (dx0 + xsize > imOut->xsize) { xsize = imOut->xsize - dx0; - if (dy0 < 0) + } + if (dy0 < 0) { ysize += dy0, sy0 = -dy0, dy0 = 0; - if (dy0 + ysize > imOut->ysize) + } + if (dy0 + ysize > imOut->ysize) { ysize = imOut->ysize - dy0; + } - if (xsize <= 0 || ysize <= 0) + if (xsize <= 0 || ysize <= 0) { return 0; + } if (!imMask) { ImagingSectionEnter(&cookie); @@ -513,30 +601,26 @@ ImagingFill2(Imaging imOut, const void* ink, Imaging imMask, } else if (strcmp(imMask->mode, "1") == 0) { ImagingSectionEnter(&cookie); - fill_mask_1(imOut, ink, imMask, dx0, dy0, sx0, sy0, - xsize, ysize, pixelsize); + fill_mask_1(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); } else if (strcmp(imMask->mode, "L") == 0) { ImagingSectionEnter(&cookie); - fill_mask_L(imOut, ink, imMask, dx0, dy0, sx0, sy0, - xsize, ysize, pixelsize); + fill_mask_L(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); } else if (strcmp(imMask->mode, "RGBA") == 0) { ImagingSectionEnter(&cookie); - fill_mask_RGBA(imOut, ink, imMask, dx0, dy0, sx0, sy0, - xsize, ysize, pixelsize); + fill_mask_RGBA(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); } else if (strcmp(imMask->mode, "RGBa") == 0) { ImagingSectionEnter(&cookie); - fill_mask_RGBa(imOut, ink, imMask, dx0, dy0, sx0, sy0, - xsize, ysize, pixelsize); + fill_mask_RGBa(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); } else { - (void) ImagingError_ValueError("bad transparency mask"); + (void)ImagingError_ValueError("bad transparency mask"); return -1; } diff --git a/src/libImaging/PcdDecode.c b/src/libImaging/PcdDecode.c index f9234389023..f13803cb688 100644 --- a/src/libImaging/PcdDecode.c +++ b/src/libImaging/PcdDecode.c @@ -5,13 +5,13 @@ * decoder for uncompressed PCD image data. * * history: - * 96-05-10 fl Created - * 96-05-18 fl New tables - * 97-01-25 fl Use PhotoYCC unpacker + * 96-05-10 fl Created + * 96-05-18 fl New tables + * 97-01-25 fl Use PhotoYCC unpacker * * notes: - * This driver supports uncompressed PCD modes only - * (resolutions up to 768x512). + * This driver supports uncompressed PCD modes only + * (resolutions up to 768x512). * * Copyright (c) Fredrik Lundh 1996-97. * Copyright (c) Secret Labs AB 1997. @@ -19,60 +19,56 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" - int -ImagingPcdDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ +ImagingPcdDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { int x; int chunk; - UINT8* out; - UINT8* ptr; + UINT8 *out; + UINT8 *ptr; ptr = buf; chunk = 3 * state->xsize; for (;;) { - - /* We need data for two full lines before we can do anything */ - if (bytes < chunk) - return ptr - buf; - - /* Unpack first line */ - out = state->buffer; - for (x = 0; x < state->xsize; x++) { - out[0] = ptr[x]; - out[1] = ptr[(x+4*state->xsize)/2]; - out[2] = ptr[(x+5*state->xsize)/2]; - out += 3; - } - - state->shuffle((UINT8*) im->image[state->y], - state->buffer, state->xsize); - - if (++state->y >= state->ysize) - return -1; /* This can hardly happen */ - - /* Unpack second line */ - out = state->buffer; - for (x = 0; x < state->xsize; x++) { - out[0] = ptr[x+state->xsize]; - out[1] = ptr[(x+4*state->xsize)/2]; - out[2] = ptr[(x+5*state->xsize)/2]; - out += 3; - } - - state->shuffle((UINT8*) im->image[state->y], - state->buffer, state->xsize); - - if (++state->y >= state->ysize) - return -1; - - ptr += chunk; - bytes -= chunk; - + /* We need data for two full lines before we can do anything */ + if (bytes < chunk) { + return ptr - buf; + } + + /* Unpack first line */ + out = state->buffer; + for (x = 0; x < state->xsize; x++) { + out[0] = ptr[x]; + out[1] = ptr[(x + 4 * state->xsize) / 2]; + out[2] = ptr[(x + 5 * state->xsize) / 2]; + out += 3; + } + + state->shuffle((UINT8 *)im->image[state->y], state->buffer, state->xsize); + + if (++state->y >= state->ysize) { + return -1; /* This can hardly happen */ + } + + /* Unpack second line */ + out = state->buffer; + for (x = 0; x < state->xsize; x++) { + out[0] = ptr[x + state->xsize]; + out[1] = ptr[(x + 4 * state->xsize) / 2]; + out[2] = ptr[(x + 5 * state->xsize) / 2]; + out += 3; + } + + state->shuffle((UINT8 *)im->image[state->y], state->buffer, state->xsize); + + if (++state->y >= state->ysize) { + return -1; + } + + ptr += chunk; + bytes -= chunk; } } diff --git a/src/libImaging/PcxDecode.c b/src/libImaging/PcxDecode.c index e5417f1bdd0..c95ffc8692c 100644 --- a/src/libImaging/PcxDecode.c +++ b/src/libImaging/PcxDecode.c @@ -5,7 +5,7 @@ * decoder for PCX image data. * * history: - * 95-09-14 fl Created + * 95-09-14 fl Created * * Copyright (c) Fredrik Lundh 1995. * Copyright (c) Secret Labs AB 1997. @@ -13,72 +13,77 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" int -ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ +ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { UINT8 n; - UINT8* ptr; + UINT8 *ptr; + + if ((state->xsize * state->bits + 7) / 8 > state->bytes) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } ptr = buf; for (;;) { + if (bytes < 1) { + return ptr - buf; + } - if (bytes < 1) - return ptr - buf; - - if ((*ptr & 0xC0) == 0xC0) { - - /* Run */ - if (bytes < 2) - return ptr - buf; - - n = ptr[0] & 0x3F; - - while (n > 0) { - if (state->x >= state->bytes) { - state->errcode = IMAGING_CODEC_OVERRUN; - break; - } - state->buffer[state->x++] = ptr[1]; - n--; - } + if ((*ptr & 0xC0) == 0xC0) { + /* Run */ + if (bytes < 2) { + return ptr - buf; + } - ptr += 2; bytes -= 2; + n = ptr[0] & 0x3F; - } else { + while (n > 0) { + if (state->x >= state->bytes) { + state->errcode = IMAGING_CODEC_OVERRUN; + break; + } + state->buffer[state->x++] = ptr[1]; + n--; + } - /* Literal */ - state->buffer[state->x++] = ptr[0]; - ptr++; bytes--; + ptr += 2; + bytes -= 2; - } + } else { + /* Literal */ + state->buffer[state->x++] = ptr[0]; + ptr++; + bytes--; + } - if (state->x >= state->bytes) { - if (state->bytes % state->xsize && state->bytes > state->xsize) { - int bands = state->bytes / state->xsize; - int stride = state->bytes / bands; - int i; - for (i=1; i< bands; i++) { // note -- skipping first band - memmove(&state->buffer[i*state->xsize], - &state->buffer[i*stride], + if (state->x >= state->bytes) { + if (state->bytes % state->xsize && state->bytes > state->xsize) { + int bands = state->bytes / state->xsize; + int stride = state->bytes / bands; + int i; + for (i = 1; i < bands; i++) { // note -- skipping first band + memmove( + &state->buffer[i * state->xsize], + &state->buffer[i * stride], state->xsize); + } + } + /* Got a full line, unpack it */ + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->buffer, + state->xsize); + + state->x = 0; + + if (++state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; } } - /* Got a full line, unpack it */ - state->shuffle((UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->buffer, - state->xsize); - - state->x = 0; - - if (++state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } - } - } } diff --git a/src/libImaging/PcxEncode.c b/src/libImaging/PcxEncode.c index 163b09b139a..549614bfd39 100644 --- a/src/libImaging/PcxEncode.c +++ b/src/libImaging/PcxEncode.c @@ -13,7 +13,6 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" enum { INIT, FETCH, ENCODE }; @@ -22,9 +21,8 @@ enum { INIT, FETCH, ENCODE }; #define LAST ystep int -ImagingPcxEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ - UINT8* ptr; +ImagingPcxEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + UINT8 *ptr; int this; int bytes_per_line = 0; int padding = 0; @@ -45,12 +43,12 @@ ImagingPcxEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) } bpp = state->bits; - if (state->bits == 24){ + if (state->bits == 24) { planes = 3; bpp = 8; } - bytes_per_line = (state->xsize*bpp + 7) / 8; + bytes_per_line = (state->xsize * bpp + 7) / 8; /* The stride here needs to be kept in sync with the version in PcxImagePlugin.py. If it's not, the header and the body of the image will be out of sync and bad things will happen on decode. @@ -59,133 +57,131 @@ ImagingPcxEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) padding = stride - bytes_per_line; - for (;;) { - switch (state->state) { - case FETCH: - - /* get a line of data */ - if (state->y >= state->ysize) { - state->errcode = IMAGING_CODEC_END; - return ptr - buf; - } - - state->shuffle(state->buffer, - (UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->xsize); - - state->y += 1; - - state->count = 1; - state->LAST = state->buffer[0]; - - state->x = 1; - - state->state = ENCODE; - /* fall through */ - - case ENCODE: - /* compress this line */ - - /* when we arrive here, "count" contains the number of - bytes having the value of "LAST" that we've already - seen */ - do { - /* If we're encoding an odd width file, and we've - got more than one plane, we need to pad each - color row with padding bytes at the end. Since - The pixels are stored RRRRRGGGGGBBBBB, so we need - to have the padding be RRRRRPGGGGGPBBBBBP. Hence - the double loop - */ - while (state->x % bytes_per_line) { - - if (state->count == 63) { - /* this run is full; flush it */ - if (bytes < 2) { - return ptr - buf; - } - ptr[0] = 0xff; - ptr[1] = state->LAST; - ptr += 2; - bytes -= 2; + case FETCH: - state->count = 0; - } - - this = state->buffer[state->x]; - - if (this == state->LAST) { - /* extend the current run */ - state->x += 1; - state->count += 1; + /* get a line of data */ + if (state->y >= state->ysize) { + state->errcode = IMAGING_CODEC_END; + return ptr - buf; + } - } else { - /* start a new run */ - if (state->count == 1 && (state->LAST < 0xc0)) { - if (bytes < 1) { + state->shuffle( + state->buffer, + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->xsize); + + state->y += 1; + + state->count = 1; + state->LAST = state->buffer[0]; + + state->x = 1; + + state->state = ENCODE; + /* fall through */ + + case ENCODE: + /* compress this line */ + + /* when we arrive here, "count" contains the number of + bytes having the value of "LAST" that we've already + seen */ + do { + /* If we're encoding an odd width file, and we've + got more than one plane, we need to pad each + color row with padding bytes at the end. Since + The pixels are stored RRRRRGGGGGBBBBB, so we need + to have the padding be RRRRRPGGGGGPBBBBBP. Hence + the double loop + */ + while (state->x % bytes_per_line) { + if (state->count == 63) { + /* this run is full; flush it */ + if (bytes < 2) { return ptr - buf; } - ptr[0] = state->LAST; - ptr += 1; - bytes -= 1; + ptr[0] = 0xff; + ptr[1] = state->LAST; + ptr += 2; + bytes -= 2; + + state->count = 0; + } + + this = state->buffer[state->x]; + + if (this == state->LAST) { + /* extend the current run */ + state->x += 1; + state->count += 1; + } else { - if (state->count > 0) { - if (bytes < 2) { + /* start a new run */ + if (state->count == 1 && (state->LAST < 0xc0)) { + if (bytes < 1) { return ptr - buf; } - ptr[0] = 0xc0 | state->count; - ptr[1] = state->LAST; - ptr += 2; - bytes -= 2; + ptr[0] = state->LAST; + ptr += 1; + bytes -= 1; + } else { + if (state->count > 0) { + if (bytes < 2) { + return ptr - buf; + } + ptr[0] = 0xc0 | state->count; + ptr[1] = state->LAST; + ptr += 2; + bytes -= 2; + } } - } - state->LAST = this; - state->count = 1; + state->LAST = this; + state->count = 1; - state->x += 1; + state->x += 1; + } } - } - /* end of line; flush the current run */ - if (state->count == 1 && (state->LAST < 0xc0)) { - if (bytes < 1 + padding) { - return ptr - buf; - } - ptr[0] = state->LAST; - ptr += 1; - bytes -= 1; - } else { - if (state->count > 0) { - if (bytes < 2 + padding) { + /* end of line; flush the current run */ + if (state->count == 1 && (state->LAST < 0xc0)) { + if (bytes < 1 + padding) { return ptr - buf; } - ptr[0] = 0xc0 | state->count; - ptr[1] = state->LAST; - ptr += 2; - bytes -= 2; + ptr[0] = state->LAST; + ptr += 1; + bytes -= 1; + } else { + if (state->count > 0) { + if (bytes < 2 + padding) { + return ptr - buf; + } + ptr[0] = 0xc0 | state->count; + ptr[1] = state->LAST; + ptr += 2; + bytes -= 2; + } } - } - /* add the padding */ - for (i = 0; i < padding; i++) { - ptr[0] = 0; - ptr += 1; - bytes -= 1; - } - /* reset for the next color plane. */ - if (state->x < planes * bytes_per_line) { - state->count = 1; - state->LAST = state->buffer[state->x]; - state->x += 1; - } - } while (state->x < planes * bytes_per_line); + /* add the padding */ + for (i = 0; i < padding; i++) { + ptr[0] = 0; + ptr += 1; + bytes -= 1; + } + /* reset for the next color plane. */ + if (state->x < planes * bytes_per_line) { + state->count = 1; + state->LAST = state->buffer[state->x]; + state->x += 1; + } + } while (state->x < planes * bytes_per_line); - /* read next line */ - state->state = FETCH; - break; + /* read next line */ + state->state = FETCH; + break; } } } - diff --git a/src/libImaging/Point.c b/src/libImaging/Point.c index 426c410c961..8883578cbff 100644 --- a/src/libImaging/Point.c +++ b/src/libImaging/Point.c @@ -19,166 +19,171 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" typedef struct { - const void* table; + const void *table; } im_point_context; static void -im_point_8_8(Imaging imOut, Imaging imIn, im_point_context* context) -{ +im_point_8_8(Imaging imOut, Imaging imIn, im_point_context *context) { int x, y; /* 8-bit source, 8-bit destination */ - UINT8* table = (UINT8*) context->table; + UINT8 *table = (UINT8 *)context->table; for (y = 0; y < imIn->ysize; y++) { - UINT8* in = imIn->image8[y]; - UINT8* out = imOut->image8[y]; - for (x = 0; x < imIn->xsize; x++) + UINT8 *in = imIn->image8[y]; + UINT8 *out = imOut->image8[y]; + for (x = 0; x < imIn->xsize; x++) { out[x] = table[in[x]]; + } } } static void -im_point_2x8_2x8(Imaging imOut, Imaging imIn, im_point_context* context) -{ +im_point_2x8_2x8(Imaging imOut, Imaging imIn, im_point_context *context) { int x, y; /* 2x8-bit source, 2x8-bit destination */ - UINT8* table = (UINT8*) context->table; + UINT8 *table = (UINT8 *)context->table; for (y = 0; y < imIn->ysize; y++) { - UINT8* in = (UINT8*) imIn->image[y]; - UINT8* out = (UINT8*) imOut->image[y]; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = (UINT8 *)imOut->image[y]; for (x = 0; x < imIn->xsize; x++) { out[0] = table[in[0]]; - out[3] = table[in[3]+256]; - in += 4; out += 4; + out[3] = table[in[3] + 256]; + in += 4; + out += 4; } } } static void -im_point_3x8_3x8(Imaging imOut, Imaging imIn, im_point_context* context) -{ +im_point_3x8_3x8(Imaging imOut, Imaging imIn, im_point_context *context) { int x, y; /* 3x8-bit source, 3x8-bit destination */ - UINT8* table = (UINT8*) context->table; + UINT8 *table = (UINT8 *)context->table; for (y = 0; y < imIn->ysize; y++) { - UINT8* in = (UINT8*) imIn->image[y]; - UINT8* out = (UINT8*) imOut->image[y]; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = (UINT8 *)imOut->image[y]; for (x = 0; x < imIn->xsize; x++) { out[0] = table[in[0]]; - out[1] = table[in[1]+256]; - out[2] = table[in[2]+512]; - in += 4; out += 4; + out[1] = table[in[1] + 256]; + out[2] = table[in[2] + 512]; + in += 4; + out += 4; } } } static void -im_point_4x8_4x8(Imaging imOut, Imaging imIn, im_point_context* context) -{ +im_point_4x8_4x8(Imaging imOut, Imaging imIn, im_point_context *context) { int x, y; /* 4x8-bit source, 4x8-bit destination */ - UINT8* table = (UINT8*) context->table; + UINT8 *table = (UINT8 *)context->table; for (y = 0; y < imIn->ysize; y++) { - UINT8* in = (UINT8*) imIn->image[y]; - UINT8* out = (UINT8*) imOut->image[y]; + UINT8 *in = (UINT8 *)imIn->image[y]; + UINT8 *out = (UINT8 *)imOut->image[y]; for (x = 0; x < imIn->xsize; x++) { out[0] = table[in[0]]; - out[1] = table[in[1]+256]; - out[2] = table[in[2]+512]; - out[3] = table[in[3]+768]; - in += 4; out += 4; + out[1] = table[in[1] + 256]; + out[2] = table[in[2] + 512]; + out[3] = table[in[3] + 768]; + in += 4; + out += 4; } } } static void -im_point_8_32(Imaging imOut, Imaging imIn, im_point_context* context) -{ +im_point_8_32(Imaging imOut, Imaging imIn, im_point_context *context) { int x, y; /* 8-bit source, 32-bit destination */ - INT32* table = (INT32*) context->table; + char *table = (char *)context->table; for (y = 0; y < imIn->ysize; y++) { - UINT8* in = imIn->image8[y]; - INT32* out = imOut->image32[y]; - for (x = 0; x < imIn->xsize; x++) - out[x] = table[in[x]]; + UINT8 *in = imIn->image8[y]; + INT32 *out = imOut->image32[y]; + for (x = 0; x < imIn->xsize; x++) { + memcpy(out + x, table + in[x] * sizeof(INT32), sizeof(INT32)); + } } } static void -im_point_32_8(Imaging imOut, Imaging imIn, im_point_context* context) -{ +im_point_32_8(Imaging imOut, Imaging imIn, im_point_context *context) { int x, y; /* 32-bit source, 8-bit destination */ - UINT8* table = (UINT8*) context->table; + UINT8 *table = (UINT8 *)context->table; for (y = 0; y < imIn->ysize; y++) { - INT32* in = imIn->image32[y]; - UINT8* out = imOut->image8[y]; + INT32 *in = imIn->image32[y]; + UINT8 *out = imOut->image8[y]; for (x = 0; x < imIn->xsize; x++) { int v = in[x]; - if (v < 0) + if (v < 0) { v = 0; - else if (v > 65535) + } else if (v > 65535) { v = 65535; + } out[x] = table[v]; } } } Imaging -ImagingPoint(Imaging imIn, const char* mode, const void* table) -{ +ImagingPoint(Imaging imIn, const char *mode, const void *table) { /* lookup table transform */ ImagingSectionCookie cookie; Imaging imOut; im_point_context context; - void (*point)(Imaging imIn, Imaging imOut, im_point_context* context); + void (*point)(Imaging imIn, Imaging imOut, im_point_context * context); - if (!imIn) - return (Imaging) ImagingError_ModeError(); + if (!imIn) { + return (Imaging)ImagingError_ModeError(); + } - if (!mode) + if (!mode) { mode = imIn->mode; + } if (imIn->type != IMAGING_TYPE_UINT8) { - if (imIn->type != IMAGING_TYPE_INT32 || strcmp(mode, "L") != 0) + if (imIn->type != IMAGING_TYPE_INT32 || strcmp(mode, "L") != 0) { goto mode_mismatch; - } else if (!imIn->image8 && strcmp(imIn->mode, mode) != 0) + } + } else if (!imIn->image8 && strcmp(imIn->mode, mode) != 0) { goto mode_mismatch; + } imOut = ImagingNew(mode, imIn->xsize, imIn->ysize); - if (!imOut) - return NULL; + if (!imOut) { + return NULL; + } /* find appropriate handler */ if (imIn->type == IMAGING_TYPE_UINT8) { if (imIn->bands == imOut->bands && imIn->type == imOut->type) { switch (imIn->bands) { - case 1: - point = im_point_8_8; - break; - case 2: - point = im_point_2x8_2x8; - break; - case 3: - point = im_point_3x8_3x8; - break; - case 4: - point = im_point_4x8_4x8; - break; - default: - /* this cannot really happen */ - point = im_point_8_8; - break; + case 1: + point = im_point_8_8; + break; + case 2: + point = im_point_2x8_2x8; + break; + case 3: + point = im_point_3x8_3x8; + break; + case 4: + point = im_point_4x8_4x8; + break; + default: + /* this cannot really happen */ + point = im_point_8_8; + break; } - } else + } else { point = im_point_8_32; - } else + } + } else { point = im_point_32_8; + } ImagingCopyPalette(imOut, imIn); @@ -191,70 +196,74 @@ ImagingPoint(Imaging imIn, const char* mode, const void* table) return imOut; - mode_mismatch: - return (Imaging) ImagingError_ValueError( - "point operation not supported for this mode" - ); +mode_mismatch: + return (Imaging)ImagingError_ValueError( + "point operation not supported for this mode"); } - Imaging -ImagingPointTransform(Imaging imIn, double scale, double offset) -{ +ImagingPointTransform(Imaging imIn, double scale, double offset) { /* scale/offset transform */ ImagingSectionCookie cookie; Imaging imOut; int x, y; - if (!imIn || (strcmp(imIn->mode, "I") != 0 && - strcmp(imIn->mode, "I;16") != 0 && - strcmp(imIn->mode, "F") != 0)) - return (Imaging) ImagingError_ModeError(); + if (!imIn || (strcmp(imIn->mode, "I") != 0 && strcmp(imIn->mode, "I;16") != 0 && + strcmp(imIn->mode, "F") != 0)) { + return (Imaging)ImagingError_ModeError(); + } imOut = ImagingNew(imIn->mode, imIn->xsize, imIn->ysize); - if (!imOut) - return NULL; + if (!imOut) { + return NULL; + } switch (imIn->type) { - case IMAGING_TYPE_INT32: - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - INT32* in = imIn->image32[y]; - INT32* out = imOut->image32[y]; - /* FIXME: add clipping? */ - for (x = 0; x < imIn->xsize; x++) - out[x] = in[x] * scale + offset; - } - ImagingSectionLeave(&cookie); - break; - case IMAGING_TYPE_FLOAT32: - ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { - FLOAT32* in = (FLOAT32*) imIn->image32[y]; - FLOAT32* out = (FLOAT32*) imOut->image32[y]; - for (x = 0; x < imIn->xsize; x++) - out[x] = in[x] * scale + offset; - } - ImagingSectionLeave(&cookie); - break; - case IMAGING_TYPE_SPECIAL: - if (strcmp(imIn->mode,"I;16") == 0) { + case IMAGING_TYPE_INT32: ImagingSectionEnter(&cookie); for (y = 0; y < imIn->ysize; y++) { - UINT16* in = (UINT16 *)imIn->image[y]; - UINT16* out = (UINT16 *)imOut->image[y]; + INT32 *in = imIn->image32[y]; + INT32 *out = imOut->image32[y]; /* FIXME: add clipping? */ - for (x = 0; x < imIn->xsize; x++) + for (x = 0; x < imIn->xsize; x++) { out[x] = in[x] * scale + offset; + } } ImagingSectionLeave(&cookie); break; - } - /* FALL THROUGH */ - default: - ImagingDelete(imOut); - return (Imaging) ImagingError_ValueError("internal error"); + case IMAGING_TYPE_FLOAT32: + ImagingSectionEnter(&cookie); + for (y = 0; y < imIn->ysize; y++) { + FLOAT32 *in = (FLOAT32 *)imIn->image32[y]; + FLOAT32 *out = (FLOAT32 *)imOut->image32[y]; + for (x = 0; x < imIn->xsize; x++) { + out[x] = in[x] * scale + offset; + } + } + ImagingSectionLeave(&cookie); + break; + case IMAGING_TYPE_SPECIAL: + if (strcmp(imIn->mode, "I;16") == 0) { + ImagingSectionEnter(&cookie); + for (y = 0; y < imIn->ysize; y++) { + char *in = (char *)imIn->image[y]; + char *out = (char *)imOut->image[y]; + /* FIXME: add clipping? */ + for (x = 0; x < imIn->xsize; x++) { + UINT16 v; + memcpy(&v, in + x * sizeof(v), sizeof(v)); + v = v * scale + offset; + memcpy(out + x * sizeof(UINT16), &v, sizeof(v)); + } + } + ImagingSectionLeave(&cookie); + break; + } + /* FALL THROUGH */ + default: + ImagingDelete(imOut); + return (Imaging)ImagingError_ValueError("internal error"); } return imOut; diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index df31381629c..dfa6d842d3f 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -43,165 +43,155 @@ typedef struct { } PixelHashData; typedef struct _PixelList { - struct _PixelList *next[3],*prev[3]; + struct _PixelList *next[3], *prev[3]; Pixel p; - unsigned int flag:1; + unsigned int flag : 1; int count; } PixelList; typedef struct _BoxNode { - struct _BoxNode *l,*r; - PixelList *head[3],*tail[3]; + struct _BoxNode *l, *r; + PixelList *head[3], *tail[3]; int axis; int volume; uint32_t pixelCount; } BoxNode; -#define _SQR(x) ((x)*(x)) -#define _DISTSQR(p1,p2) \ - _SQR((int)((p1)->c.r)-(int)((p2)->c.r))+ \ - _SQR((int)((p1)->c.g)-(int)((p2)->c.g))+ \ - _SQR((int)((p1)->c.b)-(int)((p2)->c.b)) +#define _SQR(x) ((x) * (x)) +#define _DISTSQR(p1, p2) \ + _SQR((int)((p1)->c.r) - (int)((p2)->c.r)) + \ + _SQR((int)((p1)->c.g) - (int)((p2)->c.g)) + \ + _SQR((int)((p1)->c.b) - (int)((p2)->c.b)) #define MAX_HASH_ENTRIES 65536 -#define PIXEL_HASH(r,g,b) \ - (((unsigned int)(r) )*463 ^ \ - ((unsigned int)(g)<< 8)*10069 ^ \ - ((unsigned int)(b)<<16)*64997) +#define PIXEL_HASH(r, g, b) \ + (((unsigned int)(r)) * 463 ^ ((unsigned int)(g) << 8) * 10069 ^ \ + ((unsigned int)(b) << 16) * 64997) -#define PIXEL_UNSCALE(p,q,s) \ - ((q)->c.r=(p)->c.r<<(s)), \ - ((q)->c.g=(p)->c.g<<(s)), \ - ((q)->c.b=(p)->c.b<<(s)) +#define PIXEL_UNSCALE(p, q, s) \ + ((q)->c.r = (p)->c.r << (s)), ((q)->c.g = (p)->c.g << (s)), \ + ((q)->c.b = (p)->c.b << (s)) -#define PIXEL_SCALE(p,q,s)\ - ((q)->c.r=(p)->c.r>>(s)), \ - ((q)->c.g=(p)->c.g>>(s)), \ - ((q)->c.b=(p)->c.b>>(s)) +#define PIXEL_SCALE(p, q, s) \ + ((q)->c.r = (p)->c.r >> (s)), ((q)->c.g = (p)->c.g >> (s)), \ + ((q)->c.b = (p)->c.b >> (s)) static uint32_t -unshifted_pixel_hash(const HashTable *h, const Pixel pixel) -{ - return PIXEL_HASH(pixel.c.r, pixel.c.g, pixel.c.b); +unshifted_pixel_hash(const HashTable *h, const Pixel pixel) { + return PIXEL_HASH(pixel.c.r, pixel.c.g, pixel.c.b); } static int -unshifted_pixel_cmp(const HashTable *h, const Pixel pixel1, const Pixel pixel2) -{ - if (pixel1.c.r==pixel2.c.r) { - if (pixel1.c.g==pixel2.c.g) { - if (pixel1.c.b==pixel2.c.b) { +unshifted_pixel_cmp(const HashTable *h, const Pixel pixel1, const Pixel pixel2) { + if (pixel1.c.r == pixel2.c.r) { + if (pixel1.c.g == pixel2.c.g) { + if (pixel1.c.b == pixel2.c.b) { return 0; } else { - return (int)(pixel1.c.b)-(int)(pixel2.c.b); + return (int)(pixel1.c.b) - (int)(pixel2.c.b); } } else { - return (int)(pixel1.c.g)-(int)(pixel2.c.g); + return (int)(pixel1.c.g) - (int)(pixel2.c.g); } } else { - return (int)(pixel1.c.r)-(int)(pixel2.c.r); + return (int)(pixel1.c.r) - (int)(pixel2.c.r); } } static uint32_t -pixel_hash(const HashTable *h,const Pixel pixel) -{ - PixelHashData *d=(PixelHashData *)hashtable_get_user_data(h); - return PIXEL_HASH(pixel.c.r>>d->scale, pixel.c.g>>d->scale, pixel.c.b>>d->scale); +pixel_hash(const HashTable *h, const Pixel pixel) { + PixelHashData *d = (PixelHashData *)hashtable_get_user_data(h); + return PIXEL_HASH( + pixel.c.r >> d->scale, pixel.c.g >> d->scale, pixel.c.b >> d->scale); } static int -pixel_cmp(const HashTable *h,const Pixel pixel1, const Pixel pixel2) -{ - PixelHashData *d=(PixelHashData *)hashtable_get_user_data(h); - uint32_t A,B; - A=PIXEL_HASH(pixel1.c.r>>d->scale, pixel1.c.g>>d->scale, pixel1.c.b>>d->scale); - B=PIXEL_HASH(pixel2.c.r>>d->scale, pixel2.c.g>>d->scale, pixel2.c.b>>d->scale); - return (A==B)?0:((A> d->scale, pixel1.c.g >> d->scale, pixel1.c.b >> d->scale); + B = PIXEL_HASH( + pixel2.c.r >> d->scale, pixel2.c.g >> d->scale, pixel2.c.b >> d->scale); + return (A == B) ? 0 : ((A < B) ? -1 : 1); } static void -exists_count_func(const HashTable *h, const Pixel key, uint32_t *val) -{ - *val+=1; +exists_count_func(const HashTable *h, const Pixel key, uint32_t *val) { + *val += 1; } static void -new_count_func(const HashTable *h, const Pixel key, uint32_t *val) -{ - *val=1; +new_count_func(const HashTable *h, const Pixel key, uint32_t *val) { + *val = 1; } static void -rehash_collide(const HashTable *h, - Pixel *keyp, - uint32_t *valp, - Pixel newkey, - uint32_t newval) -{ +rehash_collide( + const HashTable *h, Pixel *keyp, uint32_t *valp, Pixel newkey, uint32_t newval) { *valp += newval; } /* %% */ static HashTable * -create_pixel_hash(Pixel *pixelData,uint32_t nPixels) -{ - PixelHashData *d; - HashTable *hash; - uint32_t i; +create_pixel_hash(Pixel *pixelData, uint32_t nPixels) { + PixelHashData *d; + HashTable *hash; + uint32_t i; #ifndef NO_OUTPUT - uint32_t timer,timer2,timer3; + uint32_t timer, timer2, timer3; #endif - /* malloc check ok, small constant allocation */ - d=malloc(sizeof(PixelHashData)); - if (!d) return NULL; - hash=hashtable_new(pixel_hash,pixel_cmp); - hashtable_set_user_data(hash,d); - d->scale=0; + /* malloc check ok, small constant allocation */ + d = malloc(sizeof(PixelHashData)); + if (!d) { + return NULL; + } + hash = hashtable_new(pixel_hash, pixel_cmp); + hashtable_set_user_data(hash, d); + d->scale = 0; #ifndef NO_OUTPUT - timer=timer3=clock(); + timer = timer3 = clock(); #endif - for (i=0;iMAX_HASH_ENTRIES) { - d->scale++; + for (i = 0; i < nPixels; i++) { + if (!hashtable_insert_or_update_computed( + hash, pixelData[i], new_count_func, exists_count_func)) { + ; + } + while (hashtable_get_count(hash) > MAX_HASH_ENTRIES) { + d->scale++; #ifndef NO_OUTPUT - printf ("rehashing - new scale: %d\n",(int)d->scale); - timer2=clock(); + printf("rehashing - new scale: %d\n", (int)d->scale); + timer2 = clock(); #endif - hashtable_rehash_compute(hash,rehash_collide); + hashtable_rehash_compute(hash, rehash_collide); #ifndef NO_OUTPUT - timer2=clock()-timer2; - printf ("rehash took %f sec\n",timer2/(double)CLOCKS_PER_SEC); - timer+=timer2; + timer2 = clock() - timer2; + printf("rehash took %f sec\n", timer2 / (double)CLOCKS_PER_SEC); + timer += timer2; #endif - } - } + } + } #ifndef NO_OUTPUT - printf ("inserts took %f sec\n",(clock()-timer)/(double)CLOCKS_PER_SEC); + printf("inserts took %f sec\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif #ifndef NO_OUTPUT - printf ("total %f sec\n",(clock()-timer3)/(double)CLOCKS_PER_SEC); + printf("total %f sec\n", (clock() - timer3) / (double)CLOCKS_PER_SEC); #endif - return hash; + return hash; } static void -destroy_pixel_hash(HashTable *hash) -{ - PixelHashData *d=(PixelHashData *)hashtable_get_user_data(hash); - if (d) free(d); - hashtable_free(hash); +destroy_pixel_hash(HashTable *hash) { + PixelHashData *d = (PixelHashData *)hashtable_get_user_data(hash); + if (d) { + free(d); + } + hashtable_free(hash); } - /* 1. hash quantized pixels. */ /* 2. create R,G,B lists of sorted quantized pixels. */ /* 3. median cut. */ @@ -211,625 +201,686 @@ destroy_pixel_hash(HashTable *hash) /* 7. map each pixel to nearest average. */ static int -compute_box_volume(BoxNode *b) -{ - unsigned char rl,rh,gl,gh,bl,bh; - if (b->volume>=0) return b->volume; - if (!b->head[0]) { - b->volume=0; - } else { - rh=b->head[0]->p.c.r; - rl=b->tail[0]->p.c.r; - gh=b->head[1]->p.c.g; - gl=b->tail[1]->p.c.g; - bh=b->head[2]->p.c.b; - bl=b->tail[2]->p.c.b; - b->volume=(rh-rl+1)*(gh-gl+1)*(bh-bl+1); - } - return b->volume; +compute_box_volume(BoxNode *b) { + unsigned char rl, rh, gl, gh, bl, bh; + if (b->volume >= 0) { + return b->volume; + } + if (!b->head[0]) { + b->volume = 0; + } else { + rh = b->head[0]->p.c.r; + rl = b->tail[0]->p.c.r; + gh = b->head[1]->p.c.g; + gl = b->tail[1]->p.c.g; + bh = b->head[2]->p.c.b; + bl = b->tail[2]->p.c.b; + b->volume = (rh - rl + 1) * (gh - gl + 1) * (bh - bl + 1); + } + return b->volume; } static void -hash_to_list(const HashTable *h, const Pixel pixel, const uint32_t count, void *u) -{ - PixelHashData *d=(PixelHashData *)hashtable_get_user_data(h); - PixelList **pl=(PixelList **)u; - PixelList *p; - int i; - Pixel q; - - PIXEL_SCALE(&pixel,&q,d->scale); - - /* malloc check ok, small constant allocation */ - p=malloc(sizeof(PixelList)); - if (!p) return; - - p->flag=0; - p->p=q; - p->count=count; - for (i=0;i<3;i++) { - p->next[i]=pl[i]; - p->prev[i]=NULL; - if (pl[i]) pl[i]->prev[i]=p; - pl[i]=p; - } +hash_to_list(const HashTable *h, const Pixel pixel, const uint32_t count, void *u) { + PixelHashData *d = (PixelHashData *)hashtable_get_user_data(h); + PixelList **pl = (PixelList **)u; + PixelList *p; + int i; + Pixel q; + + PIXEL_SCALE(&pixel, &q, d->scale); + + /* malloc check ok, small constant allocation */ + p = malloc(sizeof(PixelList)); + if (!p) { + return; + } + + p->flag = 0; + p->p = q; + p->count = count; + for (i = 0; i < 3; i++) { + p->next[i] = pl[i]; + p->prev[i] = NULL; + if (pl[i]) { + pl[i]->prev[i] = p; + } + pl[i] = p; + } } static PixelList * -mergesort_pixels(PixelList *head, int i) -{ - PixelList *c,*t,*a,*b,*p; - if (!head||!head->next[i]) { - if (head) { - head->next[i]=NULL; - head->prev[i]=NULL; - } - return head; - } - for (c=t=head;c&&t;c=c->next[i],t=(t->next[i])?t->next[i]->next[i]:NULL); - if (c) { - if (c->prev[i]) c->prev[i]->next[i]=NULL; - c->prev[i]=NULL; - } - a=mergesort_pixels(head,i); - b=mergesort_pixels(c,i); - head=NULL; - p=NULL; - while (a&&b) { - if (a->p.a.v[i]>b->p.a.v[i]) { - c=a; - a=a->next[i]; - } else { - c=b; - b=b->next[i]; - } - c->prev[i]=p; - c->next[i]=NULL; - if (p) p->next[i]=c; - p=c; - if (!head) head=c; - } - if (a) { - c->next[i]=a; - a->prev[i]=c; - } else if (b) { - c->next[i]=b; - b->prev[i]=c; - } - return head; +mergesort_pixels(PixelList *head, int i) { + PixelList *c, *t, *a, *b, *p; + if (!head || !head->next[i]) { + if (head) { + head->next[i] = NULL; + head->prev[i] = NULL; + } + return head; + } + for (c = t = head; c && t; + c = c->next[i], t = (t->next[i]) ? t->next[i]->next[i] : NULL) + ; + if (c) { + if (c->prev[i]) { + c->prev[i]->next[i] = NULL; + } + c->prev[i] = NULL; + } + a = mergesort_pixels(head, i); + b = mergesort_pixels(c, i); + head = NULL; + p = NULL; + while (a && b) { + if (a->p.a.v[i] > b->p.a.v[i]) { + c = a; + a = a->next[i]; + } else { + c = b; + b = b->next[i]; + } + c->prev[i] = p; + c->next[i] = NULL; + if (p) { + p->next[i] = c; + } + p = c; + if (!head) { + head = c; + } + } + if (a) { + c->next[i] = a; + a->prev[i] = c; + } else if (b) { + c->next[i] = b; + b->prev[i] = c; + } + return head; } #if defined(TEST_MERGESORT) || defined(TEST_SORTED) static int -test_sorted(PixelList *pl[3]) -{ - int i,n,l; - PixelList *t; - - for(i=0;i<3;i++) { - n=0; - l=256; - for (t=pl[i];t;t=t->next[i]) { - if (lp.a.v[i]) return 0; - l=t->p.a.v[i]; - } - } - return 1; +test_sorted(PixelList *pl[3]) { + int i, n, l; + PixelList *t; + + for (i = 0; i < 3; i++) { + n = 0; + l = 256; + for (t = pl[i]; t; t = t->next[i]) { + if (l < t->p.a.v[i]) + return 0; + l = t->p.a.v[i]; + } + } + return 1; } #endif static int -box_heap_cmp(const Heap *h, const void *A, const void *B) -{ - BoxNode *a=(BoxNode *)A; - BoxNode *b=(BoxNode *)B; - return (int)a->pixelCount-(int)b->pixelCount; +box_heap_cmp(const Heap *h, const void *A, const void *B) { + BoxNode *a = (BoxNode *)A; + BoxNode *b = (BoxNode *)B; + return (int)a->pixelCount - (int)b->pixelCount; } -#define LUMINANCE(p) (77*(p)->c.r+150*(p)->c.g+29*(p)->c.b) +#define LUMINANCE(p) (77 * (p)->c.r + 150 * (p)->c.g + 29 * (p)->c.b) static int -splitlists(PixelList *h[3], - PixelList *t[3], - PixelList *nh[2][3], - PixelList *nt[2][3], - uint32_t nCount[2], - int axis, - uint32_t pixelCount) -{ - uint32_t left; - - PixelList *l,*r,*c,*n; - int i; - int nRight,nLeft; - int splitColourVal; +splitlists( + PixelList *h[3], + PixelList *t[3], + PixelList *nh[2][3], + PixelList *nt[2][3], + uint32_t nCount[2], + int axis, + uint32_t pixelCount) { + uint32_t left; + + PixelList *l, *r, *c, *n; + int i; + int nRight, nLeft; + int splitColourVal; #ifdef TEST_SPLIT - { - PixelList *_prevTest,*_nextTest; - int _i,_nextCount[3],_prevCount[3]; - for (_i=0;_i<3;_i++) { - for (_nextCount[_i]=0,_nextTest=h[_i];_nextTest&&_nextTest->next[_i];_nextTest=_nextTest->next[_i],_nextCount[_i]++); - for (_prevCount[_i]=0,_prevTest=t[_i];_prevTest&&_prevTest->prev[_i];_prevTest=_prevTest->prev[_i],_prevCount[_i]++); - if (_nextTest!=t[_i]) { - printf ("next-list of axis %d does not end at tail\n",_i); - exit(1); - } - if (_prevTest!=h[_i]) { - printf ("prev-list of axis %d does not end at head\n",_i); - exit(1); - } - for (;_nextTest&&_nextTest->prev[_i];_nextTest=_nextTest->prev[_i]); - for (;_prevTest&&_prevTest->next[_i];_prevTest=_prevTest->next[_i]); - if (_nextTest!=h[_i]) { - printf ("next-list of axis %d does not loop back to head\n",_i); - exit(1); - } - if (_prevTest!=t[_i]) { - printf ("prev-list of axis %d does not loop back to tail\n",_i); - exit(1); - } - } - for (_i=1;_i<3;_i++) { - if (_prevCount[_i]!=_prevCount[_i-1] || - _nextCount[_i]!=_nextCount[_i-1] || - _prevCount[_i]!=_nextCount[_i]) { - printf ("{%d %d %d} {%d %d %d}\n", + { + PixelList *_prevTest, *_nextTest; + int _i, _nextCount[3], _prevCount[3]; + for (_i = 0; _i < 3; _i++) { + for (_nextCount[_i] = 0, _nextTest = h[_i]; + _nextTest && _nextTest->next[_i]; + _nextTest = _nextTest->next[_i], _nextCount[_i]++) + ; + for (_prevCount[_i] = 0, _prevTest = t[_i]; + _prevTest && _prevTest->prev[_i]; + _prevTest = _prevTest->prev[_i], _prevCount[_i]++) + ; + if (_nextTest != t[_i]) { + printf("next-list of axis %d does not end at tail\n", _i); + exit(1); + } + if (_prevTest != h[_i]) { + printf("prev-list of axis %d does not end at head\n", _i); + exit(1); + } + for (; _nextTest && _nextTest->prev[_i]; _nextTest = _nextTest->prev[_i]) + ; + for (; _prevTest && _prevTest->next[_i]; _prevTest = _prevTest->next[_i]) + ; + if (_nextTest != h[_i]) { + printf("next-list of axis %d does not loop back to head\n", _i); + exit(1); + } + if (_prevTest != t[_i]) { + printf("prev-list of axis %d does not loop back to tail\n", _i); + exit(1); + } + } + for (_i = 1; _i < 3; _i++) { + if (_prevCount[_i] != _prevCount[_i - 1] || + _nextCount[_i] != _nextCount[_i - 1] || + _prevCount[_i] != _nextCount[_i]) { + printf( + "{%d %d %d} {%d %d %d}\n", _prevCount[0], _prevCount[1], _prevCount[2], _nextCount[0], _nextCount[1], _nextCount[2]); - exit(1); - } - } + exit(1); + } + } } #endif - nCount[0]=nCount[1]=0; - nLeft=nRight=0; - for (left=0,c=h[axis];c;) { - left=left+c->count; - nCount[0]+=c->count; - c->flag=0; - nLeft++; - c=c->next[axis]; - if (left*2>pixelCount) { - break; - } - } - if (c) { - splitColourVal=c->prev[axis]->p.a.v[axis]; - for (;c;c=c->next[axis]) { - if (splitColourVal!=c->p.a.v[axis]) { + nCount[0] = nCount[1] = 0; + nLeft = nRight = 0; + for (left = 0, c = h[axis]; c;) { + left = left + c->count; + nCount[0] += c->count; + c->flag = 0; + nLeft++; + c = c->next[axis]; + if (left * 2 > pixelCount) { break; - } - c->flag=0; - nLeft++; - nCount[0]+=c->count; - } - } - for (;c;c=c->next[axis]) { - c->flag=1; - nRight++; - nCount[1]+=c->count; - } - if (!nRight) { - for (c=t[axis],splitColourVal=t[axis]->p.a.v[axis];c;c=c->prev[axis]) { - if (splitColourVal!=c->p.a.v[axis]) { - break; - } - c->flag=1; - nRight++; - nLeft--; - nCount[0]-=c->count; - nCount[1]+=c->count; - } - } + } + } + if (c) { + splitColourVal = c->prev[axis]->p.a.v[axis]; + for (; c; c = c->next[axis]) { + if (splitColourVal != c->p.a.v[axis]) { + break; + } + c->flag = 0; + nLeft++; + nCount[0] += c->count; + } + } + for (; c; c = c->next[axis]) { + c->flag = 1; + nRight++; + nCount[1] += c->count; + } + if (!nRight) { + for (c = t[axis], splitColourVal = t[axis]->p.a.v[axis]; c; c = c->prev[axis]) { + if (splitColourVal != c->p.a.v[axis]) { + break; + } + c->flag = 1; + nRight++; + nLeft--; + nCount[0] -= c->count; + nCount[1] += c->count; + } + } #ifndef NO_OUTPUT - if (!nLeft) { - for (c=h[axis];c;c=c->next[axis]) { - printf ("[%d %d %d]\n",c->p.c.r,c->p.c.g,c->p.c.b); - } - printf ("warning... trivial split\n"); - } + if (!nLeft) { + for (c = h[axis]; c; c = c->next[axis]) { + printf("[%d %d %d]\n", c->p.c.r, c->p.c.g, c->p.c.b); + } + printf("warning... trivial split\n"); + } #endif - for (i=0;i<3;i++) { - l=r=NULL; - nh[0][i]=nt[0][i]=NULL; - nh[1][i]=nt[1][i]=NULL; - for (c=h[i];c;c=n) { - n=c->next[i]; - if (c->flag) { /* move pixel to right list*/ - if (r) r->next[i]=c; else nh[1][i]=c; - c->prev[i]=r; - r=c; - } else { /* move pixel to left list */ - if (l) l->next[i]=c; else nh[0][i]=c; - c->prev[i]=l; - l=c; - } - } - if (l) l->next[i]=NULL; - if (r) r->next[i]=NULL; - nt[0][i]=l; - nt[1][i]=r; - } - return 1; + for (i = 0; i < 3; i++) { + l = r = NULL; + nh[0][i] = nt[0][i] = NULL; + nh[1][i] = nt[1][i] = NULL; + for (c = h[i]; c; c = n) { + n = c->next[i]; + if (c->flag) { /* move pixel to right list*/ + if (r) { + r->next[i] = c; + } else { + nh[1][i] = c; + } + c->prev[i] = r; + r = c; + } else { /* move pixel to left list */ + if (l) { + l->next[i] = c; + } else { + nh[0][i] = c; + } + c->prev[i] = l; + l = c; + } + } + if (l) { + l->next[i] = NULL; + } + if (r) { + r->next[i] = NULL; + } + nt[0][i] = l; + nt[1][i] = r; + } + return 1; } static int -split(BoxNode *node) -{ - unsigned char rl,rh,gl,gh,bl,bh; - int f[3]; - int best,axis; - int i; - PixelList *heads[2][3]; - PixelList *tails[2][3]; - uint32_t newCounts[2]; - BoxNode *left,*right; - - rh=node->head[0]->p.c.r; - rl=node->tail[0]->p.c.r; - gh=node->head[1]->p.c.g; - gl=node->tail[1]->p.c.g; - bh=node->head[2]->p.c.b; - bl=node->tail[2]->p.c.b; +split(BoxNode *node) { + unsigned char rl, rh, gl, gh, bl, bh; + int f[3]; + int best, axis; + int i; + PixelList *heads[2][3]; + PixelList *tails[2][3]; + uint32_t newCounts[2]; + BoxNode *left, *right; + + rh = node->head[0]->p.c.r; + rl = node->tail[0]->p.c.r; + gh = node->head[1]->p.c.g; + gl = node->tail[1]->p.c.g; + bh = node->head[2]->p.c.b; + bl = node->tail[2]->p.c.b; #ifdef TEST_SPLIT - printf ("splitting node [%d %d %d] [%d %d %d] ",rl,gl,bl,rh,gh,bh); + printf("splitting node [%d %d %d] [%d %d %d] ", rl, gl, bl, rh, gh, bh); #endif - f[0]=(rh-rl)*77; - f[1]=(gh-gl)*150; - f[2]=(bh-bl)*29; - - best=f[0]; - axis=0; - for (i=1;i<3;i++) { - if (besttail[_i]->next[_i]) { - printf ("tail is not tail\n"); - printf ("node->tail[%d]->next[%d]=%p\n",_i,_i,node->tail[_i]->next[_i]); - } - if (node->head[_i]->prev[_i]) { - printf ("head is not head\n"); - printf ("node->head[%d]->prev[%d]=%p\n",_i,_i,node->head[_i]->prev[_i]); - } - } - - for (_i=0;_i<3;_i++) { - for (_nextCount[_i]=0,_nextTest=node->head[_i];_nextTest&&_nextTest->next[_i];_nextTest=_nextTest->next[_i],_nextCount[_i]++); - for (_prevCount[_i]=0,_prevTest=node->tail[_i];_prevTest&&_prevTest->prev[_i];_prevTest=_prevTest->prev[_i],_prevCount[_i]++); - if (_nextTest!=node->tail[_i]) { - printf ("next-list of axis %d does not end at tail\n",_i); - } - if (_prevTest!=node->head[_i]) { - printf ("prev-list of axis %d does not end at head\n",_i); - } - for (;_nextTest&&_nextTest->prev[_i];_nextTest=_nextTest->prev[_i]); - for (;_prevTest&&_prevTest->next[_i];_prevTest=_prevTest->next[_i]); - if (_nextTest!=node->head[_i]) { - printf ("next-list of axis %d does not loop back to head\n",_i); - } - if (_prevTest!=node->tail[_i]) { - printf ("prev-list of axis %d does not loop back to tail\n",_i); - } - } - for (_i=1;_i<3;_i++) { - if (_prevCount[_i]!=_prevCount[_i-1] || - _nextCount[_i]!=_nextCount[_i-1] || - _prevCount[_i]!=_nextCount[_i]) { - printf ("{%d %d %d} {%d %d %d}\n", + { + PixelList *_prevTest, *_nextTest; + int _i, _nextCount[3], _prevCount[3]; + for (_i = 0; _i < 3; _i++) { + if (node->tail[_i]->next[_i]) { + printf("tail is not tail\n"); + printf( + "node->tail[%d]->next[%d]=%p\n", _i, _i, node->tail[_i]->next[_i]); + } + if (node->head[_i]->prev[_i]) { + printf("head is not head\n"); + printf( + "node->head[%d]->prev[%d]=%p\n", _i, _i, node->head[_i]->prev[_i]); + } + } + + for (_i = 0; _i < 3; _i++) { + for (_nextCount[_i] = 0, _nextTest = node->head[_i]; + _nextTest && _nextTest->next[_i]; + _nextTest = _nextTest->next[_i], _nextCount[_i]++) + ; + for (_prevCount[_i] = 0, _prevTest = node->tail[_i]; + _prevTest && _prevTest->prev[_i]; + _prevTest = _prevTest->prev[_i], _prevCount[_i]++) + ; + if (_nextTest != node->tail[_i]) { + printf("next-list of axis %d does not end at tail\n", _i); + } + if (_prevTest != node->head[_i]) { + printf("prev-list of axis %d does not end at head\n", _i); + } + for (; _nextTest && _nextTest->prev[_i]; _nextTest = _nextTest->prev[_i]) + ; + for (; _prevTest && _prevTest->next[_i]; _prevTest = _prevTest->next[_i]) + ; + if (_nextTest != node->head[_i]) { + printf("next-list of axis %d does not loop back to head\n", _i); + } + if (_prevTest != node->tail[_i]) { + printf("prev-list of axis %d does not loop back to tail\n", _i); + } + } + for (_i = 1; _i < 3; _i++) { + if (_prevCount[_i] != _prevCount[_i - 1] || + _nextCount[_i] != _nextCount[_i - 1] || + _prevCount[_i] != _nextCount[_i]) { + printf( + "{%d %d %d} {%d %d %d}\n", _prevCount[0], _prevCount[1], _prevCount[2], _nextCount[0], _nextCount[1], _nextCount[2]); - } - } + } + } } #endif - node->axis=axis; - if (!splitlists(node->head, - node->tail, - heads, - tails, - newCounts, - axis, - node->pixelCount)) { + node->axis = axis; + if (!splitlists( + node->head, node->tail, heads, tails, newCounts, axis, node->pixelCount)) { #ifndef NO_OUTPUT - printf ("list split failed.\n"); + printf("list split failed.\n"); #endif - return 0; - } + return 0; + } #ifdef TEST_SPLIT - if (!test_sorted(heads[0])) { - printf ("bug in split"); - exit(1); - } - if (!test_sorted(heads[1])) { - printf ("bug in split"); - exit(1); - } + if (!test_sorted(heads[0])) { + printf("bug in split"); + exit(1); + } + if (!test_sorted(heads[1])) { + printf("bug in split"); + exit(1); + } #endif - /* malloc check ok, small constant allocation */ - left=malloc(sizeof(BoxNode)); - right=malloc(sizeof(BoxNode)); - if (!left||!right) { - return 0; - } - for(i=0;i<3;i++) { - left->head[i]=heads[0][i]; - left->tail[i]=tails[0][i]; - right->head[i]=heads[1][i]; - right->tail[i]=tails[1][i]; - node->head[i]=NULL; - node->tail[i]=NULL; - } + /* malloc check ok, small constant allocation */ + left = malloc(sizeof(BoxNode)); + right = malloc(sizeof(BoxNode)); + if (!left || !right) { + free(left); + free(right); + return 0; + } + for (i = 0; i < 3; i++) { + left->head[i] = heads[0][i]; + left->tail[i] = tails[0][i]; + right->head[i] = heads[1][i]; + right->tail[i] = tails[1][i]; + node->head[i] = NULL; + node->tail[i] = NULL; + } #ifdef TEST_SPLIT - if (left->head[0]) { - rh=left->head[0]->p.c.r; - rl=left->tail[0]->p.c.r; - gh=left->head[1]->p.c.g; - gl=left->tail[1]->p.c.g; - bh=left->head[2]->p.c.b; - bl=left->tail[2]->p.c.b; - printf (" left node [%3d %3d %3d] [%3d %3d %3d]\n",rl,gl,bl,rh,gh,bh); - } - if (right->head[0]) { - rh=right->head[0]->p.c.r; - rl=right->tail[0]->p.c.r; - gh=right->head[1]->p.c.g; - gl=right->tail[1]->p.c.g; - bh=right->head[2]->p.c.b; - bl=right->tail[2]->p.c.b; - printf (" right node [%3d %3d %3d] [%3d %3d %3d]\n",rl,gl,bl,rh,gh,bh); - } + if (left->head[0]) { + rh = left->head[0]->p.c.r; + rl = left->tail[0]->p.c.r; + gh = left->head[1]->p.c.g; + gl = left->tail[1]->p.c.g; + bh = left->head[2]->p.c.b; + bl = left->tail[2]->p.c.b; + printf(" left node [%3d %3d %3d] [%3d %3d %3d]\n", rl, gl, bl, rh, gh, bh); + } + if (right->head[0]) { + rh = right->head[0]->p.c.r; + rl = right->tail[0]->p.c.r; + gh = right->head[1]->p.c.g; + gl = right->tail[1]->p.c.g; + bh = right->head[2]->p.c.b; + bl = right->tail[2]->p.c.b; + printf(" right node [%3d %3d %3d] [%3d %3d %3d]\n", rl, gl, bl, rh, gh, bh); + } #endif - left->l=left->r=NULL; - right->l=right->r=NULL; - left->axis=right->axis=-1; - left->volume=right->volume=-1; - left->pixelCount=newCounts[0]; - right->pixelCount=newCounts[1]; - node->l=left; - node->r=right; - return 1; + left->l = left->r = NULL; + right->l = right->r = NULL; + left->axis = right->axis = -1; + left->volume = right->volume = -1; + left->pixelCount = newCounts[0]; + right->pixelCount = newCounts[1]; + node->l = left; + node->r = right; + return 1; } static BoxNode * -median_cut(PixelList *hl[3], - uint32_t imPixelCount, - int nPixels) -{ - PixelList *tl[3]; - int i; - BoxNode *root; - Heap* h; - BoxNode *thisNode; - - h=ImagingQuantHeapNew(box_heap_cmp); - /* malloc check ok, small constant allocation */ - root=malloc(sizeof(BoxNode)); - if (!root) { ImagingQuantHeapFree(h); return NULL; } - for(i=0;i<3;i++) { - for (tl[i]=hl[i];tl[i]&&tl[i]->next[i];tl[i]=tl[i]->next[i]); - root->head[i]=hl[i]; - root->tail[i]=tl[i]; - } - root->l=root->r=NULL; - root->axis=-1; - root->volume=-1; - root->pixelCount=imPixelCount; - - ImagingQuantHeapAdd(h,(void *)root); - while (--nPixels) { - do { - if (!ImagingQuantHeapRemove(h,(void **)&thisNode)) { - goto done; - } - } while (compute_box_volume(thisNode)==1); - if (!split(thisNode)) { +median_cut(PixelList *hl[3], uint32_t imPixelCount, int nPixels) { + PixelList *tl[3]; + int i; + BoxNode *root; + Heap *h; + BoxNode *thisNode; + + h = ImagingQuantHeapNew(box_heap_cmp); + /* malloc check ok, small constant allocation */ + root = malloc(sizeof(BoxNode)); + if (!root) { + ImagingQuantHeapFree(h); + return NULL; + } + for (i = 0; i < 3; i++) { + for (tl[i] = hl[i]; tl[i] && tl[i]->next[i]; tl[i] = tl[i]->next[i]) + ; + root->head[i] = hl[i]; + root->tail[i] = tl[i]; + } + root->l = root->r = NULL; + root->axis = -1; + root->volume = -1; + root->pixelCount = imPixelCount; + + ImagingQuantHeapAdd(h, (void *)root); + while (--nPixels) { + do { + if (!ImagingQuantHeapRemove(h, (void **)&thisNode)) { + goto done; + } + } while (compute_box_volume(thisNode) == 1); + if (!split(thisNode)) { #ifndef NO_OUTPUT - printf ("Oops, split failed...\n"); + printf("Oops, split failed...\n"); #endif - exit (1); - } - ImagingQuantHeapAdd(h,(void *)(thisNode->l)); - ImagingQuantHeapAdd(h,(void *)(thisNode->r)); - } + exit(1); + } + ImagingQuantHeapAdd(h, (void *)(thisNode->l)); + ImagingQuantHeapAdd(h, (void *)(thisNode->r)); + } done: - ImagingQuantHeapFree(h); - return root; + ImagingQuantHeapFree(h); + return root; } static void -free_box_tree(BoxNode *n) -{ - PixelList *p,*pp; - if (n->l) free_box_tree(n->l); - if (n->r) free_box_tree(n->r); - for (p=n->head[0];p;p=pp) { - pp=p->next[0]; - free(p); - } - free(n); +free_box_tree(BoxNode *n) { + PixelList *p, *pp; + if (n->l) { + free_box_tree(n->l); + } + if (n->r) { + free_box_tree(n->r); + } + for (p = n->head[0]; p; p = pp) { + pp = p->next[0]; + free(p); + } + free(n); } #ifdef TEST_SPLIT_INTEGRITY static int -checkContained(BoxNode *n,Pixel *pp) -{ - if (n->l&&n->r) { - return checkContained(n->l,pp)+checkContained(n->r,pp); - } - if (n->l||n->r) { +checkContained(BoxNode *n, Pixel *pp) { + if (n->l && n->r) { + return checkContained(n->l, pp) + checkContained(n->r, pp); + } + if (n->l || n->r) { #ifndef NO_OUTPUT - printf ("box tree is dead\n"); + printf("box tree is dead\n"); #endif - return 0; - } - if ( - pp->c.r<=n->head[0]->p.c.r && - pp->c.r>=n->tail[0]->p.c.r && - pp->c.g<=n->head[1]->p.c.g && - pp->c.g>=n->tail[1]->p.c.g && - pp->c.b<=n->head[2]->p.c.b && - pp->c.b>=n->tail[2]->p.c.b) { - return 1; - } - return 0; + return 0; + } + if (pp->c.r <= n->head[0]->p.c.r && pp->c.r >= n->tail[0]->p.c.r && + pp->c.g <= n->head[1]->p.c.g && pp->c.g >= n->tail[1]->p.c.g && + pp->c.b <= n->head[2]->p.c.b && pp->c.b >= n->tail[2]->p.c.b) { + return 1; + } + return 0; } #endif static int -annotate_hash_table(BoxNode *n,HashTable *h,uint32_t *box) -{ - PixelList *p; - PixelHashData *d=(PixelHashData *)hashtable_get_user_data(h); - Pixel q; - if (n->l&&n->r) { - return annotate_hash_table(n->l,h,box) && annotate_hash_table(n->r,h,box); - } - if (n->l||n->r) { +annotate_hash_table(BoxNode *n, HashTable *h, uint32_t *box) { + PixelList *p; + PixelHashData *d = (PixelHashData *)hashtable_get_user_data(h); + Pixel q; + if (n->l && n->r) { + return annotate_hash_table(n->l, h, box) && annotate_hash_table(n->r, h, box); + } + if (n->l || n->r) { #ifndef NO_OUTPUT - printf ("box tree is dead\n"); + printf("box tree is dead\n"); #endif - return 0; - } - for (p=n->head[0];p;p=p->next[0]) { - PIXEL_UNSCALE(&(p->p),&q,d->scale); - if (!hashtable_insert(h,q,*box)) { + return 0; + } + for (p = n->head[0]; p; p = p->next[0]) { + PIXEL_UNSCALE(&(p->p), &q, d->scale); + if (!hashtable_insert(h, q, *box)) { #ifndef NO_OUTPUT - printf ("hashtable insert failed\n"); + printf("hashtable insert failed\n"); #endif - return 0; - } - } - if (n->head[0]) (*box)++; - return 1; + return 0; + } + } + if (n->head[0]) { + (*box)++; + } + return 1; } +typedef struct { + uint32_t *distance; + uint32_t index; +} DistanceWithIndex; + static int -_sort_ulong_ptr_keys(const void *a, const void *b) -{ - uint32_t A=**(uint32_t **)a; - uint32_t B=**(uint32_t **)b; - return (A==B)?0:((Adistance == *B->distance) { + return A->index < B->index ? -1 : +1; + } + return *A->distance < *B->distance ? -1 : +1; } static int -resort_distance_tables(uint32_t *avgDist, - uint32_t **avgDistSortKey, - Pixel *p, - uint32_t nEntries) -{ - uint32_t i,j,k; - uint32_t **skRow; - uint32_t *skElt; - - for (i=0;i*(skRow[k]));k--) { - skRow[k]=skRow[k-1]; - } - if (k!=j) skRow[k]=skElt; - } - } - return 1; +resort_distance_tables( + uint32_t *avgDist, uint32_t **avgDistSortKey, Pixel *p, uint32_t nEntries) { + uint32_t i, j, k; + uint32_t **skRow; + uint32_t *skElt; + + for (i = 0; i < nEntries; i++) { + avgDist[i * nEntries + i] = 0; + for (j = 0; j < i; j++) { + avgDist[j * nEntries + i] = avgDist[i * nEntries + j] = + _DISTSQR(p + i, p + j); + } + } + for (i = 0; i < nEntries; i++) { + skRow = avgDistSortKey + i * nEntries; + for (j = 1; j < nEntries; j++) { + skElt = skRow[j]; + for (k = j; k && (*(skRow[k - 1]) > *(skRow[k])); k--) { + skRow[k] = skRow[k - 1]; + } + if (k != j) { + skRow[k] = skElt; + } + } + } + return 1; } static int -build_distance_tables(uint32_t *avgDist, - uint32_t **avgDistSortKey, - Pixel *p, - uint32_t nEntries) -{ - uint32_t i,j; - - for (i=0;i1) { - printf ("pixel in two boxes\n"); - for(i=0;i<3;i++) free (avg[i]); - free(count); - return 0; - } + if (!(i % 100)) { + printf("%05d\r", i); + fflush(stdout); + } + if (checkContained(root, pixelData + i) > 1) { + printf("pixel in two boxes\n"); + for (i = 0; i < 3; i++) { + free(avg[i]); + } + free(count); + return 0; + } #endif - if (!hashtable_lookup(medianBoxHash,pixelData[i],&paletteEntry)) { + if (!hashtable_lookup(medianBoxHash, pixelData[i], &paletteEntry)) { #ifndef NO_OUTPUT - printf ("pixel lookup failed\n"); + printf("pixel lookup failed\n"); #endif - for(i=0;i<3;i++) free (avg[i]); - free(count); - return 0; - } - if (paletteEntry>=nPaletteEntries) { + for (i = 0; i < 3; i++) { + free(avg[i]); + } + free(count); + return 0; + } + if (paletteEntry >= nPaletteEntries) { #ifndef NO_OUTPUT - printf ("panic - paletteEntry>=nPaletteEntries (%d>=%d)\n",(int)paletteEntry,(int)nPaletteEntries); + printf( + "panic - paletteEntry>=nPaletteEntries (%d>=%d)\n", + (int)paletteEntry, + (int)nPaletteEntries); #endif - for(i=0;i<3;i++) free (avg[i]); - free(count); - return 0; - } - avg[0][paletteEntry]+=pixelData[i].c.r; - avg[1][paletteEntry]+=pixelData[i].c.g; - avg[2][paletteEntry]+=pixelData[i].c.b; - count[paletteEntry]++; - } - /* malloc check ok, using calloc */ - p=calloc(nPaletteEntries, sizeof(Pixel)); - if (!p) { - for(i=0;i<3;i++) free (avg[i]); - free(count); - return 0; - } - for (i=0;i=nPaletteEntries) { + uint32_t *qp) { + uint32_t i; + + memset(count, 0, sizeof(uint32_t) * nPaletteEntries); + for (i = 0; i < 3; i++) { + memset(avg[i], 0, sizeof(uint32_t) * nPaletteEntries); + } + for (i = 0; i < nPixels; i++) { + if (qp[i] >= nPaletteEntries) { #ifndef NO_OUTPUT - printf ("scream\n"); + printf("scream\n"); #endif - return 0; - } - avg[0][qp[i]]+=pixelData[i].c.r; - avg[1][qp[i]]+=pixelData[i].c.g; - avg[2][qp[i]]+=pixelData[i].c.b; - count[qp[i]]++; - } - for (i=0;i UINT32_MAX / (sizeof(uint32_t))) { - return 0; - } - /* malloc check ok, using calloc */ - if (!(count=calloc(nPaletteEntries, sizeof(uint32_t)))) { - return 0; - } - for(i=0;i<3;i++) { - avg[i]=NULL; - } - for(i=0;i<3;i++) { - /* malloc check ok, using calloc */ - if (!(avg[i]=calloc(nPaletteEntries, sizeof(uint32_t)))) { - goto error_1; - } - } - - /* this is enough of a check, since the multiplication n*size is done above */ - if (nPaletteEntries > UINT32_MAX / nPaletteEntries) { - goto error_1; - } - /* malloc check ok, using calloc, checking n*n above */ - avgDist=calloc(nPaletteEntries*nPaletteEntries, sizeof(uint32_t)); - if (!avgDist) { goto error_1; } - - /* malloc check ok, using calloc, checking n*n above */ - avgDistSortKey=calloc(nPaletteEntries*nPaletteEntries, sizeof(uint32_t *)); - if (!avgDistSortKey) { goto error_2; } +k_means( + Pixel *pixelData, + uint32_t nPixels, + Pixel *paletteData, + uint32_t nPaletteEntries, + uint32_t *qp, + int threshold) { + uint32_t *avg[3]; + uint32_t *count; + uint32_t i; + uint32_t *avgDist; + uint32_t **avgDistSortKey; + int changes; + int built = 0; + + if (nPaletteEntries > UINT32_MAX / (sizeof(uint32_t))) { + return 0; + } + /* malloc check ok, using calloc */ + if (!(count = calloc(nPaletteEntries, sizeof(uint32_t)))) { + return 0; + } + for (i = 0; i < 3; i++) { + avg[i] = NULL; + } + for (i = 0; i < 3; i++) { + /* malloc check ok, using calloc */ + if (!(avg[i] = calloc(nPaletteEntries, sizeof(uint32_t)))) { + goto error_1; + } + } + + /* this is enough of a check, since the multiplication n*size is done above */ + if (nPaletteEntries > UINT32_MAX / nPaletteEntries) { + goto error_1; + } + /* malloc check ok, using calloc, checking n*n above */ + avgDist = calloc(nPaletteEntries * nPaletteEntries, sizeof(uint32_t)); + if (!avgDist) { + goto error_1; + } + + /* malloc check ok, using calloc, checking n*n above */ + avgDistSortKey = calloc(nPaletteEntries * nPaletteEntries, sizeof(uint32_t *)); + if (!avgDistSortKey) { + goto error_2; + } #ifndef NO_OUTPUT - printf("[");fflush(stdout); + printf("["); + fflush(stdout); #endif - while (1) { - if (!built) { - compute_palette_from_quantized_pixels(pixelData,nPixels,paletteData,nPaletteEntries,avg,count,qp); - build_distance_tables(avgDist,avgDistSortKey,paletteData,nPaletteEntries); - built=1; - } else { - recompute_palette_from_averages(paletteData,nPaletteEntries,avg,count); - resort_distance_tables(avgDist,avgDistSortKey,paletteData,nPaletteEntries); - } - changes=map_image_pixels_from_quantized_pixels(pixelData, - nPixels, - paletteData, - nPaletteEntries, - avgDist, - avgDistSortKey, - qp, - avg, - count); - if (changes<0) { - goto error_3; - } + while (1) { + if (!built) { + compute_palette_from_quantized_pixels( + pixelData, nPixels, paletteData, nPaletteEntries, avg, count, qp); + if (!build_distance_tables( + avgDist, avgDistSortKey, paletteData, nPaletteEntries)) { + goto error_3; + } + built = 1; + } else { + recompute_palette_from_averages(paletteData, nPaletteEntries, avg, count); + resort_distance_tables( + avgDist, avgDistSortKey, paletteData, nPaletteEntries); + } + changes = map_image_pixels_from_quantized_pixels( + pixelData, + nPixels, + paletteData, + nPaletteEntries, + avgDist, + avgDistSortKey, + qp, + avg, + count); + if (changes < 0) { + goto error_3; + } #ifndef NO_OUTPUT - printf (".(%d)",changes);fflush(stdout); + printf(".(%d)", changes); + fflush(stdout); #endif - if (changes<=threshold) break; - } + if (changes <= threshold) { + break; + } + } #ifndef NO_OUTPUT - printf("]\n"); + printf("]\n"); #endif - if (avgDistSortKey) free(avgDistSortKey); - if (avgDist) free(avgDist); - for(i=0;i<3;i++) if (avg[i]) free (avg[i]); - if (count) free(count); - return 1; + if (avgDistSortKey) { + free(avgDistSortKey); + } + if (avgDist) { + free(avgDist); + } + for (i = 0; i < 3; i++) { + if (avg[i]) { + free(avg[i]); + } + } + if (count) { + free(count); + } + return 1; error_3: - if (avgDistSortKey) free(avgDistSortKey); + if (avgDistSortKey) { + free(avgDistSortKey); + } error_2: - if (avgDist) free(avgDist); + if (avgDist) { + free(avgDist); + } error_1: - for(i=0;i<3;i++) if (avg[i]) free (avg[i]); - if (count) free(count); - return 0; + for (i = 0; i < 3; i++) { + if (avg[i]) { + free(avg[i]); + } + } + if (count) { + free(count); + } + return 0; } -int -quantize(Pixel *pixelData, - uint32_t nPixels, - uint32_t nQuantPixels, - Pixel **palette, - uint32_t *paletteLength, - uint32_t **quantizedPixels, - int kmeans) -{ - PixelList *hl[3]; - HashTable *h; - BoxNode *root; - uint32_t i; - uint32_t *qp; - uint32_t nPaletteEntries; - - uint32_t *avgDist; - uint32_t **avgDistSortKey; - Pixel *p; +static int +quantize( + Pixel *pixelData, + uint32_t nPixels, + uint32_t nQuantPixels, + Pixel **palette, + uint32_t *paletteLength, + uint32_t **quantizedPixels, + int kmeans) { + PixelList *hl[3]; + HashTable *h; + BoxNode *root; + uint32_t i; + uint32_t *qp; + uint32_t nPaletteEntries; + + uint32_t *avgDist; + uint32_t **avgDistSortKey; + Pixel *p; #ifndef NO_OUTPUT - uint32_t timer,timer2; + uint32_t timer, timer2; #endif #ifndef NO_OUTPUT - timer2=clock(); - printf ("create hash table..."); fflush(stdout); timer=clock(); + timer2 = clock(); + printf("create hash table..."); + fflush(stdout); + timer = clock(); #endif - h=create_pixel_hash(pixelData,nPixels); + h = create_pixel_hash(pixelData, nPixels); #ifndef NO_OUTPUT - printf ("done (%f)\n",(clock()-timer)/(double)CLOCKS_PER_SEC); + printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif - if (!h) { - goto error_0; - } + if (!h) { + goto error_0; + } #ifndef NO_OUTPUT - printf ("create lists from hash table..."); fflush(stdout); timer=clock(); + printf("create lists from hash table..."); + fflush(stdout); + timer = clock(); #endif - hl[0]=hl[1]=hl[2]=NULL; - hashtable_foreach(h,hash_to_list,hl); + hl[0] = hl[1] = hl[2] = NULL; + hashtable_foreach(h, hash_to_list, hl); #ifndef NO_OUTPUT - printf ("done (%f)\n",(clock()-timer)/(double)CLOCKS_PER_SEC); + printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif - if (!hl[0]) { - goto error_1; - } + if (!hl[0]) { + goto error_1; + } #ifndef NO_OUTPUT - printf ("mergesort lists..."); fflush(stdout); timer=clock(); + printf("mergesort lists..."); + fflush(stdout); + timer = clock(); #endif - for(i=0;i<3;i++) { - hl[i]=mergesort_pixels(hl[i],i); - } + for (i = 0; i < 3; i++) { + hl[i] = mergesort_pixels(hl[i], i); + } #ifdef TEST_MERGESORT - if (!test_sorted(hl)) { - printf ("bug in mergesort\n"); - goto error_1; - } + if (!test_sorted(hl)) { + printf("bug in mergesort\n"); + goto error_1; + } #endif #ifndef NO_OUTPUT - printf ("done (%f)\n",(clock()-timer)/(double)CLOCKS_PER_SEC); + printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif #ifndef NO_OUTPUT - printf ("median cut..."); fflush(stdout); timer=clock(); + printf("median cut..."); + fflush(stdout); + timer = clock(); #endif - root=median_cut(hl,nPixels,nQuantPixels); + root = median_cut(hl, nPixels, nQuantPixels); #ifndef NO_OUTPUT - printf ("done (%f)\n",(clock()-timer)/(double)CLOCKS_PER_SEC); + printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif - if (!root) { - goto error_1; - } - nPaletteEntries=0; + if (!root) { + goto error_1; + } + nPaletteEntries = 0; #ifndef NO_OUTPUT - printf ("median cut tree to hash table..."); fflush(stdout); timer=clock(); + printf("median cut tree to hash table..."); + fflush(stdout); + timer = clock(); #endif - annotate_hash_table(root,h,&nPaletteEntries); + annotate_hash_table(root, h, &nPaletteEntries); #ifndef NO_OUTPUT - printf ("done (%f)\n",(clock()-timer)/(double)CLOCKS_PER_SEC); + printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif #ifndef NO_OUTPUT - printf ("compute palette...\n"); fflush(stdout); timer=clock(); + printf("compute palette...\n"); + fflush(stdout); + timer = clock(); #endif - if (!compute_palette_from_median_cut(pixelData,nPixels,h,&p,nPaletteEntries)) { - goto error_3; - } + if (!compute_palette_from_median_cut(pixelData, nPixels, h, &p, nPaletteEntries)) { + goto error_3; + } #ifndef NO_OUTPUT - printf ("done (%f)\n",(clock()-timer)/(double)CLOCKS_PER_SEC); + printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif - free_box_tree(root); - root=NULL; + free_box_tree(root); + root = NULL; - /* malloc check ok, using calloc for overflow */ - qp=calloc(nPixels, sizeof(uint32_t)); - if (!qp) { goto error_4; } + /* malloc check ok, using calloc for overflow */ + qp = calloc(nPixels, sizeof(uint32_t)); + if (!qp) { + goto error_4; + } - if (nPaletteEntries > UINT32_MAX / nPaletteEntries ) { - goto error_5; - } - /* malloc check ok, using calloc for overflow, check of n*n above */ - avgDist=calloc(nPaletteEntries*nPaletteEntries, sizeof(uint32_t)); - if (!avgDist) { goto error_5; } + if (nPaletteEntries > UINT32_MAX / nPaletteEntries) { + goto error_5; + } + /* malloc check ok, using calloc for overflow, check of n*n above */ + avgDist = calloc(nPaletteEntries * nPaletteEntries, sizeof(uint32_t)); + if (!avgDist) { + goto error_5; + } - /* malloc check ok, using calloc for overflow, check of n*n above */ - avgDistSortKey=calloc(nPaletteEntries*nPaletteEntries, sizeof(uint32_t *)); - if (!avgDistSortKey) { goto error_6; } + /* malloc check ok, using calloc for overflow, check of n*n above */ + avgDistSortKey = calloc(nPaletteEntries * nPaletteEntries, sizeof(uint32_t *)); + if (!avgDistSortKey) { + goto error_6; + } - if (!build_distance_tables(avgDist,avgDistSortKey,p,nPaletteEntries)) { - goto error_7; - } + if (!build_distance_tables(avgDist, avgDistSortKey, p, nPaletteEntries)) { + goto error_7; + } - if (!map_image_pixels_from_median_box(pixelData,nPixels,p,nPaletteEntries,h,avgDist,avgDistSortKey,qp)) { - goto error_7; - } + if (!map_image_pixels_from_median_box( + pixelData, nPixels, p, nPaletteEntries, h, avgDist, avgDistSortKey, qp)) { + goto error_7; + } #ifdef TEST_NEAREST_NEIGHBOUR #include - { - uint32_t bestmatch,bestdist,dist; - HashTable *h2; - printf ("nearest neighbour search (full search)..."); fflush(stdout); timer=clock(); - h2=hashtable_new(unshifted_pixel_hash,unshifted_pixel_cmp); - for (i=0;inew),&pixel); - if (data->secondPixel || newDistdata->furthestDistance) { - data->furthestDistance=oldDist; - data->furthest.v=pixel.v; - } +compute_distances(const HashTable *h, const Pixel pixel, uint32_t *dist, void *u) { + DistanceData *data = (DistanceData *)u; + uint32_t oldDist = *dist; + uint32_t newDist; + newDist = _DISTSQR(&(data->new), &pixel); + if (data->secondPixel || newDist < oldDist) { + *dist = newDist; + oldDist = newDist; + } + if (oldDist > data->furthestDistance) { + data->furthestDistance = oldDist; + data->furthestV = pixel.v; + } } -int -quantize2(Pixel *pixelData, - uint32_t nPixels, - uint32_t nQuantPixels, - Pixel **palette, - uint32_t *paletteLength, - uint32_t **quantizedPixels, - int kmeans) -{ - HashTable *h; - uint32_t i; - uint32_t mean[3]; - Pixel *p; - DistanceData data; - - uint32_t *qp; - uint32_t *avgDist; - uint32_t **avgDistSortKey; - - /* malloc check ok, using calloc */ - p=calloc(nQuantPixels, sizeof(Pixel)); - if (!p) return 0; - mean[0]=mean[1]=mean[2]=0; - h=hashtable_new(unshifted_pixel_hash,unshifted_pixel_cmp); - for (i=0;i UINT32_MAX / nQuantPixels ) { - goto error_2; - } - - /* malloc check ok, using calloc for overflow, check of n*n above */ - avgDist=calloc(nQuantPixels*nQuantPixels, sizeof(uint32_t)); - if (!avgDist) { goto error_2; } - - /* malloc check ok, using calloc for overflow, check of n*n above */ - avgDistSortKey=calloc(nQuantPixels*nQuantPixels, sizeof(uint32_t *)); - if (!avgDistSortKey) { goto error_3; } - - if (!build_distance_tables(avgDist,avgDistSortKey,p,nQuantPixels)) { - goto error_4; - } - - if (!map_image_pixels(pixelData,nPixels,p,nQuantPixels,avgDist,avgDistSortKey,qp)) { - goto error_4; - } - if (kmeans) k_means(pixelData,nPixels,p,nQuantPixels,qp,kmeans-1); - - *paletteLength=nQuantPixels; - *palette=p; - *quantizedPixels=qp; - free(avgDistSortKey); - free(avgDist); - return 1; +static int +quantize2( + Pixel *pixelData, + uint32_t nPixels, + uint32_t nQuantPixels, + Pixel **palette, + uint32_t *paletteLength, + uint32_t **quantizedPixels, + int kmeans) { + HashTable *h; + uint32_t i; + uint32_t mean[3]; + Pixel *p; + DistanceData data; + + uint32_t *qp; + uint32_t *avgDist; + uint32_t **avgDistSortKey; + + /* malloc check ok, using calloc */ + p = calloc(nQuantPixels, sizeof(Pixel)); + if (!p) { + return 0; + } + mean[0] = mean[1] = mean[2] = 0; + h = hashtable_new(unshifted_pixel_hash, unshifted_pixel_cmp); + for (i = 0; i < nPixels; i++) { + hashtable_insert(h, pixelData[i], 0xffffffff); + mean[0] += pixelData[i].c.r; + mean[1] += pixelData[i].c.g; + mean[2] += pixelData[i].c.b; + } + data.new.c.r = (int)(.5 + (double)mean[0] / (double)nPixels); + data.new.c.g = (int)(.5 + (double)mean[1] / (double)nPixels); + data.new.c.b = (int)(.5 + (double)mean[2] / (double)nPixels); + for (i = 0; i < nQuantPixels; i++) { + data.furthestDistance = 0; + data.furthestV = pixelData[0].v; + data.secondPixel = (i == 1) ? 1 : 0; + hashtable_foreach_update(h, compute_distances, &data); + p[i].v = data.furthestV; + data.new.v = data.furthestV; + } + hashtable_free(h); + + /* malloc check ok, using calloc */ + qp = calloc(nPixels, sizeof(uint32_t)); + if (!qp) { + goto error_1; + } + + if (nQuantPixels > UINT32_MAX / nQuantPixels) { + goto error_2; + } + + /* malloc check ok, using calloc for overflow, check of n*n above */ + avgDist = calloc(nQuantPixels * nQuantPixels, sizeof(uint32_t)); + if (!avgDist) { + goto error_2; + } + + /* malloc check ok, using calloc for overflow, check of n*n above */ + avgDistSortKey = calloc(nQuantPixels * nQuantPixels, sizeof(uint32_t *)); + if (!avgDistSortKey) { + goto error_3; + } + + if (!build_distance_tables(avgDist, avgDistSortKey, p, nQuantPixels)) { + goto error_4; + } + + if (!map_image_pixels( + pixelData, nPixels, p, nQuantPixels, avgDist, avgDistSortKey, qp)) { + goto error_4; + } + if (kmeans) { + k_means(pixelData, nPixels, p, nQuantPixels, qp, kmeans - 1); + } + + *paletteLength = nQuantPixels; + *palette = p; + *quantizedPixels = qp; + free(avgDistSortKey); + free(avgDist); + return 1; error_4: - free(avgDistSortKey); + free(avgDistSortKey); error_3: - free(avgDist); + free(avgDist); error_2: - free(qp); + free(qp); error_1: - free(p); - return 0; + free(p); + return 0; } Imaging -ImagingQuantize(Imaging im, int colors, int mode, int kmeans) -{ +ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { int i, j; int x, y, v; - UINT8* pp; - Pixel* p; - Pixel* palette; + UINT8 *pp; + Pixel *p; + Pixel *palette; uint32_t paletteLength; int result; - uint32_t* newData; + uint32_t *newData; Imaging imOut; int withAlpha = 0; ImagingSectionCookie cookie; - if (!im) + if (!im) { return ImagingError_ModeError(); - if (colors < 1 || colors > 256) + } + if (colors < 1 || colors > 256) { /* FIXME: for colors > 256, consider returning an RGB image instead (see @PIL205) */ - return (Imaging) ImagingError_ValueError("bad number of colors"); + return (Imaging)ImagingError_ValueError("bad number of colors"); + } if (strcmp(im->mode, "L") != 0 && strcmp(im->mode, "P") != 0 && - strcmp(im->mode, "RGB") != 0 && strcmp(im->mode, "RGBA") !=0) + strcmp(im->mode, "RGB") != 0 && strcmp(im->mode, "RGBA") != 0) { return ImagingError_ModeError(); + } /* only octree and imagequant supports RGBA */ - if (!strcmp(im->mode, "RGBA") && mode != 2 && mode != 3) - return ImagingError_ModeError(); + if (!strcmp(im->mode, "RGBA") && mode != 2 && mode != 3) { + return ImagingError_ModeError(); + } if (im->xsize > INT_MAX / im->ysize) { return ImagingError_MemoryError(); } /* malloc check ok, using calloc for final overflow, x*y above */ p = calloc(im->xsize * im->ysize, sizeof(Pixel)); - if (!p) + if (!p) { return ImagingError_MemoryError(); + } /* collect statistics */ @@ -1541,101 +1690,112 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) /* FIXME: converting a "L" image to "P" with 256 colors should be done by a simple copy... */ - for (i = y = 0; y < im->ysize; y++) + for (i = y = 0; y < im->ysize; y++) { for (x = 0; x < im->xsize; x++, i++) { p[i].c.r = p[i].c.g = p[i].c.b = im->image8[y][x]; p[i].c.a = 255; } + } } else if (!strcmp(im->mode, "P")) { /* palette */ pp = im->palette->palette; - for (i = y = 0; y < im->ysize; y++) + for (i = y = 0; y < im->ysize; y++) { for (x = 0; x < im->xsize; x++, i++) { v = im->image8[y][x]; - p[i].c.r = pp[v*4+0]; - p[i].c.g = pp[v*4+1]; - p[i].c.b = pp[v*4+2]; - p[i].c.a = pp[v*4+3]; + p[i].c.r = pp[v * 4 + 0]; + p[i].c.g = pp[v * 4 + 1]; + p[i].c.b = pp[v * 4 + 2]; + p[i].c.a = pp[v * 4 + 3]; } + } } else if (!strcmp(im->mode, "RGB") || !strcmp(im->mode, "RGBA")) { /* true colour */ - for (i = y = 0; y < im->ysize; y++) - for (x = 0; x < im->xsize; x++, i++) + withAlpha = !strcmp(im->mode, "RGBA"); + int transparency = 0; + unsigned char r, g, b; + for (i = y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++, i++) { p[i].v = im->image32[y][x]; + if (withAlpha && p[i].c.a == 0) { + if (transparency == 0) { + transparency = 1; + r = p[i].c.r; + g = p[i].c.g; + b = p[i].c.b; + } else { + /* Set all subsequent transparent pixels + to the same colour as the first */ + p[i].c.r = r; + p[i].c.g = g; + p[i].c.b = b; + } + } + } + } } else { free(p); - return (Imaging) ImagingError_ValueError("internal error"); + return (Imaging)ImagingError_ValueError("internal error"); } ImagingSectionEnter(&cookie); switch (mode) { - case 0: - /* median cut */ - result = quantize( - p, - im->xsize*im->ysize, - colors, - &palette, - &paletteLength, - &newData, - kmeans - ); - break; - case 1: - /* maximum coverage */ - result = quantize2( - p, - im->xsize*im->ysize, - colors, - &palette, - &paletteLength, - &newData, - kmeans - ); - break; - case 2: - if (!strcmp(im->mode, "RGBA")) { - withAlpha = 1; - } - result = quantize_octree( - p, - im->xsize*im->ysize, - colors, - &palette, - &paletteLength, - &newData, - withAlpha - ); - break; - case 3: + case 0: + /* median cut */ + result = quantize( + p, + im->xsize * im->ysize, + colors, + &palette, + &paletteLength, + &newData, + kmeans); + break; + case 1: + /* maximum coverage */ + result = quantize2( + p, + im->xsize * im->ysize, + colors, + &palette, + &paletteLength, + &newData, + kmeans); + break; + case 2: + result = quantize_octree( + p, + im->xsize * im->ysize, + colors, + &palette, + &paletteLength, + &newData, + withAlpha); + break; + case 3: #ifdef HAVE_LIBIMAGEQUANT - if (!strcmp(im->mode, "RGBA")) { - withAlpha = 1; - } - result = quantize_pngquant( - p, - im->xsize, - im->ysize, - colors, - &palette, - &paletteLength, - &newData, - withAlpha - ); + result = quantize_pngquant( + p, + im->xsize, + im->ysize, + colors, + &palette, + &paletteLength, + &newData, + withAlpha); #else - result = -1; + result = -1; #endif - break; - default: - result = 0; - break; + break; + default: + result = 0; + break; } free(p); @@ -1645,22 +1805,24 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) imOut = ImagingNewDirty("P", im->xsize, im->ysize); ImagingSectionEnter(&cookie); - for (i = y = 0; y < im->ysize; y++) - for (x = 0; x < im->xsize; x++) - imOut->image8[y][x] = (unsigned char) newData[i++]; + for (i = y = 0; y < im->ysize; y++) { + for (x = 0; x < im->xsize; x++) { + imOut->image8[y][x] = (unsigned char)newData[i++]; + } + } free(newData); pp = imOut->palette->palette; - for (i = j = 0; i < (int) paletteLength; i++) { + for (i = j = 0; i < (int)paletteLength; i++) { *pp++ = palette[i].c.r; *pp++ = palette[i].c.g; *pp++ = palette[i].c.b; if (withAlpha) { - *pp++ = palette[i].c.a; + *pp++ = palette[i].c.a; } else { - *pp++ = 255; + *pp++ = 255; } } for (; i < 256; i++) { @@ -1680,14 +1842,12 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) return imOut; } else { - if (result == -1) { - return (Imaging) ImagingError_ValueError( + return (Imaging)ImagingError_ValueError( "dependency required by this method was not " "enabled at compile time"); } - return (Imaging) ImagingError_ValueError("quantization error"); - + return (Imaging)ImagingError_ValueError("quantization error"); } } diff --git a/src/libImaging/QuantHash.c b/src/libImaging/QuantHash.c index 48b7a973e42..ea75d6037f9 100644 --- a/src/libImaging/QuantHash.c +++ b/src/libImaging/QuantHash.c @@ -24,415 +24,313 @@ #include "QuantHash.h" typedef struct _HashNode { - struct _HashNode *next; - HashKey_t key; - HashVal_t value; + struct _HashNode *next; + HashKey_t key; + HashVal_t value; } HashNode; struct _HashTable { - HashNode **table; - uint32_t length; - uint32_t count; - HashFunc hashFunc; - HashCmpFunc cmpFunc; - KeyDestroyFunc keyDestroyFunc; - ValDestroyFunc valDestroyFunc; - void *userData; + HashNode **table; + uint32_t length; + uint32_t count; + HashFunc hashFunc; + HashCmpFunc cmpFunc; + void *userData; }; #define MIN_LENGTH 11 #define RESIZE_FACTOR 3 -static int _hashtable_insert_node(HashTable *,HashNode *,int,int,CollisionFunc); - -HashTable *hashtable_new(HashFunc hf,HashCmpFunc cf) { - HashTable *h; - h=malloc(sizeof(HashTable)); - if (!h) { return NULL; } - h->hashFunc=hf; - h->cmpFunc=cf; - h->keyDestroyFunc=NULL; - h->valDestroyFunc=NULL; - h->length=MIN_LENGTH; - h->count=0; - h->userData=NULL; - h->table=malloc(sizeof(HashNode *)*h->length); - if (!h->table) { free(h); return NULL; } - memset (h->table,0,sizeof(HashNode *)*h->length); - return h; -} - -static void _hashtable_destroy(const HashTable *h,const HashKey_t key,const HashVal_t val,void *u) { - if (h->keyDestroyFunc) { - h->keyDestroyFunc(h,key); - } - if (h->valDestroyFunc) { - h->valDestroyFunc(h,val); - } +static int +_hashtable_insert_node(HashTable *, HashNode *, int, int, CollisionFunc); + +HashTable * +hashtable_new(HashFunc hf, HashCmpFunc cf) { + HashTable *h; + h = malloc(sizeof(HashTable)); + if (!h) { + return NULL; + } + h->hashFunc = hf; + h->cmpFunc = cf; + h->length = MIN_LENGTH; + h->count = 0; + h->userData = NULL; + h->table = malloc(sizeof(HashNode *) * h->length); + if (!h->table) { + free(h); + return NULL; + } + memset(h->table, 0, sizeof(HashNode *) * h->length); + return h; } -static uint32_t _findPrime(uint32_t start,int dir) { - static int unit[]={0,1,0,1,0,0,0,1,0,1,0,1,0,1,0,0}; - uint32_t t; - while (start>1) { - if (!unit[start&0x0f]) { - start+=dir; - continue; - } - for (t=2;t=sqrt((double)start)) { - break; - } - start+=dir; - } - return start; -} - -static void _hashtable_rehash(HashTable *h,CollisionFunc cf,uint32_t newSize) { - HashNode **oldTable=h->table; - uint32_t i; - HashNode *n,*nn; - uint32_t oldSize; - oldSize=h->length; - h->table=malloc(sizeof(HashNode *)*newSize); - if (!h->table) { - h->table=oldTable; - return; - } - h->length=newSize; - h->count=0; - memset (h->table,0,sizeof(HashNode *)*h->length); - for (i=0;inext; - _hashtable_insert_node(h,n,0,0,cf); - } - } - free(oldTable); -} - -static void _hashtable_resize(HashTable *h) { - uint32_t newSize; - uint32_t oldSize; - oldSize=h->length; - newSize=oldSize; - if (h->count*RESIZE_FACTORlength) { - newSize=_findPrime(h->length/2-1,-1); - } else if (h->length*RESIZE_FACTORcount) { - newSize=_findPrime(h->length*2+1,+1); - } - if (newSizehashFunc(h,node->key)%h->length; - HashNode **n,*nv; - int i; - - for (n=&(h->table[hash]);*n;n=&((*n)->next)) { - nv=*n; - i=h->cmpFunc(h,nv->key,node->key); - if (!i) { - if (cf) { - nv->key=node->key; - cf(h,&(nv->key),&(nv->value),node->key,node->value); - free(node); - return 1; - } else { - if (h->valDestroyFunc) { - h->valDestroyFunc(h,nv->value); +static uint32_t +_findPrime(uint32_t start, int dir) { + static int unit[] = {0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0}; + uint32_t t; + while (start > 1) { + if (!unit[start & 0x0f]) { + start += dir; + continue; + } + for (t = 2; t < sqrt((double)start); t++) { + if (!start % t) { + break; } - if (h->keyDestroyFunc) { - h->keyDestroyFunc(h,nv->key); - } - nv->key=node->key; - nv->value=node->value; - free(node); - return 1; - } - } else if (i>0) { - break; - } - } - if (!update) { - node->next=*n; - *n=node; - h->count++; - if (resize) _hashtable_resize(h); - return 1; - } else { - return 0; - } + } + if (t >= sqrt((double)start)) { + break; + } + start += dir; + } + return start; } -static int _hashtable_insert(HashTable *h,HashKey_t key,HashVal_t val,int resize,int update) { - HashNode **n,*nv; - HashNode *t; - int i; - uint32_t hash=h->hashFunc(h,key)%h->length; - - for (n=&(h->table[hash]);*n;n=&((*n)->next)) { - nv=*n; - i=h->cmpFunc(h,nv->key,key); - if (!i) { - if (h->valDestroyFunc) { h->valDestroyFunc(h,nv->value); } - nv->value=val; - return 1; - } else if (i>0) { - break; - } - } - if (!update) { - t=malloc(sizeof(HashNode)); - if (!t) return 0; - t->next=*n; - *n=t; - t->key=key; - t->value=val; - h->count++; - if (resize) _hashtable_resize(h); - return 1; - } else { - return 0; - } +static void +_hashtable_rehash(HashTable *h, CollisionFunc cf, uint32_t newSize) { + HashNode **oldTable = h->table; + uint32_t i; + HashNode *n, *nn; + uint32_t oldSize; + oldSize = h->length; + h->table = malloc(sizeof(HashNode *) * newSize); + if (!h->table) { + h->table = oldTable; + return; + } + h->length = newSize; + h->count = 0; + memset(h->table, 0, sizeof(HashNode *) * h->length); + for (i = 0; i < oldSize; i++) { + for (n = oldTable[i]; n; n = nn) { + nn = n->next; + _hashtable_insert_node(h, n, 0, 0, cf); + } + } + free(oldTable); } -static int _hashtable_lookup_or_insert(HashTable *h,HashKey_t key,HashVal_t *retVal,HashVal_t newVal,int resize) { - HashNode **n,*nv; - HashNode *t; - int i; - uint32_t hash=h->hashFunc(h,key)%h->length; - - for (n=&(h->table[hash]);*n;n=&((*n)->next)) { - nv=*n; - i=h->cmpFunc(h,nv->key,key); - if (!i) { - *retVal=nv->value; - return 1; - } else if (i>0) { - break; - } - } - t=malloc(sizeof(HashNode)); - if (!t) return 0; - t->next=*n; - *n=t; - t->key=key; - t->value=newVal; - *retVal=newVal; - h->count++; - if (resize) _hashtable_resize(h); - return 1; +static void +_hashtable_resize(HashTable *h) { + uint32_t newSize; + uint32_t oldSize; + oldSize = h->length; + newSize = oldSize; + if (h->count * RESIZE_FACTOR < h->length) { + newSize = _findPrime(h->length / 2 - 1, -1); + } else if (h->length * RESIZE_FACTOR < h->count) { + newSize = _findPrime(h->length * 2 + 1, +1); + } + if (newSize < MIN_LENGTH) { + newSize = oldSize; + } + if (newSize != oldSize) { + _hashtable_rehash(h, NULL, newSize); + } } -int hashtable_insert_or_update_computed(HashTable *h, - HashKey_t key, - ComputeFunc newFunc, - ComputeFunc existsFunc) { - HashNode **n,*nv; - HashNode *t; - int i; - uint32_t hash=h->hashFunc(h,key)%h->length; - - for (n=&(h->table[hash]);*n;n=&((*n)->next)) { - nv=*n; - i=h->cmpFunc(h,nv->key,key); - if (!i) { - HashVal_t old=nv->value; - if (existsFunc) { - existsFunc(h,nv->key,&(nv->value)); - if (nv->value!=old) { - if (h->valDestroyFunc) { - h->valDestroyFunc(h,old); - } +static int +_hashtable_insert_node( + HashTable *h, HashNode *node, int resize, int update, CollisionFunc cf) { + uint32_t hash = h->hashFunc(h, node->key) % h->length; + HashNode **n, *nv; + int i; + + for (n = &(h->table[hash]); *n; n = &((*n)->next)) { + nv = *n; + i = h->cmpFunc(h, nv->key, node->key); + if (!i) { + if (cf) { + nv->key = node->key; + cf(h, &(nv->key), &(nv->value), node->key, node->value); + free(node); + return 1; + } else { + nv->key = node->key; + nv->value = node->value; + free(node); + return 1; } - } else { - return 0; - } - return 1; - } else if (i>0) { - break; - } - } - t=malloc(sizeof(HashNode)); - if (!t) return 0; - t->key=key; - t->next=*n; - *n=t; - if (newFunc) { - newFunc(h,t->key,&(t->value)); - } else { - free(t); - return 0; - } - h->count++; - _hashtable_resize(h); - return 1; -} - -int hashtable_update(HashTable *h,HashKey_t key,HashVal_t val) { - return _hashtable_insert(h,key,val,1,0); -} - -int hashtable_insert(HashTable *h,HashKey_t key,HashVal_t val) { - return _hashtable_insert(h,key,val,1,0); + } else if (i > 0) { + break; + } + } + if (!update) { + node->next = *n; + *n = node; + h->count++; + if (resize) { + _hashtable_resize(h); + } + return 1; + } else { + return 0; + } } -void hashtable_foreach_update(HashTable *h,IteratorUpdateFunc i,void *u) { - HashNode *n; - uint32_t x; - - if (h->table) { - for (x=0;xlength;x++) { - for (n=h->table[x];n;n=n->next) { - i(h,n->key,&(n->value),u); - } - } - } -} - -void hashtable_foreach(HashTable *h,IteratorFunc i,void *u) { - HashNode *n; - uint32_t x; - - if (h->table) { - for (x=0;xlength;x++) { - for (n=h->table[x];n;n=n->next) { - i(h,n->key,n->value,u); - } - } - } -} - -void hashtable_free(HashTable *h) { - HashNode *n,*nn; - uint32_t i; - - if (h->table) { - if (h->keyDestroyFunc || h->keyDestroyFunc) { - hashtable_foreach(h,_hashtable_destroy,NULL); - } - for (i=0;ilength;i++) { - for (n=h->table[i];n;n=nn) { - nn=n->next; - free(n); - } - } - free(h->table); - } - free(h); -} - -ValDestroyFunc hashtable_set_value_destroy_func(HashTable *h,ValDestroyFunc d) { - ValDestroyFunc r=h->valDestroyFunc; - h->valDestroyFunc=d; - return r; +static int +_hashtable_insert(HashTable *h, HashKey_t key, HashVal_t val, int resize, int update) { + HashNode **n, *nv; + HashNode *t; + int i; + uint32_t hash = h->hashFunc(h, key) % h->length; + + for (n = &(h->table[hash]); *n; n = &((*n)->next)) { + nv = *n; + i = h->cmpFunc(h, nv->key, key); + if (!i) { + nv->value = val; + return 1; + } else if (i > 0) { + break; + } + } + if (!update) { + t = malloc(sizeof(HashNode)); + if (!t) { + return 0; + } + t->next = *n; + *n = t; + t->key = key; + t->value = val; + h->count++; + if (resize) { + _hashtable_resize(h); + } + return 1; + } else { + return 0; + } } -KeyDestroyFunc hashtable_set_key_destroy_func(HashTable *h,KeyDestroyFunc d) { - KeyDestroyFunc r=h->keyDestroyFunc; - h->keyDestroyFunc=d; - return r; +int +hashtable_insert_or_update_computed( + HashTable *h, HashKey_t key, ComputeFunc newFunc, ComputeFunc existsFunc) { + HashNode **n, *nv; + HashNode *t; + int i; + uint32_t hash = h->hashFunc(h, key) % h->length; + + for (n = &(h->table[hash]); *n; n = &((*n)->next)) { + nv = *n; + i = h->cmpFunc(h, nv->key, key); + if (!i) { + if (existsFunc) { + existsFunc(h, nv->key, &(nv->value)); + } else { + return 0; + } + return 1; + } else if (i > 0) { + break; + } + } + t = malloc(sizeof(HashNode)); + if (!t) { + return 0; + } + t->key = key; + t->next = *n; + *n = t; + if (newFunc) { + newFunc(h, t->key, &(t->value)); + } else { + free(t); + return 0; + } + h->count++; + _hashtable_resize(h); + return 1; } -static int _hashtable_remove(HashTable *h, - const HashKey_t key, - HashKey_t *keyRet, - HashVal_t *valRet, - int resize) { - uint32_t hash=h->hashFunc(h,key)%h->length; - HashNode *n,*p; - int i; - - for (p=NULL,n=h->table[hash];n;p=n,n=n->next) { - i=h->cmpFunc(h,n->key,key); - if (!i) { - if (p) p=n->next; else h->table[hash]=n->next; - *keyRet=n->key; - *valRet=n->value; - free(n); - h->count++; - return 1; - } else if (i>0) { - break; - } - } - return 0; +int +hashtable_insert(HashTable *h, HashKey_t key, HashVal_t val) { + return _hashtable_insert(h, key, val, 1, 0); } -static int _hashtable_delete(HashTable *h,const HashKey_t key,int resize) { - uint32_t hash=h->hashFunc(h,key)%h->length; - HashNode *n,*p; - int i; +void +hashtable_foreach_update(HashTable *h, IteratorUpdateFunc i, void *u) { + HashNode *n; + uint32_t x; - for (p=NULL,n=h->table[hash];n;p=n,n=n->next) { - i=h->cmpFunc(h,n->key,key); - if (!i) { - if (p) p=n->next; else h->table[hash]=n->next; - if (h->valDestroyFunc) { h->valDestroyFunc(h,n->value); } - if (h->keyDestroyFunc) { h->keyDestroyFunc(h,n->key); } - free(n); - h->count++; - return 1; - } else if (i>0) { - break; - } - } - return 0; + if (h->table) { + for (x = 0; x < h->length; x++) { + for (n = h->table[x]; n; n = n->next) { + i(h, n->key, &(n->value), u); + } + } + } } -int hashtable_remove(HashTable *h,const HashKey_t key,HashKey_t *keyRet,HashVal_t *valRet) { - return _hashtable_remove(h,key,keyRet,valRet,1); -} +void +hashtable_foreach(HashTable *h, IteratorFunc i, void *u) { + HashNode *n; + uint32_t x; -int hashtable_delete(HashTable *h,const HashKey_t key) { - return _hashtable_delete(h,key,1); + if (h->table) { + for (x = 0; x < h->length; x++) { + for (n = h->table[x]; n; n = n->next) { + i(h, n->key, n->value, u); + } + } + } } -void hashtable_rehash_compute(HashTable *h,CollisionFunc cf) { - _hashtable_rehash(h,cf,h->length); -} +void +hashtable_free(HashTable *h) { + HashNode *n, *nn; + uint32_t i; -void hashtable_rehash(HashTable *h) { - _hashtable_rehash(h,NULL,h->length); + if (h->table) { + for (i = 0; i < h->length; i++) { + for (n = h->table[i]; n; n = nn) { + nn = n->next; + free(n); + } + } + free(h->table); + } + free(h); } -int hashtable_lookup_or_insert(HashTable *h,HashKey_t key,HashVal_t *valp,HashVal_t val) { - return _hashtable_lookup_or_insert(h,key,valp,val,1); +void +hashtable_rehash_compute(HashTable *h, CollisionFunc cf) { + _hashtable_rehash(h, cf, h->length); } -int hashtable_lookup(const HashTable *h,const HashKey_t key,HashVal_t *valp) { - uint32_t hash=h->hashFunc(h,key)%h->length; - HashNode *n; - int i; +int +hashtable_lookup(const HashTable *h, const HashKey_t key, HashVal_t *valp) { + uint32_t hash = h->hashFunc(h, key) % h->length; + HashNode *n; + int i; - for (n=h->table[hash];n;n=n->next) { - i=h->cmpFunc(h,n->key,key); - if (!i) { - *valp=n->value; - return 1; - } else if (i>0) { - break; - } - } - return 0; + for (n = h->table[hash]; n; n = n->next) { + i = h->cmpFunc(h, n->key, key); + if (!i) { + *valp = n->value; + return 1; + } else if (i > 0) { + break; + } + } + return 0; } -uint32_t hashtable_get_count(const HashTable *h) { - return h->count; +uint32_t +hashtable_get_count(const HashTable *h) { + return h->count; } -void *hashtable_get_user_data(const HashTable *h) { - return h->userData; +void * +hashtable_get_user_data(const HashTable *h) { + return h->userData; } -void *hashtable_set_user_data(HashTable *h,void *data) { - void *r=h->userData; - h->userData=data; - return r; +void * +hashtable_set_user_data(HashTable *h, void *data) { + void *r = h->userData; + h->userData = data; + return r; } diff --git a/src/libImaging/QuantHash.h b/src/libImaging/QuantHash.h index 028b4af8941..fc1a9900376 100644 --- a/src/libImaging/QuantHash.h +++ b/src/libImaging/QuantHash.h @@ -18,32 +18,38 @@ typedef struct _HashTable HashTable; typedef Pixel HashKey_t; typedef uint32_t HashVal_t; -typedef uint32_t (*HashFunc)(const HashTable *,const HashKey_t); -typedef int (*HashCmpFunc)(const HashTable *,const HashKey_t,const HashKey_t); -typedef void (*IteratorFunc)(const HashTable *,const HashKey_t,const HashVal_t,void *); -typedef void (*IteratorUpdateFunc)(const HashTable *,const HashKey_t,HashVal_t *,void *); -typedef void (*KeyDestroyFunc)(const HashTable *,HashKey_t); -typedef void (*ValDestroyFunc)(const HashTable *,HashVal_t); -typedef void (*ComputeFunc)(const HashTable *,const HashKey_t,HashVal_t *); -typedef void (*CollisionFunc)(const HashTable *,HashKey_t *,HashVal_t *,HashKey_t,HashVal_t); +typedef uint32_t (*HashFunc)(const HashTable *, const HashKey_t); +typedef int (*HashCmpFunc)(const HashTable *, const HashKey_t, const HashKey_t); +typedef void (*IteratorFunc)( + const HashTable *, const HashKey_t, const HashVal_t, void *); +typedef void (*IteratorUpdateFunc)( + const HashTable *, const HashKey_t, HashVal_t *, void *); +typedef void (*ComputeFunc)(const HashTable *, const HashKey_t, HashVal_t *); +typedef void (*CollisionFunc)( + const HashTable *, HashKey_t *, HashVal_t *, HashKey_t, HashVal_t); -HashTable * hashtable_new(HashFunc hf,HashCmpFunc cf); -void hashtable_free(HashTable *h); -void hashtable_foreach(HashTable *h,IteratorFunc i,void *u); -void hashtable_foreach_update(HashTable *h,IteratorUpdateFunc i,void *u); -int hashtable_insert(HashTable *h,HashKey_t key,HashVal_t val); -int hashtable_update(HashTable *h,HashKey_t key,HashVal_t val); -int hashtable_lookup(const HashTable *h,const HashKey_t key,HashVal_t *valp); -int hashtable_lookup_or_insert(HashTable *h,HashKey_t key,HashVal_t *valp,HashVal_t val); -int hashtable_insert_or_update_computed(HashTable *h,HashKey_t key,ComputeFunc newFunc,ComputeFunc existsFunc); -int hashtable_delete(HashTable *h,const HashKey_t key); -int hashtable_remove(HashTable *h,const HashKey_t key,HashKey_t *keyRet,HashVal_t *valRet); -void *hashtable_set_user_data(HashTable *h,void *data); -void *hashtable_get_user_data(const HashTable *h); -KeyDestroyFunc hashtable_set_key_destroy_func(HashTable *,KeyDestroyFunc d); -ValDestroyFunc hashtable_set_value_destroy_func(HashTable *,ValDestroyFunc d); -uint32_t hashtable_get_count(const HashTable *h); -void hashtable_rehash(HashTable *h); -void hashtable_rehash_compute(HashTable *h,CollisionFunc cf); +HashTable * +hashtable_new(HashFunc hf, HashCmpFunc cf); +void +hashtable_free(HashTable *h); +void +hashtable_foreach(HashTable *h, IteratorFunc i, void *u); +void +hashtable_foreach_update(HashTable *h, IteratorUpdateFunc i, void *u); +int +hashtable_insert(HashTable *h, HashKey_t key, HashVal_t val); +int +hashtable_lookup(const HashTable *h, const HashKey_t key, HashVal_t *valp); +int +hashtable_insert_or_update_computed( + HashTable *h, HashKey_t key, ComputeFunc newFunc, ComputeFunc existsFunc); +void * +hashtable_set_user_data(HashTable *h, void *data); +void * +hashtable_get_user_data(const HashTable *h); +uint32_t +hashtable_get_count(const HashTable *h); +void +hashtable_rehash_compute(HashTable *h, CollisionFunc cf); -#endif // __QUANTHASH_H__ +#endif // __QUANTHASH_H__ diff --git a/src/libImaging/QuantHeap.c b/src/libImaging/QuantHeap.c index 121b8727560..6fb52d8902e 100644 --- a/src/libImaging/QuantHeap.c +++ b/src/libImaging/QuantHeap.c @@ -25,10 +25,10 @@ #include "QuantHeap.h" struct _Heap { - void **heap; - int heapsize; - int heapcount; - HeapCmpFunc cf; + void **heap; + unsigned int heapsize; + unsigned int heapcount; + HeapCmpFunc cf; }; #define INITIAL_SIZE 256 @@ -36,119 +36,141 @@ struct _Heap { // #define DEBUG #ifdef DEBUG -static int _heap_test(Heap *); +static int +_heap_test(Heap *); #endif -void ImagingQuantHeapFree(Heap *h) { - free(h->heap); - free(h); +void +ImagingQuantHeapFree(Heap *h) { + free(h->heap); + free(h); } -static int _heap_grow(Heap *h,int newsize) { - void *newheap; - if (!newsize) newsize=h->heapsize<<1; - if (newsizeheapsize) return 0; - if (newsize > INT_MAX / sizeof(void *)){ - return 0; - } - /* malloc check ok, using calloc for overflow, also checking - above due to memcpy below*/ - newheap=calloc(newsize, sizeof(void *)); - if (!newheap) return 0; - memcpy(newheap,h->heap,sizeof(void *)*h->heapsize); - free(h->heap); - h->heap=newheap; - h->heapsize=newsize; - return 1; +static int +_heap_grow(Heap *h, unsigned int newsize) { + void *newheap; + if (!newsize) { + newsize = h->heapsize << 1; + } + if (newsize < h->heapsize) { + return 0; + } + if (newsize > INT_MAX / sizeof(void *)) { + return 0; + } + /* malloc check ok, using calloc for overflow, also checking + above due to memcpy below*/ + newheap = calloc(newsize, sizeof(void *)); + if (!newheap) { + return 0; + } + memcpy(newheap, h->heap, sizeof(void *) * h->heapsize); + free(h->heap); + h->heap = newheap; + h->heapsize = newsize; + return 1; } #ifdef DEBUG -static int _heap_test(Heap *h) { - int k; - for (k=1;k*2<=h->heapcount;k++) { - if (h->cf(h,h->heap[k],h->heap[k*2])<0) { - printf ("heap is bad\n"); - return 0; - } - if (k*2+1<=h->heapcount && h->cf(h,h->heap[k],h->heap[k*2+1])<0) { - printf ("heap is bad\n"); - return 0; - } - } - return 1; +static int +_heap_test(Heap *h) { + unsigned int k; + for (k = 1; k * 2 <= h->heapcount; k++) { + if (h->cf(h, h->heap[k], h->heap[k * 2]) < 0) { + printf("heap is bad\n"); + return 0; + } + if (k * 2 + 1 <= h->heapcount && h->cf(h, h->heap[k], h->heap[k * 2 + 1]) < 0) { + printf("heap is bad\n"); + return 0; + } + } + return 1; } #endif -int ImagingQuantHeapRemove(Heap* h,void **r) { - int k,l; - void *v; +int +ImagingQuantHeapRemove(Heap *h, void **r) { + unsigned int k, l; + void *v; - if (!h->heapcount) { - return 0; - } - *r=h->heap[1]; - v=h->heap[h->heapcount--]; - for (k=1;k*2<=h->heapcount;k=l) { - l=k*2; - if (lheapcount) { - if (h->cf(h,h->heap[l],h->heap[l+1])<0) { - l++; - } - } - if (h->cf(h,v,h->heap[l])>0) { - break; - } - h->heap[k]=h->heap[l]; - } - h->heap[k]=v; + if (!h->heapcount) { + return 0; + } + *r = h->heap[1]; + v = h->heap[h->heapcount--]; + for (k = 1; k * 2 <= h->heapcount; k = l) { + l = k * 2; + if (l < h->heapcount) { + if (h->cf(h, h->heap[l], h->heap[l + 1]) < 0) { + l++; + } + } + if (h->cf(h, v, h->heap[l]) > 0) { + break; + } + h->heap[k] = h->heap[l]; + } + h->heap[k] = v; #ifdef DEBUG - if (!_heap_test(h)) { printf ("oops - heap_remove messed up the heap\n"); exit(1); } + if (!_heap_test(h)) { + printf("oops - heap_remove messed up the heap\n"); + exit(1); + } #endif - return 1; + return 1; } -int ImagingQuantHeapAdd(Heap *h,void *val) { - int k; - if (h->heapcount==h->heapsize-1) { - _heap_grow(h,0); - } - k=++h->heapcount; - while (k!=1) { - if (h->cf(h,val,h->heap[k/2])<=0) { - break; - } - h->heap[k]=h->heap[k/2]; - k>>=1; - } - h->heap[k]=val; +int +ImagingQuantHeapAdd(Heap *h, void *val) { + int k; + if (h->heapcount == h->heapsize - 1) { + _heap_grow(h, 0); + } + k = ++h->heapcount; + while (k != 1) { + if (h->cf(h, val, h->heap[k / 2]) <= 0) { + break; + } + h->heap[k] = h->heap[k / 2]; + k >>= 1; + } + h->heap[k] = val; #ifdef DEBUG - if (!_heap_test(h)) { printf ("oops - heap_add messed up the heap\n"); exit(1); } + if (!_heap_test(h)) { + printf("oops - heap_add messed up the heap\n"); + exit(1); + } #endif - return 1; + return 1; } -int ImagingQuantHeapTop(Heap *h,void **r) { - if (!h->heapcount) { - return 0; - } - *r=h->heap[1]; - return 1; +int +ImagingQuantHeapTop(Heap *h, void **r) { + if (!h->heapcount) { + return 0; + } + *r = h->heap[1]; + return 1; } -Heap *ImagingQuantHeapNew(HeapCmpFunc cf) { - Heap *h; +Heap * +ImagingQuantHeapNew(HeapCmpFunc cf) { + Heap *h; - /* malloc check ok, small constant allocation */ - h=malloc(sizeof(Heap)); - if (!h) return NULL; - h->heapsize=INITIAL_SIZE; - /* malloc check ok, using calloc for overflow */ - h->heap=calloc(h->heapsize, sizeof(void *)); - if (!h->heap) { - free(h); - return NULL; - } - h->heapcount=0; - h->cf=cf; - return h; + /* malloc check ok, small constant allocation */ + h = malloc(sizeof(Heap)); + if (!h) { + return NULL; + } + h->heapsize = INITIAL_SIZE; + /* malloc check ok, using calloc for overflow */ + h->heap = calloc(h->heapsize, sizeof(void *)); + if (!h->heap) { + free(h); + return NULL; + } + h->heapcount = 0; + h->cf = cf; + return h; } diff --git a/src/libImaging/QuantHeap.h b/src/libImaging/QuantHeap.h index 77bf0d9d550..c5286dff2ba 100644 --- a/src/libImaging/QuantHeap.h +++ b/src/libImaging/QuantHeap.h @@ -16,12 +16,16 @@ typedef struct _Heap Heap; -typedef int (*HeapCmpFunc)(const Heap *,const void *,const void *); +typedef int (*HeapCmpFunc)(const Heap *, const void *, const void *); -void ImagingQuantHeapFree(Heap *); -int ImagingQuantHeapRemove(Heap *,void **); -int ImagingQuantHeapAdd(Heap *,void *); -int ImagingQuantHeapTop(Heap *,void **); +void +ImagingQuantHeapFree(Heap *); +int +ImagingQuantHeapRemove(Heap *, void **); +int +ImagingQuantHeapAdd(Heap *, void *); +int +ImagingQuantHeapTop(Heap *, void **); Heap *ImagingQuantHeapNew(HeapCmpFunc); -#endif // __QUANTHEAP_H__ +#endif // __QUANTHEAP_H__ diff --git a/src/libImaging/QuantOctree.c b/src/libImaging/QuantOctree.c index e18ab3c65a2..5e79bce358a 100644 --- a/src/libImaging/QuantOctree.c +++ b/src/libImaging/QuantOctree.c @@ -28,451 +28,511 @@ #include #include +#include "ImagingUtils.h" #include "QuantOctree.h" -typedef struct _ColorBucket{ - /* contains palette index when used for look up cube */ - uint32_t count; - uint64_t r; - uint64_t g; - uint64_t b; - uint64_t a; -} *ColorBucket; +typedef struct _ColorBucket { + /* contains palette index when used for look up cube */ + uint32_t count; + uint64_t r; + uint64_t g; + uint64_t b; + uint64_t a; +} * ColorBucket; -typedef struct _ColorCube{ - unsigned int rBits, gBits, bBits, aBits; - unsigned int rWidth, gWidth, bWidth, aWidth; - unsigned int rOffset, gOffset, bOffset, aOffset; +typedef struct _ColorCube { + unsigned int rBits, gBits, bBits, aBits; + unsigned int rWidth, gWidth, bWidth, aWidth; + unsigned int rOffset, gOffset, bOffset, aOffset; - long size; - ColorBucket buckets; -} *ColorCube; + unsigned long size; + ColorBucket buckets; +} * ColorCube; -#define MAX(a, b) (a)>(b) ? (a) : (b) +#define MAX(a, b) (a) > (b) ? (a) : (b) static ColorCube new_color_cube(int r, int g, int b, int a) { - ColorCube cube; - - /* malloc check ok, small constant allocation */ - cube = malloc(sizeof(struct _ColorCube)); - if (!cube) return NULL; - - cube->rBits = MAX(r, 0); - cube->gBits = MAX(g, 0); - cube->bBits = MAX(b, 0); - cube->aBits = MAX(a, 0); - - /* overflow check for size multiplication below */ - if (cube->rBits + cube->gBits + cube->bBits + cube->aBits > 31) { - free(cube); - return NULL; - } - - /* the width of the cube for each dimension */ - cube->rWidth = 1<rBits; - cube->gWidth = 1<gBits; - cube->bWidth = 1<bBits; - cube->aWidth = 1<aBits; - - /* the offsets of each color */ - - cube->rOffset = cube->gBits + cube->bBits + cube->aBits; - cube->gOffset = cube->bBits + cube->aBits; - cube->bOffset = cube->aBits; - cube->aOffset = 0; - - /* the number of color buckets */ - cube->size = cube->rWidth * cube->gWidth * cube->bWidth * cube->aWidth; - /* malloc check ok, overflow checked above */ - cube->buckets = calloc(cube->size, sizeof(struct _ColorBucket)); - - if (!cube->buckets) { - free(cube); - return NULL; - } - return cube; + ColorCube cube; + + /* malloc check ok, small constant allocation */ + cube = malloc(sizeof(struct _ColorCube)); + if (!cube) { + return NULL; + } + + cube->rBits = MAX(r, 0); + cube->gBits = MAX(g, 0); + cube->bBits = MAX(b, 0); + cube->aBits = MAX(a, 0); + + /* overflow check for size multiplication below */ + if (cube->rBits + cube->gBits + cube->bBits + cube->aBits > 31) { + free(cube); + return NULL; + } + + /* the width of the cube for each dimension */ + cube->rWidth = 1 << cube->rBits; + cube->gWidth = 1 << cube->gBits; + cube->bWidth = 1 << cube->bBits; + cube->aWidth = 1 << cube->aBits; + + /* the offsets of each color */ + + cube->rOffset = cube->gBits + cube->bBits + cube->aBits; + cube->gOffset = cube->bBits + cube->aBits; + cube->bOffset = cube->aBits; + cube->aOffset = 0; + + /* the number of color buckets */ + cube->size = cube->rWidth * cube->gWidth * cube->bWidth * cube->aWidth; + /* malloc check ok, overflow checked above */ + cube->buckets = calloc(cube->size, sizeof(struct _ColorBucket)); + + if (!cube->buckets) { + free(cube); + return NULL; + } + return cube; } static void free_color_cube(ColorCube cube) { - if (cube != NULL) { - free(cube->buckets); - free(cube); - } + if (cube != NULL) { + free(cube->buckets); + free(cube); + } } static long -color_bucket_offset_pos(const ColorCube cube, - unsigned int r, unsigned int g, unsigned int b, unsigned int a) -{ - return r<rOffset | g<gOffset | b<bOffset | a<aOffset; +color_bucket_offset_pos( + const ColorCube cube, + unsigned int r, + unsigned int g, + unsigned int b, + unsigned int a) { + return r << cube->rOffset | g << cube->gOffset | b << cube->bOffset | + a << cube->aOffset; } static long color_bucket_offset(const ColorCube cube, const Pixel *p) { - unsigned int r = p->c.r>>(8-cube->rBits); - unsigned int g = p->c.g>>(8-cube->gBits); - unsigned int b = p->c.b>>(8-cube->bBits); - unsigned int a = p->c.a>>(8-cube->aBits); - return color_bucket_offset_pos(cube, r, g, b, a); + unsigned int r = p->c.r >> (8 - cube->rBits); + unsigned int g = p->c.g >> (8 - cube->gBits); + unsigned int b = p->c.b >> (8 - cube->bBits); + unsigned int a = p->c.a >> (8 - cube->aBits); + return color_bucket_offset_pos(cube, r, g, b, a); } static ColorBucket color_bucket_from_cube(const ColorCube cube, const Pixel *p) { - unsigned int offset = color_bucket_offset(cube, p); - return &cube->buckets[offset]; + unsigned int offset = color_bucket_offset(cube, p); + return &cube->buckets[offset]; } static void add_color_to_color_cube(const ColorCube cube, const Pixel *p) { - ColorBucket bucket = color_bucket_from_cube(cube, p); - bucket->count += 1; - bucket->r += p->c.r; - bucket->g += p->c.g; - bucket->b += p->c.b; - bucket->a += p->c.a; + ColorBucket bucket = color_bucket_from_cube(cube, p); + bucket->count += 1; + bucket->r += p->c.r; + bucket->g += p->c.g; + bucket->b += p->c.b; + bucket->a += p->c.a; } -static long +static unsigned long count_used_color_buckets(const ColorCube cube) { - long usedBuckets = 0; - long i; - for (i=0; i < cube->size; i++) { - if (cube->buckets[i].count > 0) { - usedBuckets += 1; - } - } - return usedBuckets; + unsigned long usedBuckets = 0; + unsigned long i; + for (i = 0; i < cube->size; i++) { + if (cube->buckets[i].count > 0) { + usedBuckets += 1; + } + } + return usedBuckets; } static void avg_color_from_color_bucket(const ColorBucket bucket, Pixel *dst) { - float count = bucket->count; - dst->c.r = (int)(bucket->r / count); - dst->c.g = (int)(bucket->g / count); - dst->c.b = (int)(bucket->b / count); - dst->c.a = (int)(bucket->a / count); + float count = bucket->count; + if (count != 0) { + dst->c.r = CLIP8((int)(bucket->r / count)); + dst->c.g = CLIP8((int)(bucket->g / count)); + dst->c.b = CLIP8((int)(bucket->b / count)); + dst->c.a = CLIP8((int)(bucket->a / count)); + } else { + dst->c.r = 0; + dst->c.g = 0; + dst->c.b = 0; + dst->c.a = 0; + } } static int compare_bucket_count(const ColorBucket a, const ColorBucket b) { - return b->count - a->count; + return b->count - a->count; } static ColorBucket create_sorted_color_palette(const ColorCube cube) { - ColorBucket buckets; - if (cube->size > LONG_MAX / sizeof(struct _ColorBucket)) { - return NULL; - } - /* malloc check ok, calloc + overflow check above for memcpy */ - buckets = calloc(cube->size, sizeof(struct _ColorBucket)); - if (!buckets) return NULL; - memcpy(buckets, cube->buckets, sizeof(struct _ColorBucket)*cube->size); - - qsort(buckets, cube->size, sizeof(struct _ColorBucket), - (int (*)(void const *, void const *))&compare_bucket_count); - - return buckets; + ColorBucket buckets; + if (cube->size > LONG_MAX / sizeof(struct _ColorBucket)) { + return NULL; + } + /* malloc check ok, calloc + overflow check above for memcpy */ + buckets = calloc(cube->size, sizeof(struct _ColorBucket)); + if (!buckets) { + return NULL; + } + memcpy(buckets, cube->buckets, sizeof(struct _ColorBucket) * cube->size); + + qsort( + buckets, + cube->size, + sizeof(struct _ColorBucket), + (int (*)(void const *, void const *)) & compare_bucket_count); + + return buckets; } -void add_bucket_values(ColorBucket src, ColorBucket dst) { - dst->count += src->count; - dst->r += src->r; - dst->g += src->g; - dst->b += src->b; - dst->a += src->a; +void +add_bucket_values(ColorBucket src, ColorBucket dst) { + dst->count += src->count; + dst->r += src->r; + dst->g += src->g; + dst->b += src->b; + dst->a += src->a; } /* expand or shrink a given cube to level */ -static ColorCube copy_color_cube(const ColorCube cube, - int rBits, int gBits, int bBits, int aBits) -{ - unsigned int r, g, b, a; - long src_pos, dst_pos; - unsigned int src_reduce[4] = {0}, dst_reduce[4] = {0}; - unsigned int width[4]; - ColorCube result; - - result = new_color_cube(rBits, gBits, bBits, aBits); - if (!result) return NULL; - - if (cube->rBits > rBits) { - dst_reduce[0] = cube->rBits - result->rBits; - width[0] = cube->rWidth; - } else { - src_reduce[0] = result->rBits - cube->rBits; - width[0] = result->rWidth; - } - if (cube->gBits > gBits) { - dst_reduce[1] = cube->gBits - result->gBits; - width[1] = cube->gWidth; - } else { - src_reduce[1] = result->gBits - cube->gBits; - width[1] = result->gWidth; - } - if (cube->bBits > bBits) { - dst_reduce[2] = cube->bBits - result->bBits; - width[2] = cube->bWidth; - } else { - src_reduce[2] = result->bBits - cube->bBits; - width[2] = result->bWidth; - } - if (cube->aBits > aBits) { - dst_reduce[3] = cube->aBits - result->aBits; - width[3] = cube->aWidth; - } else { - src_reduce[3] = result->aBits - cube->aBits; - width[3] = result->aWidth; - } - - for (r=0; r>src_reduce[0], - g>>src_reduce[1], - b>>src_reduce[2], - a>>src_reduce[3]); - dst_pos = color_bucket_offset_pos(result, - r>>dst_reduce[0], - g>>dst_reduce[1], - b>>dst_reduce[2], - a>>dst_reduce[3]); - add_bucket_values( - &cube->buckets[src_pos], - &result->buckets[dst_pos] - ); +static ColorCube +copy_color_cube( + const ColorCube cube, + unsigned int rBits, + unsigned int gBits, + unsigned int bBits, + unsigned int aBits) { + unsigned int r, g, b, a; + long src_pos, dst_pos; + unsigned int src_reduce[4] = {0}, dst_reduce[4] = {0}; + unsigned int width[4]; + ColorCube result; + + result = new_color_cube(rBits, gBits, bBits, aBits); + if (!result) { + return NULL; + } + + if (cube->rBits > rBits) { + dst_reduce[0] = cube->rBits - result->rBits; + width[0] = cube->rWidth; + } else { + src_reduce[0] = result->rBits - cube->rBits; + width[0] = result->rWidth; + } + if (cube->gBits > gBits) { + dst_reduce[1] = cube->gBits - result->gBits; + width[1] = cube->gWidth; + } else { + src_reduce[1] = result->gBits - cube->gBits; + width[1] = result->gWidth; + } + if (cube->bBits > bBits) { + dst_reduce[2] = cube->bBits - result->bBits; + width[2] = cube->bWidth; + } else { + src_reduce[2] = result->bBits - cube->bBits; + width[2] = result->bWidth; + } + if (cube->aBits > aBits) { + dst_reduce[3] = cube->aBits - result->aBits; + width[3] = cube->aWidth; + } else { + src_reduce[3] = result->aBits - cube->aBits; + width[3] = result->aWidth; + } + + for (r = 0; r < width[0]; r++) { + for (g = 0; g < width[1]; g++) { + for (b = 0; b < width[2]; b++) { + for (a = 0; a < width[3]; a++) { + src_pos = color_bucket_offset_pos( + cube, + r >> src_reduce[0], + g >> src_reduce[1], + b >> src_reduce[2], + a >> src_reduce[3]); + dst_pos = color_bucket_offset_pos( + result, + r >> dst_reduce[0], + g >> dst_reduce[1], + b >> dst_reduce[2], + a >> dst_reduce[3]); + add_bucket_values( + &cube->buckets[src_pos], &result->buckets[dst_pos]); + } } - } - } - } - return result; + } + } + return result; } void subtract_color_buckets(ColorCube cube, ColorBucket buckets, long nBuckets) { - ColorBucket minuend, subtrahend; - long i; - Pixel p; - for (i=0; icount -= subtrahend->count; - minuend->r -= subtrahend->r; - minuend->g -= subtrahend->g; - minuend->b -= subtrahend->b; - minuend->a -= subtrahend->a; - } + ColorBucket minuend, subtrahend; + long i; + Pixel p; + for (i = 0; i < nBuckets; i++) { + subtrahend = &buckets[i]; + + // If the subtrahend contains no buckets, there is nothing to subtract. + if (subtrahend->count == 0) { + continue; + } + + avg_color_from_color_bucket(subtrahend, &p); + minuend = color_bucket_from_cube(cube, &p); + minuend->count -= subtrahend->count; + minuend->r -= subtrahend->r; + minuend->g -= subtrahend->g; + minuend->b -= subtrahend->b; + minuend->a -= subtrahend->a; + } } static void set_lookup_value(const ColorCube cube, const Pixel *p, long value) { - ColorBucket bucket = color_bucket_from_cube(cube, p); - bucket->count = value; + ColorBucket bucket = color_bucket_from_cube(cube, p); + bucket->count = value; } uint64_t lookup_color(const ColorCube cube, const Pixel *p) { - ColorBucket bucket = color_bucket_from_cube(cube, p); - return bucket->count; + ColorBucket bucket = color_bucket_from_cube(cube, p); + return bucket->count; } -void add_lookup_buckets(ColorCube cube, ColorBucket palette, long nColors, long offset) { - long i; - Pixel p; - for (i=offset; i= offset; i--) { + avg_color_from_color_bucket(&palette[i], &p); + set_lookup_value(cube, &p, i); + } } ColorBucket -combined_palette(ColorBucket bucketsA, long nBucketsA, ColorBucket bucketsB, long nBucketsB) { - ColorBucket result; - if (nBucketsA > LONG_MAX - nBucketsB || - (nBucketsA+nBucketsB) > LONG_MAX / sizeof(struct _ColorBucket)) { - return NULL; - } - /* malloc check ok, overflow check above */ - result = calloc(nBucketsA + nBucketsB, sizeof(struct _ColorBucket)); - if (!result) { - return NULL; - } - memcpy(result, bucketsA, sizeof(struct _ColorBucket) * nBucketsA); - memcpy(&result[nBucketsA], bucketsB, sizeof(struct _ColorBucket) * nBucketsB); - return result; +combined_palette( + ColorBucket bucketsA, + unsigned long nBucketsA, + ColorBucket bucketsB, + unsigned long nBucketsB) { + ColorBucket result; + if (nBucketsA > LONG_MAX - nBucketsB || + (nBucketsA + nBucketsB) > LONG_MAX / sizeof(struct _ColorBucket)) { + return NULL; + } + /* malloc check ok, overflow check above */ + result = calloc(nBucketsA + nBucketsB, sizeof(struct _ColorBucket)); + if (!result) { + return NULL; + } + memcpy(result, bucketsA, sizeof(struct _ColorBucket) * nBucketsA); + memcpy(&result[nBucketsA], bucketsB, sizeof(struct _ColorBucket) * nBucketsB); + return result; } static Pixel * create_palette_array(const ColorBucket palette, unsigned int paletteLength) { - Pixel *paletteArray; - unsigned int i; - - /* malloc check ok, calloc for overflow */ - paletteArray = calloc(paletteLength, sizeof(Pixel)); - if (!paletteArray) return NULL; - - for (i=0; i 64). - - For a quantization to 256 colors all 64 coarse colors will be used - plus the 192 most used color buckets from the fine color cube. - The average of all colors within one bucket is used as the actual - color for that bucket. - - For images with alpha the cubes gets a forth dimension, - 8x16x8x8 and 4x4x4x4. - */ - - /* create fine cube */ - fineCube = new_color_cube(cubeBits[0], cubeBits[1], - cubeBits[2], cubeBits[3]); - if (!fineCube) goto error; - for (i=0; i nQuantPixels) - nCoarseColors = nQuantPixels; - - /* how many space do we have in our palette for fine colors? */ - nFineColors = nQuantPixels - nCoarseColors; - - /* create fine color palette */ - paletteBucketsFine = create_sorted_color_palette(fineCube); - if (!paletteBucketsFine) goto error; - - /* remove the used fine colors from the coarse cube */ - subtract_color_buckets(coarseCube, paletteBucketsFine, nFineColors); - - /* did the subtraction cleared one or more coarse bucket? */ - while (nCoarseColors > count_used_color_buckets(coarseCube)) { - /* then we can use the free buckets for fine colors */ - nAlreadySubtracted = nFineColors; - nCoarseColors = count_used_color_buckets(coarseCube); - nFineColors = nQuantPixels - nCoarseColors; - subtract_color_buckets(coarseCube, &paletteBucketsFine[nAlreadySubtracted], - nFineColors-nAlreadySubtracted); - } - - /* create our palette buckets with fine and coarse combined */ - paletteBucketsCoarse = create_sorted_color_palette(coarseCube); - if (!paletteBucketsCoarse) goto error; - paletteBuckets = combined_palette(paletteBucketsCoarse, nCoarseColors, - paletteBucketsFine, nFineColors); - - free(paletteBucketsFine); - paletteBucketsFine = NULL; - free(paletteBucketsCoarse); - paletteBucketsCoarse = NULL; - if (!paletteBuckets) goto error; - - /* add all coarse colors to our coarse lookup cube. */ - coarseLookupCube = new_color_cube(cubeBits[4], cubeBits[5], - cubeBits[6], cubeBits[7]); - if (!coarseLookupCube) goto error; - add_lookup_buckets(coarseLookupCube, paletteBuckets, nCoarseColors, 0); - - /* expand coarse cube (64) to larger fine cube (4k). the value of each - coarse bucket is then present in the according 64 fine buckets. */ - lookupCube = copy_color_cube(coarseLookupCube, cubeBits[0], cubeBits[1], - cubeBits[2], cubeBits[3]); - if (!lookupCube) goto error; - - /* add fine colors to the lookup cube */ - add_lookup_buckets(lookupCube, paletteBuckets, nFineColors, nCoarseColors); - - /* create result pixels and map palette indices */ - /* malloc check ok, calloc for overflow */ - qp = calloc(nPixels, sizeof(Pixel)); - if (!qp) goto error; - map_image_pixels(pixelData, nPixels, lookupCube, qp); - - /* convert palette buckets to RGB pixel palette */ - *palette = create_palette_array(paletteBuckets, nQuantPixels); - if (!(*palette)) goto error; - - *quantizedPixels = qp; - *paletteLength = nQuantPixels; - - free_color_cube(coarseCube); - free_color_cube(fineCube); - free_color_cube(lookupCube); - free_color_cube(coarseLookupCube); - free(paletteBuckets); - return 1; +const unsigned int CUBE_LEVELS[8] = {4, 4, 4, 0, 2, 2, 2, 0}; +const unsigned int CUBE_LEVELS_ALPHA[8] = {3, 4, 3, 3, 2, 2, 2, 2}; + +int +quantize_octree( + Pixel *pixelData, + uint32_t nPixels, + uint32_t nQuantPixels, + Pixel **palette, + uint32_t *paletteLength, + uint32_t **quantizedPixels, + int withAlpha) { + ColorCube fineCube = NULL; + ColorCube coarseCube = NULL; + ColorCube lookupCube = NULL; + ColorCube coarseLookupCube = NULL; + ColorBucket paletteBucketsCoarse = NULL; + ColorBucket paletteBucketsFine = NULL; + ColorBucket paletteBuckets = NULL; + uint32_t *qp = NULL; + long i; + unsigned long nCoarseColors, nFineColors, nAlreadySubtracted; + const unsigned int *cubeBits; + + if (withAlpha) { + cubeBits = CUBE_LEVELS_ALPHA; + } else { + cubeBits = CUBE_LEVELS; + } + + /* + Create two color cubes, one fine grained with 8x16x8=1024 + colors buckets and a coarse with 4x4x4=64 color buckets. + The coarse one guarantees that there are color buckets available for + the whole color range (assuming nQuantPixels > 64). + + For a quantization to 256 colors all 64 coarse colors will be used + plus the 192 most used color buckets from the fine color cube. + The average of all colors within one bucket is used as the actual + color for that bucket. + + For images with alpha the cubes gets a forth dimension, + 8x16x8x8 and 4x4x4x4. + */ + + /* create fine cube */ + fineCube = new_color_cube(cubeBits[0], cubeBits[1], cubeBits[2], cubeBits[3]); + if (!fineCube) { + goto error; + } + for (i = 0; i < nPixels; i++) { + add_color_to_color_cube(fineCube, &pixelData[i]); + } + + /* create coarse cube */ + coarseCube = + copy_color_cube(fineCube, cubeBits[4], cubeBits[5], cubeBits[6], cubeBits[7]); + if (!coarseCube) { + goto error; + } + nCoarseColors = count_used_color_buckets(coarseCube); + + /* limit to nQuantPixels */ + if (nCoarseColors > nQuantPixels) { + nCoarseColors = nQuantPixels; + } + + /* how many space do we have in our palette for fine colors? */ + nFineColors = nQuantPixels - nCoarseColors; + + /* create fine color palette */ + paletteBucketsFine = create_sorted_color_palette(fineCube); + if (!paletteBucketsFine) { + goto error; + } + + /* remove the used fine colors from the coarse cube */ + subtract_color_buckets(coarseCube, paletteBucketsFine, nFineColors); + + /* did the subtraction cleared one or more coarse bucket? */ + while (nCoarseColors > count_used_color_buckets(coarseCube)) { + /* then we can use the free buckets for fine colors */ + nAlreadySubtracted = nFineColors; + nCoarseColors = count_used_color_buckets(coarseCube); + nFineColors = nQuantPixels - nCoarseColors; + subtract_color_buckets( + coarseCube, + &paletteBucketsFine[nAlreadySubtracted], + nFineColors - nAlreadySubtracted); + } + + /* create our palette buckets with fine and coarse combined */ + paletteBucketsCoarse = create_sorted_color_palette(coarseCube); + if (!paletteBucketsCoarse) { + goto error; + } + paletteBuckets = combined_palette( + paletteBucketsCoarse, nCoarseColors, paletteBucketsFine, nFineColors); + + free(paletteBucketsFine); + paletteBucketsFine = NULL; + free(paletteBucketsCoarse); + paletteBucketsCoarse = NULL; + if (!paletteBuckets) { + goto error; + } + + /* add all coarse colors to our coarse lookup cube. */ + coarseLookupCube = + new_color_cube(cubeBits[4], cubeBits[5], cubeBits[6], cubeBits[7]); + if (!coarseLookupCube) { + goto error; + } + add_lookup_buckets(coarseLookupCube, paletteBuckets, nCoarseColors, 0); + + /* expand coarse cube (64) to larger fine cube (4k). the value of each + coarse bucket is then present in the according 64 fine buckets. */ + lookupCube = copy_color_cube( + coarseLookupCube, cubeBits[0], cubeBits[1], cubeBits[2], cubeBits[3]); + if (!lookupCube) { + goto error; + } + + /* add fine colors to the lookup cube */ + add_lookup_buckets(lookupCube, paletteBuckets, nFineColors, nCoarseColors); + + /* create result pixels and map palette indices */ + /* malloc check ok, calloc for overflow */ + qp = calloc(nPixels, sizeof(Pixel)); + if (!qp) { + goto error; + } + map_image_pixels(pixelData, nPixels, lookupCube, qp); + + /* convert palette buckets to RGB pixel palette */ + *palette = create_palette_array(paletteBuckets, nQuantPixels); + if (!(*palette)) { + goto error; + } + + *quantizedPixels = qp; + *paletteLength = nQuantPixels; + + free_color_cube(coarseCube); + free_color_cube(fineCube); + free_color_cube(lookupCube); + free_color_cube(coarseLookupCube); + free(paletteBuckets); + return 1; error: - /* everything is initialized to NULL - so we are safe to call free */ - free(qp); - free_color_cube(lookupCube); - free_color_cube(coarseLookupCube); - free(paletteBucketsCoarse); - free(paletteBucketsFine); - free_color_cube(coarseCube); - free_color_cube(fineCube); - return 0; + /* everything is initialized to NULL + so we are safe to call free */ + free(qp); + free_color_cube(lookupCube); + free_color_cube(coarseLookupCube); + free(paletteBuckets); + free(paletteBucketsCoarse); + free(paletteBucketsFine); + free_color_cube(coarseCube); + free_color_cube(fineCube); + return 0; } diff --git a/src/libImaging/QuantOctree.h b/src/libImaging/QuantOctree.h index 968644eda02..e1c50407402 100644 --- a/src/libImaging/QuantOctree.h +++ b/src/libImaging/QuantOctree.h @@ -3,12 +3,7 @@ #include "QuantTypes.h" -int quantize_octree(Pixel *, - uint32_t, - uint32_t, - Pixel **, - uint32_t *, - uint32_t **, - int); +int +quantize_octree(Pixel *, uint32_t, uint32_t, Pixel **, uint32_t *, uint32_t **, int); #endif diff --git a/src/libImaging/QuantPngQuant.c b/src/libImaging/QuantPngQuant.c index a9a547540bf..7a36300e408 100644 --- a/src/libImaging/QuantPngQuant.c +++ b/src/libImaging/QuantPngQuant.c @@ -20,14 +20,13 @@ int quantize_pngquant( Pixel *pixelData, - int width, - int height, + unsigned int width, + unsigned int height, uint32_t quantPixels, Pixel **palette, uint32_t *paletteLength, uint32_t **quantizedPixels, - int withAlpha) -{ + int withAlpha) { int result = 0; liq_image *image = NULL; liq_attr *attr = NULL; @@ -41,23 +40,24 @@ quantize_pngquant( /* configure pngquant */ attr = liq_attr_create(); - if (!attr) { goto err; } + if (!attr) { + goto err; + } if (quantPixels) { liq_set_max_colors(attr, quantPixels); } /* prepare input image */ - image = liq_image_create_rgba( - attr, - pixelData, - width, - height, - 0.45455 /* gamma */); - if (!image) { goto err; } + image = liq_image_create_rgba(attr, pixelData, width, height, 0.45455 /* gamma */); + if (!image) { + goto err; + } /* quantize the image */ remap = liq_quantize_image(attr, image); - if (!remap) { goto err; } + if (!remap) { + goto err; + } liq_set_output_gamma(remap, 0.45455); liq_set_dithering_level(remap, 1); @@ -65,7 +65,9 @@ quantize_pngquant( const liq_palette *l_palette = liq_get_palette(remap); *paletteLength = l_palette->count; *palette = malloc(sizeof(Pixel) * l_palette->count); - if (!*palette) { goto err; } + if (!*palette) { + goto err; + } for (i = 0; i < l_palette->count; i++) { (*palette)[i].c.b = l_palette->entries[i].b; (*palette)[i].c.g = l_palette->entries[i].g; @@ -75,9 +77,13 @@ quantize_pngquant( /* write output pixels (pngquant uses char array) */ charMatrix = malloc(width * height); - if (!charMatrix) { goto err; } - charMatrixRows = malloc(height * sizeof(unsigned char*)); - if (!charMatrixRows) { goto err; } + if (!charMatrix) { + goto err; + } + charMatrixRows = malloc(height * sizeof(unsigned char *)); + if (!charMatrixRows) { + goto err; + } for (y = 0; y < height; y++) { charMatrixRows[y] = &charMatrix[y * width]; } @@ -87,7 +93,9 @@ quantize_pngquant( /* transcribe output pixels (pillow uses uint32_t array) */ *quantizedPixels = malloc(sizeof(uint32_t) * width * height); - if (!*quantizedPixels) { goto err; } + if (!*quantizedPixels) { + goto err; + } for (i = 0; i < width * height; i++) { (*quantizedPixels)[i] = charMatrix[i]; } @@ -95,16 +103,30 @@ quantize_pngquant( result = 1; err: - if (attr) liq_attr_destroy(attr); - if (image) liq_image_destroy(image); - if (remap) liq_result_destroy(remap); + if (attr) { + liq_attr_destroy(attr); + } + if (image) { + liq_image_destroy(image); + } + if (remap) { + liq_result_destroy(remap); + } free(charMatrix); free(charMatrixRows); - if (!result) { + if (!result) { free(*quantizedPixels); free(*palette); } return result; } +const char * +ImagingImageQuantVersion(void) { + static char version[20]; + int number = liq_version(); + sprintf(version, "%d.%d.%d", number / 10000, (number / 100) % 100, number % 100); + return version; +} + #endif diff --git a/src/libImaging/QuantPngQuant.h b/src/libImaging/QuantPngQuant.h index d539a7a0d24..d65e42590ca 100644 --- a/src/libImaging/QuantPngQuant.h +++ b/src/libImaging/QuantPngQuant.h @@ -3,9 +3,11 @@ #include "QuantTypes.h" -int quantize_pngquant(Pixel *, - int, - int, +int +quantize_pngquant( + Pixel *, + unsigned int, + unsigned int, uint32_t, Pixel **, uint32_t *, diff --git a/src/libImaging/QuantTypes.h b/src/libImaging/QuantTypes.h index 411485498ed..986b70806dc 100644 --- a/src/libImaging/QuantTypes.h +++ b/src/libImaging/QuantTypes.h @@ -20,13 +20,13 @@ typedef unsigned __int64 uint64_t; #endif typedef union { - struct { - unsigned char r,g,b,a; - } c; - struct { - unsigned char v[4]; - } a; - uint32_t v; + struct { + unsigned char r, g, b, a; + } c; + struct { + unsigned char v[4]; + } a; + uint32_t v; } Pixel; #endif diff --git a/src/libImaging/RankFilter.c b/src/libImaging/RankFilter.c index 0164861bb07..73a6baecbb2 100644 --- a/src/libImaging/RankFilter.c +++ b/src/libImaging/RankFilter.c @@ -17,90 +17,109 @@ /* Fast rank algorithm (due to Wirth), based on public domain code by Nicolas Devillard, available at http://ndevilla.free.fr */ -#define SWAP(type,a,b) { register type t=(a);(a)=(b);(b)=t; } - -#define MakeRankFunction(type)\ -static type Rank##type(type a[], int n, int k)\ -{\ - register int i, j, l, m;\ - register type x;\ - l = 0; m = n-1;\ - while (l < m) {\ - x = a[k];\ - i = l;\ - j = m;\ - do {\ - while (a[i] < x) i++;\ - while (x < a[j]) j--;\ - if (i <= j) {\ - SWAP(type, a[i], a[j]);\ - i++; j--;\ - }\ - } while (i <= j);\ - if (j < k) l = i;\ - if (k < i) m = j;\ - }\ - return a[k];\ -} +#define SWAP(type, a, b) \ + { \ + register type t = (a); \ + (a) = (b); \ + (b) = t; \ + } + +#define MakeRankFunction(type) \ + static type Rank##type(type a[], int n, int k) { \ + register int i, j, l, m; \ + register type x; \ + l = 0; \ + m = n - 1; \ + while (l < m) { \ + x = a[k]; \ + i = l; \ + j = m; \ + do { \ + while (a[i] < x) { \ + i++; \ + } \ + while (x < a[j]) { \ + j--; \ + } \ + if (i <= j) { \ + SWAP(type, a[i], a[j]); \ + i++; \ + j--; \ + } \ + } while (i <= j); \ + if (j < k) { \ + l = i; \ + } \ + if (k < i) { \ + m = j; \ + } \ + } \ + return a[k]; \ + } -MakeRankFunction(UINT8) -MakeRankFunction(INT32) -MakeRankFunction(FLOAT32) +MakeRankFunction(UINT8) MakeRankFunction(INT32) MakeRankFunction(FLOAT32) -Imaging -ImagingRankFilter(Imaging im, int size, int rank) -{ + Imaging ImagingRankFilter(Imaging im, int size, int rank) { Imaging imOut = NULL; int x, y; int i, margin, size2; - if (!im || im->bands != 1 || im->type == IMAGING_TYPE_SPECIAL) - return (Imaging) ImagingError_ModeError(); + if (!im || im->bands != 1 || im->type == IMAGING_TYPE_SPECIAL) { + return (Imaging)ImagingError_ModeError(); + } - if (!(size & 1)) - return (Imaging) ImagingError_ValueError("bad filter size"); + if (!(size & 1)) { + return (Imaging)ImagingError_ValueError("bad filter size"); + } /* malloc check ok, for overflow in the define below */ - if (size > INT_MAX / size || - size > INT_MAX / (size * sizeof(FLOAT32))) { - return (Imaging) ImagingError_ValueError("filter size too large"); + if (size > INT_MAX / size || size > INT_MAX / (size * (int)sizeof(FLOAT32))) { + return (Imaging)ImagingError_ValueError("filter size too large"); } size2 = size * size; - margin = (size-1) / 2; + margin = (size - 1) / 2; - if (rank < 0 || rank >= size2) - return (Imaging) ImagingError_ValueError("bad rank value"); + if (rank < 0 || rank >= size2) { + return (Imaging)ImagingError_ValueError("bad rank value"); + } - imOut = ImagingNew(im->mode, im->xsize - 2*margin, im->ysize - 2*margin); - if (!imOut) + imOut = ImagingNew(im->mode, im->xsize - 2 * margin, im->ysize - 2 * margin); + if (!imOut) { return NULL; + } /* malloc check ok, checked above */ -#define RANK_BODY(type) do {\ - type* buf = malloc(size2 * sizeof(type));\ - if (!buf)\ - goto nomemory;\ - for (y = 0; y < imOut->ysize; y++)\ - for (x = 0; x < imOut->xsize; x++) {\ - for (i = 0; i < size; i++)\ - memcpy(buf + i*size, &IMAGING_PIXEL_##type(im, x, y+i),\ - size * sizeof(type));\ - IMAGING_PIXEL_##type(imOut, x, y) = Rank##type(buf, size2, rank);\ - }\ - free(buf); \ -} while (0) - - if (im->image8) +#define RANK_BODY(type) \ + do { \ + type *buf = malloc(size2 * sizeof(type)); \ + if (!buf) { \ + goto nomemory; \ + } \ + for (y = 0; y < imOut->ysize; y++) { \ + for (x = 0; x < imOut->xsize; x++) { \ + for (i = 0; i < size; i++) { \ + memcpy( \ + buf + i * size, \ + &IMAGING_PIXEL_##type(im, x, y + i), \ + size * sizeof(type)); \ + } \ + IMAGING_PIXEL_##type(imOut, x, y) = Rank##type(buf, size2, rank); \ + } \ + } \ + free(buf); \ + } while (0) + + if (im->image8) { RANK_BODY(UINT8); - else if (im->type == IMAGING_TYPE_INT32) + } else if (im->type == IMAGING_TYPE_INT32) { RANK_BODY(INT32); - else if (im->type == IMAGING_TYPE_FLOAT32) + } else if (im->type == IMAGING_TYPE_FLOAT32) { RANK_BODY(FLOAT32); - else { + } else { /* safety net (we shouldn't end up here) */ ImagingDelete(imOut); - return (Imaging) ImagingError_ModeError(); + return (Imaging)ImagingError_ModeError(); } ImagingCopyPalette(imOut, im); @@ -109,5 +128,5 @@ ImagingRankFilter(Imaging im, int size, int rank) nomemory: ImagingDelete(imOut); - return (Imaging) ImagingError_MemoryError(); + return (Imaging)ImagingError_MemoryError(); } diff --git a/src/libImaging/Raw.h b/src/libImaging/Raw.h index 4d28fa54665..ab718837f4a 100644 --- a/src/libImaging/Raw.h +++ b/src/libImaging/Raw.h @@ -1,7 +1,6 @@ /* Raw.h */ typedef struct { - /* CONFIGURATION */ /* Distance between lines (0=no padding) */ diff --git a/src/libImaging/RawDecode.c b/src/libImaging/RawDecode.c index 40c0cb79a41..24abe48041f 100644 --- a/src/libImaging/RawDecode.c +++ b/src/libImaging/RawDecode.c @@ -5,7 +5,7 @@ * decoder for raw (uncompressed) image data * * history: - * 96-03-07 fl rewritten + * 96-03-07 fl rewritten * * Copyright (c) Fredrik Lundh 1996. * Copyright (c) Secret Labs AB 1997. @@ -13,77 +13,79 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" #include "Raw.h" - int -ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ +ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { enum { LINE = 1, SKIP }; - RAWSTATE* rawstate = state->context; + RAWSTATE *rawstate = state->context; - UINT8* ptr; + UINT8 *ptr; if (state->state == 0) { - - /* Initialize context variables */ - - /* get size of image data and padding */ - state->bytes = (state->xsize * state->bits + 7) / 8; - rawstate->skip = (rawstate->stride) ? - rawstate->stride - state->bytes : 0; - - /* check image orientation */ - if (state->ystep < 0) { - state->y = state->ysize-1; - state->ystep = -1; - } else - state->ystep = 1; - - state->state = LINE; - + /* Initialize context variables */ + + /* get size of image data and padding */ + state->bytes = (state->xsize * state->bits + 7) / 8; + if (rawstate->stride) { + rawstate->skip = rawstate->stride - state->bytes; + if (rawstate->skip < 0) { + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + } else { + rawstate->skip = 0; + } + + /* check image orientation */ + if (state->ystep < 0) { + state->y = state->ysize - 1; + state->ystep = -1; + } else { + state->ystep = 1; + } + + state->state = LINE; } ptr = buf; for (;;) { + if (state->state == SKIP) { + /* Skip padding between lines */ - if (state->state == SKIP) { - - /* Skip padding between lines */ - - if (bytes < rawstate->skip) - return ptr - buf; + if (bytes < rawstate->skip) { + return ptr - buf; + } - ptr += rawstate->skip; - bytes -= rawstate->skip; + ptr += rawstate->skip; + bytes -= rawstate->skip; - state->state = LINE; + state->state = LINE; + } - } + if (bytes < state->bytes) { + return ptr - buf; + } - if (bytes < state->bytes) - return ptr - buf; + /* Unpack data */ + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, + ptr, + state->xsize); - /* Unpack data */ - state->shuffle((UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, ptr, state->xsize); + ptr += state->bytes; + bytes -= state->bytes; - ptr += state->bytes; - bytes -= state->bytes; + state->y += state->ystep; - state->y += state->ystep; - - if (state->y < 0 || state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } - - state->state = SKIP; + if (state->y < 0 || state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } + state->state = SKIP; } - } diff --git a/src/libImaging/RawEncode.c b/src/libImaging/RawEncode.c index a3b74b8cffc..50de8d98275 100644 --- a/src/libImaging/RawEncode.c +++ b/src/libImaging/RawEncode.c @@ -9,81 +9,79 @@ * in ImageFile.py, but it should be solved here instead. * * history: - * 96-04-30 fl created - * 97-01-03 fl fixed padding + * 96-04-30 fl created + * 97-01-03 fl fixed padding * * Copyright (c) Fredrik Lundh 1996-97. * Copyright (c) Secret Labs AB 1997. * * See the README file for information on usage and redistribution. */ - #include "Imaging.h" int -ImagingRawEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ - UINT8* ptr; +ImagingRawEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + UINT8 *ptr; if (!state->state) { - - /* The "count" field holds the stride, if specified. Fix - things up so "bytes" is the full size, and "count" the - packed size */ - - if (state->count > 0) { - int bytes = state->count; - - /* stride must not be less than real size */ - if (state->count < state->bytes) { - state->errcode = IMAGING_CODEC_CONFIG; - return -1; - } - state->count = state->bytes; - state->bytes = bytes; - } else - state->count = state->bytes; - - /* The "ystep" field specifies the orientation */ - - if (state->ystep < 0) { - state->y = state->ysize-1; - state->ystep = -1; - } else - state->ystep = 1; - - state->state = 1; - + /* The "count" field holds the stride, if specified. Fix + things up so "bytes" is the full size, and "count" the + packed size */ + + if (state->count > 0) { + int bytes = state->count; + + /* stride must not be less than real size */ + if (state->count < state->bytes) { + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + state->count = state->bytes; + state->bytes = bytes; + } else { + state->count = state->bytes; + } + + /* The "ystep" field specifies the orientation */ + + if (state->ystep < 0) { + state->y = state->ysize - 1; + state->ystep = -1; + } else { + state->ystep = 1; + } + + state->state = 1; } if (bytes < state->bytes) { - state->errcode = IMAGING_CODEC_CONFIG; - return 0; + state->errcode = IMAGING_CODEC_CONFIG; + return 0; } ptr = buf; while (bytes >= state->bytes) { + state->shuffle( + ptr, + (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, + state->xsize); - state->shuffle(ptr, (UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->xsize); - - if (state->bytes > state->count) - /* zero-pad the buffer, if necessary */ - memset(ptr + state->count, 0, state->bytes - state->count); + if (state->bytes > state->count) { + /* zero-pad the buffer, if necessary */ + memset(ptr + state->count, 0, state->bytes - state->count); + } - ptr += state->bytes; - bytes -= state->bytes; + ptr += state->bytes; + bytes -= state->bytes; - state->y += state->ystep; - - if (state->y < 0 || state->y >= state->ysize) { - state->errcode = IMAGING_CODEC_END; - break; - } + state->y += state->ystep; + if (state->y < 0 || state->y >= state->ysize) { + state->errcode = IMAGING_CODEC_END; + break; + } } return ptr - buf; - } diff --git a/src/libImaging/Reduce.c b/src/libImaging/Reduce.c new file mode 100644 index 00000000000..60928d2bc36 --- /dev/null +++ b/src/libImaging/Reduce.c @@ -0,0 +1,1483 @@ +#include "Imaging.h" + +#include + +#define ROUND_UP(f) ((int)((f) >= 0.0 ? (f) + 0.5F : (f)-0.5F)) + +UINT32 +division_UINT32(int divider, int result_bits) { + UINT32 max_dividend = (1 << result_bits) * divider; + float max_int = (1 << 30) * 4.0; + return (UINT32)(max_int / max_dividend); +} + +void +ImagingReduceNxN(Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) { + /* The most general implementation for any xscale and yscale + */ + int x, y, xx, yy; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 ss = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image8[yy]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line0[xx + 0] + line0[xx + 1] + line1[xx + 0] + + line1[xx + 1]; + } + if (xscale & 0x01) { + ss += line0[xx + 0] + line1[xx + 0]; + } + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image8[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line[xx + 0] + line[xx + 1]; + } + if (xscale & 0x01) { + ss += line[xx + 0]; + } + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss3 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line0[xx * 4 + 0] + line0[xx * 4 + 4] + + line1[xx * 4 + 0] + line1[xx * 4 + 4]; + ss3 += line0[xx * 4 + 3] + line0[xx * 4 + 7] + + line1[xx * 4 + 3] + line1[xx * 4 + 7]; + } + if (xscale & 0x01) { + ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss3 += line0[xx * 4 + 3] + line1[xx * 4 + 3]; + } + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; + ss3 += line[xx * 4 + 3] + line[xx * 4 + 7]; + } + if (xscale & 0x01) { + ss0 += line[xx * 4 + 0]; + ss3 += line[xx * 4 + 3]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line0[xx * 4 + 0] + line0[xx * 4 + 4] + + line1[xx * 4 + 0] + line1[xx * 4 + 4]; + ss1 += line0[xx * 4 + 1] + line0[xx * 4 + 5] + + line1[xx * 4 + 1] + line1[xx * 4 + 5]; + ss2 += line0[xx * 4 + 2] + line0[xx * 4 + 6] + + line1[xx * 4 + 2] + line1[xx * 4 + 6]; + } + if (xscale & 0x01) { + ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss1 += line0[xx * 4 + 1] + line1[xx * 4 + 1]; + ss2 += line0[xx * 4 + 2] + line1[xx * 4 + 2]; + } + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; + ss1 += line[xx * 4 + 1] + line[xx * 4 + 5]; + ss2 += line[xx * 4 + 2] + line[xx * 4 + 6]; + } + if (xscale & 0x01) { + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line0[xx * 4 + 0] + line0[xx * 4 + 4] + + line1[xx * 4 + 0] + line1[xx * 4 + 4]; + ss1 += line0[xx * 4 + 1] + line0[xx * 4 + 5] + + line1[xx * 4 + 1] + line1[xx * 4 + 5]; + ss2 += line0[xx * 4 + 2] + line0[xx * 4 + 6] + + line1[xx * 4 + 2] + line1[xx * 4 + 6]; + ss3 += line0[xx * 4 + 3] + line0[xx * 4 + 7] + + line1[xx * 4 + 3] + line1[xx * 4 + 7]; + } + if (xscale & 0x01) { + ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss1 += line0[xx * 4 + 1] + line1[xx * 4 + 1]; + ss2 += line0[xx * 4 + 2] + line1[xx * 4 + 2]; + ss3 += line0[xx * 4 + 3] + line1[xx * 4 + 3]; + } + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; + ss1 += line[xx * 4 + 1] + line[xx * 4 + 5]; + ss2 += line[xx * 4 + 2] + line[xx * 4 + 6]; + ss3 += line[xx * 4 + 3] + line[xx * 4 + 7]; + } + if (xscale & 0x01) { + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + ss3 += line[xx * 4 + 3]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce1xN(Imaging imOut, Imaging imIn, int box[4], int yscale) { + /* Optimized implementation for xscale = 1. + */ + int x, y, yy; + int xscale = 1; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 ss = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image8[yy]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + ss += line0[xx + 0] + line1[xx + 0]; + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image8[yy]; + ss += line[xx + 0]; + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss3 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss3 += line0[xx * 4 + 3] + line1[xx * 4 + 3]; + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + ss0 += line[xx * 4 + 0]; + ss3 += line[xx * 4 + 3]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss1 += line0[xx * 4 + 1] + line1[xx * 4 + 1]; + ss2 += line0[xx * 4 + 2] + line1[xx * 4 + 2]; + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + UINT8 *line0 = (UINT8 *)imIn->image[yy]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + ss0 += line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss1 += line0[xx * 4 + 1] + line1[xx * 4 + 1]; + ss2 += line0[xx * 4 + 2] + line1[xx * 4 + 2]; + ss3 += line0[xx * 4 + 3] + line1[xx * 4 + 3]; + } + if (yscale & 0x01) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + ss3 += line[xx * 4 + 3]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduceNx1(Imaging imOut, Imaging imIn, int box[4], int xscale) { + /* Optimized implementation for yscale = 1. + */ + int x, y, xx; + int yscale = 1; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line = (UINT8 *)imIn->image8[yy]; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 ss = amend; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line[xx + 0] + line[xx + 1]; + } + if (xscale & 0x01) { + ss += line[xx + 0]; + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line = (UINT8 *)imIn->image[yy]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss3 = amend; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; + ss3 += line[xx * 4 + 3] + line[xx * 4 + 7]; + } + if (xscale & 0x01) { + ss0 += line[xx * 4 + 0]; + ss3 += line[xx * 4 + 3]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; + ss1 += line[xx * 4 + 1] + line[xx * 4 + 5]; + ss2 += line[xx * 4 + 2] + line[xx * 4 + 6]; + } + if (xscale & 0x01) { + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss0 += line[xx * 4 + 0] + line[xx * 4 + 4]; + ss1 += line[xx * 4 + 1] + line[xx * 4 + 5]; + ss2 += line[xx * 4 + 2] + line[xx * 4 + 6]; + ss3 += line[xx * 4 + 3] + line[xx * 4 + 7]; + } + if (xscale & 0x01) { + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + ss3 += line[xx * 4 + 3]; + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce1x2(Imaging imOut, Imaging imIn, int box[4]) { + /* Optimized implementation for xscale = 1 and yscale = 2. + */ + int xscale = 1, yscale = 2; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + ss0 = line0[xx + 0] + line1[xx + 0]; + imOut->image8[y][x] = (ss0 + amend) >> 1; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss3 = line0[xx * 4 + 3] + line1[xx * 4 + 3]; + v = MAKE_UINT32((ss0 + amend) >> 1, 0, 0, (ss3 + amend) >> 1); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss1 = line0[xx * 4 + 1] + line1[xx * 4 + 1]; + ss2 = line0[xx * 4 + 2] + line1[xx * 4 + 2]; + v = MAKE_UINT32( + (ss0 + amend) >> 1, (ss1 + amend) >> 1, (ss2 + amend) >> 1, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0]; + ss1 = line0[xx * 4 + 1] + line1[xx * 4 + 1]; + ss2 = line0[xx * 4 + 2] + line1[xx * 4 + 2]; + ss3 = line0[xx * 4 + 3] + line1[xx * 4 + 3]; + v = MAKE_UINT32( + (ss0 + amend) >> 1, + (ss1 + amend) >> 1, + (ss2 + amend) >> 1, + (ss3 + amend) >> 1); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce2x1(Imaging imOut, Imaging imIn, int box[4]) { + /* Optimized implementation for xscale = 2 and yscale = 1. + */ + int xscale = 2, yscale = 1; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + ss0 = line0[xx + 0] + line0[xx + 1]; + imOut->image8[y][x] = (ss0 + amend) >> 1; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7]; + v = MAKE_UINT32((ss0 + amend) >> 1, 0, 0, (ss3 + amend) >> 1); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6]; + v = MAKE_UINT32( + (ss0 + amend) >> 1, (ss1 + amend) >> 1, (ss2 + amend) >> 1, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7]; + v = MAKE_UINT32( + (ss0 + amend) >> 1, + (ss1 + amend) >> 1, + (ss2 + amend) >> 1, + (ss3 + amend) >> 1); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce2x2(Imaging imOut, Imaging imIn, int box[4]) { + /* Optimized implementation for xscale = 2 and yscale = 2. + */ + int xscale = 2, yscale = 2; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + ss0 = line0[xx + 0] + line0[xx + 1] + line1[xx + 0] + line1[xx + 1]; + imOut->image8[y][x] = (ss0 + amend) >> 2; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line1[xx * 4 + 0] + + line1[xx * 4 + 4]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line1[xx * 4 + 3] + + line1[xx * 4 + 7]; + v = MAKE_UINT32((ss0 + amend) >> 2, 0, 0, (ss3 + amend) >> 2); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line1[xx * 4 + 0] + + line1[xx * 4 + 4]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line1[xx * 4 + 1] + + line1[xx * 4 + 5]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line1[xx * 4 + 2] + + line1[xx * 4 + 6]; + v = MAKE_UINT32( + (ss0 + amend) >> 2, (ss1 + amend) >> 2, (ss2 + amend) >> 2, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line1[xx * 4 + 0] + + line1[xx * 4 + 4]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line1[xx * 4 + 1] + + line1[xx * 4 + 5]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line1[xx * 4 + 2] + + line1[xx * 4 + 6]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line1[xx * 4 + 3] + + line1[xx * 4 + 7]; + v = MAKE_UINT32( + (ss0 + amend) >> 2, + (ss1 + amend) >> 2, + (ss2 + amend) >> 2, + (ss3 + amend) >> 2); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce1x3(Imaging imOut, Imaging imIn, int box[4]) { + /* Optimized implementation for xscale = 1 and yscale = 3. + */ + int xscale = 1, yscale = 3; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image8[yy + 2]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + ss0 = line0[xx + 0] + line1[xx + 0] + line2[xx + 0]; + imOut->image8[y][x] = ((ss0 + amend) * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image[yy + 2]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0] + line2[xx * 4 + 0]; + ss3 = line0[xx * 4 + 3] + line1[xx * 4 + 3] + line2[xx * 4 + 3]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + 0, + 0, + ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0] + line2[xx * 4 + 0]; + ss1 = line0[xx * 4 + 1] + line1[xx * 4 + 1] + line2[xx * 4 + 1]; + ss2 = line0[xx * 4 + 2] + line1[xx * 4 + 2] + line2[xx * 4 + 2]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, + 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line1[xx * 4 + 0] + line2[xx * 4 + 0]; + ss1 = line0[xx * 4 + 1] + line1[xx * 4 + 1] + line2[xx * 4 + 1]; + ss2 = line0[xx * 4 + 2] + line1[xx * 4 + 2] + line2[xx * 4 + 2]; + ss3 = line0[xx * 4 + 3] + line1[xx * 4 + 3] + line2[xx * 4 + 3]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, + ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce3x1(Imaging imOut, Imaging imIn, int box[4]) { + /* Optimized implementation for xscale = 3 and yscale = 1. + */ + int xscale = 3, yscale = 1; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + ss0 = line0[xx + 0] + line0[xx + 1] + line0[xx + 2]; + imOut->image8[y][x] = ((ss0 + amend) * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + 0, + 0, + ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, + 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, + ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce3x3(Imaging imOut, Imaging imIn, int box[4]) { + /* Optimized implementation for xscale = 3 and yscale = 3. + */ + int xscale = 3, yscale = 3; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image8[yy + 2]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + ss0 = line0[xx + 0] + line0[xx + 1] + line0[xx + 2] + line1[xx + 0] + + line1[xx + 1] + line1[xx + 2] + line2[xx + 0] + line2[xx + 1] + + line2[xx + 2]; + imOut->image8[y][x] = ((ss0 + amend) * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image[yy + 2]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line1[xx * 4 + 0] + line1[xx * 4 + 4] + line1[xx * 4 + 8] + + line2[xx * 4 + 0] + line2[xx * 4 + 4] + line2[xx * 4 + 8]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + + line1[xx * 4 + 3] + line1[xx * 4 + 7] + line1[xx * 4 + 11] + + line2[xx * 4 + 3] + line2[xx * 4 + 7] + line2[xx * 4 + 11]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + 0, + 0, + ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line1[xx * 4 + 0] + line1[xx * 4 + 4] + line1[xx * 4 + 8] + + line2[xx * 4 + 0] + line2[xx * 4 + 4] + line2[xx * 4 + 8]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + + line1[xx * 4 + 1] + line1[xx * 4 + 5] + line1[xx * 4 + 9] + + line2[xx * 4 + 1] + line2[xx * 4 + 5] + line2[xx * 4 + 9]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + + line1[xx * 4 + 2] + line1[xx * 4 + 6] + line1[xx * 4 + 10] + + line2[xx * 4 + 2] + line2[xx * 4 + 6] + line2[xx * 4 + 10]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, + 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line1[xx * 4 + 0] + line1[xx * 4 + 4] + line1[xx * 4 + 8] + + line2[xx * 4 + 0] + line2[xx * 4 + 4] + line2[xx * 4 + 8]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + + line1[xx * 4 + 1] + line1[xx * 4 + 5] + line1[xx * 4 + 9] + + line2[xx * 4 + 1] + line2[xx * 4 + 5] + line2[xx * 4 + 9]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + + line1[xx * 4 + 2] + line1[xx * 4 + 6] + line1[xx * 4 + 10] + + line2[xx * 4 + 2] + line2[xx * 4 + 6] + line2[xx * 4 + 10]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + + line1[xx * 4 + 3] + line1[xx * 4 + 7] + line1[xx * 4 + 11] + + line2[xx * 4 + 3] + line2[xx * 4 + 7] + line2[xx * 4 + 11]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, + ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce4x4(Imaging imOut, Imaging imIn, int box[4]) { + /* Optimized implementation for xscale = 4 and yscale = 4. + */ + int xscale = 4, yscale = 4; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image8[yy + 2]; + UINT8 *line3 = (UINT8 *)imIn->image8[yy + 3]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + ss0 = line0[xx + 0] + line0[xx + 1] + line0[xx + 2] + line0[xx + 3] + + line1[xx + 0] + line1[xx + 1] + line1[xx + 2] + line1[xx + 3] + + line2[xx + 0] + line2[xx + 1] + line2[xx + 2] + line2[xx + 3] + + line3[xx + 0] + line3[xx + 1] + line3[xx + 2] + line3[xx + 3]; + imOut->image8[y][x] = (ss0 + amend) >> 4; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image[yy + 2]; + UINT8 *line3 = (UINT8 *)imIn->image[yy + 3]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line0[xx * 4 + 12] + line1[xx * 4 + 0] + line1[xx * 4 + 4] + + line1[xx * 4 + 8] + line1[xx * 4 + 12] + line2[xx * 4 + 0] + + line2[xx * 4 + 4] + line2[xx * 4 + 8] + line2[xx * 4 + 12] + + line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + + line3[xx * 4 + 12]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + + line0[xx * 4 + 15] + line1[xx * 4 + 3] + line1[xx * 4 + 7] + + line1[xx * 4 + 11] + line1[xx * 4 + 15] + line2[xx * 4 + 3] + + line2[xx * 4 + 7] + line2[xx * 4 + 11] + line2[xx * 4 + 15] + + line3[xx * 4 + 3] + line3[xx * 4 + 7] + line3[xx * 4 + 11] + + line3[xx * 4 + 15]; + v = MAKE_UINT32((ss0 + amend) >> 4, 0, 0, (ss3 + amend) >> 4); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line0[xx * 4 + 12] + line1[xx * 4 + 0] + line1[xx * 4 + 4] + + line1[xx * 4 + 8] + line1[xx * 4 + 12] + line2[xx * 4 + 0] + + line2[xx * 4 + 4] + line2[xx * 4 + 8] + line2[xx * 4 + 12] + + line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + + line3[xx * 4 + 12]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + + line0[xx * 4 + 13] + line1[xx * 4 + 1] + line1[xx * 4 + 5] + + line1[xx * 4 + 9] + line1[xx * 4 + 13] + line2[xx * 4 + 1] + + line2[xx * 4 + 5] + line2[xx * 4 + 9] + line2[xx * 4 + 13] + + line3[xx * 4 + 1] + line3[xx * 4 + 5] + line3[xx * 4 + 9] + + line3[xx * 4 + 13]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + + line0[xx * 4 + 14] + line1[xx * 4 + 2] + line1[xx * 4 + 6] + + line1[xx * 4 + 10] + line1[xx * 4 + 14] + line2[xx * 4 + 2] + + line2[xx * 4 + 6] + line2[xx * 4 + 10] + line2[xx * 4 + 14] + + line3[xx * 4 + 2] + line3[xx * 4 + 6] + line3[xx * 4 + 10] + + line3[xx * 4 + 14]; + v = MAKE_UINT32( + (ss0 + amend) >> 4, (ss1 + amend) >> 4, (ss2 + amend) >> 4, 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line0[xx * 4 + 12] + line1[xx * 4 + 0] + line1[xx * 4 + 4] + + line1[xx * 4 + 8] + line1[xx * 4 + 12] + line2[xx * 4 + 0] + + line2[xx * 4 + 4] + line2[xx * 4 + 8] + line2[xx * 4 + 12] + + line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + + line3[xx * 4 + 12]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + + line0[xx * 4 + 13] + line1[xx * 4 + 1] + line1[xx * 4 + 5] + + line1[xx * 4 + 9] + line1[xx * 4 + 13] + line2[xx * 4 + 1] + + line2[xx * 4 + 5] + line2[xx * 4 + 9] + line2[xx * 4 + 13] + + line3[xx * 4 + 1] + line3[xx * 4 + 5] + line3[xx * 4 + 9] + + line3[xx * 4 + 13]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + + line0[xx * 4 + 14] + line1[xx * 4 + 2] + line1[xx * 4 + 6] + + line1[xx * 4 + 10] + line1[xx * 4 + 14] + line2[xx * 4 + 2] + + line2[xx * 4 + 6] + line2[xx * 4 + 10] + line2[xx * 4 + 14] + + line3[xx * 4 + 2] + line3[xx * 4 + 6] + line3[xx * 4 + 10] + + line3[xx * 4 + 14]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + + line0[xx * 4 + 15] + line1[xx * 4 + 3] + line1[xx * 4 + 7] + + line1[xx * 4 + 11] + line1[xx * 4 + 15] + line2[xx * 4 + 3] + + line2[xx * 4 + 7] + line2[xx * 4 + 11] + line2[xx * 4 + 15] + + line3[xx * 4 + 3] + line3[xx * 4 + 7] + line3[xx * 4 + 11] + + line3[xx * 4 + 15]; + v = MAKE_UINT32( + (ss0 + amend) >> 4, + (ss1 + amend) >> 4, + (ss2 + amend) >> 4, + (ss3 + amend) >> 4); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduce5x5(Imaging imOut, Imaging imIn, int box[4]) { + /* Fast special case for xscale = 5 and yscale = 5. + */ + int xscale = 5, yscale = 5; + int x, y; + UINT32 ss0, ss1, ss2, ss3; + UINT32 multiplier = division_UINT32(yscale * xscale, 8); + UINT32 amend = yscale * xscale / 2; + + if (imIn->image8) { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image8[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image8[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image8[yy + 2]; + UINT8 *line3 = (UINT8 *)imIn->image8[yy + 3]; + UINT8 *line4 = (UINT8 *)imIn->image8[yy + 4]; + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + ss0 = line0[xx + 0] + line0[xx + 1] + line0[xx + 2] + line0[xx + 3] + + line0[xx + 4] + line1[xx + 0] + line1[xx + 1] + line1[xx + 2] + + line1[xx + 3] + line1[xx + 4] + line2[xx + 0] + line2[xx + 1] + + line2[xx + 2] + line2[xx + 3] + line2[xx + 4] + line3[xx + 0] + + line3[xx + 1] + line3[xx + 2] + line3[xx + 3] + line3[xx + 4] + + line4[xx + 0] + line4[xx + 1] + line4[xx + 2] + line4[xx + 3] + + line4[xx + 4]; + imOut->image8[y][x] = ((ss0 + amend) * multiplier) >> 24; + } + } + } else { + for (y = 0; y < box[3] / yscale; y++) { + int yy = box[1] + y * yscale; + UINT8 *line0 = (UINT8 *)imIn->image[yy + 0]; + UINT8 *line1 = (UINT8 *)imIn->image[yy + 1]; + UINT8 *line2 = (UINT8 *)imIn->image[yy + 2]; + UINT8 *line3 = (UINT8 *)imIn->image[yy + 3]; + UINT8 *line4 = (UINT8 *)imIn->image[yy + 4]; + if (imIn->bands == 2) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line0[xx * 4 + 12] + line0[xx * 4 + 16] + line1[xx * 4 + 0] + + line1[xx * 4 + 4] + line1[xx * 4 + 8] + line1[xx * 4 + 12] + + line1[xx * 4 + 16] + line2[xx * 4 + 0] + line2[xx * 4 + 4] + + line2[xx * 4 + 8] + line2[xx * 4 + 12] + line2[xx * 4 + 16] + + line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + + line3[xx * 4 + 12] + line3[xx * 4 + 16] + line4[xx * 4 + 0] + + line4[xx * 4 + 4] + line4[xx * 4 + 8] + line4[xx * 4 + 12] + + line4[xx * 4 + 16]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + + line0[xx * 4 + 15] + line0[xx * 4 + 19] + line1[xx * 4 + 3] + + line1[xx * 4 + 7] + line1[xx * 4 + 11] + line1[xx * 4 + 15] + + line1[xx * 4 + 19] + line2[xx * 4 + 3] + line2[xx * 4 + 7] + + line2[xx * 4 + 11] + line2[xx * 4 + 15] + line2[xx * 4 + 19] + + line3[xx * 4 + 3] + line3[xx * 4 + 7] + line3[xx * 4 + 11] + + line3[xx * 4 + 15] + line3[xx * 4 + 19] + line4[xx * 4 + 3] + + line4[xx * 4 + 7] + line4[xx * 4 + 11] + line4[xx * 4 + 15] + + line4[xx * 4 + 19]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + 0, + 0, + ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else if (imIn->bands == 3) { + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line0[xx * 4 + 12] + line0[xx * 4 + 16] + line1[xx * 4 + 0] + + line1[xx * 4 + 4] + line1[xx * 4 + 8] + line1[xx * 4 + 12] + + line1[xx * 4 + 16] + line2[xx * 4 + 0] + line2[xx * 4 + 4] + + line2[xx * 4 + 8] + line2[xx * 4 + 12] + line2[xx * 4 + 16] + + line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + + line3[xx * 4 + 12] + line3[xx * 4 + 16] + line4[xx * 4 + 0] + + line4[xx * 4 + 4] + line4[xx * 4 + 8] + line4[xx * 4 + 12] + + line4[xx * 4 + 16]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + + line0[xx * 4 + 13] + line0[xx * 4 + 17] + line1[xx * 4 + 1] + + line1[xx * 4 + 5] + line1[xx * 4 + 9] + line1[xx * 4 + 13] + + line1[xx * 4 + 17] + line2[xx * 4 + 1] + line2[xx * 4 + 5] + + line2[xx * 4 + 9] + line2[xx * 4 + 13] + line2[xx * 4 + 17] + + line3[xx * 4 + 1] + line3[xx * 4 + 5] + line3[xx * 4 + 9] + + line3[xx * 4 + 13] + line3[xx * 4 + 17] + line4[xx * 4 + 1] + + line4[xx * 4 + 5] + line4[xx * 4 + 9] + line4[xx * 4 + 13] + + line4[xx * 4 + 17]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + + line0[xx * 4 + 14] + line0[xx * 4 + 18] + line1[xx * 4 + 2] + + line1[xx * 4 + 6] + line1[xx * 4 + 10] + line1[xx * 4 + 14] + + line1[xx * 4 + 18] + line2[xx * 4 + 2] + line2[xx * 4 + 6] + + line2[xx * 4 + 10] + line2[xx * 4 + 14] + line2[xx * 4 + 18] + + line3[xx * 4 + 2] + line3[xx * 4 + 6] + line3[xx * 4 + 10] + + line3[xx * 4 + 14] + line3[xx * 4 + 18] + line4[xx * 4 + 2] + + line4[xx * 4 + 6] + line4[xx * 4 + 10] + line4[xx * 4 + 14] + + line4[xx * 4 + 18]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, + 0); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } else { // bands == 4 + for (x = 0; x < box[2] / xscale; x++) { + int xx = box[0] + x * xscale; + UINT32 v; + ss0 = line0[xx * 4 + 0] + line0[xx * 4 + 4] + line0[xx * 4 + 8] + + line0[xx * 4 + 12] + line0[xx * 4 + 16] + line1[xx * 4 + 0] + + line1[xx * 4 + 4] + line1[xx * 4 + 8] + line1[xx * 4 + 12] + + line1[xx * 4 + 16] + line2[xx * 4 + 0] + line2[xx * 4 + 4] + + line2[xx * 4 + 8] + line2[xx * 4 + 12] + line2[xx * 4 + 16] + + line3[xx * 4 + 0] + line3[xx * 4 + 4] + line3[xx * 4 + 8] + + line3[xx * 4 + 12] + line3[xx * 4 + 16] + line4[xx * 4 + 0] + + line4[xx * 4 + 4] + line4[xx * 4 + 8] + line4[xx * 4 + 12] + + line4[xx * 4 + 16]; + ss1 = line0[xx * 4 + 1] + line0[xx * 4 + 5] + line0[xx * 4 + 9] + + line0[xx * 4 + 13] + line0[xx * 4 + 17] + line1[xx * 4 + 1] + + line1[xx * 4 + 5] + line1[xx * 4 + 9] + line1[xx * 4 + 13] + + line1[xx * 4 + 17] + line2[xx * 4 + 1] + line2[xx * 4 + 5] + + line2[xx * 4 + 9] + line2[xx * 4 + 13] + line2[xx * 4 + 17] + + line3[xx * 4 + 1] + line3[xx * 4 + 5] + line3[xx * 4 + 9] + + line3[xx * 4 + 13] + line3[xx * 4 + 17] + line4[xx * 4 + 1] + + line4[xx * 4 + 5] + line4[xx * 4 + 9] + line4[xx * 4 + 13] + + line4[xx * 4 + 17]; + ss2 = line0[xx * 4 + 2] + line0[xx * 4 + 6] + line0[xx * 4 + 10] + + line0[xx * 4 + 14] + line0[xx * 4 + 18] + line1[xx * 4 + 2] + + line1[xx * 4 + 6] + line1[xx * 4 + 10] + line1[xx * 4 + 14] + + line1[xx * 4 + 18] + line2[xx * 4 + 2] + line2[xx * 4 + 6] + + line2[xx * 4 + 10] + line2[xx * 4 + 14] + line2[xx * 4 + 18] + + line3[xx * 4 + 2] + line3[xx * 4 + 6] + line3[xx * 4 + 10] + + line3[xx * 4 + 14] + line3[xx * 4 + 18] + line4[xx * 4 + 2] + + line4[xx * 4 + 6] + line4[xx * 4 + 10] + line4[xx * 4 + 14] + + line4[xx * 4 + 18]; + ss3 = line0[xx * 4 + 3] + line0[xx * 4 + 7] + line0[xx * 4 + 11] + + line0[xx * 4 + 15] + line0[xx * 4 + 19] + line1[xx * 4 + 3] + + line1[xx * 4 + 7] + line1[xx * 4 + 11] + line1[xx * 4 + 15] + + line1[xx * 4 + 19] + line2[xx * 4 + 3] + line2[xx * 4 + 7] + + line2[xx * 4 + 11] + line2[xx * 4 + 15] + line2[xx * 4 + 19] + + line3[xx * 4 + 3] + line3[xx * 4 + 7] + line3[xx * 4 + 11] + + line3[xx * 4 + 15] + line3[xx * 4 + 19] + line4[xx * 4 + 3] + + line4[xx * 4 + 7] + line4[xx * 4 + 11] + line4[xx * 4 + 15] + + line4[xx * 4 + 19]; + v = MAKE_UINT32( + ((ss0 + amend) * multiplier) >> 24, + ((ss1 + amend) * multiplier) >> 24, + ((ss2 + amend) * multiplier) >> 24, + ((ss3 + amend) * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + } + } +} + +void +ImagingReduceCorners(Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) { + /* Fill the last row and the last column for any xscale and yscale. + */ + int x, y, xx, yy; + + if (imIn->image8) { + if (box[2] % xscale) { + int scale = (box[2] % xscale) * yscale; + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + UINT32 ss = amend; + x = box[2] / xscale; + + for (yy = yy_from; yy < yy_from + yscale; yy++) { + UINT8 *line = (UINT8 *)imIn->image8[yy]; + for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } + if (box[3] % yscale) { + int scale = xscale * (box[3] % yscale); + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + y = box[3] / yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 ss = amend; + for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { + UINT8 *line = (UINT8 *)imIn->image8[yy]; + for (xx = xx_from; xx < xx_from + xscale; xx++) { + ss += line[xx + 0]; + } + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } + if (box[2] % xscale && box[3] % yscale) { + int scale = (box[2] % xscale) * (box[3] % yscale); + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + UINT32 ss = amend; + x = box[2] / xscale; + y = box[3] / yscale; + for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { + UINT8 *line = (UINT8 *)imIn->image8[yy]; + for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + imOut->image8[y][x] = (ss * multiplier) >> 24; + } + } else { + if (box[2] % xscale) { + int scale = (box[2] % xscale) * yscale; + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + x = box[2] / xscale; + + for (yy = yy_from; yy < yy_from + yscale; yy++) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + ss3 += line[xx * 4 + 3]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + if (box[3] % yscale) { + int scale = xscale * (box[3] % yscale); + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + y = box[3] / yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = xx_from; xx < xx_from + xscale; xx++) { + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + ss3 += line[xx * 4 + 3]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } + if (box[2] % xscale && box[3] % yscale) { + int scale = (box[2] % xscale) * (box[3] % yscale); + UINT32 multiplier = division_UINT32(scale, 8); + UINT32 amend = scale / 2; + UINT32 v; + UINT32 ss0 = amend, ss1 = amend, ss2 = amend, ss3 = amend; + x = box[2] / xscale; + y = box[3] / yscale; + for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { + UINT8 *line = (UINT8 *)imIn->image[yy]; + for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { + ss0 += line[xx * 4 + 0]; + ss1 += line[xx * 4 + 1]; + ss2 += line[xx * 4 + 2]; + ss3 += line[xx * 4 + 3]; + } + } + v = MAKE_UINT32( + (ss0 * multiplier) >> 24, + (ss1 * multiplier) >> 24, + (ss2 * multiplier) >> 24, + (ss3 * multiplier) >> 24); + memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); + } + } +} + +void +ImagingReduceNxN_32bpc( + Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) { + /* The most general implementation for any xscale and yscale + */ + int x, y, xx, yy; + double multiplier = 1.0 / (yscale * xscale); + + switch (imIn->type) { + case IMAGING_TYPE_INT32: + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + double ss = 0; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + INT32 *line0 = (INT32 *)imIn->image32[yy]; + INT32 *line1 = (INT32 *)imIn->image32[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line0[xx + 0] + line0[xx + 1] + line1[xx + 0] + + line1[xx + 1]; + } + if (xscale & 0x01) { + ss += line0[xx + 0] + line1[xx + 0]; + } + } + if (yscale & 0x01) { + INT32 *line = (INT32 *)imIn->image32[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line[xx + 0] + line[xx + 1]; + } + if (xscale & 0x01) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_I(imOut, x, y) = ROUND_UP(ss * multiplier); + } + } + break; + + case IMAGING_TYPE_FLOAT32: + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + double ss = 0; + for (yy = yy_from; yy < yy_from + yscale - 1; yy += 2) { + FLOAT32 *line0 = (FLOAT32 *)imIn->image32[yy]; + FLOAT32 *line1 = (FLOAT32 *)imIn->image32[yy + 1]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line0[xx + 0] + line0[xx + 1] + line1[xx + 0] + + line1[xx + 1]; + } + if (xscale & 0x01) { + ss += line0[xx + 0] + line1[xx + 0]; + } + } + if (yscale & 0x01) { + FLOAT32 *line = (FLOAT32 *)imIn->image32[yy]; + for (xx = xx_from; xx < xx_from + xscale - 1; xx += 2) { + ss += line[xx + 0] + line[xx + 1]; + } + if (xscale & 0x01) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_F(imOut, x, y) = ss * multiplier; + } + } + break; + } +} + +void +ImagingReduceCorners_32bpc( + Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) { + /* Fill the last row and the last column for any xscale and yscale. + */ + int x, y, xx, yy; + + switch (imIn->type) { + case IMAGING_TYPE_INT32: + if (box[2] % xscale) { + double multiplier = 1.0 / ((box[2] % xscale) * yscale); + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + double ss = 0; + x = box[2] / xscale; + for (yy = yy_from; yy < yy_from + yscale; yy++) { + INT32 *line = (INT32 *)imIn->image32[yy]; + for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_I(imOut, x, y) = ROUND_UP(ss * multiplier); + } + } + if (box[3] % yscale) { + double multiplier = 1.0 / (xscale * (box[3] % yscale)); + y = box[3] / yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + double ss = 0; + for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { + INT32 *line = (INT32 *)imIn->image32[yy]; + for (xx = xx_from; xx < xx_from + xscale; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_I(imOut, x, y) = ROUND_UP(ss * multiplier); + } + } + if (box[2] % xscale && box[3] % yscale) { + double multiplier = 1.0 / ((box[2] % xscale) * (box[3] % yscale)); + double ss = 0; + x = box[2] / xscale; + y = box[3] / yscale; + for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { + INT32 *line = (INT32 *)imIn->image32[yy]; + for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_I(imOut, x, y) = ROUND_UP(ss * multiplier); + } + break; + + case IMAGING_TYPE_FLOAT32: + if (box[2] % xscale) { + double multiplier = 1.0 / ((box[2] % xscale) * yscale); + for (y = 0; y < box[3] / yscale; y++) { + int yy_from = box[1] + y * yscale; + double ss = 0; + x = box[2] / xscale; + for (yy = yy_from; yy < yy_from + yscale; yy++) { + FLOAT32 *line = (FLOAT32 *)imIn->image32[yy]; + for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_F(imOut, x, y) = ss * multiplier; + } + } + if (box[3] % yscale) { + double multiplier = 1.0 / (xscale * (box[3] % yscale)); + y = box[3] / yscale; + for (x = 0; x < box[2] / xscale; x++) { + int xx_from = box[0] + x * xscale; + double ss = 0; + for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { + FLOAT32 *line = (FLOAT32 *)imIn->image32[yy]; + for (xx = xx_from; xx < xx_from + xscale; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_F(imOut, x, y) = ss * multiplier; + } + } + if (box[2] % xscale && box[3] % yscale) { + double multiplier = 1.0 / ((box[2] % xscale) * (box[3] % yscale)); + double ss = 0; + x = box[2] / xscale; + y = box[3] / yscale; + for (yy = box[1] + y * yscale; yy < box[1] + box[3]; yy++) { + FLOAT32 *line = (FLOAT32 *)imIn->image32[yy]; + for (xx = box[0] + x * xscale; xx < box[0] + box[2]; xx++) { + ss += line[xx + 0]; + } + } + IMAGING_PIXEL_F(imOut, x, y) = ss * multiplier; + } + break; + } +} + +Imaging +ImagingReduce(Imaging imIn, int xscale, int yscale, int box[4]) { + ImagingSectionCookie cookie; + Imaging imOut = NULL; + + if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "1") == 0) { + return (Imaging)ImagingError_ModeError(); + } + + if (imIn->type == IMAGING_TYPE_SPECIAL) { + return (Imaging)ImagingError_ModeError(); + } + + imOut = ImagingNewDirty( + imIn->mode, (box[2] + xscale - 1) / xscale, (box[3] + yscale - 1) / yscale); + if (!imOut) { + return NULL; + } + + ImagingSectionEnter(&cookie); + + switch (imIn->type) { + case IMAGING_TYPE_UINT8: + if (xscale == 1) { + if (yscale == 2) { + ImagingReduce1x2(imOut, imIn, box); + } else if (yscale == 3) { + ImagingReduce1x3(imOut, imIn, box); + } else { + ImagingReduce1xN(imOut, imIn, box, yscale); + } + } else if (yscale == 1) { + if (xscale == 2) { + ImagingReduce2x1(imOut, imIn, box); + } else if (xscale == 3) { + ImagingReduce3x1(imOut, imIn, box); + } else { + ImagingReduceNx1(imOut, imIn, box, xscale); + } + } else if (xscale == yscale && xscale <= 5) { + if (xscale == 2) { + ImagingReduce2x2(imOut, imIn, box); + } else if (xscale == 3) { + ImagingReduce3x3(imOut, imIn, box); + } else if (xscale == 4) { + ImagingReduce4x4(imOut, imIn, box); + } else { + ImagingReduce5x5(imOut, imIn, box); + } + } else { + ImagingReduceNxN(imOut, imIn, box, xscale, yscale); + } + + ImagingReduceCorners(imOut, imIn, box, xscale, yscale); + break; + + case IMAGING_TYPE_INT32: + case IMAGING_TYPE_FLOAT32: + ImagingReduceNxN_32bpc(imOut, imIn, box, xscale, yscale); + + ImagingReduceCorners_32bpc(imOut, imIn, box, xscale, yscale); + break; + } + + ImagingSectionLeave(&cookie); + + return imOut; +} diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index 877f25a9416..cf79d8a4e4d 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -2,79 +2,88 @@ #include - -#define ROUND_UP(f) ((int) ((f) >= 0.0 ? (f) + 0.5F : (f) - 0.5F)) - +#define ROUND_UP(f) ((int)((f) >= 0.0 ? (f) + 0.5F : (f)-0.5F)) struct filter { double (*filter)(double x); double support; }; -static inline double box_filter(double x) -{ - if (x >= -0.5 && x < 0.5) +static inline double +box_filter(double x) { + if (x > -0.5 && x <= 0.5) { return 1.0; + } return 0.0; } -static inline double bilinear_filter(double x) -{ - if (x < 0.0) +static inline double +bilinear_filter(double x) { + if (x < 0.0) { x = -x; - if (x < 1.0) - return 1.0-x; + } + if (x < 1.0) { + return 1.0 - x; + } return 0.0; } -static inline double hamming_filter(double x) -{ - if (x < 0.0) +static inline double +hamming_filter(double x) { + if (x < 0.0) { x = -x; - if (x == 0.0) + } + if (x == 0.0) { return 1.0; - if (x >= 1.0) + } + if (x >= 1.0) { return 0.0; + } x = x * M_PI; return sin(x) / x * (0.54f + 0.46f * cos(x)); } -static inline double bicubic_filter(double x) -{ - /* https://en.wikipedia.org/wiki/Bicubic_interpolation#Bicubic_convolution_algorithm */ +static inline double +bicubic_filter(double x) { + /* https://en.wikipedia.org/wiki/Bicubic_interpolation#Bicubic_convolution_algorithm + */ #define a -0.5 - if (x < 0.0) + if (x < 0.0) { x = -x; - if (x < 1.0) - return ((a + 2.0) * x - (a + 3.0)) * x*x + 1; - if (x < 2.0) + } + if (x < 1.0) { + return ((a + 2.0) * x - (a + 3.0)) * x * x + 1; + } + if (x < 2.0) { return (((x - 5) * x + 8) * x - 4) * a; + } return 0.0; #undef a } -static inline double sinc_filter(double x) -{ - if (x == 0.0) +static inline double +sinc_filter(double x) { + if (x == 0.0) { return 1.0; + } x = x * M_PI; return sin(x) / x; } -static inline double lanczos_filter(double x) -{ +static inline double +lanczos_filter(double x) { /* truncated sinc */ - if (-3.0 <= x && x < 3.0) - return sinc_filter(x) * sinc_filter(x/3); + if (-3.0 <= x && x < 3.0) { + return sinc_filter(x) * sinc_filter(x / 3); + } return 0.0; } -static struct filter BOX = { box_filter, 0.5 }; -static struct filter BILINEAR = { bilinear_filter, 1.0 }; -static struct filter HAMMING = { hamming_filter, 1.0 }; -static struct filter BICUBIC = { bicubic_filter, 2.0 }; -static struct filter LANCZOS = { lanczos_filter, 3.0 }; - +static struct filter BOX = {box_filter, 0.5}; +static struct filter BILINEAR = {bilinear_filter, 1.0}; +static struct filter HAMMING = {hamming_filter, 1.0}; +static struct filter BICUBIC = {bicubic_filter, 2.0}; +static struct filter LANCZOS = {lanczos_filter, 3.0}; /* 8 bits for result. Filter can have negative areas. In one cases the sum of the coefficients will be negative, @@ -82,54 +91,102 @@ static struct filter LANCZOS = { lanczos_filter, 3.0 }; two extra bits for overflow and int type. */ #define PRECISION_BITS (32 - 8 - 2) - -UINT8 _lookups[512] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, - 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, - 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, - 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, - 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, - 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, - 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, - 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, - 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, - 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, - 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, - 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, - 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, - 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, - 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 +/* Handles values form -640 to 639. */ +UINT8 _clip8_lookups[1280] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, + 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, + 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, + 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, + 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, + 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, + 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, + 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, + 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, + 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, + 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, + 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, + 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, }; -UINT8 *lookups = &_lookups[128]; +UINT8 *clip8_lookups = &_clip8_lookups[640]; - -static inline UINT8 clip8(int in) -{ - return lookups[in >> PRECISION_BITS]; +static inline UINT8 +clip8(int in) { + return clip8_lookups[in >> PRECISION_BITS]; } - int -precompute_coeffs(int inSize, float in0, float in1, int outSize, - struct filter *filterp, int **boundsp, double **kkp) { +precompute_coeffs( + int inSize, + float in0, + float in1, + int outSize, + struct filter *filterp, + int **boundsp, + double **kkp) { double support, scale, filterscale; double center, ww, ss; int xx, x, ksize, xmin, xmax; @@ -137,7 +194,7 @@ precompute_coeffs(int inSize, float in0, float in1, int outSize, double *kk, *k; /* prepare for horizontal stretch */ - filterscale = scale = (double) (in1 - in0) / outSize; + filterscale = scale = (double)(in1 - in0) / outSize; if (filterscale < 1.0) { filterscale = 1.0; } @@ -146,10 +203,10 @@ precompute_coeffs(int inSize, float in0, float in1, int outSize, support = filterp->support * filterscale; /* maximum number of coeffs */ - ksize = (int) ceil(support) * 2 + 1; + ksize = (int)ceil(support) * 2 + 1; // check for overflow - if (outSize > INT_MAX / (ksize * sizeof(double))) { + if (outSize > INT_MAX / (ksize * (int)sizeof(double))) { ImagingError_MemoryError(); return 0; } @@ -157,14 +214,14 @@ precompute_coeffs(int inSize, float in0, float in1, int outSize, /* coefficient buffer */ /* malloc check ok, overflow checked above */ kk = malloc(outSize * ksize * sizeof(double)); - if ( ! kk) { + if (!kk) { ImagingError_MemoryError(); return 0; } /* malloc check ok, ksize*sizeof(double) > 2*sizeof(int) */ bounds = malloc(outSize * 2 * sizeof(int)); - if ( ! bounds) { + if (!bounds) { free(kk); ImagingError_MemoryError(); return 0; @@ -175,13 +232,15 @@ precompute_coeffs(int inSize, float in0, float in1, int outSize, ww = 0.0; ss = 1.0 / filterscale; // Round the value - xmin = (int) (center - support + 0.5); - if (xmin < 0) + xmin = (int)(center - support + 0.5); + if (xmin < 0) { xmin = 0; + } // Round the value - xmax = (int) (center + support + 0.5); - if (xmax > inSize) + xmax = (int)(center + support + 0.5); + if (xmax > inSize) { xmax = inSize; + } xmax -= xmin; k = &kk[xx * ksize]; for (x = 0; x < xmax; x++) { @@ -190,8 +249,9 @@ precompute_coeffs(int inSize, float in0, float in1, int outSize, ww += w; } for (x = 0; x < xmax; x++) { - if (ww != 0.0) + if (ww != 0.0) { k[x] /= ww; + } } // Remaining values should stay empty if they are used despite of xmax. for (; x < ksize; x++) { @@ -205,38 +265,33 @@ precompute_coeffs(int inSize, float in0, float in1, int outSize, return ksize; } - void -normalize_coeffs_8bpc(int outSize, int ksize, double *prekk) -{ +normalize_coeffs_8bpc(int outSize, int ksize, double *prekk) { int x; INT32 *kk; // use the same buffer for normalized coefficients - kk = (INT32 *) prekk; + kk = (INT32 *)prekk; for (x = 0; x < outSize * ksize; x++) { if (prekk[x] < 0) { - kk[x] = (int) (-0.5 + prekk[x] * (1 << PRECISION_BITS)); + kk[x] = (int)(-0.5 + prekk[x] * (1 << PRECISION_BITS)); } else { - kk[x] = (int) (0.5 + prekk[x] * (1 << PRECISION_BITS)); + kk[x] = (int)(0.5 + prekk[x] * (1 << PRECISION_BITS)); } } } - - -void -ImagingResampleHorizontal_8bpc(Imaging imOut, Imaging imIn, int offset, - int ksize, int *bounds, double *prekk) -{ +void +ImagingResampleHorizontal_8bpc( + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *prekk) { ImagingSectionCookie cookie; int ss0, ss1, ss2, ss3; int xx, yy, x, xmin, xmax; INT32 *k, *kk; // use the same buffer for normalized coefficients - kk = (INT32 *) prekk; + kk = (INT32 *)prekk; normalize_coeffs_8bpc(imOut->xsize, ksize, prekk); ImagingSectionEnter(&cookie); @@ -246,9 +301,10 @@ ImagingResampleHorizontal_8bpc(Imaging imOut, Imaging imIn, int offset, xmin = bounds[xx * 2 + 0]; xmax = bounds[xx * 2 + 1]; k = &kk[xx * ksize]; - ss0 = 1 << (PRECISION_BITS -1); - for (x = 0; x < xmax; x++) - ss0 += ((UINT8) imIn->image8[yy + offset][x + xmin]) * k[x]; + ss0 = 1 << (PRECISION_BITS - 1); + for (x = 0; x < xmax; x++) { + ss0 += ((UINT8)imIn->image8[yy + offset][x + xmin]) * k[x]; + } imOut->image8[yy][xx] = clip8(ss0); } } @@ -256,49 +312,61 @@ ImagingResampleHorizontal_8bpc(Imaging imOut, Imaging imIn, int offset, if (imIn->bands == 2) { for (yy = 0; yy < imOut->ysize; yy++) { for (xx = 0; xx < imOut->xsize; xx++) { + UINT32 v; xmin = bounds[xx * 2 + 0]; xmax = bounds[xx * 2 + 1]; k = &kk[xx * ksize]; - ss0 = ss3 = 1 << (PRECISION_BITS -1); + ss0 = ss3 = 1 << (PRECISION_BITS - 1); for (x = 0; x < xmax; x++) { - ss0 += ((UINT8) imIn->image[yy + offset][(x + xmin)*4 + 0]) * k[x]; - ss3 += ((UINT8) imIn->image[yy + offset][(x + xmin)*4 + 3]) * k[x]; + ss0 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 0]) * + k[x]; + ss3 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 3]) * + k[x]; } - ((UINT32 *) imOut->image[yy])[xx] = MAKE_UINT32( - clip8(ss0), 0, 0, clip8(ss3)); + v = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); } } } else if (imIn->bands == 3) { for (yy = 0; yy < imOut->ysize; yy++) { for (xx = 0; xx < imOut->xsize; xx++) { + UINT32 v; xmin = bounds[xx * 2 + 0]; xmax = bounds[xx * 2 + 1]; k = &kk[xx * ksize]; - ss0 = ss1 = ss2 = 1 << (PRECISION_BITS -1); + ss0 = ss1 = ss2 = 1 << (PRECISION_BITS - 1); for (x = 0; x < xmax; x++) { - ss0 += ((UINT8) imIn->image[yy + offset][(x + xmin)*4 + 0]) * k[x]; - ss1 += ((UINT8) imIn->image[yy + offset][(x + xmin)*4 + 1]) * k[x]; - ss2 += ((UINT8) imIn->image[yy + offset][(x + xmin)*4 + 2]) * k[x]; + ss0 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 0]) * + k[x]; + ss1 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 1]) * + k[x]; + ss2 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 2]) * + k[x]; } - ((UINT32 *) imOut->image[yy])[xx] = MAKE_UINT32( - clip8(ss0), clip8(ss1), clip8(ss2), 0); + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), 0); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); } } } else { for (yy = 0; yy < imOut->ysize; yy++) { for (xx = 0; xx < imOut->xsize; xx++) { + UINT32 v; xmin = bounds[xx * 2 + 0]; xmax = bounds[xx * 2 + 1]; k = &kk[xx * ksize]; - ss0 = ss1 = ss2 = ss3 = 1 << (PRECISION_BITS -1); + ss0 = ss1 = ss2 = ss3 = 1 << (PRECISION_BITS - 1); for (x = 0; x < xmax; x++) { - ss0 += ((UINT8) imIn->image[yy + offset][(x + xmin)*4 + 0]) * k[x]; - ss1 += ((UINT8) imIn->image[yy + offset][(x + xmin)*4 + 1]) * k[x]; - ss2 += ((UINT8) imIn->image[yy + offset][(x + xmin)*4 + 2]) * k[x]; - ss3 += ((UINT8) imIn->image[yy + offset][(x + xmin)*4 + 3]) * k[x]; + ss0 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 0]) * + k[x]; + ss1 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 1]) * + k[x]; + ss2 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 2]) * + k[x]; + ss3 += ((UINT8)imIn->image[yy + offset][(x + xmin) * 4 + 3]) * + k[x]; } - ((UINT32 *) imOut->image[yy])[xx] = MAKE_UINT32( - clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); } } } @@ -306,18 +374,16 @@ ImagingResampleHorizontal_8bpc(Imaging imOut, Imaging imIn, int offset, ImagingSectionLeave(&cookie); } - void -ImagingResampleVertical_8bpc(Imaging imOut, Imaging imIn, int offset, - int ksize, int *bounds, double *prekk) -{ +ImagingResampleVertical_8bpc( + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *prekk) { ImagingSectionCookie cookie; int ss0, ss1, ss2, ss3; int xx, yy, y, ymin, ymax; INT32 *k, *kk; // use the same buffer for normalized coefficients - kk = (INT32 *) prekk; + kk = (INT32 *)prekk; normalize_coeffs_8bpc(imOut->ysize, ksize, prekk); ImagingSectionEnter(&cookie); @@ -327,9 +393,10 @@ ImagingResampleVertical_8bpc(Imaging imOut, Imaging imIn, int offset, ymin = bounds[yy * 2 + 0]; ymax = bounds[yy * 2 + 1]; for (xx = 0; xx < imOut->xsize; xx++) { - ss0 = 1 << (PRECISION_BITS -1); - for (y = 0; y < ymax; y++) - ss0 += ((UINT8) imIn->image8[y + ymin][xx]) * k[y]; + ss0 = 1 << (PRECISION_BITS - 1); + for (y = 0; y < ymax; y++) { + ss0 += ((UINT8)imIn->image8[y + ymin][xx]) * k[y]; + } imOut->image8[yy][xx] = clip8(ss0); } } @@ -340,13 +407,14 @@ ImagingResampleVertical_8bpc(Imaging imOut, Imaging imIn, int offset, ymin = bounds[yy * 2 + 0]; ymax = bounds[yy * 2 + 1]; for (xx = 0; xx < imOut->xsize; xx++) { - ss0 = ss3 = 1 << (PRECISION_BITS -1); + UINT32 v; + ss0 = ss3 = 1 << (PRECISION_BITS - 1); for (y = 0; y < ymax; y++) { - ss0 += ((UINT8) imIn->image[y + ymin][xx*4 + 0]) * k[y]; - ss3 += ((UINT8) imIn->image[y + ymin][xx*4 + 3]) * k[y]; + ss0 += ((UINT8)imIn->image[y + ymin][xx * 4 + 0]) * k[y]; + ss3 += ((UINT8)imIn->image[y + ymin][xx * 4 + 3]) * k[y]; } - ((UINT32 *) imOut->image[yy])[xx] = MAKE_UINT32( - clip8(ss0), 0, 0, clip8(ss3)); + v = MAKE_UINT32(clip8(ss0), 0, 0, clip8(ss3)); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); } } } else if (imIn->bands == 3) { @@ -355,14 +423,15 @@ ImagingResampleVertical_8bpc(Imaging imOut, Imaging imIn, int offset, ymin = bounds[yy * 2 + 0]; ymax = bounds[yy * 2 + 1]; for (xx = 0; xx < imOut->xsize; xx++) { - ss0 = ss1 = ss2 = 1 << (PRECISION_BITS -1); + UINT32 v; + ss0 = ss1 = ss2 = 1 << (PRECISION_BITS - 1); for (y = 0; y < ymax; y++) { - ss0 += ((UINT8) imIn->image[y + ymin][xx*4 + 0]) * k[y]; - ss1 += ((UINT8) imIn->image[y + ymin][xx*4 + 1]) * k[y]; - ss2 += ((UINT8) imIn->image[y + ymin][xx*4 + 2]) * k[y]; + ss0 += ((UINT8)imIn->image[y + ymin][xx * 4 + 0]) * k[y]; + ss1 += ((UINT8)imIn->image[y + ymin][xx * 4 + 1]) * k[y]; + ss2 += ((UINT8)imIn->image[y + ymin][xx * 4 + 2]) * k[y]; } - ((UINT32 *) imOut->image[yy])[xx] = MAKE_UINT32( - clip8(ss0), clip8(ss1), clip8(ss2), 0); + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), 0); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); } } } else { @@ -371,15 +440,16 @@ ImagingResampleVertical_8bpc(Imaging imOut, Imaging imIn, int offset, ymin = bounds[yy * 2 + 0]; ymax = bounds[yy * 2 + 1]; for (xx = 0; xx < imOut->xsize; xx++) { - ss0 = ss1 = ss2 = ss3 = 1 << (PRECISION_BITS -1); + UINT32 v; + ss0 = ss1 = ss2 = ss3 = 1 << (PRECISION_BITS - 1); for (y = 0; y < ymax; y++) { - ss0 += ((UINT8) imIn->image[y + ymin][xx*4 + 0]) * k[y]; - ss1 += ((UINT8) imIn->image[y + ymin][xx*4 + 1]) * k[y]; - ss2 += ((UINT8) imIn->image[y + ymin][xx*4 + 2]) * k[y]; - ss3 += ((UINT8) imIn->image[y + ymin][xx*4 + 3]) * k[y]; + ss0 += ((UINT8)imIn->image[y + ymin][xx * 4 + 0]) * k[y]; + ss1 += ((UINT8)imIn->image[y + ymin][xx * 4 + 1]) * k[y]; + ss2 += ((UINT8)imIn->image[y + ymin][xx * 4 + 2]) * k[y]; + ss3 += ((UINT8)imIn->image[y + ymin][xx * 4 + 3]) * k[y]; } - ((UINT32 *) imOut->image[yy])[xx] = MAKE_UINT32( - clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + v = MAKE_UINT32(clip8(ss0), clip8(ss1), clip8(ss2), clip8(ss3)); + memcpy(imOut->image[yy] + xx * sizeof(v), &v, sizeof(v)); } } } @@ -387,18 +457,16 @@ ImagingResampleVertical_8bpc(Imaging imOut, Imaging imIn, int offset, ImagingSectionLeave(&cookie); } - void -ImagingResampleHorizontal_32bpc(Imaging imOut, Imaging imIn, int offset, - int ksize, int *bounds, double *kk) -{ +ImagingResampleHorizontal_32bpc( + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk) { ImagingSectionCookie cookie; double ss; int xx, yy, x, xmin, xmax; double *k; ImagingSectionEnter(&cookie); - switch(imIn->type) { + switch (imIn->type) { case IMAGING_TYPE_INT32: for (yy = 0; yy < imOut->ysize; yy++) { for (xx = 0; xx < imOut->xsize; xx++) { @@ -406,8 +474,9 @@ ImagingResampleHorizontal_32bpc(Imaging imOut, Imaging imIn, int offset, xmax = bounds[xx * 2 + 1]; k = &kk[xx * ksize]; ss = 0.0; - for (x = 0; x < xmax; x++) + for (x = 0; x < xmax; x++) { ss += IMAGING_PIXEL_I(imIn, x + xmin, yy + offset) * k[x]; + } IMAGING_PIXEL_I(imOut, xx, yy) = ROUND_UP(ss); } } @@ -420,8 +489,9 @@ ImagingResampleHorizontal_32bpc(Imaging imOut, Imaging imIn, int offset, xmax = bounds[xx * 2 + 1]; k = &kk[xx * ksize]; ss = 0.0; - for (x = 0; x < xmax; x++) + for (x = 0; x < xmax; x++) { ss += IMAGING_PIXEL_F(imIn, x + xmin, yy + offset) * k[x]; + } IMAGING_PIXEL_F(imOut, xx, yy) = ss; } } @@ -430,18 +500,16 @@ ImagingResampleHorizontal_32bpc(Imaging imOut, Imaging imIn, int offset, ImagingSectionLeave(&cookie); } - void -ImagingResampleVertical_32bpc(Imaging imOut, Imaging imIn, int offset, - int ksize, int *bounds, double *kk) -{ +ImagingResampleVertical_32bpc( + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk) { ImagingSectionCookie cookie; double ss; int xx, yy, y, ymin, ymax; double *k; ImagingSectionEnter(&cookie); - switch(imIn->type) { + switch (imIn->type) { case IMAGING_TYPE_INT32: for (yy = 0; yy < imOut->ysize; yy++) { ymin = bounds[yy * 2 + 0]; @@ -449,8 +517,9 @@ ImagingResampleVertical_32bpc(Imaging imOut, Imaging imIn, int offset, k = &kk[yy * ksize]; for (xx = 0; xx < imOut->xsize; xx++) { ss = 0.0; - for (y = 0; y < ymax; y++) + for (y = 0; y < ymax; y++) { ss += IMAGING_PIXEL_I(imIn, xx, y + ymin) * k[y]; + } IMAGING_PIXEL_I(imOut, xx, yy) = ROUND_UP(ss); } } @@ -463,8 +532,9 @@ ImagingResampleVertical_32bpc(Imaging imOut, Imaging imIn, int offset, k = &kk[yy * ksize]; for (xx = 0; xx < imOut->xsize; xx++) { ss = 0.0; - for (y = 0; y < ymax; y++) + for (y = 0; y < ymax; y++) { ss += IMAGING_PIXEL_F(imIn, xx, y + ymin) * k[y]; + } IMAGING_PIXEL_F(imOut, xx, yy) = ss; } } @@ -473,35 +543,36 @@ ImagingResampleVertical_32bpc(Imaging imOut, Imaging imIn, int offset, ImagingSectionLeave(&cookie); } - -typedef void (*ResampleFunction)(Imaging imOut, Imaging imIn, int offset, - int ksize, int *bounds, double *kk); - +typedef void (*ResampleFunction)( + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk); Imaging -ImagingResampleInner(Imaging imIn, int xsize, int ysize, - struct filter *filterp, float box[4], - ResampleFunction ResampleHorizontal, - ResampleFunction ResampleVertical); - +ImagingResampleInner( + Imaging imIn, + int xsize, + int ysize, + struct filter *filterp, + float box[4], + ResampleFunction ResampleHorizontal, + ResampleFunction ResampleVertical); Imaging -ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]) -{ +ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]) { struct filter *filterp; ResampleFunction ResampleHorizontal; ResampleFunction ResampleVertical; - if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "1") == 0) - return (Imaging) ImagingError_ModeError(); + if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "1") == 0) { + return (Imaging)ImagingError_ModeError(); + } if (imIn->type == IMAGING_TYPE_SPECIAL) { - return (Imaging) ImagingError_ModeError(); + return (Imaging)ImagingError_ModeError(); } else if (imIn->image8) { ResampleHorizontal = ImagingResampleHorizontal_8bpc; ResampleVertical = ImagingResampleVertical_8bpc; } else { - switch(imIn->type) { + switch (imIn->type) { case IMAGING_TYPE_UINT8: ResampleHorizontal = ImagingResampleHorizontal_8bpc; ResampleVertical = ImagingResampleVertical_8bpc; @@ -512,44 +583,44 @@ ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]) ResampleVertical = ImagingResampleVertical_32bpc; break; default: - return (Imaging) ImagingError_ModeError(); + return (Imaging)ImagingError_ModeError(); } } /* check filter */ switch (filter) { - case IMAGING_TRANSFORM_BOX: - filterp = &BOX; - break; - case IMAGING_TRANSFORM_BILINEAR: - filterp = &BILINEAR; - break; - case IMAGING_TRANSFORM_HAMMING: - filterp = &HAMMING; - break; - case IMAGING_TRANSFORM_BICUBIC: - filterp = &BICUBIC; - break; - case IMAGING_TRANSFORM_LANCZOS: - filterp = &LANCZOS; - break; - default: - return (Imaging) ImagingError_ValueError( - "unsupported resampling filter" - ); + case IMAGING_TRANSFORM_BOX: + filterp = &BOX; + break; + case IMAGING_TRANSFORM_BILINEAR: + filterp = &BILINEAR; + break; + case IMAGING_TRANSFORM_HAMMING: + filterp = &HAMMING; + break; + case IMAGING_TRANSFORM_BICUBIC: + filterp = &BICUBIC; + break; + case IMAGING_TRANSFORM_LANCZOS: + filterp = &LANCZOS; + break; + default: + return (Imaging)ImagingError_ValueError("unsupported resampling filter"); } - return ImagingResampleInner(imIn, xsize, ysize, filterp, box, - ResampleHorizontal, ResampleVertical); + return ImagingResampleInner( + imIn, xsize, ysize, filterp, box, ResampleHorizontal, ResampleVertical); } - Imaging -ImagingResampleInner(Imaging imIn, int xsize, int ysize, - struct filter *filterp, float box[4], - ResampleFunction ResampleHorizontal, - ResampleFunction ResampleVertical) -{ +ImagingResampleInner( + Imaging imIn, + int xsize, + int ysize, + struct filter *filterp, + float box[4], + ResampleFunction ResampleHorizontal, + ResampleFunction ResampleVertical) { Imaging imTemp = NULL; Imaging imOut = NULL; @@ -562,15 +633,15 @@ ImagingResampleInner(Imaging imIn, int xsize, int ysize, need_horizontal = xsize != imIn->xsize || box[0] || box[2] != xsize; need_vertical = ysize != imIn->ysize || box[1] || box[3] != ysize; - ksize_horiz = precompute_coeffs(imIn->xsize, box[0], box[2], xsize, - filterp, &bounds_horiz, &kk_horiz); - if ( ! ksize_horiz) { + ksize_horiz = precompute_coeffs( + imIn->xsize, box[0], box[2], xsize, filterp, &bounds_horiz, &kk_horiz); + if (!ksize_horiz) { return NULL; } - ksize_vert = precompute_coeffs(imIn->ysize, box[1], box[3], ysize, - filterp, &bounds_vert, &kk_vert); - if ( ! ksize_vert) { + ksize_vert = precompute_coeffs( + imIn->ysize, box[1], box[3], ysize, filterp, &bounds_vert, &kk_vert); + if (!ksize_vert) { free(bounds_horiz); free(kk_horiz); return NULL; @@ -579,8 +650,7 @@ ImagingResampleInner(Imaging imIn, int xsize, int ysize, // First used row in the source image ybox_first = bounds_vert[0]; // Last used row in the source image - ybox_last = bounds_vert[ysize*2 - 2] + bounds_vert[ysize*2 - 1]; - + ybox_last = bounds_vert[ysize * 2 - 2] + bounds_vert[ysize * 2 - 1]; /* two-pass resize, horizontal pass */ if (need_horizontal) { @@ -591,12 +661,12 @@ ImagingResampleInner(Imaging imIn, int xsize, int ysize, imTemp = ImagingNewDirty(imIn->mode, xsize, ybox_last - ybox_first); if (imTemp) { - ResampleHorizontal(imTemp, imIn, ybox_first, - ksize_horiz, bounds_horiz, kk_horiz); + ResampleHorizontal( + imTemp, imIn, ybox_first, ksize_horiz, bounds_horiz, kk_horiz); } free(bounds_horiz); free(kk_horiz); - if ( ! imTemp) { + if (!imTemp) { free(bounds_vert); free(kk_vert); return NULL; @@ -613,15 +683,14 @@ ImagingResampleInner(Imaging imIn, int xsize, int ysize, imOut = ImagingNewDirty(imIn->mode, imIn->xsize, ysize); if (imOut) { /* imIn can be the original image or horizontally resampled one */ - ResampleVertical(imOut, imIn, 0, - ksize_vert, bounds_vert, kk_vert); + ResampleVertical(imOut, imIn, 0, ksize_vert, bounds_vert, kk_vert); } /* it's safe to call ImagingDelete with empty value if previous step was not performed. */ ImagingDelete(imTemp); free(bounds_vert); free(kk_vert); - if ( ! imOut) { + if (!imOut) { return NULL; } } else { @@ -631,7 +700,7 @@ ImagingResampleInner(Imaging imIn, int xsize, int ysize, } /* none of the previous steps are performed, copying */ - if ( ! imOut) { + if (!imOut) { imOut = ImagingCopy(imIn); } diff --git a/src/libImaging/Sgi.h b/src/libImaging/Sgi.h index 8015d6661e4..39dd6882567 100644 --- a/src/libImaging/Sgi.h +++ b/src/libImaging/Sgi.h @@ -1,7 +1,6 @@ /* Sgi.h */ typedef struct { - /* CONFIGURATION */ /* Number of bytes per channel per pixel */ diff --git a/src/libImaging/SgiRleDecode.c b/src/libImaging/SgiRleDecode.c index 1d41437fc13..4eef44ba510 100644 --- a/src/libImaging/SgiRleDecode.c +++ b/src/libImaging/SgiRleDecode.c @@ -6,7 +6,7 @@ * * history: * 2017-07-28 mb fixed for images larger than 64KB - * 2017-07-20 mb created + * 2017-07-20 mb created * * Copyright (c) Mickael Bonfill 2017. * @@ -20,93 +20,182 @@ #define RLE_COPY_FLAG 0x80 #define RLE_MAX_RUN 0x7f -static void read4B(UINT32* dest, UINT8* buf) -{ +static void +read4B(UINT32 *dest, UINT8 *buf) { *dest = (UINT32)((buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]); } -static int expandrow(UINT8* dest, UINT8* src, int n, int z) -{ +/* + SgiRleDecoding is done in a single channel row oriented set of RLE chunks. + + * The file is arranged as + - SGI Header + - Rle Offset Table + - Rle Length Table + - Scanline Data + + * Each RLE atom is c->bpc bytes wide (1 or 2) + + * Each RLE Chunk is [specifier atom] [ 1 or n data atoms ] + + * Copy Atoms are a byte with the high bit set, and the low 7 are + the number of bytes to copy from the source to the + destination. e.g. + + CBBBBBBBB or 0CHLHLHLHLHLHL (B=byte, H/L = Hi low bytes) + + * Run atoms do not have the high bit set, and the low 7 bits are + the number of copies of the next atom to copy to the + destination. e.g.: + + RB -> BBBBB or RHL -> HLHLHLHLHL + + The upshot of this is, there is no way to determine the required + length of the input buffer from reloffset and rlelength without + going through the data at that scan line. + + Furthermore, there's no requirement that individual scan lines + pointed to from the rleoffset table are in any sort of order or + used only once, or even disjoint. There's also no requirement that + all of the data in the scan line area of the image file be used + + */ +static int +expandrow(UINT8 *dest, UINT8 *src, int n, int z, int xsize, UINT8 *end_of_buffer) { + /* + * n here is the number of rlechunks + * z is the number of channels, for calculating the interleave + * offset to go to RGBA style pixels + * xsize is the row width + * end_of_buffer is the address of the end of the input buffer + */ + UINT8 pixel, count; - - for (;n > 0; n--) - { + int x = 0; + + for (; n > 0; n--) { + if (src > end_of_buffer) { + return -1; + } pixel = *src++; - if (n == 1 && pixel != 0) + if (n == 1 && pixel != 0) { return n; + } count = pixel & RLE_MAX_RUN; - if (!count) + if (!count) { return count; + } + if (x + count > xsize) { + return -1; + } + x += count; if (pixel & RLE_COPY_FLAG) { - while(count--) { + if (src + count > end_of_buffer) { + return -1; + } + while (count--) { *dest = *src++; dest += z; } - - } - else { + + } else { + if (src > end_of_buffer) { + return -1; + } pixel = *src++; while (count--) { *dest = pixel; dest += z; } } - } return 0; } -static int expandrow2(UINT16* dest, UINT16* src, int n, int z) -{ +static int +expandrow2(UINT8 *dest, const UINT8 *src, int n, int z, int xsize, UINT8 *end_of_buffer) { UINT8 pixel, count; + int x = 0; - - for (;n > 0; n--) - { - pixel = ((UINT8*)src)[1]; - ++src; - if (n == 1 && pixel != 0) + for (; n > 0; n--) { + if (src + 1 > end_of_buffer) { + return -1; + } + pixel = src[1]; + src += 2; + if (n == 1 && pixel != 0) { return n; + } count = pixel & RLE_MAX_RUN; - if (!count) + if (!count) { return count; + } + if (x + count > xsize) { + return -1; + } + x += count; if (pixel & RLE_COPY_FLAG) { - while(count--) { - *dest = *src++; - dest += z; + if (src + 2 * count > end_of_buffer) { + return -1; } - } - else { while (count--) { - *dest = *src; - dest += z; + memcpy(dest, src, 2); + src += 2; + dest += z * 2; } - ++src; + } else { + if (src + 2 > end_of_buffer) { + return -1; + } + while (count--) { + memcpy(dest, src, 2); + dest += z * 2; + } + src += 2; } } return 0; } - int -ImagingSgiRleDecode(Imaging im, ImagingCodecState state, - UINT8* buf, int bytes) -{ +ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { UINT8 *ptr; SGISTATE *c; int err = 0; + int status; + + /* size check */ + if (im->xsize > INT_MAX / im->bands || im->ysize > INT_MAX / im->bands) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } - /* Get all data from File descriptor */ - c = (SGISTATE*)state->context; + /* Get all data from File descriptor */ + c = (SGISTATE *)state->context; _imaging_seek_pyFd(state->fd, 0L, SEEK_END); c->bufsize = _imaging_tell_pyFd(state->fd); c->bufsize -= SGI_HEADER_SIZE; + + c->tablen = im->bands * im->ysize; + /* below, we populate the starttab and lentab into the bufsize, + each with 4 bytes per element of tablen + Check here before we allocate any memory + */ + if (c->bufsize < 8 * c->tablen) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + ptr = malloc(sizeof(UINT8) * c->bufsize); if (!ptr) { - return IMAGING_CODEC_MEMORY; + state->errcode = IMAGING_CODEC_MEMORY; + return -1; } _imaging_seek_pyFd(state->fd, SGI_HEADER_SIZE, SEEK_SET); - _imaging_read_pyFd(state->fd, (char*)ptr, c->bufsize); + if (_imaging_read_pyFd(state->fd, (char *)ptr, c->bufsize) != c->bufsize) { + state->errcode = IMAGING_CODEC_UNKNOWN; + return -1; + } /* decoder initialization */ @@ -118,71 +207,82 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, state->ystep = 1; } - if (im->xsize > INT_MAX / im->bands || - im->ysize > INT_MAX / im->bands) { - err = IMAGING_CODEC_MEMORY; - goto sgi_finish_decode; - } - /* Allocate memory for RLE tables and rows */ free(state->buffer); state->buffer = NULL; /* malloc overflow check above */ state->buffer = calloc(im->xsize * im->bands, sizeof(UINT8) * 2); - c->tablen = im->bands * im->ysize; c->starttab = calloc(c->tablen, sizeof(UINT32)); c->lengthtab = calloc(c->tablen, sizeof(UINT32)); - if (!state->buffer || - !c->starttab || - !c->lengthtab) { + if (!state->buffer || !c->starttab || !c->lengthtab) { err = IMAGING_CODEC_MEMORY; goto sgi_finish_decode; } /* populate offsets table */ - for (c->tabindex = 0, c->bufindex = 0; c->tabindex < c->tablen; c->tabindex++, c->bufindex+=4) + for (c->tabindex = 0, c->bufindex = 0; c->tabindex < c->tablen; + c->tabindex++, c->bufindex += 4) { read4B(&c->starttab[c->tabindex], &ptr[c->bufindex]); + } /* populate lengths table */ - for (c->tabindex = 0, c->bufindex = c->tablen * sizeof(UINT32); c->tabindex < c->tablen; c->tabindex++, c->bufindex+=4) + for (c->tabindex = 0, c->bufindex = c->tablen * sizeof(UINT32); + c->tabindex < c->tablen; + c->tabindex++, c->bufindex += 4) { read4B(&c->lengthtab[c->tabindex], &ptr[c->bufindex]); - - state->count += c->tablen * sizeof(UINT32) * 2; + } /* read compressed rows */ - for (c->rowno = 0; c->rowno < im->ysize; c->rowno++, state->y += state->ystep) - { - for (c->channo = 0; c->channo < im->bands; c->channo++) - { + for (c->rowno = 0; c->rowno < im->ysize; c->rowno++, state->y += state->ystep) { + for (c->channo = 0; c->channo < im->bands; c->channo++) { c->rleoffset = c->starttab[c->rowno + c->channo * im->ysize]; c->rlelength = c->lengthtab[c->rowno + c->channo * im->ysize]; + + // Check for underflow of rleoffset-SGI_HEADER_SIZE + if (c->rleoffset < SGI_HEADER_SIZE) { + state->errcode = IMAGING_CODEC_OVERRUN; + goto sgi_finish_decode; + } + c->rleoffset -= SGI_HEADER_SIZE; - + /* row decompression */ - if (c->bpc ==1) { - if(expandrow(&state->buffer[c->channo], &ptr[c->rleoffset], c->rlelength, im->bands)) - goto sgi_finish_decode; + if (c->bpc == 1) { + status = expandrow( + &state->buffer[c->channo], + &ptr[c->rleoffset], + c->rlelength, + im->bands, + im->xsize, + &ptr[c->bufsize-1]); + } else { + status = expandrow2( + &state->buffer[c->channo * 2], + &ptr[c->rleoffset], + c->rlelength, + im->bands, + im->xsize, + &ptr[c->bufsize-1]); } - else { - if(expandrow2((UINT16*)&state->buffer[c->channo * 2], (UINT16*)&ptr[c->rleoffset], c->rlelength, im->bands)) - goto sgi_finish_decode; + if (status == -1) { + state->errcode = IMAGING_CODEC_OVERRUN; + goto sgi_finish_decode; + } else if (status == 1) { + goto sgi_finish_decode; } - - state->count += c->rlelength; + } - + /* store decompressed data in image */ - state->shuffle((UINT8*)im->image[state->y], state->buffer, im->xsize); - + state->shuffle((UINT8 *)im->image[state->y], state->buffer, im->xsize); } - c->bufsize++; +sgi_finish_decode:; -sgi_finish_decode: ; - free(c->starttab); free(c->lengthtab); free(ptr); - if (err != 0){ - return err; + if (err != 0) { + state->errcode = err; + return -1; } - return state->count - c->bufsize; + return 0; } diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 8262d864af9..76750aaf7f2 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -34,11 +34,9 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" #include - int ImagingNewCount = 0; /* -------------------------------------------------------------------- @@ -46,18 +44,17 @@ int ImagingNewCount = 0; */ Imaging -ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) -{ +ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { Imaging im; /* linesize overflow check, roughly the current largest space req'd */ if (xsize > (INT_MAX / 4) - 1) { - return (Imaging) ImagingError_MemoryError(); + return (Imaging)ImagingError_MemoryError(); } - im = (Imaging) calloc(1, size); + im = (Imaging)calloc(1, size); if (!im) { - return (Imaging) ImagingError_MemoryError(); + return (Imaging)ImagingError_MemoryError(); } /* Setup image descriptor */ @@ -115,8 +112,9 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) im->linesize = xsize * 4; im->type = IMAGING_TYPE_INT32; - } else if (strcmp(mode, "I;16") == 0 || strcmp(mode, "I;16L") == 0 \ - || strcmp(mode, "I;16B") == 0 || strcmp(mode, "I;16N") == 0) { + } else if ( + strcmp(mode, "I;16") == 0 || strcmp(mode, "I;16L") == 0 || + strcmp(mode, "I;16B") == 0 || strcmp(mode, "I;16N") == 0) { /* EXPERIMENTAL */ /* 16-bit raw integer images */ im->bands = 1; @@ -132,10 +130,10 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) } else if (strcmp(mode, "BGR;15") == 0) { /* EXPERIMENTAL */ - /* 15-bit true colour */ + /* 15-bit reversed true colour */ im->bands = 1; im->pixelsize = 2; - im->linesize = (xsize*2 + 3) & -4; + im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; } else if (strcmp(mode, "BGR;16") == 0) { @@ -143,7 +141,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) /* 16-bit reversed true colour */ im->bands = 1; im->pixelsize = 2; - im->linesize = (xsize*2 + 3) & -4; + im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; } else if (strcmp(mode, "BGR;24") == 0) { @@ -151,7 +149,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) /* 24-bit reversed true colour */ im->bands = 1; im->pixelsize = 3; - im->linesize = (xsize*3 + 3) & -4; + im->linesize = (xsize * 3 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; } else if (strcmp(mode, "BGR;32") == 0) { @@ -159,7 +157,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) /* 32-bit reversed true colour */ im->bands = 1; im->pixelsize = 4; - im->linesize = (xsize*4 + 3) & -4; + im->linesize = (xsize * 4 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; } else if (strcmp(mode, "RGBX") == 0) { @@ -204,7 +202,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) } else { free(im); - return (Imaging) ImagingError_ValueError("unrecognized image mode"); + return (Imaging)ImagingError_ValueError("unrecognized image mode"); } /* Setup image descriptor */ @@ -212,21 +210,23 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) /* Pointer array (allocate at least one line, to avoid MemoryError exceptions on platforms where calloc(0, x) returns NULL) */ - im->image = (char **) calloc((ysize > 0) ? ysize : 1, sizeof(void *)); + im->image = (char **)calloc((ysize > 0) ? ysize : 1, sizeof(void *)); - if ( ! im->image) { + if (!im->image) { free(im); - return (Imaging) ImagingError_MemoryError(); + return (Imaging)ImagingError_MemoryError(); } /* Initialize alias pointers to pixel data. */ switch (im->pixelsize) { - case 1: case 2: case 3: - im->image8 = (UINT8 **) im->image; - break; - case 4: - im->image32 = (INT32 **) im->image; - break; + case 1: + case 2: + case 3: + im->image8 = (UINT8 **)im->image; + break; + case 4: + im->image32 = (INT32 **)im->image; + break; } ImagingDefaultArena.stats_new_count += 1; @@ -235,31 +235,32 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) } Imaging -ImagingNewPrologue(const char *mode, int xsize, int ysize) -{ +ImagingNewPrologue(const char *mode, int xsize, int ysize) { return ImagingNewPrologueSubtype( mode, xsize, ysize, sizeof(struct ImagingMemoryInstance)); } void -ImagingDelete(Imaging im) -{ - if (!im) +ImagingDelete(Imaging im) { + if (!im) { return; + } - if (im->palette) + if (im->palette) { ImagingPaletteDelete(im->palette); + } - if (im->destroy) + if (im->destroy) { im->destroy(im); + } - if (im->image) + if (im->image) { free(im->image); + } free(im); } - /* Array Storage Type */ /* ------------------ */ /* Allocate image as an array of line buffers. */ @@ -267,17 +268,20 @@ ImagingDelete(Imaging im) #define IMAGING_PAGE_SIZE (4096) struct ImagingMemoryArena ImagingDefaultArena = { - 1, // alignment - 16*1024*1024, // block_size - 0, // blocks_max - 0, // blocks_cached - NULL, // blocks_pool - 0, 0, 0, 0, 0 // Stats + 1, // alignment + 16 * 1024 * 1024, // block_size + 0, // blocks_max + 0, // blocks_cached + NULL, // blocks_pool + 0, + 0, + 0, + 0, + 0 // Stats }; int -ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max) -{ +ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max) { void *p; /* Free already cached blocks */ ImagingMemoryClearCache(arena, blocks_max); @@ -287,14 +291,14 @@ ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max) arena->blocks_pool = NULL; } else if (arena->blocks_pool != NULL) { p = realloc(arena->blocks_pool, sizeof(*arena->blocks_pool) * blocks_max); - if ( ! p) { + if (!p) { // Leave previous blocks_max value return 0; } arena->blocks_pool = p; } else { arena->blocks_pool = calloc(sizeof(*arena->blocks_pool), blocks_max); - if ( ! arena->blocks_pool) { + if (!arena->blocks_pool) { return 0; } } @@ -304,8 +308,7 @@ ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max) } void -ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size) -{ +ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size) { while (arena->blocks_cached > new_size) { arena->blocks_cached -= 1; free(arena->blocks_pool[arena->blocks_cached].ptr); @@ -314,8 +317,7 @@ ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size) } ImagingMemoryBlock -memory_get_block(ImagingMemoryArena arena, int requested_size, int dirty) -{ +memory_get_block(ImagingMemoryArena arena, int requested_size, int dirty) { ImagingMemoryBlock block = {NULL, 0}; if (arena->blocks_cached > 0) { @@ -323,16 +325,16 @@ memory_get_block(ImagingMemoryArena arena, int requested_size, int dirty) arena->blocks_cached -= 1; block = arena->blocks_pool[arena->blocks_cached]; // Reallocate if needed - if (block.size != requested_size){ + if (block.size != requested_size) { block.ptr = realloc(block.ptr, requested_size); } - if ( ! block.ptr) { - // Can't allocate, free prevous pointer (it is still valid) + if (!block.ptr) { + // Can't allocate, free previous pointer (it is still valid) free(arena->blocks_pool[arena->blocks_cached].ptr); arena->stats_freed_blocks += 1; return block; } - if ( ! dirty) { + if (!dirty) { memset(block.ptr, 0, requested_size); } arena->stats_reused_blocks += 1; @@ -352,9 +354,8 @@ memory_get_block(ImagingMemoryArena arena, int requested_size, int dirty) } void -memory_return_block(ImagingMemoryArena arena, ImagingMemoryBlock block) -{ - if (arena->blocks_cached < arena->blocks_max) { +memory_return_block(ImagingMemoryArena arena, ImagingMemoryBlock block) { + if (arena->blocks_cached < arena->blocks_max) { // Reduce block size if (block.size > arena->block_size) { block.size = arena->block_size; @@ -368,15 +369,13 @@ memory_return_block(ImagingMemoryArena arena, ImagingMemoryBlock block) } } - static void -ImagingDestroyArray(Imaging im) -{ +ImagingDestroyArray(Imaging im) { int y = 0; if (im->blocks) { while (im->blocks[y].ptr) { - memory_return_block(&ImagingDefaultArena, im->blocks[y]); + memory_return_block(&ImagingDefaultArena, im->blocks[y]); y += 1; } free(im->blocks); @@ -384,8 +383,7 @@ ImagingDestroyArray(Imaging im) } Imaging -ImagingAllocateArray(Imaging im, int dirty, int block_size) -{ +ImagingAllocateArray(Imaging im, int dirty, int block_size) { int y, line_in_block, current_block; ImagingMemoryArena arena = &ImagingDefaultArena; ImagingMemoryBlock block = {NULL, 0}; @@ -393,22 +391,23 @@ ImagingAllocateArray(Imaging im, int dirty, int block_size) char *aligned_ptr = NULL; /* 0-width or 0-height image. No need to do anything */ - if ( ! im->linesize || ! im->ysize) { + if (!im->linesize || !im->ysize) { return im; } aligned_linesize = (im->linesize + arena->alignment - 1) & -arena->alignment; lines_per_block = (block_size - (arena->alignment - 1)) / aligned_linesize; - if (lines_per_block == 0) + if (lines_per_block == 0) { lines_per_block = 1; + } blocks_count = (im->ysize + lines_per_block - 1) / lines_per_block; // printf("NEW size: %dx%d, ls: %d, lpb: %d, blocks: %d\n", // im->xsize, im->ysize, aligned_linesize, lines_per_block, blocks_count); - /* One extra ponter is always NULL */ + /* One extra pointer is always NULL */ im->blocks = calloc(sizeof(*im->blocks), blocks_count + 1); - if ( ! im->blocks) { - return (Imaging) ImagingError_MemoryError(); + if (!im->blocks) { + return (Imaging)ImagingError_MemoryError(); } /* Allocate image as an array of lines */ @@ -423,9 +422,9 @@ ImagingAllocateArray(Imaging im, int dirty, int block_size) } required = lines_remaining * aligned_linesize + arena->alignment - 1; block = memory_get_block(arena, required, dirty); - if ( ! block.ptr) { + if (!block.ptr) { ImagingDestroyArray(im); - return (Imaging) ImagingError_MemoryError(); + return (Imaging)ImagingError_MemoryError(); } im->blocks[current_block] = block; /* Bulletproof code from libc _int_memalign */ @@ -449,48 +448,45 @@ ImagingAllocateArray(Imaging im, int dirty, int block_size) return im; } - /* Block Storage Type */ /* ------------------ */ /* Allocate image as a single block. */ static void -ImagingDestroyBlock(Imaging im) -{ - if (im->block) +ImagingDestroyBlock(Imaging im) { + if (im->block) { free(im->block); + } } Imaging -ImagingAllocateBlock(Imaging im) -{ +ImagingAllocateBlock(Imaging im) { Py_ssize_t y, i; /* overflow check for malloc */ - if (im->linesize && - im->ysize > INT_MAX / im->linesize) { - return (Imaging) ImagingError_MemoryError(); + if (im->linesize && im->ysize > INT_MAX / im->linesize) { + return (Imaging)ImagingError_MemoryError(); } if (im->ysize * im->linesize <= 0) { /* some platforms return NULL for malloc(0); this fix prevents MemoryError on zero-sized images on such platforms */ - im->block = (char *) malloc(1); + im->block = (char *)malloc(1); } else { /* malloc check ok, overflow check above */ - im->block = (char *) calloc(im->ysize, im->linesize); + im->block = (char *)calloc(im->ysize, im->linesize); } - if ( ! im->block) { - return (Imaging) ImagingError_MemoryError(); + if (!im->block) { + return (Imaging)ImagingError_MemoryError(); } for (y = i = 0; y < im->ysize; y++) { im->image[y] = im->block + i; i += im->linesize; } - + im->destroy = ImagingDestroyBlock; return im; @@ -501,17 +497,17 @@ ImagingAllocateBlock(Imaging im) */ Imaging -ImagingNewInternal(const char* mode, int xsize, int ysize, int dirty) -{ +ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { Imaging im; if (xsize < 0 || ysize < 0) { - return (Imaging) ImagingError_ValueError("bad image size"); + return (Imaging)ImagingError_ValueError("bad image size"); } im = ImagingNewPrologue(mode, xsize, ysize); - if ( ! im) + if (!im) { return NULL; + } if (ImagingAllocateArray(im, dirty, ImagingDefaultArena.block_size)) { return im; @@ -529,29 +525,27 @@ ImagingNewInternal(const char* mode, int xsize, int ysize, int dirty) } Imaging -ImagingNew(const char* mode, int xsize, int ysize) -{ +ImagingNew(const char *mode, int xsize, int ysize) { return ImagingNewInternal(mode, xsize, ysize, 0); } Imaging -ImagingNewDirty(const char* mode, int xsize, int ysize) -{ +ImagingNewDirty(const char *mode, int xsize, int ysize) { return ImagingNewInternal(mode, xsize, ysize, 1); } Imaging -ImagingNewBlock(const char* mode, int xsize, int ysize) -{ +ImagingNewBlock(const char *mode, int xsize, int ysize) { Imaging im; if (xsize < 0 || ysize < 0) { - return (Imaging) ImagingError_ValueError("bad image size"); + return (Imaging)ImagingError_ValueError("bad image size"); } im = ImagingNewPrologue(mode, xsize, ysize); - if ( ! im) + if (!im) { return NULL; + } if (ImagingAllocateBlock(im)) { return im; @@ -562,33 +556,32 @@ ImagingNewBlock(const char* mode, int xsize, int ysize) } Imaging -ImagingNew2Dirty(const char* mode, Imaging imOut, Imaging imIn) -{ +ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn) { /* allocate or validate output image */ if (imOut) { /* make sure images match */ - if (strcmp(imOut->mode, mode) != 0 - || imOut->xsize != imIn->xsize - || imOut->ysize != imIn->ysize) { + if (strcmp(imOut->mode, mode) != 0 || imOut->xsize != imIn->xsize || + imOut->ysize != imIn->ysize) { return ImagingError_Mismatch(); } } else { /* create new image */ imOut = ImagingNewDirty(mode, imIn->xsize, imIn->ysize); - if (!imOut) + if (!imOut) { return NULL; + } } return imOut; } void -ImagingCopyPalette(Imaging destination, Imaging source) -{ +ImagingCopyPalette(Imaging destination, Imaging source) { if (source->palette) { - if (destination->palette) + if (destination->palette) { ImagingPaletteDelete(destination->palette); + } destination->palette = ImagingPaletteDuplicate(source->palette); } } diff --git a/src/libImaging/SunRleDecode.c b/src/libImaging/SunRleDecode.c index 50d816e387f..9d8e1292a47 100644 --- a/src/libImaging/SunRleDecode.c +++ b/src/libImaging/SunRleDecode.c @@ -15,35 +15,30 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" - int -ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ +ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { int n; - UINT8* ptr; + UINT8 *ptr; UINT8 extra_data = 0; UINT8 extra_bytes = 0; ptr = buf; for (;;) { - - if (bytes < 1) + if (bytes < 1) { return ptr - buf; + } if (ptr[0] == 0x80) { - - if (bytes < 2) + if (bytes < 2) { break; + } n = ptr[1]; - if (n == 0) { - /* Literal 0x80 (2 bytes) */ n = 1; @@ -53,10 +48,10 @@ ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) bytes -= 2; } else { - /* Run (3 bytes) */ - if (bytes < 3) + if (bytes < 3) { break; + } /* from (https://www.fileformat.info/format/sunraster/egff.htm) @@ -81,7 +76,7 @@ ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) n += 1; if (state->x + n > state->bytes) { - extra_bytes = n; /* full value */ + extra_bytes = n; /* full value */ n = state->bytes - state->x; extra_bytes -= n; extra_data = ptr[2]; @@ -91,11 +86,9 @@ ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) ptr += 3; bytes -= 3; - } } else { - /* Literal byte */ n = 1; @@ -103,18 +96,18 @@ ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) ptr += 1; bytes -= 1; - } for (;;) { state->x += n; if (state->x >= state->bytes) { - /* Got a full line, unpack it */ - state->shuffle((UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->buffer, - state->xsize); + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->buffer, + state->xsize); state->x = 0; @@ -129,7 +122,7 @@ ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) } if (state->x > 0) { - break; // assert + break; // assert } if (extra_bytes >= state->bytes) { diff --git a/src/libImaging/TgaRleDecode.c b/src/libImaging/TgaRleDecode.c index cad6bc3bcfe..95ae9b62228 100644 --- a/src/libImaging/TgaRleDecode.c +++ b/src/libImaging/TgaRleDecode.c @@ -5,8 +5,8 @@ * decoder for Targa RLE data. * * history: - * 97-01-04 fl created - * 98-09-11 fl don't one byte per pixel; take orientation into account + * 97-01-04 fl created + * 98-09-11 fl don't one byte per pixel; take orientation into account * * Copyright (c) Fredrik Lundh 1997. * Copyright (c) Secret Labs AB 1997-98. @@ -14,104 +14,115 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" - int -ImagingTgaRleDecode(Imaging im, ImagingCodecState state, - UINT8* buf, int bytes) -{ +ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { int n, depth; - UINT8* ptr; + UINT8 *ptr; + int extra_bytes = 0; ptr = buf; if (state->state == 0) { - - /* check image orientation */ - if (state->ystep < 0) { - state->y = state->ysize-1; - state->ystep = -1; - } else - state->ystep = 1; - - state->state = 1; - + /* check image orientation */ + if (state->ystep < 0) { + state->y = state->ysize - 1; + state->ystep = -1; + } else { + state->ystep = 1; + } + + state->state = 1; } depth = state->count; for (;;) { + if (bytes < 1) { + return ptr - buf; + } + + n = depth * ((ptr[0] & 0x7f) + 1); + if (ptr[0] & 0x80) { + /* Run (1 + pixelsize bytes) */ + if (bytes < 1 + depth) { + break; + } - if (bytes < 1) - return ptr - buf; - - if (ptr[0] & 0x80) { - - /* Run (1 + pixelsize bytes) */ - - if (bytes < 1 + depth) - break; - - n = depth * ((ptr[0] & 0x7f) + 1); - - if (state->x + n > state->bytes) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } + if (state->x + n > state->bytes) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } - if (depth == 1) + if (depth == 1) { memset(state->buffer + state->x, ptr[1], n); - else { + } else { int i; - for (i = 0; i < n; i += depth) - memcpy(state->buffer + state->x + i, ptr+1, depth); + for (i = 0; i < n; i += depth) { + memcpy(state->buffer + state->x + i, ptr + 1, depth); + } } ptr += 1 + depth; - bytes -= 1 + depth; - - } else { - - /* Literal (1+n+1 bytes block) */ - n = depth * (ptr[0] + 1); - - if (bytes < 1 + n) - break; - - if (state->x + n > state->bytes) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } + bytes -= 1 + depth; + } else { + /* Literal (1+n+1 bytes block) */ + if (bytes < 1 + n) { + break; + } - memcpy(state->buffer + state->x, ptr + 1, n); + if (state->x + n > state->bytes) { + extra_bytes = n; /* full value */ + n = state->bytes - state->x; + extra_bytes -= n; + } - ptr += 1 + n; - bytes -= 1 + n; + memcpy(state->buffer + state->x, ptr + 1, n); - } + ptr += 1 + n; + bytes -= 1 + n; + } - state->x += n; + for (;;) { + state->x += n; - if (state->x >= state->bytes) { + if (state->x >= state->bytes) { + /* Got a full line, unpack it */ + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->buffer, + state->xsize); - /* Got a full line, unpack it */ - state->shuffle((UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->buffer, - state->xsize); + state->x = 0; - state->x = 0; + state->y += state->ystep; - state->y += state->ystep; + if (state->y < 0 || state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } + } - if (state->y < 0 || state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; + if (extra_bytes == 0) { + break; } - } + if (state->x > 0) { + break; // assert + } + if (extra_bytes >= state->bytes) { + n = state->bytes; + } else { + n = extra_bytes; + } + memcpy(state->buffer + state->x, ptr, n); + ptr += n; + bytes -= n; + extra_bytes -= n; + } } return ptr - buf; diff --git a/src/libImaging/TgaRleEncode.c b/src/libImaging/TgaRleEncode.c new file mode 100644 index 00000000000..aa7e7b96d81 --- /dev/null +++ b/src/libImaging/TgaRleEncode.c @@ -0,0 +1,157 @@ + +#include "Imaging.h" + +#include +#include + +static int +comparePixels(const UINT8 *buf, int x, int bytesPerPixel) { + buf += x * bytesPerPixel; + return memcmp(buf, buf + bytesPerPixel, bytesPerPixel) == 0; +} + +int +ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + UINT8 *dst; + int bytesPerPixel; + + if (state->state == 0) { + if (state->ystep < 0) { + state->ystep = -1; + state->y = state->ysize - 1; + } else { + state->ystep = 1; + } + + state->state = 1; + } + + dst = buf; + bytesPerPixel = (state->bits + 7) / 8; + + while (1) { + int flushCount; + + /* + * state->count is the numbers of bytes in the packet, + * excluding the 1-byte descriptor. + */ + if (state->count == 0) { + UINT8 *row; + UINT8 descriptor; + int startX; + + assert(state->x <= state->xsize); + + /* Make sure we have space for the descriptor. */ + if (bytes < 1) { + break; + } + + if (state->x == state->xsize) { + state->x = 0; + + state->y += state->ystep; + if (state->y < 0 || state->y >= state->ysize) { + state->errcode = IMAGING_CODEC_END; + break; + } + } + + if (state->x == 0) { + state->shuffle( + state->buffer, + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->xsize); + } + + row = state->buffer; + + /* Start with a raw packet for 1 px. */ + descriptor = 0; + startX = state->x; + state->count = bytesPerPixel; + + if (state->x + 1 < state->xsize) { + int maxLookup; + int isRaw; + + isRaw = !comparePixels(row, state->x, bytesPerPixel); + ++state->x; + + /* + * A packet can contain up to 128 pixels; + * 2 are already behind (state->x points to + * the second one). + */ + maxLookup = state->x + 126; + /* A packet must not span multiple rows. */ + if (maxLookup > state->xsize - 1) { + maxLookup = state->xsize - 1; + } + + if (isRaw) { + while (state->x < maxLookup) { + if (!comparePixels(row, state->x, bytesPerPixel)) { + ++state->x; + } else { + /* Two identical pixels will go to RLE packet. */ + --state->x; + break; + } + } + + state->count += (state->x - startX) * bytesPerPixel; + } else { + descriptor |= 0x80; + + while (state->x < maxLookup) { + if (comparePixels(row, state->x, bytesPerPixel)) { + ++state->x; + } else { + break; + } + } + } + } + + /* + * state->x currently points to the last pixel to be + * included in the packet. The pixel count in the + * descriptor is 1 less than actual number of pixels in + * the packet, that is, state->x == startX if we encode + * only 1 pixel. + */ + descriptor += state->x - startX; + *dst++ = descriptor; + --bytes; + + /* Advance to past-the-last encoded pixel. */ + ++state->x; + } + + assert(bytes >= 0); + assert(state->count > 0); + assert(state->x > 0); + assert(state->count <= state->x * bytesPerPixel); + + if (bytes == 0) { + break; + } + + flushCount = state->count; + if (flushCount > bytes) { + flushCount = bytes; + } + + memcpy( + dst, state->buffer + (state->x * bytesPerPixel - state->count), flushCount); + dst += flushCount; + bytes -= flushCount; + + state->count -= flushCount; + } + + return dst - buf; +} diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index f292da3883d..428cd93d278 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -20,419 +20,969 @@ #include "TiffDecode.h" -void dump_state(const TIFFSTATE *state){ - TRACE(("State: Location %u size %d eof %d data: %p ifd: %d\n", (uint)state->loc, - (int)state->size, (uint)state->eof, state->data, state->ifd)); +/* Convert C file descriptor to WinApi HFILE if LibTiff was compiled with tif_win32.c + * + * This cast is safe, as the top 32-bits of HFILE are guaranteed to be zero, + * see + * https://learn.microsoft.com/en-us/windows/win32/winprog64/interprocess-communication + */ +#ifndef USE_WIN32_FILEIO +#define fd_to_tiff_fd(fd) (fd) +#else +#define fd_to_tiff_fd(fd) ((int)_get_osfhandle(fd)) +#endif + +void +dump_state(const TIFFSTATE *state) { + TRACE( + ("State: Location %u size %d eof %d data: %p ifd: %d\n", + (uint)state->loc, + (int)state->size, + (uint)state->eof, + state->data, + state->ifd)); } /* procs for TIFFOpenClient */ -tsize_t _tiffReadProc(thandle_t hdata, tdata_t buf, tsize_t size) { - TIFFSTATE *state = (TIFFSTATE *)hdata; - tsize_t to_read; +tsize_t +_tiffReadProc(thandle_t hdata, tdata_t buf, tsize_t size) { + TIFFSTATE *state = (TIFFSTATE *)hdata; + tsize_t to_read; - TRACE(("_tiffReadProc: %d \n", (int)size)); - dump_state(state); + TRACE(("_tiffReadProc: %d \n", (int)size)); + dump_state(state); - to_read = min(size, min(state->size, (tsize_t)state->eof) - (tsize_t)state->loc); - TRACE(("to_read: %d\n", (int)to_read)); + if (state->loc > state->eof) { + TIFFError("_tiffReadProc", "Invalid Read at loc %" PRIu64 ", eof: %" PRIu64, state->loc, state->eof); + return 0; + } + to_read = min(size, min(state->size, (tsize_t)state->eof) - (tsize_t)state->loc); + TRACE(("to_read: %d\n", (int)to_read)); - _TIFFmemcpy(buf, (UINT8 *)state->data + state->loc, to_read); - state->loc += (toff_t)to_read; + _TIFFmemcpy(buf, (UINT8 *)state->data + state->loc, to_read); + state->loc += (toff_t)to_read; - TRACE( ("location: %u\n", (uint)state->loc)); - return to_read; + TRACE(("location: %u\n", (uint)state->loc)); + return to_read; } -tsize_t _tiffWriteProc(thandle_t hdata, tdata_t buf, tsize_t size) { - TIFFSTATE *state = (TIFFSTATE *)hdata; - tsize_t to_write; +tsize_t +_tiffWriteProc(thandle_t hdata, tdata_t buf, tsize_t size) { + TIFFSTATE *state = (TIFFSTATE *)hdata; + tsize_t to_write; - TRACE(("_tiffWriteProc: %d \n", (int)size)); - dump_state(state); + TRACE(("_tiffWriteProc: %d \n", (int)size)); + dump_state(state); - to_write = min(size, state->size - (tsize_t)state->loc); - if (state->flrealloc && size>to_write) { - tdata_t new; - tsize_t newsize=state->size; - while (newsize < (size + state->size)) { - if (newsize > INT_MAX - 64*1024){ + to_write = min(size, state->size - (tsize_t)state->loc); + if (state->flrealloc && size > to_write) { + tdata_t new_data; + tsize_t newsize = state->size; + while (newsize < (size + state->size)) { + if (newsize > INT_MAX - 64 * 1024) { return 0; } - newsize += 64*1024; - // newsize*=2; // UNDONE, by 64k chunks? - } - TRACE(("Reallocing in write to %d bytes\n", (int)newsize)); + newsize += 64 * 1024; + // newsize*=2; // UNDONE, by 64k chunks? + } + TRACE(("Reallocing in write to %d bytes\n", (int)newsize)); /* malloc check ok, overflow checked above */ - new = realloc(state->data, newsize); - if (!new) { - // fail out - return 0; - } - state->data = new; - state->size = newsize; - to_write = size; - } - - TRACE(("to_write: %d\n", (int)to_write)); - - _TIFFmemcpy((UINT8 *)state->data + state->loc, buf, to_write); - state->loc += (toff_t)to_write; - state->eof = max(state->loc, state->eof); - - dump_state(state); - return to_write; + new_data = realloc(state->data, newsize); + if (!new_data) { + // fail out + return 0; + } + state->data = new_data; + state->size = newsize; + to_write = size; + } + + TRACE(("to_write: %d\n", (int)to_write)); + + _TIFFmemcpy((UINT8 *)state->data + state->loc, buf, to_write); + state->loc += (toff_t)to_write; + state->eof = max(state->loc, state->eof); + + dump_state(state); + return to_write; } -toff_t _tiffSeekProc(thandle_t hdata, toff_t off, int whence) { - TIFFSTATE *state = (TIFFSTATE *)hdata; - - TRACE(("_tiffSeekProc: off: %u whence: %d \n", (uint)off, whence)); - dump_state(state); - switch (whence) { - case 0: - state->loc = off; - break; - case 1: - state->loc += off; - break; - case 2: - state->loc = state->eof + off; - break; - } - dump_state(state); - return state->loc; +toff_t +_tiffSeekProc(thandle_t hdata, toff_t off, int whence) { + TIFFSTATE *state = (TIFFSTATE *)hdata; + + TRACE(("_tiffSeekProc: off: %u whence: %d \n", (uint)off, whence)); + dump_state(state); + switch (whence) { + case 0: + state->loc = off; + break; + case 1: + state->loc += off; + break; + case 2: + state->loc = state->eof + off; + break; + } + dump_state(state); + return state->loc; } -int _tiffCloseProc(thandle_t hdata) { - TIFFSTATE *state = (TIFFSTATE *)hdata; +int +_tiffCloseProc(thandle_t hdata) { + TIFFSTATE *state = (TIFFSTATE *)hdata; - TRACE(("_tiffCloseProc \n")); - dump_state(state); + TRACE(("_tiffCloseProc \n")); + dump_state(state); - return 0; + return 0; } +toff_t +_tiffSizeProc(thandle_t hdata) { + TIFFSTATE *state = (TIFFSTATE *)hdata; -toff_t _tiffSizeProc(thandle_t hdata) { - TIFFSTATE *state = (TIFFSTATE *)hdata; - - TRACE(("_tiffSizeProc \n")); - dump_state(state); + TRACE(("_tiffSizeProc \n")); + dump_state(state); - return (toff_t)state->size; + return (toff_t)state->size; } -int _tiffMapProc(thandle_t hdata, tdata_t* pbase, toff_t* psize) { - TIFFSTATE *state = (TIFFSTATE *)hdata; - TRACE(("_tiffMapProc input size: %u, data: %p\n", (uint)*psize, *pbase)); - dump_state(state); +int +_tiffMapProc(thandle_t hdata, tdata_t *pbase, toff_t *psize) { + TIFFSTATE *state = (TIFFSTATE *)hdata; - *pbase = state->data; - *psize = state->size; - TRACE(("_tiffMapProc returning size: %u, data: %p\n", (uint)*psize, *pbase)); - return (1); + TRACE(("_tiffMapProc input size: %u, data: %p\n", (uint)*psize, *pbase)); + dump_state(state); + + *pbase = state->data; + *psize = state->size; + TRACE(("_tiffMapProc returning size: %u, data: %p\n", (uint)*psize, *pbase)); + return (1); } -int _tiffNullMapProc(thandle_t hdata, tdata_t* pbase, toff_t* psize) { - (void) hdata; (void) pbase; (void) psize; - return (0); +int +_tiffNullMapProc(thandle_t hdata, tdata_t *pbase, toff_t *psize) { + (void)hdata; + (void)pbase; + (void)psize; + return (0); } -void _tiffUnmapProc(thandle_t hdata, tdata_t base, toff_t size) { - TRACE(("_tiffUnMapProc\n")); - (void) hdata; (void) base; (void) size; +void +_tiffUnmapProc(thandle_t hdata, tdata_t base, toff_t size) { + TRACE(("_tiffUnMapProc\n")); + (void)hdata; + (void)base; + (void)size; } -int ImagingLibTiffInit(ImagingCodecState state, int fp, int offset) { - TIFFSTATE *clientstate = (TIFFSTATE *)state->context; +int +ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset) { + TIFFSTATE *clientstate = (TIFFSTATE *)state->context; TRACE(("initing libtiff\n")); - TRACE(("filepointer: %d \n", fp)); - TRACE(("State: count %d, state %d, x %d, y %d, ystep %d\n", state->count, state->state, - state->x, state->y, state->ystep)); - TRACE(("State: xsize %d, ysize %d, xoff %d, yoff %d \n", state->xsize, state->ysize, - state->xoff, state->yoff)); - TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); - TRACE(("State: context %p \n", state->context)); - - clientstate->loc = 0; - clientstate->size = 0; - clientstate->data = 0; - clientstate->fp = fp; + TRACE(("filepointer: %d \n", fp)); + TRACE( + ("State: count %d, state %d, x %d, y %d, ystep %d\n", + state->count, + state->state, + state->x, + state->y, + state->ystep)); + TRACE( + ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", + state->xsize, + state->ysize, + state->xoff, + state->yoff)); + TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); + TRACE(("State: context %p \n", state->context)); + + clientstate->loc = 0; + clientstate->size = 0; + clientstate->data = 0; + clientstate->fp = fp; clientstate->ifd = offset; - clientstate->eof = 0; + clientstate->eof = 0; return 1; } -int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes) { - TIFFSTATE *clientstate = (TIFFSTATE *)state->context; - char *filename = "tempfile.tif"; - char *mode = "r"; - TIFF *tiff; - tsize_t size; +int +_pickUnpackers(Imaging im, ImagingCodecState state, TIFF *tiff, uint16_t planarconfig, ImagingShuffler *unpackers) { + // if number of bands is 1, there is no difference with contig case + if (planarconfig == PLANARCONFIG_SEPARATE && im->bands > 1) { + uint16_t bits_per_sample = 8; + + TIFFGetFieldDefaulted(tiff, TIFFTAG_BITSPERSAMPLE, &bits_per_sample); + if (bits_per_sample != 8 && bits_per_sample != 16) { + TRACE(("Invalid value for bits per sample: %d\n", bits_per_sample)); + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + // We'll pick appropriate set of unpackers depending on planar_configuration + // It does not matter if data is RGB(A), CMYK or LUV really, + // we just copy it plane by plane + unpackers[0] = ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "R;16N" : "R", NULL); + unpackers[1] = ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "G;16N" : "G", NULL); + unpackers[2] = ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "B;16N" : "B", NULL); + unpackers[3] = ImagingFindUnpacker("RGBA", bits_per_sample == 16 ? "A;16N" : "A", NULL); + + return im->bands; + } else { + unpackers[0] = state->shuffle; + + return 1; + } +} + +int +_decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) { + // To avoid dealing with YCbCr subsampling and other complications, let libtiff handle it + // Use a TIFFRGBAImage wrapping the tiff image, and let libtiff handle + // all of the conversion. Metadata read from the TIFFRGBAImage could + // be different from the metadata that the base tiff returns. + + INT32 current_row; + UINT8 *new_data; + UINT32 rows_per_block, row_byte_size, rows_to_read; + int ret; + TIFFRGBAImage img; + char emsg[1024] = ""; + + // Since using TIFFRGBAImage* functions, we can read whole tiff into rastrr in one call + // Let's select smaller block size. Multiplying image width by (tile length OR rows per strip) + // gives us manageable block size in pixels + if (TIFFIsTiled(tiff)) { + ret = TIFFGetFieldDefaulted(tiff, TIFFTAG_TILELENGTH, &rows_per_block); + } + else { + ret = TIFFGetFieldDefaulted(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_block); + } + + if (ret != 1 || rows_per_block==(UINT32)(-1)) { + rows_per_block = state->ysize; + } + + TRACE(("RowsPerBlock: %u \n", rows_per_block)); + + if (!(TIFFRGBAImageOK(tiff, emsg) && TIFFRGBAImageBegin(&img, tiff, 0, emsg))) { + TRACE(("Decode error, msg: %s", emsg)); + state->errcode = IMAGING_CODEC_BROKEN; + // nothing to clean up, just return + return -1; + } + + img.req_orientation = ORIENTATION_TOPLEFT; + img.col_offset = 0; + + /* overflow check for row byte size */ + if (INT_MAX / 4 < img.width) { + state->errcode = IMAGING_CODEC_MEMORY; + goto decodergba_err; + } + + // TiffRGBAImages are 32bits/pixel. + row_byte_size = img.width * 4; + + /* overflow check for realloc */ + if (INT_MAX / row_byte_size < rows_per_block) { + state->errcode = IMAGING_CODEC_MEMORY; + goto decodergba_err; + } + + state->bytes = rows_per_block * row_byte_size; + + TRACE(("BlockSize: %d \n", state->bytes)); + + /* realloc to fit whole strip */ + /* malloc check above */ + new_data = realloc(state->buffer, state->bytes); + if (!new_data) { + state->errcode = IMAGING_CODEC_MEMORY; + goto decodergba_err; + } + + state->buffer = new_data; + + for (; state->y < state->ysize; state->y += rows_per_block) { + img.row_offset = state->y; + rows_to_read = min(rows_per_block, img.height - state->y); + + if (!TIFFRGBAImageGet(&img, (UINT32 *)state->buffer, img.width, rows_to_read)) { + TRACE(("Decode Error, y: %d\n", state->y)); + state->errcode = IMAGING_CODEC_BROKEN; + goto decodergba_err; + } + +#if WORDS_BIGENDIAN + TIFFSwabArrayOfLong((UINT32 *)state->buffer, img.width * rows_to_read); +#endif + + TRACE(("Decoded strip for row %d \n", state->y)); + + // iterate over each row in the strip and stuff data into image + for (current_row = 0; + current_row < min((INT32)rows_per_block, state->ysize - state->y); + current_row++) { + TRACE(("Writing data into line %d ; \n", state->y + current_row)); + + // UINT8 * bbb = state->buffer + current_row * (state->bytes / + // rows_per_block); TRACE(("chars: %x %x %x %x\n", ((UINT8 *)bbb)[0], + // ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3])); + + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff + current_row] + + state->xoff * im->pixelsize, + state->buffer + current_row * row_byte_size, + state->xsize); + } + } + +decodergba_err: + TIFFRGBAImageEnd(&img); + if (state->errcode != 0) { + return -1; + } + return 0; +} + +int +_decodeTile(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, ImagingShuffler *unpackers) { + INT32 x, y, tile_y, current_tile_length, current_tile_width; + UINT32 tile_width, tile_length; + tsize_t tile_bytes_size, row_byte_size; + UINT8 *new_data; + + tile_bytes_size = TIFFTileSize(tiff); + + if (tile_bytes_size == 0) { + TRACE(("Decode Error, Can not calculate TileSize\n")); + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + row_byte_size = TIFFTileRowSize(tiff); + + if (row_byte_size == 0 || row_byte_size > tile_bytes_size) { + TRACE(("Decode Error, Can not calculate TileRowSize\n")); + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + /* overflow check for realloc */ + if (tile_bytes_size > INT_MAX - 1) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + + TIFFGetField(tiff, TIFFTAG_TILEWIDTH, &tile_width); + TIFFGetField(tiff, TIFFTAG_TILELENGTH, &tile_length); + + if (tile_width > INT_MAX || tile_length > INT_MAX) { + // state->x and state->y are ints + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + + if (tile_bytes_size > ((tile_length * state->bits / planes + 7) / 8) * tile_width) { + // If the tile size as expected by LibTiff isn't what we're expecting, abort. + // man: TIFFTileSize returns the equivalent size for a tile of data as it would be returned in a + // call to TIFFReadTile ... + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + state->bytes = tile_bytes_size; + + TRACE(("TIFFTileSize: %d\n", state->bytes)); + + /* realloc to fit whole tile */ + /* malloc check above */ + new_data = realloc(state->buffer, state->bytes); + if (!new_data) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + state->buffer = new_data; + + for (y = state->yoff; y < state->ysize; y += tile_length) { + int plane; + for (plane = 0; plane < planes; plane++) { + ImagingShuffler shuffler = unpackers[plane]; + for (x = state->xoff; x < state->xsize; x += tile_width) { + if (TIFFReadTile(tiff, (tdata_t)state->buffer, x, y, 0, plane) == -1) { + TRACE(("Decode Error, Tile at %dx%d\n", x, y)); + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + TRACE(("Read tile at %dx%d; \n\n", x, y)); + + current_tile_width = min((INT32) tile_width, state->xsize - x); + current_tile_length = min((INT32) tile_length, state->ysize - y); + // iterate over each line in the tile and stuff data into image + for (tile_y = 0; tile_y < current_tile_length; tile_y++) { + TRACE(("Writing tile data at %dx%d using tile_width: %d; \n", tile_y + y, x, current_tile_width)); + + // UINT8 * bbb = state->buffer + tile_y * row_byte_size; + // TRACE(("chars: %x%x%x%x\n", ((UINT8 *)bbb)[0], ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3])); + + shuffler((UINT8*) im->image[tile_y + y] + x * im->pixelsize, + state->buffer + tile_y * row_byte_size, + current_tile_width + ); + } + } + } + } + + return 0; +} + +int +_decodeStrip(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, ImagingShuffler *unpackers) { + INT32 strip_row = 0; + UINT8 *new_data; + UINT32 rows_per_strip; + int ret; + tsize_t strip_size, row_byte_size, unpacker_row_byte_size; + + ret = TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_strip); + if (ret != 1 || rows_per_strip==(UINT32)(-1)) { + rows_per_strip = state->ysize; + } + + if (rows_per_strip > INT_MAX) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + + TRACE(("RowsPerStrip: %u\n", rows_per_strip)); + + strip_size = TIFFStripSize(tiff); + if (strip_size > INT_MAX - 1) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + + unpacker_row_byte_size = (state->xsize * state->bits / planes + 7) / 8; + if (strip_size > (unpacker_row_byte_size * rows_per_strip)) { + // If the strip size as expected by LibTiff isn't what we're expecting, abort. + // man: TIFFStripSize returns the equivalent size for a strip of data as it would be returned in a + // call to TIFFReadEncodedStrip ... + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + state->bytes = strip_size; + + TRACE(("StripSize: %d \n", state->bytes)); + + row_byte_size = TIFFScanlineSize(tiff); + + // if the unpacker calculated row size is > row byte size, (at least) the last + // row of the strip will have a read buffer overflow. + if (row_byte_size == 0 || unpacker_row_byte_size > row_byte_size) { + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + TRACE(("RowsByteSize: %u \n", row_byte_size)); + + /* realloc to fit whole strip */ + /* malloc check above */ + new_data = realloc(state->buffer, state->bytes); + if (!new_data) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + + state->buffer = new_data; + + for (; state->y < state->ysize; state->y += rows_per_strip) { + int plane; + for (plane = 0; plane < planes; plane++) { + ImagingShuffler shuffler = unpackers[plane]; + if (TIFFReadEncodedStrip(tiff, TIFFComputeStrip(tiff, state->y, plane), (tdata_t)state->buffer, strip_size) == -1) { + TRACE(("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0))); + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + TRACE(("Decoded strip for row %d \n", state->y)); - /* buffer is the encoded file, bytes is the length of the encoded file */ - /* it all ends up in state->buffer, which is a uint8* from Imaging.h */ + // iterate over each row in the strip and stuff data into image + for (strip_row = 0; + strip_row < min((INT32) rows_per_strip, state->ysize - state->y); + strip_row++) { + TRACE(("Writing data into line %d ; \n", state->y + strip_row)); + + // UINT8 * bbb = state->buffer + strip_row * (state->bytes / rows_per_strip); + // TRACE(("chars: %x %x %x %x\n", ((UINT8 *)bbb)[0], ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3])); + + shuffler( + (UINT8*) im->image[state->y + state->yoff + strip_row] + + state->xoff * im->pixelsize, + state->buffer + strip_row * row_byte_size, + state->xsize); + } + } + } + + return 0; +} + +int +ImagingLibTiffDecode( + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes) { + TIFFSTATE *clientstate = (TIFFSTATE *)state->context; + char *filename = "tempfile.tif"; + char *mode = "rC"; + TIFF *tiff; + uint16_t photometric = 0; // init to not PHOTOMETRIC_YCBCR + uint16_t compression; + int readAsRGBA = 0; + uint16_t planarconfig = 0; + int planes = 1; + ImagingShuffler unpackers[4]; + INT32 img_width, img_height; + + memset(unpackers, 0, sizeof(ImagingShuffler) * 4); + + /* buffer is the encoded file, bytes is the length of the encoded file */ + /* it all ends up in state->buffer, which is a uint8* from Imaging.h */ TRACE(("in decoder: bytes %d\n", bytes)); - TRACE(("State: count %d, state %d, x %d, y %d, ystep %d\n", state->count, state->state, - state->x, state->y, state->ystep)); - TRACE(("State: xsize %d, ysize %d, xoff %d, yoff %d \n", state->xsize, state->ysize, - state->xoff, state->yoff)); - TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); - TRACE(("Buffer: %p: %c%c%c%c\n", buffer, (char)buffer[0], (char)buffer[1],(char)buffer[2], (char)buffer[3])); - TRACE(("State->Buffer: %c%c%c%c\n", (char)state->buffer[0], (char)state->buffer[1],(char)state->buffer[2], (char)state->buffer[3])); - TRACE(("Image: mode %s, type %d, bands: %d, xsize %d, ysize %d \n", - im->mode, im->type, im->bands, im->xsize, im->ysize)); - TRACE(("Image: image8 %p, image32 %p, image %p, block %p \n", - im->image8, im->image32, im->image, im->block)); - TRACE(("Image: pixelsize: %d, linesize %d \n", - im->pixelsize, im->linesize)); - - dump_state(clientstate); - clientstate->size = bytes; - clientstate->eof = clientstate->size; - clientstate->loc = 0; - clientstate->data = (tdata_t)buffer; - clientstate->flrealloc = 0; - dump_state(clientstate); + TRACE( + ("State: count %d, state %d, x %d, y %d, ystep %d\n", + state->count, + state->state, + state->x, + state->y, + state->ystep)); + TRACE( + ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", + state->xsize, + state->ysize, + state->xoff, + state->yoff)); + TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); + TRACE( + ("Buffer: %p: %c%c%c%c\n", + buffer, + (char)buffer[0], + (char)buffer[1], + (char)buffer[2], + (char)buffer[3])); + TRACE( + ("State->Buffer: %c%c%c%c\n", + (char)state->buffer[0], + (char)state->buffer[1], + (char)state->buffer[2], + (char)state->buffer[3])); + TRACE( + ("Image: mode %s, type %d, bands: %d, xsize %d, ysize %d \n", + im->mode, + im->type, + im->bands, + im->xsize, + im->ysize)); + TRACE( + ("Image: image8 %p, image32 %p, image %p, block %p \n", + im->image8, + im->image32, + im->image, + im->block)); + TRACE(("Image: pixelsize: %d, linesize %d \n", im->pixelsize, im->linesize)); + + dump_state(clientstate); + clientstate->size = bytes; + clientstate->eof = clientstate->size; + clientstate->loc = 0; + clientstate->data = (tdata_t)buffer; + clientstate->flrealloc = 0; + dump_state(clientstate); TIFFSetWarningHandler(NULL); TIFFSetWarningHandlerExt(NULL); - if (clientstate->fp) { - TRACE(("Opening using fd: %d\n",clientstate->fp)); - lseek(clientstate->fp,0,SEEK_SET); // Sometimes, I get it set to the end. - tiff = TIFFFdOpen(clientstate->fp, filename, mode); - } else { - TRACE(("Opening from string\n")); - tiff = TIFFClientOpen(filename, mode, - (thandle_t) clientstate, - _tiffReadProc, _tiffWriteProc, - _tiffSeekProc, _tiffCloseProc, _tiffSizeProc, - _tiffMapProc, _tiffUnmapProc); - } - - if (!tiff){ - TRACE(("Error, didn't get the tiff\n")); - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } - - if (clientstate->ifd){ - int rv; - uint32 ifdoffset = clientstate->ifd; - TRACE(("reading tiff ifd %u\n", ifdoffset)); - rv = TIFFSetSubDirectory(tiff, ifdoffset); - if (!rv){ - TRACE(("error in TIFFSetSubDirectory")); - return -1; - } - } - - size = TIFFScanlineSize(tiff); - TRACE(("ScanlineSize: %d \n", size)); - if (size > state->bytes) { - TRACE(("Error, scanline size > buffer size\n")); - state->errcode = IMAGING_CODEC_BROKEN; - TIFFClose(tiff); - return -1; - } - - // Have to do this row by row and shove stuff into the buffer that way, - // with shuffle. (or, just alloc a buffer myself, then figure out how to get it - // back in. Can't use read encoded stripe. - - // This thing pretty much requires that I have the whole image in one shot. - // Perhaps a stub version would work better??? - while(state->y < state->ysize){ - if (TIFFReadScanline(tiff, (tdata_t)state->buffer, (uint32)state->y, 0) == -1) { - TRACE(("Decode Error, row %d\n", state->y)); - state->errcode = IMAGING_CODEC_BROKEN; - TIFFClose(tiff); - return -1; - } - /* TRACE(("Decoded row %d \n", state->y)); */ - state->shuffle((UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->buffer, - state->xsize); - - state->y++; - } - - TIFFClose(tiff); - TRACE(("Done Decoding, Returning \n")); - // Returning -1 here to force ImageFile.load to break, rather than - // even think about looping back around. - return -1; + if (clientstate->fp) { + TRACE(("Opening using fd: %d\n", clientstate->fp)); + lseek(clientstate->fp, 0, SEEK_SET); // Sometimes, I get it set to the end. + tiff = TIFFFdOpen(fd_to_tiff_fd(clientstate->fp), filename, mode); + } else { + TRACE(("Opening from string\n")); + tiff = TIFFClientOpen( + filename, + mode, + (thandle_t)clientstate, + _tiffReadProc, + _tiffWriteProc, + _tiffSeekProc, + _tiffCloseProc, + _tiffSizeProc, + _tiffMapProc, + _tiffUnmapProc); + } + + if (!tiff) { + TRACE(("Error, didn't get the tiff\n")); + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } + + if (clientstate->ifd) { + int rv; + uint32_t ifdoffset = clientstate->ifd; + TRACE(("reading tiff ifd %u\n", ifdoffset)); + rv = TIFFSetSubDirectory(tiff, ifdoffset); + if (!rv) { + TRACE(("error in TIFFSetSubDirectory")); + goto decode_err; + } + } + + TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &img_width); + TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &img_height); + + if (state->xsize != img_width || state->ysize != img_height) { + TRACE( + ("Inconsistent Image Error: %d =? %d, %d =? %d", + state->xsize, + img_width, + state->ysize, + img_height)); + state->errcode = IMAGING_CODEC_BROKEN; + goto decode_err; + } + + + TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric); + TIFFGetField(tiff, TIFFTAG_COMPRESSION, &compression); + TIFFGetFieldDefaulted(tiff, TIFFTAG_PLANARCONFIG, &planarconfig); + + // Dealing with YCbCr images is complicated in case if subsampling + // Let LibTiff read them as RGBA + readAsRGBA = photometric == PHOTOMETRIC_YCBCR; + + if (readAsRGBA && compression == COMPRESSION_JPEG && planarconfig == PLANARCONFIG_CONTIG) { + // If using new JPEG compression, let libjpeg do RGB conversion for performance reasons + TIFFSetField(tiff, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB); + readAsRGBA = 0; + } + + if (readAsRGBA) { + _decodeAsRGBA(im, state, tiff); + } + else { + planes = _pickUnpackers(im, state, tiff, planarconfig, unpackers); + if (planes <= 0) { + goto decode_err; + } + + if (TIFFIsTiled(tiff)) { + _decodeTile(im, state, tiff, planes, unpackers); + } + else { + _decodeStrip(im, state, tiff, planes, unpackers); + } + + if (!state->errcode) { + // Check if raw mode was RGBa and it was stored on separate planes + // so we have to convert it to RGBA + if (planes > 3 && strcmp(im->mode, "RGBA") == 0) { + uint16_t extrasamples; + uint16_t* sampleinfo; + ImagingShuffler shuffle; + INT32 y; + + TIFFGetFieldDefaulted(tiff, TIFFTAG_EXTRASAMPLES, &extrasamples, &sampleinfo); + + if (extrasamples >= 1 && + (sampleinfo[0] == EXTRASAMPLE_UNSPECIFIED || sampleinfo[0] == EXTRASAMPLE_ASSOCALPHA) + ) { + shuffle = ImagingFindUnpacker("RGBA", "RGBa", NULL); + + for (y = state->yoff; y < state->ysize; y++) { + UINT8* ptr = (UINT8*) im->image[y + state->yoff] + + state->xoff * im->pixelsize; + shuffle(ptr, ptr, state->xsize); + } + } + } + } + } + + decode_err: + TIFFClose(tiff); + TRACE(("Done Decoding, Returning \n")); + // Returning -1 here to force ImageFile.load to break, rather than + // even think about looping back around. + return -1; } -int ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp) { - // Open the FD or the pointer as a tiff file, for writing. - // We may have to do some monkeying around to make this really work. - // If we have a fp, then we're good. - // If we have a memory string, we're probably going to have to malloc, then - // shuffle bytes into the writescanline process. - // Going to have to deal with the directory as well. +int +ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp) { + // Open the FD or the pointer as a tiff file, for writing. + // We may have to do some monkeying around to make this really work. + // If we have a fp, then we're good. + // If we have a memory string, we're probably going to have to malloc, then + // shuffle bytes into the writescanline process. + // Going to have to deal with the directory as well. - TIFFSTATE *clientstate = (TIFFSTATE *)state->context; - int bufsize = 64*1024; - char *mode = "w"; + TIFFSTATE *clientstate = (TIFFSTATE *)state->context; + int bufsize = 64 * 1024; + char *mode = "w"; TRACE(("initing libtiff\n")); - TRACE(("Filename %s, filepointer: %d \n", filename, fp)); - TRACE(("State: count %d, state %d, x %d, y %d, ystep %d\n", state->count, state->state, - state->x, state->y, state->ystep)); - TRACE(("State: xsize %d, ysize %d, xoff %d, yoff %d \n", state->xsize, state->ysize, - state->xoff, state->yoff)); - TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); - TRACE(("State: context %p \n", state->context)); - - clientstate->loc = 0; - clientstate->size = 0; - clientstate->eof =0; - clientstate->data = 0; - clientstate->flrealloc = 0; - clientstate->fp = fp; - - state->state = 0; - - if (fp) { - TRACE(("Opening using fd: %d for writing \n",clientstate->fp)); - clientstate->tiff = TIFFFdOpen(clientstate->fp, filename, mode); - } else { - // malloc 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); - clientstate->size = bufsize; - clientstate->flrealloc=1; - - if (!clientstate->data) { - TRACE(("Error, couldn't allocate a buffer of size %d\n", bufsize)); - return 0; - } - - clientstate->tiff = TIFFClientOpen(filename, mode, - (thandle_t) clientstate, - _tiffReadProc, _tiffWriteProc, - _tiffSeekProc, _tiffCloseProc, _tiffSizeProc, - _tiffNullMapProc, _tiffUnmapProc); /*force no mmap*/ - - } - - if (!clientstate->tiff) { - TRACE(("Error, couldn't open tiff file\n")); - return 0; - } + TRACE(("Filename %s, filepointer: %d \n", filename, fp)); + TRACE( + ("State: count %d, state %d, x %d, y %d, ystep %d\n", + state->count, + state->state, + state->x, + state->y, + state->ystep)); + TRACE( + ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", + state->xsize, + state->ysize, + state->xoff, + state->yoff)); + TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); + TRACE(("State: context %p \n", state->context)); + + clientstate->loc = 0; + clientstate->size = 0; + clientstate->eof = 0; + clientstate->data = 0; + clientstate->flrealloc = 0; + clientstate->fp = fp; + + state->state = 0; + + if (fp) { + TRACE(("Opening using fd: %d for writing \n", clientstate->fp)); + clientstate->tiff = TIFFFdOpen(fd_to_tiff_fd(clientstate->fp), filename, mode); + } else { + // 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")); + /* calloc check ok, small constant allocation */ + clientstate->data = calloc(bufsize, 1); + clientstate->size = bufsize; + clientstate->flrealloc = 1; + + if (!clientstate->data) { + TRACE(("Error, couldn't allocate a buffer of size %d\n", bufsize)); + return 0; + } + + clientstate->tiff = TIFFClientOpen( + filename, + mode, + (thandle_t)clientstate, + _tiffReadProc, + _tiffWriteProc, + _tiffSeekProc, + _tiffCloseProc, + _tiffSizeProc, + _tiffNullMapProc, + _tiffUnmapProc); /*force no mmap*/ + } + + if (!clientstate->tiff) { + TRACE(("Error, couldn't open tiff file\n")); + return 0; + } return 1; - } -int ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...){ - // after tif_dir.c->TIFFSetField. - TIFFSTATE *clientstate = (TIFFSTATE *)state->context; - va_list ap; - int status; - - va_start(ap, tag); - status = TIFFVSetField(clientstate->tiff, tag, ap); - va_end(ap); - return status; +int +ImagingLibTiffMergeFieldInfo( + ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length) { + // Refer to libtiff docs (http://www.simplesystems.org/libtiff/addingtags.html) + TIFFSTATE *clientstate = (TIFFSTATE *)state->context; + uint32_t n; + int status = 0; + + // custom fields added with ImagingLibTiffMergeFieldInfo are only used for + // decoding, ignore readcount; + int readcount = is_var_length ? TIFF_VARIABLE : 1; + // we support writing a single value, or a variable number of values + int writecount = is_var_length ? TIFF_VARIABLE : 1; + // whether the first value should encode the number of values. + int passcount = (is_var_length && field_type != TIFF_ASCII) ? 1 : 0; + + TIFFFieldInfo info[] = { + {key, + readcount, + writecount, + field_type, + FIELD_CUSTOM, + 1, + passcount, + "CustomField"}}; + + n = sizeof(info) / sizeof(info[0]); + + // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 +#if TIFFLIB_VERSION >= 20111221 && TIFFLIB_VERSION != 20120218 && \ + TIFFLIB_VERSION != 20120922 + status = TIFFMergeFieldInfo(clientstate->tiff, info, n); +#else + TIFFMergeFieldInfo(clientstate->tiff, info, n); +#endif + return status; } +int +ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...) { + // after tif_dir.c->TIFFSetField. + TIFFSTATE *clientstate = (TIFFSTATE *)state->context; + va_list ap; + int status; + + va_start(ap, tag); + status = TIFFVSetField(clientstate->tiff, tag, ap); + va_end(ap); + return status; +} -int ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes) { - /* One shot encoder. Encode everything to the tiff in the clientstate. - If we're running off of a FD, then run once, we're good, everything - ends up in the file, we close and we're done. +int +ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes) { + /* One shot encoder. Encode everything to the tiff in the clientstate. + If we're running off of a FD, then run once, we're good, everything + ends up in the file, we close and we're done. - If we're going to memory, then we need to write the whole file into memory, then - parcel it back out to the pystring buffer bytes at a time. + If we're going to memory, then we need to write the whole file into memory, then + parcel it back out to the pystring buffer bytes at a time. - */ + */ - TIFFSTATE *clientstate = (TIFFSTATE *)state->context; - TIFF *tiff = clientstate->tiff; + TIFFSTATE *clientstate = (TIFFSTATE *)state->context; + TIFF *tiff = clientstate->tiff; TRACE(("in encoder: bytes %d\n", bytes)); - TRACE(("State: count %d, state %d, x %d, y %d, ystep %d\n", state->count, state->state, - state->x, state->y, state->ystep)); - TRACE(("State: xsize %d, ysize %d, xoff %d, yoff %d \n", state->xsize, state->ysize, - state->xoff, state->yoff)); - TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); - TRACE(("Buffer: %p: %c%c%c%c\n", buffer, (char)buffer[0], (char)buffer[1],(char)buffer[2], (char)buffer[3])); - TRACE(("State->Buffer: %c%c%c%c\n", (char)state->buffer[0], (char)state->buffer[1],(char)state->buffer[2], (char)state->buffer[3])); - TRACE(("Image: mode %s, type %d, bands: %d, xsize %d, ysize %d \n", - im->mode, im->type, im->bands, im->xsize, im->ysize)); - TRACE(("Image: image8 %p, image32 %p, image %p, block %p \n", - im->image8, im->image32, im->image, im->block)); - TRACE(("Image: pixelsize: %d, linesize %d \n", - im->pixelsize, im->linesize)); - - dump_state(clientstate); - - if (state->state == 0) { - TRACE(("Encoding line bt line")); - while(state->y < state->ysize){ - state->shuffle(state->buffer, - (UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->xsize); - - if (TIFFWriteScanline(tiff, (tdata_t)(state->buffer), (uint32)state->y, 0) == -1) { - TRACE(("Encode Error, row %d\n", state->y)); - state->errcode = IMAGING_CODEC_BROKEN; - TIFFClose(tiff); - if (!clientstate->fp){ - free(clientstate->data); - } - return -1; - } - state->y++; - } - - if (state->y == state->ysize) { - state->state=1; - - TRACE(("Flushing \n")); - if (!TIFFFlush(tiff)) { - TRACE(("Error flushing the tiff")); - // likely reason is memory. - state->errcode = IMAGING_CODEC_MEMORY; - TIFFClose(tiff); - if (!clientstate->fp){ - free(clientstate->data); - } - return -1; - } - TRACE(("Closing \n")); - TIFFClose(tiff); - // reset the clientstate metadata to use it to read out the buffer. - clientstate->loc = 0; - clientstate->size = clientstate->eof; // redundant? - } - } - - if (state->state == 1 && !clientstate->fp) { - int read = (int)_tiffReadProc(clientstate, (tdata_t)buffer, (tsize_t)bytes); - TRACE(("Buffer: %p: %c%c%c%c\n", buffer, (char)buffer[0], (char)buffer[1],(char)buffer[2], (char)buffer[3])); - if (clientstate->loc == clientstate->eof) { - TRACE(("Hit EOF, calling an end, freeing data")); - state->errcode = IMAGING_CODEC_END; - free(clientstate->data); - } - return read; - } - - state->errcode = IMAGING_CODEC_END; - return 0; + TRACE( + ("State: count %d, state %d, x %d, y %d, ystep %d\n", + state->count, + state->state, + state->x, + state->y, + state->ystep)); + TRACE( + ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", + state->xsize, + state->ysize, + state->xoff, + state->yoff)); + TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); + TRACE( + ("Buffer: %p: %c%c%c%c\n", + buffer, + (char)buffer[0], + (char)buffer[1], + (char)buffer[2], + (char)buffer[3])); + TRACE( + ("State->Buffer: %c%c%c%c\n", + (char)state->buffer[0], + (char)state->buffer[1], + (char)state->buffer[2], + (char)state->buffer[3])); + TRACE( + ("Image: mode %s, type %d, bands: %d, xsize %d, ysize %d \n", + im->mode, + im->type, + im->bands, + im->xsize, + im->ysize)); + TRACE( + ("Image: image8 %p, image32 %p, image %p, block %p \n", + im->image8, + im->image32, + im->image, + im->block)); + TRACE(("Image: pixelsize: %d, linesize %d \n", im->pixelsize, im->linesize)); + + dump_state(clientstate); + + if (state->state == 0) { + TRACE(("Encoding line by line")); + while (state->y < state->ysize) { + state->shuffle( + state->buffer, + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->xsize); + + if (TIFFWriteScanline( + tiff, (tdata_t)(state->buffer), (uint32_t)state->y, 0) == -1) { + TRACE(("Encode Error, row %d\n", state->y)); + state->errcode = IMAGING_CODEC_BROKEN; + TIFFClose(tiff); + if (!clientstate->fp) { + free(clientstate->data); + } + return -1; + } + state->y++; + } + + if (state->y == state->ysize) { + state->state = 1; + + TRACE(("Flushing \n")); + if (!TIFFFlush(tiff)) { + TRACE(("Error flushing the tiff")); + // likely reason is memory. + state->errcode = IMAGING_CODEC_MEMORY; + TIFFClose(tiff); + if (!clientstate->fp) { + free(clientstate->data); + } + return -1; + } + TRACE(("Closing \n")); + TIFFClose(tiff); + // reset the clientstate metadata to use it to read out the buffer. + clientstate->loc = 0; + clientstate->size = clientstate->eof; // redundant? + } + } + + if (state->state == 1 && !clientstate->fp) { + int read = (int)_tiffReadProc(clientstate, (tdata_t)buffer, (tsize_t)bytes); + TRACE( + ("Buffer: %p: %c%c%c%c\n", + buffer, + (char)buffer[0], + (char)buffer[1], + (char)buffer[2], + (char)buffer[3])); + if (clientstate->loc == clientstate->eof) { + TRACE(("Hit EOF, calling an end, freeing data")); + state->errcode = IMAGING_CODEC_END; + free(clientstate->data); + } + return read; + } + + state->errcode = IMAGING_CODEC_END; + return 0; +} + +const char * +ImagingTiffVersion(void) { + return TIFFGetVersion(); } + #endif diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h index e14a0932922..c7c7d48ed02 100644 --- a/src/libImaging/TiffDecode.h +++ b/src/libImaging/TiffDecode.h @@ -20,33 +20,36 @@ */ #ifndef min -#define min(x,y) (( x > y ) ? y : x ) -#define max(x,y) (( x < y ) ? y : x ) +#define min(x, y) ((x > y) ? y : x) +#define max(x, y) ((x < y) ? y : x) #endif #ifndef _PIL_LIBTIFF_ #define _PIL_LIBTIFF_ typedef struct { - tdata_t data; /* tdata_t == void* */ - toff_t loc; /* toff_t == uint32 */ - tsize_t size; /* tsize_t == int32 */ - int fp; - uint32 ifd; /* offset of the ifd, used for multipage + tdata_t data; /* tdata_t == void* */ + toff_t loc; /* toff_t == uint32 */ + tsize_t size; /* tsize_t == int32 */ + int fp; + uint32_t ifd; /* offset of the ifd, used for multipage * Should be uint32 for libtiff 3.9.x * uint64 for libtiff 4.0.x */ - TIFF *tiff; /* Used in write */ - toff_t eof; - int flrealloc;/* may we realloc */ + TIFF *tiff; /* Used in write */ + toff_t eof; + int flrealloc; /* may we realloc */ } TIFFSTATE; - - -extern int ImagingLibTiffInit(ImagingCodecState state, int fp, int offset); -extern int ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp); -extern int ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...); - +extern int +ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset); +extern int +ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp); +extern int +ImagingLibTiffMergeFieldInfo( + ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length); +extern int +ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...); /* Trace debugging @@ -54,7 +57,7 @@ extern int ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...); */ /* -#define VA_ARGS(...) __VA_ARGS__ +#define VA_ARGS(...) __VA_ARGS__ #define TRACE(args) fprintf(stderr, VA_ARGS args) */ diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 691eec7c1d8..e426ed74fce 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -32,7 +32,6 @@ #include "Imaging.h" - #define R 0 #define G 1 #define B 2 @@ -45,24 +44,30 @@ #define Y 2 #define K 3 -#define CLIP(x) ((x) <= 0 ? 0 : (x) < 256 ? (x) : 255) - /* byte-swapping macros */ -#define C16N\ - (tmp[0]=in[0], tmp[1]=in[1]); -#define C16S\ - (tmp[1]=in[0], tmp[0]=in[1]); -#define C32N\ - (tmp[0]=in[0], tmp[1]=in[1], tmp[2]=in[2], tmp[3]=in[3]); -#define C32S\ - (tmp[3]=in[0], tmp[2]=in[1], tmp[1]=in[2], tmp[0]=in[3]); -#define C64N\ - (tmp[0]=in[0], tmp[1]=in[1], tmp[2]=in[2], tmp[3]=in[3],\ - tmp[4]=in[4], tmp[5]=in[5], tmp[6]=in[6], tmp[7]=in[7]); -#define C64S\ - (tmp[7]=in[0], tmp[6]=in[1], tmp[5]=in[2], tmp[4]=in[3],\ - tmp[3]=in[4], tmp[2]=in[5], tmp[1]=in[6], tmp[0]=in[7]); +#define C16N (tmp[0] = in[0], tmp[1] = in[1]); +#define C16S (tmp[1] = in[0], tmp[0] = in[1]); +#define C32N (tmp[0] = in[0], tmp[1] = in[1], tmp[2] = in[2], tmp[3] = in[3]); +#define C32S (tmp[3] = in[0], tmp[2] = in[1], tmp[1] = in[2], tmp[0] = in[3]); +#define C64N \ + (tmp[0] = in[0], \ + tmp[1] = in[1], \ + tmp[2] = in[2], \ + tmp[3] = in[3], \ + tmp[4] = in[4], \ + tmp[5] = in[5], \ + tmp[6] = in[6], \ + tmp[7] = in[7]); +#define C64S \ + (tmp[7] = in[0], \ + tmp[6] = in[1], \ + tmp[5] = in[2], \ + tmp[4] = in[3], \ + tmp[3] = in[4], \ + tmp[2] = in[5], \ + tmp[1] = in[6], \ + tmp[0] = in[7]); #ifdef WORDS_BIGENDIAN #define C16B C16N @@ -83,112 +88,164 @@ /* bit-swapping */ static UINT8 BITFLIP[] = { - 0, 128, 64, 192, 32, 160, 96, 224, 16, 144, 80, 208, 48, 176, 112, - 240, 8, 136, 72, 200, 40, 168, 104, 232, 24, 152, 88, 216, 56, 184, - 120, 248, 4, 132, 68, 196, 36, 164, 100, 228, 20, 148, 84, 212, 52, - 180, 116, 244, 12, 140, 76, 204, 44, 172, 108, 236, 28, 156, 92, 220, - 60, 188, 124, 252, 2, 130, 66, 194, 34, 162, 98, 226, 18, 146, 82, - 210, 50, 178, 114, 242, 10, 138, 74, 202, 42, 170, 106, 234, 26, 154, - 90, 218, 58, 186, 122, 250, 6, 134, 70, 198, 38, 166, 102, 230, 22, - 150, 86, 214, 54, 182, 118, 246, 14, 142, 78, 206, 46, 174, 110, 238, - 30, 158, 94, 222, 62, 190, 126, 254, 1, 129, 65, 193, 33, 161, 97, - 225, 17, 145, 81, 209, 49, 177, 113, 241, 9, 137, 73, 201, 41, 169, - 105, 233, 25, 153, 89, 217, 57, 185, 121, 249, 5, 133, 69, 197, 37, - 165, 101, 229, 21, 149, 85, 213, 53, 181, 117, 245, 13, 141, 77, 205, - 45, 173, 109, 237, 29, 157, 93, 221, 61, 189, 125, 253, 3, 131, 67, - 195, 35, 163, 99, 227, 19, 147, 83, 211, 51, 179, 115, 243, 11, 139, - 75, 203, 43, 171, 107, 235, 27, 155, 91, 219, 59, 187, 123, 251, 7, - 135, 71, 199, 39, 167, 103, 231, 23, 151, 87, 215, 55, 183, 119, 247, - 15, 143, 79, 207, 47, 175, 111, 239, 31, 159, 95, 223, 63, 191, 127, - 255 -}; + 0, 128, 64, 192, 32, 160, 96, 224, 16, 144, 80, 208, 48, 176, 112, 240, + 8, 136, 72, 200, 40, 168, 104, 232, 24, 152, 88, 216, 56, 184, 120, 248, + 4, 132, 68, 196, 36, 164, 100, 228, 20, 148, 84, 212, 52, 180, 116, 244, + 12, 140, 76, 204, 44, 172, 108, 236, 28, 156, 92, 220, 60, 188, 124, 252, + 2, 130, 66, 194, 34, 162, 98, 226, 18, 146, 82, 210, 50, 178, 114, 242, + 10, 138, 74, 202, 42, 170, 106, 234, 26, 154, 90, 218, 58, 186, 122, 250, + 6, 134, 70, 198, 38, 166, 102, 230, 22, 150, 86, 214, 54, 182, 118, 246, + 14, 142, 78, 206, 46, 174, 110, 238, 30, 158, 94, 222, 62, 190, 126, 254, + 1, 129, 65, 193, 33, 161, 97, 225, 17, 145, 81, 209, 49, 177, 113, 241, + 9, 137, 73, 201, 41, 169, 105, 233, 25, 153, 89, 217, 57, 185, 121, 249, + 5, 133, 69, 197, 37, 165, 101, 229, 21, 149, 85, 213, 53, 181, 117, 245, + 13, 141, 77, 205, 45, 173, 109, 237, 29, 157, 93, 221, 61, 189, 125, 253, + 3, 131, 67, 195, 35, 163, 99, 227, 19, 147, 83, 211, 51, 179, 115, 243, + 11, 139, 75, 203, 43, 171, 107, 235, 27, 155, 91, 219, 59, 187, 123, 251, + 7, 135, 71, 199, 39, 167, 103, 231, 23, 151, 87, 215, 55, 183, 119, 247, + 15, 143, 79, 207, 47, 175, 111, 239, 31, 159, 95, 223, 63, 191, 127, 255}; /* Unpack to "1" image */ static void -unpack1(UINT8* out, const UINT8* in, int pixels) -{ +unpack1(UINT8 *out, const UINT8 *in, int pixels) { /* bits (msb first, white is non-zero) */ while (pixels > 0) { UINT8 byte = *in++; switch (pixels) { - default: *out++ = (byte & 128) ? 255 : 0; byte <<= 1; - case 7: *out++ = (byte & 128) ? 255 : 0; byte <<= 1; - case 6: *out++ = (byte & 128) ? 255 : 0; byte <<= 1; - case 5: *out++ = (byte & 128) ? 255 : 0; byte <<= 1; - case 4: *out++ = (byte & 128) ? 255 : 0; byte <<= 1; - case 3: *out++ = (byte & 128) ? 255 : 0; byte <<= 1; - case 2: *out++ = (byte & 128) ? 255 : 0; byte <<= 1; - case 1: *out++ = (byte & 128) ? 255 : 0; + default: + *out++ = (byte & 128) ? 255 : 0; + byte <<= 1; + case 7: + *out++ = (byte & 128) ? 255 : 0; + byte <<= 1; + case 6: + *out++ = (byte & 128) ? 255 : 0; + byte <<= 1; + case 5: + *out++ = (byte & 128) ? 255 : 0; + byte <<= 1; + case 4: + *out++ = (byte & 128) ? 255 : 0; + byte <<= 1; + case 3: + *out++ = (byte & 128) ? 255 : 0; + byte <<= 1; + case 2: + *out++ = (byte & 128) ? 255 : 0; + byte <<= 1; + case 1: + *out++ = (byte & 128) ? 255 : 0; } pixels -= 8; } } static void -unpack1I(UINT8* out, const UINT8* in, int pixels) -{ +unpack1I(UINT8 *out, const UINT8 *in, int pixels) { /* bits (msb first, white is zero) */ while (pixels > 0) { UINT8 byte = *in++; switch (pixels) { - default: *out++ = (byte & 128) ? 0 : 255; byte <<= 1; - case 7: *out++ = (byte & 128) ? 0 : 255; byte <<= 1; - case 6: *out++ = (byte & 128) ? 0 : 255; byte <<= 1; - case 5: *out++ = (byte & 128) ? 0 : 255; byte <<= 1; - case 4: *out++ = (byte & 128) ? 0 : 255; byte <<= 1; - case 3: *out++ = (byte & 128) ? 0 : 255; byte <<= 1; - case 2: *out++ = (byte & 128) ? 0 : 255; byte <<= 1; - case 1: *out++ = (byte & 128) ? 0 : 255; + default: + *out++ = (byte & 128) ? 0 : 255; + byte <<= 1; + case 7: + *out++ = (byte & 128) ? 0 : 255; + byte <<= 1; + case 6: + *out++ = (byte & 128) ? 0 : 255; + byte <<= 1; + case 5: + *out++ = (byte & 128) ? 0 : 255; + byte <<= 1; + case 4: + *out++ = (byte & 128) ? 0 : 255; + byte <<= 1; + case 3: + *out++ = (byte & 128) ? 0 : 255; + byte <<= 1; + case 2: + *out++ = (byte & 128) ? 0 : 255; + byte <<= 1; + case 1: + *out++ = (byte & 128) ? 0 : 255; } pixels -= 8; } } static void -unpack1R(UINT8* out, const UINT8* in, int pixels) -{ +unpack1R(UINT8 *out, const UINT8 *in, int pixels) { /* bits (lsb first, white is non-zero) */ while (pixels > 0) { UINT8 byte = *in++; switch (pixels) { - default: *out++ = (byte & 1) ? 255 : 0; byte >>= 1; - case 7: *out++ = (byte & 1) ? 255 : 0; byte >>= 1; - case 6: *out++ = (byte & 1) ? 255 : 0; byte >>= 1; - case 5: *out++ = (byte & 1) ? 255 : 0; byte >>= 1; - case 4: *out++ = (byte & 1) ? 255 : 0; byte >>= 1; - case 3: *out++ = (byte & 1) ? 255 : 0; byte >>= 1; - case 2: *out++ = (byte & 1) ? 255 : 0; byte >>= 1; - case 1: *out++ = (byte & 1) ? 255 : 0; + default: + *out++ = (byte & 1) ? 255 : 0; + byte >>= 1; + case 7: + *out++ = (byte & 1) ? 255 : 0; + byte >>= 1; + case 6: + *out++ = (byte & 1) ? 255 : 0; + byte >>= 1; + case 5: + *out++ = (byte & 1) ? 255 : 0; + byte >>= 1; + case 4: + *out++ = (byte & 1) ? 255 : 0; + byte >>= 1; + case 3: + *out++ = (byte & 1) ? 255 : 0; + byte >>= 1; + case 2: + *out++ = (byte & 1) ? 255 : 0; + byte >>= 1; + case 1: + *out++ = (byte & 1) ? 255 : 0; } pixels -= 8; } } static void -unpack1IR(UINT8* out, const UINT8* in, int pixels) -{ +unpack1IR(UINT8 *out, const UINT8 *in, int pixels) { /* bits (lsb first, white is zero) */ while (pixels > 0) { UINT8 byte = *in++; switch (pixels) { - default: *out++ = (byte & 1) ? 0 : 255; byte >>= 1; - case 7: *out++ = (byte & 1) ? 0 : 255; byte >>= 1; - case 6: *out++ = (byte & 1) ? 0 : 255; byte >>= 1; - case 5: *out++ = (byte & 1) ? 0 : 255; byte >>= 1; - case 4: *out++ = (byte & 1) ? 0 : 255; byte >>= 1; - case 3: *out++ = (byte & 1) ? 0 : 255; byte >>= 1; - case 2: *out++ = (byte & 1) ? 0 : 255; byte >>= 1; - case 1: *out++ = (byte & 1) ? 0 : 255; + default: + *out++ = (byte & 1) ? 0 : 255; + byte >>= 1; + case 7: + *out++ = (byte & 1) ? 0 : 255; + byte >>= 1; + case 6: + *out++ = (byte & 1) ? 0 : 255; + byte >>= 1; + case 5: + *out++ = (byte & 1) ? 0 : 255; + byte >>= 1; + case 4: + *out++ = (byte & 1) ? 0 : 255; + byte >>= 1; + case 3: + *out++ = (byte & 1) ? 0 : 255; + byte >>= 1; + case 2: + *out++ = (byte & 1) ? 0 : 255; + byte >>= 1; + case 1: + *out++ = (byte & 1) ? 0 : 255; } pixels -= 8; } } static void -unpack18(UINT8* out, const UINT8* in, int pixels) -{ - /* Unpack a '|b1' image, which is a numpy boolean. +unpack18(UINT8 *out, const UINT8 *in, int pixels) { + /* Unpack a '|b1' image, which is a numpy boolean. 1 == true, 0==false, in bytes */ int i; @@ -197,169 +254,197 @@ unpack18(UINT8* out, const UINT8* in, int pixels) } } - - /* Unpack to "L" image */ static void -unpackL2(UINT8* out, const UINT8* in, int pixels) -{ +unpackL2(UINT8 *out, const UINT8 *in, int pixels) { /* nibbles (msb first, white is non-zero) */ while (pixels > 0) { UINT8 byte = *in++; switch (pixels) { - default: *out++ = ((byte >> 6) & 0x03U) * 0x55U; byte <<= 2; - case 3: *out++ = ((byte >> 6) & 0x03U) * 0x55U; byte <<= 2; - case 2: *out++ = ((byte >> 6) & 0x03U) * 0x55U; byte <<= 2; - case 1: *out++ = ((byte >> 6) & 0x03U) * 0x55U; + default: + *out++ = ((byte >> 6) & 0x03U) * 0x55U; + byte <<= 2; + case 3: + *out++ = ((byte >> 6) & 0x03U) * 0x55U; + byte <<= 2; + case 2: + *out++ = ((byte >> 6) & 0x03U) * 0x55U; + byte <<= 2; + case 1: + *out++ = ((byte >> 6) & 0x03U) * 0x55U; } pixels -= 4; } } static void -unpackL2I(UINT8* out, const UINT8* in, int pixels) -{ +unpackL2I(UINT8 *out, const UINT8 *in, int pixels) { /* nibbles (msb first, white is zero) */ while (pixels > 0) { UINT8 byte = *in++; switch (pixels) { - default: *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); byte <<= 2; - case 3: *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); byte <<= 2; - case 2: *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); byte <<= 2; - case 1: *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); + default: + *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); + byte <<= 2; + case 3: + *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); + byte <<= 2; + case 2: + *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); + byte <<= 2; + case 1: + *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); } pixels -= 4; } } static void -unpackL2R(UINT8* out, const UINT8* in, int pixels) -{ +unpackL2R(UINT8 *out, const UINT8 *in, int pixels) { /* nibbles (bit order reversed, white is non-zero) */ while (pixels > 0) { UINT8 byte = *in++; byte = BITFLIP[byte]; switch (pixels) { - default: *out++ = ((byte >> 6) & 0x03U) * 0x55U; byte <<= 2; - case 3: *out++ = ((byte >> 6) & 0x03U) * 0x55U; byte <<= 2; - case 2: *out++ = ((byte >> 6) & 0x03U) * 0x55U; byte <<= 2; - case 1: *out++ = ((byte >> 6) & 0x03U) * 0x55U; + default: + *out++ = ((byte >> 6) & 0x03U) * 0x55U; + byte <<= 2; + case 3: + *out++ = ((byte >> 6) & 0x03U) * 0x55U; + byte <<= 2; + case 2: + *out++ = ((byte >> 6) & 0x03U) * 0x55U; + byte <<= 2; + case 1: + *out++ = ((byte >> 6) & 0x03U) * 0x55U; } pixels -= 4; } } static void -unpackL2IR(UINT8* out, const UINT8* in, int pixels) -{ +unpackL2IR(UINT8 *out, const UINT8 *in, int pixels) { /* nibbles (bit order reversed, white is zero) */ while (pixels > 0) { UINT8 byte = *in++; byte = BITFLIP[byte]; switch (pixels) { - default: *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); byte <<= 2; - case 3: *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); byte <<= 2; - case 2: *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); byte <<= 2; - case 1: *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); + default: + *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); + byte <<= 2; + case 3: + *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); + byte <<= 2; + case 2: + *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); + byte <<= 2; + case 1: + *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); } pixels -= 4; } } static void -unpackL4(UINT8* out, const UINT8* in, int pixels) -{ +unpackL4(UINT8 *out, const UINT8 *in, int pixels) { /* nibbles (msb first, white is non-zero) */ while (pixels > 0) { UINT8 byte = *in++; switch (pixels) { - default: *out++ = ((byte >> 4) & 0x0FU) * 0x11U; byte <<= 4; - case 1: *out++ = ((byte >> 4) & 0x0FU) * 0x11U; + default: + *out++ = ((byte >> 4) & 0x0FU) * 0x11U; + byte <<= 4; + case 1: + *out++ = ((byte >> 4) & 0x0FU) * 0x11U; } pixels -= 2; } } static void -unpackL4I(UINT8* out, const UINT8* in, int pixels) -{ +unpackL4I(UINT8 *out, const UINT8 *in, int pixels) { /* nibbles (msb first, white is zero) */ while (pixels > 0) { UINT8 byte = *in++; switch (pixels) { - default: *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); byte <<= 4; - case 1: *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); + default: + *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); + byte <<= 4; + case 1: + *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); } pixels -= 2; } } static void -unpackL4R(UINT8* out, const UINT8* in, int pixels) -{ +unpackL4R(UINT8 *out, const UINT8 *in, int pixels) { /* nibbles (bit order reversed, white is non-zero) */ while (pixels > 0) { UINT8 byte = *in++; byte = BITFLIP[byte]; switch (pixels) { - default: *out++ = ((byte >> 4) & 0x0FU) * 0x11U; byte <<= 4; - case 1: *out++ = ((byte >> 4) & 0x0FU) * 0x11U; + default: + *out++ = ((byte >> 4) & 0x0FU) * 0x11U; + byte <<= 4; + case 1: + *out++ = ((byte >> 4) & 0x0FU) * 0x11U; } pixels -= 2; } } static void -unpackL4IR(UINT8* out, const UINT8* in, int pixels) -{ +unpackL4IR(UINT8 *out, const UINT8 *in, int pixels) { /* nibbles (bit order reversed, white is zero) */ while (pixels > 0) { UINT8 byte = *in++; byte = BITFLIP[byte]; switch (pixels) { - default: *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); byte <<= 4; - case 1: *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); + default: + *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); + byte <<= 4; + case 1: + *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); } pixels -= 2; } } static void -unpackLA(UINT8* _out, const UINT8* in, int pixels) -{ +unpackLA(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* LA, pixel interleaved */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[0], in[0], in[0], in[1]); + UINT32 iv = MAKE_UINT32(in[0], in[0], in[0], in[1]); + memcpy(_out, &iv, sizeof(iv)); in += 2; + _out += 4; } } static void -unpackLAL(UINT8* _out, const UINT8* in, int pixels) -{ +unpackLAL(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* LA, line interleaved */ - for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[i], in[i], in[i], in[i+pixels]); + for (i = 0; i < pixels; i++, _out += 4) { + UINT32 iv = MAKE_UINT32(in[i], in[i], in[i], in[i + pixels]); + memcpy(_out, &iv, sizeof(iv)); } } static void -unpackLI(UINT8* out, const UINT8* in, int pixels) -{ +unpackLI(UINT8 *out, const UINT8 *in, int pixels) { /* negative */ int i; - for (i = 0; i < pixels; i++) + for (i = 0; i < pixels; i++) { out[i] = ~in[i]; + } } static void -unpackLR(UINT8* out, const UINT8* in, int pixels) -{ +unpackLR(UINT8 *out, const UINT8 *in, int pixels) { int i; /* RGB, bit reversed */ for (i = 0; i < pixels; i++) { @@ -368,8 +453,7 @@ unpackLR(UINT8* out, const UINT8* in, int pixels) } static void -unpackL16(UINT8* out, const UINT8* in, int pixels) -{ +unpackL16(UINT8 *out, const UINT8 *in, int pixels) { /* int16 (upper byte, little endian) */ int i; for (i = 0; i < pixels; i++) { @@ -379,8 +463,7 @@ unpackL16(UINT8* out, const UINT8* in, int pixels) } static void -unpackL16B(UINT8* out, const UINT8* in, int pixels) -{ +unpackL16B(UINT8 *out, const UINT8 *in, int pixels) { int i; /* int16 (upper byte, big endian) */ for (i = 0; i < pixels; i++) { @@ -389,66 +472,86 @@ unpackL16B(UINT8* out, const UINT8* in, int pixels) } } - /* Unpack to "P" image */ static void -unpackP1(UINT8* out, const UINT8* in, int pixels) -{ +unpackP1(UINT8 *out, const UINT8 *in, int pixels) { /* bits */ while (pixels > 0) { UINT8 byte = *in++; switch (pixels) { - default: *out++ = (byte >> 7) & 1; byte <<= 1; - case 7: *out++ = (byte >> 7) & 1; byte <<= 1; - case 6: *out++ = (byte >> 7) & 1; byte <<= 1; - case 5: *out++ = (byte >> 7) & 1; byte <<= 1; - case 4: *out++ = (byte >> 7) & 1; byte <<= 1; - case 3: *out++ = (byte >> 7) & 1; byte <<= 1; - case 2: *out++ = (byte >> 7) & 1; byte <<= 1; - case 1: *out++ = (byte >> 7) & 1; + default: + *out++ = (byte >> 7) & 1; + byte <<= 1; + case 7: + *out++ = (byte >> 7) & 1; + byte <<= 1; + case 6: + *out++ = (byte >> 7) & 1; + byte <<= 1; + case 5: + *out++ = (byte >> 7) & 1; + byte <<= 1; + case 4: + *out++ = (byte >> 7) & 1; + byte <<= 1; + case 3: + *out++ = (byte >> 7) & 1; + byte <<= 1; + case 2: + *out++ = (byte >> 7) & 1; + byte <<= 1; + case 1: + *out++ = (byte >> 7) & 1; } pixels -= 8; } } static void -unpackP2(UINT8* out, const UINT8* in, int pixels) -{ +unpackP2(UINT8 *out, const UINT8 *in, int pixels) { /* bit pairs */ while (pixels > 0) { UINT8 byte = *in++; switch (pixels) { - default: *out++ = (byte >> 6) & 3; byte <<= 2; - case 3: *out++ = (byte >> 6) & 3; byte <<= 2; - case 2: *out++ = (byte >> 6) & 3; byte <<= 2; - case 1: *out++ = (byte >> 6) & 3; + default: + *out++ = (byte >> 6) & 3; + byte <<= 2; + case 3: + *out++ = (byte >> 6) & 3; + byte <<= 2; + case 2: + *out++ = (byte >> 6) & 3; + byte <<= 2; + case 1: + *out++ = (byte >> 6) & 3; } pixels -= 4; } } static void -unpackP4(UINT8* out, const UINT8* in, int pixels) -{ +unpackP4(UINT8 *out, const UINT8 *in, int pixels) { /* nibbles */ while (pixels > 0) { UINT8 byte = *in++; switch (pixels) { - default: *out++ = (byte >> 4) & 15; byte <<= 4; - case 1: *out++ = (byte >> 4) & 15; + default: + *out++ = (byte >> 4) & 15; + byte <<= 4; + case 1: + *out++ = (byte >> 4) & 15; } pixels -= 2; } } static void -unpackP2L(UINT8* out, const UINT8* in, int pixels) -{ +unpackP2L(UINT8 *out, const UINT8 *in, int pixels) { int i, j, m, s; /* bit layers */ m = 128; - s = (pixels+7)/8; + s = (pixels + 7) / 8; for (i = j = 0; i < pixels; i++) { out[i] = ((in[j] & m) ? 1 : 0) + ((in[j + s] & m) ? 2 : 0); if ((m >>= 1) == 0) { @@ -459,15 +562,14 @@ unpackP2L(UINT8* out, const UINT8* in, int pixels) } static void -unpackP4L(UINT8* out, const UINT8* in, int pixels) -{ +unpackP4L(UINT8 *out, const UINT8 *in, int pixels) { int i, j, m, s; /* bit layers (trust the optimizer ;-) */ m = 128; - s = (pixels+7)/8; + s = (pixels + 7) / 8; for (i = j = 0; i < pixels; i++) { out[i] = ((in[j] & m) ? 1 : 0) + ((in[j + s] & m) ? 2 : 0) + - ((in[j + 2*s] & m) ? 4 : 0) + ((in[j + 3*s] & m) ? 8 : 0); + ((in[j + 2 * s] & m) ? 4 : 0) + ((in[j + 3 * s] & m) ? 8 : 0); if ((m >>= 1) == 0) { m = 128; j++; @@ -475,354 +577,410 @@ unpackP4L(UINT8* out, const UINT8* in, int pixels) } } - /* Unpack to "RGB" image */ void -ImagingUnpackRGB(UINT8* _out, const UINT8* in, int pixels) -{ +ImagingUnpackRGB(UINT8 *_out, const UINT8 *in, int pixels) { int i = 0; - UINT32* out = (UINT32*) _out; /* RGB triplets */ - for (; i < pixels-1; i++) { - out[i] = MASK_UINT32_CHANNEL_3 | *(UINT32*)&in[0]; + for (; i < pixels - 1; i++) { + UINT32 iv; + memcpy(&iv, in, sizeof(iv)); + iv |= MASK_UINT32_CHANNEL_3; + memcpy(_out, &iv, sizeof(iv)); in += 3; + _out += 4; } for (; i < pixels; i++) { - out[i] = MAKE_UINT32(in[0], in[1], in[2], 255); + UINT32 iv = MAKE_UINT32(in[0], in[1], in[2], 255); + memcpy(_out, &iv, sizeof(iv)); in += 3; + _out += 4; } } void -unpackRGB16L(UINT8* _out, const UINT8* in, int pixels) -{ +unpackRGB16L(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* 16-bit RGB triplets, little-endian order */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[1], in[3], in[5], 255); + UINT32 iv = MAKE_UINT32(in[1], in[3], in[5], 255); + memcpy(_out, &iv, sizeof(iv)); in += 6; + _out += 4; } } void -unpackRGB16B(UINT8* _out, const UINT8* in, int pixels) -{ +unpackRGB16B(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* 16-bit RGB triplets, big-endian order */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[0], in[2], in[4], 255); + UINT32 iv = MAKE_UINT32(in[0], in[2], in[4], 255); + memcpy(_out, &iv, sizeof(iv)); in += 6; + _out += 4; } } static void -unpackRGBL(UINT8* _out, const UINT8* in, int pixels) -{ +unpackRGBL(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGB, line interleaved */ - for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[i], in[i+pixels], in[i+pixels+pixels], 255); + for (i = 0; i < pixels; i++, _out += 4) { + UINT32 iv = MAKE_UINT32(in[i], in[i + pixels], in[i + pixels + pixels], 255); + memcpy(_out, &iv, sizeof(iv)); } } static void -unpackRGBR(UINT8* _out, const UINT8* in, int pixels) -{ +unpackRGBR(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGB, bit reversed */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(BITFLIP[in[0]], BITFLIP[in[1]], - BITFLIP[in[2]], 255); + UINT32 iv = MAKE_UINT32(BITFLIP[in[0]], BITFLIP[in[1]], BITFLIP[in[2]], 255); + memcpy(_out, &iv, sizeof(iv)); in += 3; + _out += 4; } } void -ImagingUnpackBGR(UINT8* _out, const UINT8* in, int pixels) -{ +ImagingUnpackBGR(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGB, reversed bytes */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[2], in[1], in[0], 255); + UINT32 iv = MAKE_UINT32(in[2], in[1], in[0], 255); + memcpy(_out, &iv, sizeof(iv)); in += 3; + _out += 4; } } void -ImagingUnpackRGB15(UINT8* out, const UINT8* in, int pixels) -{ +ImagingUnpackRGB15(UINT8 *out, const UINT8 *in, int pixels) { int i, pixel; /* RGB, 5 bits per pixel */ for (i = 0; i < pixels; i++) { pixel = in[0] + (in[1] << 8); out[R] = (pixel & 31) * 255 / 31; - out[G] = ((pixel>>5) & 31) * 255 / 31; - out[B] = ((pixel>>10) & 31) * 255 / 31; + out[G] = ((pixel >> 5) & 31) * 255 / 31; + out[B] = ((pixel >> 10) & 31) * 255 / 31; out[A] = 255; - out += 4; in += 2; + out += 4; + in += 2; } } void -ImagingUnpackRGBA15(UINT8* out, const UINT8* in, int pixels) -{ +ImagingUnpackRGBA15(UINT8 *out, const UINT8 *in, int pixels) { int i, pixel; /* RGB, 5/5/5/1 bits per pixel */ for (i = 0; i < pixels; i++) { pixel = in[0] + (in[1] << 8); out[R] = (pixel & 31) * 255 / 31; - out[G] = ((pixel>>5) & 31) * 255 / 31; - out[B] = ((pixel>>10) & 31) * 255 / 31; - out[A] = (pixel>>15) * 255; - out += 4; in += 2; + out[G] = ((pixel >> 5) & 31) * 255 / 31; + out[B] = ((pixel >> 10) & 31) * 255 / 31; + out[A] = (pixel >> 15) * 255; + out += 4; + in += 2; } } void -ImagingUnpackBGR15(UINT8* out, const UINT8* in, int pixels) -{ +ImagingUnpackBGR15(UINT8 *out, const UINT8 *in, int pixels) { int i, pixel; /* RGB, reversed bytes, 5 bits per pixel */ for (i = 0; i < pixels; i++) { pixel = in[0] + (in[1] << 8); out[B] = (pixel & 31) * 255 / 31; - out[G] = ((pixel>>5) & 31) * 255 / 31; - out[R] = ((pixel>>10) & 31) * 255 / 31; + out[G] = ((pixel >> 5) & 31) * 255 / 31; + out[R] = ((pixel >> 10) & 31) * 255 / 31; out[A] = 255; - out += 4; in += 2; + out += 4; + in += 2; } } void -ImagingUnpackBGRA15(UINT8* out, const UINT8* in, int pixels) -{ +ImagingUnpackBGRA15(UINT8 *out, const UINT8 *in, int pixels) { int i, pixel; - /* RGB, reversed bytes, 5/5/5/1 bits per pixel */ + /* RGB, rearranged channels, 5/5/5/1 bits per pixel */ for (i = 0; i < pixels; i++) { pixel = in[0] + (in[1] << 8); out[B] = (pixel & 31) * 255 / 31; - out[G] = ((pixel>>5) & 31) * 255 / 31; - out[R] = ((pixel>>10) & 31) * 255 / 31; - out[A] = (pixel>>15) * 255; - out += 4; in += 2; + out[G] = ((pixel >> 5) & 31) * 255 / 31; + out[R] = ((pixel >> 10) & 31) * 255 / 31; + out[A] = (pixel >> 15) * 255; + out += 4; + in += 2; } } void -ImagingUnpackRGB16(UINT8* out, const UINT8* in, int pixels) -{ +ImagingUnpackRGB16(UINT8 *out, const UINT8 *in, int pixels) { int i, pixel; /* RGB, 5/6/5 bits per pixel */ for (i = 0; i < pixels; i++) { pixel = in[0] + (in[1] << 8); out[R] = (pixel & 31) * 255 / 31; - out[G] = ((pixel>>5) & 63) * 255 / 63; - out[B] = ((pixel>>11) & 31) * 255 / 31; + out[G] = ((pixel >> 5) & 63) * 255 / 63; + out[B] = ((pixel >> 11) & 31) * 255 / 31; out[A] = 255; - out += 4; in += 2; + out += 4; + in += 2; } } void -ImagingUnpackBGR16(UINT8* out, const UINT8* in, int pixels) -{ +ImagingUnpackBGR16(UINT8 *out, const UINT8 *in, int pixels) { int i, pixel; /* RGB, reversed bytes, 5/6/5 bits per pixel */ for (i = 0; i < pixels; i++) { pixel = in[0] + (in[1] << 8); out[B] = (pixel & 31) * 255 / 31; - out[G] = ((pixel>>5) & 63) * 255 / 63; - out[R] = ((pixel>>11) & 31) * 255 / 31; + out[G] = ((pixel >> 5) & 63) * 255 / 63; + out[R] = ((pixel >> 11) & 31) * 255 / 31; out[A] = 255; - out += 4; in += 2; + out += 4; + in += 2; } } void -ImagingUnpackRGB4B(UINT8* out, const UINT8* in, int pixels) -{ +ImagingUnpackRGB4B(UINT8 *out, const UINT8 *in, int pixels) { int i, pixel; /* RGB, 4 bits per pixel */ for (i = 0; i < pixels; i++) { pixel = in[0] + (in[1] << 8); out[R] = (pixel & 15) * 17; - out[G] = ((pixel>>4) & 15) * 17; - out[B] = ((pixel>>8) & 15) * 17; + out[G] = ((pixel >> 4) & 15) * 17; + out[B] = ((pixel >> 8) & 15) * 17; out[A] = 255; - out += 4; in += 2; + out += 4; + in += 2; } } void -ImagingUnpackRGBA4B(UINT8* out, const UINT8* in, int pixels) -{ +ImagingUnpackRGBA4B(UINT8 *out, const UINT8 *in, int pixels) { int i, pixel; /* RGBA, 4 bits per pixel */ for (i = 0; i < pixels; i++) { pixel = in[0] + (in[1] << 8); out[R] = (pixel & 15) * 17; - out[G] = ((pixel>>4) & 15) * 17; - out[B] = ((pixel>>8) & 15) * 17; - out[A] = ((pixel>>12) & 15) * 17; - out += 4; in += 2; + out[G] = ((pixel >> 4) & 15) * 17; + out[B] = ((pixel >> 8) & 15) * 17; + out[A] = ((pixel >> 12) & 15) * 17; + out += 4; + in += 2; } } static void -ImagingUnpackBGRX(UINT8* _out, const UINT8* in, int pixels) -{ +ImagingUnpackBGRX(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGB, reversed bytes with padding */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[2], in[1], in[0], 255); + UINT32 iv = MAKE_UINT32(in[2], in[1], in[0], 255); + memcpy(_out, &iv, sizeof(iv)); in += 4; + _out += 4; } } static void -ImagingUnpackXRGB(UINT8* _out, const UINT8* in, int pixels) -{ +ImagingUnpackXRGB(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGB, leading pad */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[1], in[2], in[3], 255); + UINT32 iv = MAKE_UINT32(in[1], in[2], in[3], 255); + memcpy(_out, &iv, sizeof(iv)); in += 4; + _out += 4; } } static void -ImagingUnpackXBGR(UINT8* _out, const UINT8* in, int pixels) -{ +ImagingUnpackXBGR(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGB, reversed bytes, leading pad */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[3], in[2], in[1], 255); + UINT32 iv = MAKE_UINT32(in[3], in[2], in[1], 255); + memcpy(_out, &iv, sizeof(iv)); in += 4; + _out += 4; } } /* Unpack to "RGBA" image */ static void -unpackRGBALA(UINT8* _out, const UINT8* in, int pixels) -{ +unpackRGBALA(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* greyscale with alpha */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[0], in[0], in[0], in[1]); + UINT32 iv = MAKE_UINT32(in[0], in[0], in[0], in[1]); + memcpy(_out, &iv, sizeof(iv)); in += 2; + _out += 4; } } static void -unpackRGBALA16B(UINT8* _out, const UINT8* in, int pixels) -{ +unpackRGBALA16B(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* 16-bit greyscale with alpha, big-endian */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[0], in[0], in[0], in[2]); + UINT32 iv = MAKE_UINT32(in[0], in[0], in[0], in[2]); + memcpy(_out, &iv, sizeof(iv)); in += 4; + _out += 4; } } static void -unpackRGBa16L(UINT8* _out, const UINT8* in, int pixels) -{ +unpackRGBa16L(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* premultiplied 16-bit RGBA, little-endian */ for (i = 0; i < pixels; i++) { int a = in[7]; - if ( ! a) { - out[i] = 0; + UINT32 iv; + if (!a) { + iv = 0; } else if (a == 255) { - out[i] = MAKE_UINT32(in[1], in[3], in[5], a); + iv = MAKE_UINT32(in[1], in[3], in[5], a); } else { - out[i] = MAKE_UINT32(CLIP(in[1] * 255 / a), - CLIP(in[3] * 255 / a), - CLIP(in[5] * 255 / a), a); + iv = MAKE_UINT32( + CLIP8(in[1] * 255 / a), + CLIP8(in[3] * 255 / a), + CLIP8(in[5] * 255 / a), + a); } + memcpy(_out, &iv, sizeof(iv)); in += 8; + _out += 4; } } static void -unpackRGBa16B(UINT8* _out, const UINT8* in, int pixels) -{ +unpackRGBa16B(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* premultiplied 16-bit RGBA, big-endian */ for (i = 0; i < pixels; i++) { int a = in[6]; - if ( ! a) { - out[i] = 0; + UINT32 iv; + if (!a) { + iv = 0; } else if (a == 255) { - out[i] = MAKE_UINT32(in[0], in[2], in[4], a); + iv = MAKE_UINT32(in[0], in[2], in[4], a); } else { - out[i] = MAKE_UINT32(CLIP(in[0] * 255 / a), - CLIP(in[2] * 255 / a), - CLIP(in[4] * 255 / a), a); + iv = MAKE_UINT32( + CLIP8(in[0] * 255 / a), + CLIP8(in[2] * 255 / a), + CLIP8(in[4] * 255 / a), + a); } + memcpy(_out, &iv, sizeof(iv)); in += 8; + _out += 4; } } static void -unpackRGBa(UINT8* _out, const UINT8* in, int pixels) -{ +unpackRGBa(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* premultiplied RGBA */ + for (i = 0; i < pixels; i++) { + int a = in[3]; + UINT32 iv; + if (!a) { + iv = 0; + } else if (a == 255) { + iv = MAKE_UINT32(in[0], in[1], in[2], a); + } else { + iv = MAKE_UINT32( + CLIP8(in[0] * 255 / a), + CLIP8(in[1] * 255 / a), + CLIP8(in[2] * 255 / a), + a); + } + memcpy(_out, &iv, sizeof(iv)); + in += 4; + _out += 4; + } +} + +static void +unpackRGBaskip1(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; + UINT32 *out = (UINT32 *)_out; /* premultiplied RGBA */ for (i = 0; i < pixels; i++) { int a = in[3]; - if ( ! a) { + if (!a) { out[i] = 0; } else if (a == 255) { out[i] = MAKE_UINT32(in[0], in[1], in[2], a); } else { - out[i] = MAKE_UINT32(CLIP(in[0] * 255 / a), - CLIP(in[1] * 255 / a), - CLIP(in[2] * 255 / a), a); + out[i] = MAKE_UINT32( + CLIP8(in[0] * 255 / a), + CLIP8(in[1] * 255 / a), + CLIP8(in[2] * 255 / a), + a); } - in += 4; + in += 5; } } static void -unpackBGRa(UINT8* _out, const UINT8* in, int pixels) -{ +unpackRGBaskip2(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; - /* premultiplied BGRA */ + UINT32 *out = (UINT32 *)_out; + /* premultiplied RGBA */ for (i = 0; i < pixels; i++) { int a = in[3]; - if ( ! a) { + if (!a) { out[i] = 0; } else if (a == 255) { - out[i] = MAKE_UINT32(in[2], in[1], in[0], a); + out[i] = MAKE_UINT32(in[0], in[1], in[2], a); + } else { + out[i] = MAKE_UINT32( + CLIP8(in[0] * 255 / a), + CLIP8(in[1] * 255 / a), + CLIP8(in[2] * 255 / a), + a); + } + in += 6; + } +} + +static void +unpackBGRa(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* premultiplied BGRA */ + for (i = 0; i < pixels; i++) { + int a = in[3]; + UINT32 iv; + if (!a) { + iv = 0; + } else if (a == 255) { + iv = MAKE_UINT32(in[2], in[1], in[0], a); } else { - out[i] = MAKE_UINT32(CLIP(in[2] * 255 / a), - CLIP(in[1] * 255 / a), - CLIP(in[0] * 255 / a), a); + iv = MAKE_UINT32( + CLIP8(in[2] * 255 / a), + CLIP8(in[1] * 255 / a), + CLIP8(in[0] * 255 / a), + a); } + memcpy(_out, &iv, sizeof(iv)); in += 4; + _out += 4; } } static void -unpackRGBAI(UINT8* out, const UINT8* in, int pixels) -{ +unpackRGBAI(UINT8 *out, const UINT8 *in, int pixels) { int i; /* RGBA, inverted RGB bytes (FlashPix) */ for (i = 0; i < pixels; i++) { @@ -830,94 +988,118 @@ unpackRGBAI(UINT8* out, const UINT8* in, int pixels) out[G] = ~in[1]; out[B] = ~in[2]; out[A] = in[3]; - out += 4; in += 4; + out += 4; + in += 4; } } static void -unpackRGBAL(UINT8* _out, const UINT8* in, int pixels) -{ +unpackRGBAL(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGBA, line interleaved */ - for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[i], in[i+pixels], in[i+pixels+pixels], - in[i+pixels+pixels+pixels]); + for (i = 0; i < pixels; i++, _out += 4) { + UINT32 iv = MAKE_UINT32( + in[i], + in[i + pixels], + in[i + pixels + pixels], + in[i + pixels + pixels + pixels]); + memcpy(_out, &iv, sizeof(iv)); } } void -unpackRGBA16L(UINT8* _out, const UINT8* in, int pixels) -{ +unpackRGBA16L(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* 16-bit RGBA, little-endian order */ - for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[1], in[3], in[5], in[7]); + for (i = 0; i < pixels; i++, _out += 4) { + UINT32 iv = MAKE_UINT32(in[1], in[3], in[5], in[7]); + memcpy(_out, &iv, sizeof(iv)); in += 8; } } void -unpackRGBA16B(UINT8* _out, const UINT8* in, int pixels) -{ +unpackRGBA16B(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* 16-bit RGBA, big-endian order */ - for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[0], in[2], in[4], in[6]); + for (i = 0; i < pixels; i++, _out += 4) { + UINT32 iv = MAKE_UINT32(in[0], in[2], in[4], in[6]); + memcpy(_out, &iv, sizeof(iv)); in += 8; } } static void -unpackARGB(UINT8* _out, const UINT8* in, int pixels) -{ +unpackARGB(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGBA, leading pad */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[1], in[2], in[3], in[0]); + UINT32 iv = MAKE_UINT32(in[1], in[2], in[3], in[0]); + memcpy(_out, &iv, sizeof(iv)); in += 4; + _out += 4; } } static void -unpackABGR(UINT8* _out, const UINT8* in, int pixels) -{ +unpackABGR(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* RGBA, reversed bytes */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[3], in[2], in[1], in[0]); + UINT32 iv = MAKE_UINT32(in[3], in[2], in[1], in[0]); + memcpy(_out, &iv, sizeof(iv)); in += 4; + _out += 4; } } static void -unpackBGRA(UINT8* _out, const UINT8* in, int pixels) -{ +unpackBGRA(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; - /* RGBA, reversed bytes */ + /* RGBA, rearranged channels */ for (i = 0; i < pixels; i++) { - out[i] = MAKE_UINT32(in[2], in[1], in[0], in[3]); + UINT32 iv = MAKE_UINT32(in[2], in[1], in[0], in[3]); + memcpy(_out, &iv, sizeof(iv)); in += 4; + _out += 4; } } +static void +unpackBGRA16L(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* 16-bit RGBA, little-endian order, rearranged channels */ + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[5], in[3], in[1], in[7]); + memcpy(_out, &iv, sizeof(iv)); + in += 8; + _out += 4; + } +} + +static void +unpackBGRA16B(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* 16-bit RGBA, big-endian order, rearranged channels */ + for (i = 0; i < pixels; i++) { + UINT32 iv = MAKE_UINT32(in[4], in[2], in[0], in[6]); + memcpy(_out, &iv, sizeof(iv)); + in += 8; + _out += 4; + } +} /* Unpack to "CMYK" image */ static void -unpackCMYKI(UINT8* _out, const UINT8* in, int pixels) -{ +unpackCMYKI(UINT8 *_out, const UINT8 *in, int pixels) { int i; - UINT32* out = (UINT32*) _out; /* CMYK, inverted bytes (Photoshop 2.5) */ for (i = 0; i < pixels; i++) { - out[i] = ~MAKE_UINT32(in[0], in[1], in[2], in[3]); + UINT32 iv = ~MAKE_UINT32(in[0], in[1], in[2], in[3]); + memcpy(_out, &iv, sizeof(iv)); in += 4; + _out += 4; } } @@ -933,8 +1115,7 @@ unpackCMYKI(UINT8* _out, const UINT8* in, int pixels) internally, and we'll unshift for saving and whatnot. */ void -ImagingUnpackLAB(UINT8* out, const UINT8* in, int pixels) -{ +ImagingUnpackLAB(UINT8 *out, const UINT8 *in, int pixels) { int i; /* LAB triplets */ for (i = 0; i < pixels; i++) { @@ -942,32 +1123,44 @@ ImagingUnpackLAB(UINT8* out, const UINT8* in, int pixels) out[1] = in[1] ^ 128; /* signed in outside world */ out[2] = in[2] ^ 128; out[3] = 255; - out += 4; in += 3; + out += 4; + in += 3; } } static void -unpackI16N_I16B(UINT8* out, const UINT8* in, int pixels){ +unpackI16N_I16B(UINT8 *out, const UINT8 *in, int pixels) { int i; - UINT8* tmp = (UINT8*) out; + UINT8 *tmp = (UINT8 *)out; for (i = 0; i < pixels; i++) { C16B; - in += 2; tmp += 2; + in += 2; + tmp += 2; } - } static void -unpackI16N_I16(UINT8* out, const UINT8* in, int pixels){ +unpackI16N_I16(UINT8 *out, const UINT8 *in, int pixels) { int i; - UINT8* tmp = (UINT8*) out; + UINT8 *tmp = (UINT8 *)out; for (i = 0; i < pixels; i++) { C16L; - in += 2; tmp += 2; + in += 2; + tmp += 2; + } +} +static void +unpackI16R_I16(UINT8 *out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++) { + out[0] = BITFLIP[in[0]]; + out[1] = BITFLIP[in[1]]; + in += 2; + out += 2; } } static void -unpackI12_I16(UINT8* out, const UINT8* in, int pixels){ +unpackI12_I16(UINT8 *out, const UINT8 *in, int pixels) { /* Fillorder 1/MSB -> LittleEndian, for 12bit integer greyscale tiffs. According to the TIFF spec: @@ -989,82 +1182,100 @@ unpackI12_I16(UINT8* out, const UINT8* in, int pixels){ int i; UINT16 pixel; #ifdef WORDS_BIGENDIAN - UINT8* tmp = (UINT8 *)&pixel; + UINT8 *tmp = (UINT8 *)&pixel; #endif - UINT16* out16 = (UINT16 *)out; - for (i = 0; i < pixels-1; i+=2) { - pixel = (((UINT16) in[0]) << 4 ) + (in[1] >>4); + for (i = 0; i < pixels - 1; i += 2) { + pixel = (((UINT16)in[0]) << 4) + (in[1] >> 4); #ifdef WORDS_BIGENDIAN - out[0] = tmp[1]; out[1] = tmp[0]; + out[0] = tmp[1]; + out[1] = tmp[0]; #else - out16[0] = pixel; + memcpy(out, &pixel, sizeof(pixel)); #endif - pixel = (((UINT16) (in[1] & 0x0F)) << 8) + in[2]; + out += 2; + pixel = (((UINT16)(in[1] & 0x0F)) << 8) + in[2]; #ifdef WORDS_BIGENDIAN - out[2] = tmp[1]; out[3] = tmp[0]; + out[0] = tmp[1]; + out[1] = tmp[0]; #else - out16[1] = pixel; + memcpy(out, &pixel, sizeof(pixel)); #endif - in += 3; out16 += 2; out+=4; + in += 3; + out += 2; } - if (i == pixels-1) { - pixel = (((UINT16) in[0]) << 4 ) + (in[1] >>4); + if (i == pixels - 1) { + pixel = (((UINT16)in[0]) << 4) + (in[1] >> 4); #ifdef WORDS_BIGENDIAN - out[0] = tmp[1]; out[1] = tmp[0]; + out[0] = tmp[1]; + out[1] = tmp[0]; #else - out16[0] = pixel; + memcpy(out, &pixel, sizeof(pixel)); #endif } } - static void -copy1(UINT8* out, const UINT8* in, int pixels) -{ +copy1(UINT8 *out, const UINT8 *in, int pixels) { /* L, P */ memcpy(out, in, pixels); } static void -copy2(UINT8* out, const UINT8* in, int pixels) -{ +copy2(UINT8 *out, const UINT8 *in, int pixels) { /* I;16 */ - memcpy(out, in, pixels*2); + memcpy(out, in, pixels * 2); } static void -copy4(UINT8* out, const UINT8* in, int pixels) -{ +copy4(UINT8 *out, const UINT8 *in, int pixels) { /* RGBA, CMYK quadruples */ memcpy(out, in, 4 * pixels); } +static void +copy4skip1(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++) { + memcpy(_out, in, 4); + in += 5; + _out += 4; + } +} + +static void +copy4skip2(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++) { + memcpy(_out, in, 4); + in += 6; + _out += 4; + } +} /* Unpack to "I" and "F" images */ -#define UNPACK_RAW(NAME, GET, INTYPE, OUTTYPE)\ -static void NAME(UINT8* out_, const UINT8* in, int pixels)\ -{\ - int i;\ - OUTTYPE* out = (OUTTYPE*) out_;\ - for (i = 0; i < pixels; i++, in += sizeof(INTYPE))\ - out[i] = (OUTTYPE) ((INTYPE) GET);\ -} +#define UNPACK_RAW(NAME, GET, INTYPE, OUTTYPE) \ + static void NAME(UINT8 *out_, const UINT8 *in, int pixels) { \ + int i; \ + OUTTYPE *out = (OUTTYPE *)out_; \ + for (i = 0; i < pixels; i++, in += sizeof(INTYPE)) { \ + out[i] = (OUTTYPE)((INTYPE)GET); \ + } \ + } -#define UNPACK(NAME, COPY, INTYPE, OUTTYPE)\ -static void NAME(UINT8* out_, const UINT8* in, int pixels)\ -{\ - int i;\ - OUTTYPE* out = (OUTTYPE*) out_;\ - INTYPE tmp_;\ - UINT8* tmp = (UINT8*) &tmp_;\ - for (i = 0; i < pixels; i++, in += sizeof(INTYPE)) {\ - COPY;\ - out[i] = (OUTTYPE) tmp_;\ - }\ -} +#define UNPACK(NAME, COPY, INTYPE, OUTTYPE) \ + static void NAME(UINT8 *out_, const UINT8 *in, int pixels) { \ + int i; \ + OUTTYPE *out = (OUTTYPE *)out_; \ + INTYPE tmp_; \ + UINT8 *tmp = (UINT8 *)&tmp_; \ + for (i = 0; i < pixels; i++, in += sizeof(INTYPE)) { \ + COPY; \ + out[i] = (OUTTYPE)tmp_; \ + } \ + } UNPACK_RAW(unpackI8, in[0], UINT8, INT32) UNPACK_RAW(unpackI8S, in[0], INT8, INT32) @@ -1104,12 +1315,10 @@ UNPACK(unpackF64BF, C64B, FLOAT64, FLOAT32) UNPACK(unpackF64NF, C64N, FLOAT64, FLOAT32) #endif - /* Misc. unpackers */ static void -band0(UINT8* out, const UINT8* in, int pixels) -{ +band0(UINT8 *out, const UINT8 *in, int pixels) { int i; /* band 0 only */ for (i = 0; i < pixels; i++) { @@ -1119,8 +1328,7 @@ band0(UINT8* out, const UINT8* in, int pixels) } static void -band1(UINT8* out, const UINT8* in, int pixels) -{ +band1(UINT8 *out, const UINT8 *in, int pixels) { int i; /* band 1 only */ for (i = 0; i < pixels; i++) { @@ -1130,8 +1338,7 @@ band1(UINT8* out, const UINT8* in, int pixels) } static void -band2(UINT8* out, const UINT8* in, int pixels) -{ +band2(UINT8 *out, const UINT8 *in, int pixels) { int i; /* band 2 only */ for (i = 0; i < pixels; i++) { @@ -1141,8 +1348,7 @@ band2(UINT8* out, const UINT8* in, int pixels) } static void -band3(UINT8* out, const UINT8* in, int pixels) -{ +band3(UINT8 *out, const UINT8 *in, int pixels) { /* band 3 only */ int i; for (i = 0; i < pixels; i++) { @@ -1152,8 +1358,7 @@ band3(UINT8* out, const UINT8* in, int pixels) } static void -band0I(UINT8* out, const UINT8* in, int pixels) -{ +band0I(UINT8 *out, const UINT8 *in, int pixels) { int i; /* band 0 only */ for (i = 0; i < pixels; i++) { @@ -1163,8 +1368,7 @@ band0I(UINT8* out, const UINT8* in, int pixels) } static void -band1I(UINT8* out, const UINT8* in, int pixels) -{ +band1I(UINT8 *out, const UINT8 *in, int pixels) { int i; /* band 1 only */ for (i = 0; i < pixels; i++) { @@ -1174,8 +1378,7 @@ band1I(UINT8* out, const UINT8* in, int pixels) } static void -band2I(UINT8* out, const UINT8* in, int pixels) -{ +band2I(UINT8 *out, const UINT8 *in, int pixels) { int i; /* band 2 only */ for (i = 0; i < pixels; i++) { @@ -1185,8 +1388,7 @@ band2I(UINT8* out, const UINT8* in, int pixels) } static void -band3I(UINT8* out, const UINT8* in, int pixels) -{ +band3I(UINT8 *out, const UINT8 *in, int pixels) { /* band 3 only */ int i; for (i = 0; i < pixels; i++) { @@ -1195,9 +1397,97 @@ band3I(UINT8* out, const UINT8* in, int pixels) } } +static void +band016B(UINT8* out, const UINT8* in, int pixels) +{ + int i; + /* band 0 only, big endian */ + for (i = 0; i < pixels; i++) { + out[0] = in[0]; + out += 4; in += 2; + } +} + +static void +band116B(UINT8* out, const UINT8* in, int pixels) +{ + int i; + /* band 1 only, big endian */ + for (i = 0; i < pixels; i++) { + out[1] = in[0]; + out += 4; in += 2; + } +} + +static void +band216B(UINT8* out, const UINT8* in, int pixels) +{ + int i; + /* band 2 only, big endian */ + for (i = 0; i < pixels; i++) { + out[2] = in[0]; + out += 4; in += 2; + } +} + +static void +band316B(UINT8* out, const UINT8* in, int pixels) +{ + int i; + /* band 3 only, big endian */ + for (i = 0; i < pixels; i++) { + out[3] = in[0]; + out += 4; in += 2; + } +} + +static void +band016L(UINT8* out, const UINT8* in, int pixels) +{ + int i; + /* band 0 only, little endian */ + for (i = 0; i < pixels; i++) { + out[0] = in[1]; + out += 4; in += 2; + } +} + +static void +band116L(UINT8* out, const UINT8* in, int pixels) +{ + int i; + /* band 1 only, little endian */ + for (i = 0; i < pixels; i++) { + out[1] = in[1]; + out += 4; in += 2; + } +} + +static void +band216L(UINT8* out, const UINT8* in, int pixels) +{ + int i; + /* band 2 only, little endian */ + for (i = 0; i < pixels; i++) { + out[2] = in[1]; + out += 4; in += 2; + } +} + +static void +band316L(UINT8* out, const UINT8* in, int pixels) +{ + int i; + /* band 3 only, little endian */ + for (i = 0; i < pixels; i++) { + out[3] = in[1]; + out += 4; in += 2; + } +} + static struct { - const char* mode; - const char* rawmode; + const char *mode; + const char *rawmode; int bits; ImagingShuffler unpack; } unpackers[] = { @@ -1214,235 +1504,290 @@ static struct { /* exception: rawmodes "I" and "F" are always native endian byte order */ /* bilevel */ - {"1", "1", 1, unpack1}, - {"1", "1;I", 1, unpack1I}, - {"1", "1;R", 1, unpack1R}, - {"1", "1;IR", 1, unpack1IR}, - {"1", "1;8", 1, unpack18}, + {"1", "1", 1, unpack1}, + {"1", "1;I", 1, unpack1I}, + {"1", "1;R", 1, unpack1R}, + {"1", "1;IR", 1, unpack1IR}, + {"1", "1;8", 8, unpack18}, /* greyscale */ - {"L", "L;2", 2, unpackL2}, - {"L", "L;2I", 2, unpackL2I}, - {"L", "L;2R", 2, unpackL2R}, - {"L", "L;2IR", 2, unpackL2IR}, - - {"L", "L;4", 4, unpackL4}, - {"L", "L;4I", 4, unpackL4I}, - {"L", "L;4R", 4, unpackL4R}, - {"L", "L;4IR", 4, unpackL4IR}, - - {"L", "L", 8, copy1}, - {"L", "L;I", 8, unpackLI}, - {"L", "L;R", 8, unpackLR}, - {"L", "L;16", 16, unpackL16}, - {"L", "L;16B", 16, unpackL16B}, + {"L", "L;2", 2, unpackL2}, + {"L", "L;2I", 2, unpackL2I}, + {"L", "L;2R", 2, unpackL2R}, + {"L", "L;2IR", 2, unpackL2IR}, + + {"L", "L;4", 4, unpackL4}, + {"L", "L;4I", 4, unpackL4I}, + {"L", "L;4R", 4, unpackL4R}, + {"L", "L;4IR", 4, unpackL4IR}, + + {"L", "L", 8, copy1}, + {"L", "L;I", 8, unpackLI}, + {"L", "L;R", 8, unpackLR}, + {"L", "L;16", 16, unpackL16}, + {"L", "L;16B", 16, unpackL16B}, /* greyscale w. alpha */ - {"LA", "LA", 16, unpackLA}, - {"LA", "LA;L", 16, unpackLAL}, + {"LA", "LA", 16, unpackLA}, + {"LA", "LA;L", 16, unpackLAL}, + + /* greyscale w. alpha premultiplied */ + {"La", "La", 16, unpackLA}, /* palette */ - {"P", "P;1", 1, unpackP1}, - {"P", "P;2", 2, unpackP2}, - {"P", "P;2L", 2, unpackP2L}, - {"P", "P;4", 4, unpackP4}, - {"P", "P;4L", 4, unpackP4L}, - {"P", "P", 8, copy1}, - {"P", "P;R", 8, unpackLR}, + {"P", "P;1", 1, unpackP1}, + {"P", "P;2", 2, unpackP2}, + {"P", "P;2L", 2, unpackP2L}, + {"P", "P;4", 4, unpackP4}, + {"P", "P;4L", 4, unpackP4L}, + {"P", "P", 8, copy1}, + {"P", "P;R", 8, unpackLR}, /* palette w. alpha */ - {"PA", "PA", 16, unpackLA}, - {"PA", "PA;L", 16, unpackLAL}, + {"PA", "PA", 16, unpackLA}, + {"PA", "PA;L", 16, unpackLAL}, /* true colour */ - {"RGB", "RGB", 24, ImagingUnpackRGB}, - {"RGB", "RGB;L", 24, unpackRGBL}, - {"RGB", "RGB;R", 24, unpackRGBR}, - {"RGB", "RGB;16L", 48, unpackRGB16L}, - {"RGB", "RGB;16B", 48, unpackRGB16B}, - {"RGB", "BGR", 24, ImagingUnpackBGR}, - {"RGB", "RGB;15", 16, ImagingUnpackRGB15}, - {"RGB", "BGR;15", 16, ImagingUnpackBGR15}, - {"RGB", "RGB;16", 16, ImagingUnpackRGB16}, - {"RGB", "BGR;16", 16, ImagingUnpackBGR16}, - {"RGB", "RGB;4B", 16, ImagingUnpackRGB4B}, - {"RGB", "BGR;5", 16, ImagingUnpackBGR15}, /* compat */ - {"RGB", "RGBX", 32, copy4}, - {"RGB", "RGBX;L", 32, unpackRGBAL}, - {"RGB", "BGRX", 32, ImagingUnpackBGRX}, - {"RGB", "XRGB", 24, ImagingUnpackXRGB}, - {"RGB", "XBGR", 32, ImagingUnpackXBGR}, - {"RGB", "YCC;P", 24, ImagingUnpackYCC}, - {"RGB", "R", 8, band0}, - {"RGB", "G", 8, band1}, - {"RGB", "B", 8, band2}, + {"RGB", "RGB", 24, ImagingUnpackRGB}, + {"RGB", "RGB;L", 24, unpackRGBL}, + {"RGB", "RGB;R", 24, unpackRGBR}, + {"RGB", "RGB;16L", 48, unpackRGB16L}, + {"RGB", "RGB;16B", 48, unpackRGB16B}, + {"RGB", "BGR", 24, ImagingUnpackBGR}, + {"RGB", "RGB;15", 16, ImagingUnpackRGB15}, + {"RGB", "BGR;15", 16, ImagingUnpackBGR15}, + {"RGB", "RGB;16", 16, ImagingUnpackRGB16}, + {"RGB", "BGR;16", 16, ImagingUnpackBGR16}, + {"RGB", "RGB;4B", 16, ImagingUnpackRGB4B}, + {"RGB", "BGR;5", 16, ImagingUnpackBGR15}, /* compat */ + {"RGB", "RGBX", 32, copy4}, + {"RGB", "RGBX;L", 32, unpackRGBAL}, + {"RGB", "RGBA;L", 32, unpackRGBAL}, + {"RGB", "RGBA;15", 16, ImagingUnpackRGBA15}, + {"RGB", "BGRX", 32, ImagingUnpackBGRX}, + {"RGB", "XRGB", 32, ImagingUnpackXRGB}, + {"RGB", "XBGR", 32, ImagingUnpackXBGR}, + {"RGB", "YCC;P", 24, ImagingUnpackYCC}, + {"RGB", "R", 8, band0}, + {"RGB", "G", 8, band1}, + {"RGB", "B", 8, band2}, + {"RGB", "R;16L", 16, band016L}, + {"RGB", "G;16L", 16, band116L}, + {"RGB", "B;16L", 16, band216L}, + {"RGB", "R;16B", 16, band016B}, + {"RGB", "G;16B", 16, band116B}, + {"RGB", "B;16B", 16, band216B}, /* true colour w. alpha */ - {"RGBA", "LA", 16, unpackRGBALA}, - {"RGBA", "LA;16B", 32, unpackRGBALA16B}, - {"RGBA", "RGBA", 32, copy4}, - {"RGBA", "RGBa", 32, unpackRGBa}, - {"RGBA", "RGBa;16L", 64, unpackRGBa16L}, - {"RGBA", "RGBa;16B", 64, unpackRGBa16B}, - {"RGBA", "BGRa", 32, unpackBGRa}, - {"RGBA", "RGBA;I", 32, unpackRGBAI}, - {"RGBA", "RGBA;L", 32, unpackRGBAL}, - {"RGBA", "RGBA;15", 16, ImagingUnpackRGBA15}, - {"RGBA", "BGRA;15", 16, ImagingUnpackBGRA15}, - {"RGBA", "RGBA;4B", 16, ImagingUnpackRGBA4B}, - {"RGBA", "RGBA;16L", 64, unpackRGBA16L}, - {"RGBA", "RGBA;16B", 64, unpackRGBA16B}, - {"RGBA", "BGRA", 32, unpackBGRA}, - {"RGBA", "ARGB", 32, unpackARGB}, - {"RGBA", "ABGR", 32, unpackABGR}, - {"RGBA", "YCCA;P", 32, ImagingUnpackYCCA}, - {"RGBA", "R", 8, band0}, - {"RGBA", "G", 8, band1}, - {"RGBA", "B", 8, band2}, - {"RGBA", "A", 8, band3}, + {"RGBA", "LA", 16, unpackRGBALA}, + {"RGBA", "LA;16B", 32, unpackRGBALA16B}, + {"RGBA", "RGBA", 32, copy4}, + {"RGBA", "RGBAX", 40, copy4skip1}, + {"RGBA", "RGBAXX", 48, copy4skip2}, + {"RGBA", "RGBa", 32, unpackRGBa}, + {"RGBA", "RGBaX", 40, unpackRGBaskip1}, + {"RGBA", "RGBaXX", 48, unpackRGBaskip2}, + {"RGBA", "RGBa;16L", 64, unpackRGBa16L}, + {"RGBA", "RGBa;16B", 64, unpackRGBa16B}, + {"RGBA", "BGRa", 32, unpackBGRa}, + {"RGBA", "RGBA;I", 32, unpackRGBAI}, + {"RGBA", "RGBA;L", 32, unpackRGBAL}, + {"RGBA", "RGBA;15", 16, ImagingUnpackRGBA15}, + {"RGBA", "BGRA;15", 16, ImagingUnpackBGRA15}, + {"RGBA", "RGBA;4B", 16, ImagingUnpackRGBA4B}, + {"RGBA", "RGBA;16L", 64, unpackRGBA16L}, + {"RGBA", "RGBA;16B", 64, unpackRGBA16B}, + {"RGBA", "BGRA", 32, unpackBGRA}, + {"RGBA", "BGRA;16L", 64, unpackBGRA16L}, + {"RGBA", "BGRA;16B", 64, unpackBGRA16B}, + {"RGBA", "ARGB", 32, unpackARGB}, + {"RGBA", "ABGR", 32, unpackABGR}, + {"RGBA", "YCCA;P", 32, ImagingUnpackYCCA}, + {"RGBA", "R", 8, band0}, + {"RGBA", "G", 8, band1}, + {"RGBA", "B", 8, band2}, + {"RGBA", "A", 8, band3}, + {"RGBA", "R;16L", 16, band016L}, + {"RGBA", "G;16L", 16, band116L}, + {"RGBA", "B;16L", 16, band216L}, + {"RGBA", "A;16L", 16, band316L}, + {"RGBA", "R;16B", 16, band016B}, + {"RGBA", "G;16B", 16, band116B}, + {"RGBA", "B;16B", 16, band216B}, + {"RGBA", "A;16B", 16, band316B}, #ifdef WORDS_BIGENDIAN - {"RGB", "RGB;16N", 64, unpackRGB16B}, - {"RGBA", "RGBa;16N", 64, unpackRGBa16B}, - {"RGBA", "RGBA;16N", 64, unpackRGBA16B}, - {"RGBX", "RGBX;16N", 64, unpackRGBA16B}, + {"RGB", "RGB;16N", 48, unpackRGB16B}, + {"RGBA", "RGBa;16N", 64, unpackRGBa16B}, + {"RGBA", "RGBA;16N", 64, unpackRGBA16B}, + {"RGBX", "RGBX;16N", 64, unpackRGBA16B}, + {"RGB", "R;16N", 16, band016B}, + {"RGB", "G;16N", 16, band116B}, + {"RGB", "B;16N", 16, band216B}, + + {"RGBA", "R;16N", 16, band016B}, + {"RGBA", "G;16N", 16, band116B}, + {"RGBA", "B;16N", 16, band216B}, + {"RGBA", "A;16N", 16, band316B}, #else - {"RGB", "RGB;16N", 64, unpackRGB16L}, - {"RGBA", "RGBa;16N", 64, unpackRGBa16L}, - {"RGBA", "RGBA;16N", 64, unpackRGBA16L}, - {"RGBX", "RGBX;16N", 64, unpackRGBA16B}, + {"RGB", "RGB;16N", 48, unpackRGB16L}, + {"RGBA", "RGBa;16N", 64, unpackRGBa16L}, + {"RGBA", "RGBA;16N", 64, unpackRGBA16L}, + {"RGBX", "RGBX;16N", 64, unpackRGBA16L}, + {"RGB", "R;16N", 16, band016L}, + {"RGB", "G;16N", 16, band116L}, + {"RGB", "B;16N", 16, band216L}, + + + {"RGBA", "R;16N", 16, band016L}, + {"RGBA", "G;16N", 16, band116L}, + {"RGBA", "B;16N", 16, band216L}, + {"RGBA", "A;16N", 16, band316L}, #endif - /* true colour w. alpha premultiplied */ - {"RGBa", "RGBa", 32, copy4}, - {"RGBa", "BGRa", 32, unpackBGRA}, - {"RGBa", "aRGB", 32, unpackARGB}, - {"RGBa", "aBGR", 32, unpackABGR}, + {"RGBa", "RGBa", 32, copy4}, + {"RGBa", "BGRa", 32, unpackBGRA}, + {"RGBa", "aRGB", 32, unpackARGB}, + {"RGBa", "aBGR", 32, unpackABGR}, /* true colour w. padding */ - {"RGBX", "RGB", 24, ImagingUnpackRGB}, - {"RGBX", "RGB;L", 24, unpackRGBL}, - {"RGBX", "RGB;16B", 48, unpackRGB16B}, - {"RGBX", "BGR", 24, ImagingUnpackBGR}, - {"RGBX", "RGB;15", 16, ImagingUnpackRGB15}, - {"RGBX", "BGR;15", 16, ImagingUnpackBGR15}, - {"RGBX", "RGB;4B", 16, ImagingUnpackRGB4B}, - {"RGBX", "BGR;5", 16, ImagingUnpackBGR15}, /* compat */ - {"RGBX", "RGBX", 32, copy4}, - {"RGBX", "RGBX;L", 32, unpackRGBAL}, - {"RGBX", "RGBX;16L", 64, unpackRGBA16L}, - {"RGBX", "RGBX;16B", 64, unpackRGBA16B}, - {"RGBX", "BGRX", 32, ImagingUnpackBGRX}, - {"RGBX", "XRGB", 24, ImagingUnpackXRGB}, - {"RGBX", "XBGR", 32, ImagingUnpackXBGR}, - {"RGBX", "YCC;P", 24, ImagingUnpackYCC}, - {"RGBX", "R", 8, band0}, - {"RGBX", "G", 8, band1}, - {"RGBX", "B", 8, band2}, - {"RGBX", "X", 8, band3}, + {"RGBX", "RGB", 24, ImagingUnpackRGB}, + {"RGBX", "RGB;L", 24, unpackRGBL}, + {"RGBX", "RGB;16B", 48, unpackRGB16B}, + {"RGBX", "BGR", 24, ImagingUnpackBGR}, + {"RGBX", "RGB;15", 16, ImagingUnpackRGB15}, + {"RGBX", "BGR;15", 16, ImagingUnpackBGR15}, + {"RGBX", "RGB;4B", 16, ImagingUnpackRGB4B}, + {"RGBX", "BGR;5", 16, ImagingUnpackBGR15}, /* compat */ + {"RGBX", "RGBX", 32, copy4}, + {"RGBX", "RGBXX", 40, copy4skip1}, + {"RGBX", "RGBXXX", 48, copy4skip2}, + {"RGBX", "RGBX;L", 32, unpackRGBAL}, + {"RGBX", "RGBX;16L", 64, unpackRGBA16L}, + {"RGBX", "RGBX;16B", 64, unpackRGBA16B}, + {"RGBX", "BGRX", 32, ImagingUnpackBGRX}, + {"RGBX", "XRGB", 32, ImagingUnpackXRGB}, + {"RGBX", "XBGR", 32, ImagingUnpackXBGR}, + {"RGBX", "YCC;P", 24, ImagingUnpackYCC}, + {"RGBX", "R", 8, band0}, + {"RGBX", "G", 8, band1}, + {"RGBX", "B", 8, band2}, + {"RGBX", "X", 8, band3}, /* colour separation */ - {"CMYK", "CMYK", 32, copy4}, - {"CMYK", "CMYK;I", 32, unpackCMYKI}, - {"CMYK", "CMYK;L", 32, unpackRGBAL}, - {"CMYK", "C", 8, band0}, - {"CMYK", "M", 8, band1}, - {"CMYK", "Y", 8, band2}, - {"CMYK", "K", 8, band3}, - {"CMYK", "C;I", 8, band0I}, - {"CMYK", "M;I", 8, band1I}, - {"CMYK", "Y;I", 8, band2I}, - {"CMYK", "K;I", 8, band3I}, + {"CMYK", "CMYK", 32, copy4}, + {"CMYK", "CMYKX", 40, copy4skip1}, + {"CMYK", "CMYKXX", 48, copy4skip2}, + {"CMYK", "CMYK;I", 32, unpackCMYKI}, + {"CMYK", "CMYK;L", 32, unpackRGBAL}, + {"CMYK", "CMYK;16L", 64, unpackRGBA16L}, + {"CMYK", "CMYK;16B", 64, unpackRGBA16B}, + {"CMYK", "C", 8, band0}, + {"CMYK", "M", 8, band1}, + {"CMYK", "Y", 8, band2}, + {"CMYK", "K", 8, band3}, + {"CMYK", "C;I", 8, band0I}, + {"CMYK", "M;I", 8, band1I}, + {"CMYK", "Y;I", 8, band2I}, + {"CMYK", "K;I", 8, band3I}, + +#ifdef WORDS_BIGENDIAN + {"CMYK", "CMYK;16N", 64, unpackRGBA16B}, +#else + {"CMYK", "CMYK;16N", 64, unpackRGBA16L}, +#endif /* video (YCbCr) */ - {"YCbCr", "YCbCr", 24, ImagingUnpackRGB}, - {"YCbCr", "YCbCr;L", 24, unpackRGBL}, - {"YCbCr", "YCbCrX", 32, copy4}, - {"YCbCr", "YCbCrK", 32, copy4}, + {"YCbCr", "YCbCr", 24, ImagingUnpackRGB}, + {"YCbCr", "YCbCr;L", 24, unpackRGBL}, + {"YCbCr", "YCbCrX", 32, copy4}, + {"YCbCr", "YCbCrK", 32, copy4}, /* LAB Color */ - {"LAB", "LAB", 24, ImagingUnpackLAB}, - {"LAB", "L", 8, band0}, - {"LAB", "A", 8, band1}, - {"LAB", "B", 8, band2}, + {"LAB", "LAB", 24, ImagingUnpackLAB}, + {"LAB", "L", 8, band0}, + {"LAB", "A", 8, band1}, + {"LAB", "B", 8, band2}, /* HSV Color */ - {"HSV", "HSV", 24, ImagingUnpackRGB}, - {"HSV", "H", 8, band0}, - {"HSV", "S", 8, band1}, - {"HSV", "V", 8, band2}, + {"HSV", "HSV", 24, ImagingUnpackRGB}, + {"HSV", "H", 8, band0}, + {"HSV", "S", 8, band1}, + {"HSV", "V", 8, band2}, /* integer variations */ - {"I", "I", 32, copy4}, - {"I", "I;8", 8, unpackI8}, - {"I", "I;8S", 8, unpackI8S}, - {"I", "I;16", 16, unpackI16}, - {"I", "I;16S", 16, unpackI16S}, - {"I", "I;16B", 16, unpackI16B}, - {"I", "I;16BS", 16, unpackI16BS}, - {"I", "I;16N", 16, unpackI16N}, - {"I", "I;16NS", 16, unpackI16NS}, - {"I", "I;32", 32, unpackI32}, - {"I", "I;32S", 32, unpackI32S}, - {"I", "I;32B", 32, unpackI32B}, - {"I", "I;32BS", 32, unpackI32BS}, - {"I", "I;32N", 32, unpackI32N}, - {"I", "I;32NS", 32, unpackI32NS}, + {"I", "I", 32, copy4}, + {"I", "I;8", 8, unpackI8}, + {"I", "I;8S", 8, unpackI8S}, + {"I", "I;16", 16, unpackI16}, + {"I", "I;16S", 16, unpackI16S}, + {"I", "I;16B", 16, unpackI16B}, + {"I", "I;16BS", 16, unpackI16BS}, + {"I", "I;16N", 16, unpackI16N}, + {"I", "I;16NS", 16, unpackI16NS}, + {"I", "I;32", 32, unpackI32}, + {"I", "I;32S", 32, unpackI32S}, + {"I", "I;32B", 32, unpackI32B}, + {"I", "I;32BS", 32, unpackI32BS}, + {"I", "I;32N", 32, unpackI32N}, + {"I", "I;32NS", 32, unpackI32NS}, /* floating point variations */ - {"F", "F", 32, copy4}, - {"F", "F;8", 8, unpackF8}, - {"F", "F;8S", 8, unpackF8S}, - {"F", "F;16", 16, unpackF16}, - {"F", "F;16S", 16, unpackF16S}, - {"F", "F;16B", 16, unpackF16B}, - {"F", "F;16BS", 16, unpackF16BS}, - {"F", "F;16N", 16, unpackF16N}, - {"F", "F;16NS", 16, unpackF16NS}, - {"F", "F;32", 32, unpackF32}, - {"F", "F;32S", 32, unpackF32S}, - {"F", "F;32B", 32, unpackF32B}, - {"F", "F;32BS", 32, unpackF32BS}, - {"F", "F;32N", 32, unpackF32N}, - {"F", "F;32NS", 32, unpackF32NS}, - {"F", "F;32F", 32, unpackF32F}, - {"F", "F;32BF", 32, unpackF32BF}, - {"F", "F;32NF", 32, unpackF32NF}, + {"F", "F", 32, copy4}, + {"F", "F;8", 8, unpackF8}, + {"F", "F;8S", 8, unpackF8S}, + {"F", "F;16", 16, unpackF16}, + {"F", "F;16S", 16, unpackF16S}, + {"F", "F;16B", 16, unpackF16B}, + {"F", "F;16BS", 16, unpackF16BS}, + {"F", "F;16N", 16, unpackF16N}, + {"F", "F;16NS", 16, unpackF16NS}, + {"F", "F;32", 32, unpackF32}, + {"F", "F;32S", 32, unpackF32S}, + {"F", "F;32B", 32, unpackF32B}, + {"F", "F;32BS", 32, unpackF32BS}, + {"F", "F;32N", 32, unpackF32N}, + {"F", "F;32NS", 32, unpackF32NS}, + {"F", "F;32F", 32, unpackF32F}, + {"F", "F;32BF", 32, unpackF32BF}, + {"F", "F;32NF", 32, unpackF32NF}, #ifdef FLOAT64 - {"F", "F;64F", 64, unpackF64F}, - {"F", "F;64BF", 64, unpackF64BF}, - {"F", "F;64NF", 64, unpackF64NF}, + {"F", "F;64F", 64, unpackF64F}, + {"F", "F;64BF", 64, unpackF64BF}, + {"F", "F;64NF", 64, unpackF64NF}, #endif /* storage modes */ - {"I;16", "I;16", 16, copy2}, - {"I;16B", "I;16B", 16, copy2}, - {"I;16L", "I;16L", 16, copy2}, + {"I;16", "I;16", 16, copy2}, + {"I;16B", "I;16B", 16, copy2}, + {"I;16L", "I;16L", 16, copy2}, + + {"I;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. + {"I;16L", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. + {"I;16B", "I;16N", 16, unpackI16N_I16B}, - {"I;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. - {"I;16L", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. - {"I;16B", "I;16N", 16, unpackI16N_I16B}, + {"I;16", "I;16R", 16, unpackI16R_I16}, - {"I;16", "I;12", 12, unpackI12_I16}, // 12 bit Tiffs stored in 16bits. + {"I;16", "I;12", 12, unpackI12_I16}, // 12 bit Tiffs stored in 16bits. {NULL} /* sentinel */ }; - ImagingShuffler -ImagingFindUnpacker(const char* mode, const char* rawmode, int* bits_out) -{ +ImagingFindUnpacker(const char *mode, const char *rawmode, int *bits_out) { int i; /* find a suitable pixel unpacker */ - for (i = 0; unpackers[i].rawmode; i++) + for (i = 0; unpackers[i].rawmode; i++) { if (strcmp(unpackers[i].mode, mode) == 0 && strcmp(unpackers[i].rawmode, rawmode) == 0) { - if (bits_out) + if (bits_out) { *bits_out = unpackers[i].bits; + } return unpackers[i].unpack; } + } /* FIXME: configure a general unpacker based on the type codes... */ diff --git a/src/libImaging/UnpackYCC.c b/src/libImaging/UnpackYCC.c index 19da1f65443..0b177bdd4f5 100644 --- a/src/libImaging/UnpackYCC.c +++ b/src/libImaging/UnpackYCC.c @@ -5,7 +5,7 @@ * code to convert and unpack PhotoYCC data * * history: - * 97-01-25 fl Moved from PcdDecode.c + * 97-01-25 fl Moved from PcdDecode.c * * Copyright (c) Fredrik Lundh 1996-97. * Copyright (c) Secret Labs AB 1997. @@ -13,150 +13,151 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" - /* Tables generated by pcdtables.py, based on transforms taken from the "Colour Space Conversions FAQ" by Roberts/Ford. */ -static INT16 L[] = { 0, 1, 3, 4, 5, 7, 8, 10, 11, 12, 14, 15, 16, 18, -19, 20, 22, 23, 24, 26, 27, 29, 30, 31, 33, 34, 35, 37, 38, 39, 41, -42, 43, 45, 46, 48, 49, 50, 52, 53, 54, 56, 57, 58, 60, 61, 62, 64, -65, 67, 68, 69, 71, 72, 73, 75, 76, 77, 79, 80, 82, 83, 84, 86, 87, -88, 90, 91, 92, 94, 95, 96, 98, 99, 101, 102, 103, 105, 106, 107, 109, -110, 111, 113, 114, 115, 117, 118, 120, 121, 122, 124, 125, 126, 128, -129, 130, 132, 133, 134, 136, 137, 139, 140, 141, 143, 144, 145, 147, -148, 149, 151, 152, 153, 155, 156, 158, 159, 160, 162, 163, 164, 166, -167, 168, 170, 171, 173, 174, 175, 177, 178, 179, 181, 182, 183, 185, -186, 187, 189, 190, 192, 193, 194, 196, 197, 198, 200, 201, 202, 204, -205, 206, 208, 209, 211, 212, 213, 215, 216, 217, 219, 220, 221, 223, -224, 225, 227, 228, 230, 231, 232, 234, 235, 236, 238, 239, 240, 242, -243, 245, 246, 247, 249, 250, 251, 253, 254, 255, 257, 258, 259, 261, -262, 264, 265, 266, 268, 269, 270, 272, 273, 274, 276, 277, 278, 280, -281, 283, 284, 285, 287, 288, 289, 291, 292, 293, 295, 296, 297, 299, -300, 302, 303, 304, 306, 307, 308, 310, 311, 312, 314, 315, 317, 318, -319, 321, 322, 323, 325, 326, 327, 329, 330, 331, 333, 334, 336, 337, -338, 340, 341, 342, 344, 345, 346 }; +static INT16 L[] = { + 0, 1, 3, 4, 5, 7, 8, 10, 11, 12, 14, 15, 16, 18, 19, 20, + 22, 23, 24, 26, 27, 29, 30, 31, 33, 34, 35, 37, 38, 39, 41, 42, + 43, 45, 46, 48, 49, 50, 52, 53, 54, 56, 57, 58, 60, 61, 62, 64, + 65, 67, 68, 69, 71, 72, 73, 75, 76, 77, 79, 80, 82, 83, 84, 86, + 87, 88, 90, 91, 92, 94, 95, 96, 98, 99, 101, 102, 103, 105, 106, 107, + 109, 110, 111, 113, 114, 115, 117, 118, 120, 121, 122, 124, 125, 126, 128, 129, + 130, 132, 133, 134, 136, 137, 139, 140, 141, 143, 144, 145, 147, 148, 149, 151, + 152, 153, 155, 156, 158, 159, 160, 162, 163, 164, 166, 167, 168, 170, 171, 173, + 174, 175, 177, 178, 179, 181, 182, 183, 185, 186, 187, 189, 190, 192, 193, 194, + 196, 197, 198, 200, 201, 202, 204, 205, 206, 208, 209, 211, 212, 213, 215, 216, + 217, 219, 220, 221, 223, 224, 225, 227, 228, 230, 231, 232, 234, 235, 236, 238, + 239, 240, 242, 243, 245, 246, 247, 249, 250, 251, 253, 254, 255, 257, 258, 259, + 261, 262, 264, 265, 266, 268, 269, 270, 272, 273, 274, 276, 277, 278, 280, 281, + 283, 284, 285, 287, 288, 289, 291, 292, 293, 295, 296, 297, 299, 300, 302, 303, + 304, 306, 307, 308, 310, 311, 312, 314, 315, 317, 318, 319, 321, 322, 323, 325, + 326, 327, 329, 330, 331, 333, 334, 336, 337, 338, 340, 341, 342, 344, 345, 346}; -static INT16 CB[] = { -345, -343, -341, -338, -336, -334, -332, -329, --327, -325, -323, -321, -318, -316, -314, -312, -310, -307, -305, --303, -301, -298, -296, -294, -292, -290, -287, -285, -283, -281, --278, -276, -274, -272, -270, -267, -265, -263, -261, -258, -256, --254, -252, -250, -247, -245, -243, -241, -239, -236, -234, -232, --230, -227, -225, -223, -221, -219, -216, -214, -212, -210, -207, --205, -203, -201, -199, -196, -194, -192, -190, -188, -185, -183, --181, -179, -176, -174, -172, -170, -168, -165, -163, -161, -159, --156, -154, -152, -150, -148, -145, -143, -141, -139, -137, -134, --132, -130, -128, -125, -123, -121, -119, -117, -114, -112, -110, --108, -105, -103, -101, -99, -97, -94, -92, -90, -88, -85, -83, -81, --79, -77, -74, -72, -70, -68, -66, -63, -61, -59, -57, -54, -52, -50, --48, -46, -43, -41, -39, -37, -34, -32, -30, -28, -26, -23, -21, -19, --17, -15, -12, -10, -8, -6, -3, -1, 0, 2, 4, 7, 9, 11, 13, 16, 18, 20, -22, 24, 27, 29, 31, 33, 35, 38, 40, 42, 44, 47, 49, 51, 53, 55, 58, -60, 62, 64, 67, 69, 71, 73, 75, 78, 80, 82, 84, 86, 89, 91, 93, 95, -98, 100, 102, 104, 106, 109, 111, 113, 115, 118, 120, 122, 124, 126, -129, 131, 133, 135, 138, 140, 142, 144, 146, 149, 151, 153, 155, 157, -160, 162, 164, 166, 169, 171, 173, 175, 177, 180, 182, 184, 186, 189, -191, 193, 195, 197, 200, 202, 204, 206, 208, 211, 213, 215, 217, 220 }; +static INT16 CB[] = { + -345, -343, -341, -338, -336, -334, -332, -329, -327, -325, -323, -321, -318, -316, + -314, -312, -310, -307, -305, -303, -301, -298, -296, -294, -292, -290, -287, -285, + -283, -281, -278, -276, -274, -272, -270, -267, -265, -263, -261, -258, -256, -254, + -252, -250, -247, -245, -243, -241, -239, -236, -234, -232, -230, -227, -225, -223, + -221, -219, -216, -214, -212, -210, -207, -205, -203, -201, -199, -196, -194, -192, + -190, -188, -185, -183, -181, -179, -176, -174, -172, -170, -168, -165, -163, -161, + -159, -156, -154, -152, -150, -148, -145, -143, -141, -139, -137, -134, -132, -130, + -128, -125, -123, -121, -119, -117, -114, -112, -110, -108, -105, -103, -101, -99, + -97, -94, -92, -90, -88, -85, -83, -81, -79, -77, -74, -72, -70, -68, + -66, -63, -61, -59, -57, -54, -52, -50, -48, -46, -43, -41, -39, -37, + -34, -32, -30, -28, -26, -23, -21, -19, -17, -15, -12, -10, -8, -6, + -3, -1, 0, 2, 4, 7, 9, 11, 13, 16, 18, 20, 22, 24, + 27, 29, 31, 33, 35, 38, 40, 42, 44, 47, 49, 51, 53, 55, + 58, 60, 62, 64, 67, 69, 71, 73, 75, 78, 80, 82, 84, 86, + 89, 91, 93, 95, 98, 100, 102, 104, 106, 109, 111, 113, 115, 118, + 120, 122, 124, 126, 129, 131, 133, 135, 138, 140, 142, 144, 146, 149, + 151, 153, 155, 157, 160, 162, 164, 166, 169, 171, 173, 175, 177, 180, + 182, 184, 186, 189, 191, 193, 195, 197, 200, 202, 204, 206, 208, 211, + 213, 215, 217, 220}; -static INT16 GB[] = { 67, 67, 66, 66, 65, 65, 65, 64, 64, 63, 63, 62, -62, 62, 61, 61, 60, 60, 59, 59, 59, 58, 58, 57, 57, 56, 56, 56, 55, -55, 54, 54, 53, 53, 52, 52, 52, 51, 51, 50, 50, 49, 49, 49, 48, 48, -47, 47, 46, 46, 46, 45, 45, 44, 44, 43, 43, 43, 42, 42, 41, 41, 40, -40, 40, 39, 39, 38, 38, 37, 37, 37, 36, 36, 35, 35, 34, 34, 34, 33, -33, 32, 32, 31, 31, 31, 30, 30, 29, 29, 28, 28, 28, 27, 27, 26, 26, -25, 25, 25, 24, 24, 23, 23, 22, 22, 22, 21, 21, 20, 20, 19, 19, 19, -18, 18, 17, 17, 16, 16, 15, 15, 15, 14, 14, 13, 13, 12, 12, 12, 11, -11, 10, 10, 9, 9, 9, 8, 8, 7, 7, 6, 6, 6, 5, 5, 4, 4, 3, 3, 3, 2, 2, -1, 1, 0, 0, 0, 0, 0, -1, -1, -2, -2, -2, -3, -3, -4, -4, -5, -5, -5, --6, -6, -7, -7, -8, -8, -8, -9, -9, -10, -10, -11, -11, -11, -12, -12, --13, -13, -14, -14, -14, -15, -15, -16, -16, -17, -17, -18, -18, -18, --19, -19, -20, -20, -21, -21, -21, -22, -22, -23, -23, -24, -24, -24, --25, -25, -26, -26, -27, -27, -27, -28, -28, -29, -29, -30, -30, -30, --31, -31, -32, -32, -33, -33, -33, -34, -34, -35, -35, -36, -36, -36, --37, -37, -38, -38, -39, -39, -39, -40, -40, -41, -41, -42 }; +static INT16 GB[] = { + 67, 67, 66, 66, 65, 65, 65, 64, 64, 63, 63, 62, 62, 62, 61, 61, + 60, 60, 59, 59, 59, 58, 58, 57, 57, 56, 56, 56, 55, 55, 54, 54, + 53, 53, 52, 52, 52, 51, 51, 50, 50, 49, 49, 49, 48, 48, 47, 47, + 46, 46, 46, 45, 45, 44, 44, 43, 43, 43, 42, 42, 41, 41, 40, 40, + 40, 39, 39, 38, 38, 37, 37, 37, 36, 36, 35, 35, 34, 34, 34, 33, + 33, 32, 32, 31, 31, 31, 30, 30, 29, 29, 28, 28, 28, 27, 27, 26, + 26, 25, 25, 25, 24, 24, 23, 23, 22, 22, 22, 21, 21, 20, 20, 19, + 19, 19, 18, 18, 17, 17, 16, 16, 15, 15, 15, 14, 14, 13, 13, 12, + 12, 12, 11, 11, 10, 10, 9, 9, 9, 8, 8, 7, 7, 6, 6, 6, + 5, 5, 4, 4, 3, 3, 3, 2, 2, 1, 1, 0, 0, 0, 0, 0, + -1, -1, -2, -2, -2, -3, -3, -4, -4, -5, -5, -5, -6, -6, -7, -7, + -8, -8, -8, -9, -9, -10, -10, -11, -11, -11, -12, -12, -13, -13, -14, -14, + -14, -15, -15, -16, -16, -17, -17, -18, -18, -18, -19, -19, -20, -20, -21, -21, + -21, -22, -22, -23, -23, -24, -24, -24, -25, -25, -26, -26, -27, -27, -27, -28, + -28, -29, -29, -30, -30, -30, -31, -31, -32, -32, -33, -33, -33, -34, -34, -35, + -35, -36, -36, -36, -37, -37, -38, -38, -39, -39, -39, -40, -40, -41, -41, -42}; -static INT16 CR[] = { -249, -247, -245, -243, -241, -239, -238, -236, --234, -232, -230, -229, -227, -225, -223, -221, -219, -218, -216, --214, -212, -210, -208, -207, -205, -203, -201, -199, -198, -196, --194, -192, -190, -188, -187, -185, -183, -181, -179, -178, -176, --174, -172, -170, -168, -167, -165, -163, -161, -159, -157, -156, --154, -152, -150, -148, -147, -145, -143, -141, -139, -137, -136, --134, -132, -130, -128, -127, -125, -123, -121, -119, -117, -116, --114, -112, -110, -108, -106, -105, -103, -101, -99, -97, -96, -94, --92, -90, -88, -86, -85, -83, -81, -79, -77, -76, -74, -72, -70, -68, --66, -65, -63, -61, -59, -57, -55, -54, -52, -50, -48, -46, -45, -43, --41, -39, -37, -35, -34, -32, -30, -28, -26, -25, -23, -21, -19, -17, --15, -14, -12, -10, -8, -6, -4, -3, -1, 0, 2, 4, 5, 7, 9, 11, 13, 15, -16, 18, 20, 22, 24, 26, 27, 29, 31, 33, 35, 36, 38, 40, 42, 44, 46, -47, 49, 51, 53, 55, 56, 58, 60, 62, 64, 66, 67, 69, 71, 73, 75, 77, -78, 80, 82, 84, 86, 87, 89, 91, 93, 95, 97, 98, 100, 102, 104, 106, -107, 109, 111, 113, 115, 117, 118, 120, 122, 124, 126, 128, 129, 131, -133, 135, 137, 138, 140, 142, 144, 146, 148, 149, 151, 153, 155, 157, -158, 160, 162, 164, 166, 168, 169, 171, 173, 175, 177, 179, 180, 182, -184, 186, 188, 189, 191, 193, 195, 197, 199, 200, 202, 204, 206, 208, -209, 211, 213, 215 }; +static INT16 CR[] = { + -249, -247, -245, -243, -241, -239, -238, -236, -234, -232, -230, -229, -227, -225, + -223, -221, -219, -218, -216, -214, -212, -210, -208, -207, -205, -203, -201, -199, + -198, -196, -194, -192, -190, -188, -187, -185, -183, -181, -179, -178, -176, -174, + -172, -170, -168, -167, -165, -163, -161, -159, -157, -156, -154, -152, -150, -148, + -147, -145, -143, -141, -139, -137, -136, -134, -132, -130, -128, -127, -125, -123, + -121, -119, -117, -116, -114, -112, -110, -108, -106, -105, -103, -101, -99, -97, + -96, -94, -92, -90, -88, -86, -85, -83, -81, -79, -77, -76, -74, -72, + -70, -68, -66, -65, -63, -61, -59, -57, -55, -54, -52, -50, -48, -46, + -45, -43, -41, -39, -37, -35, -34, -32, -30, -28, -26, -25, -23, -21, + -19, -17, -15, -14, -12, -10, -8, -6, -4, -3, -1, 0, 2, 4, + 5, 7, 9, 11, 13, 15, 16, 18, 20, 22, 24, 26, 27, 29, + 31, 33, 35, 36, 38, 40, 42, 44, 46, 47, 49, 51, 53, 55, + 56, 58, 60, 62, 64, 66, 67, 69, 71, 73, 75, 77, 78, 80, + 82, 84, 86, 87, 89, 91, 93, 95, 97, 98, 100, 102, 104, 106, + 107, 109, 111, 113, 115, 117, 118, 120, 122, 124, 126, 128, 129, 131, + 133, 135, 137, 138, 140, 142, 144, 146, 148, 149, 151, 153, 155, 157, + 158, 160, 162, 164, 166, 168, 169, 171, 173, 175, 177, 179, 180, 182, + 184, 186, 188, 189, 191, 193, 195, 197, 199, 200, 202, 204, 206, 208, + 209, 211, 213, 215}; -static INT16 GR[] = { 127, 126, 125, 124, 123, 122, 121, 121, 120, 119, -118, 117, 116, 115, 114, 113, 112, 111, 110, 109, 108, 108, 107, 106, -105, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95, 95, 94, 93, 92, 91, -90, 89, 88, 87, 86, 85, 84, 83, 83, 82, 81, 80, 79, 78, 77, 76, 75, -74, 73, 72, 71, 70, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, -58, 57, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 45, 44, -43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 32, 31, 30, 29, 28, -27, 26, 25, 24, 23, 22, 21, 20, 19, 19, 18, 17, 16, 15, 14, 13, 12, -11, 10, 9, 8, 7, 6, 6, 5, 4, 3, 2, 1, 0, 0, -1, -2, -3, -4, -5, -5, --6, -7, -8, -9, -10, -11, -12, -13, -14, -15, -16, -17, -18, -18, -19, --20, -21, -22, -23, -24, -25, -26, -27, -28, -29, -30, -31, -31, -32, --33, -34, -35, -36, -37, -38, -39, -40, -41, -42, -43, -44, -44, -45, --46, -47, -48, -49, -50, -51, -52, -53, -54, -55, -56, -56, -57, -58, --59, -60, -61, -62, -63, -64, -65, -66, -67, -68, -69, -69, -70, -71, --72, -73, -74, -75, -76, -77, -78, -79, -80, -81, -82, -82, -83, -84, --85, -86, -87, -88, -89, -90, -91, -92, -93, -94, -94, -95, -96, -97, --98, -99, -100, -101, -102, -103, -104, -105, -106, -107, -107, -108 }; +static INT16 GR[] = { + 127, 126, 125, 124, 123, 122, 121, 121, 120, 119, 118, 117, 116, 115, 114, + 113, 112, 111, 110, 109, 108, 108, 107, 106, 105, 104, 103, 102, 101, 100, + 99, 98, 97, 96, 95, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, + 85, 84, 83, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, + 71, 70, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, + 57, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 45, + 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 32, 31, + 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 19, 18, 17, + 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 6, 5, 4, 3, + 2, 1, 0, 0, -1, -2, -3, -4, -5, -5, -6, -7, -8, -9, -10, + -11, -12, -13, -14, -15, -16, -17, -18, -18, -19, -20, -21, -22, -23, -24, + -25, -26, -27, -28, -29, -30, -31, -31, -32, -33, -34, -35, -36, -37, -38, + -39, -40, -41, -42, -43, -44, -44, -45, -46, -47, -48, -49, -50, -51, -52, + -53, -54, -55, -56, -56, -57, -58, -59, -60, -61, -62, -63, -64, -65, -66, + -67, -68, -69, -69, -70, -71, -72, -73, -74, -75, -76, -77, -78, -79, -80, + -81, -82, -82, -83, -84, -85, -86, -87, -88, -89, -90, -91, -92, -93, -94, + -94, -95, -96, -97, -98, -99, -100, -101, -102, -103, -104, -105, -106, -107, -107, + -108}; -#define R 0 -#define G 1 -#define B 2 -#define A 3 +#define R 0 +#define G 1 +#define B 2 +#define A 3 -#define YCC2RGB(rgb, y, cb, cr) {\ - int l = L[y];\ - int r = l + CR[cr];\ - int g = l + GR[cr] + GB[cb];\ - int b = l + CB[cb];\ - rgb[0] = (r <= 0) ? 0 : (r >= 255) ? 255 : r;\ - rgb[1] = (g <= 0) ? 0 : (g >= 255) ? 255 : g;\ - rgb[2] = (b <= 0) ? 0 : (b >= 255) ? 255 : b;\ -} +#define YCC2RGB(rgb, y, cb, cr) \ + { \ + int l = L[y]; \ + int r = l + CR[cr]; \ + int g = l + GR[cr] + GB[cb]; \ + int b = l + CB[cb]; \ + rgb[0] = (r <= 0) ? 0 : (r >= 255) ? 255 : r; \ + rgb[1] = (g <= 0) ? 0 : (g >= 255) ? 255 : g; \ + rgb[2] = (b <= 0) ? 0 : (b >= 255) ? 255 : b; \ + } void -ImagingUnpackYCC(UINT8* out, const UINT8* in, int pixels) -{ +ImagingUnpackYCC(UINT8 *out, const UINT8 *in, int pixels) { int i; /* PhotoYCC triplets */ for (i = 0; i < pixels; i++) { - YCC2RGB(out, in[0], in[1], in[2]); - out[A] = 255; - out += 4; in += 3; + YCC2RGB(out, in[0], in[1], in[2]); + out[A] = 255; + out += 4; + in += 3; } } void -ImagingUnpackYCCA(UINT8* out, const UINT8* in, int pixels) -{ +ImagingUnpackYCCA(UINT8 *out, const UINT8 *in, int pixels) { int i; /* PhotoYCC triplets plus premultiplied alpha */ for (i = 0; i < pixels; i++) { - /* Divide by alpha */ - UINT8 rgb[3]; - rgb[0] = (in[3] == 0) ? 0 : (((int) in[0] * 255) / in[3]); - rgb[1] = (in[3] == 0) ? 0 : (((int) in[1] * 255) / in[3]); - rgb[2] = (in[3] == 0) ? 0 : (((int) in[2] * 255) / in[3]); - /* Convert non-multiplied data to RGB */ - YCC2RGB(out, rgb[0], rgb[1], rgb[2]); - out[A] = in[3]; - out += 4; in += 4; + /* Divide by alpha */ + UINT8 rgb[3]; + rgb[0] = (in[3] == 0) ? 0 : (((int)in[0] * 255) / in[3]); + rgb[1] = (in[3] == 0) ? 0 : (((int)in[1] * 255) / in[3]); + rgb[2] = (in[3] == 0) ? 0 : (((int)in[2] * 255) / in[3]); + /* Convert non-multiplied data to RGB */ + YCC2RGB(out, rgb[0], rgb[1], rgb[2]); + out[A] = in[3]; + out += 4; + in += 4; } } diff --git a/src/libImaging/UnsharpMask.c b/src/libImaging/UnsharpMask.c index ec3bb23cc25..643ced49f17 100644 --- a/src/libImaging/UnsharpMask.c +++ b/src/libImaging/UnsharpMask.c @@ -6,27 +6,24 @@ /* Originally released under LGPL. Graciously donated to PIL for distribution under the standard PIL license in 2009." */ -#include "Python.h" #include "Imaging.h" - typedef UINT8 pixel[4]; - -static inline UINT8 clip8(int in) -{ - if (in >= 255) - return 255; - if (in <= 0) +static inline UINT8 +clip8(int in) { + if (in >= 255) { + return 255; + } + if (in <= 0) { return 0; - return (UINT8) in; + } + return (UINT8)in; } - Imaging -ImagingUnsharpMask(Imaging imOut, Imaging imIn, float radius, int percent, - int threshold) -{ +ImagingUnsharpMask( + Imaging imOut, Imaging imIn, float radius, int percent, int threshold) { ImagingSectionCookie cookie; Imaging result; @@ -40,8 +37,9 @@ ImagingUnsharpMask(Imaging imOut, Imaging imIn, float radius, int percent, /* First, do a gaussian blur on the image, putting results in imOut temporarily. All format checks are in gaussian blur. */ result = ImagingGaussianBlur(imOut, imIn, radius, 3); - if (!result) + if (!result) { return NULL; + } /* Now, go through each pixel, compare "normal" pixel to blurred pixel. If the difference is more than threshold values, apply @@ -51,8 +49,7 @@ ImagingUnsharpMask(Imaging imOut, Imaging imIn, float radius, int percent, ImagingSectionEnter(&cookie); for (y = 0; y < imIn->ysize; y++) { - if (imIn->image8) - { + if (imIn->image8) { lineIn8 = imIn->image8[y]; lineOut8 = imOut->image8[y]; for (x = 0; x < imIn->xsize; x++) { @@ -72,20 +69,24 @@ ImagingUnsharpMask(Imaging imOut, Imaging imIn, float radius, int percent, for (x = 0; x < imIn->xsize; x++) { /* compare in/out pixels, apply sharpening */ diff = lineIn[x][0] - lineOut[x][0]; - lineOut[x][0] = abs(diff) > threshold ? - clip8(lineIn[x][0] + diff * percent / 100) : lineIn[x][0]; + lineOut[x][0] = abs(diff) > threshold + ? clip8(lineIn[x][0] + diff * percent / 100) + : lineIn[x][0]; diff = lineIn[x][1] - lineOut[x][1]; - lineOut[x][1] = abs(diff) > threshold ? - clip8(lineIn[x][1] + diff * percent / 100) : lineIn[x][1]; + lineOut[x][1] = abs(diff) > threshold + ? clip8(lineIn[x][1] + diff * percent / 100) + : lineIn[x][1]; diff = lineIn[x][2] - lineOut[x][2]; - lineOut[x][2] = abs(diff) > threshold ? - clip8(lineIn[x][2] + diff * percent / 100) : lineIn[x][2]; + lineOut[x][2] = abs(diff) > threshold + ? clip8(lineIn[x][2] + diff * percent / 100) + : lineIn[x][2]; diff = lineIn[x][3] - lineOut[x][3]; - lineOut[x][3] = abs(diff) > threshold ? - clip8(lineIn[x][3] + diff * percent / 100) : lineIn[x][3]; + lineOut[x][3] = abs(diff) > threshold + ? clip8(lineIn[x][3] + diff * percent / 100) + : lineIn[x][3]; } } } diff --git a/src/libImaging/XbmDecode.c b/src/libImaging/XbmDecode.c index 8a203841bef..d6690de3d28 100644 --- a/src/libImaging/XbmDecode.c +++ b/src/libImaging/XbmDecode.c @@ -5,7 +5,7 @@ * decoder for XBM hex image data * * history: - * 96-04-13 fl Created + * 96-04-13 fl Created * * Copyright (c) Fredrik Lundh 1996. * Copyright (c) Secret Labs AB 1997. @@ -13,69 +13,66 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" -#define HEX(v) ((v >= '0' && v <= '9') ? v - '0' :\ - (v >= 'a' && v <= 'f') ? v - 'a' + 10 :\ - (v >= 'A' && v <= 'F') ? v - 'A' + 10 : 0) +#define HEX(v) \ + ((v >= '0' && v <= '9') ? v - '0' \ + : (v >= 'a' && v <= 'f') ? v - 'a' + 10 \ + : (v >= 'A' && v <= 'F') ? v - 'A' + 10 \ + : 0) int -ImagingXbmDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ +ImagingXbmDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { enum { BYTE = 1, SKIP }; - UINT8* ptr; + UINT8 *ptr; - if (!state->state) - state->state = SKIP; + if (!state->state) { + state->state = SKIP; + } ptr = buf; for (;;) { + if (state->state == SKIP) { + /* Skip forward until next 'x' */ - if (state->state == SKIP) { - - /* Skip forward until next 'x' */ - - while (bytes > 0) { - if (*ptr == 'x') - break; - ptr++; - bytes--; - } + while (bytes > 0) { + if (*ptr == 'x') { + break; + } + ptr++; + bytes--; + } - if (bytes == 0) - return ptr - buf; + if (bytes == 0) { + return ptr - buf; + } - state->state = BYTE; + state->state = BYTE; + } - } + if (bytes < 3) { + return ptr - buf; + } - if (bytes < 3) - return ptr - buf; + state->buffer[state->x] = (HEX(ptr[1]) << 4) + HEX(ptr[2]); - state->buffer[state->x] = (HEX(ptr[1])<<4) + HEX(ptr[2]); + if (++state->x >= state->bytes) { + /* Got a full line, unpack it */ + state->shuffle((UINT8 *)im->image[state->y], state->buffer, state->xsize); - if (++state->x >= state->bytes) { + state->x = 0; - /* Got a full line, unpack it */ - state->shuffle((UINT8*) im->image[state->y], state->buffer, - state->xsize); + if (++state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } + } - state->x = 0; - - if (++state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } - } - - ptr += 3; - bytes -= 3; - - state->state = SKIP; + ptr += 3; + bytes -= 3; + state->state = SKIP; } - } diff --git a/src/libImaging/XbmEncode.c b/src/libImaging/XbmEncode.c index e066fd6b583..eec4c0d8462 100644 --- a/src/libImaging/XbmEncode.c +++ b/src/libImaging/XbmEncode.c @@ -5,7 +5,7 @@ * encoder for Xbm data * * history: - * 96-11-01 fl created + * 96-11-01 fl created * * Copyright (c) Fredrik Lundh 1996. * Copyright (c) Secret Labs AB 1997. @@ -13,93 +13,83 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" - int -ImagingXbmEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ +ImagingXbmEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { const char *hex = "0123456789abcdef"; - UINT8* ptr = buf; + UINT8 *ptr = buf; int i, n; if (!state->state) { + /* 8 pixels are stored in no more than 6 bytes */ + state->bytes = 6 * (state->xsize + 7) / 8; - /* 8 pixels are stored in no more than 6 bytes */ - state->bytes = 6*(state->xsize+7)/8; - - state->state = 1; - + state->state = 1; } if (bytes < state->bytes) { - state->errcode = IMAGING_CODEC_MEMORY; - return 0; + state->errcode = IMAGING_CODEC_MEMORY; + return 0; } ptr = buf; while (bytes >= state->bytes) { - - state->shuffle(state->buffer, - (UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->xsize); - - if (state->y < state->ysize-1) { - - /* any line but the last */ - for (n = 0; n < state->xsize; n += 8) { - - i = state->buffer[n/8]; - - *ptr++ = '0'; - *ptr++ = 'x'; - *ptr++ = hex[(i>>4)&15]; - *ptr++ = hex[i&15]; - *ptr++ = ','; - bytes -= 5; - - if (++state->count >= 79/5) { - *ptr++ = '\n'; - bytes--; - state->count = 0; - } - - } - - state->y++; - - } else { - - /* last line */ - for (n = 0; n < state->xsize; n += 8) { - - i = state->buffer[n/8]; - - *ptr++ = '0'; - *ptr++ = 'x'; - *ptr++ = hex[(i>>4)&15]; - *ptr++ = hex[i&15]; - - if (n < state->xsize-8) { - *ptr++ = ','; - if (++state->count >= 79/5) { - *ptr++ = '\n'; - bytes--; - state->count = 0; - } - } else - *ptr++ = '\n'; - - bytes -= 5; - - } - - state->errcode = IMAGING_CODEC_END; - break; - } + state->shuffle( + state->buffer, + (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, + state->xsize); + + if (state->y < state->ysize - 1) { + /* any line but the last */ + for (n = 0; n < state->xsize; n += 8) { + i = state->buffer[n / 8]; + + *ptr++ = '0'; + *ptr++ = 'x'; + *ptr++ = hex[(i >> 4) & 15]; + *ptr++ = hex[i & 15]; + *ptr++ = ','; + bytes -= 5; + + if (++state->count >= 79 / 5) { + *ptr++ = '\n'; + bytes--; + state->count = 0; + } + } + + state->y++; + + } else { + /* last line */ + for (n = 0; n < state->xsize; n += 8) { + i = state->buffer[n / 8]; + + *ptr++ = '0'; + *ptr++ = 'x'; + *ptr++ = hex[(i >> 4) & 15]; + *ptr++ = hex[i & 15]; + + if (n < state->xsize - 8) { + *ptr++ = ','; + if (++state->count >= 79 / 5) { + *ptr++ = '\n'; + bytes--; + state->count = 0; + } + } else { + *ptr++ = '\n'; + } + + bytes -= 5; + } + + state->errcode = IMAGING_CODEC_END; + break; + } } return ptr - buf; diff --git a/src/libImaging/Zip.h b/src/libImaging/Zip.h deleted file mode 100644 index 21a336f908a..00000000000 --- a/src/libImaging/Zip.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * declarations for the ZIP codecs - * - * Copyright (c) Fredrik Lundh 1996. - */ - - -#include "zlib.h" - - -/* modes */ -#define ZIP_PNG 0 /* continuous, filtered image data */ -#define ZIP_PNG_PALETTE 1 /* non-continuous data, disable filtering */ -#define ZIP_TIFF_PREDICTOR 2 /* TIFF, with predictor */ -#define ZIP_TIFF 3 /* TIFF, without predictor */ - - -typedef struct { - - /* CONFIGURATION */ - - /* Codec mode */ - int mode; - - /* Optimize (max compression) SLOW!!! */ - int optimize; - - /* 0 no compression, 9 best compression, -1 default compression */ - int compress_level; - /* compression strategy Z_XXX */ - int compress_type; - - /* Predefined dictionary (experimental) */ - char* dictionary; - int dictionary_size; - - /* PRIVATE CONTEXT (set by decoder/encoder) */ - - z_stream z_stream; /* (de)compression stream */ - - UINT8* previous; /* previous line (allocated) */ - - int last_output; /* # bytes last output by inflate */ - - /* Compressor specific stuff */ - UINT8* prior; /* filter storage (allocated) */ - UINT8* up; - UINT8* average; - UINT8* paeth; - - UINT8* output; /* output data */ - - int prefix; /* size of filter prefix (0 for TIFF data) */ - - int interlaced; /* is the image interlaced? (PNG) */ - - int pass; /* current pass of the interlaced image (PNG) */ - -} ZIPSTATE; diff --git a/src/libImaging/ZipCodecs.h b/src/libImaging/ZipCodecs.h new file mode 100644 index 00000000000..50218b6c69a --- /dev/null +++ b/src/libImaging/ZipCodecs.h @@ -0,0 +1,58 @@ +/* + * The Python Imaging Library. + * $Id$ + * + * declarations for the ZIP codecs + * + * Copyright (c) Fredrik Lundh 1996. + */ + +#include "zlib.h" + +/* modes */ +#define ZIP_PNG 0 /* continuous, filtered image data */ +#define ZIP_PNG_PALETTE 1 /* non-continuous data, disable filtering */ +#define ZIP_TIFF_PREDICTOR 2 /* TIFF, with predictor */ +#define ZIP_TIFF 3 /* TIFF, without predictor */ + +typedef struct { + /* CONFIGURATION */ + + /* Codec mode */ + int mode; + + /* Optimize (max compression) SLOW!!! */ + int optimize; + + /* 0 no compression, 9 best compression, -1 default compression */ + int compress_level; + /* compression strategy Z_XXX */ + int compress_type; + + /* Predefined dictionary (experimental) */ + char *dictionary; + int dictionary_size; + + /* PRIVATE CONTEXT (set by decoder/encoder) */ + + z_stream z_stream; /* (de)compression stream */ + + UINT8 *previous; /* previous line (allocated) */ + + int last_output; /* # bytes last output by inflate */ + + /* Compressor specific stuff */ + UINT8 *prior; /* filter storage (allocated) */ + UINT8 *up; + UINT8 *average; + UINT8 *paeth; + + UINT8 *output; /* output data */ + + int prefix; /* size of filter prefix (0 for TIFF data) */ + + int interlaced; /* is the image interlaced? (PNG) */ + + int pass; /* current pass of the interlaced image (PNG) */ + +} ZIPSTATE; diff --git a/src/libImaging/ZipDecode.c b/src/libImaging/ZipDecode.c index e96e3200c14..8749678341e 100644 --- a/src/libImaging/ZipDecode.c +++ b/src/libImaging/ZipDecode.c @@ -15,23 +15,22 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" -#ifdef HAVE_LIBZ +#ifdef HAVE_LIBZ -#include "Zip.h" +#include "ZipCodecs.h" -static const int OFFSET[] = { 7, 3, 3, 1, 1, 0, 0 }; -static const int STARTING_COL[] = { 0, 4, 0, 2, 0, 1, 0 }; -static const int STARTING_ROW[] = { 0, 0, 4, 0, 2, 0, 1 }; -static const int COL_INCREMENT[] = { 8, 8, 4, 4, 2, 2, 1 }; -static const int ROW_INCREMENT[] = { 8, 8, 8, 4, 4, 2, 2 }; +static const int OFFSET[] = {7, 3, 3, 1, 1, 0, 0}; +static const int STARTING_COL[] = {0, 4, 0, 2, 0, 1, 0}; +static const int STARTING_ROW[] = {0, 0, 4, 0, 2, 0, 1}; +static const int COL_INCREMENT[] = {8, 8, 4, 4, 2, 2, 1}; +static const int ROW_INCREMENT[] = {8, 8, 8, 4, 4, 2, 2}; /* Get the length in bytes of a scanline in the pass specified, * for interlaced images */ -static int get_row_len(ImagingCodecState state, int pass) -{ +static int +get_row_len(ImagingCodecState state, int pass) { int row_len = (state->xsize + OFFSET[pass]) / COL_INCREMENT[pass]; return ((row_len * state->bits) + 7) / 8; } @@ -41,20 +40,19 @@ static int get_row_len(ImagingCodecState state, int pass) /* -------------------------------------------------------------------- */ int -ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ - ZIPSTATE* context = (ZIPSTATE*) state->context; +ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + ZIPSTATE *context = (ZIPSTATE *)state->context; int err; int n; - UINT8* ptr; + UINT8 *ptr; int i, bpp; int row_len; if (!state->state) { - /* Initialization */ - if (context->mode == ZIP_PNG || context->mode == ZIP_PNG_PALETTE) + if (context->mode == ZIP_PNG || context->mode == ZIP_PNG_PALETTE) { context->prefix = 1; /* PNG */ + } /* overflow check for malloc */ if (state->bytes > INT_MAX - 1) { @@ -65,8 +63,8 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) prefix, and allocate a buffer to hold the previous line */ free(state->buffer); /* malloc check ok, overflow checked above */ - state->buffer = (UINT8*) malloc(state->bytes+1); - context->previous = (UINT8*) malloc(state->bytes+1); + state->buffer = (UINT8 *)malloc(state->bytes + 1); + context->previous = (UINT8 *)malloc(state->bytes + 1); if (!state->buffer || !context->previous) { state->errcode = IMAGING_CODEC_MEMORY; return -1; @@ -75,12 +73,12 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) context->last_output = 0; /* Initialize to black */ - memset(context->previous, 0, state->bytes+1); + memset(context->previous, 0, state->bytes + 1); /* Setup decompression context */ - context->z_stream.zalloc = (alloc_func) NULL; - context->z_stream.zfree = (free_func) NULL; - context->z_stream.opaque = (voidpf) NULL; + context->z_stream.zalloc = (alloc_func)NULL; + context->z_stream.zfree = (free_func)NULL; + context->z_stream.opaque = (voidpf)NULL; err = inflateInit(&context->z_stream); if (err < 0) { @@ -97,7 +95,6 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) /* Ready to decode */ state->state = 1; - } if (context->interlaced) { @@ -112,21 +109,20 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) /* Decompress what we've got this far */ while (context->z_stream.avail_in > 0) { - context->z_stream.next_out = state->buffer + context->last_output; - context->z_stream.avail_out = - row_len + context->prefix - context->last_output; + context->z_stream.avail_out = row_len + context->prefix - context->last_output; err = inflate(&context->z_stream, Z_NO_FLUSH); if (err < 0) { /* Something went wrong inside the compression library */ - if (err == Z_DATA_ERROR) + if (err == Z_DATA_ERROR) { state->errcode = IMAGING_CODEC_BROKEN; - else if (err == Z_MEM_ERROR) + } else if (err == Z_MEM_ERROR) { state->errcode = IMAGING_CODEC_MEMORY; - else + } else { state->errcode = IMAGING_CODEC_CONFIG; + } free(context->previous); context->previous = NULL; inflateEnd(&context->z_stream); @@ -142,68 +138,74 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) /* Apply predictor */ switch (context->mode) { - case ZIP_PNG: - switch (state->buffer[0]) { - case 0: - break; - case 1: - /* prior */ - bpp = (state->bits + 7) / 8; - for (i = bpp+1; i <= row_len; i++) - state->buffer[i] += state->buffer[i-bpp]; - break; - case 2: - /* up */ - for (i = 1; i <= row_len; i++) - state->buffer[i] += context->previous[i]; - break; - case 3: - /* average */ - bpp = (state->bits + 7) / 8; - for (i = 1; i <= bpp; i++) - state->buffer[i] += context->previous[i]/2; - for (; i <= row_len; i++) - state->buffer[i] += - (state->buffer[i-bpp] + context->previous[i])/2; + case ZIP_PNG: + switch (state->buffer[0]) { + case 0: + break; + case 1: + /* prior */ + bpp = (state->bits + 7) / 8; + for (i = bpp + 1; i <= row_len; i++) { + state->buffer[i] += state->buffer[i - bpp]; + } + break; + case 2: + /* up */ + for (i = 1; i <= row_len; i++) { + state->buffer[i] += context->previous[i]; + } + break; + case 3: + /* average */ + bpp = (state->bits + 7) / 8; + for (i = 1; i <= bpp; i++) { + state->buffer[i] += context->previous[i] / 2; + } + for (; i <= row_len; i++) { + state->buffer[i] += + (state->buffer[i - bpp] + context->previous[i]) / 2; + } + break; + case 4: + /* paeth filtering */ + bpp = (state->bits + 7) / 8; + for (i = 1; i <= bpp; i++) { + state->buffer[i] += context->previous[i]; + } + for (; i <= row_len; i++) { + int a, b, c; + int pa, pb, pc; + + /* fetch pixels */ + a = state->buffer[i - bpp]; + b = context->previous[i]; + c = context->previous[i - bpp]; + + /* distances to surrounding pixels */ + pa = abs(b - c); + pb = abs(a - c); + pc = abs(a + b - 2 * c); + + /* pick predictor with the shortest distance */ + state->buffer[i] += (pa <= pb && pa <= pc) ? a + : (pb <= pc) ? b + : c; + } + break; + default: + state->errcode = IMAGING_CODEC_UNKNOWN; + free(context->previous); + context->previous = NULL; + inflateEnd(&context->z_stream); + return -1; + } break; - case 4: - /* paeth filtering */ + case ZIP_TIFF_PREDICTOR: bpp = (state->bits + 7) / 8; - for (i = 1; i <= bpp; i++) - state->buffer[i] += context->previous[i]; - for (; i <= row_len; i++) { - int a, b, c; - int pa, pb, pc; - - /* fetch pixels */ - a = state->buffer[i-bpp]; - b = context->previous[i]; - c = context->previous[i-bpp]; - - /* distances to surrounding pixels */ - pa = abs(b - c); - pb = abs(a - c); - pc = abs(a + b - 2*c); - - /* pick predictor with the shortest distance */ - state->buffer[i] += - (pa <= pb && pa <= pc) ? a : (pb <= pc) ? b : c; - + for (i = bpp + 1; i <= row_len; i++) { + state->buffer[i] += state->buffer[i - bpp]; } break; - default: - state->errcode = IMAGING_CODEC_UNKNOWN; - free(context->previous); - context->previous = NULL; - inflateEnd(&context->z_stream); - return -1; - } - break; - case ZIP_TIFF_PREDICTOR: - bpp = (state->bits + 7) / 8; - for (i = bpp+1; i <= row_len; i++) - state->buffer[i] += state->buffer[i-bpp]; - break; } /* Stuff data into the image */ @@ -212,20 +214,22 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) if (state->bits >= 8) { /* Stuff pixels in their correct location, one by one */ for (i = 0; i < row_len; i += ((state->bits + 7) / 8)) { - state->shuffle((UINT8*) im->image[state->y] + - col * im->pixelsize, - state->buffer + context->prefix + i, 1); + state->shuffle( + (UINT8 *)im->image[state->y] + col * im->pixelsize, + state->buffer + context->prefix + i, + 1); col += COL_INCREMENT[context->pass]; } } else { /* Handle case with more than a pixel in each byte */ - int row_bits = ((state->xsize + OFFSET[context->pass]) - / COL_INCREMENT[context->pass]) * state->bits; + int row_bits = ((state->xsize + OFFSET[context->pass]) / + COL_INCREMENT[context->pass]) * + state->bits; for (i = 0; i < row_bits; i += state->bits) { UINT8 byte = *(state->buffer + context->prefix + (i / 8)); byte <<= (i % 8); - state->shuffle((UINT8*) im->image[state->y] + - col * im->pixelsize, &byte, 1); + state->shuffle( + (UINT8 *)im->image[state->y] + col * im->pixelsize, &byte, 1); col += COL_INCREMENT[context->pass]; } } @@ -242,13 +246,14 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) row_len = get_row_len(state, context->pass); /* Since we're moving to the "first" line, the previous line * should be black to make filters work correctly */ - memset(state->buffer, 0, state->bytes+1); + memset(state->buffer, 0, state->bytes + 1); } } else { - state->shuffle((UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->buffer + context->prefix, - state->xsize); + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->buffer + context->prefix, + state->xsize); state->y++; } @@ -256,7 +261,6 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) context->last_output = 0; if (state->y >= state->ysize || err == Z_STREAM_END) { - /* The image and the data should end simultaneously */ /* if (state->y < state->ysize || err != Z_STREAM_END) state->errcode = IMAGING_CODEC_BROKEN; */ @@ -265,26 +269,23 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) context->previous = NULL; inflateEnd(&context->z_stream); return -1; /* end of file (errcode=0) */ - } /* Swap buffer pointers */ ptr = state->buffer; state->buffer = context->previous; context->previous = ptr; - } return bytes; /* consumed all of it */ - } - -int ImagingZipDecodeCleanup(ImagingCodecState state){ +int +ImagingZipDecodeCleanup(ImagingCodecState state) { /* called to free the decompression engine when the decode terminates due to a corrupt or truncated image */ - ZIPSTATE* context = (ZIPSTATE*) state->context; + ZIPSTATE *context = (ZIPSTATE *)state->context; /* Clean up */ if (context->previous) { diff --git a/src/libImaging/ZipEncode.c b/src/libImaging/ZipEncode.c index fa1c4e72876..edbce36822c 100644 --- a/src/libImaging/ZipEncode.c +++ b/src/libImaging/ZipEncode.c @@ -14,25 +14,22 @@ * See the README file for information on usage and redistribution. */ - #include "Imaging.h" -#ifdef HAVE_LIBZ +#ifdef HAVE_LIBZ -#include "Zip.h" +#include "ZipCodecs.h" int -ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ - ZIPSTATE* context = (ZIPSTATE*) state->context; +ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + ZIPSTATE *context = (ZIPSTATE *)state->context; int err; int compress_level, compress_type; - UINT8* ptr; + UINT8 *ptr; int i, bpp, s, sum; ImagingSectionCookie cookie; if (!state->state) { - /* Initialization */ /* Valid modes are ZIP_PNG, ZIP_PNG_PALETTE, and ZIP_TIFF */ @@ -47,14 +44,14 @@ ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) and allocate filter buffers */ free(state->buffer); /* malloc check ok, overflow checked above */ - state->buffer = (UINT8*) malloc(state->bytes+1); - context->previous = (UINT8*) malloc(state->bytes+1); - context->prior = (UINT8*) malloc(state->bytes+1); - context->up = (UINT8*) malloc(state->bytes+1); - context->average = (UINT8*) malloc(state->bytes+1); - context->paeth = (UINT8*) malloc(state->bytes+1); - if (!state->buffer || !context->previous || !context->prior || - !context->up || !context->average || !context->paeth) { + state->buffer = (UINT8 *)malloc(state->bytes + 1); + context->previous = (UINT8 *)malloc(state->bytes + 1); + context->prior = (UINT8 *)malloc(state->bytes + 1); + context->up = (UINT8 *)malloc(state->bytes + 1); + context->average = (UINT8 *)malloc(state->bytes + 1); + context->paeth = (UINT8 *)malloc(state->bytes + 1); + if (!state->buffer || !context->previous || !context->prior || !context->up || + !context->average || !context->paeth) { free(context->paeth); free(context->average); free(context->up); @@ -72,7 +69,7 @@ ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) context->paeth[0] = 4; /* Initialise previous buffer to black */ - memset(context->previous, 0, state->bytes+1); + memset(context->previous, 0, state->bytes + 1); /* Setup compression context */ context->z_stream.zalloc = (alloc_func)0; @@ -81,33 +78,37 @@ ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) context->z_stream.next_in = 0; context->z_stream.avail_in = 0; - compress_level = (context->optimize) ? Z_BEST_COMPRESSION - : context->compress_level; + compress_level = + (context->optimize) ? Z_BEST_COMPRESSION : context->compress_level; if (context->compress_type == -1) { - compress_type = (context->mode == ZIP_PNG) ? Z_FILTERED - : Z_DEFAULT_STRATEGY; + compress_type = + (context->mode == ZIP_PNG) ? Z_FILTERED : Z_DEFAULT_STRATEGY; } else { compress_type = context->compress_type; } - err = deflateInit2(&context->z_stream, - /* compression level */ - compress_level, - /* compression method */ - Z_DEFLATED, - /* compression memory resources */ - 15, 9, - /* compression strategy (image data are filtered)*/ - compress_type); + err = deflateInit2( + &context->z_stream, + /* compression level */ + compress_level, + /* compression method */ + Z_DEFLATED, + /* compression memory resources */ + 15, + 9, + /* compression strategy (image data are filtered)*/ + compress_type); if (err < 0) { state->errcode = IMAGING_CODEC_CONFIG; return -1; } if (context->dictionary && context->dictionary_size > 0) { - err = deflateSetDictionary(&context->z_stream, (unsigned char *)context->dictionary, - context->dictionary_size); + err = deflateSetDictionary( + &context->z_stream, + (unsigned char *)context->dictionary, + context->dictionary_size); if (err < 0) { state->errcode = IMAGING_CODEC_CONFIG; return -1; @@ -116,7 +117,6 @@ ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) /* Ready to decode */ state->state = 1; - } /* Setup the destination buffer */ @@ -128,12 +128,13 @@ ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) if (err < 0) { /* Something went wrong inside the compression library */ - if (err == Z_DATA_ERROR) + if (err == Z_DATA_ERROR) { state->errcode = IMAGING_CODEC_BROKEN; - else if (err == Z_MEM_ERROR) + } else if (err == Z_MEM_ERROR) { state->errcode = IMAGING_CODEC_MEMORY; - else + } else { state->errcode = IMAGING_CODEC_CONFIG; + } free(context->paeth); free(context->average); free(context->up); @@ -146,200 +147,194 @@ ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) ImagingSectionEnter(&cookie); for (;;) { - switch (state->state) { + case 1: + + /* Compress image data */ + while (context->z_stream.avail_out > 0) { + if (state->y >= state->ysize) { + /* End of image; now flush compressor buffers */ + state->state = 2; + break; + } - case 1: - - /* Compress image data */ - while (context->z_stream.avail_out > 0) { - - if (state->y >= state->ysize) { - /* End of image; now flush compressor buffers */ - state->state = 2; - break; - - } - - /* Stuff image data into the compressor */ - state->shuffle(state->buffer+1, - (UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->xsize); - - state->y++; - - context->output = state->buffer; + /* Stuff image data into the compressor */ + state->shuffle( + state->buffer + 1, + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->xsize); - if (context->mode == ZIP_PNG) { + state->y++; - /* Filter the image data. For each line, select - the filter that gives the least total distance - from zero for the filtered data (taken from - LIBPNG) */ + context->output = state->buffer; - bpp = (state->bits + 7) / 8; + if (context->mode == ZIP_PNG) { + /* Filter the image data. For each line, select + the filter that gives the least total distance + from zero for the filtered data (taken from + LIBPNG) */ - /* 0. No filter */ - for (i = 1, sum = 0; i <= state->bytes; i++) { - UINT8 v = state->buffer[i]; - sum += (v < 128) ? v : 256 - v; - } + bpp = (state->bits + 7) / 8; - /* 2. Up. We'll test this first to save time when - an image line is identical to the one above. */ - if (sum > 0) { - for (i = 1, s = 0; i <= state->bytes; i++) { - UINT8 v = state->buffer[i] - context->previous[i]; - context->up[i] = v; - s += (v < 128) ? v : 256 - v; - } - if (s < sum) { - context->output = context->up; - sum = s; /* 0 if line was duplicated */ - } - } - - /* 1. Prior */ - if (sum > 0) { - for (i = 1, s = 0; i <= bpp; i++) { + /* 0. No filter */ + for (i = 1, sum = 0; i <= state->bytes; i++) { UINT8 v = state->buffer[i]; - context->prior[i] = v; - s += (v < 128) ? v : 256 - v; - } - for (; i <= state->bytes; i++) { - UINT8 v = state->buffer[i] - state->buffer[i-bpp]; - context->prior[i] = v; - s += (v < 128) ? v : 256 - v; - } - if (s < sum) { - context->output = context->prior; - sum = s; /* 0 if line is solid */ + sum += (v < 128) ? v : 256 - v; } - } - /* 3. Average (not very common in real-life images, - so its only used with the optimize option) */ - if (context->optimize && sum > 0) { - for (i = 1, s = 0; i <= bpp; i++) { - UINT8 v = state->buffer[i] - context->previous[i]/2; - context->average[i] = v; - s += (v < 128) ? v : 256 - v; - } - for (; i <= state->bytes; i++) { - UINT8 v = state->buffer[i] - - (state->buffer[i-bpp] + context->previous[i])/2; - context->average[i] = v; - s += (v < 128) ? v : 256 - v; + /* 2. Up. We'll test this first to save time when + an image line is identical to the one above. */ + if (sum > 0) { + for (i = 1, s = 0; i <= state->bytes; i++) { + UINT8 v = state->buffer[i] - context->previous[i]; + context->up[i] = v; + s += (v < 128) ? v : 256 - v; + } + if (s < sum) { + context->output = context->up; + sum = s; /* 0 if line was duplicated */ + } } - if (s < sum) { - context->output = context->average; - sum = s; - } - } - /* 4. Paeth */ - if (sum > 0) { - for (i = 1, s = 0; i <= bpp; i++) { - UINT8 v = state->buffer[i] - context->previous[i]; - context->paeth[i] = v; - s += (v < 128) ? v : 256 - v; + /* 1. Prior */ + if (sum > 0) { + for (i = 1, s = 0; i <= bpp; i++) { + UINT8 v = state->buffer[i]; + context->prior[i] = v; + s += (v < 128) ? v : 256 - v; + } + for (; i <= state->bytes; i++) { + UINT8 v = state->buffer[i] - state->buffer[i - bpp]; + context->prior[i] = v; + s += (v < 128) ? v : 256 - v; + } + if (s < sum) { + context->output = context->prior; + sum = s; /* 0 if line is solid */ + } } - for (; i <= state->bytes; i++) { - UINT8 v; - int a, b, c; - int pa, pb, pc; - - /* fetch pixels */ - a = state->buffer[i-bpp]; - b = context->previous[i]; - c = context->previous[i-bpp]; - - /* distances to surrounding pixels */ - pa = abs(b - c); - pb = abs(a - c); - pc = abs(a + b - 2*c); - - /* pick predictor with the shortest distance */ - v = state->buffer[i] - - ((pa <= pb && pa <= pc) ? a : - (pb <= pc) ? b : c); - context->paeth[i] = v; - s += (v < 128) ? v : 256 - v; + + /* 3. Average (not very common in real-life images, + so its only used with the optimize option) */ + if (context->optimize && sum > 0) { + for (i = 1, s = 0; i <= bpp; i++) { + UINT8 v = state->buffer[i] - context->previous[i] / 2; + context->average[i] = v; + s += (v < 128) ? v : 256 - v; + } + for (; i <= state->bytes; i++) { + UINT8 v = + state->buffer[i] - + (state->buffer[i - bpp] + context->previous[i]) / 2; + context->average[i] = v; + s += (v < 128) ? v : 256 - v; + } + if (s < sum) { + context->output = context->average; + sum = s; + } } - if (s < sum) { - context->output = context->paeth; - sum = s; + + /* 4. Paeth */ + if (sum > 0) { + for (i = 1, s = 0; i <= bpp; i++) { + UINT8 v = state->buffer[i] - context->previous[i]; + context->paeth[i] = v; + s += (v < 128) ? v : 256 - v; + } + for (; i <= state->bytes; i++) { + UINT8 v; + int a, b, c; + int pa, pb, pc; + + /* fetch pixels */ + a = state->buffer[i - bpp]; + b = context->previous[i]; + c = context->previous[i - bpp]; + + /* distances to surrounding pixels */ + pa = abs(b - c); + pb = abs(a - c); + pc = abs(a + b - 2 * c); + + /* pick predictor with the shortest distance */ + v = state->buffer[i] - ((pa <= pb && pa <= pc) ? a + : (pb <= pc) ? b + : c); + context->paeth[i] = v; + s += (v < 128) ? v : 256 - v; + } + if (s < sum) { + context->output = context->paeth; + sum = s; + } } } - } - /* Compress this line */ - context->z_stream.next_in = context->output; - context->z_stream.avail_in = state->bytes+1; - - err = deflate(&context->z_stream, Z_NO_FLUSH); - - if (err < 0) { - /* Something went wrong inside the compression library */ - if (err == Z_DATA_ERROR) - state->errcode = IMAGING_CODEC_BROKEN; - else if (err == Z_MEM_ERROR) - state->errcode = IMAGING_CODEC_MEMORY; - else - state->errcode = IMAGING_CODEC_CONFIG; - free(context->paeth); - free(context->average); - free(context->up); - free(context->prior); - free(context->previous); - deflateEnd(&context->z_stream); - ImagingSectionLeave(&cookie); - return -1; - } + /* Compress this line */ + context->z_stream.next_in = context->output; + context->z_stream.avail_in = state->bytes + 1; - /* Swap buffer pointers */ - ptr = state->buffer; - state->buffer = context->previous; - context->previous = ptr; + err = deflate(&context->z_stream, Z_NO_FLUSH); - } + if (err < 0) { + /* Something went wrong inside the compression library */ + if (err == Z_DATA_ERROR) { + state->errcode = IMAGING_CODEC_BROKEN; + } else if (err == Z_MEM_ERROR) { + state->errcode = IMAGING_CODEC_MEMORY; + } else { + state->errcode = IMAGING_CODEC_CONFIG; + } + free(context->paeth); + free(context->average); + free(context->up); + free(context->prior); + free(context->previous); + deflateEnd(&context->z_stream); + ImagingSectionLeave(&cookie); + return -1; + } - if (context->z_stream.avail_out == 0) - break; /* Buffer full */ + /* Swap buffer pointers */ + ptr = state->buffer; + state->buffer = context->previous; + context->previous = ptr; + } - case 2: + if (context->z_stream.avail_out == 0) { + break; /* Buffer full */ + } - /* End of image data; flush compressor buffers */ + case 2: - while (context->z_stream.avail_out > 0) { + /* End of image data; flush compressor buffers */ - err = deflate(&context->z_stream, Z_FINISH); + while (context->z_stream.avail_out > 0) { + err = deflate(&context->z_stream, Z_FINISH); - if (err == Z_STREAM_END) { + if (err == Z_STREAM_END) { + free(context->paeth); + free(context->average); + free(context->up); + free(context->prior); + free(context->previous); - free(context->paeth); - free(context->average); - free(context->up); - free(context->prior); - free(context->previous); + deflateEnd(&context->z_stream); - deflateEnd(&context->z_stream); + state->errcode = IMAGING_CODEC_END; - state->errcode = IMAGING_CODEC_END; + break; + } - break; + if (context->z_stream.avail_out == 0) { + break; /* Buffer full */ + } } - - if (context->z_stream.avail_out == 0) - break; /* Buffer full */ - - } - } ImagingSectionLeave(&cookie); return bytes - context->z_stream.avail_out; - } /* Should never ever arrive here... */ @@ -354,22 +349,19 @@ ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) int ImagingZipEncodeCleanup(ImagingCodecState state) { - ZIPSTATE* context = (ZIPSTATE*) state->context; + ZIPSTATE *context = (ZIPSTATE *)state->context; if (context->dictionary) { - free (context->dictionary); + free(context->dictionary); context->dictionary = NULL; } return -1; } - - -const char* -ImagingZipVersion(void) -{ - return ZLIB_VERSION; +const char * +ImagingZipVersion(void) { + return zlibVersion(); } #endif diff --git a/src/libImaging/codec_fd.c b/src/libImaging/codec_fd.c index 7bd4dadf868..5261681107b 100644 --- a/src/libImaging/codec_fd.c +++ b/src/libImaging/codec_fd.c @@ -1,11 +1,8 @@ #include "Python.h" #include "Imaging.h" -#include "../py3.h" - Py_ssize_t -_imaging_read_pyFd(PyObject *fd, char* dest, Py_ssize_t bytes) -{ +_imaging_read_pyFd(PyObject *fd, char *dest, Py_ssize_t bytes) { /* dest should be a buffer bytes long, returns length of read -1 on error */ @@ -30,16 +27,13 @@ _imaging_read_pyFd(PyObject *fd, char* dest, Py_ssize_t bytes) Py_DECREF(result); return length; - err: +err: Py_DECREF(result); return -1; - } Py_ssize_t -_imaging_write_pyFd(PyObject *fd, char* src, Py_ssize_t bytes) -{ - +_imaging_write_pyFd(PyObject *fd, char *src, Py_ssize_t bytes) { PyObject *result; PyObject *byteObj; @@ -50,29 +44,25 @@ _imaging_write_pyFd(PyObject *fd, char* src, Py_ssize_t bytes) Py_DECREF(result); return bytes; - } int -_imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence) -{ +_imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence) { PyObject *result; result = PyObject_CallMethod(fd, "seek", "ni", offset, whence); Py_DECREF(result); return 0; - } Py_ssize_t -_imaging_tell_pyFd(PyObject *fd) -{ +_imaging_tell_pyFd(PyObject *fd) { PyObject *result; Py_ssize_t location; result = PyObject_CallMethod(fd, "tell", NULL); - location = PyInt_AsSsize_t(result); + location = PyLong_AsSsize_t(result); Py_DECREF(result); return location; diff --git a/src/map.c b/src/map.c index 76b3160129f..c298bd1482a 100644 --- a/src/map.c +++ b/src/map.c @@ -20,310 +20,61 @@ #include "Python.h" -#include "Imaging.h" - -#include "py3.h" +#include "libImaging/Imaging.h" /* compatibility wrappers (defined in _imaging.c) */ -extern int PyImaging_CheckBuffer(PyObject* buffer); -extern int PyImaging_GetBuffer(PyObject* buffer, Py_buffer *view); - -/* -------------------------------------------------------------------- */ -/* Standard mapper */ - -typedef struct { - PyObject_HEAD - char* base; - int size; - int offset; -#ifdef _WIN32 - HANDLE hFile; - HANDLE hMap; -#endif -} ImagingMapperObject; - -static PyTypeObject ImagingMapperType; - -ImagingMapperObject* -PyImaging_MapperNew(const char* filename, int readonly) -{ - ImagingMapperObject *mapper; - - if (PyType_Ready(&ImagingMapperType) < 0) - return NULL; - - mapper = PyObject_New(ImagingMapperObject, &ImagingMapperType); - if (mapper == NULL) - return NULL; - - mapper->base = NULL; - mapper->size = mapper->offset = 0; - -#ifdef _WIN32 - mapper->hFile = (HANDLE)-1; - mapper->hMap = (HANDLE)-1; - - /* FIXME: currently supports readonly mappings only */ - mapper->hFile = CreateFile( - filename, - GENERIC_READ, - FILE_SHARE_READ, - NULL, OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, - NULL); - if (mapper->hFile == (HANDLE)-1) { - PyErr_SetString(PyExc_IOError, "cannot open file"); - Py_DECREF(mapper); - return NULL; - } - - mapper->hMap = CreateFileMapping( - mapper->hFile, NULL, - PAGE_READONLY, - 0, 0, NULL); - if (mapper->hMap == (HANDLE)-1) { - CloseHandle(mapper->hFile); - PyErr_SetString(PyExc_IOError, "cannot map file"); - Py_DECREF(mapper); - return NULL; - } - - mapper->base = (char*) MapViewOfFile( - mapper->hMap, - FILE_MAP_READ, - 0, 0, 0); - - mapper->size = GetFileSize(mapper->hFile, 0); -#endif - - return mapper; -} - -static void -mapping_dealloc(ImagingMapperObject* mapper) -{ -#ifdef _WIN32 - if (mapper->base != 0) - UnmapViewOfFile(mapper->base); - if (mapper->hMap != (HANDLE)-1) - CloseHandle(mapper->hMap); - if (mapper->hFile != (HANDLE)-1) - CloseHandle(mapper->hFile); - mapper->base = 0; - mapper->hMap = mapper->hFile = (HANDLE)-1; -#endif - PyObject_Del(mapper); -} - -/* -------------------------------------------------------------------- */ -/* standard file operations */ +extern int +PyImaging_CheckBuffer(PyObject *buffer); +extern int +PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view); -static PyObject* -mapping_read(ImagingMapperObject* mapper, PyObject* args) -{ - PyObject* buf; - - int size = -1; - if (!PyArg_ParseTuple(args, "|i", &size)) - return NULL; - - /* check size */ - if (size < 0 || mapper->offset + size > mapper->size) - size = mapper->size - mapper->offset; - if (size < 0) - size = 0; - - buf = PyBytes_FromStringAndSize(NULL, size); - if (!buf) - return NULL; - - if (size > 0) { - memcpy(PyBytes_AsString(buf), mapper->base + mapper->offset, size); - mapper->offset += size; - } - - return buf; -} - -static PyObject* -mapping_seek(ImagingMapperObject* mapper, PyObject* args) -{ - int offset; - int whence = 0; - if (!PyArg_ParseTuple(args, "i|i", &offset, &whence)) - return NULL; - - switch (whence) { - case 0: /* SEEK_SET */ - mapper->offset = offset; - break; - case 1: /* SEEK_CUR */ - mapper->offset += offset; - break; - case 2: /* SEEK_END */ - mapper->offset = mapper->size + offset; - break; - default: - /* FIXME: raise ValueError? */ - break; - } - - Py_INCREF(Py_None); - return Py_None; -} - -/* -------------------------------------------------------------------- */ -/* map entire image */ - -extern PyObject*PyImagingNew(Imaging im); - -static void -ImagingDestroyMap(Imaging im) -{ - return; /* nothing to do! */ -} - -static PyObject* -mapping_readimage(ImagingMapperObject* mapper, PyObject* args) -{ - int y, size; - Imaging im; - - char* mode; - int xsize; - int ysize; - int stride; - int orientation; - if (!PyArg_ParseTuple(args, "s(ii)ii", &mode, &xsize, &ysize, - &stride, &orientation)) - return NULL; - - if (stride <= 0) { - /* FIXME: maybe we should call ImagingNewPrologue instead */ - if (!strcmp(mode, "L") || !strcmp(mode, "P")) - stride = xsize; - else if (!strcmp(mode, "I;16") || !strcmp(mode, "I;16B")) - stride = xsize * 2; - else - stride = xsize * 4; - } - - size = ysize * stride; - - if (mapper->offset + size > mapper->size) { - PyErr_SetString(PyExc_IOError, "image file truncated"); - return NULL; - } - - im = ImagingNewPrologue(mode, xsize, ysize); - if (!im) - return NULL; - - /* setup file pointers */ - if (orientation > 0) - for (y = 0; y < ysize; y++) - im->image[y] = mapper->base + mapper->offset + y * stride; - else - for (y = 0; y < ysize; y++) - im->image[ysize-y-1] = mapper->base + mapper->offset + y * stride; - - im->destroy = ImagingDestroyMap; - - mapper->offset += size; - - return PyImagingNew(im); -} - -static struct PyMethodDef methods[] = { - /* standard file interface */ - {"read", (PyCFunction)mapping_read, 1}, - {"seek", (PyCFunction)mapping_seek, 1}, - /* extensions */ - {"readimage", (PyCFunction)mapping_readimage, 1}, - {NULL, NULL} /* sentinel */ -}; - -static PyTypeObject ImagingMapperType = { - PyVarObject_HEAD_INIT(NULL, 0) - "ImagingMapper", /*tp_name*/ - sizeof(ImagingMapperObject), /*tp_size*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)mapping_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ -}; - -PyObject* -PyImaging_Mapper(PyObject* self, PyObject* args) -{ - char* filename; - if (!PyArg_ParseTuple(args, "s", &filename)) - return NULL; - - return (PyObject*) PyImaging_MapperNew(filename, 1); -} +extern PyObject * +PyImagingNew(Imaging im); /* -------------------------------------------------------------------- */ /* Buffer mapper */ typedef struct ImagingBufferInstance { struct ImagingMemoryInstance im; - PyObject* target; + PyObject *target; Py_buffer view; } ImagingBufferInstance; static void -mapping_destroy_buffer(Imaging im) -{ - ImagingBufferInstance* buffer = (ImagingBufferInstance*) im; +mapping_destroy_buffer(Imaging im) { + ImagingBufferInstance *buffer = (ImagingBufferInstance *)im; PyBuffer_Release(&buffer->view); Py_XDECREF(buffer->target); } -PyObject* -PyImaging_MapBuffer(PyObject* self, PyObject* args) -{ +PyObject * +PyImaging_MapBuffer(PyObject *self, PyObject *args) { Py_ssize_t y, size; Imaging im; - PyObject* target; + PyObject *target; Py_buffer view; - char* mode; - char* codec; - PyObject* bbox; + char *mode; + char *codec; Py_ssize_t offset; int xsize, ysize; int stride; int ystep; - if (!PyArg_ParseTuple(args, "O(ii)sOn(sii)", &target, &xsize, &ysize, - &codec, &bbox, &offset, &mode, &stride, &ystep)) + if (!PyArg_ParseTuple( + args, + "O(ii)sn(sii)", + &target, + &xsize, + &ysize, + &codec, + &offset, + &mode, + &stride, + &ystep)) { return NULL; + } if (!PyImaging_CheckBuffer(target)) { PyErr_SetString(PyExc_TypeError, "expected string or buffer"); @@ -331,20 +82,21 @@ PyImaging_MapBuffer(PyObject* self, PyObject* args) } if (stride <= 0) { - if (!strcmp(mode, "L") || !strcmp(mode, "P")) + if (!strcmp(mode, "L") || !strcmp(mode, "P")) { stride = xsize; - else if (!strncmp(mode, "I;16", 4)) + } else if (!strncmp(mode, "I;16", 4)) { stride = xsize * 2; - else + } else { stride = xsize * 4; + } } - if (stride > 0 && ysize > INT_MAX / stride) { + if (stride > 0 && ysize > PY_SSIZE_T_MAX / stride) { PyErr_SetString(PyExc_MemoryError, "Integer overflow in ysize"); return NULL; } - size = (Py_ssize_t) ysize * stride; + size = (Py_ssize_t)ysize * stride; if (offset > PY_SSIZE_T_MAX - size) { PyErr_SetString(PyExc_MemoryError, "Integer overflow in offset"); @@ -352,37 +104,43 @@ PyImaging_MapBuffer(PyObject* self, PyObject* args) } /* check buffer size */ - if (PyImaging_GetBuffer(target, &view) < 0) + if (PyImaging_GetBuffer(target, &view) < 0) { return NULL; + } if (view.len < 0) { PyErr_SetString(PyExc_ValueError, "buffer has negative size"); + PyBuffer_Release(&view); return NULL; } if (offset + size > view.len) { PyErr_SetString(PyExc_ValueError, "buffer is not large enough"); + PyBuffer_Release(&view); return NULL; } - im = ImagingNewPrologueSubtype( - mode, xsize, ysize, sizeof(ImagingBufferInstance)); - if (!im) + im = ImagingNewPrologueSubtype(mode, xsize, ysize, sizeof(ImagingBufferInstance)); + if (!im) { + PyBuffer_Release(&view); return NULL; + } /* setup file pointers */ - if (ystep > 0) - for (y = 0; y < ysize; y++) - im->image[y] = (char*)view.buf + offset + y * stride; - else - for (y = 0; y < ysize; y++) - im->image[ysize-y-1] = (char*)view.buf + offset + y * stride; + if (ystep > 0) { + for (y = 0; y < ysize; y++) { + im->image[y] = (char *)view.buf + offset + y * stride; + } + } else { + for (y = 0; y < ysize; y++) { + im->image[ysize - y - 1] = (char *)view.buf + offset + y * stride; + } + } im->destroy = mapping_destroy_buffer; Py_INCREF(target); - ((ImagingBufferInstance*) im)->target = target; - ((ImagingBufferInstance*) im)->view = view; + ((ImagingBufferInstance *)im)->target = target; + ((ImagingBufferInstance *)im)->view = view; return PyImagingNew(im); } - diff --git a/src/outline.c b/src/outline.c index 25e63aeaf56..0a9a3646ef0 100644 --- a/src/outline.c +++ b/src/outline.c @@ -19,32 +19,31 @@ #include "Python.h" -#include "Imaging.h" - +#include "libImaging/Imaging.h" /* -------------------------------------------------------------------- */ -/* Class */ +/* Class */ typedef struct { - PyObject_HEAD - ImagingOutline outline; + PyObject_HEAD ImagingOutline outline; } OutlineObject; static PyTypeObject OutlineType; #define PyOutline_Check(op) (Py_TYPE(op) == &OutlineType) -static OutlineObject* -_outline_new(void) -{ +static OutlineObject * +_outline_new(void) { OutlineObject *self; - if (PyType_Ready(&OutlineType) < 0) + if (PyType_Ready(&OutlineType) < 0) { return NULL; + } self = PyObject_New(OutlineObject, &OutlineType); - if (self == NULL) - return NULL; + if (self == NULL) { + return NULL; + } self->outline = ImagingOutlineNew(); @@ -52,44 +51,41 @@ _outline_new(void) } static void -_outline_dealloc(OutlineObject* self) -{ +_outline_dealloc(OutlineObject *self) { ImagingOutlineDelete(self->outline); PyObject_Del(self); } ImagingOutline -PyOutline_AsOutline(PyObject* outline) -{ - if (PyOutline_Check(outline)) - return ((OutlineObject*) outline)->outline; +PyOutline_AsOutline(PyObject *outline) { + if (PyOutline_Check(outline)) { + return ((OutlineObject *)outline)->outline; + } return NULL; } - /* -------------------------------------------------------------------- */ -/* Factories */ +/* Factories */ -PyObject* -PyOutline_Create(PyObject* self, PyObject* args) -{ - if (!PyArg_ParseTuple(args, ":outline")) +PyObject * +PyOutline_Create(PyObject *self, PyObject *args) { + if (!PyArg_ParseTuple(args, ":outline")) { return NULL; + } - return (PyObject*) _outline_new(); + return (PyObject *)_outline_new(); } - /* -------------------------------------------------------------------- */ -/* Methods */ +/* Methods */ -static PyObject* -_outline_move(OutlineObject* self, PyObject* args) -{ +static PyObject * +_outline_move(OutlineObject *self, PyObject *args) { float x0, y0; - if (!PyArg_ParseTuple(args, "ff", &x0, &y0)) - return NULL; + if (!PyArg_ParseTuple(args, "ff", &x0, &y0)) { + return NULL; + } ImagingOutlineMove(self->outline, x0, y0); @@ -97,12 +93,12 @@ _outline_move(OutlineObject* self, PyObject* args) return Py_None; } -static PyObject* -_outline_line(OutlineObject* self, PyObject* args) -{ +static PyObject * +_outline_line(OutlineObject *self, PyObject *args) { float x1, y1; - if (!PyArg_ParseTuple(args, "ff", &x1, &y1)) - return NULL; + if (!PyArg_ParseTuple(args, "ff", &x1, &y1)) { + return NULL; + } ImagingOutlineLine(self->outline, x1, y1); @@ -110,12 +106,12 @@ _outline_line(OutlineObject* self, PyObject* args) return Py_None; } -static PyObject* -_outline_curve(OutlineObject* self, PyObject* args) -{ +static PyObject * +_outline_curve(OutlineObject *self, PyObject *args) { float x1, y1, x2, y2, x3, y3; - if (!PyArg_ParseTuple(args, "ffffff", &x1, &y1, &x2, &y2, &x3, &y3)) - return NULL; + if (!PyArg_ParseTuple(args, "ffffff", &x1, &y1, &x2, &y2, &x3, &y3)) { + return NULL; + } ImagingOutlineCurve(self->outline, x1, y1, x2, y2, x3, y3); @@ -123,11 +119,11 @@ _outline_curve(OutlineObject* self, PyObject* args) return Py_None; } -static PyObject* -_outline_close(OutlineObject* self, PyObject* args) -{ - if (!PyArg_ParseTuple(args, ":close")) +static PyObject * +_outline_close(OutlineObject *self, PyObject *args) { + if (!PyArg_ParseTuple(args, ":close")) { return NULL; + } ImagingOutlineClose(self->outline); @@ -135,12 +131,12 @@ _outline_close(OutlineObject* self, PyObject* args) return Py_None; } -static PyObject* -_outline_transform(OutlineObject* self, PyObject* args) -{ +static PyObject * +_outline_transform(OutlineObject *self, PyObject *args) { double a[6]; - if (!PyArg_ParseTuple(args, "(dddddd)", a+0, a+1, a+2, a+3, a+4, a+5)) + if (!PyArg_ParseTuple(args, "(dddddd)", a + 0, a + 1, a + 2, a + 3, a + 4, a + 5)) { return NULL; + } ImagingOutlineTransform(self->outline, a); @@ -149,44 +145,43 @@ _outline_transform(OutlineObject* self, PyObject* args) } static struct PyMethodDef _outline_methods[] = { - {"line", (PyCFunction)_outline_line, 1}, - {"curve", (PyCFunction)_outline_curve, 1}, - {"move", (PyCFunction)_outline_move, 1}, - {"close", (PyCFunction)_outline_close, 1}, - {"transform", (PyCFunction)_outline_transform, 1}, + {"line", (PyCFunction)_outline_line, METH_VARARGS}, + {"curve", (PyCFunction)_outline_curve, METH_VARARGS}, + {"move", (PyCFunction)_outline_move, METH_VARARGS}, + {"close", (PyCFunction)_outline_close, METH_VARARGS}, + {"transform", (PyCFunction)_outline_transform, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; static PyTypeObject OutlineType = { - PyVarObject_HEAD_INIT(NULL, 0) - "Outline", /*tp_name*/ - sizeof(OutlineObject), /*tp_size*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_outline_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _outline_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0) "Outline", /*tp_name*/ + sizeof(OutlineObject), /*tp_size*/ + 0, /*tp_itemsize*/ + /* methods */ + (destructor)_outline_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + _outline_methods, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ }; diff --git a/src/path.c b/src/path.c index b56ea838e79..3e3431575ec 100644 --- a/src/path.c +++ b/src/path.c @@ -25,69 +25,69 @@ * See the README file for information on usage and redistribution. */ - #include "Python.h" -#include "Imaging.h" +#include "libImaging/Imaging.h" #include -#include "py3.h" - /* compatibility wrappers (defined in _imaging.c) */ -extern int PyImaging_CheckBuffer(PyObject* buffer); -extern int PyImaging_GetBuffer(PyObject* buffer, Py_buffer *view); +extern int +PyImaging_CheckBuffer(PyObject *buffer); +extern int +PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view); /* -------------------------------------------------------------------- */ /* Class */ /* -------------------------------------------------------------------- */ typedef struct { - PyObject_HEAD - Py_ssize_t count; + PyObject_HEAD Py_ssize_t count; double *xy; int index; /* temporary use, e.g. in decimate */ } PyPathObject; static PyTypeObject PyPathType; -static double* -alloc_array(Py_ssize_t count) -{ - double* xy; +static double * +alloc_array(Py_ssize_t count) { + double *xy; if (count < 0) { - PyErr_NoMemory(); - return NULL; + return ImagingError_MemoryError(); } - if (count > (SIZE_MAX / (2 * sizeof(double))) - 1 ) { - PyErr_NoMemory(); - return NULL; + if ((unsigned long long)count > (SIZE_MAX / (2 * sizeof(double))) - 1) { + return ImagingError_MemoryError(); + } + xy = calloc(2 * count + 1, sizeof(double)); + if (!xy) { + ImagingError_MemoryError(); } - xy = malloc(2 * count * sizeof(double) + 1); - if (!xy) - PyErr_NoMemory(); return xy; } -static PyPathObject* -path_new(Py_ssize_t count, double* xy, int duplicate) -{ +static PyPathObject * +path_new(Py_ssize_t count, double *xy, int duplicate) { PyPathObject *path; if (duplicate) { /* duplicate path */ - double* p = alloc_array(count); - if (!p) + double *p = alloc_array(count); + if (!p) { return NULL; + } memcpy(p, xy, count * 2 * sizeof(double)); xy = p; } - if (PyType_Ready(&PyPathType) < 0) + if (PyType_Ready(&PyPathType) < 0) { + free(xy); return NULL; + } path = PyObject_New(PyPathObject, &PyPathType); - if (path == NULL) + if (path == NULL) { + free(xy); return NULL; + } path->count = count; path->xy = xy; @@ -96,8 +96,7 @@ path_new(Py_ssize_t count, double* xy, int duplicate) } static void -path_dealloc(PyPathObject* path) -{ +path_dealloc(PyPathObject *path) { free(path->xy); PyObject_Del(path); } @@ -109,17 +108,17 @@ path_dealloc(PyPathObject* path) #define PyPath_Check(op) (Py_TYPE(op) == &PyPathType) Py_ssize_t -PyPath_Flatten(PyObject* data, double **pxy) -{ +PyPath_Flatten(PyObject *data, double **pxy) { Py_ssize_t i, j, n; double *xy; if (PyPath_Check(data)) { /* This was another path object. */ - PyPathObject *path = (PyPathObject*) data; + PyPathObject *path = (PyPathObject *)data; xy = alloc_array(path->count); - if (!xy) + if (!xy) { return -1; + } memcpy(xy, path->xy, 2 * path->count * sizeof(double)); *pxy = xy; return path->count; @@ -129,13 +128,15 @@ PyPath_Flatten(PyObject* data, double **pxy) /* Assume the buffer contains floats */ Py_buffer buffer; if (PyImaging_GetBuffer(data, &buffer) == 0) { - int n = buffer.len / (2 * sizeof(float)); - float *ptr = (float*) buffer.buf; + float *ptr = (float *)buffer.buf; + n = buffer.len / (2 * sizeof(float)); xy = alloc_array(n); - if (!xy) + if (!xy) { return -1; - for (i = 0; i < n+n; i++) + } + for (i = 0; i < n + n; i++) { xy[i] = ptr[i]; + } *pxy = xy; PyBuffer_Release(&buffer); return n; @@ -151,50 +152,47 @@ PyPath_Flatten(PyObject* data, double **pxy) j = 0; n = PyObject_Length(data); /* Just in case __len__ breaks (or doesn't exist) */ - if (PyErr_Occurred()) + if (PyErr_Occurred()) { return -1; + } /* Allocate for worst case */ xy = alloc_array(n); - if (!xy) + if (!xy) { return -1; + } + +#define assign_item_to_array(op, decref) \ +if (PyFloat_Check(op)) { \ + xy[j++] = PyFloat_AS_DOUBLE(op); \ +} else if (PyLong_Check(op)) { \ + xy[j++] = (float)PyLong_AS_LONG(op); \ +} else if (PyNumber_Check(op)) { \ + xy[j++] = PyFloat_AsDouble(op); \ +} else if (PyArg_ParseTuple(op, "dd", &x, &y)) { \ + xy[j++] = x; \ + xy[j++] = y; \ +} else { \ + PyErr_SetString(PyExc_ValueError, "incorrect coordinate type"); \ + if (decref) { \ + Py_DECREF(op); \ + } \ + free(xy); \ + return -1; \ +} /* Copy table to path array */ if (PyList_Check(data)) { for (i = 0; i < n; i++) { double x, y; PyObject *op = PyList_GET_ITEM(data, i); - if (PyFloat_Check(op)) - xy[j++] = PyFloat_AS_DOUBLE(op); - else if (PyInt_Check(op)) - xy[j++] = (float) PyInt_AS_LONG(op); - else if (PyNumber_Check(op)) - xy[j++] = PyFloat_AsDouble(op); - else if (PyArg_ParseTuple(op, "dd", &x, &y)) { - xy[j++] = x; - xy[j++] = y; - } else { - free(xy); - return -1; - } + assign_item_to_array(op, 0); } } else if (PyTuple_Check(data)) { for (i = 0; i < n; i++) { double x, y; PyObject *op = PyTuple_GET_ITEM(data, i); - if (PyFloat_Check(op)) - xy[j++] = PyFloat_AS_DOUBLE(op); - else if (PyInt_Check(op)) - xy[j++] = (float) PyInt_AS_LONG(op); - else if (PyNumber_Check(op)) - xy[j++] = PyFloat_AsDouble(op); - else if (PyArg_ParseTuple(op, "dd", &x, &y)) { - xy[j++] = x; - xy[j++] = y; - } else { - free(xy); - return -1; - } + assign_item_to_array(op, 0); } } else { for (i = 0; i < n; i++) { @@ -202,8 +200,7 @@ PyPath_Flatten(PyObject* data, double **pxy) PyObject *op = PySequence_GetItem(data, i); if (!op) { /* treat IndexError as end of sequence */ - if (PyErr_Occurred() && - PyErr_ExceptionMatches(PyExc_IndexError)) { + if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_IndexError)) { PyErr_Clear(); break; } else { @@ -211,20 +208,7 @@ PyPath_Flatten(PyObject* data, double **pxy) return -1; } } - if (PyFloat_Check(op)) - xy[j++] = PyFloat_AS_DOUBLE(op); - else if (PyInt_Check(op)) - xy[j++] = (float) PyInt_AS_LONG(op); - else if (PyNumber_Check(op)) - xy[j++] = PyFloat_AsDouble(op); - else if (PyArg_ParseTuple(op, "dd", &x, &y)) { - xy[j++] = x; - xy[j++] = y; - } else { - Py_DECREF(op); - free(xy); - return -1; - } + assign_item_to_array(op, 1); Py_DECREF(op); } } @@ -236,51 +220,48 @@ PyPath_Flatten(PyObject* data, double **pxy) } *pxy = xy; - return j/2; + return j / 2; } - /* -------------------------------------------------------------------- */ /* Factories */ /* -------------------------------------------------------------------- */ -PyObject* -PyPath_Create(PyObject* self, PyObject* args) -{ - PyObject* data; +PyObject * +PyPath_Create(PyObject *self, PyObject *args) { + PyObject *data; Py_ssize_t count; double *xy; if (PyArg_ParseTuple(args, "n:Path", &count)) { - /* number of vertices */ xy = alloc_array(count); - if (!xy) + if (!xy) { return NULL; + } } else { - /* sequence or other path */ PyErr_Clear(); - if (!PyArg_ParseTuple(args, "O", &data)) + if (!PyArg_ParseTuple(args, "O", &data)) { return NULL; + } count = PyPath_Flatten(data, &xy); - if (count < 0) + if (count < 0) { return NULL; + } } - return (PyObject*) path_new(count, xy, 0); + return (PyObject *)path_new(count, xy, 0); } - /* -------------------------------------------------------------------- */ /* Methods */ /* -------------------------------------------------------------------- */ -static PyObject* -path_compact(PyPathObject* self, PyObject* args) -{ +static PyObject * +path_compact(PyPathObject *self, PyObject *args) { /* Simple-minded method to shorten path. A point is removed if the city block distance to the previous point is less than the given distance */ @@ -289,16 +270,18 @@ path_compact(PyPathObject* self, PyObject* args) double cityblock = 2.0; - if (!PyArg_ParseTuple(args, "|d:compact", &cityblock)) + if (!PyArg_ParseTuple(args, "|d:compact", &cityblock)) { return NULL; + } xy = self->xy; /* remove bogus vertices */ for (i = j = 1; i < self->count; i++) { - if (fabs(xy[j+j-2]-xy[i+i]) + fabs(xy[j+j-1]-xy[i+i+1]) >= cityblock) { - xy[j+j] = xy[i+i]; - xy[j+j+1] = xy[i+i+1]; + if (fabs(xy[j + j - 2] - xy[i + i]) + fabs(xy[j + j - 1] - xy[i + i + 1]) >= + cityblock) { + xy[j + j] = xy[i + i]; + xy[j + j + 1] = xy[i + i + 1]; j++; } } @@ -313,97 +296,107 @@ path_compact(PyPathObject* self, PyObject* args) return Py_BuildValue("i", i); /* number of removed vertices */ } -static PyObject* -path_getbbox(PyPathObject* self, PyObject* args) -{ +static PyObject * +path_getbbox(PyPathObject *self, PyObject *args) { /* Find bounding box */ Py_ssize_t i; double *xy; double x0, y0, x1, y1; - if (!PyArg_ParseTuple(args, ":getbbox")) + if (!PyArg_ParseTuple(args, ":getbbox")) { return NULL; + } xy = self->xy; - x0 = x1 = xy[0]; - y0 = y1 = xy[1]; + if (self->count == 0) { + x0 = x1 = 0; + y0 = y1 = 0; + } else { + x0 = x1 = xy[0]; + y0 = y1 = xy[1]; - for (i = 1; i < self->count; i++) { - if (xy[i+i] < x0) - x0 = xy[i+i]; - if (xy[i+i] > x1) - x1 = xy[i+i]; - if (xy[i+i+1] < y0) - y0 = xy[i+i+1]; - if (xy[i+i+1] > y1) - y1 = xy[i+i+1]; + for (i = 1; i < self->count; i++) { + if (xy[i + i] < x0) { + x0 = xy[i + i]; + } + if (xy[i + i] > x1) { + x1 = xy[i + i]; + } + if (xy[i + i + 1] < y0) { + y0 = xy[i + i + 1]; + } + if (xy[i + i + 1] > y1) { + y1 = xy[i + i + 1]; + } + } } return Py_BuildValue("dddd", x0, y0, x1, y1); } -static PyObject* -path_getitem(PyPathObject* self, Py_ssize_t i) -{ - if (i < 0) +static PyObject * +path_getitem(PyPathObject *self, Py_ssize_t i) { + if (i < 0) { i = self->count + i; + } if (i < 0 || i >= self->count) { PyErr_SetString(PyExc_IndexError, "path index out of range"); return NULL; } - return Py_BuildValue("dd", self->xy[i+i], self->xy[i+i+1]); + return Py_BuildValue("dd", self->xy[i + i], self->xy[i + i + 1]); } -static PyObject* -path_getslice(PyPathObject* self, Py_ssize_t ilow, Py_ssize_t ihigh) -{ +static PyObject * +path_getslice(PyPathObject *self, Py_ssize_t ilow, Py_ssize_t ihigh) { /* adjust arguments */ - if (ilow < 0) + if (ilow < 0) { ilow = 0; - else if (ilow >= self->count) + } else if (ilow >= self->count) { ilow = self->count; - if (ihigh < 0) + } + if (ihigh < 0) { ihigh = 0; - if (ihigh < ilow) + } + if (ihigh < ilow) { ihigh = ilow; - else if (ihigh > self->count) + } else if (ihigh > self->count) { ihigh = self->count; + } - return (PyObject*) path_new(ihigh - ilow, self->xy + ilow * 2, 1); + return (PyObject *)path_new(ihigh - ilow, self->xy + ilow * 2, 1); } static Py_ssize_t -path_len(PyPathObject* self) -{ +path_len(PyPathObject *self) { return self->count; } -static PyObject* -path_map(PyPathObject* self, PyObject* args) -{ +static PyObject * +path_map(PyPathObject *self, PyObject *args) { /* Map coordinate set through function */ Py_ssize_t i; double *xy; - PyObject* function; + PyObject *function; - if (!PyArg_ParseTuple(args, "O:map", &function)) + if (!PyArg_ParseTuple(args, "O:map", &function)) { return NULL; + } xy = self->xy; /* apply function to coordinate set */ for (i = 0; i < self->count; i++) { - double x = xy[i+i]; - double y = xy[i+i+1]; - PyObject* item = PyObject_CallFunction(function, "dd", x, y); + double x = xy[i + i]; + double y = xy[i + i + 1]; + PyObject *item = PyObject_CallFunction(function, "dd", x, y); if (!item || !PyArg_ParseTuple(item, "dd", &x, &y)) { Py_XDECREF(item); return NULL; } - xy[i+i] = x; - xy[i+i+1] = y; + xy[i + i] = x; + xy[i + i + 1] = y; Py_DECREF(item); } @@ -412,56 +405,56 @@ path_map(PyPathObject* self, PyObject* args) } static int -path_setitem(PyPathObject* self, Py_ssize_t i, PyObject* op) -{ - double* xy; +path_setitem(PyPathObject *self, Py_ssize_t i, PyObject *op) { + double *xy; if (i < 0 || i >= self->count) { - PyErr_SetString(PyExc_IndexError, - "path assignment index out of range"); + PyErr_SetString(PyExc_IndexError, "path assignment index out of range"); return -1; } if (op == NULL) { - PyErr_SetString(PyExc_TypeError, - "cannot delete from path"); + PyErr_SetString(PyExc_TypeError, "cannot delete from path"); return -1; } - xy = &self->xy[i+i]; + xy = &self->xy[i + i]; - if (!PyArg_ParseTuple(op, "dd", &xy[0], &xy[1])) + if (!PyArg_ParseTuple(op, "dd", &xy[0], &xy[1])) { return -1; + } return 0; } -static PyObject* -path_tolist(PyPathObject* self, PyObject* args) -{ +static PyObject * +path_tolist(PyPathObject *self, PyObject *args) { PyObject *list; Py_ssize_t i; int flat = 0; - if (!PyArg_ParseTuple(args, "|i:tolist", &flat)) + if (!PyArg_ParseTuple(args, "|i:tolist", &flat)) { return NULL; + } if (flat) { - list = PyList_New(self->count*2); - for (i = 0; i < self->count*2; i++) { - PyObject* item; + list = PyList_New(self->count * 2); + for (i = 0; i < self->count * 2; i++) { + PyObject *item; item = PyFloat_FromDouble(self->xy[i]); - if (!item) + if (!item) { goto error; + } PyList_SetItem(list, i, item); } } else { list = PyList_New(self->count); for (i = 0; i < self->count; i++) { - PyObject* item; - item = Py_BuildValue("dd", self->xy[i+i], self->xy[i+i+1]); - if (!item) + PyObject *item; + item = Py_BuildValue("dd", self->xy[i + i], self->xy[i + i + 1]); + if (!item) { goto error; + } PyList_SetItem(list, i, item); } } @@ -473,9 +466,8 @@ path_tolist(PyPathObject* self, PyObject* args) return NULL; } -static PyObject* -path_transform(PyPathObject* self, PyObject* args) -{ +static PyObject * +path_transform(PyPathObject *self, PyObject *args) { /* Apply affine transform to coordinate set */ Py_ssize_t i; double *xy; @@ -483,146 +475,135 @@ path_transform(PyPathObject* self, PyObject* args) double wrap = 0.0; - if (!PyArg_ParseTuple(args, "(dddddd)|d:transform", - &a, &b, &c, &d, &e, &f, - &wrap)) + if (!PyArg_ParseTuple( + args, "(dddddd)|d:transform", &a, &b, &c, &d, &e, &f, &wrap)) { return NULL; + } xy = self->xy; /* transform the coordinate set */ - if (b == 0.0 && d == 0.0) + if (b == 0.0 && d == 0.0) { /* scaling */ for (i = 0; i < self->count; i++) { - xy[i+i] = a*xy[i+i]+c; - xy[i+i+1] = e*xy[i+i+1]+f; + xy[i + i] = a * xy[i + i] + c; + xy[i + i + 1] = e * xy[i + i + 1] + f; } - else + } else { /* affine transform */ for (i = 0; i < self->count; i++) { - double x = xy[i+i]; - double y = xy[i+i+1]; - xy[i+i] = a*x+b*y+c; - xy[i+i+1] = d*x+e*y+f; + double x = xy[i + i]; + double y = xy[i + i + 1]; + xy[i + i] = a * x + b * y + c; + xy[i + i + 1] = d * x + e * y + f; } + } /* special treatment of geographical map data */ - if (wrap != 0.0) - for (i = 0; i < self->count; i++) - xy[i+i] = fmod(xy[i+i], wrap); + if (wrap != 0.0) { + for (i = 0; i < self->count; i++) { + xy[i + i] = fmod(xy[i + i], wrap); + } + } Py_INCREF(Py_None); return Py_None; } static struct PyMethodDef methods[] = { - {"getbbox", (PyCFunction)path_getbbox, 1}, - {"tolist", (PyCFunction)path_tolist, 1}, - {"compact", (PyCFunction)path_compact, 1}, - {"map", (PyCFunction)path_map, 1}, - {"transform", (PyCFunction)path_transform, 1}, + {"getbbox", (PyCFunction)path_getbbox, METH_VARARGS}, + {"tolist", (PyCFunction)path_tolist, METH_VARARGS}, + {"compact", (PyCFunction)path_compact, METH_VARARGS}, + {"map", (PyCFunction)path_map, METH_VARARGS}, + {"transform", (PyCFunction)path_transform, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; -static PyObject* -path_getattr_id(PyPathObject* self, void* closure) -{ - return Py_BuildValue("n", (Py_ssize_t) self->xy); +static PyObject * +path_getattr_id(PyPathObject *self, void *closure) { + return Py_BuildValue("n", (Py_ssize_t)self->xy); } -static struct PyGetSetDef getsetters[] = { - { "id", (getter) path_getattr_id }, - { NULL } -}; +static struct PyGetSetDef getsetters[] = {{"id", (getter)path_getattr_id}, {NULL}}; -static PyObject* -path_subscript(PyPathObject* self, PyObject* item) { +static PyObject * +path_subscript(PyPathObject *self, PyObject *item) { if (PyIndex_Check(item)) { Py_ssize_t i; i = PyNumber_AsSsize_t(item, PyExc_IndexError); - if (i == -1 && PyErr_Occurred()) + if (i == -1 && PyErr_Occurred()) { return NULL; + } return path_getitem(self, i); } if (PySlice_Check(item)) { int len = 4; Py_ssize_t start, stop, step, slicelength; -#if PY_VERSION_HEX >= 0x03020000 - if (PySlice_GetIndicesEx(item, len, &start, &stop, &step, &slicelength) < 0) + if (PySlice_GetIndicesEx(item, len, &start, &stop, &step, &slicelength) < 0) { return NULL; -#else - if (PySlice_GetIndicesEx((PySliceObject*)item, len, &start, &stop, &step, &slicelength) < 0) - return NULL; -#endif + } if (slicelength <= 0) { double *xy = alloc_array(0); - return (PyObject*) path_new(0, xy, 0); - } - else if (step == 1) { + return (PyObject *)path_new(0, xy, 0); + } else if (step == 1) { return path_getslice(self, start, stop); - } - else { + } else { PyErr_SetString(PyExc_TypeError, "slice steps not supported"); return NULL; } - } - else { - PyErr_Format(PyExc_TypeError, - "Path indices must be integers, not %.200s", - Py_TYPE(&item)->tp_name); + } else { + PyErr_Format( + PyExc_TypeError, + "Path indices must be integers, not %.200s", + Py_TYPE(item)->tp_name); return NULL; } } static PySequenceMethods path_as_sequence = { - (lenfunc)path_len, /*sq_length*/ - (binaryfunc)0, /*sq_concat*/ - (ssizeargfunc)0, /*sq_repeat*/ - (ssizeargfunc)path_getitem, /*sq_item*/ - (ssizessizeargfunc)path_getslice, /*sq_slice*/ - (ssizeobjargproc)path_setitem, /*sq_ass_item*/ - (ssizessizeobjargproc)0, /*sq_ass_slice*/ + (lenfunc)path_len, /*sq_length*/ + (binaryfunc)0, /*sq_concat*/ + (ssizeargfunc)0, /*sq_repeat*/ + (ssizeargfunc)path_getitem, /*sq_item*/ + (ssizessizeargfunc)path_getslice, /*sq_slice*/ + (ssizeobjargproc)path_setitem, /*sq_ass_item*/ + (ssizessizeobjargproc)0, /*sq_ass_slice*/ }; static PyMappingMethods path_as_mapping = { - (lenfunc)path_len, - (binaryfunc)path_subscript, - NULL -}; + (lenfunc)path_len, (binaryfunc)path_subscript, NULL}; static PyTypeObject PyPathType = { - PyVarObject_HEAD_INIT(NULL, 0) - "Path", /*tp_name*/ - sizeof(PyPathObject), /*tp_size*/ - 0, /*tp_itemsize*/ + PyVarObject_HEAD_INIT(NULL, 0) "Path", /*tp_name*/ + sizeof(PyPathObject), /*tp_size*/ + 0, /*tp_itemsize*/ /* methods */ - (destructor)path_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - &path_as_sequence, /*tp_as_sequence */ - &path_as_mapping, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - getsetters, /*tp_getset*/ + (destructor)path_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + &path_as_sequence, /*tp_as_sequence */ + &path_as_mapping, /*tp_as_mapping */ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + methods, /*tp_methods*/ + 0, /*tp_members*/ + getsetters, /*tp_getset*/ }; - diff --git a/src/py3.h b/src/py3.h deleted file mode 100644 index 310583845f7..00000000000 --- a/src/py3.h +++ /dev/null @@ -1,56 +0,0 @@ -/* - Python3 definition file to consistently map the code to Python 2 or - Python 3. - - PyInt and PyLong were merged into PyLong in Python 3, so all PyInt functions - are mapped to PyLong. - - PyString, on the other hand, was split into PyBytes and PyUnicode. We map - both back onto PyString, so use PyBytes or PyUnicode where appropriate. The - only exception to this is _imagingft.c, where PyUnicode is left alone. -*/ - -#if PY_VERSION_HEX >= 0x03000000 -#define PY_ARG_BYTES_LENGTH "y#" - -/* Map PyInt -> PyLong */ -#define PyInt_AsLong PyLong_AsLong -#define PyInt_Check PyLong_Check -#define PyInt_FromLong PyLong_FromLong -#define PyInt_AS_LONG PyLong_AS_LONG -#define PyInt_FromSsize_t PyLong_FromSsize_t -#define PyInt_AsSsize_t PyLong_AsSsize_t - -#else /* PY_VERSION_HEX < 0x03000000 */ -#define PY_ARG_BYTES_LENGTH "s#" - -#if !defined(KEEP_PY_UNICODE) -/* Map PyUnicode -> PyString */ -#undef PyUnicode_AsString -#undef PyUnicode_AS_STRING -#undef PyUnicode_Check -#undef PyUnicode_FromStringAndSize -#undef PyUnicode_FromString -#undef PyUnicode_FromFormat -#undef PyUnicode_DecodeFSDefault - -#define PyUnicode_AsString PyString_AsString -#define PyUnicode_AS_STRING PyString_AS_STRING -#define PyUnicode_Check PyString_Check -#define PyUnicode_FromStringAndSize PyString_FromStringAndSize -#define PyUnicode_FromString PyString_FromString -#define PyUnicode_FromFormat PyString_FromFormat -#define PyUnicode_DecodeFSDefault PyString_FromString -#endif - -/* Map PyBytes -> PyString */ -#define PyBytesObject PyStringObject -#define PyBytes_AsString PyString_AsString -#define PyBytes_AS_STRING PyString_AS_STRING -#define PyBytes_Check PyString_Check -#define PyBytes_AsStringAndSize PyString_AsStringAndSize -#define PyBytes_FromStringAndSize PyString_FromStringAndSize -#define PyBytes_FromString PyString_FromString -#define _PyBytes_Resize _PyString_Resize - -#endif /* PY_VERSION_HEX < 0x03000000 */ diff --git a/src/thirdparty/fribidi-shim/fribidi.c b/src/thirdparty/fribidi-shim/fribidi.c new file mode 100644 index 00000000000..5663da86b92 --- /dev/null +++ b/src/thirdparty/fribidi-shim/fribidi.c @@ -0,0 +1,108 @@ + +#ifndef _WIN32 +#include +#else +#define WIN32_LEAN_AND_MEAN +#include +#endif + +#define FRIBIDI_SHIM_IMPLEMENTATION +#include "fribidi.h" + + +/* FriBiDi>=1.0.0 adds bracket_types param, ignore and call legacy function */ +static FriBidiLevel fribidi_get_par_embedding_levels_ex_compat( + const FriBidiCharType *bidi_types, + const FriBidiBracketType *bracket_types, + const FriBidiStrIndex len, + FriBidiParType *pbase_dir, + FriBidiLevel *embedding_levels) +{ + return fribidi_get_par_embedding_levels( + bidi_types, len, pbase_dir, embedding_levels); +} + +/* FriBiDi>=1.0.0 gets bracket types here, ignore */ +static void fribidi_get_bracket_types_compat( + const FriBidiChar *str, + const FriBidiStrIndex len, + const FriBidiCharType *types, + FriBidiBracketType *btypes) +{ /* no-op*/ } + + +int load_fribidi(void) { + int error = 0; + const char **p_fribidi_version_info = 0; + + p_fribidi = 0; + + /* Microsoft needs a totally different system */ +#ifndef _WIN32 +#define LOAD_FUNCTION(func) \ + func = (t_##func)dlsym(p_fribidi, #func); \ + error = error || (func == 0); + + p_fribidi = dlopen("libfribidi.so", RTLD_LAZY); + if (!p_fribidi) { + p_fribidi = dlopen("libfribidi.so.0", RTLD_LAZY); + } + if (!p_fribidi) { + p_fribidi = dlopen("libfribidi.dylib", RTLD_LAZY); + } + if (!p_fribidi) { + p_fribidi = dlopen("/usr/local/lib/libfribidi.dylib", RTLD_LAZY); + } +#else +#define LOAD_FUNCTION(func) \ + func = (t_##func)GetProcAddress(p_fribidi, #func); \ + error = error || (func == 0); + + p_fribidi = LoadLibrary("fribidi"); + if (!p_fribidi) { + p_fribidi = LoadLibrary("fribidi-0"); + } + /* MSYS2 */ + if (!p_fribidi) { + p_fribidi = LoadLibrary("libfribidi-0"); + } +#endif + + if (!p_fribidi) { + return 1; + } + + /* load FriBiDi>=1.0.0 functions first, use error to detect version */ + LOAD_FUNCTION(fribidi_get_par_embedding_levels_ex); + LOAD_FUNCTION(fribidi_get_bracket_types); + if (error) { + /* using FriBiDi<1.0.0, ignore new parameters */ + error = 0; + fribidi_get_par_embedding_levels_ex = &fribidi_get_par_embedding_levels_ex_compat; + fribidi_get_bracket_types = &fribidi_get_bracket_types_compat; + } + + LOAD_FUNCTION(fribidi_unicode_to_charset); + LOAD_FUNCTION(fribidi_charset_to_unicode); + LOAD_FUNCTION(fribidi_get_bidi_types); + LOAD_FUNCTION(fribidi_get_par_embedding_levels); + +#ifndef _WIN32 + p_fribidi_version_info = (const char**)dlsym(p_fribidi, "fribidi_version_info"); + if (error || (p_fribidi_version_info == 0) || (*p_fribidi_version_info == 0)) { + dlclose(p_fribidi); + p_fribidi = 0; + return 2; + } +#else + p_fribidi_version_info = (const char**)GetProcAddress(p_fribidi, "fribidi_version_info"); + if (error || (p_fribidi_version_info == 0) || (*p_fribidi_version_info == 0)) { + FreeLibrary(p_fribidi); + p_fribidi = 0; + return 2; + } +#endif + fribidi_version_info = *p_fribidi_version_info; + + return 0; +} diff --git a/src/thirdparty/fribidi-shim/fribidi.h b/src/thirdparty/fribidi-shim/fribidi.h new file mode 100644 index 00000000000..7e175c3db80 --- /dev/null +++ b/src/thirdparty/fribidi-shim/fribidi.h @@ -0,0 +1,115 @@ + +#define FRIBIDI_MAJOR_VERSION 1 + +/* fribidi-types.h */ + +# if defined (_SVR4) || defined (SVR4) || defined (__OpenBSD__) || \ + defined (_sgi) || defined (__sun) || defined (sun) || \ + defined (__digital__) || defined (__HP_cc) +# include +# elif defined (_AIX) +# include +# else +# include +# endif + +typedef uint32_t FriBidiChar; +typedef int FriBidiStrIndex; + +typedef FriBidiChar FriBidiBracketType; + + + +/* fribidi-char-sets.h */ + +typedef enum +{ + _FRIBIDI_CHAR_SET_NOT_FOUND, + FRIBIDI_CHAR_SET_UTF8, + FRIBIDI_CHAR_SET_CAP_RTL, + FRIBIDI_CHAR_SET_ISO8859_6, + FRIBIDI_CHAR_SET_ISO8859_8, + FRIBIDI_CHAR_SET_CP1255, + FRIBIDI_CHAR_SET_CP1256, + _FRIBIDI_CHAR_SETS_NUM_PLUS_ONE +} +FriBidiCharSet; + + + +/* fribidi-bidi-types.h */ + +typedef signed char FriBidiLevel; + +#define FRIBIDI_TYPE_LTR_VAL 0x00000110L +#define FRIBIDI_TYPE_RTL_VAL 0x00000111L +#define FRIBIDI_TYPE_ON_VAL 0x00000040L + +typedef uint32_t FriBidiCharType; +#define FRIBIDI_TYPE_LTR FRIBIDI_TYPE_LTR_VAL + +typedef uint32_t FriBidiParType; +#define FRIBIDI_PAR_LTR FRIBIDI_TYPE_LTR_VAL +#define FRIBIDI_PAR_RTL FRIBIDI_TYPE_RTL_VAL +#define FRIBIDI_PAR_ON FRIBIDI_TYPE_ON_VAL + +#define FRIBIDI_LEVEL_IS_RTL(lev) ((lev) & 1) +#define FRIBIDI_DIR_TO_LEVEL(dir) ((FriBidiLevel) (FRIBIDI_IS_RTL(dir) ? 1 : 0)) +#define FRIBIDI_IS_RTL(p) ((p) & 0x00000001L) +#define FRIBIDI_IS_EXPLICIT_OR_BN_OR_WS(p) ((p) & 0x00901000L) + + + +/* functions */ + +#ifdef FRIBIDI_SHIM_IMPLEMENTATION +#ifdef _MSC_VER +#define FRIBIDI_ENTRY +#else +#define FRIBIDI_ENTRY __attribute__((visibility ("hidden"))) +#endif +#else +#define FRIBIDI_ENTRY extern +#endif + +#define FRIBIDI_FUNC(ret, name, ...) \ + typedef ret (*t_##name) (__VA_ARGS__); \ + FRIBIDI_ENTRY t_##name name; + +FRIBIDI_FUNC(FriBidiStrIndex, fribidi_unicode_to_charset, + FriBidiCharSet, const FriBidiChar *, FriBidiStrIndex, char *); + +FRIBIDI_FUNC(FriBidiStrIndex, fribidi_charset_to_unicode, + FriBidiCharSet, const char *, FriBidiStrIndex, FriBidiChar *); + +FRIBIDI_FUNC(void, fribidi_get_bidi_types, + const FriBidiChar *, const FriBidiStrIndex, FriBidiCharType *); + +FRIBIDI_FUNC(FriBidiLevel, fribidi_get_par_embedding_levels, + const FriBidiCharType *, const FriBidiStrIndex, FriBidiParType *, + FriBidiLevel *); + +/* FriBiDi>=1.0.0 */ +FRIBIDI_FUNC(FriBidiLevel, fribidi_get_par_embedding_levels_ex, + const FriBidiCharType *, const FriBidiBracketType *, const FriBidiStrIndex, + FriBidiParType *, FriBidiLevel *); + +/* FriBiDi>=1.0.0 */ +FRIBIDI_FUNC(void, fribidi_get_bracket_types, + const FriBidiChar *, const FriBidiStrIndex, const FriBidiCharType *, + FriBidiBracketType *); + +#undef FRIBIDI_FUNC + +/* constant, not a function */ +FRIBIDI_ENTRY const char *fribidi_version_info; + + + +/* shim */ + +FRIBIDI_ENTRY void *p_fribidi; + +FRIBIDI_ENTRY int load_fribidi(void); + +#undef FRIBIDI_ENTRY diff --git a/src/thirdparty/raqm/AUTHORS b/src/thirdparty/raqm/AUTHORS new file mode 100644 index 00000000000..bd5c3ac6b6a --- /dev/null +++ b/src/thirdparty/raqm/AUTHORS @@ -0,0 +1,9 @@ +Abderraouf Adjal +Ali Yousuf +Anood Almuharbi +Asma Albahanta +Fahad Alsaidi +Ibtisam Almabsali +Khaled Hosny +Mazoon Almaamari +Shamsa Alqassabi diff --git a/src/thirdparty/raqm/COPYING b/src/thirdparty/raqm/COPYING new file mode 100644 index 00000000000..c605a5dc67a --- /dev/null +++ b/src/thirdparty/raqm/COPYING @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright © 2015 Information Technology Authority (ITA) +Copyright © 2016-2022 Khaled Hosny + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/thirdparty/raqm/NEWS b/src/thirdparty/raqm/NEWS new file mode 100644 index 00000000000..ae1128485f2 --- /dev/null +++ b/src/thirdparty/raqm/NEWS @@ -0,0 +1,117 @@ +Overview of changes leading to 0.8.0 +Monday, December 13, 2021 +==================================== + +Remove autotools build. + +Support using SheenBiDi instead of FriBiDi for Unicode BiDi support. + +Fix running tests with Python <= 3.6. + +New API: + * raqm_get_par_resolved_direction + * raqm_get_direction_at_index + +Overview of changes leading to 0.7.2 +Monday, September 27, 2021 +==================================== + +Fix test failure with newer HarfBuzz versions. + +Apply FT_Face transformation matrix when built against FreeType 2.11 or later. + +Add meson build system. Autotools build system will be dropped in next release. + +Improve MSVC support. + +Build and documentation fixes. + +Overview of changes leading to 0.7.1 +Sunday, November 22, 2020 +==================================== + +Require HarfBuzz >= 2.0.0 + +Build and documentation fixes. + +Overview of changes leading to 0.7.0 +Monday, May 27, 2019 +==================================== + +New API: + * raqm_version + * raqm_version_string + * raqm_version_atleast + * RAQM_VERSION_MAJOR + * RAQM_VERSION_MICRO + * RAQM_VERSION_MINOR + * RAQM_VERSION_STRING + * RAQM_VERSION_ATLEAST + +Overview of changes leading to 0.6.0 +Sunday, May 5, 2019 +==================================== + +Fix TTB direction regression from the previous release. + +Correctly detect script of Common and Inherite characters at start of text. + +Undef HAVE_CONFIG_H workaround, for older versions of Fribidi. + +Drop test suite dependency on GLib. + +Port test runner to Python instead of shell script. + +New API: +* raqm_set_invisible_glyph() + +Overview of changes leading to 0.5.0 +Saturday, February 24, 2018 +==================================== + +Use FriBiDi 1.x API when available. + +Overview of changes leading to 0.4.0 +Sunday, January 21, 2018 +==================================== + +Set begin-of-text and end-of-text HarfBuzz buffer flags. + +Dynamically allocate memory instead of using stack allocation for input text. + +Accept zero length text and do nothing instead of treating it as error. + +Overview of changes leading to 0.3.0 +Monday, August 21, 2017 +==================================== + +Fix stack corruption on MSVC. + +New API: +* raqm_set_freetype_load_flags + +Overview of changes leading to 0.2.0 +Wednesday, August 25, 2016 +==================================== + +Fix building with MSVC due to lacking C99 support. + +Make multiple fonts support actually work. Start and length now respect the +input encoding. + +New API: +* raqm_index_to_position +* raqm_position_to_index +* raqm_set_language + +Overview of changes leading to 0.1.1 +Sunday, May 1, 2016 +==================================== + +Fix make check on 32-bit systems. + +Overview of changes leading to 0.1.0 +Wednesday, January 20, 2016 +==================================== + +First release. diff --git a/src/thirdparty/raqm/README.md b/src/thirdparty/raqm/README.md new file mode 100644 index 00000000000..3354a4d2550 --- /dev/null +++ b/src/thirdparty/raqm/README.md @@ -0,0 +1,85 @@ +Raqm +==== + +[![Build](https://github.com/HOST-Oman/libraqm/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/HOST-Oman/libraqm/actions) + +Raqm is a small library that encapsulates the logic for complex text layout and +provides a convenient API. + +It currently provides bidirectional text support (using [FriBiDi][1] or +[SheenBidi][2]), shaping (using [HarfBuzz][3]), and proper script itemization. +As a result, Raqm can support most writing systems covered by Unicode. + +The documentation can be accessed on the web at: +> http://host-oman.github.io/libraqm/ + +Raqm (Arabic: رَقْم) is writing, also number or digit and the Arabic word for +digital (رَقَمِيّ) shares the same root, so it is a play on “digital writing”. + +Building +-------- + +Raqm depends on the following libraries: +* [FreeType][4] +* [HarfBuzz][3] +* [FriBiDi][1] or [SheenBidi][2] + +To build the documentation you will also need: +* [GTK-Doc][5] + +To install dependencies on Fedora: + + sudo dnf install freetype-devel harfbuzz-devel fribidi-devel meson gtk-doc + +To install dependencies on Ubuntu: + + sudo apt-get install libfreetype6-dev libharfbuzz-dev libfribidi-dev meson gtk-doc-tools + +On Mac OS X you can use Homebrew: + + brew install freetype harfbuzz fribidi meson gtk-doc + export XML_CATALOG_FILES="/usr/local/etc/xml/catalog" # for the docs + +Once you have the source code and the dependencies, you can proceed to build. +To do that, run the customary sequence of commands in the source code +directory: + + $ meson build + $ ninja -C build + $ ninja -C build install + +To build the documentation, pass `-Ddocs=true` to the `meson`. + +To run the tests: + + $ ninja -C build test + +Contributing +------------ + +Once you have made a change that you are happy with, contribute it back, we’ll +be happy to integrate it! Just fork the repository and make a pull request. + +Projects using Raqm +------------------- + +1. [ImageMagick](https://github.com/ImageMagick/ImageMagick) +2. [LibGD](https://github.com/libgd/libgd) +3. [FontView](https://github.com/googlei18n/fontview) +4. [Pillow](https://github.com/python-pillow) +5. [mplcairo](https://github.com/anntzer/mplcairo) +6. [CEGUI](https://github.com/cegui/cegui) + +The following projects have patches to support complex text layout using Raqm: + +2. SDL_ttf: https://bugzilla.libsdl.org/show_bug.cgi?id=3211 +3. Pygame: https://bitbucket.org/pygame/pygame/pull-requests/52 +4. Blender: https://developer.blender.org/D1809 + + + +[1]: https://github.com/fribidi/fribidi +[2]: https://github.com/Tehreer/SheenBidi +[3]: https://github.com/harfbuzz/harfbuzz +[4]: https://freetype.org/ +[5]: https://www.gtk.org/gtk-doc diff --git a/src/thirdparty/raqm/raqm-version.h b/src/thirdparty/raqm/raqm-version.h new file mode 100644 index 00000000000..78b70a5615e --- /dev/null +++ b/src/thirdparty/raqm/raqm-version.h @@ -0,0 +1,44 @@ +/* + * Copyright © 2011 Google, Inc. + * + * This is part of HarfBuzz, a text shaping library. + * + * Permission is hereby granted, without written agreement and without + * license or royalty fees, to use, copy, modify, and distribute this + * software and its documentation for any purpose, provided that the + * above copyright notice and the following two paragraphs appear in + * all copies of this software. + * + * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES + * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN + * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH + * DAMAGE. + * + * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS + * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + * Google Author(s): Behdad Esfahbod + */ + +#ifndef _RAQM_H_IN_ +#error "Include instead." +#endif + +#ifndef _RAQM_VERSION_H_ +#define _RAQM_VERSION_H_ + +#define RAQM_VERSION_MAJOR 0 +#define RAQM_VERSION_MINOR 9 +#define RAQM_VERSION_MICRO 0 + +#define RAQM_VERSION_STRING "0.9.0" + +#define RAQM_VERSION_ATLEAST(major,minor,micro) \ + ((major)*10000+(minor)*100+(micro) <= \ + RAQM_VERSION_MAJOR*10000+RAQM_VERSION_MINOR*100+RAQM_VERSION_MICRO) + +#endif /* _RAQM_VERSION_H_ */ diff --git a/src/thirdparty/raqm/raqm.c b/src/thirdparty/raqm/raqm.c new file mode 100644 index 00000000000..13f6e1f023c --- /dev/null +++ b/src/thirdparty/raqm/raqm.c @@ -0,0 +1,2432 @@ +/* + * Copyright © 2015 Information Technology Authority (ITA) + * Copyright © 2016-2022 Khaled Hosny + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#ifdef RAQM_SHEENBIDI +#include +#else +#ifdef HAVE_FRIBIDI_SYSTEM +#include +#else +#include "../fribidi-shim/fribidi.h" +#endif +#endif + +#include +#include + +#include "raqm.h" + +/** + * SECTION:raqm + * @title: Raqm + * @short_description: A library for complex text layout + * @include: raqm.h + * + * Raqm is a light weight text layout library with strong emphasis on + * supporting languages and writing systems that require complex text layout. + * + * The main object in Raqm API is #raqm_t, it stores all the states of the + * input text, its properties, and the output of the layout process. + * + * To start, you create a #raqm_t object, add text and font(s) to it, run the + * layout process, and finally query about the output. For example: + * + * |[ + * #include "raqm.h" + * + * int + * main (int argc, char *argv[]) + * { + * const char *fontfile; + * const char *text; + * const char *direction; + * const char *language; + * int ret = 1; + * + * FT_Library library = NULL; + * FT_Face face = NULL; + * + * if (argc < 5) + * { + * printf ("Usage: %s FONT_FILE TEXT DIRECTION LANG\n", argv[0]); + * return 1; + * } + * + * fontfile = argv[1]; + * text = argv[2]; + * direction = argv[3]; + * language = argv[4]; + * + * if (FT_Init_FreeType (&library) == 0) + * { + * if (FT_New_Face (library, fontfile, 0, &face) == 0) + * { + * if (FT_Set_Char_Size (face, face->units_per_EM, 0, 0, 0) == 0) + * { + * raqm_t *rq = raqm_create (); + * if (rq != NULL) + * { + * raqm_direction_t dir = RAQM_DIRECTION_DEFAULT; + * + * if (strcmp (direction, "r") == 0) + * dir = RAQM_DIRECTION_RTL; + * else if (strcmp (direction, "l") == 0) + * dir = RAQM_DIRECTION_LTR; + * + * if (raqm_set_text_utf8 (rq, text, strlen (text)) && + * raqm_set_freetype_face (rq, face) && + * raqm_set_par_direction (rq, dir) && + * raqm_set_language (rq, language, 0, strlen (text)) && + * raqm_layout (rq)) + * { + * size_t count, i; + * raqm_glyph_t *glyphs = raqm_get_glyphs (rq, &count); + * + * ret = !(glyphs != NULL || count == 0); + * + * printf("glyph count: %zu\n", count); + * for (i = 0; i < count; i++) + * { + * printf ("gid#%d off: (%d, %d) adv: (%d, %d) idx: %d\n", + * glyphs[i].index, + * glyphs[i].x_offset, + * glyphs[i].y_offset, + * glyphs[i].x_advance, + * glyphs[i].y_advance, + * glyphs[i].cluster); + * } + * } + * + * raqm_destroy (rq); + * } + * } + * + * FT_Done_Face (face); + * } + * + * FT_Done_FreeType (library); + * } + * + * return ret; + * } + * ]| + * To compile this example: + * |[ + * cc -o test test.c `pkg-config --libs --cflags raqm` + * ]| + */ + +/* For enabling debug mode */ +/*#define RAQM_DEBUG 1*/ +#ifdef RAQM_DEBUG +#define RAQM_DBG(...) fprintf (stderr, __VA_ARGS__) +#else +#define RAQM_DBG(...) +#endif + +#ifdef RAQM_TESTING +# define RAQM_TEST(...) printf (__VA_ARGS__) +# define SCRIPT_TO_STRING(script) \ + char buff[5]; \ + hb_tag_to_string (hb_script_to_iso15924_tag (script), buff); \ + buff[4] = '\0'; +#else +# define RAQM_TEST(...) +#endif + +#define RAQM_BIDI_LEVEL_IS_RTL(level) \ + ((level) & 1) + +#ifdef RAQM_SHEENBIDI + typedef SBLevel _raqm_bidi_level_t; +#else + typedef FriBidiLevel _raqm_bidi_level_t; +#endif + +typedef struct { + FT_Face ftface; + int ftloadflags; + hb_language_t lang; + hb_script_t script; +} _raqm_text_info; + +typedef struct _raqm_run raqm_run_t; + +struct _raqm { + int ref_count; + + uint32_t *text; + char *text_utf8; + size_t text_len; + size_t text_capacity_bytes; + + _raqm_text_info *text_info; + + raqm_direction_t base_dir; + raqm_direction_t resolved_dir; + + hb_feature_t *features; + size_t features_len; + + raqm_run_t *runs; + raqm_run_t *runs_pool; + + raqm_glyph_t *glyphs; + size_t glyphs_capacity; + + int invisible_glyph; +}; + +struct _raqm_run { + uint32_t pos; + uint32_t len; + + hb_direction_t direction; + hb_script_t script; + hb_font_t *font; + hb_buffer_t *buffer; + + raqm_run_t *next; +}; + +static uint32_t +_raqm_u8_to_u32_index (raqm_t *rq, + uint32_t index); + +static void +_raqm_init_text_info (raqm_t *rq) +{ + hb_language_t default_lang = hb_language_get_default (); + for (size_t i = 0; i < rq->text_len; i++) + { + rq->text_info[i].ftface = NULL; + rq->text_info[i].ftloadflags = -1; + rq->text_info[i].lang = default_lang; + rq->text_info[i].script = HB_SCRIPT_INVALID; + } +} + +static void +_raqm_release_text_info (raqm_t *rq) +{ + if (!rq->text_info) + return; + + for (size_t i = 0; i < rq->text_len; i++) + { + if (rq->text_info[i].ftface) + FT_Done_Face (rq->text_info[i].ftface); + } +} + +static bool +_raqm_compare_text_info (_raqm_text_info a, + _raqm_text_info b) +{ + if (a.ftface != b.ftface) + return false; + + if (a.ftloadflags != b.ftloadflags) + return false; + + if (a.lang != b.lang) + return false; + + if (a.script != b.script) + return false; + + return true; +} + +static void +_raqm_free_text(raqm_t* rq) +{ + free (rq->text); + rq->text = NULL; + rq->text_info = NULL; + rq->text_utf8 = NULL; + rq->text_len = 0; + rq->text_capacity_bytes = 0; +} + +static bool +_raqm_alloc_text(raqm_t *rq, + size_t len, + bool need_utf8) +{ + /* Allocate contiguous memory block for texts and text_info */ + size_t mem_size = (sizeof (uint32_t) + sizeof (_raqm_text_info)) * len; + if (need_utf8) + mem_size += sizeof (char) * len; + + if (mem_size > rq->text_capacity_bytes) + { + void* new_mem = realloc (rq->text, mem_size); + if (!new_mem) + { + _raqm_free_text (rq); + return false; + } + + rq->text_capacity_bytes = mem_size; + rq->text = new_mem; + } + + rq->text_info = (_raqm_text_info*)(rq->text + len); + rq->text_utf8 = need_utf8 ? (char*)(rq->text_info + len) : NULL; + + return true; +} + +static raqm_run_t* +_raqm_alloc_run (raqm_t *rq) +{ + raqm_run_t *run = rq->runs_pool; + if (run) + { + rq->runs_pool = run->next; + } + else + { + run = malloc (sizeof (raqm_run_t)); + run->font = NULL; + run->buffer = NULL; + } + + run->pos = 0; + run->len = 0; + run->direction = HB_DIRECTION_INVALID; + run->script = HB_SCRIPT_INVALID; + run->next = NULL; + + return run; +} + +static void +_raqm_free_runs (raqm_run_t *runs) +{ + while (runs) + { + raqm_run_t *run = runs; + runs = runs->next; + + if (run->buffer) + hb_buffer_destroy (run->buffer); + + if (run->font) + hb_font_destroy (run->font); + + free (run); + } +} + +/** + * raqm_create: + * + * Creates a new #raqm_t with all its internal states initialized to their + * defaults. + * + * Return value: + * A newly allocated #raqm_t with a reference count of 1. The initial reference + * count should be released with raqm_destroy() when you are done using the + * #raqm_t. Returns %NULL in case of error. + * + * Since: 0.1 + */ +raqm_t * +raqm_create (void) +{ + raqm_t *rq; + + rq = malloc (sizeof (raqm_t)); + if (!rq) + return NULL; + + rq->ref_count = 1; + + rq->base_dir = RAQM_DIRECTION_DEFAULT; + rq->resolved_dir = RAQM_DIRECTION_DEFAULT; + + rq->features = NULL; + rq->features_len = 0; + + rq->invisible_glyph = 0; + + rq->text = NULL; + rq->text_utf8 = NULL; + rq->text_info = NULL; + rq->text_capacity_bytes = 0; + rq->text_len = 0; + + rq->runs = NULL; + rq->runs_pool = NULL; + + rq->glyphs = NULL; + rq->glyphs_capacity = 0; + + return rq; +} + +/** + * raqm_reference: + * @rq: a #raqm_t. + * + * Increases the reference count on @rq by one. This prevents @rq from being + * destroyed until a matching call to raqm_destroy() is made. + * + * Return value: + * The referenced #raqm_t. + * + * Since: 0.1 + */ +raqm_t * +raqm_reference (raqm_t *rq) +{ + if (rq) + rq->ref_count++; + + return rq; +} + +/** + * raqm_destroy: + * @rq: a #raqm_t. + * + * Decreases the reference count on @rq by one. If the result is zero, then @rq + * and all associated resources are freed. + * See raqm_reference(). + * + * Since: 0.1 + */ +void +raqm_destroy (raqm_t *rq) +{ + if (!rq || --rq->ref_count != 0) + return; + + _raqm_release_text_info (rq); + _raqm_free_text (rq); + _raqm_free_runs (rq->runs); + _raqm_free_runs (rq->runs_pool); + free (rq->glyphs); + free (rq->features); + free (rq); +} + +/** + * raqm_clear_contents: + * @rq: a #raqm_t. + * + * Clears internal state of previously used raqm_t object, making it ready + * for reuse and keeping some of allocated memory to increase performance. + * + * Since: 0.9 + */ +void +raqm_clear_contents (raqm_t *rq) +{ + if (!rq) + return; + + _raqm_release_text_info (rq); + + /* Return allocated runs to the pool, keep hb buffers for reuse */ + raqm_run_t *run = rq->runs; + while (run) + { + if (run->buffer) + hb_buffer_reset (run->buffer); + + if (run->font) + { + hb_font_destroy (run->font); + run->font = NULL; + } + + if (!run->next) + { + run->next = rq->runs_pool; + rq->runs_pool = rq->runs; + rq->runs = NULL; + break; + } + + run = run->next; + } + + rq->text_len = 0; + rq->resolved_dir = RAQM_DIRECTION_DEFAULT; +} + +/** + * raqm_set_text: + * @rq: a #raqm_t. + * @text: a UTF-32 encoded text string. + * @len: the length of @text. + * + * Adds @text to @rq to be used for layout. It must be a valid UTF-32 text, any + * invalid character will be replaced with U+FFFD. The text should typically + * represent a full paragraph, since doing the layout of chunks of text + * separately can give improper output. + * + * Return value: + * %true if no errors happened, %false otherwise. + * + * Since: 0.1 + */ +bool +raqm_set_text (raqm_t *rq, + const uint32_t *text, + size_t len) +{ + if (!rq || !text) + return false; + + /* Call raqm_clear_contents to reuse this raqm_t */ + if (rq->text_len) + return false; + + /* Empty string, don’t fail but do nothing */ + if (!len) + return true; + + if (!_raqm_alloc_text(rq, len, false)) + return false; + + rq->text_len = len; + memcpy (rq->text, text, sizeof (uint32_t) * len); + _raqm_init_text_info (rq); + + return true; +} + +static void * +_raqm_get_utf8_codepoint (const void *str, + uint32_t *out_codepoint) +{ + const char *s = (const char *)str; + + if (0xf0 == (0xf8 & s[0])) + { + *out_codepoint = ((0x07 & s[0]) << 18) | ((0x3f & s[1]) << 12) | ((0x3f & s[2]) << 6) | (0x3f & s[3]); + s += 4; + } + else if (0xe0 == (0xf0 & s[0])) + { + *out_codepoint = ((0x0f & s[0]) << 12) | ((0x3f & s[1]) << 6) | (0x3f & s[2]); + s += 3; + } + else if (0xc0 == (0xe0 & s[0])) + { + *out_codepoint = ((0x1f & s[0]) << 6) | (0x3f & s[1]); + s += 2; + } + else + { + *out_codepoint = s[0]; + s += 1; + } + + return (void *)s; +} + +static size_t +_raqm_u8_to_u32 (const char *text, size_t len, uint32_t *unicode) +{ + size_t in_len = 0; + uint32_t *out_utf32 = unicode; + const char *in_utf8 = text; + + while ((*in_utf8 != '\0') && (in_len < len)) + { + in_utf8 = _raqm_get_utf8_codepoint (in_utf8, out_utf32); + ++out_utf32; + ++in_len; + } + + return (out_utf32 - unicode); +} + +/** + * raqm_set_text_utf8: + * @rq: a #raqm_t. + * @text: a UTF-8 encoded text string. + * @len: the length of @text in UTF-8 bytes. + * + * Same as raqm_set_text(), but for text encoded in UTF-8 encoding. + * + * Return value: + * %true if no errors happened, %false otherwise. + * + * Since: 0.1 + */ +bool +raqm_set_text_utf8 (raqm_t *rq, + const char *text, + size_t len) +{ + if (!rq || !text) + return false; + + /* Call raqm_clear_contents to reuse this raqm_t */ + if (rq->text_len) + return false; + + /* Empty string, don’t fail but do nothing */ + if (!len) + return true; + + if (!_raqm_alloc_text(rq, len, true)) + return false; + + rq->text_len = _raqm_u8_to_u32 (text, len, rq->text); + memcpy (rq->text_utf8, text, sizeof (char) * len); + _raqm_init_text_info (rq); + + return true; +} + +/** + * raqm_set_par_direction: + * @rq: a #raqm_t. + * @dir: the direction of the paragraph. + * + * Sets the paragraph direction, also known as block direction in CSS. For + * horizontal text, this controls the overall direction in the Unicode + * Bidirectional Algorithm, so when the text is mainly right-to-left (with or + * without some left-to-right) text, then the base direction should be set to + * #RAQM_DIRECTION_RTL and vice versa. + * + * The default is #RAQM_DIRECTION_DEFAULT, which determines the paragraph + * direction based on the first character with strong bidi type (see [rule + * P2](https://unicode.org/reports/tr9/#P2) in Unicode Bidirectional Algorithm), + * which can be good enough for many cases but has problems when a mainly + * right-to-left paragraph starts with a left-to-right character and vice versa + * as the detected paragraph direction will be the wrong one, or when text does + * not contain any characters with string bidi types (e.g. only punctuation or + * numbers) as this will default to left-to-right paragraph direction. + * + * For vertical, top-to-bottom text, #RAQM_DIRECTION_TTB should be used. Raqm, + * however, provides limited vertical text support and does not handle rotated + * horizontal text in vertical text, instead everything is treated as vertical + * text. + * + * Return value: + * %true if no errors happened, %false otherwise. + * + * Since: 0.1 + */ +bool +raqm_set_par_direction (raqm_t *rq, + raqm_direction_t dir) +{ + if (!rq) + return false; + + rq->base_dir = dir; + + return true; +} + +/** + * raqm_set_language: + * @rq: a #raqm_t. + * @lang: a BCP47 language code. + * @start: index of first character that should use @face. + * @len: number of characters using @face. + * + * Sets a [BCP47 language + * code](https://www.w3.org/International/articles/language-tags/) to be used + * for @len-number of characters staring at @start. The @start and @len are + * input string array indices (i.e. counting bytes in UTF-8 and scaler values + * in UTF-32). + * + * This method can be used repeatedly to set different languages for different + * parts of the text. + * + * Return value: + * %true if no errors happened, %false otherwise. + * + * Stability: + * Unstable + * + * Since: 0.2 + */ +bool +raqm_set_language (raqm_t *rq, + const char *lang, + size_t start, + size_t len) +{ + hb_language_t language; + size_t end = start + len; + + if (!rq) + return false; + + if (!rq->text_len) + return true; + + if (rq->text_utf8) + { + start = _raqm_u8_to_u32_index (rq, start); + end = _raqm_u8_to_u32_index (rq, end); + } + + if (start >= rq->text_len || end > rq->text_len) + return false; + + if (!rq->text_info) + return false; + + language = hb_language_from_string (lang, -1); + for (size_t i = start; i < end; i++) + { + rq->text_info[i].lang = language; + } + + return true; +} + +/** + * raqm_add_font_feature: + * @rq: a #raqm_t. + * @feature: (transfer none): a font feature string. + * @len: length of @feature, -1 for %NULL-terminated. + * + * Adds a font feature to be used by the #raqm_t during text layout. This is + * usually used to turn on optional font features that are not enabled by + * default, for example `dlig` or `ss01`, but can be also used to turn off + * default font features. + * + * @feature is string representing a single font feature, in the syntax + * understood by hb_feature_from_string(). + * + * This function can be called repeatedly, new features will be appended to the + * end of the features list and can potentially override previous features. + * + * Return value: + * %true if parsing @feature succeeded, %false otherwise. + * + * Since: 0.1 + */ +bool +raqm_add_font_feature (raqm_t *rq, + const char *feature, + int len) +{ + hb_bool_t ok; + hb_feature_t fea; + + if (!rq) + return false; + + ok = hb_feature_from_string (feature, len, &fea); + if (ok) + { + void* new_features = realloc (rq->features, + sizeof (hb_feature_t) * (rq->features_len + 1)); + if (!new_features) + return false; + + rq->features = new_features; + rq->features[rq->features_len] = fea; + rq->features_len++; + } + + return ok; +} + +static hb_font_t * +_raqm_create_hb_font (raqm_t *rq, + FT_Face face, + int loadflags) +{ + hb_font_t *font = hb_ft_font_create_referenced (face); + + if (loadflags >= 0) + hb_ft_font_set_load_flags (font, loadflags); + + return font; +} + +static bool +_raqm_set_freetype_face (raqm_t *rq, + FT_Face face, + size_t start, + size_t end) +{ + if (!rq) + return false; + + if (!rq->text_len) + return true; + + if (start >= rq->text_len || end > rq->text_len) + return false; + + if (!rq->text_info) + return false; + + for (size_t i = start; i < end; i++) + { + if (rq->text_info[i].ftface) + FT_Done_Face (rq->text_info[i].ftface); + rq->text_info[i].ftface = face; + FT_Reference_Face (face); + } + + return true; +} + +/** + * raqm_set_freetype_face: + * @rq: a #raqm_t. + * @face: an #FT_Face. + * + * Sets an #FT_Face to be used for all characters in @rq. + * + * See also raqm_set_freetype_face_range(). + * + * Return value: + * %true if no errors happened, %false otherwise. + * + * Since: 0.1 + */ +bool +raqm_set_freetype_face (raqm_t *rq, + FT_Face face) +{ + return _raqm_set_freetype_face (rq, face, 0, rq->text_len); +} + +/** + * raqm_set_freetype_face_range: + * @rq: a #raqm_t. + * @face: an #FT_Face. + * @start: index of first character that should use @face. + * @len: number of characters using @face. + * + * Sets an #FT_Face to be used for @len-number of characters staring at @start. + * The @start and @len are input string array indices (i.e. counting bytes in + * UTF-8 and scaler values in UTF-32). + * + * This method can be used repeatedly to set different faces for different + * parts of the text. It is the responsibility of the client to make sure that + * face ranges cover the whole text. + * + * See also raqm_set_freetype_face(). + * + * Return value: + * %true if no errors happened, %false otherwise. + * + * Since: 0.1 + */ +bool +raqm_set_freetype_face_range (raqm_t *rq, + FT_Face face, + size_t start, + size_t len) +{ + size_t end = start + len; + + if (!rq) + return false; + + if (!rq->text_len) + return true; + + if (rq->text_utf8) + { + start = _raqm_u8_to_u32_index (rq, start); + end = _raqm_u8_to_u32_index (rq, end); + } + + return _raqm_set_freetype_face (rq, face, start, end); +} + +static bool +_raqm_set_freetype_load_flags (raqm_t *rq, + int flags, + size_t start, + size_t end) +{ + if (!rq) + return false; + + if (!rq->text_len) + return true; + + if (start >= rq->text_len || end > rq->text_len) + return false; + + if (!rq->text_info) + return false; + + for (size_t i = start; i < end; i++) + rq->text_info[i].ftloadflags = flags; + + return true; +} + +/** + * raqm_set_freetype_load_flags: + * @rq: a #raqm_t. + * @flags: FreeType load flags. + * + * Sets the load flags passed to FreeType when loading glyphs, should be the + * same flags used by the client when rendering FreeType glyphs. + * + * This requires version of HarfBuzz that has hb_ft_font_set_load_flags(), for + * older version the flags will be ignored. + * + * Return value: + * %true if no errors happened, %false otherwise. + * + * Since: 0.3 + */ +bool +raqm_set_freetype_load_flags (raqm_t *rq, + int flags) +{ + return _raqm_set_freetype_load_flags(rq, flags, 0, rq->text_len); +} + +/** + * raqm_set_freetype_load_flags_range: + * @rq: a #raqm_t. + * @flags: FreeType load flags. + * @start: index of first character that should use @flags. + * @len: number of characters using @flags. + * + * Sets the load flags passed to FreeType when loading glyphs for @len-number + * of characters staring at @start. Flags should be the same as used by the + * client when rendering corresponding FreeType glyphs. The @start and @len + * are input string array indices (i.e. counting bytes in UTF-8 and scaler + * values in UTF-32). + * + * This method can be used repeatedly to set different flags for different + * parts of the text. It is the responsibility of the client to make sure that + * flag ranges cover the whole text. + * + * This requires version of HarfBuzz that has hb_ft_font_set_load_flags(), for + * older version the flags will be ignored. + * + * See also raqm_set_freetype_load_flags(). + * + * Return value: + * %true if no errors happened, %false otherwise. + * + * Since: 0.9 + */ +bool +raqm_set_freetype_load_flags_range (raqm_t *rq, + int flags, + size_t start, + size_t len) +{ + size_t end = start + len; + + if (!rq) + return false; + + if (!rq->text_len) + return true; + + if (rq->text_utf8) + { + start = _raqm_u8_to_u32_index (rq, start); + end = _raqm_u8_to_u32_index (rq, end); + } + + return _raqm_set_freetype_load_flags (rq, flags, start, end); +} + +/** + * raqm_set_invisible_glyph: + * @rq: a #raqm_t. + * @gid: glyph id to use for invisible glyphs. + * + * Sets the glyph id to be used for invisible glyhphs. + * + * If @gid is negative, invisible glyphs will be suppressed from the output. + * + * If @gid is zero, invisible glyphs will be rendered as space. + * + * If @gid is a positive number, it will be used for invisible glyphs. + * + * Return value: + * %true if no errors happened, %false otherwise. + * + * Since: 0.6 + */ +bool +raqm_set_invisible_glyph (raqm_t *rq, + int gid) +{ + if (!rq) + return false; + + rq->invisible_glyph = gid; + return true; +} + +static bool +_raqm_itemize (raqm_t *rq); + +static bool +_raqm_shape (raqm_t *rq); + +/** + * raqm_layout: + * @rq: a #raqm_t. + * + * Run the text layout process on @rq. This is the main Raqm function where the + * Unicode Bidirectional Text algorithm will be applied to the text in @rq, + * text shaping, and any other part of the layout process. + * + * Return value: + * %true if the layout process was successful, %false otherwise. + * + * Since: 0.1 + */ +bool +raqm_layout (raqm_t *rq) +{ + if (!rq) + return false; + + if (!rq->text_len) + return true; + + if (!rq->text_info) + return false; + + for (size_t i = 0; i < rq->text_len; i++) + { + if (!rq->text_info[i].ftface) + return false; + } + + if (!_raqm_itemize (rq)) + return false; + + if (!_raqm_shape (rq)) + return false; + + return true; +} + +static uint32_t +_raqm_u32_to_u8_index (raqm_t *rq, + uint32_t index); + +/** + * raqm_get_glyphs: + * @rq: a #raqm_t. + * @length: (out): output array length. + * + * Gets the final result of Raqm layout process, an array of #raqm_glyph_t + * containing the glyph indices in the font, their positions and other possible + * information. + * + * Return value: (transfer none): + * An array of #raqm_glyph_t, or %NULL in case of error. This is owned by @rq + * and must not be freed. + * + * Since: 0.1 + */ +raqm_glyph_t * +raqm_get_glyphs (raqm_t *rq, + size_t *length) +{ + size_t count = 0; + + if (!rq || !rq->runs || !length) + { + if (length) + *length = 0; + return NULL; + } + + for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) + count += hb_buffer_get_length (run->buffer); + + if (count > rq->glyphs_capacity) + { + void* new_mem = realloc (rq->glyphs, sizeof (raqm_glyph_t) * count); + if (!new_mem) + { + *length = 0; + return NULL; + } + + rq->glyphs = new_mem; + rq->glyphs_capacity = count; + } + + *length = count; + + RAQM_TEST ("Glyph information:\n"); + + count = 0; + for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) + { + size_t len; + hb_glyph_info_t *info; + hb_glyph_position_t *position; + + len = hb_buffer_get_length (run->buffer); + info = hb_buffer_get_glyph_infos (run->buffer, NULL); + position = hb_buffer_get_glyph_positions (run->buffer, NULL); + + for (size_t i = 0; i < len; i++) + { + rq->glyphs[count + i].index = info[i].codepoint; + rq->glyphs[count + i].cluster = info[i].cluster; + rq->glyphs[count + i].x_advance = position[i].x_advance; + rq->glyphs[count + i].y_advance = position[i].y_advance; + rq->glyphs[count + i].x_offset = position[i].x_offset; + rq->glyphs[count + i].y_offset = position[i].y_offset; + rq->glyphs[count + i].ftface = rq->text_info[info[i].cluster].ftface; + + RAQM_TEST ("glyph [%d]\tx_offset: %d\ty_offset: %d\tx_advance: %d\tfont: %s\n", + rq->glyphs[count + i].index, rq->glyphs[count + i].x_offset, + rq->glyphs[count + i].y_offset, rq->glyphs[count + i].x_advance, + rq->glyphs[count + i].ftface->family_name); + } + + count += len; + } + + if (rq->text_utf8) + { +#ifdef RAQM_TESTING + RAQM_TEST ("\nUTF-32 clusters:"); + for (size_t i = 0; i < count; i++) + RAQM_TEST (" %02d", rq->glyphs[i].cluster); + RAQM_TEST ("\n"); +#endif + + for (size_t i = 0; i < count; i++) + rq->glyphs[i].cluster = _raqm_u32_to_u8_index (rq, + rq->glyphs[i].cluster); + +#ifdef RAQM_TESTING + RAQM_TEST ("UTF-8 clusters: "); + for (size_t i = 0; i < count; i++) + RAQM_TEST (" %02d", rq->glyphs[i].cluster); + RAQM_TEST ("\n"); +#endif + } + return rq->glyphs; +} + +/** + * raqm_get_par_resolved_direction: + * @rq: a #raqm_t. + * + * Gets the resolved direction of the paragraph; + * + * Return value: + * The #raqm_direction_t specifying the resolved direction of text, + * or #RAQM_DIRECTION_DEFAULT if raqm_layout() has not been called on @rq. + * + * Since: 0.8 + */ +RAQM_API raqm_direction_t +raqm_get_par_resolved_direction (raqm_t *rq) +{ + if (!rq) + return RAQM_DIRECTION_DEFAULT; + + return rq->resolved_dir; +} + +/** + * raqm_get_direction_at_index: + * @rq: a #raqm_t. + * @index: (in): character index. + * + * Gets the resolved direction of the character at specified index; + * + * Return value: + * The #raqm_direction_t specifying the resolved direction of text at the + * specified index, or #RAQM_DIRECTION_DEFAULT if raqm_layout() has not been + * called on @rq. + * + * Since: 0.8 + */ +RAQM_API raqm_direction_t +raqm_get_direction_at_index (raqm_t *rq, + size_t index) +{ + if (!rq) + return RAQM_DIRECTION_DEFAULT; + + for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) + { + if (run->pos <= index && index < run->pos + run->len) { + switch (run->direction) { + case HB_DIRECTION_LTR: + return RAQM_DIRECTION_LTR; + case HB_DIRECTION_RTL: + return RAQM_DIRECTION_RTL; + case HB_DIRECTION_TTB: + return RAQM_DIRECTION_TTB; + default: + return RAQM_DIRECTION_DEFAULT; + } + } + } + + return RAQM_DIRECTION_DEFAULT; +} + +static bool +_raqm_resolve_scripts (raqm_t *rq); + +static hb_direction_t +_raqm_hb_dir (raqm_t *rq, _raqm_bidi_level_t level) +{ + hb_direction_t dir = HB_DIRECTION_LTR; + + if (rq->base_dir == RAQM_DIRECTION_TTB) + dir = HB_DIRECTION_TTB; + else if (RAQM_BIDI_LEVEL_IS_RTL(level)) + dir = HB_DIRECTION_RTL; + + return dir; +} + +typedef struct { + size_t pos; + size_t len; + _raqm_bidi_level_t level; +} _raqm_bidi_run; + +#ifdef RAQM_SHEENBIDI +static _raqm_bidi_run * +_raqm_bidi_itemize (raqm_t *rq, size_t *run_count) +{ + _raqm_bidi_run *runs; + SBAlgorithmRef bidi; + SBParagraphRef par; + SBUInteger par_len; + SBLineRef line; + + SBLevel base_level = SBLevelDefaultLTR; + SBCodepointSequence input = { + SBStringEncodingUTF32, + (void *) rq->text, + rq->text_len + }; + + if (rq->base_dir == RAQM_DIRECTION_RTL) + base_level = 1; + else if (rq->base_dir == RAQM_DIRECTION_LTR) + base_level = 0; + + /* paragraph */ + bidi = SBAlgorithmCreate (&input); + par = SBAlgorithmCreateParagraph (bidi, 0, INT32_MAX, base_level); + par_len = SBParagraphGetLength (par); + + /* lines */ + line = SBParagraphCreateLine (par, 0, par_len); + *run_count = SBLineGetRunCount (line); + + if (SBParagraphGetBaseLevel (par) == 0) + rq->resolved_dir = RAQM_DIRECTION_LTR; + else + rq->resolved_dir = RAQM_DIRECTION_RTL; + + runs = malloc (sizeof (_raqm_bidi_run) * (*run_count)); + if (runs) + { + const SBRun *sheenbidi_runs = SBLineGetRunsPtr(line); + + for (size_t i = 0; i < (*run_count); ++i) + { + runs[i].pos = sheenbidi_runs[i].offset; + runs[i].len = sheenbidi_runs[i].length; + runs[i].level = sheenbidi_runs[i].level; + } + } + + SBLineRelease (line); + SBParagraphRelease (par); + SBAlgorithmRelease (bidi); + + return runs; +} +#else +static void +_raqm_reverse_run (_raqm_bidi_run *run, const size_t len) +{ + assert (run); + + for (size_t i = 0; i < len / 2; i++) + { + _raqm_bidi_run temp = run[i]; + run[i] = run[len - 1 - i]; + run[len - 1 - i] = temp; + } +} + +static _raqm_bidi_run * +_raqm_reorder_runs (const FriBidiCharType *types, + const size_t len, + const FriBidiParType base_dir, + /* input and output */ + FriBidiLevel *levels, + /* output */ + size_t *run_count) +{ + FriBidiLevel level; + FriBidiLevel last_level = -1; + FriBidiLevel max_level = 0; + size_t run_start = 0; + size_t run_index = 0; + _raqm_bidi_run *runs = NULL; + size_t count = 0; + + if (len == 0) + { + *run_count = 0; + return NULL; + } + + assert (types); + assert (levels); + + /* L1. Reset the embedding levels of some chars: + 4. any sequence of white space characters at the end of the line. */ + for (int i = len - 1; + i >= 0 && FRIBIDI_IS_EXPLICIT_OR_BN_OR_WS (types[i]); i--) + { + levels[i] = FRIBIDI_DIR_TO_LEVEL (base_dir); + } + + /* Find max_level of the line. We don't reuse the paragraph + * max_level, both for a cleaner API, and that the line max_level + * may be far less than paragraph max_level. */ + for (int i = len - 1; i >= 0; i--) + { + if (levels[i] > max_level) + max_level = levels[i]; + } + + for (size_t i = 0; i < len; i++) + { + if (levels[i] != last_level) + count++; + + last_level = levels[i]; + } + + runs = malloc (sizeof (_raqm_bidi_run) * count); + + while (run_start < len) + { + size_t run_end = run_start; + while (run_end < len && levels[run_start] == levels[run_end]) + { + run_end++; + } + + runs[run_index].pos = run_start; + runs[run_index].level = levels[run_start]; + runs[run_index].len = run_end - run_start; + run_start = run_end; + run_index++; + } + + /* L2. Reorder. */ + for (level = max_level; level > 0; level--) + { + for (int i = count - 1; i >= 0; i--) + { + if (runs[i].level >= level) + { + int end = i; + for (i--; (i >= 0 && runs[i].level >= level); i--) + ; + _raqm_reverse_run (runs + i + 1, end - i); + } + } + } + + *run_count = count; + return runs; +} + +static _raqm_bidi_run * +_raqm_bidi_itemize (raqm_t *rq, size_t *run_count) +{ + FriBidiParType par_type = FRIBIDI_PAR_ON; + _raqm_bidi_run *runs = NULL; + + FriBidiCharType *types; + _raqm_bidi_level_t *levels; + int max_level = 0; + FriBidiBracketType *btypes; + + types = calloc (rq->text_len, sizeof (FriBidiCharType)); + btypes = calloc (rq->text_len, sizeof (FriBidiBracketType)); + levels = calloc (rq->text_len, sizeof (_raqm_bidi_level_t)); + + if (!types || !levels || !btypes) + goto done; + + if (rq->base_dir == RAQM_DIRECTION_RTL) + par_type = FRIBIDI_PAR_RTL; + else if (rq->base_dir == RAQM_DIRECTION_LTR) + par_type = FRIBIDI_PAR_LTR; + + fribidi_get_bidi_types (rq->text, rq->text_len, types); + fribidi_get_bracket_types (rq->text, rq->text_len, types, btypes); + max_level = fribidi_get_par_embedding_levels_ex (types, btypes, + rq->text_len, &par_type, + levels); + + if (par_type == FRIBIDI_PAR_LTR) + rq->resolved_dir = RAQM_DIRECTION_LTR; + else + rq->resolved_dir = RAQM_DIRECTION_RTL; + + if (max_level == 0) + goto done; + + /* Get the number of bidi runs */ + runs = _raqm_reorder_runs (types, rq->text_len, par_type, levels, run_count); + +done: + free (types); + free (levels); + free (btypes); + + return runs; +} +#endif + +static bool +_raqm_itemize (raqm_t *rq) +{ + _raqm_bidi_run *runs = NULL; + raqm_run_t *last; + size_t run_count = 0; + bool ok = true; + +#ifdef RAQM_TESTING + switch (rq->base_dir) + { + case RAQM_DIRECTION_RTL: + RAQM_TEST ("Direction is: RTL\n\n"); + break; + case RAQM_DIRECTION_LTR: + RAQM_TEST ("Direction is: LTR\n\n"); + break; + case RAQM_DIRECTION_TTB: + RAQM_TEST ("Direction is: TTB\n\n"); + break; + case RAQM_DIRECTION_DEFAULT: + default: + RAQM_TEST ("Direction is: DEFAULT\n\n"); + break; + } +#endif + + if (!_raqm_resolve_scripts (rq)) + { + ok = false; + goto done; + } + + if (rq->base_dir == RAQM_DIRECTION_TTB) + { + /* Treat every thing as LTR in vertical text */ + run_count = 1; + rq->resolved_dir = RAQM_DIRECTION_TTB; + runs = malloc (sizeof (_raqm_bidi_run)); + if (runs) + { + runs->pos = 0; + runs->len = rq->text_len; + runs->level = 0; + } + } else { + runs = _raqm_bidi_itemize (rq, &run_count); + } + + if (!runs) + { + ok = false; + goto done; + } + +#ifdef RAQM_TESTING + RAQM_TEST ("Number of runs before script itemization: %zu\n\n", run_count); + + RAQM_TEST ("BiDi Runs:\n"); + for (size_t i = 0; i < run_count; i++) + { + RAQM_TEST ("run[%zu]:\t start: %zu\tlength: %zu\tlevel: %d\n", + i, runs[i].pos, runs[i].len, runs[i].level); + } + RAQM_TEST ("\n"); +#endif + + last = NULL; + for (size_t i = 0; i < run_count; i++) + { + raqm_run_t *run = _raqm_alloc_run (rq); + if (!run) + { + ok = false; + goto done; + } + + if (!rq->runs) + rq->runs = run; + + if (last) + last->next = run; + + run->direction = _raqm_hb_dir (rq, runs[i].level); + + if (HB_DIRECTION_IS_BACKWARD (run->direction)) + { + run->pos = runs[i].pos + runs[i].len - 1; + run->script = rq->text_info[run->pos].script; + run->font = _raqm_create_hb_font (rq, rq->text_info[run->pos].ftface, + rq->text_info[run->pos].ftloadflags); + for (int j = runs[i].len - 1; j >= 0; j--) + { + _raqm_text_info info = rq->text_info[runs[i].pos + j]; + if (!_raqm_compare_text_info (rq->text_info[run->pos], info)) + { + raqm_run_t *newrun = _raqm_alloc_run (rq); + if (!newrun) + { + ok = false; + goto done; + } + newrun->pos = runs[i].pos + j; + newrun->len = 1; + newrun->direction = _raqm_hb_dir (rq, runs[i].level); + newrun->script = info.script; + newrun->font = _raqm_create_hb_font (rq, info.ftface, + info.ftloadflags); + run->next = newrun; + run = newrun; + } + else + { + run->len++; + run->pos = runs[i].pos + j; + } + } + } + else + { + run->pos = runs[i].pos; + run->script = rq->text_info[run->pos].script; + run->font = _raqm_create_hb_font (rq, rq->text_info[run->pos].ftface, + rq->text_info[run->pos].ftloadflags); + for (size_t j = 0; j < runs[i].len; j++) + { + _raqm_text_info info = rq->text_info[runs[i].pos + j]; + if (!_raqm_compare_text_info (rq->text_info[run->pos], info)) + { + raqm_run_t *newrun = _raqm_alloc_run (rq); + if (!newrun) + { + ok = false; + goto done; + } + newrun->pos = runs[i].pos + j; + newrun->len = 1; + newrun->direction = _raqm_hb_dir (rq, runs[i].level); + newrun->script = info.script; + newrun->font = _raqm_create_hb_font (rq, info.ftface, + info.ftloadflags); + run->next = newrun; + run = newrun; + } + else + run->len++; + } + } + + last = run; + last->next = NULL; + } + +#ifdef RAQM_TESTING + run_count = 0; + for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) + run_count++; + RAQM_TEST ("Number of runs after script itemization: %zu\n\n", run_count); + + run_count = 0; + RAQM_TEST ("Final Runs:\n"); + for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) + { + SCRIPT_TO_STRING (run->script); + RAQM_TEST ("run[%zu]:\t start: %d\tlength: %d\tdirection: %s\tscript: %s\tfont: %s\n", + run_count++, run->pos, run->len, + hb_direction_to_string (run->direction), buff, + rq->text_info[run->pos].ftface->family_name); + } + RAQM_TEST ("\n"); +#endif + +done: + free (runs); + + return ok; +} + +/* Stack to handle script detection */ +typedef struct { + size_t capacity; + size_t size; + int *pair_index; + hb_script_t *script; +} _raqm_stack_t; + +/* Special paired characters for script detection */ +static size_t paired_len = 34; +static const uint32_t paired_chars[] = +{ + 0x0028, 0x0029, /* ascii paired punctuation */ + 0x003c, 0x003e, + 0x005b, 0x005d, + 0x007b, 0x007d, + 0x00ab, 0x00bb, /* guillemets */ + 0x2018, 0x2019, /* general punctuation */ + 0x201c, 0x201d, + 0x2039, 0x203a, + 0x3008, 0x3009, /* chinese paired punctuation */ + 0x300a, 0x300b, + 0x300c, 0x300d, + 0x300e, 0x300f, + 0x3010, 0x3011, + 0x3014, 0x3015, + 0x3016, 0x3017, + 0x3018, 0x3019, + 0x301a, 0x301b +}; + +static void +_raqm_stack_free (_raqm_stack_t *stack) +{ + free (stack->script); + free (stack->pair_index); + free (stack); +} + +/* Stack handling functions */ +static _raqm_stack_t * +_raqm_stack_new (size_t max) +{ + _raqm_stack_t *stack; + stack = calloc (1, sizeof (_raqm_stack_t)); + if (!stack) + return NULL; + + stack->script = malloc (sizeof (hb_script_t) * max); + if (!stack->script) + { + _raqm_stack_free (stack); + return NULL; + } + + stack->pair_index = malloc (sizeof (int) * max); + if (!stack->pair_index) + { + _raqm_stack_free (stack); + return NULL; + } + + stack->size = 0; + stack->capacity = max; + + return stack; +} + +static bool +_raqm_stack_pop (_raqm_stack_t *stack) +{ + if (!stack->size) + { + RAQM_DBG ("Stack is Empty\n"); + return false; + } + + stack->size--; + + return true; +} + +static hb_script_t +_raqm_stack_top (_raqm_stack_t *stack) +{ + if (!stack->size) + { + RAQM_DBG ("Stack is Empty\n"); + return HB_SCRIPT_INVALID; /* XXX: check this */ + } + + return stack->script[stack->size]; +} + +static bool +_raqm_stack_push (_raqm_stack_t *stack, + hb_script_t script, + int pair_index) +{ + if (stack->size == stack->capacity) + { + RAQM_DBG ("Stack is Full\n"); + return false; + } + + stack->size++; + stack->script[stack->size] = script; + stack->pair_index[stack->size] = pair_index; + + return true; +} + +static int +_get_pair_index (const uint32_t ch) +{ + int lower = 0; + int upper = paired_len - 1; + + while (lower <= upper) + { + int mid = (lower + upper) / 2; + if (ch < paired_chars[mid]) + upper = mid - 1; + else if (ch > paired_chars[mid]) + lower = mid + 1; + else + return mid; + } + + return -1; +} + +#define STACK_IS_EMPTY(script) ((script)->size <= 0) +#define IS_OPEN(pair_index) (((pair_index) & 1) == 0) + +/* Resolve the script for each character in the input string, if the character + * script is common or inherited it takes the script of the character before it + * except paired characters which we try to make them use the same script. We + * then split the BiDi runs, if necessary, on script boundaries. + */ +static bool +_raqm_resolve_scripts (raqm_t *rq) +{ + int last_script_index = -1; + int last_set_index = -1; + hb_script_t last_script = HB_SCRIPT_INVALID; + _raqm_stack_t *stack = NULL; + hb_unicode_funcs_t* unicode_funcs = hb_unicode_funcs_get_default (); + + for (size_t i = 0; i < rq->text_len; ++i) + rq->text_info[i].script = hb_unicode_script (unicode_funcs, rq->text[i]); + +#ifdef RAQM_TESTING + RAQM_TEST ("Before script detection:\n"); + for (size_t i = 0; i < rq->text_len; ++i) + { + SCRIPT_TO_STRING (rq->text_info[i].script); + RAQM_TEST ("script for ch[%zu]\t%s\n", i, buff); + } + RAQM_TEST ("\n"); +#endif + + stack = _raqm_stack_new (rq->text_len); + if (!stack) + return false; + + for (int i = 0; i < (int) rq->text_len; i++) + { + if (rq->text_info[i].script == HB_SCRIPT_COMMON && last_script_index != -1) + { + int pair_index = _get_pair_index (rq->text[i]); + if (pair_index >= 0) + { + if (IS_OPEN (pair_index)) + { + /* is a paired character */ + rq->text_info[i].script = last_script; + last_set_index = i; + _raqm_stack_push (stack, rq->text_info[i].script, pair_index); + } + else + { + /* is a close paired character */ + /* find matching opening (by getting the last even index for current + * odd index) */ + while (!STACK_IS_EMPTY (stack) && + stack->pair_index[stack->size] != (pair_index & ~1)) + { + _raqm_stack_pop (stack); + } + if (!STACK_IS_EMPTY (stack)) + { + rq->text_info[i].script = _raqm_stack_top (stack); + last_script = rq->text_info[i].script; + last_set_index = i; + } + else + { + rq->text_info[i].script = last_script; + last_set_index = i; + } + } + } + else + { + rq->text_info[i].script = last_script; + last_set_index = i; + } + } + else if (rq->text_info[i].script == HB_SCRIPT_INHERITED && + last_script_index != -1) + { + rq->text_info[i].script = last_script; + last_set_index = i; + } + else + { + for (int j = last_set_index + 1; j < i; ++j) + rq->text_info[j].script = rq->text_info[i].script; + last_script = rq->text_info[i].script; + last_script_index = i; + last_set_index = i; + } + } + + /* Loop backwards and change any remaining Common or Inherit characters to + * take the script if the next character. + * https://github.com/HOST-Oman/libraqm/issues/95 + */ + for (int i = rq->text_len - 2; i >= 0; --i) + { + if (rq->text_info[i].script == HB_SCRIPT_INHERITED || + rq->text_info[i].script == HB_SCRIPT_COMMON) + rq->text_info[i].script = rq->text_info[i + 1].script; + } + +#ifdef RAQM_TESTING + RAQM_TEST ("After script detection:\n"); + for (size_t i = 0; i < rq->text_len; ++i) + { + SCRIPT_TO_STRING (rq->text_info[i].script); + RAQM_TEST ("script for ch[%zu]\t%s\n", i, buff); + } + RAQM_TEST ("\n"); +#endif + + _raqm_stack_free (stack); + + return true; +} + +static void +_raqm_ft_transform (int *x, + int *y, + FT_Matrix matrix) +{ + FT_Vector vector; + vector.x = *x; + vector.y = *y; + + FT_Vector_Transform (&vector, &matrix); + + *x = vector.x; + *y = vector.y; +} + +static bool +_raqm_shape (raqm_t *rq) +{ + hb_buffer_flags_t hb_buffer_flags = HB_BUFFER_FLAG_BOT | HB_BUFFER_FLAG_EOT; + + if (rq->invisible_glyph < 0) + hb_buffer_flags |= HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES; + + for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) + { + if (!run->buffer) + run->buffer = hb_buffer_create (); + + hb_buffer_add_utf32 (run->buffer, rq->text, rq->text_len, + run->pos, run->len); + hb_buffer_set_script (run->buffer, run->script); + hb_buffer_set_language (run->buffer, rq->text_info[run->pos].lang); + hb_buffer_set_direction (run->buffer, run->direction); + hb_buffer_set_flags (run->buffer, hb_buffer_flags); + + if (rq->invisible_glyph > 0) + hb_buffer_set_invisible_glyph (run->buffer, rq->invisible_glyph); + + hb_shape_full (run->font, run->buffer, rq->features, rq->features_len, + NULL); + + { + FT_Matrix matrix; + hb_glyph_position_t *pos; + unsigned int len; + + FT_Get_Transform (hb_ft_font_get_face (run->font), &matrix, NULL); + pos = hb_buffer_get_glyph_positions (run->buffer, &len); + for (unsigned int i = 0; i < len; i++) + { + _raqm_ft_transform (&pos[i].x_advance, &pos[i].y_advance, matrix); + _raqm_ft_transform (&pos[i].x_offset, &pos[i].y_offset, matrix); + } + } + } + + return true; +} + +/* Count equivalent UTF-8 bytes in codepoint */ +static size_t +_raqm_count_codepoint_utf8_bytes (uint32_t chr) +{ + if (0 == ((uint32_t) 0xffffff80 & chr)) + return 1; + else if (0 == ((uint32_t) 0xfffff800 & chr)) + return 2; + else if (0 == ((uint32_t) 0xffff0000 & chr)) + return 3; + else + return 4; +} + +/* Convert index from UTF-32 to UTF-8 */ +static uint32_t +_raqm_u32_to_u8_index (raqm_t *rq, + uint32_t index) +{ + size_t length = 0; + + for (uint32_t i = 0; i < index; ++i) + length += _raqm_count_codepoint_utf8_bytes (rq->text[i]); + + return length; +} + +/* Convert index from UTF-8 to UTF-32 */ +static uint32_t +_raqm_u8_to_u32_index (raqm_t *rq, + uint32_t index) +{ + const unsigned char *s = (const unsigned char *) rq->text_utf8; + const unsigned char *t = s; + size_t length = 0; + + while (((size_t) (s - t) < index) && ('\0' != *s)) + { + if (0xf0 == (0xf8 & *s)) + s += 4; + else if (0xe0 == (0xf0 & *s)) + s += 3; + else if (0xc0 == (0xe0 & *s)) + s += 2; + else + s += 1; + + length++; + } + + if ((size_t) (s-t) > index) + length--; + + return length; +} + +static bool +_raqm_allowed_grapheme_boundary (hb_codepoint_t l_char, + hb_codepoint_t r_char); + +static bool +_raqm_in_hangul_syllable (hb_codepoint_t ch); + +/** + * raqm_index_to_position: + * @rq: a #raqm_t. + * @index: (inout): character index. + * @x: (out): output x position. + * @y: (out): output y position. + * + * Calculates the cursor position after the character at @index. If the character + * is right-to-left, then the cursor will be at the left of it, whereas if the + * character is left-to-right, then the cursor will be at the right of it. + * + * Return value: + * %true if the process was successful, %false otherwise. + * + * Since: 0.2 + */ +bool +raqm_index_to_position (raqm_t *rq, + size_t *index, + int *x, + int *y) +{ + /* We don't currently support multiline, so y is always 0 */ + *y = 0; + *x = 0; + + if (rq == NULL) + return false; + + if (rq->text_utf8) + *index = _raqm_u8_to_u32_index (rq, *index); + + if (*index >= rq->text_len) + return false; + + RAQM_TEST ("\n"); + + while (*index < rq->text_len) + { + if (_raqm_allowed_grapheme_boundary (rq->text[*index], rq->text[*index + 1])) + break; + + ++*index; + } + + for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) + { + size_t len; + hb_glyph_info_t *info; + hb_glyph_position_t *position; + len = hb_buffer_get_length (run->buffer); + info = hb_buffer_get_glyph_infos (run->buffer, NULL); + position = hb_buffer_get_glyph_positions (run->buffer, NULL); + + for (size_t i = 0; i < len; i++) + { + uint32_t curr_cluster = info[i].cluster; + uint32_t next_cluster = curr_cluster; + *x += position[i].x_advance; + + if (run->direction == HB_DIRECTION_LTR) + { + for (size_t j = i + 1; j < len && next_cluster == curr_cluster; j++) + next_cluster = info[j].cluster; + } + else + { + for (int j = i - 1; i != 0 && j >= 0 && next_cluster == curr_cluster; + j--) + next_cluster = info[j].cluster; + } + + if (next_cluster == curr_cluster) + next_cluster = run->pos + run->len; + + if (*index < next_cluster && *index >= curr_cluster) + { + if (run->direction == HB_DIRECTION_RTL) + *x -= position[i].x_advance; + *index = curr_cluster; + goto found; + } + } + } + +found: + if (rq->text_utf8) + *index = _raqm_u32_to_u8_index (rq, *index); + RAQM_TEST ("The position is %d at index %zu\n",*x ,*index); + return true; +} + +/** + * raqm_position_to_index: + * @rq: a #raqm_t. + * @x: x position. + * @y: y position. + * @index: (out): output character index. + * + * Returns the @index of the character at @x and @y position within text. + * If the position is outside the text, the last character is chosen as + * @index. + * + * Return value: + * %true if the process was successful, %false in case of error. + * + * Since: 0.2 + */ +bool +raqm_position_to_index (raqm_t *rq, + int x, + int y, + size_t *index) +{ + int delta_x = 0, current_x = 0; + (void)y; + + if (rq == NULL) + return false; + + if (x < 0) /* Get leftmost index */ + { + if (rq->resolved_dir == RAQM_DIRECTION_RTL) + *index = rq->text_len; + else + *index = 0; + return true; + } + + RAQM_TEST ("\n"); + + for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) + { + size_t len; + hb_glyph_info_t *info; + hb_glyph_position_t *position; + len = hb_buffer_get_length (run->buffer); + info = hb_buffer_get_glyph_infos (run->buffer, NULL); + position = hb_buffer_get_glyph_positions (run->buffer, NULL); + + for (size_t i = 0; i < len; i++) + { + delta_x = position[i].x_advance; + if (x < (current_x + delta_x)) + { + bool before = false; + if (run->direction == HB_DIRECTION_LTR) + before = (x < current_x + (delta_x / 2)); + else + before = (x > current_x + (delta_x / 2)); + + if (before) + *index = info[i].cluster; + else + { + uint32_t curr_cluster = info[i].cluster; + uint32_t next_cluster = curr_cluster; + if (run->direction == HB_DIRECTION_LTR) + for (size_t j = i + 1; j < len && next_cluster == curr_cluster; j++) + next_cluster = info[j].cluster; + else + for (int j = i - 1; i != 0 && j >= 0 && next_cluster == curr_cluster; + j--) + next_cluster = info[j].cluster; + + if (next_cluster == curr_cluster) + next_cluster = run->pos + run->len; + + *index = next_cluster; + } + if (_raqm_allowed_grapheme_boundary (rq->text[*index],rq->text[*index + 1])) + { + RAQM_TEST ("The start-index is %zu at position %d \n", *index, x); + return true; + } + + while (*index < (unsigned)run->pos + run->len) + { + if (_raqm_allowed_grapheme_boundary (rq->text[*index], + rq->text[*index + 1])) + { + *index += 1; + break; + } + *index += 1; + } + RAQM_TEST ("The start-index is %zu at position %d \n", *index, x); + return true; + } + else + current_x += delta_x; + } + } + + /* Get rightmost index*/ + if (rq->resolved_dir == RAQM_DIRECTION_RTL) + *index = 0; + else + *index = rq->text_len; + + RAQM_TEST ("The start-index is %zu at position %d \n", *index, x); + + return true; +} + +typedef enum +{ + RAQM_GRAPHEM_CR, + RAQM_GRAPHEM_LF, + RAQM_GRAPHEM_CONTROL, + RAQM_GRAPHEM_EXTEND, + RAQM_GRAPHEM_REGIONAL_INDICATOR, + RAQM_GRAPHEM_PREPEND, + RAQM_GRAPHEM_SPACING_MARK, + RAQM_GRAPHEM_HANGUL_SYLLABLE, + RAQM_GRAPHEM_OTHER +} _raqm_grapheme_t; + +static _raqm_grapheme_t +_raqm_get_grapheme_break (hb_codepoint_t ch, + hb_unicode_general_category_t category); + +static bool +_raqm_allowed_grapheme_boundary (hb_codepoint_t l_char, + hb_codepoint_t r_char) +{ + hb_unicode_general_category_t l_category; + hb_unicode_general_category_t r_category; + _raqm_grapheme_t l_grapheme, r_grapheme; + hb_unicode_funcs_t* unicode_funcs = hb_unicode_funcs_get_default (); + + l_category = hb_unicode_general_category (unicode_funcs, l_char); + r_category = hb_unicode_general_category (unicode_funcs, r_char); + l_grapheme = _raqm_get_grapheme_break (l_char, l_category); + r_grapheme = _raqm_get_grapheme_break (r_char, r_category); + + if (l_grapheme == RAQM_GRAPHEM_CR && r_grapheme == RAQM_GRAPHEM_LF) + return false; /*Do not break between a CR and LF GB3*/ + if (l_grapheme == RAQM_GRAPHEM_CONTROL || l_grapheme == RAQM_GRAPHEM_CR || + l_grapheme == RAQM_GRAPHEM_LF || r_grapheme == RAQM_GRAPHEM_CONTROL || + r_grapheme == RAQM_GRAPHEM_CR || r_grapheme == RAQM_GRAPHEM_LF) + return true; /*Break before and after CONTROL GB4, GB5*/ + if (r_grapheme == RAQM_GRAPHEM_HANGUL_SYLLABLE) + return false; /*Do not break Hangul syllable sequences. GB6, GB7, GB8*/ + if (l_grapheme == RAQM_GRAPHEM_REGIONAL_INDICATOR && + r_grapheme == RAQM_GRAPHEM_REGIONAL_INDICATOR) + return false; /*Do not break between regional indicator symbols. GB8a*/ + if (r_grapheme == RAQM_GRAPHEM_EXTEND) + return false; /*Do not break before extending characters. GB9*/ + /*Do not break before SpacingMarks, or after Prepend characters.GB9a, GB9b*/ + if (l_grapheme == RAQM_GRAPHEM_PREPEND) + return false; + if (r_grapheme == RAQM_GRAPHEM_SPACING_MARK) + return false; + return true; /*Otherwise, break everywhere. GB1, GB2, GB10*/ +} + +static _raqm_grapheme_t +_raqm_get_grapheme_break (hb_codepoint_t ch, + hb_unicode_general_category_t category) +{ + _raqm_grapheme_t gb_type; + + gb_type = RAQM_GRAPHEM_OTHER; + switch ((int)category) + { + case HB_UNICODE_GENERAL_CATEGORY_FORMAT: + if (ch == 0x200C || ch == 0x200D) + gb_type = RAQM_GRAPHEM_EXTEND; + else + gb_type = RAQM_GRAPHEM_CONTROL; + break; + + case HB_UNICODE_GENERAL_CATEGORY_CONTROL: + if (ch == 0x000D) + gb_type = RAQM_GRAPHEM_CR; + else if (ch == 0x000A) + gb_type = RAQM_GRAPHEM_LF; + else + gb_type = RAQM_GRAPHEM_CONTROL; + break; + + case HB_UNICODE_GENERAL_CATEGORY_SURROGATE: + case HB_UNICODE_GENERAL_CATEGORY_LINE_SEPARATOR: + case HB_UNICODE_GENERAL_CATEGORY_PARAGRAPH_SEPARATOR: + case HB_UNICODE_GENERAL_CATEGORY_UNASSIGNED: + if ((ch >= 0xFFF0 && ch <= 0xFFF8) || + (ch >= 0xE0000 && ch <= 0xE0FFF)) + gb_type = RAQM_GRAPHEM_CONTROL; + break; + + case HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK: + case HB_UNICODE_GENERAL_CATEGORY_ENCLOSING_MARK: + case HB_UNICODE_GENERAL_CATEGORY_SPACING_MARK: + if (ch != 0x102B && ch != 0x102C && ch != 0x1038 && + (ch < 0x1062 || ch > 0x1064) && (ch < 0x1067 || ch > 0x106D) && + ch != 0x1083 && (ch < 0x1087 || ch > 0x108C) && ch != 0x108F && + (ch < 0x109A || ch > 0x109C) && ch != 0x1A61 && ch != 0x1A63 && + ch != 0x1A64 && ch != 0xAA7B && ch != 0xAA70 && ch != 0x11720 && + ch != 0x11721) /**/ + gb_type = RAQM_GRAPHEM_SPACING_MARK; + + else if (ch == 0x09BE || ch == 0x09D7 || + ch == 0x0B3E || ch == 0x0B57 || ch == 0x0BBE || ch == 0x0BD7 || + ch == 0x0CC2 || ch == 0x0CD5 || ch == 0x0CD6 || + ch == 0x0D3E || ch == 0x0D57 || ch == 0x0DCF || ch == 0x0DDF || + ch == 0x1D165 || (ch >= 0x1D16E && ch <= 0x1D172)) + gb_type = RAQM_GRAPHEM_EXTEND; + break; + + case HB_UNICODE_GENERAL_CATEGORY_OTHER_LETTER: + if (ch == 0x0E33 || ch == 0x0EB3) + gb_type = RAQM_GRAPHEM_SPACING_MARK; + break; + + case HB_UNICODE_GENERAL_CATEGORY_OTHER_SYMBOL: + if (ch >= 0x1F1E6 && ch <= 0x1F1FF) + gb_type = RAQM_GRAPHEM_REGIONAL_INDICATOR; + break; + + default: + gb_type = RAQM_GRAPHEM_OTHER; + break; + } + + if (_raqm_in_hangul_syllable (ch)) + gb_type = RAQM_GRAPHEM_HANGUL_SYLLABLE; + + return gb_type; +} + +static bool +_raqm_in_hangul_syllable (hb_codepoint_t ch) +{ + (void)ch; + return false; +} + +/** + * raqm_version: + * @major: (out): Library major version component. + * @minor: (out): Library minor version component. + * @micro: (out): Library micro version component. + * + * Returns library version as three integer components. + * + * Since: 0.7 + **/ +void +raqm_version (unsigned int *major, + unsigned int *minor, + unsigned int *micro) +{ + *major = RAQM_VERSION_MAJOR; + *minor = RAQM_VERSION_MINOR; + *micro = RAQM_VERSION_MICRO; +} + +/** + * raqm_version_string: + * + * Returns library version as a string with three components. + * + * Return value: library version string. + * + * Since: 0.7 + **/ +const char * +raqm_version_string (void) +{ + return RAQM_VERSION_STRING; +} + +/** + * raqm_version_atleast: + * @major: Library major version component. + * @minor: Library minor version component. + * @micro: Library micro version component. + * + * Checks if library version is less than or equal the specified version. + * + * Return value: + * %true if library version is less than or equal the specfied version, %false + * otherwise. + * + * Since: 0.7 + **/ +bool +raqm_version_atleast (unsigned int major, + unsigned int minor, + unsigned int micro) +{ + return RAQM_VERSION_ATLEAST (major, minor, micro); +} + +/** + * RAQM_VERSION_ATLEAST: + * @major: Library major version component. + * @minor: Library minor version component. + * @micro: Library micro version component. + * + * Checks if library version is less than or equal the specified version. + * + * Return value: + * %true if library version is less than or equal the specfied version, %false + * otherwise. + * + * Since: 0.7 + **/ + +/** + * RAQM_VERSION_STRING: + * + * Library version as a string with three components. + * + * Since: 0.7 + **/ + +/** + * RAQM_VERSION_MAJOR: + * + * Library major version component. + * + * Since: 0.7 + **/ + +/** + * RAQM_VERSION_MINOR: + * + * Library minor version component. + * + * Since: 0.7 + **/ + +/** + * RAQM_VERSION_MICRO: + * + * Library micro version component. + * + * Since: 0.7 + **/ diff --git a/src/libImaging/raqm.h b/src/thirdparty/raqm/raqm.h similarity index 76% rename from src/libImaging/raqm.h rename to src/thirdparty/raqm/raqm.h index eae1f43b7ea..bdb5a50d884 100644 --- a/src/libImaging/raqm.h +++ b/src/thirdparty/raqm/raqm.h @@ -1,6 +1,6 @@ /* * Copyright © 2015 Information Technology Authority (ITA) - * Copyright © 2016 Khaled Hosny + * Copyright © 2016-2022 Khaled Hosny * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -24,17 +24,18 @@ #ifndef _RAQM_H_ #define _RAQM_H_ +#define _RAQM_H_IN_ #ifdef HAVE_CONFIG_H #include "config.h" #endif -#ifndef bool -typedef int bool; -#endif -#ifndef uint32_t -typedef UINT32 uint32_t; +#ifndef RAQM_API +#define RAQM_API #endif + +#include +#include #include #include FT_FREETYPE_H @@ -42,6 +43,8 @@ typedef UINT32 uint32_t; extern "C" { #endif +#include "raqm-version.h" + /** * raqm_t: * @@ -94,87 +97,109 @@ typedef struct raqm_glyph_t { FT_Face ftface; } raqm_glyph_t; -/** - * version 0.1 of the raqm_glyph_t structure - */ -typedef struct raqm_glyph_t_01 { - unsigned int index; - int x_advance; - int y_advance; - int x_offset; - int y_offset; - uint32_t cluster; -} raqm_glyph_t_01; - - -raqm_t * +RAQM_API raqm_t * raqm_create (void); -raqm_t * +RAQM_API raqm_t * raqm_reference (raqm_t *rq); -void +RAQM_API void raqm_destroy (raqm_t *rq); -bool +RAQM_API void +raqm_clear_contents (raqm_t *rq); + +RAQM_API bool raqm_set_text (raqm_t *rq, const uint32_t *text, size_t len); -bool +RAQM_API bool raqm_set_text_utf8 (raqm_t *rq, const char *text, size_t len); -bool +RAQM_API bool raqm_set_par_direction (raqm_t *rq, raqm_direction_t dir); -bool +RAQM_API bool raqm_set_language (raqm_t *rq, const char *lang, size_t start, size_t len); -bool +RAQM_API bool raqm_add_font_feature (raqm_t *rq, const char *feature, int len); -bool +RAQM_API bool raqm_set_freetype_face (raqm_t *rq, FT_Face face); -bool +RAQM_API bool raqm_set_freetype_face_range (raqm_t *rq, FT_Face face, size_t start, size_t len); -bool +RAQM_API bool raqm_set_freetype_load_flags (raqm_t *rq, int flags); -bool +RAQM_API bool +raqm_set_freetype_load_flags_range (raqm_t *rq, + int flags, + size_t start, + size_t len); + +RAQM_API bool +raqm_set_invisible_glyph (raqm_t *rq, + int gid); + +RAQM_API bool raqm_layout (raqm_t *rq); -raqm_glyph_t * +RAQM_API raqm_glyph_t * raqm_get_glyphs (raqm_t *rq, size_t *length); -bool +RAQM_API raqm_direction_t +raqm_get_par_resolved_direction (raqm_t *rq); + +RAQM_API raqm_direction_t +raqm_get_direction_at_index (raqm_t *rq, + size_t index); + +RAQM_API bool raqm_index_to_position (raqm_t *rq, size_t *index, int *x, int *y); -bool +RAQM_API bool raqm_position_to_index (raqm_t *rq, int x, int y, size_t *index); +RAQM_API void +raqm_version (unsigned int *major, + unsigned int *minor, + unsigned int *micro); + +RAQM_API const char * +raqm_version_string (void); + +RAQM_API bool +raqm_version_atleast (unsigned int major, + unsigned int minor, + unsigned int micro); + + #ifdef __cplusplus } #endif +#undef _RAQM_H_IN_ #endif /* _RAQM_H_ */ diff --git a/tox.ini b/tox.ini index 67ce46bff6e..21b5d4b506d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,32 @@ # 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, "pip install tox" -# and then run "tox" from this directory. +# test suite on all supported python versions. To use it, +# "python3 -m pip install tox" and then run "tox" from this directory. [tox] -envlist = py27, py34, py35, py36 +envlist = + lint + py{37,38,39,310,311,py3} +minversion = 1.9 [testenv] +extras = + tests commands = - {envpython} setup.py clean - {envpython} setup.py build_ext --inplace + make clean + {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . {envpython} selftest.py - {envpython} -m pytest -qq + {envpython} -m pytest -W always {posargs} +deps = + cffi + numpy + +[testenv:lint] +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/.gitignore b/winbuild/.gitignore deleted file mode 100644 index adc679fa889..00000000000 --- a/winbuild/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -*.zip -*.tar.gz -*.msi -*.asc -__pycache__ -depends/ \ No newline at end of file diff --git a/winbuild/README.md b/winbuild/README.md index 5ecbf18c6d1..d8538fbf392 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -1,18 +1,29 @@ -Quick README ------------- - -For more extensive info, see the Windows build instructions `docs/build.rst`. - -* See https://github.com/python-pillow/Pillow/issues/553#issuecomment-37877416 and https://github.com/matplotlib/matplotlib/issues/1717#issuecomment-13343859 - -* Works best with Python 3.4, due to virtualenv and pip batteries included. Python3+ required for fetch command. -* Check config.py for virtual env paths, suffix for 64-bit releases. Defaults to `x64`, set `X64_EXT` to change. -* When running in CI with one Python per invocation, set the `PYTHON` env variable to the Python folder. (e.g. `PYTHON`=`c:\Python27\`) This overrides the matrix in config.py and will just build and test for the specific Python. -* `python get_pythons.py` downloads all the Python releases, and their signatures. (Manually) Install in `c:\PythonXX[x64]\`. -* `python build_dep.py` downloads and creates a build script for all the dependencies, in 32 and 64-bit versions, and with both compiler versions. -* (in powershell) `build_deps.cmd` invokes the dependency build. -* `python build.py --clean` makes Pillow for the matrix of Pythons. -* `python test.py` runs the tests on Pillow in all the virtual envs. -* Currently working with zlib, libjpeg, freetype, and libtiff on Python 2.7, and 3.4, both 32 and 64-bit, on a local win7 pro machine and appveyor.com -* WebP is built, not detected. -* LCMS, OpenJPEG and libimagequant are not building. +Quick README +------------ + +For more extensive info, see the [Windows build instructions](build.rst). + +* See [Current Windows Build/Testing process (Pillow#553)](https://github.com/python-pillow/Pillow/issues/553#issuecomment-37877416), + [Definitive docs for how to compile on Windows (matplotlib#1717)](https://github.com/matplotlib/matplotlib/issues/1717#issuecomment-13343859), + [Test Windows with GitHub Actions (Pillow#4084)](https://github.com/python-pillow/Pillow/pull/4084). + + +* Requires Microsoft Visual Studio 2017 or newer with C++ component. +* Requires NASM for libjpeg-turbo, a required dependency when using this script. +* Requires CMake 3.12 or newer (available as Visual Studio component). +* Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor). +* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). + +The following is a simplified version of the script used on AppVeyor: +``` +set PYTHON=C:\Python38\bin +cd /D C:\Pillow\winbuild +C:\Python37\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends +build\build_dep_all.cmd +build\build_pillow.cmd install +cd .. +path C:\Pillow\winbuild\build\bin;%PATH% +%PYTHON%\python.exe selftest.py +%PYTHON%\python.exe -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests +build\build_pillow.cmd bdist_wheel +``` diff --git a/winbuild/appveyor_build_msys2.sh b/winbuild/appveyor_build_msys2.sh deleted file mode 100644 index 489f9411eac..00000000000 --- a/winbuild/appveyor_build_msys2.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -cd /c/pillow && /mingw32/$EXECUTABLE setup.py install diff --git a/winbuild/appveyor_install_msys2_deps.sh b/winbuild/appveyor_install_msys2_deps.sh deleted file mode 100644 index b13dc9e98b0..00000000000 --- a/winbuild/appveyor_install_msys2_deps.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -pacman -S --noconfirm mingw32/mingw-w64-i686-python3 \ - mingw32/mingw-w64-i686-python3-pip \ - mingw32/mingw-w64-i686-python3-setuptools \ - mingw32/mingw-w64-i686-python2-pip \ - mingw32/mingw-w64-i686-python2-setuptools \ - mingw-w64-i686-libjpeg-turbo - -/mingw32/bin/pip install pytest pytest-cov olefile -/mingw32/bin/pip3 install pytest pytest-cov olefile diff --git a/winbuild/appveyor_install_pypy.cmd b/winbuild/appveyor_install_pypy.cmd deleted file mode 100644 index 39bf6678275..00000000000 --- a/winbuild/appveyor_install_pypy.cmd +++ /dev/null @@ -1,3 +0,0 @@ -curl -fsSL -o pypy2.zip https://bitbucket.org/pypy/pypy/downloads/pypy2-v5.8.0-win32.zip -7z x pypy2.zip -oc:\ -c:\Python34\Scripts\virtualenv.exe -p c:\pypy2-v5.8.0-win32\pypy.exe c:\vp\pypy2 \ No newline at end of file diff --git a/winbuild/build.py b/winbuild/build.py deleted file mode 100755 index 859a4027787..00000000000 --- a/winbuild/build.py +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env python3 - -import subprocess -import shutil -import sys -import getopt -import os - -from config import (compilers, compiler_from_env, pythons, pyversion_from_env, - VIRT_BASE, X64_EXT) - - -def setup_vms(): - ret = [] - for py in pythons: - for arch in ('', X64_EXT): - ret.append("virtualenv -p c:/Python%s%s/python.exe --clear %s%s%s" - % (py, arch, VIRT_BASE, py, arch)) - ret.append(r"%s%s%s\Scripts\pip.exe install pytest pytest-cov" % - (VIRT_BASE, py, arch)) - return "\n".join(ret) - - -def run_script(params): - (version, script) = params - try: - print("Running %s" % version) - filename = 'build_pillow_%s.cmd' % version - with open(filename, 'w') as f: - f.write(script) - - command = ['powershell', "./%s" % filename] - proc = subprocess.Popen(command, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - (trace, stderr) = proc.communicate() - status = proc.returncode - print("-- stderr --") - print(stderr) - print("-- stdout --") - print(trace) - print("Done with %s: %s" % (version, status)) - return (version, status, trace, stderr) - except Exception as msg: - print("Error with %s: %s" % (version, str(msg))) - return (version, -1, "", str(msg)) - - -def header(op): - return r""" -setlocal -set MPLSRC=%%~dp0\.. -set INCLIB=%%~dp0\depends -set BLDOPT=%s -cd /D %%MPLSRC%% -""" % (op) - - -def footer(): - return """endlocal -exit -""" - - -def build_one(py_ver, compiler): - # UNDONE virtual envs if we're not running on appveyor - args = {} - args.update(compiler) - if 'PYTHON' in os.environ: - args['python_path'] = "%PYTHON%" - else: - args['python_path'] = "%s%s\\Scripts" % (VIRT_BASE, py_ver) - - args['executable'] = "python.exe" - if 'EXECUTABLE' in os.environ: - args['executable'] = "%EXECUTABLE%" - - args['py_ver'] = py_ver - if '34' in py_ver: - args['tcl_ver'] = '86' - else: - args['tcl_ver'] = '85' - - return r""" -setlocal EnableDelayedExpansion -call "%%ProgramFiles%%\Microsoft SDKs\Windows\%(env_version)s\Bin\SetEnv.Cmd" /Release %(env_flags)s -set DISTUTILS_USE_SDK=1 -set LIB=%%LIB%%;%%INCLIB%%\%(inc_dir)s -set INCLUDE=%%INCLUDE%%;%%INCLIB%%\%(inc_dir)s;%%INCLIB%%\tcl%(tcl_ver)s\include - -setlocal -set LIB=%%LIB%%;C:\Python%(py_ver)s\tcl -call %(python_path)s\%(executable)s setup.py %%BLDOPT%% -endlocal - -endlocal -""" % args - - -def clean(): - try: - shutil.rmtree('../build') - except: - # could already be removed - pass - run_script(('virtualenvs', setup_vms())) - - -def main(op): - scripts = [] - - for py_version, compiler_version in pythons.items(): - scripts.append((py_version, - "\n".join([header(op), - build_one(py_version, - compilers[(compiler_version, - 32)]), - footer()]))) - - scripts.append(("%s%s" % (py_version, X64_EXT), - "\n".join([header(op), - build_one("%sx64" % py_version, - compilers[(compiler_version, - 64)]), - footer()]))) - - results = map(run_script, scripts) - - for (version, status, trace, err) in results: - print("Compiled %s: %s" % (version, status and 'ERR' or 'OK')) - - -def run_one(op): - - compiler = compiler_from_env() - py_version = pyversion_from_env() - - run_script((py_version, - "\n".join([header(op), - build_one(py_version, compiler), - footer()]) - )) - - -if __name__ == '__main__': - opts, args = getopt.getopt(sys.argv[1:], '', ['clean', 'dist', 'wheel']) - opts = dict(opts) - - if '--clean' in opts: - clean() - - op = 'install' - if '--dist' in opts: - op = "bdist_wininst --user-access-control=auto" - elif '--wheel' in opts: - op = "bdist_wheel" - - if 'PYTHON' in os.environ: - run_one(op) - else: - main(op) diff --git a/winbuild/build.rst b/winbuild/build.rst index 86c4cf4c736..71666977127 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -1,94 +1,112 @@ Building Pillow on Windows ========================== -.. note:: For most people, the :doc:`installation instructions - ` should be sufficient +.. note:: For most people, the `installation instructions + <../docs/installation.rst#windows-installation>`_ should + be sufficient. -This page will describe a build setup to build Pillow against the -supported Python versions in 32 and 64-bit modes, using freely -available Microsoft compilers. This has been developed and tested -against 64-bit Windows 7 Professional and Windows Server 2012 -64-bit version on Amazon EC2. +This page describes the steps necessary to build Pillow using the same +scripts used on GitHub Actions and AppVeyor CIs. Prerequisites ------------- -Extra Build Helpers -^^^^^^^^^^^^^^^^^^^ - -* Powershell (available by default on Windows Server) -* GitHub client (provides git+bash shell) - -Optional: -* GPG (for checking signatures) (UNDONE -- Python signature checking) +Compilers +^^^^^^^^^ -Pythons -^^^^^^^ +Download and install: -The build routines expect Python to be installed at C:\PythonXX for -32-bit versions or C:\PythonXXx64 for the 64-bit versions. +* `Microsoft Visual Studio 2017 or newer or Build Tools for Visual Studio 2017 or newer + `_ + (MSVC C++ build tools, and any Windows SDK version required) -Download Python 3.4, install it, and add it to the path. This is the -Python that we will use to bootstrap the build process. (The download -routines are using 3 features, and installing 3.4 gives us pip and -virtualenv as well, reducing the number of packages that we need to -install.) +* `CMake 3.12 or newer `_ + (also available as Visual Studio component C++ CMake tools for Windows) -Download the rest of the Pythons by opening a command window, changing -to the `winbuild` directory, and running `python -get_pythons.py`. +* x86/x64: `NASM `_ -UNDONE -- gpg verify the signatures (note that we can download from -https) +Any version of Visual Studio 2017 or newer should be supported, +including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. -Run each installer and set the proper path to the installation. Don't -set any of them as the default Python, or add them to the path. +Paths to CMake (if standalone) and NASM must be added to the ``PATH`` environment variable. +Visual Studio is found automatically with ``vswhere.exe``. +Build configuration +------------------- -Compilers -^^^^^^^^^ +The following environment variables, if set, will override the default +behaviour of ``build_prepare.py``: -Download and install: +* ``PYTHON`` + ``EXECUTABLE`` point to the target version of Python. + If ``PYTHON`` is unset, the version of Python used to run + ``build_prepare.py`` will be used. If only ``PYTHON`` is set, + ``EXECUTABLE`` defaults to ``python.exe``. +* ``ARCHITECTURE`` is used to select a ``x86``, ``x64`` or ``ARM64`` build. + By default, uses same architecture as the version of Python used to run ``build_prepare.py``. +* ``PILLOW_BUILD`` can be used to override the ``winbuild\build`` directory + path, used to store generated build scripts and compiled libraries. + **Warning:** This directory is wiped when ``build_prepare.py`` is run. +* ``PILLOW_DEPS`` points to the directory used to store downloaded + dependencies. By default ``winbuild\depends`` is used. -* `Microsoft Windows SDK for Windows 7 and .NET Framework 3.5 - SP1 `_ +``build_prepare.py`` also supports the following command line parameters: -* `Microsoft Windows SDK for Windows 7 and .NET Framework - 4 `_ - -* `CMake-2.8.10.2-win32-x86.exe `_ - -The samples and the .NET SDK portions aren't required, just the -compilers and other tools. UNDONE -- check exact wording. +* ``-v`` will print generated scripts. +* ``--no-imagequant`` will skip GPL-licensed ``libimagequant`` optional dependency +* ``--no-fribidi`` or ``--no-raqm`` will skip optional LGPL-licensed dependency FriBiDi + (required for Raqm text shaping). +* ``--python=`` and ``--executable=`` override ``PYTHON`` and ``EXECUTABLE``. +* ``--architecture=`` overrides ``ARCHITECTURE``. +* ``--dir=`` and ``--depends=`` override ``PILLOW_BUILD`` + and ``PILLOW_DEPS``. Dependencies ------------ -The script 'build_dep.py' downloads and builds the dependencies. Open -a command window, change directory into `winbuild` and run `python -build_dep.py`. +Dependencies will be automatically downloaded by ``build_prepare.py``. +By default, downloaded dependencies are stored in ``winbuild\depends``; +set the ``PILLOW_DEPS`` environment variable to override this location. -This will download libjpeg, libtiff, libz, and freetype. It will then -compile 32 and 64-bit versions of the libraries, with both versions of -the compilers. - -UNDONE -- lcms fails. -UNDONE -- webp, jpeg2k not recognized +To build all dependencies, run ``winbuild\build\build_dep_all.cmd``, +or run the individual scripts to build each dependency separately. Building Pillow --------------- -Once the dependencies are built, run `python build.py --clean` to -build and install Pillow in virtualenvs for each python -build. `build.py --dist` will build Windows installers instead of -installing into virtualenvs. +Once the dependencies are built, run +``winbuild\build\build_pillow.cmd install`` to build and install +Pillow for the selected version of Python. +``winbuild\build\build_pillow.cmd bdist_wheel`` will build wheels +instead of installing Pillow. -UNDONE -- suppressed output, what about failures. +You can also use ``winbuild\build\build_pillow.cmd --inplace develop`` to build +and install Pillow in develop mode (instead of ``python3 -m pip install --editable``). Testing Pillow -------------- -Build and install Pillow, then run `python test.py` from the -`winbuild` directory. +Some binary dependencies (e.g. ``fribidi.dll``) will be stored in the +``winbuild\build\bin`` directory; this directory should be added to ``PATH`` +before running tests. + +Build and install Pillow, then run ``python3 -m pytest`` from the root Pillow +directory. + +Example +------- + +The following is a simplified version of the script used on AppVeyor: + +.. code-block:: + set PYTHON=C:\Python38\bin + cd /D C:\Pillow\winbuild + C:\Python37\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends + build\build_dep_all.cmd + build\build_pillow.cmd install + cd .. + path C:\Pillow\winbuild\build\bin;%PATH% + %PYTHON%\python.exe selftest.py + %PYTHON%\python.exe -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests + build\build_pillow.cmd bdist_wheel diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py deleted file mode 100644 index a059b1ee8b1..00000000000 --- a/winbuild/build_dep.py +++ /dev/null @@ -1,301 +0,0 @@ -from unzip import unzip -from untar import untar -import os - -from config import compilers, compiler_from_env, libs - - -def _relpath(*args): - return os.path.join(os.getcwd(), *args) - - -build_dir = _relpath('build') -inc_dir = _relpath('depends') - - -def check_sig(filename, signame): - # UNDONE -- need gpg - return filename - - -def mkdirs(): - try: - os.mkdir(build_dir) - except OSError: - pass - try: - os.mkdir(inc_dir) - except OSError: - pass - for compiler in compilers.values(): - try: - os.mkdir(os.path.join(inc_dir, compiler['inc_dir'])) - except OSError: - pass - - -def extract(src, dest): - if '.zip' in src: - return unzip(src, dest) - if '.tar.gz' in src or '.tgz' in src: - return untar(src, dest) - - -def extract_libs(): - for name, lib in libs.items(): - filename = lib['filename'] - if name == 'openjpeg': - for compiler in compilers.values(): - if not os.path.exists(os.path.join( - build_dir, lib['dir']+compiler['inc_dir'])): - extract(filename, build_dir) - os.rename(os.path.join(build_dir, lib['dir']), - os.path.join( - build_dir, lib['dir']+compiler['inc_dir'])) - else: - extract(filename, build_dir) - - -def extract_openjpeg(compiler): - return r""" -rem build openjpeg -setlocal -@echo on -cd %%BUILD%% -mkdir %%INCLIB%%\openjpeg-2.0 -copy /Y /B openjpeg-2.0.0-win32-x86\include\openjpeg-2.0 %%INCLIB%%\openjpeg-2.0 -copy /Y /B openjpeg-2.0.0-win32-x86\bin\ %%INCLIB%% -copy /Y /B openjpeg-2.0.0-win32-x86\lib\ %%INCLIB%% -endlocal -""" % compiler - - -def cp_tk(ver_85, ver_86): - versions = {'ver_85': ver_85, 'ver_86': ver_86} - return r""" -mkdir %%INCLIB%%\tcl85\include\X11 -copy /Y /B %%BUILD%%\tcl%(ver_85)s\generic\*.h %%INCLIB%%\tcl85\include\ -copy /Y /B %%BUILD%%\tk%(ver_85)s\generic\*.h %%INCLIB%%\tcl85\include\ -copy /Y /B %%BUILD%%\tk%(ver_85)s\xlib\X11\* %%INCLIB%%\tcl85\include\X11\ - -mkdir %%INCLIB%%\tcl86\include\X11 -copy /Y /B %%BUILD%%\tcl%(ver_86)s\generic\*.h %%INCLIB%%\tcl86\include\ -copy /Y /B %%BUILD%%\tk%(ver_86)s\generic\*.h %%INCLIB%%\tcl86\include\ -copy /Y /B %%BUILD%%\tk%(ver_86)s\xlib\X11\* %%INCLIB%%\tcl86\include\X11\ -""" % versions - - -def header(): - return r"""setlocal -set MSBUILD=C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe -set CMAKE="cmake.exe" -set INCLIB=%~dp0\depends -set BUILD=%~dp0\build -""" + "\n".join(r'set %s=%%BUILD%%\%s' % (k.upper(), v['dir']) - for (k, v) in libs.items() if v['dir']) - - -def setup_compiler(compiler): - return r"""setlocal EnableDelayedExpansion -call "%%ProgramFiles%%\Microsoft SDKs\Windows\%(env_version)s\Bin\SetEnv.Cmd" /Release %(env_flags)s -set INCLIB=%%INCLIB%%\%(inc_dir)s -""" % compiler - - -def end_compiler(): - return """ -endlocal -""" - - -def nmake_openjpeg(compiler): - atts = {'op_ver': '2.1'} - atts.update(compiler) - return r""" -rem build openjpeg -setlocal -@echo on -cd /D %%OPENJPEG%%%(inc_dir)s -%%CMAKE%% -DBUILD_THIRDPARTY:BOOL=OFF -G "NMake Makefiles" . -nmake -f Makefile clean -nmake -f Makefile -copy /Y /B bin\* %%INCLIB%% -mkdir %%INCLIB%%\openjpeg-%(op_ver)s -copy /Y /B src\lib\openjp2\*.h %%INCLIB%%\openjpeg-%(op_ver)s -endlocal -""" % atts - - -def msbuild_openjpeg(compiler): - atts = {'op_ver': '2.1'} - atts.update(compiler) - return r""" -rem build openjpeg -setlocal -@echo on -cd /D %%OPENJPEG%%%(inc_dir)s - -%%CMAKE%% -DBUILD_THIRDPARTY:BOOL=OFF -G "NMake Makefiles" . -nmake -f Makefile clean -nmake -f Makefile -copy /Y /B bin\* %%INCLIB%% -mkdir %%INCLIB%%\openjpeg-%(op_ver)s -copy /Y /B src\lib\openjp2\*.h %%INCLIB%%\openjpeg-%(op_ver)s -endlocal -""" % atts - - -def nmake_libs(compiler): - # undone -- pre, makes, headers, libs - return r""" -rem Build libjpeg -setlocal -cd /D %%JPEG%% -nmake -f makefile.vc setup-vc6 -nmake -f makefile.vc clean -nmake -f makefile.vc libjpeg.lib -copy /Y /B *.dll %%INCLIB%% -copy /Y /B *.lib %%INCLIB%% -copy /Y /B j*.h %%INCLIB%% -endlocal - -rem Build zlib -setlocal -cd /D %%ZLIB%% -nmake -f win32\Makefile.msc clean -nmake -f win32\Makefile.msc zlib.lib -copy /Y /B *.dll %%INCLIB%% -copy /Y /B *.lib %%INCLIB%% -copy /Y /B zlib.lib %%INCLIB%%\z.lib -copy /Y /B zlib.h %%INCLIB%% -copy /Y /B zconf.h %%INCLIB%% -endlocal - -rem Build webp -setlocal -cd /D %%WEBP%% -rd /S /Q %%WEBP%%\output\release-static -nmake -f Makefile.vc CFG=release-static RTLIBCFG=static OBJDIR=output all -copy /Y /B output\release-static\%(webp_platform)s\lib\* %%INCLIB%% -mkdir %%INCLIB%%\webp -copy /Y /B src\webp\*.h %%INCLIB%%\\webp -endlocal - -rem Build libtiff -setlocal -rem do after building jpeg and zlib -copy %%~dp0\nmake.opt %%TIFF%% - -cd /D %%TIFF%% -nmake -f makefile.vc clean -nmake -f makefile.vc lib -copy /Y /B libtiff\*.dll %%INCLIB%% -copy /Y /B libtiff\*.lib %%INCLIB%% -copy /Y /B libtiff\tiff*.h %%INCLIB%% -endlocal - - -""" % compiler - - -def msbuild_freetype(compiler): - if compiler['env_version'] == 'v7.1': - return msbuild_freetype_71(compiler) - return msbuild_freetype_70(compiler) - - -def msbuild_freetype_71(compiler): - return r""" -rem Build freetype -setlocal -rd /S /Q %%FREETYPE%%\objs -%%MSBUILD%% %%FREETYPE%%\builds\windows\vc%(vc_version)s\freetype.sln /t:Clean;Build /p:Configuration="Release" /p:Platform=%(platform)s /m -xcopy /Y /E /Q %%FREETYPE%%\include %%INCLIB%% -copy /Y /B %%FREETYPE%%\objs\vc%(vc_version)s\%(platform)s\*.lib %%INCLIB%%\freetype.lib -endlocal -""" % compiler - - -def msbuild_freetype_70(compiler): - return r""" -rem Build freetype -setlocal -py -3 %%~dp0\fixproj.py %%FREETYPE%%\builds\windows\vc%(vc_version)s\freetype.sln %(platform)s -py -3 %%~dp0\fixproj.py %%FREETYPE%%\builds\windows\vc%(vc_version)s\freetype.vcproj %(platform)s -rd /S /Q %%FREETYPE%%\objs -%%MSBUILD%% %%FREETYPE%%\builds\windows\vc%(vc_version)s\freetype.sln /t:Clean;Build /p:Configuration="LIB Release";Platform=%(platform)s /m -xcopy /Y /E /Q %%FREETYPE%%\include %%INCLIB%% -xcopy /Y /E /Q %%FREETYPE%%\objs\win32\vc%(vc_version)s %%INCLIB%% -copy /Y /B %%FREETYPE%%\objs\win32\vc%(vc_version)s\*.lib %%INCLIB%%\freetype.lib -endlocal -""" % compiler - - -def build_lcms2(compiler): - if compiler['env_version'] == 'v7.1': - return build_lcms_71(compiler) - return build_lcms_70(compiler) - - -def build_lcms_70(compiler): - """Link error here on x64""" - if compiler['platform'] == 'x64': - return '' - - """Build LCMS on VC2008. This version is only 32bit/Win32""" - return r""" -rem Build lcms2 -setlocal -rd /S /Q %%LCMS%%\Lib -rd /S /Q %%LCMS%%\Projects\VC%(vc_version)s\Release -%%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:Clean /p:Configuration="Release" /p:Platform=Win32 /m -%%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:lcms2_static /p:Configuration="Release" /p:Platform=Win32 /m -xcopy /Y /E /Q %%LCMS%%\include %%INCLIB%% -copy /Y /B %%LCMS%%\Projects\VC%(vc_version)s\Release\*.lib %%INCLIB%% -endlocal -""" % compiler - - -def build_lcms_71(compiler): - return r""" -rem Build lcms2 -setlocal -rd /S /Q %%LCMS%%\Lib -rd /S /Q %%LCMS%%\Projects\VC%(vc_version)s\Release -%%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:Clean /p:Configuration="Release" /p:Platform=%(platform)s /m -%%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:lcms2_static /p:Configuration="Release" /p:Platform=%(platform)s /m -xcopy /Y /E /Q %%LCMS%%\include %%INCLIB%% -copy /Y /B %%LCMS%%\Lib\MS\*.lib %%INCLIB%% -endlocal -""" % compiler - - -def add_compiler(compiler): - script.append(setup_compiler(compiler)) - script.append(nmake_libs(compiler)) - - # script.append(extract_openjpeg(compiler)) - - script.append(msbuild_freetype(compiler)) - script.append(build_lcms2(compiler)) - # script.append(nmake_openjpeg(compiler)) - script.append(end_compiler()) - - -mkdirs() -extract_libs() -script = [header(), - cp_tk(libs['tk-8.5']['version'], - libs['tk-8.6']['version'])] - - -if 'PYTHON' in os.environ: - add_compiler(compiler_from_env()) -else: - # for compiler in compilers.values(): - # add_compiler(compiler) - add_compiler(compilers[(7.0, 32)]) - # add_compiler(compilers[(7.1, 64)]) - -with open('build_deps.cmd', 'w') as f: - f.write("\n".join(script)) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py new file mode 100644 index 00000000000..b4b15cc1ef0 --- /dev/null +++ b/winbuild/build_prepare.py @@ -0,0 +1,686 @@ +import os +import platform +import re +import shutil +import struct +import subprocess +import sys + + +def cmd_cd(path): + return f"cd /D {path}" + + +def cmd_set(name, value): + return f"set {name}={value}" + + +def cmd_append(name, value): + op = "path " if name == "PATH" else f"set {name}=" + return op + f"%{name}%;{value}" + + +def cmd_copy(src, tgt): + return f'copy /Y /B "{src}" "{tgt}"' + + +def cmd_xcopy(src, tgt): + return f'xcopy /Y /E "{src}" "{tgt}"' + + +def cmd_mkdir(path): + return f'mkdir "{path}"' + + +def cmd_rmdir(path): + return f'rmdir /S /Q "{path}"' + + +def cmd_nmake(makefile=None, target="", params=None): + if params is None: + params = "" + elif isinstance(params, list) or isinstance(params, tuple): + params = " ".join(params) + else: + params = str(params) + + return " ".join( + [ + "{nmake}", + "-nologo", + f'-f "{makefile}"' if makefile is not None else "", + f"{params}", + f'"{target}"', + ] + ) + + +def cmd_cmake(params=None, file="."): + if params is None: + params = "" + elif isinstance(params, list) or isinstance(params, tuple): + params = " ".join(params) + else: + params = str(params) + return " ".join( + [ + "{cmake}", + "-DCMAKE_VERBOSE_MAKEFILE=ON", + "-DCMAKE_RULE_MESSAGES:BOOL=OFF", + "-DCMAKE_BUILD_TYPE=Release", + f"{params}", + '-G "NMake Makefiles"', + f'"{file}"', + ] + ) + + +def cmd_msbuild( + file, configuration="Release", target="Build", platform="{msbuild_arch}" +): + return " ".join( + [ + "{msbuild}", + f"{file}", + f'/t:"{target}"', + f'/p:Configuration="{configuration}"', + f"/p:Platform={platform}", + "/m", + ] + ) + + +SF_PROJECTS = "https://sourceforge.net/projects" + +architectures = { + "x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"}, + "x64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, + "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, +} + +header = [ + cmd_set("INCLUDE", "{inc_dir}"), + cmd_set("INCLIB", "{lib_dir}"), + cmd_set("LIB", "{lib_dir}"), + cmd_append("PATH", "{bin_dir}"), +] + +# dependencies, listed in order of compilation +deps = { + "libjpeg": { + "url": SF_PROJECTS + + "/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( + [ + "-DENABLE_SHARED:BOOL=FALSE", + "-DWITH_JPEG8:BOOL=TRUE", + "-DWITH_CRT_DLL:BOOL=TRUE", + ] + ), + cmd_nmake(target="clean"), + cmd_nmake(target="jpeg-static"), + cmd_copy("jpeg-static.lib", "libjpeg.lib"), + cmd_nmake(target="cjpeg-static"), + cmd_copy("cjpeg-static.exe", "cjpeg.exe"), + cmd_nmake(target="djpeg-static"), + cmd_copy("djpeg-static.exe", "djpeg.exe"), + ], + "headers": ["j*.h"], + "libs": ["libjpeg.lib"], + "bins": ["cjpeg.exe", "djpeg.exe"], + }, + "zlib": { + "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"), + cmd_copy("zlib.lib", "z.lib"), + ], + "headers": [r"z*.h"], + "libs": [r"*.lib"], + }, + "xz": { + "url": SF_PROJECTS + "/lzmautils/files/xz-5.2.7.tar.gz/download", + "filename": "xz-5.2.7.tar.gz", + "dir": "xz-5.2.7", + "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", + "license": "COPYRIGHT", + "patch": { + r"cmake\LZMACodec.cmake": { + # fix typo + "${{LZMA_FOUND}}": "${{LIBLZMA_FOUND}}", + }, + 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"), + cmd_nmake(target="tiff"), + ], + "headers": [r"libtiff\tiff*.h"], + "libs": [r"libtiff\*.lib"], + # "bins": [r"libtiff\*.dll"], + }, + "libpng": { + "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.38/lpng1638.zip/download", + "filename": "lpng1638.zip", + "dir": "lpng1638", + "license": "LICENSE", + "build": [ + # lint: do not inline + cmd_cmake(("-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF")), + cmd_nmake(target="clean"), + cmd_nmake(), + cmd_copy("libpng16_static.lib", "libpng16.lib"), + ], + "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 + "MultiThreaded": "MultiThreadedDLL", # noqa: E501 + # freetype doesn't specify SDK version, MSBuild may guess incorrectly + '': '\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;FT_CONFIG_OPTION_USE_BROTLI", # noqa: E501 + "": r"{dir_harfbuzz}\src;{inc_dir}", # noqa: E501 + "": "{lib_dir}", # noqa: E501 + "": "zlib.lib;libpng16.lib;brotlicommon-static.lib;brotlidec-static.lib", # noqa: E501 + }, + r"src/autofit/afshaper.c": { + # link against harfbuzz.lib + "#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ": '#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ\n#pragma comment(lib, "harfbuzz.lib")', # noqa: E501 + }, + }, + "build": [ + cmd_rmdir("objs"), + cmd_msbuild( + r"builds\windows\vc2010\freetype.sln", "Release Static", "Clean" + ), + cmd_msbuild( + r"builds\windows\vc2010\freetype.sln", "Release Static", "Build" + ), + cmd_xcopy("include", "{inc_dir}"), + ], + "libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"], + # "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", + "license": "COPYING", + "patch": { + r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { + # default is /MD for x86 and /MT for x64, we need /MD always + "MultiThreaded": "MultiThreadedDLL", # noqa: E501 + # retarget to default toolset (selected by vcvarsall.bat) + "v143": "$(DefaultPlatformToolset)", # noqa: E501 + # retarget to latest (selected by vcvarsall.bat) + "10.0": "$(WindowsSDKVersion)", # noqa: E501 + } + }, + "build": [ + cmd_rmdir("Lib"), + cmd_rmdir(r"Projects\VC2022\Release"), + cmd_msbuild(r"Projects\VC2022\lcms2.sln", "Release", "Clean"), + cmd_msbuild( + r"Projects\VC2022\lcms2.sln", "Release", "lcms2_static:Rebuild" + ), + cmd_xcopy("include", "{inc_dir}"), + ], + "libs": [r"Lib\MS\*.lib"], + }, + "openjpeg": { + "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", + "build": [ + 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"), + cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.0"), + ], + "libs": [r"bin\*.lib"], + }, + "libimagequant": { + # commit: Merge branch 'master' into msvc (matches 2.17.0 tag) + "url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip", # noqa: E501 + "filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip", + "dir": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab", + "license": "COPYRIGHT", + "patch": { + "CMakeLists.txt": { + "if(OPENMP_FOUND)": "if(false)", + "install": "#install", + } + }, + "build": [ + # lint: do not inline + cmd_cmake(), + cmd_nmake(target="clean"), + cmd_nmake(target="imagequant_a"), + cmd_copy("imagequant_a.lib", "imagequant.lib"), + ], + "headers": [r"*.h"], + "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", + "license": "COPYING", + "build": [ + cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), + cmd_nmake(target="clean"), + cmd_nmake(target="harfbuzz"), + ], + "headers": [r"src\*.h"], + "libs": [r"*.lib"], + }, + "fribidi": { + "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"), + cmd_nmake(target="fribidi"), + ], + "bins": [r"*.dll"], + }, +} + + +# based on distutils._msvccompiler from CPython 3.7.4 +def find_msvs(): + root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles") + if not root: + print("Program Files not found") + return None + + try: + vspath = ( + subprocess.check_output( + [ + os.path.join( + root, "Microsoft Visual Studio", "Installer", "vswhere.exe" + ), + "-latest", + "-prerelease", + "-requires", + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "-property", + "installationPath", + "-products", + "*", + ] + ) + .decode(encoding="mbcs") + .strip() + ) + except (subprocess.CalledProcessError, OSError, UnicodeDecodeError): + print("vswhere not found") + return None + + if not os.path.isdir(os.path.join(vspath, "VC", "Auxiliary", "Build")): + print("Visual Studio seems to be missing C compiler") + return None + + vs = { + "header": [], + # nmake selected by vcvarsall + "nmake": "nmake.exe", + "vs_dir": vspath, + } + + # vs2017 + msbuild = os.path.join(vspath, "MSBuild", "15.0", "Bin", "MSBuild.exe") + if os.path.isfile(msbuild): + vs["msbuild"] = f'"{msbuild}"' + else: + # vs2019 + msbuild = os.path.join(vspath, "MSBuild", "Current", "Bin", "MSBuild.exe") + if os.path.isfile(msbuild): + vs["msbuild"] = f'"{msbuild}"' + else: + print("Visual Studio MSBuild not found") + return None + + vcvarsall = os.path.join(vspath, "VC", "Auxiliary", "Build", "vcvarsall.bat") + if not os.path.isfile(vcvarsall): + print("Visual Studio vcvarsall not found") + return None + vs["header"].append(f'call "{vcvarsall}" {{vcvars_arch}}') + + return vs + + +def extract_dep(url, filename): + import tarfile + import urllib.request + import zipfile + + file = os.path.join(depends_dir, filename) + if not os.path.exists(file): + ex = None + for i in range(3): + try: + print("Fetching %s (attempt %d)..." % (url, i + 1)) + content = urllib.request.urlopen(url).read() + with open(file, "wb") as f: + f.write(content) + break + except urllib.error.URLError as e: + ex = e + else: + raise RuntimeError(ex) + + print("Extracting " + filename) + if filename.endswith(".zip"): + with zipfile.ZipFile(file) as zf: + zf.extractall(sources_dir) + elif filename.endswith(".tar.gz") or filename.endswith(".tgz"): + with tarfile.open(file, "r:gz") as tgz: + tgz.extractall(sources_dir) + else: + raise RuntimeError("Unknown archive type: " + filename) + + +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", newline="") as f: + f.write(os.linesep.join(lines)) + if verbose: + for line in lines: + print(" " + line) + + +def get_footer(dep): + lines = [] + for out in dep.get("headers", []): + lines.append(cmd_copy(out, "{inc_dir}")) + for out in dep.get("libs", []): + lines.append(cmd_copy(out, "{lib_dir}")) + for out in dep.get("bins", []): + lines.append(cmd_copy(out, "{bin_dir}")) + return lines + + +def build_dep(name): + dep = deps[name] + dir = dep["dir"] + file = f"build_dep_{name}.cmd" + + 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: + text = f.read() + for patch_from, patch_to in patch_list.items(): + patch_from = patch_from.format(**prefs) + patch_to = patch_to.format(**prefs) + assert patch_from in text + text = text.replace(patch_from, patch_to) + with open(patch_file, "w") as f: + print(f"Patching {patch_file}") + f.write(text) + + banner = f"Building {name} ({dir})" + lines = [ + "@echo " + ("=" * 70), + f"@echo ==== {banner:<60} ====", + "@echo " + ("=" * 70), + "cd /D %s" % os.path.join(sources_dir, dir), + *prefs["header"], + *dep.get("build", []), + *get_footer(dep), + ] + + write_script(file, lines) + return file + + +def build_dep_all(): + lines = ["@echo on"] + for dep_name in deps: + if dep_name in disabled: + continue + script = build_dep(dep_name) + lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') + lines.append("if errorlevel 1 echo Build failed! && exit /B 1") + lines.append("@echo All Pillow dependencies built successfully!") + write_script("build_dep_all.cmd", lines) + + +def build_pillow(): + lines = [ + "@echo ---- Building Pillow (build_ext %*) ----", + 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 + ] + + write_script("build_pillow.cmd", lines) + + +if __name__ == "__main__": + # winbuild directory + winbuild_dir = os.path.dirname(os.path.realpath(__file__)) + + verbose = False + disabled = [] + depends_dir = os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")) + python_dir = os.environ.get("PYTHON") + python_exe = os.environ.get("EXECUTABLE", "python.exe") + architecture = os.environ.get( + "ARCHITECTURE", + "ARM64" + if platform.machine() == "ARM64" + else ("x86" if struct.calcsize("P") == 4 else "x64"), + ) + build_dir = os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build")) + sources_dir = "" + for arg in sys.argv[1:]: + if arg == "-v": + verbose = True + elif arg == "--no-imagequant": + disabled += ["libimagequant"] + elif arg == "--no-raqm" or arg == "--no-fribidi": + disabled += ["fribidi"] + elif arg.startswith("--depends="): + depends_dir = arg[10:] + elif arg.startswith("--python="): + python_dir = arg[9:] + elif arg.startswith("--executable="): + python_exe = arg[13:] + elif arg.startswith("--architecture="): + architecture = arg[15:] + elif arg.startswith("--dir="): + build_dir = arg[6:] + elif arg == "--srcdir": + sources_dir = os.path.sep + "src" + else: + raise ValueError("Unknown parameter: " + arg) + + # dependency cache directory + os.makedirs(depends_dir, exist_ok=True) + print("Caching dependencies in:", depends_dir) + + if python_dir is None: + python_dir = os.path.dirname(os.path.realpath(sys.executable)) + python_exe = os.path.basename(sys.executable) + print("Target Python:", os.path.join(python_dir, python_exe)) + + arch_prefs = architectures[architecture] + print("Target Architecture:", architecture) + + msvs = find_msvs() + if msvs is None: + raise RuntimeError( + "Visual Studio not found. Please install Visual Studio 2017 or newer." + ) + print("Found Visual Studio at:", msvs["vs_dir"]) + + print("Using output directory:", build_dir) + + # build directory for *.h files + inc_dir = os.path.join(build_dir, "inc") + # build directory for *.lib files + lib_dir = os.path.join(build_dir, "lib") + # build directory for *.bin files + 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, license_dir]: + os.makedirs(path, exist_ok=True) + + prefs = { + # Python paths / preferences + "python_dir": python_dir, + "python_exe": python_exe, + "architecture": architecture, + **arch_prefs, + # Pillow paths + "pillow_dir": os.path.realpath(os.path.join(winbuild_dir, "..")), + "winbuild_dir": winbuild_dir, + # Build paths + "build_dir": build_dir, + "inc_dir": inc_dir, + "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 + # TODO find NASM automatically + # script header + "header": sum([header, msvs["header"], ["@echo on"]], []), + } + + for k, v in deps.items(): + prefs[f"dir_{k}"] = os.path.join(sources_dir, v["dir"]) + + print() + + write_script(".gitignore", ["*"]) + build_dep_all() + build_pillow() diff --git a/winbuild/config.py b/winbuild/config.py deleted file mode 100644 index e755577b556..00000000000 --- a/winbuild/config.py +++ /dev/null @@ -1,143 +0,0 @@ -import os - -SF_MIRROR = 'http://iweb.dl.sourceforge.net' -PILLOW_DEPENDS_DIR = 'C:\\pillow-depends\\' - -pythons = { # '26': 7, - '27': 7, - 'pypy2': 7, - # '32': 7, - '33': 7.1, - '34': 7.1} - -VIRT_BASE = "c:/vp/" -X64_EXT = os.environ.get('X64_EXT', "x64") - -libs = { - # 'openjpeg': { - # 'filename': 'openjpeg-2.0.0-win32-x86.zip', - # 'version': '2.0' - # }, - 'zlib': { - 'url': 'http://zlib.net/zlib1211.zip', - 'filename': PILLOW_DEPENDS_DIR + 'zlib1211.zip', - 'dir': 'zlib-1.2.11', - }, - 'jpeg': { - 'url': 'http://www.ijg.org/files/jpegsr9b.zip', - 'filename': PILLOW_DEPENDS_DIR + 'jpegsr9b.zip', - 'dir': 'jpeg-9b', - }, - 'tiff': { - 'url': 'ftp://download.osgeo.org/libtiff/tiff-4.0.9.zip', - 'filename': PILLOW_DEPENDS_DIR + 'tiff-4.0.9.zip', - 'dir': 'tiff-4.0.9', - }, - 'freetype': { - 'url': 'https://download.savannah.gnu.org/releases/freetype/freetype-2.8.1.tar.gz', - 'filename': PILLOW_DEPENDS_DIR + 'freetype-2.8.1.tar.gz', - 'dir': 'freetype-2.8.1', - }, - 'lcms': { - 'url': SF_MIRROR+'/project/lcms/lcms/2.7/lcms2-2.7.zip', - 'filename': PILLOW_DEPENDS_DIR + 'lcms2-2.7.zip', - 'dir': 'lcms2-2.7', - }, - 'tcl-8.5': { - 'url': SF_MIRROR+'/project/tcl/Tcl/8.5.19/tcl8519-src.zip', - 'filename': PILLOW_DEPENDS_DIR + 'tcl8519-src.zip', - 'dir': '', - }, - 'tk-8.5': { - 'url': SF_MIRROR+'/project/tcl/Tcl/8.5.19/tk8519-src.zip', - 'filename': PILLOW_DEPENDS_DIR + 'tk8519-src.zip', - 'dir': '', - 'version': '8.5.19', - }, - 'tcl-8.6': { - 'url': SF_MIRROR+'/project/tcl/Tcl/8.6.8/tcl868-src.zip', - 'filename': PILLOW_DEPENDS_DIR + 'tcl868-src.zip', - 'dir': '', - }, - 'tk-8.6': { - 'url': SF_MIRROR+'/project/tcl/Tcl/8.6.8/tk868-src.zip', - 'filename': PILLOW_DEPENDS_DIR + 'tk868-src.zip', - 'dir': '', - 'version': '8.6.8', - }, - 'webp': { - 'url': 'http://downloads.webmproject.org/releases/webp/libwebp-0.6.1.tar.gz', - 'filename': PILLOW_DEPENDS_DIR + 'libwebp-0.6.1.tar.gz', - 'dir': 'libwebp-0.6.1', - }, - 'openjpeg': { - 'url': SF_MIRROR+'/project/openjpeg/openjpeg/2.3.0/openjpeg-2.3.0.tar.gz', - 'filename': PILLOW_DEPENDS_DIR + 'openjpeg-2.3.0.tar.gz', - 'dir': 'openjpeg-2.3.0', - }, -} - -compilers = { - (7, 64): { - 'env_version': 'v7.0', - 'vc_version': '2008', - 'env_flags': '/x64 /xp', - 'inc_dir': 'msvcr90-x64', - 'platform': 'x64', - 'webp_platform': 'x64', - }, - (7, 32): { - 'env_version': 'v7.0', - 'vc_version': '2008', - 'env_flags': '/x86 /xp', - 'inc_dir': 'msvcr90-x32', - 'platform': 'Win32', - 'webp_platform': 'x86', - }, - (7.1, 64): { - 'env_version': 'v7.1', - 'vc_version': '2010', - 'env_flags': '/x64 /vista', - 'inc_dir': 'msvcr10-x64', - 'platform': 'x64', - 'webp_platform': 'x64', - }, - (7.1, 32): { - 'env_version': 'v7.1', - 'vc_version': '2010', - 'env_flags': '/x86 /vista', - 'inc_dir': 'msvcr10-x32', - 'platform': 'Win32', - 'webp_platform': 'x86', - }, -} - - -def pyversion_from_env(): - py = os.environ['PYTHON'] - - py_version = '27' - for k in pythons: - if k in py: - py_version = k - break - - if '64' in py: - py_version = '%s%s' % (py_version, X64_EXT) - - return py_version - - -def compiler_from_env(): - py = os.environ['PYTHON'] - - for k, v in pythons.items(): - if k in py: - compiler_version = v - break - - bit = 32 - if '64' in py: - bit = 64 - - return compilers[(compiler_version, bit)] diff --git a/winbuild/fetch.py b/winbuild/fetch.py deleted file mode 100644 index 7d140fd358d..00000000000 --- a/winbuild/fetch.py +++ /dev/null @@ -1,18 +0,0 @@ -import os -import sys -import urllib.parse -import urllib.request - - -def fetch(url): - name = urllib.parse.urlsplit(url)[2].split('/')[-1] - - if not os.path.exists(name): - print("Fetching", url) - content = urllib.request.urlopen(url).read() - with open(name, 'wb') as fd: - fd.write(content) - return name - -if __name__ == '__main__': - fetch(sys.argv[1]) diff --git a/winbuild/fixproj.py b/winbuild/fixproj.py deleted file mode 100644 index 9b5203fb81f..00000000000 --- a/winbuild/fixproj.py +++ /dev/null @@ -1,8 +0,0 @@ -import sys - -with open(sys.argv[1], 'r') as fd: - content = '\n'.join(line.strip() for line in fd if line.strip()) -if len(sys.argv) == 3: - content = content.replace('Win32', sys.argv[2]).replace('x64', sys.argv[2]) -with open(sys.argv[1], 'w') as fd: - fd.write(content) diff --git a/winbuild/fribidi.cmake b/winbuild/fribidi.cmake new file mode 100644 index 00000000000..27b8d17a8ed --- /dev/null +++ b/winbuild/fribidi.cmake @@ -0,0 +1,102 @@ +cmake_minimum_required(VERSION 3.12) + +project(fribidi) + +add_definitions(-D_CRT_SECURE_NO_WARNINGS) + +include_directories(${CMAKE_CURRENT_BINARY_DIR}) +include_directories(lib) + +function(extract_regex_1 var text regex) + string(REGEX MATCH ${regex} _ ${text}) + set(${var} "${CMAKE_MATCH_1}" PARENT_SCOPE) +endfunction() + + +function(fribidi_conf) + file(READ configure.ac FRIBIDI_CONF) + extract_regex_1(FRIBIDI_MAJOR_VERSION "${FRIBIDI_CONF}" "\\(fribidi_major_version, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_MINOR_VERSION "${FRIBIDI_CONF}" "\\(fribidi_minor_version, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_MICRO_VERSION "${FRIBIDI_CONF}" "\\(fribidi_micro_version, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_INTERFACE_VERSION "${FRIBIDI_CONF}" "\\(fribidi_interface_version, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_INTERFACE_AGE "${FRIBIDI_CONF}" "\\(fribidi_interface_age, ([0-9]+)\\)") + extract_regex_1(FRIBIDI_BINARY_AGE "${FRIBIDI_CONF}" "\\(fribidi_binary_age, ([0-9]+)\\)") + set(FRIBIDI_VERSION "${FRIBIDI_MAJOR_VERSION}.${FRIBIDI_MINOR_VERSION}.${FRIBIDI_MICRO_VERSION}") + set(PACKAGE "fribidi") + set(PACKAGE_NAME "GNU FriBidi") + set(PACKAGE_BUGREPORT "https://github.com/fribidi/fribidi/issues/new") + set(SIZEOF_INT 4) + set(FRIBIDI_MSVC_BUILD_PLACEHOLDER "#define FRIBIDI_BUILT_WITH_MSVC") + message("detected ${PACKAGE_NAME} version ${FRIBIDI_VERSION}") + configure_file(lib/fribidi-config.h.in lib/fribidi-config.h @ONLY) +endfunction() +fribidi_conf() + + +function(prepend var prefix) + set(out "") + foreach(f ${ARGN}) + list(APPEND out "${prefix}${f}") + endforeach() + set(${var} "${out}" PARENT_SCOPE) +endfunction() + +macro(fribidi_definitions _TGT) + target_compile_definitions(${_TGT} PUBLIC + HAVE_MEMSET + HAVE_MEMMOVE + HAVE_STRDUP + HAVE_STDLIB_H=1 + HAVE_STRING_H=1 + HAVE_MEMORY_H=1 + #HAVE_STRINGS_H + #HAVE_SYS_TIMES_H + STDC_HEADERS=1 + HAVE_STRINGIZE=1) +endmacro() + +function(fribidi_gen _NAME _OUTNAME _PARAM) + set(_OUT lib/${_OUTNAME}) + prepend(_DEP "${CMAKE_CURRENT_SOURCE_DIR}/gen.tab/" ${ARGN}) + add_executable(gen-${_NAME} + gen.tab/gen-${_NAME}.c + gen.tab/packtab.c) + fribidi_definitions(gen-${_NAME}) + target_compile_definitions(gen-${_NAME} + PUBLIC DONT_HAVE_FRIBIDI_CONFIG_H) + add_custom_command( + COMMAND gen-${_NAME} ${_PARAM} ${_DEP} > ${_OUT} + DEPENDS ${_DEP} + OUTPUT ${_OUT}) + list(APPEND FRIBIDI_SOURCES_GENERATED "${_OUT}") + set(FRIBIDI_SOURCES_GENERATED ${FRIBIDI_SOURCES_GENERATED} PARENT_SCOPE) +endfunction() + +fribidi_gen(unicode-version fribidi-unicode-version.h "" + unidata/ReadMe.txt unidata/BidiMirroring.txt) + + +macro(fribidi_tab _NAME) + fribidi_gen(${_NAME}-tab ${_NAME}.tab.i 2 ${ARGN}) + target_sources(gen-${_NAME}-tab + PRIVATE lib/fribidi-unicode-version.h) +endmacro() + +fribidi_tab(bidi-type unidata/UnicodeData.txt) +fribidi_tab(joining-type unidata/UnicodeData.txt unidata/ArabicShaping.txt) +fribidi_tab(arabic-shaping unidata/UnicodeData.txt) +fribidi_tab(mirroring unidata/BidiMirroring.txt) +fribidi_tab(brackets unidata/BidiBrackets.txt unidata/UnicodeData.txt) +fribidi_tab(brackets-type unidata/BidiBrackets.txt) + + +file(GLOB FRIBIDI_SOURCES lib/*.c) +file(GLOB FRIBIDI_HEADERS lib/*.h) + +add_library(fribidi SHARED + ${FRIBIDI_SOURCES} + ${FRIBIDI_HEADERS} + ${FRIBIDI_SOURCES_GENERATED}) +fribidi_definitions(fribidi) +target_compile_definitions(fribidi + PUBLIC "-DFRIBIDI_BUILD") diff --git a/winbuild/get_pythons.py b/winbuild/get_pythons.py deleted file mode 100644 index 448450afb25..00000000000 --- a/winbuild/get_pythons.py +++ /dev/null @@ -1,12 +0,0 @@ -from fetch import fetch -import os - -if __name__ == '__main__': - for version in ['2.7.10', '3.4.3']: - for platform in ['', '.amd64']: - for extension in ['', '.asc']: - fetch('https://www.python.org/ftp/python/%s/python-%s%s.msi%s' - % (version, version, platform, extension)) - - # find pip, if it's not in the path! - os.system('pip install virtualenv') diff --git a/winbuild/nmake.opt b/winbuild/nmake.opt deleted file mode 100644 index b155daaccc0..00000000000 --- a/winbuild/nmake.opt +++ /dev/null @@ -1,218 +0,0 @@ -# $Id: nmake.opt,v 1.18 2006/06/07 16:33:45 dron Exp $ -# -# Copyright (C) 2004, Andrey Kiselev -# -# Permission to use, copy, modify, distribute, and sell this software and -# its documentation for any purpose is hereby granted without fee, provided -# that (i) the above copyright notices and this permission notice appear in -# all copies of the software and related documentation, and (ii) the names of -# Sam Leffler and Silicon Graphics may not be used in any advertising or -# publicity relating to the software without the specific, prior written -# permission of Sam Leffler and Silicon Graphics. -# -# THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, -# EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY -# WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. -# -# IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR -# ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, -# OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -# WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF -# LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE -# OF THIS SOFTWARE. - -# Compile time parameters for MS Visual C++ compiler. -# You may edit this file to specify building options. - -# -###### Edit the following lines to choose a feature set you need. ####### -# - -# -# Select WINMODE_CONSOLE to build a library which reports errors to stderr, or -# WINMODE_WINDOWED to build such that errors are reported via MessageBox(). -# -WINMODE_CONSOLE = 1 -#WINMODE_WINDOWED = 1 - -# -# Comment out the following lines to disable internal codecs. -# -# Support for CCITT Group 3 & 4 algorithms -CCITT_SUPPORT = 1 -# Support for Macintosh PackBits algorithm -PACKBITS_SUPPORT = 1 -# Support for LZW algorithm -LZW_SUPPORT = 1 -# Support for ThunderScan 4-bit RLE algorithm -THUNDER_SUPPORT = 1 -# Support for NeXT 2-bit RLE algorithm -NEXT_SUPPORT = 1 -# Support for LogLuv high dynamic range encoding -LOGLUV_SUPPORT = 1 - -# -# Uncomment and edit following lines to enable JPEG support. -# -JPEG_SUPPORT = 1 -JPEGDIR = $(BUILD)\jpeg-9b -JPEG_INCLUDE = -I$(JPEGDIR) -JPEG_LIB = $(JPEGDIR)/libjpeg.lib - -# -# Uncomment and edit following lines to enable ZIP support -# (required for Deflate compression and Pixar log-format) -# -ZIP_SUPPORT = 1 -ZLIBDIR = $(BUILD)\zlib-1.2.8 -ZLIB_INCLUDE = -I$(ZLIBDIR) -ZLIB_LIB = $(ZLIBDIR)/zlib.lib - -# -# Uncomment and edit following lines to enable ISO JBIG support -# -#JBIG_SUPPORT = 1 -#JBIGDIR = d:/projects/jbigkit -#JBIG_INCLUDE = -I$(JBIGDIR)/libjbig -#JBIG_LIB = $(JBIGDIR)/libjbig/jbig.lib - -# -# Uncomment following line to enable Pixar log-format algorithm -# (Zlib required). -# -#PIXARLOG_SUPPORT = 1 - -# -# Comment out the following lines to disable strip chopping -# (whether or not to convert single-strip uncompressed images to multiple -# strips of specified size to reduce memory usage). Default strip size -# is 8192 bytes, it can be configured via the STRIP_SIZE_DEFAULT parameter -# -STRIPCHOP_SUPPORT = 1 -STRIP_SIZE_DEFAULT = 8192 - -# -# Comment out the following lines to disable treating the fourth sample with -# no EXTRASAMPLE_ value as being ASSOCALPHA. Many packages produce RGBA -# files but don't mark the alpha properly. -# -EXTRASAMPLE_AS_ALPHA_SUPPORT = 1 - -# -# Comment out the following lines to disable picking up YCbCr subsampling -# info from the JPEG data stream to support files lacking the tag. -# See Bug 168 in Bugzilla, and JPEGFixupTestSubsampling() for details. -# -CHECK_JPEG_YCBCR_SUBSAMPLING = 1 - -# -####################### Compiler related options. ####################### -# - -# -# Pick debug or optimized build flags. We default to an optimized build -# with no debugging information. -# NOTE: /EHsc option required if you want to build the C++ stream API -# -OPTFLAGS = /Ox /MD /EHsc /W3 /D_CRT_SECURE_NO_DEPRECATE -#OPTFLAGS = /Zi - -# -# Uncomment following line to enable using Windows Common RunTime Library -# instead of Windows specific system calls. See notes on top of tif_unix.c -# module for details. -# -USE_WIN_CRT_LIB = 1 - -# Compiler specific options. You may probably want to adjust compilation -# parameters in CFLAGS variable. Refer to your compiler documentation -# for the option reference. -# -MAKE = nmake /nologo -CC = cl /nologo -CXX = cl /nologo -AR = lib /nologo -LD = link /nologo - -CFLAGS = $(OPTFLAGS) $(INCL) $(EXTRAFLAGS) -CXXFLAGS = $(OPTFLAGS) $(INCL) $(EXTRAFLAGS) -EXTRAFLAGS = -LIBS = - -# Name of the output shared library -DLLNAME = libtiff.dll - -# -########### There is nothing to edit below this line normally. ########### -# - -# Set the native cpu bit order -EXTRAFLAGS = -DFILLODER_LSB2MSB $(EXTRAFLAGS) - -!IFDEF WINMODE_WINDOWED -EXTRAFLAGS = -DTIF_PLATFORM_WINDOWED $(EXTRAFLAGS) -LIBS = user32.lib $(LIBS) -!ELSE -EXTRAFLAGS = -DTIF_PLATFORM_CONSOLE $(EXTRAFLAGS) -!ENDIF - -# Codec stuff -!IFDEF CCITT_SUPPORT -EXTRAFLAGS = -DCCITT_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF PACKBITS_SUPPORT -EXTRAFLAGS = -DPACKBITS_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF LZW_SUPPORT -EXTRAFLAGS = -DLZW_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF THUNDER_SUPPORT -EXTRAFLAGS = -DTHUNDER_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF NEXT_SUPPORT -EXTRAFLAGS = -DNEXT_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF LOGLUV_SUPPORT -EXTRAFLAGS = -DLOGLUV_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF JPEG_SUPPORT -LIBS = $(LIBS) $(JPEG_LIB) -EXTRAFLAGS = -DJPEG_SUPPORT -DOJPEG_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF ZIP_SUPPORT -LIBS = $(LIBS) $(ZLIB_LIB) -EXTRAFLAGS = -DZIP_SUPPORT $(EXTRAFLAGS) -!IFDEF PIXARLOG_SUPPORT -EXTRAFLAGS = -DPIXARLOG_SUPPORT $(EXTRAFLAGS) -!ENDIF -!ENDIF - -!IFDEF JBIG_SUPPORT -LIBS = $(LIBS) $(JBIG_LIB) -EXTRAFLAGS = -DJBIG_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF STRIPCHOP_SUPPORT -EXTRAFLAGS = -DSTRIPCHOP_DEFAULT=TIFF_STRIPCHOP -DSTRIP_SIZE_DEFAULT=$(STRIP_SIZE_DEFAULT) $(EXTRAFLAGS) -!ENDIF - -!IFDEF EXTRASAMPLE_AS_ALPHA_SUPPORT -EXTRAFLAGS = -DDEFAULT_EXTRASAMPLE_AS_ALPHA $(EXTRAFLAGS) -!ENDIF - -!IFDEF CHECK_JPEG_YCBCR_SUBSAMPLING -EXTRAFLAGS = -DCHECK_JPEG_YCBCR_SUBSAMPLING $(EXTRAFLAGS) -!ENDIF - -!IFDEF USE_WIN_CRT_LIB -EXTRAFLAGS = -DAVOID_WIN32_FILEIO $(EXTRAFLAGS) -!ELSE -EXTRAFLAGS = -DUSE_WIN32_FILEIO $(EXTRAFLAGS) -!ENDIF diff --git a/winbuild/test.py b/winbuild/test.py deleted file mode 100755 index 84e0713087a..00000000000 --- a/winbuild/test.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 - -import subprocess -import os -import glob -import sys - -from config import pythons, VIRT_BASE, X64_EXT - - -def test_one(params): - python, architecture = params - try: - print("Running: %s, %s" % params) - command = [r'%s\%s%s\Scripts\python.exe' % - (VIRT_BASE, python, architecture), - 'test-installed.py', - '--processes=-0', - '--process-timeout=30', - ] - command.extend(glob.glob('Tests/test*.py')) - proc = subprocess.Popen(command, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE) - (trace, stderr) = proc.communicate() - status = proc.returncode - print("Done with %s, %s -- %s" % (python, architecture, status)) - return (python, architecture, status, trace) - except Exception as msg: - print("Error with %s, %s: %s" % (python, architecture, msg)) - return (python, architecture, -1, str(msg)) - - -if __name__ == '__main__': - - os.chdir('..') - matrix = [(python, architecture) for python in pythons - for architecture in ('', X64_EXT)] - - results = map(test_one, matrix) - - for (python, architecture, status, trace) in results: - print("%s%s: %s" % (python, architecture, status and 'ERR' or 'PASS')) - - res = all(status for (python, architecture, status, trace) in results) - sys.exit(res) diff --git a/winbuild/untar.py b/winbuild/untar.py deleted file mode 100644 index 95b1c22549e..00000000000 --- a/winbuild/untar.py +++ /dev/null @@ -1,10 +0,0 @@ -import sys -import tarfile - - -def untar(src, dest): - with tarfile.open(src, 'r:gz') as tgz: - tgz.extractall(dest) - -if __name__ == '__main__': - untar(sys.argv[1], sys.argv[2]) diff --git a/winbuild/unzip.py b/winbuild/unzip.py deleted file mode 100644 index 92d385fa657..00000000000 --- a/winbuild/unzip.py +++ /dev/null @@ -1,10 +0,0 @@ -import sys -import zipfile - - -def unzip(src, dest): - with zipfile.ZipFile(src) as zf: - zf.extractall(dest) - -if __name__ == '__main__': - unzip(sys.argv[1], sys.argv[2])