diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 6ce5200b6ea..00000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,100 +0,0 @@ -skip_commits: - files: - - ".github/**/*" - - ".gitmodules" - - "docs/**/*" - - "wheels/**/*" - -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: - COVERAGE_CORE: sysmon - EXECUTABLE: python.exe - TEST_OPTIONS: - DEPLOY: YES - matrix: - - PYTHON: C:/Python312 - ARCHITECTURE: x86 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - - PYTHON: C:/Python38-x64 - ARCHITECTURE: AMD64 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - - -install: -- '%PYTHON%\%EXECUTABLE% --version' -- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip' -- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip -- 7z x pillow-test-images.zip -oc:\ -- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images -- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip -- 7z x nasm-win64.zip -oc:\ -- choco install ghostscript --version=10.3.1 -- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH% -- cd c:\pillow\winbuild\ -- ps: | - c:\python38\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: -- cd c:\pillow -- winbuild\build\build_env.cmd -- '%PYTHON%\%EXECUTABLE% -m pip install -v -C raqm=vendor -C fribidi=vendor .' -- '%PYTHON%\%EXECUTABLE% selftest.py --installed' - -test_script: -- cd c:\pillow -- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma' -- 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: -- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe -- .\codecov.exe --file coverage.xml --name %PYTHON% --flags AppVeyor - -matrix: - fast_finish: true - -cache: -- '%LOCALAPPDATA%\pip\Cache' - -artifacts: -- path: pillow\*.egg - name: egg -- path: pillow\*.whl - name: wheel - -before_deploy: - - cd c:\pillow - - '%PYTHON%\%EXECUTABLE% -m pip wheel -v -C raqm=vendor -C fribidi=vendor .' - - ps: Get-ChildItem .\*.whl | % { 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 index c71546f007b..6da27b975cc 100755 --- a/.ci/after_success.sh +++ b/.ci/after_success.sh @@ -2,8 +2,4 @@ # gather the coverage data python3 -m pip install coverage -if [[ $MATRIX_DOCKER ]]; then - python3 -m coverage xml --ignore-errors -else - python3 -m coverage xml -fi +python3 -m coverage xml diff --git a/.ci/build.sh b/.ci/build.sh index e678f68ec85..ae10cb67155 100755 --- a/.ci/build.sh +++ b/.ci/build.sh @@ -3,8 +3,5 @@ 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 index 30b64349d70..9553eb8f442 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -2,73 +2,58 @@ aptget_update() { - if [ ! -z $1 ]; then + if [ -n "$1" ]; then echo "" echo "Retrying apt-get update..." echo "" fi - output=`sudo apt-get update 2>&1` + 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 +aptget_update || aptget_update retry || aptget_update retry 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\ - sway wl-clipboard libopenblas-dev -fi +sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\ + ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ + cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ + sway wl-clipboard libopenblas-dev nasm python3 -m pip install --upgrade pip python3 -m pip install --upgrade wheel -# TODO Update condition when cffi supports 3.13 -if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi python3 -m pip install coverage python3 -m pip install defusedxml +python3 -m pip install ipython 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 +# optional test dependencies, only install if there's a binary package. +python3 -m pip install --only-binary=:all: numpy || true +python3 -m pip install --only-binary=:all: pyarrow || true + +# PyQt6 doesn't support PyPy3 +if [[ $GHA_PYTHON_VERSION == 3.* ]]; then + sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 + # TODO Update condition when pyqt6 supports free-threading + if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi +fi -if [[ $(uname) != CYGWIN* ]]; then - # TODO Update condition when NumPy supports 3.13 - if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi - - # PyQt6 doesn't support PyPy3 - if [[ $GHA_PYTHON_VERSION == 3.* ]]; then - sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 - python3 -m pip install pyqt6 - fi - - # Pyroma uses non-isolated build and fails with old setuptools - if [[ - $GHA_PYTHON_VERSION == pypy3.9 - || $GHA_PYTHON_VERSION == 3.8 - || $GHA_PYTHON_VERSION == 3.9 - ]]; then - # To match pyproject.toml - python3 -m pip install "setuptools>=67.8" - fi +# webp +pushd depends && ./install_webp.sh && popd - # webp - pushd depends && ./install_webp.sh && popd +# libimagequant +pushd depends && ./install_imagequant.sh && popd - # libimagequant - pushd depends && ./install_imagequant.sh && popd +# raqm +pushd depends && sudo ./install_raqm.sh && popd - # raqm - pushd depends && ./install_raqm.sh && popd +# libavif +pushd depends && ./install_libavif.sh && popd - # extra test images - pushd depends && ./install_extra_test_images.sh && popd -else - cd depends && ./install_extra_test_images.sh && cd .. -fi +# extra test images +pushd depends && ./install_extra_test_images.sh && popd diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 0d0f81fbfad..6e869a5c2e9 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.19.1 +cibuildwheel==3.3.1 diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 6dd43248837..c64343a7347 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1 +1,15 @@ -mypy==1.10.1 +mypy==1.19.1 +arro3-compute +arro3-core +IceSpringPySideStubs-PyQt6 +IceSpringPySideStubs-PySide6 +ipython +numpy +packaging +pyarrow-stubs +pybind11 +pytest +types-atheris +types-defusedxml +types-olefile +types-setuptools diff --git a/.ci/test.cmd b/.ci/test.cmd new file mode 100644 index 00000000000..acfac3d1acd --- /dev/null +++ b/.ci/test.cmd @@ -0,0 +1,3 @@ +python.exe -c "from PIL import Image" +IF ERRORLEVEL 1 EXIT /B +python.exe -bb -m pytest -vv -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests diff --git a/.ci/test.sh b/.ci/test.sh index 8ff7c5f6483..87a605d84be 100755 --- a/.ci/test.sh +++ b/.ci/test.sh @@ -4,4 +4,4 @@ set -e python3 -c "from PIL import Image" -python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests $REVERSE +python3 -bb -m pytest -vv -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests $REVERSE diff --git a/.clang-format b/.clang-format index 3199e330b96..1871d1f7a7b 100644 --- a/.clang-format +++ b/.clang-format @@ -1,9 +1,10 @@ # A clang-format style that approximates Python's PEP 7 # Useful for IDE integration +Language: C BasedOnStyle: Google AlwaysBreakAfterReturnType: All AllowShortIfStatementsOnASingleLine: false -AlignAfterOpenBracket: AlwaysBreak +AlignAfterOpenBracket: BlockIndent BinPackArguments: false BinPackParameters: false BreakBeforeBraces: Attach @@ -11,7 +12,26 @@ ColumnLimit: 88 DerivePointerAlignment: false IndentGotoLabels: false IndentWidth: 4 +PointerAlignment: Right +ReflowComments: true +SortIncludes: false +SpaceBeforeParens: ControlStatements +SpacesInParentheses: false +TabWidth: 4 +UseTab: Never +--- Language: Cpp +BasedOnStyle: Google +AlwaysBreakAfterReturnType: All +AllowShortIfStatementsOnASingleLine: false +AlignAfterOpenBracket: BlockIndent +BinPackArguments: false +BinPackParameters: false +BreakBeforeBraces: Attach +ColumnLimit: 88 +DerivePointerAlignment: false +IndentGotoLabels: false +IndentWidth: 4 PointerAlignment: Right ReflowComments: true SortIncludes: false diff --git a/.coveragerc b/.coveragerc index 018cc1cbfb1..1f474015e30 100644 --- a/.coveragerc +++ b/.coveragerc @@ -18,7 +18,5 @@ exclude_also = [run] omit = - Tests/32bit_segfault_check.py - Tests/bench_cffi_access.py - Tests/check_*.py + checks/*.py Tests/createfontdatachunk.py diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d03fcf0d9da..c098e32ebc8 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -9,7 +9,7 @@ Please send a pull request to the `main` branch. Please include [documentation]( - Fork the Pillow repository. - Create a branch from `main`. - Develop bug fixes, features, tests, etc. -- Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests. +- Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) 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 @@ -17,9 +17,8 @@ Please send a pull request to the `main` branch. Please include [documentation]( - Separate code commits from reformatting commits. - Provide tests for any newly added code. - Follow PEP 8. -- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor. +- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running extra tests. - Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests. -- Do not add to the [changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) for proposed changes, as that is updated after changes are merged. ## Reporting Issues diff --git a/.github/ISSUE_TEMPLATE/RELEASE.md b/.github/ISSUE_TEMPLATE/RELEASE.md new file mode 100644 index 00000000000..20d1ac79b2a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/RELEASE.md @@ -0,0 +1,46 @@ +--- +name: "Maintainers only: Release" +about: For maintainers to schedule a quarterly release +labels: Release +--- + +## Main release + +Released quarterly on January 2nd, April 1st, July 1st and October 15th. + +* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 +* [ ] Develop and prepare release in `main` branch. + * [ ] Add release notes e.g. https://github.com/python-pillow/Pillow/pull/8885 +* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in `main` branch. +* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them. +* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` +* [ ] Run pre-release check via `make release-test` in a freshly cloned repo. +* [ ] Create branch and tag for release e.g.: + ```bash + git branch [[MAJOR.MINOR]].x + git tag [[MAJOR.MINOR]].0 + git push --tags + ``` +* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) has passed, including the "Upload release to PyPI" job. This will have been triggered by the new tag. +* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases). +* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then: + ```bash + git push --all + ``` + +## Publicize release + +* [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321 + +## Documentation + +* [ ] 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/.github/mergify.yml b/.github/mergify.yml index 3c20661376f..14222db1094 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -8,8 +8,6 @@ pull_request_rules: - status-success=Docker Test Successful - status-success=Windows Test Successful - status-success=MinGW - - 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 index 3711d91f0d5..de0ab480519 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -3,18 +3,19 @@ tag-template: "$NEXT_MINOR_VERSION" change-template: '- $TITLE #$NUMBER [@$AUTHOR]' categories: - - title: "Dependencies" - label: "Dependency" + - title: "Removals" + label: "Removal" - title: "Deprecations" label: "Deprecation" - title: "Documentation" label: "Documentation" - - title: "Removals" - label: "Removal" + - title: "Dependencies" + label: "Dependency" - title: "Testing" label: "Testing" - title: "Type hints" label: "Type hints" + - title: "Other changes" exclude-labels: - "changelog: skip" @@ -23,6 +24,4 @@ template: | https://pillow.readthedocs.io/en/stable/releasenotes/$NEXT_MINOR_VERSION.html - ## Changes - $CHANGES diff --git a/.github/renovate.json b/.github/renovate.json index d1d82433553..8187fc15b95 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,17 +1,22 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:base" + "config:recommended" ], "labels": [ "Dependency" ], + "minimumReleaseAge": "7 days", "packageRules": [ { "groupName": "github-actions", - "matchManagers": ["github-actions"], - "separateMajorMinor": "false" + "matchManagers": [ + "github-actions" + ], + "separateMajorMinor": false } ], - "schedule": ["on the 3rd day of the month"] + "schedule": [ + "* * 3 * *" + ] } diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index eb73fc6a7cd..3f78c98b6f6 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -6,11 +6,13 @@ on: - "**" paths: - ".github/workflows/cifuzz.yml" + - ".github/workflows/wheels-dependencies.sh" - "**.c" - "**.h" pull_request: paths: - ".github/workflows/cifuzz.yml" + - ".github/workflows/wheels-dependencies.sh" - "**.c" - "**.h" workflow_dispatch: @@ -22,6 +24,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: Fuzzing: runs-on: ubuntu-latest @@ -42,13 +47,13 @@ jobs: language: python dry-run: false - name: Upload New Crash - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: failure() && steps.build.outcome == 'success' with: name: artifacts path: ./out/artifacts - name: Upload Legacy Crash - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: steps.run.outcome == 'success' with: name: crash diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 92e860cb547..857881c0102 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -32,10 +32,12 @@ jobs: name: Docs steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + with: + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.x" cache: pip @@ -46,19 +48,35 @@ jobs: - name: Build system information run: python3 .github/workflows/system-info.py + - name: Cache libavif + uses: actions/cache@v5 + id: cache-libavif + with: + path: ~/cache-libavif + key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }} + - name: Cache libimagequant - uses: actions/cache@v4 + uses: actions/cache@v5 id: cache-libimagequant with: path: ~/cache-libimagequant key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + - name: Cache libwebp + uses: actions/cache@v5 + id: cache-libwebp + with: + path: ~/cache-libwebp + key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }} + - name: Install Linux dependencies run: | .ci/install.sh env: GHA_PYTHON_VERSION: "3.x" + GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }} GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} + GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }} - name: Build run: | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cc4760288e5..e2f8bf47ac4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,53 +2,31 @@ name: Lint on: [push, pull_request, workflow_dispatch] +permissions: {} + env: FORCE_COLOR: 1 - -permissions: - contents: read + PREK_COLOR: always + RUFF_OUTPUT_FORMAT: github concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - build: - + lint: runs-on: ubuntu-latest - name: Lint - steps: - - uses: actions/checkout@v4 - - - name: pre-commit cache - uses: actions/cache@v4 - 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@v5 - with: - python-version: "3.x" - 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 - - - name: Mypy - run: tox -e mypy + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: actions/setup-python@v6 + with: + python-version: "3.x" + - name: Install uv + uses: astral-sh/setup-uv@v7 + - name: Lint + run: uvx --with tox-uv tox -e lint + - name: Mypy + run: uvx --with tox-uv tox -e mypy diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index f8f191d387a..7c768af483e 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -3,34 +3,35 @@ set -e brew install \ + aom \ + dav1d \ freetype \ ghostscript \ + jpeg-turbo \ libimagequant \ - libjpeg \ + libraqm \ libtiff \ little-cms2 \ openjpeg \ + rav1e \ + svt-av1 \ webp -if [[ "$ImageOS" == "macos13" ]]; then - brew install --ignore-dependencies libraqm -else - brew install libraqm -fi export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" -# TODO Update condition when cffi supports 3.13 -if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi - python3 -m pip install coverage python3 -m pip install defusedxml +python3 -m pip install ipython 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 +# optional test dependencies, only install if there's a binary package. +python3 -m pip install --only-binary=:all: numpy || true +python3 -m pip install --only-binary=:all: pyarrow || true -# TODO Update condition when NumPy supports 3.13 -if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi +# libavif +pushd depends && ./install_libavif.sh && popd # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index a8ddef22c86..12633284f49 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -14,6 +14,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: update_release_draft: permissions: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 545c2e3644a..9d19028387d 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -6,21 +6,26 @@ on: workflow_dispatch: permissions: - issues: write + contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: stale: if: github.repository_owner == 'python-pillow' + permissions: + issues: write runs-on: ubuntu-latest steps: - name: "Check issues" - uses: actions/stale@v9 + uses: actions/stale@v10 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "Awaiting OP Action" diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml deleted file mode 100644 index 1269ef8cb4a..00000000000 --- a/.github/workflows/test-cygwin.yml +++ /dev/null @@ -1,151 +0,0 @@ -name: Test Cygwin - -on: - push: - branches: - - "**" - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - pull_request: - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - COVERAGE_CORE: sysmon - -jobs: - build: - runs-on: windows-latest - strategy: - fail-fast: false - matrix: - python-minor-version: [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@v4 - - - name: Install Cygwin - uses: cygwin/cygwin-install-action@v4 - with: - packages: > - gcc-g++ - ghostscript - git - ImageMagick - 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 - wget - xorg-server-extra - zlib-devel - - - name: Add Lapack to PATH - uses: egor-tensin/cleanup-path@v4 - with: - dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' - - - name: Select Python version - run: | - ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 - - - name: pip cache - uses: actions/cache@v4 - 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: 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@v4 - 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@v4 - with: - file: ./coverage.xml - flags: GHA_Cygwin - name: Cygwin Python 3.${{ matrix.python-minor-version }} - token: ${{ secrets.CODECOV_ORG_TOKEN }} - - 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 index 6afed74db31..08226738e11 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -26,16 +26,19 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + os: ["ubuntu-latest"] docker: [ # Run slower jobs first to give them a headstart and reduce waiting time - ubuntu-22.04-jammy-arm64v8, ubuntu-24.04-noble-ppc64le, ubuntu-24.04-noble-s390x, # Then run the remainder @@ -44,41 +47,46 @@ jobs: amazon-2023-amd64, arch, centos-stream-9-amd64, - debian-11-bullseye-amd64, + centos-stream-10-amd64, debian-12-bookworm-x86, debian-12-bookworm-amd64, - fedora-39-amd64, - fedora-40-amd64, + debian-13-trixie-x86, + debian-13-trixie-amd64, + fedora-42-amd64, + fedora-43-amd64, gentoo, - ubuntu-20.04-focal-amd64, ubuntu-22.04-jammy-amd64, ubuntu-24.04-noble-amd64, ] dockerTag: [main] include: - - docker: "ubuntu-22.04-jammy-arm64v8" - qemu-arch: "aarch64" - docker: "ubuntu-24.04-noble-ppc64le" qemu-arch: "ppc64le" - docker: "ubuntu-24.04-noble-s390x" qemu-arch: "s390x" + - docker: "ubuntu-24.04-noble-arm64v8" + os: "ubuntu-24.04-arm" + dockerTag: main name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + with: + persist-credentials: false - 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 }} + uses: docker/setup-qemu-action@v3 + with: + platforms: ${{ matrix.qemu-arch }} - name: Docker pull run: | - docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + docker pull ${{ matrix.qemu-arch && format('--platform=linux/{0}', matrix.qemu-arch)}} pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} - name: Docker build run: | @@ -89,22 +97,21 @@ jobs: - name: After success run: | - PATH="$PATH:~/.local/bin" docker start pillow_container + sudo docker cp pillow_container:/Pillow /Pillow + sudo chown -R runner /Pillow 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 + cd /Pillow .ci/after_success.sh - env: - MATRIX_DOCKER: ${{ matrix.docker }} - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: flags: GHA_Docker name: ${{ matrix.docker }} - gcov: true token: ${{ secrets.CODECOV_ORG_TOKEN }} success: diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index a773ca45304..808373a65a1 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -28,6 +28,7 @@ concurrency: env: COVERAGE_CORE: sysmon + FORCE_COLOR: 1 jobs: build: @@ -45,7 +46,9 @@ jobs: steps: - name: Checkout Pillow - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + persist-credentials: false - name: Set up shell run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH @@ -58,36 +61,35 @@ jobs: mingw-w64-x86_64-gcc \ mingw-w64-x86_64-ghostscript \ mingw-w64-x86_64-lcms2 \ + mingw-w64-x86_64-libavif \ mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libjpeg-turbo \ mingw-w64-x86_64-libraqm \ mingw-w64-x86_64-libtiff \ mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-openjpeg2 \ - mingw-w64-x86_64-python3-cffi \ - mingw-w64-x86_64-python3-numpy \ - mingw-w64-x86_64-python3-olefile \ - mingw-w64-x86_64-python3-setuptools \ + mingw-w64-x86_64-python-numpy \ + mingw-w64-x86_64-python-olefile \ + mingw-w64-x86_64-python-pip \ + mingw-w64-x86_64-python-pytest \ + mingw-w64-x86_64-python-pytest-cov \ + mingw-w64-x86_64-python-pytest-timeout \ mingw-w64-x86_64-python-pyqt6 - python3 -m ensurepip - python3 -m pip install pyroma pytest pytest-cov pytest-timeout - pushd depends && ./install_extra_test_images.sh && popd - name: Build Pillow - run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install . + run: CFLAGS="-coverage" python3 -m pip install . - name: Test Pillow run: | python3 selftest.py --installed - python3 -c "from PIL import Image" - python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests + .ci/test.sh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: - file: ./coverage.xml + files: ./coverage.xml flags: GHA_Windows name: "MSYS2 MinGW" token: ${{ secrets.CODECOV_ORG_TOKEN }} diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml new file mode 100644 index 00000000000..87eace64376 --- /dev/null +++ b/.github/workflows/test-valgrind-memory.yml @@ -0,0 +1,63 @@ +name: Test Valgrind Memory Leaks + +# like the Docker tests, but running valgrind only on *.c/*.h changes. + +# this is very expensive. Only run on the pull request. +on: + # push: + # branches: + # - "**" + # paths: + # - ".github/workflows/test-valgrind.yml" + # - "**.c" + # - "**.h" + pull_request: + paths: + - ".github/workflows/test-valgrind.yml" + - "**.c" + - "**.h" + - "depends/docker-test-valgrind-memory.sh" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: 1 + +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@v6 + with: + persist-credentials: false + + - 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 1001 + sudo chown -R 1001 $GITHUB_WORKSPACE + docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} /Pillow/depends/docker-test-valgrind-memory.sh + sudo chown -R runner $GITHUB_WORKSPACE diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 63aec586b79..f14dab61623 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -24,6 +24,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: build: @@ -39,7 +42,9 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + with: + persist-credentials: false - name: Build system information run: python3 .github/workflows/system-info.py diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index ee265774b2e..45392a689b1 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -28,57 +28,70 @@ concurrency: env: COVERAGE_CORE: sysmon + FORCE_COLOR: 1 jobs: build: - runs-on: windows-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14", "3.15"] + architecture: ["x64"] + os: ["windows-latest"] + include: + # Test the oldest Python on 32-bit + - { python-version: "3.10", architecture: "x86", os: "windows-2022" } - timeout-minutes: 30 + timeout-minutes: 45 - name: Python ${{ matrix.python-version }} + name: Python ${{ matrix.python-version }} (${{ matrix.architecture }}) steps: - name: Checkout Pillow - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + persist-credentials: false - name: Checkout cached dependencies - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: + persist-credentials: false repository: python-pillow/pillow-depends path: winbuild\depends - name: Checkout extra test images - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: + persist-credentials: false repository: python-pillow/test-images path: Tests\test-images # sets env: pythonLocation - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} allow-prereleases: true + 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: Install Python dependencies - run: > - python3 -m pip install - coverage>=7.4.2 - defusedxml - olefile - pyroma - pytest - pytest-cov - pytest-timeout + - name: Upgrade pip + run: | + python3 -m pip install --upgrade pip + + - name: Install CPython dependencies + if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'" + run: | + python3 -m pip install PyQt6 + + - name: Install PyArrow dependency + run: | + python3 -m pip install --only-binary=:all: pyarrow || true - name: Install dependencies id: install @@ -86,8 +99,8 @@ jobs: choco install nasm --no-progress echo "C:\Program Files\NASM" >> $env:GITHUB_PATH - choco install ghostscript --version=10.3.1 --no-progress - echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH + choco install ghostscript --version=10.7.0 --no-progress + echo "C:\Program Files\gs\gs10.07.0\bin" >> $env:GITHUB_PATH # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images @@ -100,7 +113,7 @@ jobs: - name: Cache build id: build-cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: winbuild\build key: @@ -137,6 +150,10 @@ jobs: if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libpng.cmd" + - name: Build dependencies / libavif + if: steps.build-cache.outputs.cache-hit != 'true' && matrix.architecture == 'x64' + run: "& winbuild\\build\\build_dep_libavif.cmd" + # for FreeType WOFF2 font support - name: Build dependencies / brotli if: steps.build-cache.outputs.cache-hit != 'true' @@ -172,13 +189,14 @@ jobs: # trim ~150MB for each job - name: Optimize build cache if: steps.build-cache.outputs.cache-hit != 'true' - run: rmdir /S /Q winbuild\build\src - shell: cmd + run: | + rm -rf winbuild\build\src + shell: bash - name: Build Pillow run: | $FLAGS="-C raqm=vendor -C fribidi=vendor" - cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ." + cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS .[tests]" & $env:pythonLocation\python.exe selftest.py --installed shell: pwsh @@ -190,9 +208,7 @@ jobs: - 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 + .ci\test.cmd - name: Prepare to upload errors if: failure() @@ -201,7 +217,7 @@ jobs: shell: bash - name: Upload errors - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: failure() with: name: errors @@ -213,9 +229,9 @@ jobs: shell: pwsh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: - file: ./coverage.xml + files: ./coverage.xml flags: GHA_Windows name: ${{ runner.os }} Python ${{ matrix.python-version }} token: ${{ secrets.CODECOV_ORG_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aa5646caf44..80bbfb45f67 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,6 +29,7 @@ concurrency: env: COVERAGE_CORE: sysmon FORCE_COLOR: 1 + PIP_DISABLE_PIP_VERSION_CHECK: 1 jobs: build: @@ -37,44 +38,43 @@ jobs: fail-fast: false matrix: os: [ - "macos-14", + "macos-latest", "ubuntu-latest", ] python-version: [ - "pypy3.10", - "pypy3.9", + "pypy3.11", + "3.15t", + "3.15", + "3.14t", + "3.14", + "3.13t", "3.13", "3.12", "3.11", "3.10", - "3.9", - "3.8", ] include: - - python-version: "3.11" - PYTHONOPTIMIZE: 1 - REVERSE: "--reverse" - - python-version: "3.10" - PYTHONOPTIMIZE: 2 - # M1 only available for 3.10+ - - os: "macos-13" - python-version: "3.9" - - os: "macos-13" - python-version: "3.8" + - { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } + - { python-version: "3.11", PYTHONOPTIMIZE: 2 } + # Free-threaded + - { python-version: "3.15t", disable-gil: true } + - { python-version: "3.14t", disable-gil: true } + - { python-version: "3.13t", disable-gil: true } + # Intel + - { os: "macos-26-intel", python-version: "3.10" } exclude: - - os: "macos-14" - python-version: "3.9" - - os: "macos-14" - python-version: "3.8" + - { os: "macos-latest", python-version: "3.10" } runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + with: + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -83,24 +83,47 @@ jobs: ".ci/*.sh" "pyproject.toml" + - name: Set PYTHON_GIL + if: "${{ matrix.disable-gil }}" + run: | + echo "PYTHON_GIL=0" >> $GITHUB_ENV + - name: Build system information run: python3 .github/workflows/system-info.py + - name: Cache libavif + if: startsWith(matrix.os, 'ubuntu') + uses: actions/cache@v5 + id: cache-libavif + with: + path: ~/cache-libavif + key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }} + - name: Cache libimagequant if: startsWith(matrix.os, 'ubuntu') - uses: actions/cache@v4 + uses: actions/cache@v5 id: cache-libimagequant with: path: ~/cache-libimagequant key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + - name: Cache libwebp + if: startsWith(matrix.os, 'ubuntu') + uses: actions/cache@v5 + id: cache-libwebp + with: + path: ~/cache-libwebp + key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }} + - name: Install Linux dependencies if: startsWith(matrix.os, 'ubuntu') run: | .ci/install.sh env: GHA_PYTHON_VERSION: ${{ matrix.python-version }} + GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }} GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} + GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }} - name: Install macOS dependencies if: startsWith(matrix.os, 'macOS') @@ -110,7 +133,7 @@ jobs: GHA_PYTHON_VERSION: ${{ matrix.python-version }} - name: Register gcc problem matcher - if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'" + if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'" run: echo "::add-matcher::.github/problem-matchers/gcc.json" - name: Build @@ -139,7 +162,7 @@ jobs: mkdir -p Tests/errors - name: Upload errors - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: failure() with: name: errors @@ -150,11 +173,10 @@ jobs: .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} - gcov: true token: ${{ secrets.CODECOV_ORG_TOKEN }} success: diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 97f70ed84e9..331eae2249b 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -1,150 +1,415 @@ #!/bin/bash -# Define custom utilities -# Test for macOS with [ -n "$IS_MACOS" ] -if [ -z "$IS_MACOS" ]; then - export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} - export MB_ML_VER=${AUDITWHEEL_POLICY:9} + +# Safety check - Pillow builds require that CIBW_ARCHS is set, and that it only +# contains a single value (even though cibuildwheel allows multiple values in +# CIBW_ARCHS). This check doesn't work on Linux because of how the CIBW_ARCHS +# variable is exposed. +function check_cibw_archs { + if [[ -z "$CIBW_ARCHS" ]]; then + echo "ERROR: Pillow builds require CIBW_ARCHS be defined." + exit 1 + fi + if [[ "$CIBW_ARCHS" == *" "* ]]; then + echo "ERROR: Pillow builds only support a single architecture in CIBW_ARCHS." + exit 1 + fi +} + +# Setup that needs to be done before multibuild utils are invoked. Process +# potential cross-build platforms before native platforms to ensure that we pick +# up the cross environment. +PROJECTDIR=$(pwd) +if [[ "$CIBW_PLATFORM" == "ios" ]]; then + check_cibw_archs + # On iOS, CIBW_ARCHS is actually a multi-arch - arm64_iphoneos, + # arm64_iphonesimulator or x86_64_iphonesimulator. Split into the CPU + # platform, and the iOS SDK. + PLAT=$(echo $CIBW_ARCHS | sed "s/\(.*\)_\(.*\)/\1/") + IOS_SDK=$(echo $CIBW_ARCHS | sed "s/\(.*\)_\(.*\)/\2/") + + # Build iOS builds in `build/iphoneos` or `build/iphonesimulator` + # (depending on the build target). Install them into `build/deps/iphoneos` + # or `build/deps/iphonesimulator` + WORKDIR=$(pwd)/build/$IOS_SDK + BUILD_PREFIX=$(pwd)/build/deps/$IOS_SDK + + # GNU tooling insists on using aarch64 rather than arm64 + if [[ $PLAT == "arm64" ]]; then + GNU_ARCH=aarch64 + else + GNU_ARCH=x86_64 + fi + + IOS_SDK_PATH=$(xcrun --sdk $IOS_SDK --show-sdk-path) + CMAKE_SYSTEM_NAME=iOS + IOS_HOST_TRIPLE=$PLAT-apple-ios$IPHONEOS_DEPLOYMENT_TARGET + if [[ "$IOS_SDK" == "iphonesimulator" ]]; then + IOS_HOST_TRIPLE=$IOS_HOST_TRIPLE-simulator + fi + + # GNU Autotools doesn't recognize the existence of arm64-apple-ios-simulator + # as a valid host. However, the only difference between arm64-apple-ios and + # arm64-apple-ios-simulator is the choice of sysroot, and that is + # coordinated by CC, CFLAGS etc. From the perspective of configure, the two + # platforms are identical, so we can use arm64-apple-ios consistently. + # This (mostly) avoids us needing to patch config.sub in dependency sources. + HOST_CONFIGURE_FLAGS="--disable-shared --enable-static --host=$GNU_ARCH-apple-ios --build=$GNU_ARCH-apple-darwin" + + # CMake has native support for iOS. However, most of that support is based + # on using the Xcode builder, which isn't very helpful for most of Pillow's + # dependencies. Therefore, we lean on the OSX configurations, plus CC, CFLAGS + # etc. to ensure the right sysroot is selected. + HOST_CMAKE_FLAGS="-DCMAKE_SYSTEM_NAME=$CMAKE_SYSTEM_NAME -DCMAKE_SYSTEM_PROCESSOR=$GNU_ARCH -DCMAKE_OSX_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET -DCMAKE_OSX_SYSROOT=$IOS_SDK_PATH -DBUILD_SHARED_LIBS=NO -DENABLE_SHARED=NO" + + # Meson needs to be pointed at a cross-platform configuration file + # This will be generated once CC etc. have been evaluated. + HOST_MESON_FLAGS="--cross-file $WORKDIR/meson-cross.txt -Dprefer_static=true -Ddefault_library=static" + +elif [[ "$(uname -s)" == "Darwin" ]]; then + check_cibw_archs + # Build macOS dependencies in `build/darwin` + # Install them into `build/deps/darwin` + PLAT=$CIBW_ARCHS + WORKDIR=$(pwd)/build/darwin + BUILD_PREFIX=$(pwd)/build/deps/darwin +else + # Build prefix will default to /usr/local + PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}" + WORKDIR=$(pwd)/build + MB_ML_LIBC=${AUDITWHEEL_POLICY::9} + MB_ML_VER=${AUDITWHEEL_POLICY:9} fi -export PLAT=$CIBW_ARCHS + +# Define custom utilities source wheels/multibuild/common_utils.sh source wheels/multibuild/library_builders.sh -if [ -z "$IS_MACOS" ]; then +if [[ -z "$IS_MACOS" ]]; then source wheels/multibuild/manylinux_utils.sh fi ARCHIVE_SDIR=pillow-depends-main -# Package versions for fresh source builds -FREETYPE_VERSION=2.13.2 -HARFBUZZ_VERSION=8.5.0 -LIBPNG_VERSION=1.6.43 -JPEGTURBO_VERSION=3.0.3 -OPENJPEG_VERSION=2.5.2 -XZ_VERSION=5.4.5 -TIFF_VERSION=4.6.0 -LCMS2_VERSION=2.16 -if [[ -n "$IS_MACOS" ]]; then - GIFLIB_VERSION=5.2.2 -else - GIFLIB_VERSION=5.2.1 -fi -if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then - ZLIB_VERSION=1.3.1 -else - ZLIB_VERSION=1.2.8 -fi -LIBWEBP_VERSION=1.4.0 +# Package versions for fresh source builds. +FREETYPE_VERSION=2.14.3 +HARFBUZZ_VERSION=13.2.1 +LIBPNG_VERSION=1.6.56 +JPEGTURBO_VERSION=3.1.4.1 +OPENJPEG_VERSION=2.5.4 +XZ_VERSION=5.8.3 +ZSTD_VERSION=1.5.7 +TIFF_VERSION=4.7.1 +LCMS2_VERSION=2.18 +ZLIB_NG_VERSION=2.3.3 +LIBWEBP_VERSION=1.6.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 -BROTLI_VERSION=1.1.0 - -if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then - function build_openjpeg { - local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz) - (cd $out_dir \ - && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ - && make install) - touch openjpeg-stamp - } -fi +BROTLI_VERSION=1.2.0 +LIBAVIF_VERSION=1.4.1 + +function build_pkg_config { + if [ -e pkg-config-stamp ]; then return; fi + # This essentially duplicates the Homebrew recipe. + # On iOS, we need a binary that can be executed on the build machine; but we + # can create a host-specific pc-path to store iOS .pc files. To ensure a + # macOS-compatible build, we temporarily clear environment flags that set + # iOS-specific values. + if [[ -n "$IOS_SDK" ]]; then + ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS + ORIGINAL_IPHONEOS_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET + unset HOST_CONFIGURE_FLAGS + unset IPHONEOS_DEPLOYMENT_TARGET + fi + + CFLAGS="$CFLAGS -Wno-int-conversion" CPPFLAGS="" build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \ + --disable-debug --disable-host-tool --with-internal-glib \ + --with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \ + --with-system-include-path=$(xcrun --show-sdk-path --sdk macosx)/usr/include + + if [[ -n "$IOS_SDK" ]]; then + HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS + IPHONEOS_DEPLOYMENT_TARGET=$ORIGINAL_IPHONEOS_DEPLOYMENT_TARGET + fi; + + export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config + touch pkg-config-stamp +} + +function build_zlib_ng { + if [ -e zlib-stamp ]; then return; fi + # zlib-ng uses a "configure" script, but it's not a GNU autotools script, so + # it doesn't honor the usual flags. Temporarily disable any + # cross-compilation flags. + ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS + unset HOST_CONFIGURE_FLAGS + + build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --installnamedir=$BUILD_PREFIX/lib --zlib-compat + + HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS + touch zlib-stamp +} function build_brotli { - local cmake=$(get_modern_cmake) - local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-1.1.0.tar.gz) + if [ -e brotli-stamp ]; then return; fi + local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) + (cd $out_dir \ + && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib -DCMAKE_MACOSX_BUNDLE=OFF $HOST_CMAKE_FLAGS . \ + && make -j4 install) + touch brotli-stamp +} + +function build_harfbuzz { + if [ -e harfbuzz-stamp ]; then return; fi + python3 -m pip install meson ninja + + local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ - && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ - && make install) - if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp /usr/local/lib64/libbrotli* /usr/local/lib - cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig + && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=minsize -Dfreetype=enabled -Dglib=disabled -Dtests=disabled $HOST_MESON_FLAGS) + (cd $out_dir/build \ + && meson install) + touch harfbuzz-stamp +} + +function build_libavif { + if [ -e libavif-stamp ]; then return; fi + + python3 -m pip install meson ninja + + if ([[ "$PLAT" == "x86_64" ]] && [[ -z "$IOS_SDK" ]]) || [ -n "$SANITIZER" ]; then + build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03 fi + + local build_type=MinSizeRel + local build_shared=ON + local lto=ON + + local libavif_cmake_flags + + if [[ -n "$IS_MACOS" ]]; then + lto=OFF + libavif_cmake_flags=( + -DCMAKE_C_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \ + -DCMAKE_CXX_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \ + -DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,-S,-x,-dead_strip_dylibs" \ + ) + if [[ -n "$IOS_SDK" ]]; then + build_shared=OFF + fi + else + if [[ "$MB_ML_VER" == 2014 ]] && [[ "$PLAT" == "x86_64" ]]; then + build_type=Release + fi + libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now") + fi + if [[ -n "$IOS_SDK" ]] && [[ "$PLAT" == "x86_64" ]]; then + libavif_cmake_flags+=(-DAOM_TARGET_CPU=generic) + else + libavif_cmake_flags+=( + -DAVIF_CODEC_AOM_DECODE=OFF \ + -DAVIF_CODEC_DAV1D=LOCAL + ) + fi + + local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz) + + # CONFIG_AV1_HIGHBITDEPTH=0 is a flag for libaom (included as a subproject + # of libavif) that disables support for encoding high bit depth images. + (cd $out_dir \ + && cmake \ + -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \ + -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \ + -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \ + -DBUILD_SHARED_LIBS=$build_shared \ + -DAVIF_LIBSHARPYUV=LOCAL \ + -DAVIF_LIBYUV=LOCAL \ + -DAVIF_CODEC_AOM=LOCAL \ + -DCONFIG_AV1_HIGHBITDEPTH=0 \ + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=$lto \ + -DCMAKE_C_VISIBILITY_PRESET=hidden \ + -DCMAKE_CXX_VISIBILITY_PRESET=hidden \ + -DCMAKE_BUILD_TYPE=$build_type \ + "${libavif_cmake_flags[@]}" \ + $HOST_CMAKE_FLAGS . ) + + if [[ -n "$IOS_SDK" ]]; then + # libavif's CMake configuration generates a meson cross file... but it + # doesn't work for iOS cross-compilation. Copy in Pillow-generated + # meson-cross config to replace the cmake-generated version. + cp $WORKDIR/meson-cross.txt $out_dir/crossfile-apple.meson + fi + + (cd $out_dir && make -j4 install) + + touch libavif-stamp +} + +function build_zstd { + if [ -e zstd-stamp ]; then return; fi + local out_dir=$(fetch_unpack https://github.com/facebook/zstd/releases/download/v$ZSTD_VERSION/zstd-$ZSTD_VERSION.tar.gz) + (cd $out_dir \ + && make -j4 install) + touch zstd-stamp } function build { - if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then - sudo chown -R runner /usr/local - fi build_xz - if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then + if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel fi - build_new_zlib + if [[ -n "$IS_MACOS" ]]; then + CFLAGS="$CFLAGS -headerpad_max_install_names" build_zlib_ng + else + build_zlib_ng + fi build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto - if [ -n "$IS_MACOS" ]; then - build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto - build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib + if [[ -n "$IS_MACOS" ]]; then + build_simple xorgproto 2025.1 https://www.x.org/pub/individual/proto + build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist - if [[ "$CIBW_ARCHS" == "arm64" ]]; then - cp /usr/local/share/pkgconfig/xcb-proto.pc /usr/local/lib/pkgconfig - fi else - sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc + sed "s/\${pc_sysrootdir\}//" $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc fi build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib build_libjpeg_turbo - build_tiff + if [[ -n "$IS_MACOS" ]]; then + # Custom tiff build to include jpeg; by default, configure won't include + # headers/libs in the custom macOS/iOS prefix. Explicitly disable webp, + # libdeflate and zstd, because on x86_64 macs, it will pick up the + # Homebrew versions of those libraries from /usr/local. + build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ + --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ + --disable-webp --disable-libdeflate --disable-zstd + else + build_zstd + build_tiff + fi + + build_libavif build_libpng build_lcms2 build_openjpeg - if [ -f /usr/local/lib64/libopenjp2.so ]; then - cp /usr/local/lib64/libopenjp2.so /usr/local/lib - fi - ORIGINAL_CFLAGS=$CFLAGS - CFLAGS="$CFLAGS -O3 -DNDEBUG" + webp_cflags="-O3 -DNDEBUG" if [[ -n "$IS_MACOS" ]]; then - CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" + webp_cflags="$webp_cflags -Wl,-headerpad_max_install_names" + fi + webp_ldflags="" + if [[ -n "$IOS_SDK" ]]; then + webp_ldflags="$webp_ldflags -llzma -lz" fi - build_libwebp - CFLAGS=$ORIGINAL_CFLAGS + CFLAGS="$CFLAGS $webp_cflags" LDFLAGS="$LDFLAGS $webp_ldflags" build_simple libwebp $LIBWEBP_VERSION \ + https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \ + --enable-libwebpmux --enable-libwebpdemux build_brotli - if [ -n "$IS_MACOS" ]; then + if [[ -n "$IS_MACOS" ]]; then # Custom freetype build build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no else build_freetype fi - if [ -z "$IS_MACOS" ]; then - export FREETYPE_LIBS=-lfreetype - export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/ - fi - build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no - if [ -z "$IS_MACOS" ]; then - export FREETYPE_LIBS="" - export FREETYPE_CFLAGS="" + if [[ -z "$IOS_SDK" ]]; then + # On iOS, there's no vendor-provided raqm, and we can't ship it due to + # licensing, so there's no point building harfbuzz. + build_harfbuzz fi } +function create_meson_cross_config { + cat << EOF > $WORKDIR/meson-cross.txt +[binaries] +pkg-config = '$BUILD_PREFIX/bin/pkg-config' +cmake = '$(which cmake)' +c = '$CC' +cpp = '$CXX' +strip = '$STRIP' + +[built-in options] +c_args = '$CFLAGS -I$BUILD_PREFIX/include' +cpp_args = '$CXXFLAGS -I$BUILD_PREFIX/include' +c_link_args = '$CFLAGS -L$BUILD_PREFIX/lib' +cpp_link_args = '$CFLAGS -L$BUILD_PREFIX/lib' + +[host_machine] +system = 'darwin' +subsystem = 'ios' +kernel = 'xnu' +cpu_family = '$(uname -m)' +cpu = '$(uname -m)' +endian = 'little' + +EOF +} + +# Perform all dependency builds in the build subfolder. +mkdir -p $WORKDIR +pushd $WORKDIR > /dev/null + # Any stuff that you need to do before you start building the wheels # Runs in the root directory of this repository. -curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip -untar pillow-depends-main.zip +if [[ ! -d $WORKDIR/pillow-depends-main ]]; then + if [[ ! -f $PROJECTDIR/pillow-depends-main.zip ]]; then + echo "Download pillow dependency sources..." + curl -fSL -o $PROJECTDIR/pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip + fi + echo "Unpacking pillow dependency sources..." + untar $PROJECTDIR/pillow-depends-main.zip +fi if [[ -n "$IS_MACOS" ]]; then - # libtiff and libxcb cause a conflict with building libtiff and libxcb - # libxau and libxdmcp cause an issue on macOS < 11 - # remove cairo to fix building harfbuzz on arm64 - # remove lcms2 and libpng to fix building openjpeg on arm64 - # remove jpeg-turbo to avoid inclusion on arm64 - # remove webp and zstd to avoid inclusion on x86_64 - # curl from brew requires zstd, use system curl - brew remove --ignore-dependencies libpng libtiff libxcb libxau libxdmcp curl cairo lcms2 zstd - if [[ "$CIBW_ARCHS" == "arm64" ]]; then - brew remove --ignore-dependencies jpeg-turbo - else - brew remove --ignore-dependencies webp - fi + # Ensure the basic structure of the build prefix directory exists. + mkdir -p "$BUILD_PREFIX/bin" + mkdir -p "$BUILD_PREFIX/lib" + + # Ensure pkg-config is available. This is done *before* setting CC, CFLAGS + # etc. to ensure that the build is *always* a macOS build, even when building + # for iOS. + build_pkg_config + + # Ensure cmake is available, and that the default prefix used by CMake is + # the build prefix + python3 -m pip install cmake + export CMAKE_PREFIX_PATH=$BUILD_PREFIX + + if [[ -n "$IOS_SDK" ]]; then + export AR="$(xcrun --find --sdk $IOS_SDK ar)" + export CPP="$(xcrun --find --sdk $IOS_SDK clang) -E" + export CC=$(xcrun --find --sdk $IOS_SDK clang) + export CXX=$(xcrun --find --sdk $IOS_SDK clang++) + export LD=$(xcrun --find --sdk $IOS_SDK ld) + export STRIP=$(xcrun --find --sdk $IOS_SDK strip) + + CPPFLAGS="$CPPFLAGS --sysroot=$IOS_SDK_PATH" + CFLAGS="-target $IOS_HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET" + CXXFLAGS="-target $IOS_HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET" - brew install pkg-config + # Having IPHONEOS_DEPLOYMENT_TARGET in the environment causes problems + # with some cross-building toolchains, because it introduces implicit + # behavior into clang. + unset IPHONEOS_DEPLOYMENT_TARGET + + # Now that we know CC etc., we can create a meson cross-configuration file + create_meson_cross_config + fi fi wrap_wheel_builder build +# A safety catch for iOS. iOS can't use dynamic libraries, but clang will prefer +# to link dynamic libraries to static libraries. The only way to reliably +# prevent this is to not have dynamic libraries available in the first place. +# The build process *shouldn't* generate any dylibs... but just in case, purge +# any dylibs that *have* been installed into the build prefix directory. +if [[ -n "$IOS_SDK" ]]; then + find "$BUILD_PREFIX" -name "*.dylib" -exec rm -rf {} \; +fi + +# Return to the project root to finish the build +popd > /dev/null + # Append licenses for filename in wheels/dependency_licenses/*; do echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1 index f593c722854..e6453d09118 100644 --- a/.github/workflows/wheels-test.ps1 +++ b/.github/workflows/wheels-test.ps1 @@ -9,14 +9,18 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") { C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null } $env:path += ";$pillow\winbuild\build\bin\" -& "$venv\Scripts\activate.ps1" +if (Test-Path $venv\Scripts\pypy.exe) { + $python = "pypy.exe" +} else { + $python = "python.exe" +} & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f cd $pillow -& python -VV +& $venv\Scripts\$python -VV if (!$?) { exit $LASTEXITCODE } -& python selftest.py +& $venv\Scripts\$python selftest.py if (!$?) { exit $LASTEXITCODE } -& python -m pytest -vx Tests\check_wheel.py +& $venv\Scripts\$python -m pytest -vv -x checks\check_wheel.py if (!$?) { exit $LASTEXITCODE } -& python -m pytest -vx Tests +& $venv\Scripts\$python -m pytest -vv -x Tests if (!$?) { exit $LASTEXITCODE } diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index a3376ac929c..d73b6be5847 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -1,20 +1,29 @@ #!/bin/bash set -e +# Ensure fribidi is installed by the system. if [[ "$OSTYPE" == "darwin"* ]]; then - brew install fribidi - export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" - if [ -f /opt/homebrew/lib/libfribidi.dylib ]; then - sudo cp /opt/homebrew/lib/libfribidi.dylib /usr/local/lib + # If Homebrew is on the path during the build, it may leak into the wheels. + # However, we *do* need Homebrew to provide a copy of fribidi for + # testing purposes so that we can verify the fribidi shim works as expected. + if [[ "$(uname -m)" == "x86_64" ]]; then + HOMEBREW_PREFIX=/usr/local + else + HOMEBREW_PREFIX=/opt/homebrew fi + $HOMEBREW_PREFIX/bin/brew install fribidi + + # Add the lib folder for fribidi so that the vendored library can be found. + # Don't use $HOMEWBREW_PREFIX/lib directly - use the lib folder where the + # installed copy of fribidi is cellared. This ensures we don't pick up the + # Homebrew version of any other library that we're dependent on (most notably, + # freetype). + export DYLD_LIBRARY_PATH=$(dirname $(realpath $HOMEBREW_PREFIX/lib/libfribidi.dylib)) elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then apk add curl fribidi else yum install -y fribidi fi -if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ] && !([[ "$OSTYPE" == "darwin"* ]] && [[ $(python3 --version) == *"3.13."* ]]); then - python3 -m pip install numpy -fi if [ ! -d "test-images-main" ]; then curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip @@ -24,5 +33,5 @@ fi # Runs tests python3 selftest.py -python3 -m pytest Tests/check_wheel.py -python3 -m pytest +python3 -m pytest -vv -x checks/check_wheel.py +python3 -m pytest -vv -x diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3d6099c1cf5..af2f9b3e825 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,10 +1,19 @@ name: Wheels on: + schedule: + # ┌───────────── minute (0 - 59) + # │ ┌───────────── hour (0 - 23) + # │ │ ┌───────────── day of the month (1 - 31) + # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) + # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) + # │ │ │ │ │ + - cron: "42 1 * * 0,3" push: paths: - ".ci/requirements-cibw.txt" - ".github/workflows/wheel*" + - "pyproject.toml" - "setup.py" - "wheels/*" - "winbuild/build_prepare.py" @@ -15,6 +24,7 @@ on: paths: - ".ci/requirements-cibw.txt" - ".github/workflows/wheel*" + - "pyproject.toml" - "setup.py" - "wheels/*" - "winbuild/build_prepare.py" @@ -29,97 +39,80 @@ concurrency: cancel-in-progress: true env: + EXPECTED_DISTS: 91 FORCE_COLOR: 1 jobs: - build-1-QEMU-emulated-wheels: - name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: - - pp39 - - pp310 - - cp38 - - cp39 - - cp310 - - cp311 - - cp312 - - cp313 - spec: - - manylinux2014 - - manylinux_2_28 - - musllinux - exclude: - - { python-version: pp39, spec: musllinux } - - { python-version: pp310, spec: musllinux } - - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - # https://github.com/docker/setup-qemu-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Install cibuildwheel - run: | - python3 -m pip install -r .ci/requirements-cibw.txt - - - name: Build wheels - run: | - python3 -m cibuildwheel --output-dir wheelhouse - env: - # Build only the currently selected Linux architecture (so we can - # parallelise for speed). - CIBW_ARCHS: "aarch64" - # Likewise, select only one Python version per job to speed this up. - CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*" - CIBW_PRERELEASE_PYTHONS: True - # Extra options for manylinux. - CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} - - - uses: actions/upload-artifact@v4 - with: - name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }} - path: ./wheelhouse/*.whl - - build-2-native-wheels: + build-native-wheels: + if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' name: ${{ matrix.name }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - - name: "macOS x86_64" - os: macos-13 + - name: "macOS 10.10 x86_64" + platform: macos + os: macos-26-intel cibw_arch: x86_64 + build: "cp3{10,11}*" macosx_deployment_target: "10.10" + - name: "macOS 10.13 x86_64" + platform: macos + os: macos-26-intel + cibw_arch: x86_64 + build: "cp3{12,13}*" + macosx_deployment_target: "10.13" + - name: "macOS 10.15 x86_64" + platform: macos + os: macos-26-intel + cibw_arch: x86_64 + build: "{cp314,pp3}*" + macosx_deployment_target: "10.15" - name: "macOS arm64" - os: macos-14 + platform: macos + os: macos-latest cibw_arch: arm64 macosx_deployment_target: "11.0" - name: "manylinux2014 and musllinux x86_64" + platform: linux os: ubuntu-latest cibw_arch: x86_64 + manylinux: "manylinux2014" - name: "manylinux_2_28 x86_64" + platform: linux os: ubuntu-latest cibw_arch: x86_64 build: "*manylinux*" - manylinux: "manylinux_2_28" + - name: "manylinux2014 and musllinux aarch64" + platform: linux + os: ubuntu-24.04-arm + cibw_arch: aarch64 + manylinux: "manylinux2014" + - name: "manylinux_2_28 aarch64" + platform: linux + os: ubuntu-24.04-arm + cibw_arch: aarch64 + build: "*manylinux*" + - name: "iOS arm64 device" + platform: ios + os: macos-latest + cibw_arch: arm64_iphoneos + - name: "iOS arm64 simulator" + platform: ios + os: macos-latest + cibw_arch: arm64_iphonesimulator + - name: "iOS x86_64 simulator" + platform: ios + os: macos-15-intel + cibw_arch: x86_64_iphonesimulator steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: + persist-credentials: false submodules: true - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.x" @@ -131,40 +124,48 @@ jobs: run: | python3 -m cibuildwheel --output-dir wheelhouse env: + CIBW_PLATFORM: ${{ matrix.platform }} CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BUILD: ${{ matrix.build }} + CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy + CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.manylinux }} + CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} - CIBW_PRERELEASE_PYTHONS: True - CIBW_SKIP: pp38-* - CIBW_TEST_SKIP: cp38-macosx_arm64 MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v6 with: - name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} + name: dist-${{ matrix.name }} path: ./wheelhouse/*.whl windows: + if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' name: Windows ${{ matrix.cibw_arch }} - runs-on: windows-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - cibw_arch: x86 + os: windows-latest - cibw_arch: AMD64 + os: windows-latest - cibw_arch: ARM64 + os: windows-11-arm steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + with: + persist-credentials: false - name: Checkout extra test images - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: + persist-credentials: false repository: python-pillow/test-images path: Tests\test-images - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.x" @@ -185,30 +186,23 @@ jobs: - name: Build wheels run: | - 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 - ) - ) - ) - call winbuild\\build\\build_env.cmd - %pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse + for f in winbuild/build/license/*; do + name=$(basename "${f%.*}") + # Skip FriBiDi license, it is not included in the wheel. + [[ $name == fribidi* ]] && continue + # Skip imagequant license, it is not included in the wheel. + [[ $name == libimagequant* ]] && continue + echo "" >> LICENSE + echo "===== $name =====" >> LICENSE + echo "" >> LICENSE + cat "$f" >> LICENSE + done + cmd //c "winbuild\\build\\build_env.cmd && $pythonLocation\\python.exe -m cibuildwheel . --output-dir wheelhouse" env: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_CACHE_PATH: "C:\\cibw" - CIBW_PRERELEASE_PYTHONS: True - CIBW_SKIP: pp38-* + CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm -v {project}:C:\pillow @@ -217,42 +211,78 @@ jobs: -e CI -e GITHUB_ACTIONS mcr.microsoft.com/windows/servercore:ltsc2022 powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test' - shell: cmd + shell: bash - name: Upload wheels - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: dist-windows-${{ matrix.cibw_arch }} path: ./wheelhouse/*.whl - name: Upload fribidi.dll - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: fribidi-windows-${{ matrix.cibw_arch }} path: winbuild\build\bin\fribidi* sdist: + if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + with: + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.x" - cache: pip - cache-dependency-path: "Makefile" - run: make sdist - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v6 with: name: dist-sdist path: dist/*.tar.gz + count-dists: + needs: [build-native-wheels, windows, sdist] + runs-on: ubuntu-latest + name: Count dists + steps: + - uses: actions/download-artifact@v8 + with: + pattern: dist-* + path: dist + merge-multiple: true + - name: "What did we get?" + run: | + ls -alR + echo "Number of dists, should be $EXPECTED_DISTS:" + files=$(ls dist 2>/dev/null | wc -l) + echo $files + [ "$files" -eq $EXPECTED_DISTS ] || exit 1 + + scientific-python-nightly-wheels-publish: + if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') + needs: count-dists + runs-on: ubuntu-latest + name: Upload wheels to scientific-python-nightly-wheels + steps: + - uses: actions/download-artifact@v8 + with: + pattern: dist-!(sdist)* + path: dist + merge-multiple: true + - name: Upload wheels to scientific-python-nightly-wheels + uses: scientific-python/upload-nightly-action@5748273c71e2d8d3a61f3a11a16421c8954f9ecf # 0.6.3 + with: + artifacts_path: dist + anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} + pypi-publish: - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist] + if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + needs: count-dists runs-on: ubuntu-latest name: Upload release to PyPI environment: @@ -261,10 +291,12 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: pattern: dist-* path: dist merge-multiple: true - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + with: + attestations: true diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 00000000000..10002656245 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,6 @@ +# https://docs.zizmor.sh/configuration/ +rules: + unpinned-uses: + config: + policies: + "*": ref-pin diff --git a/.gitignore b/.gitignore index 1dd6c917524..3033c2ea7ae 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ lib64/ parts/ sdist/ var/ +wheelhouse/ *.egg-info/ .installed.cfg *.egg @@ -90,5 +91,9 @@ Tests/images/msp Tests/images/picins Tests/images/sunraster +# Test and dependency downloads +pillow-depends-main.zip +pillow-test-images.zip + # pyinstaller *.spec diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6a76e8c00cb..53fd0a3ca59 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,30 +1,30 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.7 + rev: v0.15.4 hooks: - - id: ruff + - id: ruff-check args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.4.2 + rev: 26.1.0 hooks: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.7.8 + rev: 1.9.4 hooks: - id: bandit args: [--severity-level=high] files: ^src/ - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.5 + rev: v1.5.6 hooks: - id: remove-tabs exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v18.1.5 + rev: v22.1.0 hooks: - id: clang-format types: [c] @@ -36,43 +36,51 @@ repos: - id: rst-backticks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v6.0.0 hooks: + - id: check-case-conflict - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable - id: check-merge-conflict - id: check-json - id: check-toml - id: check-yaml + args: [--allow-multiple-documents] - id: end-of-file-fixer exclude: ^Tests/images/ - id: trailing-whitespace - exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ + exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.28.4 + rev: 0.37.0 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: v1.22.0 + hooks: + - id: zizmor + - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: v0.9.1 + rev: v1.0.2 hooks: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 1.8.0 + rev: v2.16.2 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.18 + rev: v0.25 hooks: - id: validate-pyproject + additional_dependencies: [trove-classifiers>=2024.10.12] - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.3.1 + rev: 1.7.1 hooks: - id: tox-ini-fmt diff --git a/.readthedocs.yml b/.readthedocs.yml index def6282dd56..3e03c76ea9b 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,5 +1,8 @@ version: 2 +sphinx: + configuration: docs/conf.py + formats: [pdf] build: diff --git a/CHANGES.rst b/CHANGES.rst index d071f3214bc..dfbbd24b331 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,181 @@ Changelog (Pillow) ================== +11.1.0 and newer +---------------- + +See GitHub Releases: + +- https://github.com/python-pillow/Pillow/releases + +11.0.0 (2024-10-15) +------------------- + +- Update licence to MIT-CMU #8460 + [hugovk] + +- Conditionally define ImageCms type hint to avoid requiring core #8197 + [radarhere] + +- Support writing LONG8 offsets in AppendingTiffWriter #8417 + [radarhere] + +- Use ImageFile.MAXBLOCK when saving TIFF images #8461 + [radarhere] + +- Do not close provided file handles with libtiff when saving #8458 + [radarhere] + +- Support ImageFilter.BuiltinFilter for I;16* images #8438 + [radarhere] + +- Use ImagingCore.ptr instead of ImagingCore.id #8341 + [homm, radarhere, hugovk] + +- Updated EPS mode when opening images without transparency #8281 + [Yay295, radarhere] + +- Use transparency when combining P frames from APNGs #8443 + [radarhere] + +- Support all resampling filters when resizing I;16* images #8422 + [radarhere] + +- Free memory on early return #8413 + [radarhere] + +- Cast int before potentially exceeding INT_MAX #8402 + [radarhere] + +- Check image value before use #8400 + [radarhere] + +- Improved copying imagequant libraries #8420 + [radarhere] + +- Use Capsule for WebP saving #8386 + [homm, radarhere] + +- Fixed writing multiple StripOffsets to TIFF #8317 + [Yay295, radarhere] + +- Fix dereference before checking for NULL in ImagingTransformAffine #8398 + [PavlNekrasov] + +- Use transposed size after opening for TIFF images #8390 + [radarhere, homm] + +- Improve ImageFont error messages #8338 + [yngvem, radarhere, hugovk] + +- Mention MAX_TEXT_CHUNK limit in PNG error message #8391 + [radarhere] + +- Cast Dib handle to int #8385 + [radarhere] + +- Accept float stroke widths #8369 + [radarhere] + +- Deprecate ICNS (width, height, scale) sizes in favour of load(scale) #8352 + [radarhere] + +- Improved handling of RGBA palettes when saving GIF images #8366 + [radarhere] + +- Deprecate isImageType #8364 + [radarhere] + +- Support converting more modes to LAB by converting to RGBA first #8358 + [radarhere] + +- Deprecate support for FreeType 2.9.0 #8356 + [hugovk, radarhere] + +- Removed unused TiffImagePlugin IFD_LEGACY_API #8355 + [radarhere] + +- Handle duplicate EXIF header #8350 + [zakajd, radarhere] + +- Return early from BoxBlur if either width or height is zero #8347 + [radarhere] + +- Check text is either string or bytes #8308 + [radarhere] + +- Added writing XMP bytes to JPEG #8286 + [radarhere] + +- Support JPEG2000 RGBA palettes #8256 + [radarhere] + +- Expand C image to match GIF frame image size #8237 + [radarhere] + +- Allow saving I;16 images as PPM #8231 + [radarhere] + +- When IFD is missing, connect get_ifd() dictionary to Exif #8230 + [radarhere] + +- Skip truncated ICO mask if LOAD_TRUNCATED_IMAGES is enabled #8180 + [radarhere] + +- Treat unknown JPEG2000 colorspace as unspecified #8343 + [radarhere] + +- Updated error message when saving WebP with invalid width or height #8322 + [radarhere, hugovk] + +- Remove warning if NumPy failed to raise an error during conversion #8326 + [radarhere] + +- If left and right sides meet in ImageDraw.rounded_rectangle(), do not draw rectangle to fill gap #8304 + [radarhere] + +- Remove WebP support without anim, mux/demux, and with buggy alpha #8213 + [homm, radarhere] + +- Add missing TIFF CMYK;16B reader #8298 + [homm] + +- Remove all WITH_* flags from _imaging.c and other flags #8211 + [homm] + +- Improve ImageDraw2 shape methods #8265 + [radarhere] + +- Lock around usages of imaging memory arenas #8238 + [lysnikolaou] + +- Deprecate JpegImageFile huffman_ac and huffman_dc #8274 + [radarhere] + +- Deprecate ImageMath lambda_eval and unsafe_eval options argument #8242 + [radarhere] + +- Changed ContainerIO to subclass IO #8240 + [radarhere] + +- Move away from APIs that use borrowed references under the free-threaded build #8216 + [hugovk, lysnikolaou] + +- Allow size argument to resize() to be a NumPy array #8201 + [radarhere] + +- Drop support for Python 3.8 #8183 + [hugovk, radarhere] + +- Add support for Python 3.13 #8181 + [hugovk, radarhere] + +- Fix incompatibility with NumPy 1.20 #8187 + [neutrinoceros, radarhere] + +- Remove PSFile, PyAccess and USE_CFFI_ACCESS #8182 + [hugovk, radarhere] + 10.4.0 (2024-07-01) ------------------- diff --git a/LICENSE b/LICENSE index 7990a6e57dc..c011511a4ab 100644 --- a/LICENSE +++ b/LICENSE @@ -5,9 +5,9 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2024 by Jeffrey A. Clark and contributors + Copyright © 2010 by Jeffrey 'Alex' Clark and contributors -Like PIL, Pillow is licensed under the open source HPND License: +Like PIL, Pillow is licensed under the open source MIT-CMU License: By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply diff --git a/MANIFEST.in b/MANIFEST.in index af25dfd2db5..d4623a4a8e5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,6 +13,8 @@ include LICENSE include Makefile include tox.ini graft Tests +graft Tests/images +graft checks graft src graft depends graft winbuild @@ -20,15 +22,25 @@ graft docs graft _custom_build # build/src control detritus -exclude .appveyor.yml exclude .clang-format exclude .coveragerc exclude .editorconfig exclude .readthedocs.yml exclude codecov.yml exclude renovate.json +exclude Tests/images/README.md +exclude Tests/images/crash*.tif +exclude Tests/images/string_dimension.tiff global-exclude .git* global-exclude *.pyc global-exclude *.so prune .ci prune wheels +prune winbuild/build +prune winbuild/depends +prune Tests/errors +prune Tests/images/jpeg2000 +prune Tests/images/msp +prune Tests/images/picins +prune Tests/images/sunraster +prune Tests/test-images diff --git a/Makefile b/Makefile index 94f7565d826..6e050c715d2 100644 --- a/Makefile +++ b/Makefile @@ -17,14 +17,16 @@ coverage: .PHONY: doc .PHONY: html doc html: - python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . $(MAKE) -C docs html .PHONY: htmlview htmlview: - python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . $(MAKE) -C docs htmlview +.PHONY: htmllive +htmllive: + $(MAKE) -C docs htmllive + .PHONY: doccheck doccheck: $(MAKE) doc @@ -45,6 +47,7 @@ help: @echo " docserve run an HTTP server on the docs directory" @echo " html make HTML docs" @echo " htmlview open the index page built by the html target in your browser" + @echo " htmllive rebuild and reload HTML files in your browser" @echo " install make and install" @echo " install-coverage make and install with C coverage" @echo " lint run the lint checks" @@ -72,7 +75,7 @@ debug: .PHONY: release-test release-test: - python3 Tests/check_release_notes.py + python3 checks/check_release_notes.py python3 -m pip install -e .[tests] python3 selftest.py python3 -m pytest Tests @@ -94,13 +97,27 @@ test: python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest python3 -m pytest -qq +.PHONY: test-p +test-p: + python3 -c "import xdist" > /dev/null 2>&1 || python3 -m pip install pytest-xdist + python3 -m pytest -qq -n auto + + .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 \ + PILLOW_VALGRIND_TEST=true 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: valgrind-leak +valgrind-leak: + python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind + PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ + --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ + --log-file=/tmp/valgrind-output \ + python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output + .PHONY: readme readme: python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 @@ -117,7 +134,7 @@ lint-fix: python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black python3 -m black . python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff - python3 -m ruff --fix . + python3 -m ruff check --fix . .PHONY: mypy mypy: diff --git a/README.md b/README.md index b4c6d2987ff..04c9ae8abd8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## Python Imaging Library (Fork) -Pillow is the friendly PIL fork by [Jeffrey A. Clark and +Pillow is the friendly PIL fork by [Jeffrey '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 @@ -36,22 +36,16 @@ As of 2019, Pillow development is 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 build status (Wheels) Code coverage - Fuzzing Status @@ -98,7 +92,7 @@ This library provides extensive file format support, an efficient internal repre 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 +## More information - [Documentation](https://pillow.readthedocs.io/) - [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html) @@ -107,9 +101,9 @@ The core image library is designed for fast access to data stored in a few basic - [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) +- [Changelog](https://github.com/python-pillow/Pillow/releases) - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork) -## Report a Vulnerability +## 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/RELEASING.md b/RELEASING.md index 9e6ec5dd4c1..3c6188c8261 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,46 +1,25 @@ -# Release Checklist +# 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 +## Main release Released quarterly on January 2nd, April 1st, July 1st and October 15th. -* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 -* [ ] Develop and prepare release in `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 the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them. -* [ ] In compliance with [PEP 440](https://peps.python.org/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.: - ```bash - git branch 5.2.x - git tag 5.2.0 - git push --tags - ``` -* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) - has passed, including the "Upload release to PyPI" job. This will have been triggered - by the new tag. -* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases). -* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), - increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then: - ```bash - git push --all - ``` -## Point Release +* [ ] Create a new issue and select the "Maintainers only: Release" template. + +## Point release Released as needed for security, installation or critical bug fixes. * [ ] Make necessary changes in `main` branch. -* [ ] Update `CHANGES.rst`. * [ ] Check out release branch e.g.: ```bash git checkout -t remotes/origin/5.2.x ``` * [ ] Cherry pick individual commits from `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`. +* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in release branch e.g. `5.2.x`. * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Run pre-release check via `make release-test`. * [ ] Create tag for release e.g.: @@ -60,7 +39,7 @@ Released as needed for security, installation or critical bug fixes. git push ``` -## Embargoed Release +## Embargoed release Released as needed privately to individual vendors for critical security-related bug fixes. @@ -84,7 +63,7 @@ Released as needed privately to individual vendors for critical security-related git push origin 2.5.x ``` -## Publicize Release +## Publicize release * [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321 @@ -92,7 +71,7 @@ Released as needed privately to individual vendors for critical security-related * [ ] 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 +## Docker images * [ ] Update Pillow in the Docker Images repository ```bash diff --git a/Tests/README.rst b/Tests/README.rst index 2d014e5a46e..a955ec4fa56 100644 --- a/Tests/README.rst +++ b/Tests/README.rst @@ -1,4 +1,4 @@ -Pillow Tests +Pillow tests ============ Test scripts are named ``test_xxx.py``. Helper classes and functions can be found in ``helper.py``. diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py deleted file mode 100644 index c7d105836aa..00000000000 --- a/Tests/bench_cffi_access.py +++ /dev/null @@ -1,54 +0,0 @@ -from __future__ import annotations - -import time - -from PIL import PyAccess - -from .helper import hopper - -# Not running this test by default. No DOS against CI. - - -def iterate_get(size, access) -> None: - (w, h) = size - for x in range(w): - for y in range(h): - access[(x, y)] - - -def iterate_set(size, access) -> None: - (w, h) = size - for x in range(w): - for y in range(h): - access[(x, y)] = (x % 256, y % 256, 0) - - -def timer(func, label, *args) -> None: - iterations = 5000 - starttime = time.time() - for x in range(iterations): - func(*args) - if time.time() - starttime > 10: - break - endtime = time.time() - print( - f"{label}: completed {x + 1} iterations in {endtime - starttime:.4f}s, " - f"{(endtime - starttime) / (x + 1.0):.6f}s per iteration" - ) - - -def test_direct() -> None: - 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 access is not None - assert caccess[(0, 0)] == access[(0, 0)] - - print(f"Size: {im.width}x{im.height}") - 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/check_wheel.py b/Tests/check_wheel.py deleted file mode 100644 index 4b91984f58a..00000000000 --- a/Tests/check_wheel.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import annotations - -import sys - -from PIL import features - - -def test_wheel_modules() -> None: - expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} - - # tkinter is not available in cibuildwheel installed CPython on Windows - try: - import tkinter - - assert tkinter - except ImportError: - expected_modules.remove("tkinter") - - assert set(features.get_supported_modules()) == expected_modules - - -def test_wheel_codecs() -> None: - expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"} - - assert set(features.get_supported_codecs()) == expected_codecs - - -def test_wheel_features() -> None: - expected_features = { - "webp_anim", - "webp_mux", - "transp_webp", - "raqm", - "fribidi", - "harfbuzz", - "libjpeg_turbo", - "xcb", - } - - if sys.platform == "win32": - expected_features.remove("xcb") - - assert set(features.get_supported_features()) == expected_features diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py old mode 100755 new mode 100644 index 41c76f87eac..0a3fdb809ed --- a/Tests/createfontdatachunk.py +++ b/Tests/createfontdatachunk.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 from __future__ import annotations import base64 diff --git a/Tests/fonts/AdobeVFPrototypeDuplicates.ttf b/Tests/fonts/AdobeVFPrototypeDuplicates.ttf new file mode 100644 index 00000000000..acf0bc156c8 Binary files /dev/null and b/Tests/fonts/AdobeVFPrototypeDuplicates.ttf differ diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt index 3c8a23197e9..94989af9035 100644 --- a/Tests/fonts/LICENSE.txt +++ b/Tests/fonts/LICENSE.txt @@ -2,7 +2,7 @@ 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/ -AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype +AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype. AdobeVFPrototypeDuplicates.ttf is a modified version of this 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/ diff --git a/Tests/fonts/fuzz_font-5203009437302784 b/Tests/fonts/fuzz_font-5203009437302784 index 0465e48c204..fb401fea225 100644 --- a/Tests/fonts/fuzz_font-5203009437302784 +++ b/Tests/fonts/fuzz_font-5203009437302784 @@ -1,10 +1,10 @@ STARTFONT FONT SIZE 10 -FONTBOUNDINGBOX -CHARS +FONTBOUNDINGBOX 1 1 0 0 +CHARS 1 STARTCHAR -ENCODING +ENCODING 65 BBX 2 5 ENDCHAR ENDFONT diff --git a/Tests/helper.py b/Tests/helper.py index 605a214d400..d77b4b807ec 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -9,17 +9,21 @@ import shutil import subprocess import sys -import sysconfig import tempfile from functools import lru_cache from io import BytesIO -from typing import Any, Callable, Sequence import pytest from packaging.version import parse as parse_version from PIL import Image, ImageFile, ImageMath, features +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + from pathlib import Path + from typing import Any + logger = logging.getLogger(__name__) uploader = None @@ -51,17 +55,15 @@ def convert_to_comparable( 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()) + new_a.putdata(a.get_flattened_data()) + new_b.putdata(b.get_flattened_data()) elif a.mode == "I;16": new_a = a.convert("I") new_b = b.convert("I") return new_a, new_b -def assert_deep_equal( - a: Sequence[Any], b: Sequence[Any], msg: str | None = None -) -> None: +def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None: try: assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" except Exception: @@ -97,12 +99,14 @@ def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) - def assert_image_equal_tofile( - a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None + a: Image.Image, + filename: str | Path, + msg: str | None = None, + mode: str | None = None, ) -> None: - with Image.open(filename) as img: - if mode: - img = img.convert(mode) - assert_image_equal(a, img, msg) + with Image.open(filename) as im: + converted_im = im.convert(mode) if mode else im + assert_image_equal(a, converted_im, msg) def assert_image_similar( @@ -138,21 +142,14 @@ def assert_image_similar( def assert_image_similar_tofile( a: Image.Image, - filename: str, + filename: str | Path, epsilon: float, msg: str | None = None, - mode: str | None = None, ) -> 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: Sequence[Any], msg: str | None = None) -> None: - assert items.count(items[0]) == len(items), msg - - def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None: assert items.count(items[0]) != len(items), msg @@ -166,11 +163,25 @@ def assert_tuple_approx_equal( pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets)) +def timeout_unless_slower_valgrind(timeout: float) -> pytest.MarkDecorator: + if "PILLOW_VALGRIND_TEST" in os.environ: + return pytest.mark.pil_noop_mark() + return pytest.mark.timeout(timeout) + + def skip_unless_feature(feature: str) -> pytest.MarkDecorator: reason = f"{feature} not available" return pytest.mark.skipif(not features.check(feature), reason=reason) +def has_feature_version(feature: str, required: str) -> bool: + version = features.version(feature) + assert version is not None + version_required = parse_version(required) + version_available = parse_version(version) + return version_available >= version_required + + def skip_unless_feature_version( feature: str, required: str, reason: str | None = None ) -> pytest.MarkDecorator: @@ -270,17 +281,13 @@ def _cached_hopper(mode: str) -> Image.Image: im = hopper("L") else: im = hopper() - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): - im = im.convert(mode) - else: - try: - im = im.convert(mode) - except ImportError: - if mode == "LAB": - im = Image.open("Tests/images/hopper.Lab.tif") - else: - raise + try: + im = im.convert(mode) + except ImportError: + if mode == "LAB": + im = Image.open("Tests/images/hopper.Lab.tif") + else: + raise return im @@ -294,16 +301,6 @@ def djpeg_available() -> bool: return False -def cjpeg_available() -> bool: - if shutil.which("cjpeg"): - try: - subprocess.check_call(["cjpeg", "-version"]) - return True - except subprocess.CalledProcessError: # pragma: no cover - return False - return False - - def netpbm_available() -> bool: return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) @@ -328,16 +325,7 @@ def magick_command() -> list[str] | None: return None -def on_appveyor() -> bool: - return "APPVEYOR" in os.environ - - -def on_github_actions() -> bool: - return "GITHUB_ACTIONS" in os.environ - - def on_ci() -> bool: - # GitHub Actions and AppVeyor have "CI" return "CI" in os.environ @@ -359,10 +347,6 @@ def is_pypy() -> bool: return hasattr(sys, "pypy_translation_info") -def is_mingw() -> bool: - return sysconfig.get_platform() == "mingw" - - class CachedProperty: def __init__(self, func: Callable[[Any], Any]) -> None: self.func = func diff --git a/Tests/images/avif/exif.avif b/Tests/images/avif/exif.avif new file mode 100644 index 00000000000..07964487f3c Binary files /dev/null and b/Tests/images/avif/exif.avif differ diff --git a/Tests/images/avif/hopper-missing-pixi.avif b/Tests/images/avif/hopper-missing-pixi.avif new file mode 100644 index 00000000000..c793ade1c21 Binary files /dev/null and b/Tests/images/avif/hopper-missing-pixi.avif differ diff --git a/Tests/images/avif/hopper.avif b/Tests/images/avif/hopper.avif new file mode 100644 index 00000000000..87e4394f059 Binary files /dev/null and b/Tests/images/avif/hopper.avif differ diff --git a/Tests/images/avif/hopper.heif b/Tests/images/avif/hopper.heif new file mode 100644 index 00000000000..f6dbd1b4775 Binary files /dev/null and b/Tests/images/avif/hopper.heif differ diff --git a/Tests/images/avif/hopper_avif_write.png b/Tests/images/avif/hopper_avif_write.png new file mode 100644 index 00000000000..a47a0562bbc Binary files /dev/null and b/Tests/images/avif/hopper_avif_write.png differ diff --git a/Tests/images/avif/icc_profile.avif b/Tests/images/avif/icc_profile.avif new file mode 100644 index 00000000000..658cfec176e Binary files /dev/null and b/Tests/images/avif/icc_profile.avif differ diff --git a/Tests/images/avif/icc_profile_none.avif b/Tests/images/avif/icc_profile_none.avif new file mode 100644 index 00000000000..c73e70a3a52 Binary files /dev/null and b/Tests/images/avif/icc_profile_none.avif differ diff --git a/Tests/images/avif/rot0mir0.avif b/Tests/images/avif/rot0mir0.avif new file mode 100644 index 00000000000..f5720309365 Binary files /dev/null and b/Tests/images/avif/rot0mir0.avif differ diff --git a/Tests/images/avif/rot0mir1.avif b/Tests/images/avif/rot0mir1.avif new file mode 100644 index 00000000000..8a9fb5e5431 Binary files /dev/null and b/Tests/images/avif/rot0mir1.avif differ diff --git a/Tests/images/avif/rot1mir0.avif b/Tests/images/avif/rot1mir0.avif new file mode 100644 index 00000000000..0c7c7620fd4 Binary files /dev/null and b/Tests/images/avif/rot1mir0.avif differ diff --git a/Tests/images/avif/rot1mir1.avif b/Tests/images/avif/rot1mir1.avif new file mode 100644 index 00000000000..0181b088cec Binary files /dev/null and b/Tests/images/avif/rot1mir1.avif differ diff --git a/Tests/images/avif/rot2mir0.avif b/Tests/images/avif/rot2mir0.avif new file mode 100644 index 00000000000..ddaa02f3f89 Binary files /dev/null and b/Tests/images/avif/rot2mir0.avif differ diff --git a/Tests/images/avif/rot2mir1.avif b/Tests/images/avif/rot2mir1.avif new file mode 100644 index 00000000000..63326b86caa Binary files /dev/null and b/Tests/images/avif/rot2mir1.avif differ diff --git a/Tests/images/avif/rot3mir0.avif b/Tests/images/avif/rot3mir0.avif new file mode 100644 index 00000000000..ed5dbe3d0bb Binary files /dev/null and b/Tests/images/avif/rot3mir0.avif differ diff --git a/Tests/images/avif/rot3mir1.avif b/Tests/images/avif/rot3mir1.avif new file mode 100644 index 00000000000..ccd8861d5ae Binary files /dev/null and b/Tests/images/avif/rot3mir1.avif differ diff --git a/Tests/images/avif/star.avifs b/Tests/images/avif/star.avifs new file mode 100644 index 00000000000..f2753395f8c Binary files /dev/null and b/Tests/images/avif/star.avifs differ diff --git a/Tests/images/avif/star.gif b/Tests/images/avif/star.gif new file mode 100644 index 00000000000..52076cafdd8 Binary files /dev/null and b/Tests/images/avif/star.gif differ diff --git a/Tests/images/avif/star.png b/Tests/images/avif/star.png new file mode 100644 index 00000000000..468dcde005d Binary files /dev/null and b/Tests/images/avif/star.png differ diff --git a/Tests/images/avif/transparency.avif b/Tests/images/avif/transparency.avif new file mode 100644 index 00000000000..f808357fc7c Binary files /dev/null and b/Tests/images/avif/transparency.avif differ diff --git a/Tests/images/avif/xmp_tags_orientation.avif b/Tests/images/avif/xmp_tags_orientation.avif new file mode 100644 index 00000000000..41faa60501b Binary files /dev/null and b/Tests/images/avif/xmp_tags_orientation.avif differ diff --git a/Tests/images/bmp/html/bkgd.png b/Tests/images/bmp/html/bkgd.png deleted file mode 100644 index d66ca9d6526..00000000000 Binary files a/Tests/images/bmp/html/bkgd.png and /dev/null differ diff --git a/Tests/images/bmp/html/bmpsuite.html b/Tests/images/bmp/html/bmpsuite.html deleted file mode 100644 index b8e327ed927..00000000000 --- a/Tests/images/bmp/html/bmpsuite.html +++ /dev/null @@ -1,578 +0,0 @@ - - - - - -BMP Suite Image List - - - - - - - -

BMP Suite Image List

- -

For BMP Suite -version 2.3

- -

This document describes the images in BMP Suite, and shows what -I allege to be the correct way to interpret them. PNG and JPEG images are -used for reference. -

- -

It also shows how your web browser displays the BMP images, -but that’s not its main purpose. -BMP is poor image format to use on web pages, so a web browser’s -level of support for it is arguably not important.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileVer.Correct displayIn your browserNotes
g/pal1.bmp31 bit/pixel paletted image, in which black is the first color in - the palette.
g/pal1wb.bmp31 bit/pixel paletted image, in which white is the first color in - the palette.
g/pal1bg.bmp31 bit/pixel paletted image, with colors other than black and white.
q/pal1p1.bmp31 bit/pixel paletted image, with only one color in the palette. - The documentation says that 1-bpp images have a palette size of 2 - (not “up to 2”), but it would be silly for a viewer not to - support a size of 1.
q/pal2.bmp3A paletted image with 2 bits/pixel. Usually only 1, 4, - and 8 are allowed, but 2 is legal on Windows CE.
g/pal4.bmp3Paletted image with 12 palette colors, and 4 bits/pixel.
g/pal4rle.bmp34-bit image that uses RLE compression.
q/pal4rletrns.bmp3
- or

- or
An RLE-compressed image that used “delta” - codes to skip over some pixels, leaving them undefined. Some viewers - make undefined pixels transparent, others make them black, and - others assign them palette color 0 (purple, in this case).
g/pal8.bmp3Our standard paletted image, with 252 palette colors, and 8 - bits/pixel.
g/pal8-0.bmp3Every field that can be set to 0 is set to 0: pixels/meter=0; - colors used=0 (meaning the default 256); size-of-image=0.
g/pal8rle.bmp38-bit image that uses RLE compression.
q/pal8rletrns.bmp3
- or

- or
8-bit version of q/pal4rletrns.bmp.
g/pal8w126.bmp3Images with different widths and heights. - In BMP format, rows are padded to a multiple of four bytes, so we - test all four possibilities.
g/pal8w125.bmp3
g/pal8w124.bmp3
g/pal8topdown.bmp3BMP images are normally stored from the bottom up, but - there is a way to store them from the top down.
q/pal8offs.bmp3A file with some unused bytes between the palette and the - image. This is probably valid, but I’m not 100% sure.
q/pal8oversizepal.bmp3An 8-bit image with 300 palette colors. This may be invalid, - because the documentation could - be interpreted to imply that 8-bit images aren’t allowed - to have more than 256 colors.
g/pal8nonsquare.bmp3 -
- or
- -
An image with non-square pixels: the X pixels/meter is twice - the Y pixels/meter. Image editors can be expected to - leave the image “squashed”; image viewers should - consider stretching it to its correct proportions.
g/pal8os2.bmpOS/2v1An OS/2-style bitmap.
q/pal8os2sp.bmpOS/2v1An OS/2v1 with a less-than-full-sized palette. - Probably not valid, but such files have been seen in the wild.
q/pal8os2v2.bmpOS/2v2My attempt to make an OS/2v2 bitmap.
q/pal8os2v2-16.bmpOS/2v2An OS/2v2 bitmap whose header has only 16 bytes, instead of the full 64.
g/pal8v4.bmp4A v4 bitmap. I’m not sure that the gamma and chromaticity values in - this file are sensible, because I can’t find any detailed documentation - of them.
g/pal8v5.bmp5A v5 bitmap. Version 5 has additional colorspace options over v4, so it - is easier to create, and ought to be more portable.
g/rgb16.bmp3A 16-bit image with the default color format: 5 bits each for red, - green, and blue, and 1 unused bit. - The whitest colors should (I assume) be displayed as pure white: - (255,255,255), not - (248,248,248).
g/rgb16-565.bmp3A 16-bit image with a BITFIELDS segment indicating 5 red, 6 green, - and 5 blue bits. This is a standard 16-bit format, even supported by - old versions of Windows that don’t support any other non-default 16-bit - formats. - The whitest colors should be displayed as pure white: - (255,255,255), not - (248,252,248).
g/rgb16-565pal.bmp3A 16-bit image with both a BITFIELDS segment and a palette.
q/rgb16-231.bmp3An unusual and silly 16-bit image, with 2 red bits, 3 green bits, and 1 - blue bit. Most viewers do support this image, but the colors may be darkened - with a yellow-green shadow. That’s because they’re doing simple - bit-shifting (possibly including one round of bit replication), instead of - proper scaling.
q/rgba16-4444.bmp5A 16-bit image with an alpha channel. There are 4 bits for each color - channel, and 4 bits for the alpha channel. - It’s not clear if this is valid, but I can’t find anything that - suggests it isn’t. -
g/rgb24.bmp3A perfectly ordinary 24-bit (truecolor) image.
g/rgb24pal.bmp3A 24-bit image, with a palette containing 256 colors. There is little if - any reason for a truecolor image to contain a palette, but it is legal.
q/rgb24largepal.bmp3A 24-bit image, with a palette containing 300 colors. - The fact that the palette has more than 256 colors may cause some viewers - to complain, but the documentation does not mention a size limit.
q/rgb24prof.bmp5My attempt to make a BMP file with an embedded color profile.
q/rgb24lprof.bmp5My attempt to make a BMP file with a linked color profile.
q/rgb24jpeg.bmp5My attempt to make BMP files with embedded JPEG and PNG images. - These are not likely to be supported by much of anything (they’re - intended for printers).
q/rgb24png.bmp5
g/rgb32.bmp3A 32-bit image using the default color format for 32-bit images (no - BITFIELDS segment). There are 8 bits per color channel, and 8 unused - bits. The unused bits are set to 0.
g/rgb32bf.bmp3A 32-bit image with a BITFIELDS segment. As usual, there are 8 bits per - color channel, and 8 unused bits. But the color channels are in an unusual - order, so the viewer must read the BITFIELDS, and not just guess.
q/rgb32fakealpha.bmp3
- or
- -
Same as g/rgb32.bmp, except that the unused bits are set to something - other than 0. - If the image becomes transparent toward the bottom, it probably means - the viewer uses heuristics to guess whether the undefined - data represents transparency.
q/rgb32-111110.bmp3A 32 bits/pixel image, with all 32 bits used: 11 each for red and - green, and 10 for blue. As far as I know, this is perfectly valid, but it - is unusual.
q/rgba32.bmp5A BMP with an alpha channel. Transparency is barely documented, - so it’s possible that this file is not correctly formed. - The color channels are in an unusual order, to prevent viewers from - passing this test by making a lucky guess.
q/rgba32abf.bmp3An image of type BI_ALHPABITFIELDS. Supposedly, this was used on - Windows CE. I don’t know whether it is constructed correctly.
b/badbitcount.bmp3N/AHeader indicates an absurdly large number of bits/pixel.
b/badbitssize.bmp3N/AHeader incorrectly indicates that the bitmap is several GB in size.
b/baddens1.bmp3N/ADensity (pixels per meter) suggests the image is much - larger in one dimension than the other.
b/baddens2.bmp3N/A
b/badfilesize.bmp3N/AHeader incorrectly indicates that the file is several GB in size.
b/badheadersize.bmp?N/AHeader size is 66 bytes, which is not a valid size for any known BMP - version.
b/badpalettesize.bmp3N/AHeader incorrectly indicates that the palette contains an absurdly large - number of colors.
b/badplanes.bmp3N/AThe “planes” setting, which is required to be 1, is not 1.
b/badrle.bmp3N/AAn invalid RLE-compressed image that tries to cause buffer overruns.
b/badwidth.bmp3N/AThe image claims to be a negative number of pixels in width.
b/pal8badindex.bmp3N/AMany of the palette indices used in the image are not present in the - palette.
b/reallybig.bmp3N/AAn image with a very large reported width and height.
b/rletopdown.bmp3N/AAn RLE-compressed image that tries to use top-down orientation, - which isn’t allowed.
b/shortfile.bmp3N/AA file that has been truncated in the middle of the bitmap.
- - - - diff --git a/Tests/images/bmp/html/fakealpha.png b/Tests/images/bmp/html/fakealpha.png deleted file mode 100644 index 89292bcbb48..00000000000 Binary files a/Tests/images/bmp/html/fakealpha.png and /dev/null differ diff --git a/Tests/images/bmp/html/pal1p1.png b/Tests/images/bmp/html/pal1p1.png deleted file mode 100644 index 92fc0f945b0..00000000000 Binary files a/Tests/images/bmp/html/pal1p1.png and /dev/null differ diff --git a/Tests/images/bmp/html/pal2.png b/Tests/images/bmp/html/pal2.png deleted file mode 100644 index 1bbfe175fc3..00000000000 Binary files a/Tests/images/bmp/html/pal2.png and /dev/null differ diff --git a/Tests/images/bmp/html/pal4rletrns-0.png b/Tests/images/bmp/html/pal4rletrns-0.png deleted file mode 100644 index b689c842aa0..00000000000 Binary files a/Tests/images/bmp/html/pal4rletrns-0.png and /dev/null differ diff --git a/Tests/images/bmp/html/pal4rletrns-b.png b/Tests/images/bmp/html/pal4rletrns-b.png deleted file mode 100644 index 9befa575fa4..00000000000 Binary files a/Tests/images/bmp/html/pal4rletrns-b.png and /dev/null differ diff --git a/Tests/images/bmp/html/pal4rletrns.png b/Tests/images/bmp/html/pal4rletrns.png deleted file mode 100644 index 9b0c0443641..00000000000 Binary files a/Tests/images/bmp/html/pal4rletrns.png and /dev/null differ diff --git a/Tests/images/bmp/html/pal8nonsquare-v.png b/Tests/images/bmp/html/pal8nonsquare-v.png deleted file mode 100644 index a1cd1ab180d..00000000000 Binary files a/Tests/images/bmp/html/pal8nonsquare-v.png and /dev/null differ diff --git a/Tests/images/bmp/html/pal8rletrns-0.png b/Tests/images/bmp/html/pal8rletrns-0.png deleted file mode 100644 index a1c1fda507f..00000000000 Binary files a/Tests/images/bmp/html/pal8rletrns-0.png and /dev/null differ diff --git a/Tests/images/bmp/html/pal8rletrns-b.png b/Tests/images/bmp/html/pal8rletrns-b.png deleted file mode 100644 index 1ede504d4e5..00000000000 Binary files a/Tests/images/bmp/html/pal8rletrns-b.png and /dev/null differ diff --git a/Tests/images/bmp/html/pal8rletrns.png b/Tests/images/bmp/html/pal8rletrns.png deleted file mode 100644 index 2d8e957f1f5..00000000000 Binary files a/Tests/images/bmp/html/pal8rletrns.png and /dev/null differ diff --git a/Tests/images/bmp/html/rgb16-231.png b/Tests/images/bmp/html/rgb16-231.png deleted file mode 100644 index 76efe526e5a..00000000000 Binary files a/Tests/images/bmp/html/rgb16-231.png and /dev/null differ diff --git a/Tests/images/bmp/html/rgb24.jpg b/Tests/images/bmp/html/rgb24.jpg deleted file mode 100644 index c43698c9b15..00000000000 Binary files a/Tests/images/bmp/html/rgb24.jpg and /dev/null differ diff --git a/Tests/images/bmp/html/rgba16-4444.png b/Tests/images/bmp/html/rgba16-4444.png deleted file mode 100644 index bfeda6faedb..00000000000 Binary files a/Tests/images/bmp/html/rgba16-4444.png and /dev/null differ diff --git a/Tests/images/bmp/html/rgba32.png b/Tests/images/bmp/html/rgba32.png deleted file mode 100644 index 25e542a6551..00000000000 Binary files a/Tests/images/bmp/html/rgba32.png and /dev/null differ diff --git a/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff b/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff index b72509cc4b3..2ebb287e26d 100644 Binary files a/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff and b/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff differ diff --git a/Tests/images/colr_bungee.png b/Tests/images/colr_bungee.png index b10a60be057..9ec6b11823b 100644 Binary files a/Tests/images/colr_bungee.png and b/Tests/images/colr_bungee.png differ diff --git a/Tests/images/colr_bungee_mask.png b/Tests/images/colr_bungee_mask.png index f13e1767749..79106b63949 100644 Binary files a/Tests/images/colr_bungee_mask.png and b/Tests/images/colr_bungee_mask.png differ diff --git a/Tests/images/colr_bungee_older.png b/Tests/images/colr_bungee_older.png new file mode 100644 index 00000000000..b10a60be057 Binary files /dev/null and b/Tests/images/colr_bungee_older.png differ diff --git a/Tests/images/crash-5762152299364352.fli b/Tests/images/crash-5762152299364352.fli index 944fe0b56c7..d7588eea88f 100644 Binary files a/Tests/images/crash-5762152299364352.fli and b/Tests/images/crash-5762152299364352.fli differ diff --git a/Tests/images/drawing_emf_ref_72_144.png b/Tests/images/drawing_emf_ref_72_144.png new file mode 100644 index 00000000000..000377b6c7b Binary files /dev/null and b/Tests/images/drawing_emf_ref_72_144.png differ diff --git a/Tests/images/eps/1.bmp b/Tests/images/eps/1.bmp new file mode 100644 index 00000000000..194c85784c5 Binary files /dev/null and b/Tests/images/eps/1.bmp differ diff --git a/Tests/images/1.eps b/Tests/images/eps/1.eps similarity index 100% rename from Tests/images/1.eps rename to Tests/images/eps/1.eps diff --git a/Tests/images/eps/1_boundingbox_after_imagedata.eps b/Tests/images/eps/1_boundingbox_after_imagedata.eps new file mode 100644 index 00000000000..08f8c4681a9 Binary files /dev/null and b/Tests/images/eps/1_boundingbox_after_imagedata.eps differ diff --git a/Tests/images/eps/1_second_imagedata.eps b/Tests/images/eps/1_second_imagedata.eps new file mode 100644 index 00000000000..e6309a3b4ed Binary files /dev/null and b/Tests/images/eps/1_second_imagedata.eps differ diff --git a/Tests/images/binary_preview_map.eps b/Tests/images/eps/binary_preview_map.eps similarity index 100% rename from Tests/images/binary_preview_map.eps rename to Tests/images/eps/binary_preview_map.eps diff --git a/Tests/images/create_eps.gnuplot b/Tests/images/eps/create_eps.gnuplot similarity index 100% rename from Tests/images/create_eps.gnuplot rename to Tests/images/eps/create_eps.gnuplot diff --git a/Tests/images/illu10_no_preview.eps b/Tests/images/eps/illu10_no_preview.eps similarity index 100% rename from Tests/images/illu10_no_preview.eps rename to Tests/images/eps/illu10_no_preview.eps diff --git a/Tests/images/illu10_preview.eps b/Tests/images/eps/illu10_preview.eps similarity index 100% rename from Tests/images/illu10_preview.eps rename to Tests/images/eps/illu10_preview.eps diff --git a/Tests/images/illuCS6_no_preview.eps b/Tests/images/eps/illuCS6_no_preview.eps similarity index 100% rename from Tests/images/illuCS6_no_preview.eps rename to Tests/images/eps/illuCS6_no_preview.eps diff --git a/Tests/images/illuCS6_preview.eps b/Tests/images/eps/illuCS6_preview.eps similarity index 100% rename from Tests/images/illuCS6_preview.eps rename to Tests/images/eps/illuCS6_preview.eps diff --git a/Tests/images/non_zero_bb.eps b/Tests/images/eps/non_zero_bb.eps similarity index 100% rename from Tests/images/non_zero_bb.eps rename to Tests/images/eps/non_zero_bb.eps diff --git a/Tests/images/non_zero_bb.png b/Tests/images/eps/non_zero_bb.png similarity index 100% rename from Tests/images/non_zero_bb.png rename to Tests/images/eps/non_zero_bb.png diff --git a/Tests/images/non_zero_bb_scale2.png b/Tests/images/eps/non_zero_bb_scale2.png similarity index 100% rename from Tests/images/non_zero_bb_scale2.png rename to Tests/images/eps/non_zero_bb_scale2.png diff --git a/Tests/images/pil_sample_cmyk.eps b/Tests/images/eps/pil_sample_cmyk.eps similarity index 100% rename from Tests/images/pil_sample_cmyk.eps rename to Tests/images/eps/pil_sample_cmyk.eps diff --git a/Tests/images/reqd_showpage.eps b/Tests/images/eps/reqd_showpage.eps similarity index 100% rename from Tests/images/reqd_showpage.eps rename to Tests/images/eps/reqd_showpage.eps diff --git a/Tests/images/reqd_showpage.png b/Tests/images/eps/reqd_showpage.png similarity index 100% rename from Tests/images/reqd_showpage.png rename to Tests/images/eps/reqd_showpage.png diff --git a/Tests/images/reqd_showpage_transparency.png b/Tests/images/eps/reqd_showpage_transparency.png similarity index 100% rename from Tests/images/reqd_showpage_transparency.png rename to Tests/images/eps/reqd_showpage_transparency.png diff --git a/Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps b/Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps similarity index 100% rename from Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps rename to Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps diff --git a/Tests/images/zero_bb.eps b/Tests/images/eps/zero_bb.eps similarity index 100% rename from Tests/images/zero_bb.eps rename to Tests/images/eps/zero_bb.eps diff --git a/Tests/images/zero_bb.png b/Tests/images/eps/zero_bb.png similarity index 100% rename from Tests/images/zero_bb.png rename to Tests/images/eps/zero_bb.png diff --git a/Tests/images/zero_bb_emptyline.eps b/Tests/images/eps/zero_bb_emptyline.eps similarity index 100% rename from Tests/images/zero_bb_emptyline.eps rename to Tests/images/eps/zero_bb_emptyline.eps diff --git a/Tests/images/zero_bb_eof_before_boundingbox.eps b/Tests/images/eps/zero_bb_eof_before_boundingbox.eps similarity index 100% rename from Tests/images/zero_bb_eof_before_boundingbox.eps rename to Tests/images/eps/zero_bb_eof_before_boundingbox.eps diff --git a/Tests/images/zero_bb_scale2.png b/Tests/images/eps/zero_bb_scale2.png similarity index 100% rename from Tests/images/zero_bb_scale2.png rename to Tests/images/eps/zero_bb_scale2.png diff --git a/Tests/images/zero_bb_trailer.eps b/Tests/images/eps/zero_bb_trailer.eps similarity index 100% rename from Tests/images/zero_bb_trailer.eps rename to Tests/images/eps/zero_bb_trailer.eps diff --git a/Tests/images/frame_size.mpo b/Tests/images/frame_size.mpo new file mode 100644 index 00000000000..ee5c6cdf7a9 Binary files /dev/null and b/Tests/images/frame_size.mpo differ diff --git a/Tests/images/full_gimp_palette.gpl b/Tests/images/full_gimp_palette.gpl new file mode 100644 index 00000000000..004217210df --- /dev/null +++ b/Tests/images/full_gimp_palette.gpl @@ -0,0 +1,260 @@ +GIMP Palette +Name: fullpalette +Columns: 4 +# + 0 0 0 Index 0 + 1 1 1 Index 1 + 2 2 2 Index 2 + 3 3 3 Index 3 + 4 4 4 Index 4 + 5 5 5 Index 5 + 6 6 6 Index 6 + 7 7 7 Index 7 + 8 8 8 Index 8 + 9 9 9 Index 9 + 10 10 10 Index 10 + 11 11 11 Index 11 + 12 12 12 Index 12 + 13 13 13 Index 13 + 14 14 14 Index 14 + 15 15 15 Index 15 + 16 16 16 Index 16 + 17 17 17 Index 17 + 18 18 18 Index 18 + 19 19 19 Index 19 + 20 20 20 Index 20 + 21 21 21 Index 21 + 22 22 22 Index 22 + 23 23 23 Index 23 + 24 24 24 Index 24 + 25 25 25 Index 25 + 26 26 26 Index 26 + 27 27 27 Index 27 + 28 28 28 Index 28 + 29 29 29 Index 29 + 30 30 30 Index 30 + 31 31 31 Index 31 + 32 32 32 Index 32 + 33 33 33 Index 33 + 34 34 34 Index 34 + 35 35 35 Index 35 + 36 36 36 Index 36 + 37 37 37 Index 37 + 38 38 38 Index 38 + 39 39 39 Index 39 + 40 40 40 Index 40 + 41 41 41 Index 41 + 42 42 42 Index 42 + 43 43 43 Index 43 + 44 44 44 Index 44 + 45 45 45 Index 45 + 46 46 46 Index 46 + 47 47 47 Index 47 + 48 48 48 Index 48 + 49 49 49 Index 49 + 50 50 50 Index 50 + 51 51 51 Index 51 + 52 52 52 Index 52 + 53 53 53 Index 53 + 54 54 54 Index 54 + 55 55 55 Index 55 + 56 56 56 Index 56 + 57 57 57 Index 57 + 58 58 58 Index 58 + 59 59 59 Index 59 + 60 60 60 Index 60 + 61 61 61 Index 61 + 62 62 62 Index 62 + 63 63 63 Index 63 + 64 64 64 Index 64 + 65 65 65 Index 65 + 66 66 66 Index 66 + 67 67 67 Index 67 + 68 68 68 Index 68 + 69 69 69 Index 69 + 70 70 70 Index 70 + 71 71 71 Index 71 + 72 72 72 Index 72 + 73 73 73 Index 73 + 74 74 74 Index 74 + 75 75 75 Index 75 + 76 76 76 Index 76 + 77 77 77 Index 77 + 78 78 78 Index 78 + 79 79 79 Index 79 + 80 80 80 Index 80 + 81 81 81 Index 81 + 82 82 82 Index 82 + 83 83 83 Index 83 + 84 84 84 Index 84 + 85 85 85 Index 85 + 86 86 86 Index 86 + 87 87 87 Index 87 + 88 88 88 Index 88 + 89 89 89 Index 89 + 90 90 90 Index 90 + 91 91 91 Index 91 + 92 92 92 Index 92 + 93 93 93 Index 93 + 94 94 94 Index 94 + 95 95 95 Index 95 + 96 96 96 Index 96 + 97 97 97 Index 97 + 98 98 98 Index 98 + 99 99 99 Index 99 +100 100 100 Index 100 +101 101 101 Index 101 +102 102 102 Index 102 +103 103 103 Index 103 +104 104 104 Index 104 +105 105 105 Index 105 +106 106 106 Index 106 +107 107 107 Index 107 +108 108 108 Index 108 +109 109 109 Index 109 +110 110 110 Index 110 +111 111 111 Index 111 +112 112 112 Index 112 +113 113 113 Index 113 +114 114 114 Index 114 +115 115 115 Index 115 +116 116 116 Index 116 +117 117 117 Index 117 +118 118 118 Index 118 +119 119 119 Index 119 +120 120 120 Index 120 +121 121 121 Index 121 +122 122 122 Index 122 +123 123 123 Index 123 +124 124 124 Index 124 +125 125 125 Index 125 +126 126 126 Index 126 +127 127 127 Index 127 +128 128 128 Index 128 +129 129 129 Index 129 +130 130 130 Index 130 +131 131 131 Index 131 +132 132 132 Index 132 +133 133 133 Index 133 +134 134 134 Index 134 +135 135 135 Index 135 +136 136 136 Index 136 +137 137 137 Index 137 +138 138 138 Index 138 +139 139 139 Index 139 +140 140 140 Index 140 +141 141 141 Index 141 +142 142 142 Index 142 +143 143 143 Index 143 +144 144 144 Index 144 +145 145 145 Index 145 +146 146 146 Index 146 +147 147 147 Index 147 +148 148 148 Index 148 +149 149 149 Index 149 +150 150 150 Index 150 +151 151 151 Index 151 +152 152 152 Index 152 +153 153 153 Index 153 +154 154 154 Index 154 +155 155 155 Index 155 +156 156 156 Index 156 +157 157 157 Index 157 +158 158 158 Index 158 +159 159 159 Index 159 +160 160 160 Index 160 +161 161 161 Index 161 +162 162 162 Index 162 +163 163 163 Index 163 +164 164 164 Index 164 +165 165 165 Index 165 +166 166 166 Index 166 +167 167 167 Index 167 +168 168 168 Index 168 +169 169 169 Index 169 +170 170 170 Index 170 +171 171 171 Index 171 +172 172 172 Index 172 +173 173 173 Index 173 +174 174 174 Index 174 +175 175 175 Index 175 +176 176 176 Index 176 +177 177 177 Index 177 +178 178 178 Index 178 +179 179 179 Index 179 +180 180 180 Index 180 +181 181 181 Index 181 +182 182 182 Index 182 +183 183 183 Index 183 +184 184 184 Index 184 +185 185 185 Index 185 +186 186 186 Index 186 +187 187 187 Index 187 +188 188 188 Index 188 +189 189 189 Index 189 +190 190 190 Index 190 +191 191 191 Index 191 +192 192 192 Index 192 +193 193 193 Index 193 +194 194 194 Index 194 +195 195 195 Index 195 +196 196 196 Index 196 +197 197 197 Index 197 +198 198 198 Index 198 +199 199 199 Index 199 +200 200 200 Index 200 +201 201 201 Index 201 +202 202 202 Index 202 +203 203 203 Index 203 +204 204 204 Index 204 +205 205 205 Index 205 +206 206 206 Index 206 +207 207 207 Index 207 +208 208 208 Index 208 +209 209 209 Index 209 +210 210 210 Index 210 +211 211 211 Index 211 +212 212 212 Index 212 +213 213 213 Index 213 +214 214 214 Index 214 +215 215 215 Index 215 +216 216 216 Index 216 +217 217 217 Index 217 +218 218 218 Index 218 +219 219 219 Index 219 +220 220 220 Index 220 +221 221 221 Index 221 +222 222 222 Index 222 +223 223 223 Index 223 +224 224 224 Index 224 +225 225 225 Index 225 +226 226 226 Index 226 +227 227 227 Index 227 +228 228 228 Index 228 +229 229 229 Index 229 +230 230 230 Index 230 +231 231 231 Index 231 +232 232 232 Index 232 +233 233 233 Index 233 +234 234 234 Index 234 +235 235 235 Index 235 +236 236 236 Index 236 +237 237 237 Index 237 +238 238 238 Index 238 +239 239 239 Index 239 +240 240 240 Index 240 +241 241 241 Index 241 +242 242 242 Index 242 +243 243 243 Index 243 +244 244 244 Index 244 +245 245 245 Index 245 +246 246 246 Index 246 +247 247 247 Index 247 +248 248 248 Index 248 +249 249 249 Index 249 +250 250 250 Index 250 +251 251 251 Index 251 +252 252 252 Index 252 +253 253 253 Index 253 +254 254 254 Index 254 +255 255 255 Index 255 diff --git a/Tests/images/hopper_bpp2.xpm b/Tests/images/hopper_bpp2.xpm new file mode 100644 index 00000000000..fe97b83baf1 --- /dev/null +++ b/Tests/images/hopper_bpp2.xpm @@ -0,0 +1,390 @@ +/* XPM */ +static const char *hopper[] = { +/* columns rows colors chars-per-pixel */ +"128 128 256 2 ", +" c #0C0C0D", +". c #0A0708", +"X c #1C0A04", +"o c #120B0C", +"O c #170808", +"+ c #0B110D", +"@ c #16120C", +"# c #0D0D12", +"$ c #0D0D1A", +"% c #070A16", +"& c #120D13", +"* c #120E1A", +"= c #1A0C16", +"- c #0D1114", +"; c #0D121B", +": c #091518", +"> c #131215", +", c #14131B", +"< c #1A141C", +"1 c #1B191D", +"2 c #191517", +"3 c #250906", +"4 c #390904", +"5 c #27150A", +"6 c #250A18", +"7 c #251719", +"8 c #361410", +"9 c #342215", +"0 c #0C0C24", +"q c #0C0D2B", +"w c #060927", +"e c #130D24", +"r c #150D2A", +"t c #0C1225", +"y c #0C122C", +"u c #061227", +"i c #151422", +"p c #1A1522", +"a c #1C1B23", +"s c #13132C", +"d c #19172A", +"f c #0C0D35", +"g c #130E37", +"h c #0D1436", +"j c #131333", +"k c #13143C", +"l c #191838", +"z c #241926", +"x c #231B38", +"c c #2E1226", +"v c #372628", +"b c #292538", +"n c #362B37", +"m c #2F2A2F", +"M c #1A2233", +"N c #4C150D", +"B c #740F10", +"V c #512916", +"C c #793419", +"Z c #6D2C13", +"A c #4E1524", +"S c #741624", +"D c #4E332E", +"F c #6F3629", +"G c #574438", +"H c #744831", +"J c #775A2E", +"K c #0E1444", +"L c #141443", +"P c #1B1A44", +"I c #14144B", +"U c #1A1B4C", +"Y c #181747", +"T c #1B1B53", +"R c #181955", +"E c #0F0E44", +"W c #231C46", +"Q c #231C56", +"! c #1C234E", +"~ c #272547", +"^ c #2E2F52", +"/ c #2E3765", +"( c #483947", +") c #742D4A", +"_ c #364970", +"` c #534A51", +"' c #6E534D", +"] c #756654", +"[ c #53556D", +"{ c #6B5B69", +"} c #746B71", +"| c #5E616A", +" . c #880C15", +".. c #881217", +"X. c #8D0D0F", +"o. c #8B3218", +"O. c #8C3828", +"+. c #AC2F30", +"@. c #9A1825", +"#. c #CE202B", +"$. c #8A452A", +"%. c #974A2B", +"&. c #884934", +"*. c #954B35", +"=. c #995539", +"-. c #895736", +";. c #A75738", +":. c #A84E30", +">. c #996839", +",. c #B6683B", +"<. c #AE6835", +"1. c #A35419", +"2. c #D26D19", +"3. c #CC712E", +"4. c #CD6922", +"5. c #A83152", +"6. c #985845", +"7. c #8A5748", +"8. c #AE5A46", +"9. c #916A4F", +"0. c #A96647", +"q. c #B76947", +"w. c #BA744A", +"e. c #B97757", +"r. c #AB6F53", +"t. c #8D736D", +"y. c #B27669", +"u. c #91566F", +"i. c #C56B4A", +"p. c #C8764B", +"a. c #C87856", +"s. c #D47A59", +"d. c #C96E53", +"f. c #C77C64", +"g. c #D17969", +"h. c #D45D68", +"j. c #C52A46", +"k. c #D58932", +"l. c #B38355", +"z. c #968775", +"x. c #BA8667", +"c. c #B38C74", +"v. c #AB9C73", +"b. c #C9845A", +"n. c #D7855B", +"m. c #D39454", +"M. c #E28C5B", +"N. c #F7B251", +"B. c #C78867", +"V. c #D98866", +"C. c #D8956A", +"Z. c #C79878", +"A. c #D89876", +"S. c #CD8C70", +"D. c #E38A68", +"F. c #E5956A", +"G. c #E79776", +"H. c #ED9176", +"J. c #D6A371", +"K. c #E8A379", +"L. c #F3A677", +"P. c #D8A05D", +"I. c #3D65AB", +"U. c #3F67B2", +"Y. c #3B5C9C", +"T. c #506796", +"R. c #72748D", +"E. c #446AAE", +"W. c #4869A9", +"Q. c #4166B2", +"!. c #436BB3", +"~. c #496EB4", +"^. c #476DB9", +"/. c #4A71B6", +"(. c #4C73BA", +"). c #4772B6", +"_. c #5176BC", +"`. c #547BBD", +"'. c #577BB7", +"]. c #5572A9", +"[. c #6B7CAA", +"{. c #505B8C", +"}. c #557CC1", +"|. c #4C73C2", +" X c #897987", +".X c #9F7593", +"XX c #C46B87", +"oX c #5981BF", +"OX c #5884BD", +"+X c #768AB9", +"@X c #7288B5", +"#X c #5C83C3", +"$X c #5D8AC5", +"%X c #6186C5", +"&X c #648AC6", +"*X c #6B8DC6", +"=X c #668BC9", +"-X c #6B8ECA", +";X c #6586C6", +":X c #738DC7", +">X c #6D91CB", +",X c #6C94C6", +" b R.DXPXLXHXHXHXHXCX~ / T.Y.T.T.W.T.W.E.Q.I.E.I.I.E.E.I.I.I.I.I.Y.I.Q.^.Q.E.E.E.E.Q.Q.~.U.U.U.U.U.U.Q.Q.U.U.U.U.U.U.Q.Q.Q.Q.U.U.U.Q.~.~.Q.U.Q.~.^._._._._._._._._.(.(.(.", +"L k f L L k y h T R I L U U L U R R T L E E E R I R I U l XuX' fXV v [ / P h z V Z.G a y l [ 7XCXHXJXHXHXCXb ! {.{.T.{._ _ {.T.W.W.T.T.W.I.I.U.U.I.I.E.W.I.I.Q.Q.E.E.E.Q.Q.~.~.~.U.U.U.U.Q.Q.Q.Q.U.U.U.U.U.Q.~.~.~.U.U.U.U.U.Q.~.Q.Q.Q.~.~._._._.'.`._._._._._.", +"L k f L L k 0 h T T I E U U L T T T U L h h E U R R E U W R.{ D pXF z l L U ^ p F fXD i P W Y ~ n CXHXHXHXFX8Xl W ~ ~ l ^ b b ^ ^ / [ T.W._.U.^.U.U.Q.E.W.W.~.^.E.E.E.~.~.Q.~.~.~.~.~.~.~.~.Q.Q.Q.Q.U.U.U.U.U.U.~.Q.U.U.U.U.U.U.^.~.~.Q.~.~._._.`.`.`.`._._.|.|.", +"k k f L L k 0 h U T L h I U I T T T U k h k E U Q I E U k ` m ' hXV z k U I Q d V fX( j L U W W z VXLX8X XuX( z b x d ` X X` n n b b ! {.W.~.I.Q.Q.Q.E.W.].~.I.~.~.~.~.~.~.~.Q.~.~.~.~.~.~.~.~.~.Q.Q.Q.U.U.U.U.U.Q.U.U.~.~.Q.Q.Q.Q.Q.Q.Q.~._._._._._._._._.|.|.", +"k k f L L k q j U T L h U U I T R T U k h h E I E R I I k b p ' Z.V z k ! T U p H Z.c k U L U W n CXCXn z = c c v 7X8X` 8XPX} c R.tX` n b / {.].W.~.~.E.W.E.~.^.~.~.~.~.^.~.~.~.~.~.E.E.~.~.~.~.~.~.~.~.~.~.U.U.~.U.~.~.~.~.~.Q.U.Q.~.~.E.~.~./././.(.(.(.(.(.(.", +"h k h P L k q j U T L h U U I T R T U h h h E E I R I E k d p ' dXV x P U L L z J fXv l L L P j n IX` m = = 7 ' HXLXKXCX7XKXtXrXLXKXJXqXv n ^ {.T.T.T.W.].E.Q.^.~.~.~.~.^.^.~.~.E.E.E.E.E.~.~.~.~.~.~.~.~.~.U.U.~.~.~.~.U.U.U.U.Q.Q.~.~.E.~.~.E.~.~.~./././.(.(.", +"j k k P Y k q h U U k h U R I R R T U h h h E E R R E I Y d d ' Z.V p L ! ! Y = H Z.v j h ! l b n iXtX7Xa p t.LXZX0XHXPXKXKXPXLXLXCXbXAXVXn m 8XVXeX[.T.W.W.^.^.~.~.~.^.^.^.^.~.E.E.E.E.E.~.U.U.~.~.~.~.~.~.~.~.~.~.~.U.I.I.I.~.~.Q.Q.~.~.~.~.E.~.~.~./.(.(.(.(.", +"j k k U P k 0 y L U k h U R I R T Q T E f E E E E I I I f k z ` Z.V d U T L P z >.B.c j l l l } IXKXKX8Xp ` t.` t.' G ] tXIXIXwX] ' z.t.` { c n PXKXLXUX+X].E.~.~.~.~.~.^.^.^.~.Q.E.E.E.E.U.U.U.~.~.~.~.~.~.~.~.~.~.I.I.I.I.~.~.~.Q.Q.Q.~.~.~.~.~.~.~./.(.(.(.(.", +"j k R.~ k k q q Y T k f Q T I I I Q I L E L E E R I I E f d x ` dXV d T T T U z -.Z.c b s b CXLXKXLXKX} z 7 7 z.hXSXSXz.AXbXmXbX0XJXmXmX` 1 n b iXKXLXLXLXDX@XT.].W.E.(.U.|.^.^.~.~.W.E.~.~.~.~.~.~.U.~.~.~.~.~.~.U.I.I.I.U.~.~.Q.~.~.~.~.E.~.~.~.~.~.~.~.~.^.^.", +"j ~ DX[ W q s q Q I f h Q L R R R R I I L f E I I I I L P d x ` dXV d R R T Y z -.Z.7 0 ` GXLXKXJXLXJX` 7 7 7 t.hXMXmXJXLXJXJXJXJXSXSXmX' n z b 7XKXKXPXKXLXPXBX].T.W./.^.^.U.|.~.~.~.~.~.~.~.^.~.U.U.~.~.~.~.~.~.~.U.U.I.U.~.~.~.~.~.~.E.E.~.~.~.~.~.~.~.~.^.^.", +"r x DXIX~ s $ b L U L L Y Q L I I I T L f L U E T T L k k d x ` dXV x T R U L p 9.Z.7 | KXKXLXLXJXSXZXD v 7 7 D mXMXmXSXZXZXCXZXAXZXdXmXG v n n 7XKXLXKXKXKXLXKXDX[.T.]./.I.}.U.~.~.~.~.~.~.~.U.~.U.U.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.U.~.~.~.~.^.^.^.^.", +"r b CXPX X[ iX[ Y U L k P [.~ k U T U L f f f L I U U k f d x ` dXV z T T U L z 9.x.D LXHXJXJXZXqXqXmXD @ 7 7 9 ] mXbXJXKXKXKXLXJXJXMXv.9 7 7 7 } HXKXKXHXLXJXLXKXDX[.T.W.(.~.^.Q.Q.E.E.E.~.U.U.~.U.U.U.~.U.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.U.~.~.~.~.~.^.^.^.", +"d x VXKXPXGXCX` P U L Y ~ BX| l k P k k k P w k h L L P j d d ( dXZ z P ! L k z 9.B.mXJXJXSX9Xz.t.D 5 5 5 7 7 9 hXmXv.mXLXKXKXKXJXSXv.mX] v c v D t.xXZXJXJXJXLXPXKXUXT.W.E.~.~.Q.Q.E.E.E.E.Q.Q.~.I.I.~.~.E.E.~.~.~.~.~.~.~.~.~.~.~.~.I.~.~.U.U.~.~.~.~.^.^.^.(.", +"x 8XGXPXHXHXtXb k U U k l CXtXd b ~ | {.j q k f P / h k k d d ( dXF < k ! L k z 7.zXSXJXSXt.] V 3 3 X 5 @ 2 c 7 z.v.bXSXAXKXLXLXZXmXhXMX' 7 n 7 9 3 8 ] qXZXJXLXLXKXPX@X].W.I.^.~.~.~.E.E.~.~.~.E.I.E.~.~.E.E.~.~.~.~.~.~.~.~.~.~.~.~.~.~.U.U.U.~.~.~.~.^.^.(.(.", +"uXGXHXLXJXAX} & W Q g g ~ DXCX` [ VXDX[ s j s y ^ eX~ j j d l ( pXF 7 k ! L L z 7.nXJXAX] D 3 3 3 X ' ] 7 1 = 9 t.SXSXMX9XZXJXJXxXmXSXSXJ v v 7 9 ] 9 9 5 ' 9XxXJXHXGXDX{.'.).~.~.~.E.E.E.E.~.~.~.~.~.~.~.E.I.~.~.~.~.~.~.I.I.~.~.~.~.~.~.~.U.~.U.U.~.~.^.(.(.(.", +"iXFXPXLXLXLXyX( k W k ~ b CXGXFXPXGXtXl l s 0 j ^ DX` d d d x D pXF z P T L P z ' AXAXz.5 X 3 9.] 9 5 v.5 G ` 9 J hXhXhXmX9X' ] qXhXhXMX] 9 D 9 G z.5 ] t.8 8 G wXHXPXIX[.T.W.].~.~.~.~.E.E.~.~.~.~.~.~.~.E.E.~.I.~.~.~.~.I.I.I.~.~.~.~.U.U.U.~.U.U.U.~.^.(.(.(.", +"d n } LXLXCXVX[ W W d ` tXHXAXAXHXIX^ x j l w s ` GX7X7 n } ~ D dXH p k ! P k l ` xX8X2 @ 5 7 gXbXhXhXv.hXmXSXmXMX9.J 5 5 V 9 9 9 V G dXhXSXSXdXhXl.MXMXdXV J V 9 wXLXFXtXR.T.T.W.~.^.~./.E.E.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.).~.~.~.^.^.^.^.~.^.(.(.(.", +"d d [ LXVX( ^ ~ k ^ 7XFXLXHXJXHXAX} x l k w l i ` GXCX8XbX Xx v Z.H z k ! P d d i . & @ . 2 7 v z.v.V dXmXdXZ.mXSXSXSXbXt.` 7 D ] bXJXSXSXSXMXSXSXl.hXMXhXmXMXV 5 v xXxX} ^ ! {.W.~.^.U.).E.E.E.~.~.E.E.~.~.~.E.~.~.~.~.~.~.~.~.~.~.).).).).(.(.(.(.(.(.(.(.(.(.", +"f ~ ` PXR.l l j Y ~ { uXFXLXJXHXFXuX~ W f f d a } HXZXxXyXn d n Z.H 7 j P l j r p & o @ @ @ o 7 X 9 D ] V 5 hXbXqXv.] G D ` n ` G ' 0X9XmXmXz.G 9.9.3 8 ] hXgX9XwXv D z > $ l ! W.~.^.U.).E.~.E.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.^.~.)./.(././.(.(.(.(.(.(.(.(.(.(.", +"g W ^ DX( l l q L W r b ` CXLXLXPXPXR.k k ^ | 8XCXHXbXxX{ < d v Z.J 7 j l d r * . > 2 o . @ 2 = 7 X X 5 D 5 5 5 9 9 9 @ 7 7 2 v 7 7 v 9 v V D G qXgXxXD 3 3 ' z.D 9 2 > 1 # d u Y.W.~.~.).E.W.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.^.~.)././././.(.(.(.(.(.(.(.(.(.(.", +"U U / eXP P l f L L d r b CXPXR. XUXDX/ k ~ ` 7XCXZXZXyXv p k v B.-.o d l i * * & o o 2 @ . & . < & o 7 o 7 2 @ @ @ 7 2 v < z < z 7 2 7 9 7 5 9 ] z.ZXCX` 7 5 X @ @ & . o # % u _ W.E.E.E.E.E././././././.~.~.~.~.~.~.~.~.~.~.~././.(.(./.(.(.(.(.(.(.(.(.(.(.(.", +"f U ~ / L U k f U Y k x d DXVX~ x W {.[ f d 2 7 t.ZXZXxX} x k z x.-.3 d a $ & & . 2 o . @ . # p # , & . > & o z 7 2 2 2 o & < & < . > 7 > 7 7 7 v ' m 7 @ 2 . @ . + . > . > % y _ W.E.E.E.E.~./././.(.(././.~.~.(.(.~./.~.~.~.~.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"I T P P ^ ~ k k L U q l ~ DX{.W W Q Q ~ d * * o { HXxXVXCX^ q z x.>.5 i , # & & > o > . + + > . # . * * . < & . o o 2 7 & 2 2 2 > > < > 2 7 o o @ . @ . o . . + . . # & . . ; u / ].W././././.(././.(.(././././.(.(.(.(.(.(././.(.(.(.(.(.(.(.(.(.(.(.(.(.(.}.}.", +"E Q L P Q Q P f f L k k ^ BXU ~ P W T Y j i * * XFX` b 7XR.l 7 l.>.7 , # # < o o . > . - - - $ # # . & , . & . o o o . . o . o o . . . . . o o o @ . . . . 2 . + . - . > > . w _ ].W./././.~.(./.(.(.(.(././.(.(.(.(.(.(.(.(.(.(./.(.(.).).(.(.(.(.(.(.(.(.(.}.", +"I U U U T Q k h L L h P Q / T L U T T U j 0 0 r 7XuXd r d ^ l < r.0.5 ; - - - & . > # # - + % # . . + . # # . . . . . . . . . . & # . . o o o o . o o . . . . . + . # # . > ; w _ ].]./.E.(.(.}.(.(.(.(./././././.(.(.(.(.(.(.(././././.(.(.(.(._.(.(.(.(.(.(.(.", +"L U U U T Q k q k L k P U Q U U U T T U k q q r X( j d 0 t y 7 r.0.@ ; + - - & . > & . . - - , - + + + . . + > . . . . . . . . . . . . o o o o o o o . . . . . . . % . . . - t / ].].]./.`.(.|.(.(._._.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"L U U T T Q k q k k k P I I I T R R T U L f q f / L W q w s s 2 0.r.X ; % - # # > > . # > + . . . . . @ @ o . o o o o o o o o o o o o o o o X X o o o o o . # # - . # > o . # w ^ ].].'./.`.(.}.(._.`.`.`._._._.`.`.`.`._.(.(.(._._.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"L U I T R Q j q k k h L I I R T R R T U L f q f E T U f j j 0 7 0.l.X ; % - # . . # . . . . o @ X X X X X X X 3 3 3 3 3 o o 3 3 o o 3 3 3 3 3 3 3 3 X X o X o o . . . & o o - % ! ].].'./.(.(.}.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.(.`.`._.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"k U U T R Q k q l k f L T I T T R R T U k f 0 q I R E L q q q 7 >.l.X , % + . . . & = o @ X X X 3 4 N N N V V V V N N N N N N N N N V V F F H H H H F V 8 3 3 3 o . & o . . > % P ].].'./.'._.}.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.(.(.(._._.`.(.(.(.(.(.(._._.", +"k P U T R Q k q k k k L T I T T R R T U k q 0 q I I T f w j j 6 >.r.3 - . + + . > . X @ X X 3 N F 6.r.y.y.y.y.y.y.r.r.0.6.7.6.7.7.6.6.0.r.y.B.B.y.y.x.x.y.7.V 3 X . & & @ . # % h ].].'.'.'.`.}.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`._._._._.(._._.}.", +"k P U T Q Q k q f l k L T E T T R R R U j 0 0 y k E I L k j q 7 0.<.3 # . + . . o o X X 3 9 ' c.Z.A.aXaXaXaXjXjXjXA.A.A.S.B.S.B.S.S.A.A.G.K.K.G.A.C.C.Z.A.Z.r.' 9 o X o o @ - ; u ].].'.'.'.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.}.}.`.`.`.`.`._._._.}.}.", +"d W U R R Q k q f k g L T I Q R T R R U j 0 0 y k L I I f f f 6 r.>.3 # . . . . X 7 7 8 G t.pXaXaXA.A.fXzXzXzXlXzXjXzXlXzXzXzXzXkXzXkXzXlXlXzXjXK.K.K.A.A.K.Z.c.t.G 5 3 X o . t u '.'.'.'.'.'.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.}.`.}.}.`.`.`.`.`.`.}.}.}.}.", +"` b U Q R Q k q q l L L L T T R T T R U k 0 0 t j U I E L f f 7 r.r.X . . @ o o v ' F H y.Z.fXfXK.jXjXzXzXzXzXlXcXlXzXlXzXzXnXcXzXlXlXlXzXnXnXcXlXjXzXA.jXA.J.B.c.t.-.D 3 X & % K '.'.].'.'.oX`.oX`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.}.}.`.}.}.}.#X#X", +"xXz W Q I _ ~ y j j f U I I U U U R T U k q 0 t k U I L L f f = 0.l.X @ @ . . 9 ' ' H 0.B.A.fXfXjXjXzXzXzXlXcXcXzXzXlXnXcXzXzXzXcXcXlXcXzXzXzXzXzXcXjXA.G.A.C.B.Z.y.e.-.D o @ % K '.'./.'.'.'.oXoXoX`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.}.}.}.}.}.}.}.}.}.}.", +"HX7Xx ^ eXBXM $ l x Y U R R I R I I T U k q 0 y k U I I k j j = 0.l.X > o o 7 D ' H 7.y.B.A.fXfXjXzXzXzXzXzXjXjXzXzXlXnXlXzXzXzXjXzXzXsXsXD.B.e.x.x.S.A.B.B.S.Z.Z.c.l.e.' 7 @ , ! [.'.`.'.'.oX#XoXoXoXoXoXoXoXoXoXoXoXoXoX`.`.`.`.}.}.#X#X}.#X#X}.}.}.#X}.#X}.}.", +"AXZX{ CXPX| d 0 ` R.d Y U R I U E L R U k q q y L U U U k h l o r.l.X > . o v D H H r.x.l.B.A.A.B.B.B.e.e.e.B.S.jXzXzXlXzXzXcXzXfXjXjXD.D.a.q.e.r.e.Z.A.jXA.Z.Z.Z.c.e.e.9.G 5 # ^ [.'.`.'.'.'.'.oXoXoXoX#X#XoXoX#XoXoXoXoXoXoXoX#X#X#X#X#X#X#X#X}.}.#X#X#X#X#X#X", +"ZXHXHXGXVXb i i ` FX^ W Y g ~ P k L U I k q q q L U I U k q y @ >.l.X $ & 7 9 F ' 7.r.e.x.Z.A.A.A.V.a.a.a.f.B.A.A.jXzXzXzXzXlXzXzXfXA.D.D.8.*.=.*.6.r.B.fXaXfXc.c.Z.B.9.t.` v 7 ^ @XoX'.'.#X%X}.#XoXoXoX#X#X#XoXoXoXoXoX#X#X#XoX#X#X#X#X#X#X}.}.#X#X}.}.#X#X$X$X", +"HXAXZXFX{ x b # { FXrXx x {.eX^ k L U L k q 0 q f L E E f 0 t @ >.<.X , & 7 G 7.7.9.r.x.Z.Z.S.S.B.e.0.6.6.0.0.0.B.A.jXK.A.jXlXzXjXfXA.g.i.:.%.*.O.Z F Z Z F F F H ' -.7.t.` v c [ @X@X'.oX=X#X#XoXoXoX#X#X#X#XoX#XoXoX#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X;X;X", +"ZXAXZXGX` b & & ` ZXHX} uXGX8Xl k L U L j r r 0 r W Y f q 0 i 5 w.>.5 , . 9 7.9.-.-.9.c.c.x.B.B.x.r.9.7.7.6.r.y.r.B.A.jXG.jXlXlXzXK.B.e.0.=.C N Z &.6.*.&.H N V t.' V F ' { v v [ @XoX'.oX#X`.#X#X#XoX#X#X#X#XoX#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X$X", +"AXJXHXFXIX^ x = ] ZXAXAXCXVXb j k L U Y d i & & x ^ ~ f q q i X 0.<.5 & 7 D 9.9.-.J H ' G V V N 4 8 N N N N V Z $.r.G.lXlXzXlXnXlXK.b.0.C C 6.B.r.y.y.S.r.c.SX9.8 V A D ` ' D D R.eX+X%X$XoXoX$X#X#X#X#X#X#X#XoX&X#X#X#X#X#X#X#X#X#X#X#X%X%X#X#X#X#X#X#X#X#X#X#X", +"gXAXCXFXPXuXz D bXAXSXZXCX{ b k L L Y W r ` n v 7XtXx j w r r 3 0.>.8 2 9 ] y.9.H F 8 D D V N 0.cXaXy.r.r.B.S.*.O.Z <.n.kXkXL.L.F.p.;.0.jXy.9.V N N F F r.r.c.MXD V ' F D ' u.' XR.+X%X$X$XoX#X#X#X#X#X#X#X#X#X#X#X%X&X&X%X#X#X#X#X#X%X%X%X#X#X%X#X#X#X#X#X;X;X", +"z.ZX` ( { eX{ n CXZXZXZXxXm x k L U Y W r ` } z.iX` r w r r 0 3 0.>.8 D G 9.r.-.F V 3 8 3 N r.y.y.7.F V V N &.B.a.w.p.p.F.F.F.b.p.,.,.q.0.6.V H N V V N C $.H C H F V N V H r.9XgXrX[.*X&XOX&X=X%X%X%X#X#X#X#X#X&X&X&X&X&X&X%X%X%X%X%X%X%X%X%X#X#X#X#X%X%X;X=X=X", +"z.xXx ~ f x x z t.ZXAXAXCX} x Y U T U k p v ] ] } z r r 0 r s 6 r.r.8 D 9.r.6.C V V D D 8 7.9.r.' V N N N N Z F 0.;.;.s.n.p.a.p.3.1.p.p.;.Z O.V 4 N N N B o.o.%.0.-.H H &.r.f.6.y.yX@X:X'.oX-X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X%X%X%X%X#X%X%X%X&X=X=X=X", +"} } k W U k 0 & v CXAXCXGXCX~ f L T U k z D t.] 9 o p d w r d = >.l.N D c.e.0.6.F H V F V H F 6.V N V 4 4 N &.6.C O.%.s.i.F.zXkXF.n.M.s.;.i.8.=.&.O.F O.%.%.;.q.B.e.b.w.;.;.q.o.Z 0XeX:X[.@X-X'.&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X%X=X=X=X=X=X%X%X", +"[ ( Q L I U r * 7 CX8Xv } iXR.j L T U k r ( } t.{ . r r s 0 r 6 >.w.N ' y.$.$.=.6.6.6.r.6.$.O.O.O.C O.O.=.=.=.w.d.n.n.M.:.G.lXlXL.,.H.H.D.d.q.g.a.q.0.,.q.d.s.p.a.b.C.w.<.;.d.s.Z t. X[.:X;X;X=X&X&X&X&X&X&X&X&X=X=X=X=X&X&X&X&X&X&X&X=X=X=X=X=X&X=X=X=X=X;X;X%X", +"~ k L T T U 0 $ b CX` x x x ^ k L L U P d ( ' } 7X` r r s s 0 o 0.w.4 7.6.O.8.8.0.6.B.B.0.;.o.o.o.o.:.s.G.V.s.V.s.H.kXF.%.G.lXlXlXF.H.L.D.s.H.G.kXzXsXD.s.D.F.n.V.V.V.w.<.n.V.a.O.y. X[.:X;X;X2X=X=X=X=X=X=X=X=X=X=X=X=X=X&X&X&X&X&X=X=X=X=X=X=X%X=X=X=X=X=X=X=X", +"f k Y U L k 0 $ b 7Xj W Q L k q L U U Y r p 2 7 [ } d d 0 s t o r.r.4 F &.o.q.8.=.;.a.C.w.w.:.O.O.;.d.sXsXkXH.s.i.L.lXs.q.kXlXzXlXK.a.kXkXV.a.G.L.H.D.M.p.D.F.V.A.A.A.B.q.D.kXV.%.y.eXoX@X*X$X=X=X=X=X=X=X=X=X-X=X=X=X=X=X=X&X&X&X&X=X=X=X=X=X=X=X=X=X=X=X=X=X=X", +"k j L k P P q 0 x ^ Y Q R R L k h U U k j * # * p x k l q 0 t @ >.w.8 V 6.C a.g.q.%.w.n.p.p.d.q.:.i.i.V.s.i.q.d.lXkXH.,.V.lXlXlXzXlXs.G.K.lXkXn.n.i.p.i.p.p.G.C.B.V.B.B.a.q.V.A.=..X@X*XoX-X=X$X=X=X=X=X>X-X-X>X-X-X-X-X-X=X=X&X-X-X-X=X=X=X&X&X=X=X=X=X=X=X=X=X", +"j k L U U k q 0 j j Y U U L L L Y W L k y w ; $ 0 q h k q y y o >.w.4 8 r.O.V.s.8.%.d.s.n.p.n.q.%.i.p.a.a.f.sXlXlXzXF.q.kXlXnXcXnXnXjXB.zXlXlXnXlXkXG.V.V.G.L.F.V.n.b.b.8.o.i.D.r.t.@X+X@X*X-X>X>X-X*X*X*X-X*X*X*X*X-X>X-X*X&X*X-X-X-X-X=X=X&X&X=X=X=X=X=X=X=X=X", +"j k L U U k q 0 j f k Y Y k k L U W L k y u t 0 w j f f k y s 3 0.l.3 V r.=.D.s.:.%.i.i.n.n.V.q.,.s.G.kXlXnXnXcXlXnXb.C.zXlXcXnXnXlXnXS.G.zXlXlXlXlXzXK.K.K.F.V.V.n.,.C.d.o.;.S.c.8XeX:X@X;X-X=X>X-X*X*X*X-X*X*X>X-X-X-X-X*X*X*X-X-X-X-X=X=X=X=X=X=X=X=X=X-X-X=X", +"j k L L P j 0 0 j k L P Y k k L U U L h y 0 r r f f f f k w i 5 >.w.V 9XcXe.V.V.%.q.s.i.p.n.b.w.:.,.L.nXnXnXnXnXnXlXq.jXlXlXcXnXnXlXlXjXq.sXzXzXlXlXK.F.G.F.V.n.V.p.w.C.sX8.q.f.9X8X+X*X*X-X2X-X>X-X-X-X-X-X-X-X>X-X-X-X-X-X*X>X-X-X-X-X-X=X=X=X=X=X-X-X-X-X-X-X", +"h k L L P j 0 0 j k Y U P Y k Y Y U k h y w r r j r j f y t r X C C.c.hXcXA.K.p.w.A.C.q.<.p.a.b.p.i.F.lXcXnXlXzXjXq.q.G.kXlXcXlXnXnXnXzX0.:.s.H.G.G.G.K.kXkXF.n.a.e.b.C.kXD.q.y.0XeX+X*X*X-X-X=X>X>X-X-X-X-X-X-X>X-X-X-X>X-X-X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"h k L L k s $ $ y j L Y Y L L Y Y U k h y 0 r r r f k r r d & 9 w.C.c.hXcXA.G.n.C.jXG.q.<.p.w.b.D.i.p.D.kXkXV.i.;.:.D.H.kXlXnXcXcXlXlXlXsXi.8.8.:.;.a.G.G.G.F.a.a.a.l.C.kXs.q.S.8X@X:X>X-X-X>X=X>X-X-X-X-X>X-X-X-X-X*X>X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"k k L L l t $ $ y k L Y L L L Y Y P k h y 0 r r r f f s r # 7 t.J.P.c.hXMXfXC.G.C.K.K.a.q.i.w.p.F.M.i.:.%.;.;.:.o.s.kXH.kXzXcXlXlXlXzXzXsXH.8.H.H.H.sXkXD.V.V.a.b.e.B.A.G.q.V.B.8X@X*X-X-X-X2X2X>X>X-X-X>X>X>X-X-X-X-X>X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"k L L L l t % $ y k L I P P L L L Y h h y y s q r r r s * & ' hXK.J.x.hXcXx.B.K.C.A.J.b.i.i.p.b.a.F.D.s.V.H.V.i.:.H.D.V.G.sXzXzXjXG.sXV.s.s.:.q.H.kXkXH.D.n.n.V.b.e.B.A.b.V.G.c.eX:X>X-X&X*X*X&X>X>X-X-X>X>X>X>X>X-X>X>X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"k k L k j t # ; y k L I L L L k g L f h y y s 0 q r s r * D wXgXJ.P.x.cXMXl.b.C.jXA.C.e.q.i.a.b.p.n.V.H.sXkXV.%.o.;.O.%.q.q.f.B.B.f.8.:.Z O.B O.s.G.H.D.H.V.V.C.B.e.B.0.C.K.s.pXeX&X=X-X,X,XX>X-X-X>X>X>X>X>X>X>X>X>X-X-X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"f P k f ~ 0 $ ; j w I T L L f L Y P k y y y y % 0 w y i ` 0XgXhXP.P.x.cXhXl.x.A.b.b.%.w.p.i.a.n.p.n.V.D.G.V.:.o.V.q.Z Z C o.$.$.$.O.=.o.%.g.:.B :.s.g.s.V.n.V.C.V.B.e.q.q.w.A.9X+X-X=X>X*X*X*X,X*X>X>X>X>X-X>X>XX>X-X-X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X2X3X3X", +"d j f W s s $ % s k g L L k h g g k f y t t t t w y t | tXwXgXfXP.P.B.cXhXc.x.A.C.B.C.K.V.i.p.n.n.a.F.V.V.i.o.i.G.G.V.V.V.0.o.N C 8.g.V.g.D.i.Z B ;.d.s.s.s.V.C.B.C.kXG.K.C.fX0X+X=X=X-X*X*X*X:X*X>X>X>X>X-X>X>X>X>X>X-X*X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X-X>X>X", +"p d W / d $ i i 0 k P U P k L k g f q q i i $ i $ d 8XiXrXgXgXfXP.P.B.cXhXZ.x.S.jXA.jXkXsXa.p.n.F.n.n.V.i.O.:.D.H.D.H.V.H.H.g.q.a.V.s.V.s.a.V.q.B o.:.,.i.s.V.C.V.C.jXzXC.fXgX8X+X*X-X>X,X*X:X,X*X>X>X>X-X-X>X>X>X-X-X*X*X*X*X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X>X", +"( m X[ d s $ $ l q j k k k h f g k j 0 i , & * x 7XiXtXyXqXgXdXP.P.B.cXhXZ.r.e.jXzXjXlXjXa.b.V.C.n.a.p.%.o.s.D.H.H.G.H.G.G.sXH.sXH.V.V.D.V.H.s.O.B O.;.a.V.s.b.b.A.zXjXK.dX0X8X+X*X-X>X,X:X:X,X*X>X>X>X-X-X-X>X-X-X-X*X*X*X*X*X-X*X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"qX0X8Xx r r * $ 0 s j j j f g k g j j r * & 6 D XrXuXyXyXgXgXfXP.P.B.zXhXZ.9.=.A.jXjXzXr.e.V.b.F.b.n.<.o.:.s.D.G.sXsXkXsXkXlXzXzXsXsXD.H.G.G.V.s.O.o.:.i.s.V.C.B.B.zXjXB.c.7XeX+X*X-X>X>X*X*X-X-X>X>X>X-X-X-X>X-X-X*X*X*X*X-X-X-X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X", +"mXqX} z p z $ * b ` b j l w k k h f s i = 6 A u.rXrXyXxXqXqXxXfXJ.P.B.zXhXZ.-.H >.B.A.0.N -.e.C.C.C.n.:.o.p.d.H.G.sXG.kXsXkXlXzXzXkXsXsXsXG.G.V.V.i.o.d.n.V.F.b.B.*.r.B.e.9XeX@X+X*X-X-X-X*X*X-X-X-X>X>X-X-X-X>X-X-X-X-X-X-X-X-X*X*X*X*X-X-X*X*X-X-X-X-X-X-X-X-X", +"hXqXD z z e * ( R.[ d $ d s q q h h j r 6 c A u..XvXxXyXqXqXxXdXP.P.x.hXzXZ.7.H -.0.0.Z 4 H w.V.V.G.V.,.:.s.s.D.s.a.q.d.i.d.H.H.g.s.i.s.s.a.s.D.s.V.;.s.H.F.G.V.e.C F 6.fXwX+X+X+X+X-X-X-X*X-X-X-X-X>X-X-X*X-X>X>X>X-X-X-X-X>X>X*X*X*X*X*X*X*X*X-X-X-X-X-X-X-X-X", +"bXqX( z = p & ` } p d d 0 s q k q h j $ 6 c A &.y.gXxXxXyXqXgXhXP.P.B.fXzXZ.9.H H =.x.9.V V e.C.C.F.kXs.i.d.s.d.%.o.o.o.o.o.:.:.o.o.o.o.o.o.o.:.q.d.D.F.kXF.n.B.B.C *.c.c.z.eX+X:X:X-X5X-X-X>X>X-X-X>X-X-X-X-X>X>X>X>X-X-X>X>X>X-X*X*X*X*X*X-X*X*X-X-X-X-X-X-X-X", +"ZXxX.Xz z = . ` n x d s l q q h f j s r = A S S r.9XgXyXxXqXgXfXC.F.m.fXfXe.6.H F V D D V N 0.b.V.G.L.L.i.i.:.O.o.%.:.;.8.8.+.+.:.d.H.D.s.D.n.a.i.s.n.L.L.F.S.Z.e.H -.y.qXtX@X,X*X-X5X5X5X>X>X>X>X,X,X,X,X,X-X-XX,X*X>X>X*X*X-X-X-X>X-X-X-X*X*X-X-X-X-X-X-X2X", +"0XyXuX( = p . { ^ * d j j h y s r j s r 6 A S B 6.pXgXyXgXgXgXfXF.F.m.jXZ.0.>.;.Z N 3 9 v V =.b.b.n.G.L.V.p.a.p.s.s.g.H.sXsXH.sXsXH.G.sXH.D.s.n.V.p.F.L.F.n.A.B.9.H 9.c.yXtX+X+X-X2X5X5X5X-X>X>X>X>X,X,X>X>X-X-X>X,X,X*X*X*X*X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X>X", +"` = { } z & < 7X{ * d r q j 0 s s r y r 6 A S B ..g.gXgXgXgXgXfXF.P.B.fXZ.>.;.,.<.Z N N N N F w.p.n.L.F.n.p.M.n.s.n.n.V.H.sXsXH.H.D.H.V.d.p.n.F.F.p.F.L.n.n.B.x.-.H 9.0XtXeX+XX>X>X*X,X>X>X>X>X>X>X>X-X-X-X*X*X,X*X*X*X*X,X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X>X", +"z p r z r * & } Xx d r r s 0 s s s t * 6 N S B B r.gXgXgXgXhXfXP.P.B.J.Z.0.,.<.3.3.<.Z N N Z 0.a.b.V.F.M.3.n.M.s.p.p.p.i.q.8.;.:.:.8.q.p.D.F.V.V.a.M.M.M.a.B.r.J -.t.8X7X@X:XX>X,X,X,X>X>XX>X>X>X>X-X-X*X*X*X*X*X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"i d r s r * $ < } b d r d r 0 d y s i & 3 N B B B O.aXgXgXgXhXA.P.P.l.J.Z.0.,.<.3.2.k.k.<.Z N *.w.p.p.F.n.p.n.n.F.D.n.d.,.;.;.;.;.:.;.<.p.n.n.p.V.V.n.n.s.a.y.7.7.9.} 8XeX:XX>X>X>X>XX>X>X>X>X>XX*X*X*X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"y q j q $ r * * b x d r d q i i t i = 6 N B B B B ..h.aXgXgXhXZ.P.m.b.J.A.>.<.<.3.2.2.2.3.1.B *.q.n.p.p.,.n.D.s.p.s.s.a.V.G.G.G.D.V.V.a.p.p.p.n.C.V.s.D.i.a.y.H 9.t.} eX+X1XX>X>X,X>X>XX>X>X>X>XX>X*X>X>X>X*X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"l w h l s $ . & $ i s q j q y 0 t * 6 A B B B B B .+.aXaXaXgXZ.P.m.b.K.A.<.,.3.4.2.2.2.4.1...O.d.w.q.a.n.n.D.F.F.D.V.H.zXzXzXzXzXL.kXkXL.L.L.L.C.a.n.d.s.q.B.' } 8XeX+X+X,X,X,X,X,X,X>X2X3X3X3XXX>X>X>X>X>X>X>X>X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"y l h s $ $ * & i 0 j f h q t ; , o 4 A S .B B B .. .g.aXaXgXZ.m.P.m.G.C.q.3.4.4.2.2.2.4.4.o.o.s.q.p.p.V.a.n.V.s.D.D.D.L.lXlXlXzXH.kXkXL.L.F.L.B.b.a.;.a.e.c.t.} eXeX+X>X>X,X,X,X,X,X2X2X3X3X3XX>XX>X>X>XX>X>X>X-X-X-X-X-X-X-X-X-X-X-X-X", +"y h j s r $ # # i t q y h q t , = c A S . .B B .X...+.aXpXpXx.k.N.m.n.n.,.p.4.4.2.2.2.4.3.Z B e.b.q.w.p.b.p.n.n.M.n.n.F.F.L.kXH.G.kXzXkXL.C.C.e.0.;.q.q.r.y.t. XeX:X1X3X2X1X,X,X,X,X>X2X2X3X3XX>XX>X>X>XX>X>X>X-X-X-X-X-X-X-X-X-X-X-X", +"y s s s r $ # # $ d l q q q i = c A S ..X.X.B B .X...@.f.S.aXe.k.N.k.p.p.<.3.4.3.2.2.2.3.,.Z F A.b.;.;.w.p.,.p.n.n.n.p.n.n.F.V.V.D.G.A.F.F.n.e.0.$.%.o.%.x.y.D 7X[.+X5X3X3X-X>X,X,X,X,X>X>XX>X>X>XX>X>X-X-X-X-X-X-X-X-X-X-X-X-X", +"r s s i r $ # # ; d j q r d = 6 A S S ..X.X. .X. .....B :.S.aXe.k.N.k.<.<.<.3.<.2.2.2.k.<.$.8 gXaXB.r.%.$.%.%.;.a.n.b.a.s.e.e.q.e.g.B.f.a.a.q.=.F C Z C r.Z.t.o ^ R.+X5X3X6X6X2X>X,X,X,X>X>XX>X>XX>X-X-X-X-X-X-X-X-X-X-X-X-X", +"r s s r r $ # - ; i q r r 6 6 A S S ....X.X. . . ..... .o.g.S.b.k.N.k.<.<.1.3.3.k.2.2.3.;.4 9 AXc.S.f.*.Z N Z C =.0.q.0.=.$.&.=.6.6.6.=.=.%.%.o.Z N Z r.B.Z.t.X < M T.:X5X3X3X4X3XX>X>X>XX>X-X-X-X-X-X-X-X-X-X-X-X-X", +"s s s r r $ # - ; i r r 6 6 N S S .X.X.X.X. .B . . . .B i.V.b.k.N.k.<.<.:.3.3.3.4.k.<.4 X t.bX9.S.f.e.$.N N N Z F F Z V N V F F V N N Z Z C o.Z Z 0.B.B.Z.t.@ > $ y {.NX3X3X-X3X3XX>X>X>X>XX>X>X-X-X-X-X-X-X-X-X-X-X-X", +"j j j r 0 $ # ; i p p 6 8 A S .. .X.X.X.X. . .B B B ..B X.:.D.,.k.N.k.1.:.%.p.<.,.<.-.8 X X wXgXH S.f.f.r.C C Z Z Z N 4 4 8 8 4 4 4 4 4 N C $.$.F 6.f.a.Z.Z.t.o . - $ y [ +X1X6X3X4X4XXX>X>XX>X>X>X-X>X>X>X>X2X2X2X2X", +"j j j q 0 $ # ; , * c c A S .. . .X.X.X.X. .B B B B .. .X.+.D.;.k.N.N.1.1.%.w.%.0.V 3 3 = & xXgXZ A.B.e.r.*.&.&.&.H V N 8 8 3 3 3 4 N V F =.6.*.*.0.e.B.B.pXz.X < # # ; % ^ @X,X>X3X4X3X1XX>XX>XX>X>X>X>X>X>X>X2X2X2X2X", +"j j j q 0 $ # - , z ( ) S S ..X. .X.X.X.X.X.B B B S B ..X.X.g.;.m.N.P.1.%.$.r.H 3 3 @ o * . CXmXV C.B.e.e.;.$.6.=.7.F V N 8 8 8 4 N V F &.6.6.*.6.f.e.B.B.mXt.o # , & . - + h _ @X:X1XNX1XX>XX>X>X>X>X2X2X2X", +"s s d w r $ # . 7 } vXXXS ..X.X.X.X.X.X.X.B B B B B B X.....f.;.m.L.P.-.Z N 3 X o o # # # & CXZXF e.B.e.e.;.;.=.6.6.&.F Z V N 8 N N Z C =.6.*.*.0.f.e.B.hXZX} ; # # & & # # ; u w ! {.+XNX,X,X4X1XXX3X4X4X1XX*X*X*X-X.Z 3 X X . . # # # # > @ CXZXF 0.A.e.0.q.;.=.;.=.O.O.C Z V V F C O.%.6.;.%.;.e.e.B.Z.SXAX| % # # # # # # # # ; t u h _ +XNX+XX>X4X4XX , i t t y h h ^ T.+X+X+X1X1XX>X>X # # # # # # # # & # # i d i r t h w h {.eXNX1X1X,X,X1X1X1XX4XX,X,X & o XFXHXt.&.f.a.,.w.<.q.0.=.&.*.*.=.8.;.:.;.0.q.;.;.e.0.pXAXZXPXVXo & # # # # # # # # & & < < , * , a i d y y l / T.+XNX1X1X1X,X,X1X4XX>X>X-X5X > > + + + . . . & # # | CXHXZXF 0.s.,.<.w.w.;.=.&.=.*.;.8.;.:.;.w.q.;.0.r.r.ZXAXGXGX} = o # # # # # # # # & & & < < < , < , i i i t y h h ^ _ ].+X1XNX1X,X1XX2X>X-X-X5X5X5X5X", +"s q q 0 , o 9 9XgXgXxXyXxXCXgXD 3 o o @ + + + . - . # - > # , . > . . . @ . . . . . . # # , | IXHXHXt.F e.a.,.<.w.;.=.&.=.%.;.8.;.;.;.q.;.=.=.-.hXAXHXGXGX` o & . # # . . . # # & < < * , < < > < < , , i d s y h l h h ! [ @XNX1X1X1X1XX-X3X2X>X-X5X-X5X-X", +"j q s 0 * o 9 9XgXxXxXyXtX X7 o o o . + . . . . & # # - - - - + + . . . o o . . > . # . # # ` IXFXLXZXF =.e.p.<.w.q.=.*.*.o.;.;.;.=.,.q.;.*.F c.ZXJXHXGXFX< & . . . # . . # # # # # * , i i , , < < < p i i i i s d l l y q l ! T.NX1X,X,X1X*XX>X2X2X-X-X5X5X", +"y y r r = o 5 9XqXbXwX7 2 2 . 2 & & & > # # & o & & # + + . . . . . . . o & & & # # , > - # n GXFXHXJXmX&.r.w.p.w.e.=.$.=.C $.$.$.=.%.w.;.C 9.ZXHXHXLXGX7X& $ # . . # . . . # # * , , , i i i i < < < < < p p p a i i d d s s j h ! T.NX1X,X,X*XX2X2X-X-X5X5X", +"0 s r r p & @ qXbXyX7 2 o & > & & = & & & # & # . . . . o o o o . . . # # & * * * * * , # & z FXPXHXLXAXZX6.a.s.e.e.6.*.=.C $.*.$.O.0.0.$.7.AXHXKXPXPXGXD . $ # . . . # # # # # * ; ; ; i i i i , < p p < < < < z < < p a p i r y h L _ @XeX1X,XX-X-X-X-X", +"i i $ r * & o wXxX` o < . # # # = = & & & & # - . . . . o o o o # # # # $ $ $ * * * * & . & 2 tXFXLXPXLXJXcX6.g.A.e.r.0.%.C *.&.$.&.e.0.H SXHXKXKXLXPXCX2 . # # & # # # . # # - # $ ; ; i t i i i i i i < < < < 7 < < < < < < p d s y h h ^ [.1X:X5X5X5X-X-X-X-X", +"p = * * * & o 0XyXo 7 . $ 1 # > & & & & & > - - . . . . o o o o # # # $ $ $ $ $ $ # * * & & & ` FXGXKXKXLXJXpX6.B.B.b.e.=.$.r.&.&.y.r.H SXLXLXKXKXKXLX X& & & & > # # # # - - - # - ; i i i i i i i i i , , < < < < < < < < p a a x i t y h / NX:X5X5X5X-X-X-X-X", +"v = * i # & = 0X' o . 1 , . & > - - & & & > & - . . . . . . . . # # # # # # # # # # & & , & < < xXFXPXKXKXLXJXc.r.B.A.B.e.0.y.0.r.y.7.AXLXLXKXKXKXKXPXn , > & > > > # # - - > > > , , i i i i i i i i i i , p < p , < , p i , , & & z i t y u [.:X > . + - & & & & & & # # # # . . . . . o o . . . . + + + + . - # p * t.GXPXKXKXKXJXJXx.e.B.B.x.e.y.e.e.c.AXLXLXKXKXKXKXKXtX# , > > > , ; # # - > > > > > > , , , i i i i i i i i i p i i , i i i t $ 7 o 7 > $ s u _ :X5X5X5X5X-X-X-X", +"yXD & > # # o } o X > > - # o @ + + o o & & & & . # # # # # # . o o o o . . . . + . . . - # , p ` GXGXKXKXLXLXJXAXx.Z.fXMXnXcXcXcX9XAXKXKXKXKXKXKXLX| , , # < & , ; # - - > > > > > > > , , , i i i i i i i i i , i i i * t i i = 2 & > a % y K :X5X5X5X-X-X-X-X", +"xX8X3 o o o 2 n o o > > > > & & > - # & o . & # . . . # # # # . # o & o . . . . . . # # # & , , m IXGXLXKXKXKXKXLXJXHXAXAXZXxXwXD 5 D FXKXKXKXKXLXCX, , 1 > . > > > > > > > , , , , , , , , , , i i i i i i , , , , , , , , , , # > , , > - $ q @X:X5X3X-X>X*X>X", +"xXqXG X o o = z & & o > > & & # & # # . # # # # . . # # # # # . . # # - # # # # # # # # # # & & < tXPXPXLXKXKXKXKX8X` ` n v z < < < & m CXKXKXKXKX} , - > > > > > > > > , , , , , , , i , , , , i i i i i i , , i i i i , , * $ $ ; , , > & # 0 {.:X3X-X>X,X*X # # # . . . - - - # # # . . . . # # # # # # # # # # # # # & < } FXPXKXLXKXLX} < . a > & < > & > z > m IXKXLXGXb 1 , > < > > , , , , , , , , , , i i i i , , p i p p p p i i p p i , , * $ $ $ , , , & & - t ! 1X3X*X>X*X>X>X", +"gXgXgXt.3 7 7 & & & o & & & # # # - # # # # # . > > > # . . . # . . # # # # # # . # # # # # # & > ` FXKXKXKXLX{ > & 1 1 < a < & 1 o . . a n GXKX8X< , 1 > < > > , , , , , , , , p p p p i , , , i i i p p p i i i i , , * * * $ $ , * * & & - t h +X*X > # # . . . . . . - - # # # # . # # # # # # & , b CXPXLXKX| # & , , . > , > 1 > 2 < < > & ` GXn , > , > > < > , , , , , , , , a a p p p , , , , i i i i i i i i i , , , , * $ , * * * & # # $ q {.+XX3X-X", +"hXpXgX0X' 7 X o o o o & & & & & # # # - > # # . > > # # # # . . . # - # # # . . # # # # # # # # & a R.PXKX| , , , # & & & > & > & & & & . z # ` 1 # 1 > > > 1 > < < , , , , , , p p p p p i , , , i i i i i i i i i i i , , , * * * * * & # # $ q ! 1X:X,X > > > . . # . . . - # # . . . # # # # # # # # # # # # # & # a ` PX} , # # & . > > . > > # > 2 < & a . , 1 a > z , > , < > , , , , , , , , p p p p i , , , i i i i i i i p i i i , i , , * $ $ * * & # # $ t u [.+X,X > > . . . . . . - - # . . . # . # # # # . # # # # # > , , # z | & , > . # # > # . # . > & & < > # a , 1 , , , , & 1 > > , , , , , , , , p p p p i , , , , , , , , , , i , * , * , , , , $ $ * & & # # # % u _ +X,X-X3X3X", +"fXpXpX9Xt.9 X o o . . # # # & > # . . . . # # - > # . # # # . . . . . . # # # # # # # # # # # > , , a # - 1 . > 1 > > > < . > m 7 z m , , , , < , , , , , , , > , , , , , , i p p i , i p p , , , , , * * , , i , , , , , , , * $ $ $ # & & & - ; u P +X1X5X3X2X", +"fXpXpX9Xt.5 X o o . # # # # # # # - . . # # > > # . . # # # # . . . . . # # # # # # # # # # # & , , 1 > # # . > # # # 8XtXCXCXCXCXCXiX7Xa > , > , , , , , , , > , > > > , , , , , , , i p p < , , , , * * , , i i , , , , , * * $ $ $ # # # # # # % y [.:X > > - # . . . # # # # # . . # # # # # # # # # # # # # ; , , & , 1 & 1 , | CXPXKXKXKXKXKXPXIX| 1 > # > > > , , , , > , , > > & > , , , , , p p p , , , , , * * , , , i , , , * * * $ , , ; - # # # # # % % [ +X1X3X3X", +"fXdXpX9Xt.3 o o o # # # # # # . - - - - # > > > # . . . . # - # # # # # # # # # # & # # # # # # # ; ; , > , 1 # > 1 uXIXKXKXLXKXKXKXPXIX} 1 # , > > > , , , > > , , > > , , , , , , , p p i , , , , , * * * , , , , , * * * * $ , # # # # # # # # # % ^ :X1X2X3X", +"fXdX9X9Xt.X o o o # . . # # # # . . . # # # # # - # . . . # # # # - - - # # # # # # # # # # # # , # # , , > < a < { CXLXKXKXKXKXKXKXPXGX( > , ; , , , , , , , < , , , , , , , , , i p p i , & # , , , * $ $ * , , , * * * * * $ # # # # # $ $ # o . $ P NX1X3X2X", +"fXdXpX9X' X o o # # . . # # # & . . # # # # # # > > # . . # # # - - # - # # # # # # # # # # # # # ; , , a 1 > > n tXFXKXKXKXKXKXKXPXGXtX, , a # , , , , , , , < , , , , , , , , p p p i , * # # , , * * $ $ * , , * * * * * * # # # . # # $ * # o . ; u +X1X3X2X", +"fXpXpX9XG X o o # # # # # # # # . . . . - # # # > > - . # . . . . . # # # # # # # # # # # # # # # , , # , , & m 7XCXLXKXKXKXKXKXKXPXCX` , , 1 , , , 1 1 , , , , , < , , , , i i p p i , * $ & & , , , , * $ * , , , * * * * $ # # # . . # $ * & o . # w [.1X @ - . > > - # # . . . . . # # # # # # # # # # # # # & , # , a a # m 7XCXGXKXKXLXKXKXKXPXCXuX< 1 a , i 1 1 1 1 , > > , 1 1 1 < 1 , p a p p < , $ # & * , , , , , ; , , , , , * & & # # # # . # # $ $ # & & # y {.1X & # . . # # # . . > . 1 @ . @ > . . > . . . . . 2 & . # # > . > > & # # # # & , # a , a , 7XCXGXKXLXKXKXKXKXLXCXVX( z a 1 , i a 1 , , > > > > 1 - 1 , , 1 , , , & = o , # * = < & , $ , , ; , , - ; & & & # # $ # & # # # $ $ ; # $ t [ NX4X3X", +"aXpXpX9X9 X & # & & & o . . # # > . . 1 . . 1 . @ @ o 2 . o > > . . . & , < . 1 & & & # # # # # # , , , i M R.CXFXKXKXKXKXKXPXIXrX} z < < a , 1 1 1 , < , > > > < > < a 1 , a < < = = = . & = 6 7 & a # - % ; , , # # # & & & - # # # & # # $ $ $ $ $ 0 _ NX4X3X", +"fXZ.9X9X5 X & # # # & o . . # # . o 8X8X8X} } | ` ` n m 2 > o . 2 & > & . < . # # & # # # # # # # 1 # , , a ` iXIXPXLXKXKXLXCX7X} n , , , a , , < < < < < > > , , a < < , , < = 6 6 6 c 6 c 6 6 6 6 , ; t : & & = * & & & & . # # # # & # # $ $ $ ; * 0 ^ NX4X4X", +"fXZ.9X9XX o & # # . o o # . # # 7 . 8XxXKXKXLX7X7X7XrXuXiXxXtXrX8X X{ ` & . 1 > # & # # # # # # # , # , , , a 7XVXHXKXLXLXFXrX{ ` i i a i a i , i < p < < , , , * x p p p < 1 ` D ) ) ) ) ) ) ) ) u.2 + + ; o = & & o > * # # - # # # o # # $ $ $ ; * 0 ! NX3X4X", +"dXZ.9Xz.X o & # . # o o & # # # . z 8XCXLXKXCXt.8X7X7XrXtXiXxXxXiXiXtXtX{ > . . & & > # # # # # & # , a i i , | iXFXLXHXZX7X} } n t i i a $ i p i p p p p , , , d r * < z & o 8X5.5.5.5.5.5.5.5.5.vX7 + ; , = = 2 > @ > $ % . . # # # & # # $ $ # ; # $ l +X & & & & & # # - > , # , , i a , m uXVXZXFXtX X8X8Xi t i i a * p p p p p i i 1 , , ; i x = < < 6 0X5.5.5.5.5.5.5.5.5.vXv @ = = 8 3 3 3 X 2 , % # . # # # & & & ; ; ; - , $ h [.1X4X", +"pXpXpX' o o . # & & o o . . # # < D yXCXCXVXFX7X8XuXVXCXCXCXVXCXCXVXxXiXVX7X` . & & & & # & > > , , , , , i a & 8XCXFXxX8XrXVX` , i p , p p p p i a i i , 1 , , 1 > 6 7 { rXu.u.u.) .Xu.u.u.XXu..X7X' t.u.7.O.@.O.7.9.] ( r o o # # & & > > ; ; ; , < , y {.1X4X", +"pXpX9XD o o . & > o o o . . # # & ` XuXFXFX7Xt.8XrXrXrXtXuXuXiXiXuXyXtXuXtX7Xz & & & & # # > > # , # a i * a < ` VXxXrX8X8X7X= < * < * * p , , i a i 1 , , , > @ 5 A ' yXPXvXu..X} uX X.Xu.vX{ BX} ] cXXXh.+.j.+.h.pX9X' = o = & - & > > > ; ; - # , $ y [ 1X4X", +"dX9X9X9 o > # # . . o o . . # # & n { } } } X7X7X7X7XrXtXtXuXuXtXtXtXtXrXrX7Xz # & # & # # # # > , ; ; , , , < & uXyX} ` 8Xv & < * * * * , , , i , , , < < < 7 4 V O.u.vXPXvXu..X{ VX X7Xt.vX} BXR.9.aXg.h.j.#.#.h.g.pX' z & > & > > > > $ ; ; ; > , $ y / 1X4X", +"dXpX9X9 @ > . # # . o o o o & > . & & # . . < z m n n ` ` { } } 8X7X8X7X7X7X7Xz # # # # # # # # > > , , , * , * < ' 8X` D { & = , , , , , , , , ; ; , < = 6 6 8 Z *.8.8.5.h.5.u. XrX8X X X7Xu.t.8X} ' 8.5.j.#.#.#.#.+.r.A & . > # > > > , ; ; ; ; > > , y ^ NXNX", +"pXpXz.5 @ > . - . . o o o . # # # # . # & & # # & & & # . . # & & & 2 z m n n & # . # # # # # > > & # # $ * * & = 7 ` D ( c & & # - - - , , 1 1 1 1 , 2 6 c A F 0.G.G.D.+.j.5.XXrXVX` 7XiXGX( { uXrXu.5.j.#.#.#.#.#.+.h.c * # > # & > > , , ; ; ; - > i t h NX4X", +"pXpXt.X @ > . > . . . o . . # # . . . # # - # # # # # # # # # & & . o & . . # & # # . # # # # & > & # # $ $ # & & & 7 ( D . < & # - - - - ; 1 1 1 1 , = 6 c D F e.K.A.B.+.+.5.u.rXVX| 7XrXCX' t.uX.Xu.5.j.+.+.+.+.+.+.y.v ; . . # , > > , ; ; ; ; > > i t u NX4X", +"pX9X] X o & . # # . o o o . # # - > > . . . . # . . # # # # # # o o 2 . # < & . # # # # # # # # & & # # # # # # & & > m < > # , - - - - - - , , 1 , > 2 7 c A A F H F D A A A A ( ( a M b ` c v n m c 4 N N 4 4 N 4 N N O . - , ; , > & & $ ; ; ; , , , t u NX1X", +"pX9X' X o & . . . . o o o o . # # + - > > > > # # > > > > > > & > & < . . & . # # # # # # # # # & & & # # # # - > & > & . < # - - - - - > , < 1 , ; - > < 7 6 6 6 7 o 7 6 6 7 z z < i a z < , 1 < < 6 c o 2 @ @ 7 3 3 X = . - - ; , * & * $ ; ; ; , , * t u @X1X", +"pX9XD X o . . . . . . . . . . . > . . . . . # & # & # # . . . . . . > & , . . > # # # # # # # # & # # # # - - , & , > > # - , # - - , * < < p < , ; ; - , > < < p < z z a < = 7 z z a , p x d - , < < z < , , p = < z = = o . # , * * * * $ ; ; ; - , # t u {.NX", +"pXc.D X o . # # . . o o o o # & # . @ > - > & & & & & & & & & & > . > . > # , # # # # . # # # # & . # # # ; ; , , . # > . - , , , , , < z 6 = * # ; : : : , 1 1 i i p p ; a 7 7 < < < z z = i i , 1 < & , p i r r r * * o & & > $ * , , * , ; ; ; # , # i q _ NX" +}; diff --git a/Tests/images/hopper_rgb.xpm b/Tests/images/hopper_rgb.xpm new file mode 100644 index 00000000000..063833b3a35 --- /dev/null +++ b/Tests/images/hopper_rgb.xpm @@ -0,0 +1,11174 @@ +/* XPM */ +static char *dummy[]={ +"128 128 11043 3", +".F0 c #000000", +".Jj c #000001", +"#94 c #000002", +".F5 c #000003", +"ax0 c #000004", +".Gy c #000005", +".HF c #000006", +".Mc c #000007", +"#G5 c #000008", +".F2 c #00000b", +".HC c #00000d", +".Dj c #000010", +"#g# c #000012", +".7l c #00001d", +"#aa c #00001e", +".qa c #000028", +".LB c #00002d", +".KU c #000100", +"aPY c #000102", +".KG c #000103", +".KV c #000104", +".KW c #000106", +"abf c #000107", +"acJ c #000108", +".EX c #00010e", +"#ve c #000114", +"#D2 c #000121", +".LC c #000127", +".Hv c #000200", +".Jo c #000201", +"aD6 c #000204", +"abc c #000205", +".SU c #000206", +".QD c #00020b", +"axP c #00020e", +".Hp c #000210", +"#7j c #000214", +"#Fs c #00021e", +"#eC c #000220", +"a.O c #000223", +".4f c #00022e", +".2z c #000230", +".Jn c #000300", +".Ky c #000304", +".Jk c #000305", +".Rr c #000307", +".IN c #000308", +"a#G c #00030a", +"aua c #000313", +".BL c #000314", +"#hQ c #000321", +".N5 c #000332", +".Kz c #000405", +".KX c #000407", +"aIk c #000408", +".KS c #00040a", +"aP7 c #00040c", +"#92 c #00040d", +"aCO c #00040f", +".Di c #000412", +"avW c #000417", +"#hF c #00041b", +"#kY c #000421", +"#mm c #000422", +".4e c #000436", +".T5 c #000437", +"ab# c #000507", +"aJL c #000508", +".KR c #00050a", +"az2 c #00050b", +"az4 c #00050f", +"#tr c #000512", +"#nM c #000522", +"aN2 c #000609", +"#uZ c #000616", +"#3T c #000617", +"atm c #00061f", +"#A5 c #00062b", +"#hW c #00062d", +"a#D c #000701", +"aPD c #000709", +".Jl c #00070a", +".IX c #00070f", +"#2x c #000715", +"#tb c #000716", +"#Fx c #000717", +"avo c #00071d", +".BN c #00071f", +"axg c #000720", +"#86 c #000722", +"#On c #000728", +"#Cy c #000729", +"arw c #000730", +"abe c #000809", +"#2w c #00080c", +".Jm c #00080d", +"#5D c #000818", +".A# c #00081d", +"axQ c #000820", +".IW c #00090f", +".IY c #000911", +"#ts c #00091b", +"asO c #000927", +"asc c #00092b", +"az3 c #000a11", +"awR c #000a25", +"asN c #000a26", +"#Rr c #000a2b", +"aOL c #000a2c", +"acE c #000b00", +"#tt c #000b1c", +"akR c #000b24", +"akS c #000b27", +"#87 c #000b29", +"aPH c #000b2d", +"aM1 c #000c2e", +"#D4 c #000c2f", +"#Cx c #000c30", +"atn c #000d28", +"aCB c #000e0a", +".Sa c #000f33", +"#7m c #000f35", +"#tq c #001020", +".TG c #001032", +"aiF c #001034", +"adG c #001232", +".QE c #001439", +".Vi c #00143a", +"#2y c #001c36", +"aAn c #010000", +".H0 c #010001", +"azp c #010002", +"adp c #010003", +".I. c #010008", +"#k3 c #010018", +".2A c #010020", +".lR c #010037", +".l2 c #01003f", +".IZ c #010107", +"awn c #01010a", +"#wx c #010124", +".DB c #01012f", +"acM c #010202", +"asx c #010204", +".Uv c #01020b", +"#x4 c #01020d", +".FX c #010301", +"as7 c #010304", +"aQD c #01030a", +"#7h c #010311", +"Qta c #010332", +"aK0 c #010405", +".Kq c #010406", +".Kk c #010409", +".Mh c #01040b", +".Mi c #010410", +"#5C c #010414", +"#K. c #010420", +".SH c #010424", +".ht c #010428", +".fZ c #010429", +"aPV c #010507", +".Kr c #010509", +"#8n c #01050e", +"ayT c #010513", +"#kL c #010518", +"#jl c #01051a", +"#p# c #010523", +".IU c #010609", +"#7i c #010615", +"#mc c #010616", +"awo c #010618", +".Xt c #010622", +"#qx c #010623", +"#rX c #010624", +"aM. c #01070a", +"#rP c #010714", +"#qo c #010715", +"#VG c #010718", +"akQ c #01071d", +"ab6 c #010720", +"aIb c #010723", +".qc c #010731", +"#Rs c #010823", +"aGQ c #010824", +".IV c #01090d", +"#7k c #01091f", +"#Rp c #010929", +"acF c #010b10", +"#vc c #010d1c", +"#5F c #010d25", +".Go c #020000", +"agL c #020002", +"aai c #020003", +"aj6 c #020004", +"#Ls c #02000a", +"#7R c #020015", +"#eW c #020022", +".oS c #020032", +"#95 c #020102", +"aup c #020109", +"#x3 c #020113", +"#Fr c #020124", +".Oh c #02012f", +".oR c #020132", +".Ei c #020200", +".Kn c #020203", +".QC c #020207", +".I5 c #020209", +"ay4 c #020210", +"#PV c #020218", +"acm c #02021b", +"#M3 c #020225", +"#hT c #02022d", +"a#J c #020303", +"asA c #020304", +".KB c #020305", +"aL3 c #020306", +"aeQ c #020307", +"abg c #020309", +".Gz c #02030a", +"#3i c #02030c", +"avV c #02030e", +"#c5 c #020317", +".#Y c #020332", +"as4 c #020404", +"#93 c #020406", +".Jp c #020408", +"aMj c #02040b", +"#8m c #02040d", +"#ZI c #02040f", +".Ju c #020412", +"#wL c #020413", +"#Cs c #02041b", +"#Ly c #020420", +".Re c #020425", +".PJ c #02042a", +".5O c #020430", +"#2v c #020506", +".Kx c #020507", +".KQ c #020509", +".Jq c #02050a", +".SI c #020523", +".Lw c #020525", +"#nR c #020528", +".ew c #020529", +".bq c #02052a", +".Ip c #020534", +".Jr c #020608", +"a#H c #02060c", +"axf c #020613", +".Y2 c #020616", +"#o2 c #020617", +"#nB c #02061a", +"#SS c #020626", +"a#C c #020703", +".SV c #02070c", +"aD7 c #020710", +"#nC c #020713", +"#He c #020716", +"#jx c #020733", +"#o3 c #020810", +"ayR c #020818", +"a.M c #020820", +"#Ro c #020822", +"#hU c #020835", +"aP8 c #02090f", +"adF c #020924", +"#to c #020926", +"#SR c #02092b", +"ahw c #020a2a", +".eJ c #020a4b", +".iX c #020a4c", +"#vb c #020b20", +"#Rq c #020b2f", +"#D5 c #020c2d", +"#A4 c #020c32", +"ajL c #020d2a", +".Gq c #030000", +"azg c #030001", +".HO c #030002", +".H2 c #030003", +".I6 c #030005", +"ayY c #030006", +".F1 c #030010", +".HB c #030013", +".SM c #030037", +"aJP c #030103", +"abb c #030105", +"ami c #030108", +"axq c #030109", +"aAF c #03010b", +"az6 c #03010e", +"ay5 c #030111", +".nt c #03013b", +".QB c #030202", +"a#K c #030203", +"arQ c #030204", +"asF c #030205", +"aj7 c #030207", +"awP c #030208", +"avC c #03020a", +"#wN c #030214", +"#wu c #03021d", +"#c4 c #030221", +"#Lx c #030225", +".km c #030244", +".KF c #030305", +"aN9 c #030306", +"#8p c #030307", +"aeP c #030308", +".I3 c #030309", +".I4 c #03030a", +"#1l c #030312", +".FR c #030316", +"#P1 c #030326", +"#A2 c #030327", +".LS c #030400", +".FY c #030402", +"acL c #030404", +".KD c #030406", +"a.E c #030408", +"ahj c #030409", +".2e c #03040a", +"#43 c #03040c", +"#Yg c #03040f", +"aAb c #030412", +".bn c #030433", +"a#I c #030504", +"aOV c #030508", +"a#F c #03050d", +"avn c #030512", +"ae2 c #03051e", +".Lx c #030523", +".J9 c #030526", +".IA c #03052b", +".MH c #03052c", +".Sy c #030535", +".GY c #030539", +"aOY c #030608", +".Oo c #03060a", +".M2 c #03060b", +"#mb c #03061e", +"#kK c #030621", +"#jk c #030623", +"#hE c #030625", +".#1 c #03062a", +"Qte c #03062b", +".Lj c #03062c", +"aPW c #030709", +"aCJ c #03070a", +".PV c #03070c", +"aNh c #03070d", +"#1. c #03070e", +"aL4 c #03070f", +"aAa c #030714", +"am7 c #030718", +"#eD c #030719", +"#Oz c #03071d", +".Im c #03072d", +".Lk c #030731", +".5N c #030738", +".Op c #03080d", +"#SO c #03081c", +"aa9 c #030900", +"#qp c #03090e", +"#91 c #030912", +"#85 c #03091f", +"ajK c #030922", +".hE c #03093b", +"acD c #030a00", +"#rQ c #030a0d", +"aiD c #030a27", +"aav c #030b28", +"#jy c #030b36", +"aPw c #030c0e", +"ayP c #030c16", +"alW c #030d23", +"aiE c #030f30", +"#tp c #031026", +"aBf c #031114", +"#vd c #03111d", +"adI c #031539", +".xN c #040000", +".ET c #040001", +".KH c #040002", +".Oq c #040003", +".HU c #040004", +".Wj c #040005", +".HJ c #040007", +".HI c #040008", +"ayJ c #04000f", +"#MX c #040014", +".vH c #04001e", +"#jw c #040027", +"#IE c #040028", +".Gu c #040101", +".HP c #040102", +"aAD c #040105", +"aei c #040108", +"acn c #040111", +".Me c #040201", +".Ji c #040202", +".FD c #040203", +"aJN c #040204", +".F8 c #040205", +"ax9 c #040206", +"avU c #040208", +"ai3 c #040209", +"#8l c #04020a", +"avm c #04020b", +"aAE c #04020c", +"axM c #04020f", +".5K c #040212", +".0O c #040225", +".LM c #040304", +"arR c #040305", +"#8q c #040306", +".H7 c #040307", +"aqZ c #040308", +"av5 c #04030b", +"#Ub c #040312", +".Hq c #040314", +"aFs c #040315", +"aaQ c #04031d", +"#Ox c #040326", +".Q7 c #04032a", +".Px c #04032b", +".nw c #04033c", +".LR c #040400", +"acN c #040402", +"ann c #040405", +"anm c #040406", +".KP c #040407", +"aq0 c #040408", +"aBp c #040409", +".Mb c #04040a", +".EV c #04040b", +"aCM c #04040c", +"auN c #04040d", +"aCN c #04040e", +"#Rl c #04040f", +"#SM c #040413", +"aD8 c #040416", +".N2 c #04042b", +".DI c #040432", +".hH c #040447", +".KT c #040500", +"abk c #040505", +".KE c #040507", +"#3S c #040509", +"aBq c #04050a", +"aA# c #04050b", +"atl c #04050c", +"aeR c #04050d", +"#9Z c #04050f", +".Ea c #04051c", +"#tn c #04052a", +".cY c #040534", +".MT c #040535", +".df c #040547", +"abl c #040604", +"abj c #040607", +"aJK c #040608", +"aOX c #040609", +".Vh c #04060b", +"aI. c #04060d", +"#81 c #04060f", +"alS c #040618", +".MP c #040624", +".IB c #040627", +".Lv c #04062c", +".Ht c #040704", +".Hz c #040705", +".IT c #040709", +"aCK c #04070c", +".NA c #040712", +"#wv c #040717", +"aQE c #04071d", +"#Ha c #040722", +".Sx c #040732", +".Kw c #040809", +".IO c #04080d", +"aO# c #04080e", +"awQ c #040817", +".FQ c #040818", +"aiC c #040822", +".Q8 c #040833", +".2y c #04083c", +"abd c #04090b", +"aBo c #04090d", +"aKS c #040911", +".VT c #040933", +".g. c #04093b", +"ayy c #040a0a", +".Ks c #040a0e", +"aFt c #040a27", +".eG c #040a3c", +"ab. c #040b0d", +"aBh c #040b10", +"#IK c #040b21", +"aOd c #040c12", +"ab7 c #040c27", +".gb c #040c4e", +"ahx c #040d2f", +"aqL c #040e3e", +"aCC c #04100d", +"#88 c #041032", +"ajM c #041434", +"adH c #041738", +".EP c #050000", +"#2O c #050001", +".KI c #050002", +"aDl c #050004", +"#pf c #050009", +"#6e c #05000b", +"#yc c #050020", +".87 c #050024", +"amJ c #050106", +"al9 c #050107", +"#zt c #05010f", +"aaR c #050112", +".Ay c #050123", +"#u7 c #05013e", +"ail c #050206", +".J# c #050207", +"afv c #050209", +"#ZG c #05020a", +"#09 c #05020b", +"axN c #050211", +"#2L c #050221", +".Sw c #05022a", +"aN3 c #050304", +".Gw c #050305", +".Mf c #050307", +"ale c #050309", +"awI c #05030a", +"av6 c #05030b", +".3X c #05030c", +"awN c #05030e", +"a#E c #05030f", +"axd c #050310", +"aAc c #050313", +".5J c #050318", +"#qz c #05031d", +".2s c #050323", +".0V c #050326", +".Ed c #050403", +"alc c #050404", +"anl c #050406", +"arf c #050407", +"#6I c #050408", +"#44 c #050409", +".HH c #05040a", +".H9 c #05040b", +".I# c #05040c", +"awl c #05040d", +"awM c #05040e", +"#W8 c #05040f", +"aDB c #050411", +"#Rk c #050413", +"#Z0 c #050418", +".uj c #05041d", +"aaO c #05041f", +".4g c #050422", +".rw c #050426", +"#zz c #05042d", +"#u2 c #050430", +".kn c #050446", +".Ej c #050503", +"aee c #050504", +".O8 c #050506", +".KC c #050507", +"apj c #050508", +"apk c #050509", +"ajw c #05050a", +"aFp c #05050b", +".I2 c #05050c", +"#Rm c #05050d", +"#SN c #05050e", +"#YA c #05050f", +"#Z1 c #050510", +"aGP c #050515", +"#ST c #05051f", +".Fn c #050538", +"Qtx c #050548", +"asB c #050607", +"ay0 c #050609", +"ahi c #05060a", +"ay1 c #05060b", +"aqK c #05060c", +"asa c #05060e", +"#hX c #050624", +"#x6 c #050626", +"#Cv c #050628", +"#o4 c #05062d", +"#rR c #05062e", +"#eL c #05062f", +".Rg c #05063e", +".FV c #050705", +".IR c #050706", +"as6 c #050708", +"aOT c #050709", +"ayZ c #05070b", +"aGO c #05070e", +"aeS c #05070f", +"#zu c #05071f", +"#IG c #050723", +".K. c #050725", +".2v c #05072c", +".at c #050730", +".et c #050736", +".iR c #05073f", +"aA. c #05080c", +"aMk c #05080e", +"aJy c #050814", +"aJz c #050815", +"aPG c #05081e", +"ab5 c #050820", +".PK c #050826", +"#ms c #050827", +"aNi c #05090f", +"aFq c #050911", +"#90 c #050912", +"#Rt c #05091b", +"aOK c #05091f", +".iV c #05093f", +".iW c #050943", +"aD5 c #050a08", +"ayO c #050a13", +"#Hb c #050a20", +"#mr c #050a30", +"#zC c #050a34", +".da c #050a3c", +".g# c #050a3f", +".hF c #050a43", +".Kt c #050b11", +"#hP c #050b2d", +".bC c #050b3d", +"ayB c #050c11", +"#Uc c #050c1e", +"am8 c #050c20", +"#k1 c #050c35", +"#vf c #050d1e", +".a# c #050d4f", +"#5E c #050e22", +"QtH c #050e37", +"aD0 c #050f09", +"#tc c #050f16", +".BM c #050f25", +"#Hd c #050f27", +"aQB c #051112", +".W6 c #05193e", +".A9 c #060000", +".KJ c #060002", +".Nv c #060004", +"aMd c #060005", +".HT c #060008", +"#7S c #060009", +"#bK c #06000e", +"##9 c #06000f", +"#AW c #060010", +".7m c #06001c", +".xt c #060026", +".HV c #060103", +"ax4 c #060104", +"aNb c #060105", +"#8k c #060106", +".lM c #060125", +"#3k c #060205", +"ahh c #060206", +"aqj c #060207", +".HL c #060208", +"ak4 c #060209", +"aj0 c #06020a", +".k. c #060229", +".CE c #060303", +".O7 c #060304", +"#45 c #060305", +"awm c #060307", +".I8 c #060308", +"aef c #06030a", +".Zf c #060320", +".Ze c #060329", +".Wb c #06032d", +".Uo c #06032f", +".Gv c #060404", +"ai2 c #060405", +"aK2 c #060406", +"aPP c #060407", +"atd c #060409", +"aeg c #06040b", +"atA c #06040c", +"awk c #06040d", +"ay# c #06040e", +"a.G c #060410", +"avk c #060411", +"a.H c #060413", +"#Yz c #060418", +"#W7 c #060419", +".4# c #06041a", +".2t c #06041c", +".0P c #06041e", +".sU c #060420", +".oT c #060436", +".lS c #06043d", +".l1 c #060443", +"acP c #060500", +"agO c #060505", +".LP c #060506", +"api c #060507", +"ano c #060508", +"amg c #060509", +"anp c #06050a", +"avl c #06050b", +".I0 c #06050c", +"avB c #06050d", +"aAG c #06050e", +"awL c #060510", +"asM c #060513", +"aCR c #06051a", +".4. c #060523", +".MU c #06052f", +".nu c #06053e", +".nz c #060543", +".l3 c #060544", +".ko c #060547", +".LU c #060601", +"acO c #060603", +"ank c #060607", +"aqY c #060608", +".TE c #060609", +".S. c #06060a", +"aim c #06060b", +"aGM c #06060c", +".Kl c #06060d", +"aqJ c #06060e", +"#VF c #06060f", +"aog c #060610", +"ay2 c #060611", +"#IL c #060612", +"asb c #060614", +"aIa c #060616", +"a#b c #060620", +"#Ka c #060626", +"#Cw c #06062a", +".Ri c #060641", +"acR c #060705", +"abm c #060706", +"abh c #06070b", +"aH8 c #06070c", +"art c #06070e", +"af7 c #06070f", +".S# c #060710", +"aBr c #060713", +"#82 c #060714", +"#hR c #060722", +"#nD c #06072e", +"#qq c #06072f", +".0U c #060738", +".GR c #06073c", +".PO c #06073f", +".hy c #060740", +".f4 c #060741", +".c6 c #060742", +".U. c #060747", +".FU c #060806", +".Ny c #06080a", +"abi c #06080b", +".Nz c #06080f", +"af8 c #060810", +".P. c #060812", +"amY c #06081a", +"amZ c #06081b", +"ajJ c #06081d", +"#mn c #060823", +".IG c #060830", +".hU c #060831", +".PI c #060834", +"#qv c #060836", +".GT c #06083d", +".eA c #060841", +".8X c #060845", +".Js c #06090d", +"aMl c #06090f", +"aMm c #060910", +".KY c #060912", +"ay3 c #060917", +"aM0 c #06091f", +".IC c #060927", +"Qtf c #06092d", +".Oc c #060935", +".PA c #060938", +".Kv c #060a0b", +"aPZ c #060a0c", +"aNT c #060a0d", +"aCL c #060a0f", +"aL5 c #060a1f", +"#M4 c #060a20", +"#ta c #060a26", +"#g. c #060a27", +"#IJ c #060a29", +".br c #060a2d", +".eH c #060a44", +"aGN c #060b11", +"aJA c #060b12", +"#rW c #060b33", +"Qtr c #060b3d", +".db c #060b40", +".Ku c #060c12", +"aKi c #060c14", +"#qw c #060c34", +"aO7 c #060d13", +"aNm c #060d14", +"#ZJ c #060d1e", +"alV c #060d21", +"#PT c #060d2d", +"#7l c #06102c", +"a.N c #06102f", +"aOc c #061117", +"#tu c #06111f", +"#Fw c #06122c", +"aBg c #061719", +".Jf c #070000", +".EC c #070002", +".Eo c #070004", +".El c #070009", +"#1W c #07000c", +"ahK c #070017", +"aD3 c #070100", +".M5 c #070101", +".KK c #070102", +"abp c #070103", +"aq# c #070104", +"aqi c #070106", +"aK8 c #070107", +"aK7 c #070108", +"#Ow c #070127", +".Er c #070200", +".Gs c #070202", +".sF c #070203", +".M4 c #070204", +"aNc c #070206", +"aM# c #070208", +".Wh c #07020a", +".Uu c #07020b", +"aER c #070212", +"#Or c #070213", +"##0 c #070228", +"#mp c #07022a", +".Je c #070303", +".Jd c #070304", +"amf c #070306", +"af6 c #070307", +".HK c #070309", +"#2N c #070311", +"#mt c #070318", +".XG c #070323", +".sJ c #070326", +"#.E c #070327", +"#wH c #07032c", +".PW c #070401", +".LZ c #070402", +".KO c #070405", +"auq c #070406", +".H4 c #070407", +"aec c #070408", +".I7 c #070409", +"ar0 c #07040a", +"ai4 c #07040b", +"aCi c #070410", +"avT c #070411", +"aqH c #070414", +"#6d c #07041a", +"#2M c #07041d", +".Fq c #070433", +".GZ c #070437", +"#y. c #070438", +"#wD c #07043b", +"aJM c #070505", +"aed c #070506", +"aK1 c #070507", +"azn c #070508", +"ax8 c #070509", +"ark c #07050a", +"#3j c #07050b", +"auM c #07050c", +"aun c #07050d", +"auL c #07050e", +"#9Y c #07050f", +"auK c #070510", +"au# c #070511", +"au. c #070512", +"aCQ c #070513", +".HD c #070514", +".4a c #070515", +"#bt c #070518", +"#VE c #070519", +"#Ua c #07051a", +"#VI c #070520", +"#9w c #070521", +"#7Q c #070522", +".XF c #07052d", +"#x9 c #070537", +".kd c #070541", +".Ef c #070605", +"abn c #070606", +".LL c #070608", +"arX c #070609", +".Mg c #07060b", +".XR c #07060c", +".IP c #07060d", +"acH c #07060e", +"azs c #07060f", +"awj c #070611", +"asL c #070612", +"as# c #070613", +"apQ c #070614", +"atk c #070615", +"#pe c #07061e", +"#LA c #070621", +".sV c #070622", +".5I c #070624", +"#II c #07062c", +".LV c #070701", +"abX c #070706", +"awx c #070708", +".H6 c #070709", +"apl c #07070b", +"aky c #07070c", +"apN c #07070e", +"aoc c #07070f", +"#YB c #070710", +"aH9 c #070711", +"aru c #070712", +"arv c #070713", +"ao5 c #070714", +"aoh c #070715", +"#nE c #070732", +".iL c #07073a", +".T9 c #070740", +".bv c #070742", +".Ra c #070743", +"aD4 c #070804", +"acK c #070809", +"#8o c #07080c", +".O9 c #07080e", +"aLn c #07080f", +"af9 c #070812", +"aCP c #070813", +"#nN c #070823", +"#nS c #070824", +"#pd c #070828", +".fW c #070837", +".ho c #07083a", +".Of c #07083d", +".#5 c #070843", +"Qtj c #070844", +".bH c #07084a", +".FZ c #070907", +".IS c #070909", +"as5 c #07090a", +".Kp c #07090b", +"aOI c #070910", +"aeT c #070912", +".EW c #070914", +"#W9 c #070919", +"#D0 c #070922", +"#qy c #070923", +"#rY c #070924", +"#MU c #070926", +"#gg c #070932", +".rB c #070935", +"#o9 c #070937", +".Sz c #07093c", +".Ul c #070946", +".Um c #070948", +".Hy c #070a07", +"aNj c #070a10", +"aPm c #070a12", +".Jt c #070a13", +"am0 c #070a1d", +"#rO c #070a25", +"#P2 c #070a26", +"#qn c #070a27", +".c2 c #070a2d", +".ex c #070a2e", +"#zD c #070a31", +".Iz c #070a35", +".R. c #070a39", +"aOG c #070b0f", +"aO8 c #070b11", +"am1 c #070b1f", +"#Rn c #070b20", +"#Ft c #070b22", +".JY c #070b35", +".2x c #070b3d", +".bD c #070b45", +"aOH c #070c14", +"ayD c #070c15", +"a.L c #070c23", +"#va c #070c28", +"#Oo c #070c2a", +"Qts c #070c41", +"#p. c #070d34", +".iY c #070d4f", +".Dk c #070f22", +"aO6 c #071017", +"ayQ c #07101c", +"#hV c #07103c", +"ayS c #07111e", +"aNl c #071319", +"#89 c #071437", +".P# c #071d45", +".M6 c #080000", +".zu c #080001", +"abq c #080003", +"aps c #080004", +"aFW c #080005", +".SW c #080006", +"ayN c #08000a", +"#Ix c #08000e", +".ml c #080010", +".7e c #08001c", +"#s7 c #080025", +"am# c #080101", +".HS c #08010c", +"#Ot c #08010d", +"#J2 c #08010f", +".9. c #080110", +".ES c #080201", +".A5 c #080202", +"al# c #080203", +"a#M c #080204", +"aEe c #080206", +"aGX c #080207", +"aKR c #080208", +"agy c #08021d", +"#ab c #08021e", +".HX c #080302", +".Jc c #080305", +"azl c #080306", +"aMa c #080309", +"aiS c #08030e", +"#zs c #080315", +".n# c #080321", +".Oi c #080327", +"#.F c #080328", +".L0 c #080402", +"aPO c #080403", +"awO c #080406", +"atI c #080407", +".HM c #080408", +"aeb c #080409", +"aMb c #08040a", +".Hb c #080410", +"aHt c #080415", +".XH c #08041f", +".V7 c #080426", +".lO c #080428", +"#hS c #08042c", +"#u6 c #08043f", +".Km c #080502", +".LY c #080503", +".TD c #080504", +"a#L c #080506", +".HN c #080507", +".HY c #080508", +"aea c #080509", +".J. c #08050a", +"ald c #08050c", +"aBe c #08050e", +".3W c #080512", +"at9 c #080513", +"apM c #080515", +".HA c #08051a", +".V8 c #080521", +"#wI c #080527", +"acQ c #080600", +".R9 c #080605", +"aJO c #080608", +"azm c #080609", +"aPS c #08060a", +"aoR c #08060b", +"afy c #08060c", +"afx c #08060d", +"auo c #08060e", +"avj c #080611", +"avS c #080612", +"aap c #080613", +"apL c #080615", +"#Rj c #08061a", +"#r2 c #080624", +".B8 c #08062e", +".MQ c #080631", +"#u3 c #080636", +".l7 c #080648", +"aa8 c #080700", +".Eg c #080706", +".Nx c #080708", +".LN c #080709", +"aow c #08070b", +"anq c #08070c", +"aEB c #08070d", +"aHj c #08070e", +"atB c #08070f", +"azr c #080710", +"avi c #080711", +"awK c #080712", +"apO c #080713", +"aof c #080714", +"as. c #080715", +"aqI c #080716", +"aBs c #08071d", +".5P c #080724", +"#v# c #08072b", +".2r c #080730", +".0N c #080733", +"#eB c #080735", +".LQ c #080803", +"acS c #080806", +"aw1 c #08080a", +"aO. c #08080b", +"amM c #08080d", +".I1 c #08080e", +"aO0 c #080810", +".Wi c #080811", +"aod c #080813", +"apP c #080816", +"#Kb c #08081b", +"#wK c #08081c", +"#M5 c #08081d", +"#ew c #080831", +"asz c #08090b", +"alG c #08090d", +"avN c #08090f", +"ahk c #080911", +"acj c #080923", +"aaP c #080925", +"acl c #080926", +"#jn c #080934", +".hp c #080938", +".fV c #08093b", +"#dc c #080943", +".PD c #080944", +"aOF c #080a0e", +"aJB c #080a11", +"ain c #080a12", +"aio c #080a13", +"ag. c #080a14", +"#wM c #080a16", +".4b c #080a2d", +".iM c #080a39", +".N6 c #080a3e", +".T4 c #080a41", +"##1 c #080a47", +".Hw c #080b08", +".Hu c #080b09", +"aLo c #080b12", +"aQt c #080b14", +".KZ c #080b18", +"aas c #080b24", +".CA c #080b26", +"#o1 c #080b29", +".f0 c #080b2e", +".hu c #080b2f", +".vU c #080b32", +".SG c #080b36", +".J8 c #080b37", +".KA c #080c0d", +"aKj c #080c13", +"#84 c #080c1e", +"Qtt c #080c46", +"#Hc c #080d30", +".Z# c #080d45", +"#tv c #080e18", +"adE c #080e28", +"#nL c #080e35", +"#SQ c #080f2e", +"aCA c #081011", +"#Om c #081133", +".bR c #08123a", +"agl c #081339", +"aPx c #081717", +"#5G c #081736", +"a#N c #090003", +"#D7 c #090013", +".Eb c #090016", +".Rl c #09001b", +"#VJ c #09001e", +"aLc c #090107", +"aBl c #09010a", +"#zF c #09011e", +"#4x c #090203", +"arU c #090207", +"#9W c #09020d", +"ayH c #090214", +"##R c #09021a", +".CB c #09021d", +"#nP c #090229", +".Gl c #090301", +".Eq c #090302", +"aCG c #090305", +"arV c #090308", +"aK6 c #090309", +"aFn c #09030b", +".Up c #09031e", +"#bL c #09031f", +"adX c #090324", +".bY c #090325", +"#r1 c #090326", +".QA c #090404", +"aAo c #090406", +".Gb c #090407", +".Jb c #090409", +".Ja c #09040a", +".Uw c #09040c", +"#9X c #09040e", +"adY c #09041e", +"#qC c #090428", +".IQ c #090502", +".LW c #090503", +"abo c #090506", +".M3 c #090507", +"aq8 c #090508", +"ae# c #090509", +"afu c #09050a", +".BJ c #09050b", +"#4w c #090511", +"ayU c #090514", +"#PW c #090517", +"#Ln c #09051d", +".Ui c #090523", +".Uh c #090528", +".V6 c #090530", +"#wE c #09053e", +".L1 c #090603", +".LX c #090604", +"acT c #090607", +"#6J c #090608", +".Nw c #09060a", +".Dg c #09060b", +"arl c #09060c", +"afz c #09060d", +"aLl c #09060e", +"aLm c #09060f", +".5u c #090612", +"#SU c #090616", +"#9x c #090619", +".Zg c #09061e", +"#1k c #090621", +"#yb c #09062c", +".Ug c #090631", +".T6 c #090632", +".VW c #090636", +".oH c #09063a", +"acC c #090700", +".Md c #090701", +"aM8 c #090709", +".F7 c #09070a", +"aba c #09070b", +"anO c #09070c", +"aj9 c #09070e", +"atE c #09070f", +"aJx c #090710", +"aEC c #090711", +"awi c #090712", +"avR c #090713", +"auI c #090714", +"ao4 c #090716", +".2u c #09071a", +"#ZZ c #090721", +"a#. c #090722", +".XL c #09072f", +"#Fu c #090733", +".nk c #09073d", +"aw0 c #09080a", +"ay. c #09080b", +"aQA c #09080d", +"#ZH c #090810", +"aDn c #090811", +"aFr c #090813", +"aOJ c #090814", +"aoe c #090816", +"ay6 c #09081f", +"a## c #090824", +"#9v c #090826", +"#J9 c #09082b", +"#x8 c #090833", +"#zA c #090834", +"#y# c #090836", +"axp c #09090b", +"atb c #09090c", +"alb c #09090f", +".GB c #090914", +"aF6 c #090918", +"#OA c #09091b", +"adB c #09091e", +".39 c #090930", +"#qk c #090936", +"#js c #09093a", +".gv c #09093b", +".Fm c #09093c", +".Ko c #090a0b", +"aaj c #090a0c", +"ayv c #090a0d", +"avd c #090a0f", +"auB c #090a10", +"aOa c #090a11", +".TF c #090a12", +"aip c #090a15", +"ahl c #090a16", +"amQ c #090a17", +"#83 c #090a1a", +"ae1 c #090a22", +"ack c #090a26", +"#Cu c #090a29", +".Kd c #090a2d", +".hM c #090a30", +"#mq c #090a33", +"#hH c #090a35", +"#oY c #090a36", +".W# c #090a49", +".gd c #090a4c", +"aCI c #090b0d", +"akz c #090b13", +"amP c #090b16", +"#Ru c #090b18", +".A. c #090b19", +"#nA c #090b2d", +"#nK c #090b39", +".iQ c #090b41", +"#VH c #090c21", +"#Oy c #090c28", +".xH c #090c31", +".Lu c #090c38", +".J0 c #090c3b", +".V0 c #090c52", +"aPX c #090d0f", +".EY c #090d1b", +"#PU c #090d28", +".XA c #090d48", +"aQa c #090e0c", +"aNU c #090e13", +"aPE c #090e15", +"#IH c #090e24", +"aau c #090e29", +"aP9 c #090f14", +"#SP c #090f28", +"#ml c #090f37", +".5D c #090f3e", +".0K c #090f44", +"#vg c #09111f", +"ab8 c #09122f", +"ayx c #091311", +"aLr c #09131a", +".Aa c #091731", +".M. c #0a0000", +"#8j c #0a0003", +"#ye c #0a000c", +"aco c #0a0104", +"aBi c #0a010c", +".zy c #0a0203", +"aiT c #0a0206", +".On c #0a020a", +"ayK c #0a0214", +"#IB c #0a0216", +"aak c #0a030d", +".HR c #0a0310", +"adZ c #0a0313", +".za c #0a0329", +"#kZ c #0a032a", +".0b c #0a0400", +".ER c #0a0402", +".A8 c #0a0404", +"acU c #0a0407", +".F9 c #0a0408", +"as9 c #0a0409", +"aK5 c #0a040a", +".89 c #0a0420", +"afi c #0a0423", +"#ID c #0a042a", +".Jh c #0a0504", +"aN1 c #0a0505", +"ame c #0a0506", +"ahT c #0a0507", +"azi c #0a0508", +"am. c #0a0509", +"aM9 c #0a050a", +"aMc c #0a050b", +".86 c #0a0520", +"#Rf c #0a052b", +".k# c #0a052c", +"#v. c #0a0530", +".ks c #0a0545", +"axe c #0a0605", +".HW c #0a0606", +"ar# c #0a0609", +"agN c #0a060a", +"agM c #0a060b", +"aJw c #0a060f", +"a.F c #0a0612", +".Ar c #0a0628", +"#dl c #0a062a", +"#H. c #0a062e", +".CH c #0a0706", +"#96 c #0a0709", +"ahU c #0a070b", +".I9 c #0a070c", +"agP c #0a070e", +"aao c #0a0713", +"aob c #0a0717", +"a#c c #0a0719", +"#r3 c #0a071a", +".0Q c #0a071c", +"#Yy c #0a0721", +"#W6 c #0a0722", +"#4u c #0a0726", +".sZ c #0a0734", +".H1 c #0a080a", +"aPR c #0a080b", +"aOU c #0a080c", +"anN c #0a080d", +"aN5 c #0a080e", +"ai6 c #0a080f", +"atD c #0a0810", +"aDm c #0a0812", +"atj c #0a0813", +"at5 c #0a0814", +"at6 c #0a0815", +"aoa c #0a0817", +"apK c #0a0818", +"#9u c #0a0824", +"#6c c #0a0825", +".ry c #0a082a", +".Zl c #0a082d", +"#zB c #0a0836", +".G2 c #0a0838", +"#f7 c #0a0849", +".kk c #0a084b", +"adq c #0a0907", +".Ee c #0a0908", +"arS c #0a090b", +".Gx c #0a090c", +"ai1 c #0a090e", +"avh c #0a0910", +"avA c #0a0911", +"avQ c #0a0913", +"auH c #0a0914", +"akL c #0a0919", +"#P3 c #0a091a", +"#MW c #0a0920", +".sT c #0a0925", +"#u0 c #0a0930", +".IF c #0a0933", +"#ex c #0a093a", +".dx c #0a0941", +".H5 c #0a0a0a", +"atT c #0a0a10", +"#Yf c #0a0a11", +"acG c #0a0a12", +"aDD c #0a0a17", +"akD c #0a0a19", +"a.I c #0a0a1b", +"#jA c #0a0a26", +"a#a c #0a0a27", +"#H# c #0a0a2d", +"#nw c #0a0a35", +".Uj c #0a0a3a", +".es c #0a0a3d", +".gw c #0a0a3e", +".XK c #0a0a41", +".VY c #0a0a46", +".LT c #0a0b05", +".FW c #0a0b09", +"asw c #0a0b0c", +"ayw c #0a0b0d", +"azM c #0a0b0f", +"ave c #0a0b10", +"a.D c #0a0b11", +".Dh c #0a0b15", +".0A c #0a0b16", +"ag# c #0a0b17", +"amR c #0a0b18", +"akC c #0a0b1a", +"amX c #0a0b1b", +"alR c #0a0b1d", +"#zw c #0a0b28", +"#zy c #0a0b2f", +".do c #0a0b31", +"#l8 c #0a0b35", +"Qt# c #0a0b3c", +".Zk c #0a0b3f", +".c5 c #0a0b46", +".bu c #0a0b47", +"aOW c #0a0c0e", +"acI c #0a0c13", +"alH c #0a0c14", +"alI c #0a0c15", +"aDy c #0a0c18", +"#uY c #0a0c28", +".MJ c #0a0c40", +".hx c #0a0c43", +".W. c #0a0c48", +"#gh c #0a0c49", +".Py c #0a0d38", +".Ll c #0a0d39", +".kp c #0a0d50", +"aP0 c #0a0e10", +"ayA c #0a0e12", +"aQC c #0a0e16", +".MI c #0a0e38", +".i7 c #0a0e3d", +".hG c #0a0e4b", +".qd c #0a0f3c", +"#Yh c #0a1023", +"aD9 c #0a102f", +"#kX c #0a1038", +".35 c #0a1040", +".2n c #0a1042", +"#PS c #0a1437", +"aQd c #0a171a", +".Os c #0b0000", +".PZ c #0b0001", +".M# c #0b0003", +".Nu c #0b0004", +"aLj c #0b0005", +"#Cn c #0b0013", +"#0B c #0b0107", +"aFo c #0b010c", +".Jg c #0b0200", +"aCF c #0b0203", +".M1 c #0b020a", +"aaS c #0b0306", +".PU c #0b030b", +"aiQ c #0b0314", +"aIS c #0b040e", +"akN c #0b0410", +"#P0 c #0b0417", +".oY c #0b0433", +".kt c #0b0437", +".Df c #0b0505", +".O6 c #0b0508", +"aK4 c #0b050b", +"az9 c #0b050e", +"##S c #0b0515", +"ayG c #0b0517", +".Wc c #0b051f", +"#bO c #0b0521", +".FB c #0b0524", +"#G9 c #0b052a", +"#wG c #0b0534", +"axO c #0b0605", +".Gn c #0b0606", +"ax6 c #0b0609", +"ak5 c #0b060a", +"al8 c #0b060d", +"#MY c #0b0617", +"ahL c #0b0618", +"#Lp c #0b061c", +".yR c #0b0622", +".nf c #0b0624", +".lH c #0b062a", +"#bM c #0b062b", +"#VA c #0b062c", +"#t. c #0b0634", +".Gr c #0b0706", +"aq9 c #0b070a", +"af5 c #0b070b", +"ajv c #0b070c", +"asr c #0b070d", +".F3 c #0b0714", +"#bs c #0b0724", +"#J8 c #0b072f", +".nA c #0b0742", +".ge c #0b0743", +"#ti c #0b0745", +".CI c #0b0808", +"alF c #0b080c", +"a.C c #0b080d", +"aoT c #0b080e", +"aeh c #0b080f", +"aEZ c #0b0812", +"axL c #0b0815", +"alO c #0b0817", +"ao# c #0b0818", +"#LB c #0b081f", +"#VD c #0b0822", +"#U# c #0b0823", +"#k0 c #0b0831", +".HZ c #0b090b", +"#2u c #0b090c", +"ai0 c #0b090d", +"aoS c #0b090e", +"ahV c #0b090f", +"atQ c #0b0910", +"au2 c #0b0911", +"avP c #0b0912", +"auG c #0b0913", +"an1 c #0b0914", +"an0 c #0b0915", +"at7 c #0b0916", +"ao. c #0b0919", +"#uX c #0b092f", +".p8 c #0b0933", +"#ya c #0b0934", +".Zd c #0b0938", +".Uf c #0b093d", +".lU c #0b0948", +"aCH c #0b0a0b", +"abW c #0b0a0c", +".H8 c #0b0a0f", +"avg c #0b0a10", +"auF c #0b0a11", +".HG c #0b0a12", +"anS c #0b0a13", +"anT c #0b0a14", +"anU c #0b0a15", +"aDC c #0b0a17", +"alP c #0b0a19", +"ab3 c #0b0a1e", +".Kb c #0b0a35", +"#kS c #0b0a3b", +"#jr c #0b0a3f", +".kc c #0b0a42", +"#f6 c #0b0a47", +".l4 c #0b0a49", +"anQ c #0b0b0f", +"anR c #0b0b10", +"aj8 c #0b0b11", +"aP1 c #0b0b12", +".GA c #0b0b14", +"aEO c #0b0b19", +"alK c #0b0b1a", +"ajz c #0b0b1b", +"ab4 c #0b0b21", +".Ke c #0b0b22", +".5H c #0b0b31", +"#kN c #0b0b36", +".V9 c #0b0b3a", +"#jo c #0b0b3b", +".Wa c #0b0b43", +".f6 c #0b0b49", +"asy c #0b0c0d", +"azN c #0b0c0e", +"aA0 c #0b0c10", +"amh c #0b0c11", +"awf c #0b0c12", +"aKg c #0b0c13", +"akA c #0b0c16", +"ajx c #0b0c17", +"alJ c #0b0c18", +"ahm c #0b0c19", +"ajy c #0b0c1a", +"#MV c #0b0c26", +"#ma c #0b0c30", +"#jm c #0b0c33", +"#hy c #0b0c34", +"#kG c #0b0c35", +".cX c #0b0c3e", +".Rf c #0b0c42", +".ez c #0b0c46", +".ML c #0b0c47", +".eC c #0b0c48", +".#4 c #0b0c49", +".hA c #0b0c4b", +".Hx c #0b0d0b", +"as3 c #0b0d0e", +"aKh c #0b0d14", +"aQn c #0b0d15", +"aCd c #0b0d18", +"#D1 c #0b0d2a", +".Li c #0b0d3a", +"#mk c #0b0d3b", +".PB c #0b0d40", +".f3 c #0b0d44", +".7a c #0b0d4a", +"aLp c #0b0e15", +".0B c #0b0e16", +"ae3 c #0b0e28", +".iO c #0b0e34", +".hs c #0b0e35", +".JZ c #0b0e3a", +".Io c #0b0e3b", +".ga c #0b0e4b", +"aMn c #0b0f15", +"aat c #0b0f29", +".Ih c #0b0f3b", +".eI c #0b0f4c", +"azO c #0b1012", +"aLs c #0b1017", +"#ww c #0b1019", +".E# c #0b1024", +"#Ue c #0b102c", +"#k2 c #0b1033", +"#Fv c #0b1035", +"#jv c #0b1038", +".uw c #0b1043", +".s7 c #0b1044", +".V2 c #0b104c", +".de c #0b1053", +"aQ# c #0b1110", +"#hO c #0b1139", +"aD1 c #0b120a", +"auO c #0b122a", +"#jz c #0b1237", +"aL6 c #0b183a", +".Qx c #0c0000", +"a#x c #0c0001", +".R7 c #0c0004", +"ad0 c #0c0005", +"aH6 c #0c0009", +"#1V c #0c0011", +"#nO c #0c0026", +"aiU c #0c0100", +".Rt c #0c0106", +"#Kc c #0c010c", +".Dd c #0c0200", +"abZ c #0c020a", +"ayV c #0c0211", +".FS c #0c0214", +"aHw c #0c0215", +"amc c #0c0301", +".PY c #0c0307", +".LJ c #0c030b", +"akM c #0c030d", +"#9V c #0c030e", +"#zr c #0c031c", +"#bF c #0c0320", +".z# c #0c032f", +".YA c #0c0400", +"agI c #0c0403", +".KN c #0c0404", +".XS c #0c040a", +".Rq c #0c040c", +"#Os c #0c0410", +"#MZ c #0c0411", +"#J1 c #0c0413", +".7n c #0c0414", +"#eG c #0c0427", +"adW c #0c042a", +"#uV c #0c042e", +".an c #0c0435", +"aC2 c #0c0507", +"aqf c #0c0509", +"apw c #0c050a", +"aBk c #0c050d", +"aHv c #0c0517", +"#6a c #0c0522", +".B9 c #0c0533", +"a#B c #0c0605", +".B. c #0c0606", +"#6K c #0c0607", +".Ep c #0c0608", +"aAA c #0c0609", +".Ma c #0c060a", +"aBC c #0c060b", +"aK3 c #0c060c", +"aA3 c #0c060e", +"aKf c #0c0610", +"aJv c #0c0611", +".Ca c #0c0629", +".sI c #0c0635", +".Gt c #0c0706", +"agJ c #0c0707", +"ae. c #0c0709", +"azj c #0c070a", +".G. c #0c070b", +"am5 c #0c0710", +"aiR c #0c0715", +"#eX c #0c0723", +".A3 c #0c0726", +"#dm c #0c072c", +"#Yw c #0c072d", +".Gp c #0c0807", +"#8r c #0c080a", +"aNa c #0c080c", +"amI c #0c080d", +"ap5 c #0c080e", +"aBn c #0c080f", +"aIT c #0c0811", +"QtO c #0c081b", +"#Rg c #0c082b", +"#VB c #0c082c", +"#pc c #0c082d", +".0H c #0c082f", +".p2 c #0c083a", +"ax3 c #0c090a", +"aN6 c #0c090d", +"aqp c #0c090f", +"afw c #0c0910", +"aO1 c #0c0912", +".FC c #0c0916", +"an9 c #0c0919", +"#4v c #0c0921", +"#Ri c #0c0923", +"#7P c #0c0927", +"#ZY c #0c0928", +"ax1 c #0c0a0b", +"aIl c #0c0a0c", +"aPQ c #0c0a0d", +"ala c #0c0a0e", +".F6 c #0c0a0f", +"ai5 c #0c0a11", +"aPF c #0c0a12", +"aoZ c #0c0a13", +"ao0 c #0c0a14", +"anZ c #0c0a15", +"apI c #0c0a16", +"at4 c #0c0a17", +"ao2 c #0c0a19", +"an8 c #0c0a1a", +"ahv c #0c0a1f", +"#zE c #0c0a2d", +".Pw c #0c0a39", +".oU c #0c0a3c", +".V5 c #0c0a3d", +".nj c #0c0a40", +".LK c #0c0b0d", +"aoV c #0c0b0f", +"aoW c #0c0b10", +"aoX c #0c0b11", +"aoY c #0c0b12", +"apH c #0c0b13", +"azG c #0c0b15", +"aqD c #0c0b16", +"aNV c #0c0b17", +"aaq c #0c0b19", +"aF7 c #0c0b1a", +"#wJ c #0c0b26", +"#qD c #0c0b2a", +".rv c #0c0b2d", +".N1 c #0c0b39", +"#nx c #0c0b3a", +".XE c #0c0b3c", +"#ny c #0c0b3e", +"#l9 c #0c0b3f", +"aN7 c #0c0c0f", +"auE c #0c0c10", +"avO c #0c0c11", +"atS c #0c0c12", +"aGg c #0c0c14", +"ayE c #0c0c17", +"aga c #0c0c1a", +"amS c #0c0c1b", +"#zx c #0c0c2d", +"#D3 c #0c0c2e", +"#hz c #0c0c39", +".MG c #0c0c3a", +"#kT c #0c0c3b", +"#hI c #0c0c3c", +"#kH c #0c0c3f", +".N8 c #0c0c48", +".Iu c #0c0c4c", +"aA1 c #0c0d10", +"azP c #0c0d11", +"avf c #0c0d12", +"auC c #0c0d13", +"aEY c #0c0d14", +"amO c #0c0d16", +"akB c #0c0d18", +"aeU c #0c0d19", +"aeV c #0c0d1b", +"aiq c #0c0d1c", +"agj c #0c0d24", +"#hG c #0c0d34", +"#kM c #0c0d35", +".MB c #0c0d3a", +"#f5 c #0c0d40", +".PM c #0c0d43", +".Lm c #0c0d45", +".Ln c #0c0d49", +"Qti c #0c0d4b", +".kq c #0c0d52", +"amN c #0c0e16", +"aCc c #0c0e19", +"#kW c #0c0e3c", +"Qtm c #0c0e42", +".SJ c #0c0e47", +".SK c #0c0e4b", +".VZ c #0c0e50", +"aOZ c #0c0f11", +".fY c #0c0f35", +".2w c #0c0f3a", +".N4 c #0c0f3b", +".dq c #0c0f3e", +".dc c #0c0f4c", +"aPU c #0c1011", +"#DX c #0c101d", +"a.K c #0c1026", +".go c #0c103f", +".bE c #0c104d", +".Ub c #0c105d", +".5E c #0c1141", +".rK c #0c1147", +".oV c #0c1244", +"ab9 c #0c1633", +"aaw c #0c1635", +"a.P c #0c183a", +"ahy c #0c183d", +".L8 c #0d0000", +".Nt c #0d0003", +".Qy c #0d0004", +"azX c #0d000c", +".FK c #0d0011", +"aJJ c #0d0100", +"afk c #0d0107", +"aze c #0d0201", +".xQ c #0d0204", +"alT c #0d0208", +"abY c #0d020b", +".M7 c #0d0300", +".O4 c #0d0302", +"az7 c #0d0310", +"acB c #0d0400", +"al. c #0d0403", +".2H c #0d0406", +".EA c #0d0407", +".O5 c #0d0408", +"ahN c #0d0409", +"aph c #0d040a", +".Rs c #0d040b", +".ST c #0d040c", +"aIR c #0d040e", +".H# c #0d0434", +"#6L c #0d0504", +"#97 c #0d0506", +"aK9 c #0d050b", +".Kj c #0d050d", +"ayX c #0d050f", +".Gf c #0d0510", +"ajY c #0d0511", +"#dn c #0d0515", +"#hY c #0d0519", +"aAB c #0d060a", +"aqh c #0d060b", +".En c #0d060d", +".HQ c #0d0613", +"#PX c #0d0614", +"#bP c #0d0615", +"ayL c #0d0618", +".MW c #0d0619", +".R8 c #0d0707", +".5R c #0d0709", +"aAm c #0d070b", +"arc c #0d070c", +"akG c #0d0714", +"#bE c #0d0732", +"#wF c #0d0738", +"aIj c #0d0808", +"ajE c #0d0817", +"agz c #0d081e", +"afj c #0d0820", +".lN c #0d082c", +"#bN c #0d082d", +".j9 c #0d082f", +".VQ c #0d083c", +"#tj c #0d0847", +"apz c #0d090d", +"aq1 c #0d090e", +"ak3 c #0d0911", +"aiy c #0d0919", +"#Yx c #0d0928", +"#VC c #0d0929", +"#ZX c #0d092c", +".rn c #0d0931", +".VU c #0d0932", +".Sv c #0d0938", +".Sr c #0d0939", +".Fo c #0d093b", +".oX c #0d0940", +".CD c #0d0a0a", +"atG c #0d0a0c", +"aO9 c #0d0a0d", +"are c #0d0a0e", +"azE c #0d0a14", +"alN c #0d0a17", +"an3 c #0d0a1a", +"#Rh c #0d0a29", +"#c0 c #0d0a35", +".Q6 c #0d0a38", +".Q1 c #0d0a39", +"#wC c #0d0a3e", +"#u5 c #0d0a42", +"aN4 c #0d0b0d", +".EU c #0d0b0f", +"aoU c #0d0b10", +"aqw c #0d0b13", +"aqx c #0d0b14", +"aqy c #0d0b15", +".Kf c #0d0b16", +"arp c #0d0b17", +"at3 c #0d0b18", +"amU c #0d0b19", +"ao3 c #0d0b1a", +"an7 c #0d0b1b", +"#A6 c #0d0b2c", +"#nQ c #0d0b32", +".q# c #0d0b34", +".uo c #0d0b36", +".Pr c #0d0b39", +".G1 c #0d0b3c", +".oQ c #0d0b3d", +".nh c #0d0b40", +".lZ c #0d0b4a", +".Hc c #0d0c0a", +".LO c #0d0c0e", +"aqt c #0d0c10", +"aqu c #0d0c11", +"aqv c #0d0c12", +"arm c #0d0c13", +"arn c #0d0c14", +"aro c #0d0c15", +"awh c #0d0c16", +"ao1 c #0d0c17", +"aGf c #0d0c18", +"aEP c #0d0c1a", +"akK c #0d0c1c", +"#Oq c #0d0c21", +"#Cz c #0d0c2a", +"#A3 c #0d0c34", +"#u1 c #0d0c35", +"#eK c #0d0c3c", +"#ql c #0d0c3e", +"#hA c #0d0c3f", +"#kR c #0d0c40", +".kl c #0d0c4e", +"aOB c #0d0d07", +"atU c #0d0d13", +"aEX c #0d0d17", +"aGe c #0d0d1a", +"aeW c #0d0d1c", +"#wy c #0d0d31", +"#rT c #0d0d3c", +"#kO c #0d0d3d", +"#mg c #0d0d3e", +".bm c #0d0d40", +".SL c #0d0d4b", +".Lo c #0d0d4c", +".iT c #0d0d4d", +"aCh c #0d0e12", +"atW c #0d0e13", +"auD c #0d0e14", +"aIU c #0d0e15", +"aDH c #0d0e16", +"aDz c #0d0e1a", +"agb c #0d0e1c", +"air c #0d0e1d", +"akP c #0d0e1f", +".zo c #0d0e30", +"#jj c #0d0e36", +"#rN c #0d0e37", +"#hD c #0d0e38", +".qe c #0d0e40", +".c8 c #0d0e47", +".ab c #0d0e50", +"aFm c #0d0f0b", +"as8 c #0d0f10", +"aNk c #0d0f16", +"aQk c #0d0f17", +"aEN c #0d0f1c", +"adC c #0d0f26", +"#zv c #0d0f29", +"#A0 c #0d0f2a", +".eT c #0d0f35", +".ev c #0d0f36", +"#f4 c #0d0f3b", +".ux c #0d0f3d", +".2q c #0d0f3e", +".#7 c #0d0f44", +".XJ c #0d0f4c", +".l6 c #0d0f51", +"a.J c #0d1023", +"aiB c #0d1028", +".s9 c #0d103f", +".PH c #0d1042", +".0S c #0d1045", +".a. c #0d104d", +"Qtu c #0d104e", +"aQb c #0d110d", +"aLq c #0d1117", +"#DY c #0d1121", +".4d c #0d1141", +".qn c #0d114a", +"#Lz c #0d1228", +"adD c #0d122a", +".A2 c #0d1232", +".36 c #0d1243", +".bG c #0d1255", +"aQ. c #0d1315", +"Qtw c #0d1355", +"aMq c #0d141a", +"Qtv c #0d1657", +".dp c #0d173f", +".Hs c #0e0000", +"aDL c #0e0008", +"azW c #0e000e", +".L7 c #0e0100", +"#nU c #0e0105", +"#0A c #0e010a", +"aC1 c #0e0202", +".KL c #0e0301", +"au1 c #0e0305", +"aGK c #0e030d", +"amb c #0e0401", +".KM c #0e0403", +"a#w c #0e0404", +".xO c #0e0406", +"aGY c #0e0408", +"az8 c #0e040f", +"aiP c #0e0415", +"#pb c #0e042a", +"#8t c #0e0506", +".Qz c #0e0508", +"apv c #0e0509", +"acV c #0e050a", +"#eZ c #0e050c", +"#Z2 c #0e0516", +"#mo c #0e052c", +".Rk c #0e0531", +".SN c #0e0532", +".CQ c #0e0600", +"#46 c #0e0604", +".Ex c #0e0606", +".zx c #0e0607", +"arb c #0e060c", +"#qE c #0e060d", +".IM c #0e060e", +"#gs c #0e0616", +"afh c #0e0629", +".TC c #0e0707", +"aAz c #0e070b", +"aal c #0e0712", +".Wd c #0e0716", +"#yd c #0e0720", +".PP c #0e072f", +".qg c #0e0734", +".W5 c #0e0805", +"afs c #0e0808", +"#8s c #0e0809", +".Or c #0e080b", +"asD c #0e080d", +"ahM c #0e0814", +"ahq c #0e0819", +"#gj c #0e0833", +".t9 c #0e0836", +"#rU c #0e083f", +".Vg c #0e090a", +"aBV c #0e090e", +"#2t c #0e0913", +"ajZ c #0e0915", +"ajD c #0e0917", +".hT c #0e0922", +"#4t c #0e0928", +".b1 c #0e0935", +"#eJ c #0e0942", +"aq6 c #0e0a0d", +"amL c #0e0a0e", +"aP2 c #0e0a13", +".5Q c #0e0a17", +"#6b c #0e0a28", +"#th c #0e0a44", +".kg c #0e0a50", +".kh c #0e0a52", +"ar. c #0e0b0d", +"alE c #0e0b0f", +"arj c #0e0b11", +"ahW c #0e0b12", +"aLt c #0e0b13", +"auJ c #0e0b18", +"an4 c #0e0b1b", +"adA c #0e0b1c", +"agf c #0e0b1f", +"ahu c #0e0b20", +".oI c #0e0b3c", +".Rj c #0e0b40", +".nn c #0e0b42", +".YB c #0e0c08", +"apG c #0e0c11", +"awe c #0e0c12", +"atP c #0e0c13", +"ar5 c #0e0c14", +"ar6 c #0e0c15", +"ar7 c #0e0c16", +"anY c #0e0c17", +".HE c #0e0c18", +"at8 c #0e0c19", +"an2 c #0e0c1b", +"apJ c #0e0c1c", +".uk c #0e0c25", +".oM c #0e0c36", +".T8 c #0e0c40", +".ns c #0e0c46", +"ar2 c #0e0d11", +"ar3 c #0e0d12", +"ar4 c #0e0d13", +"asJ c #0e0d14", +"asK c #0e0d15", +"aBX c #0e0d16", +"atg c #0e0d17", +"anX c #0e0d18", +"aEW c #0e0d19", +"aDE c #0e0d1b", +"amV c #0e0d1c", +"amW c #0e0d1d", +"#X. c #0e0d21", +"agi c #0e0d24", +".ui c #0e0d25", +".LD c #0e0d27", +".sS c #0e0d29", +"#te c #0e0d3b", +"#md c #0e0d3d", +"#nG c #0e0d3e", +"#oZ c #0e0d40", +"#hL c #0e0d41", +".lQ c #0e0d42", +".e3 c #0e0d46", +".Eh c #0e0e0d", +"atf c #0e0e12", +"atX c #0e0e13", +"atV c #0e0e14", +"aCg c #0e0e15", +"azK c #0e0e16", +"aAZ c #0e0e17", +"aAY c #0e0e18", +"aCf c #0e0e19", +"aDG c #0e0e1a", +"agc c #0e0e1e", +"#D6 c #0e0e2b", +"#IF c #0e0e31", +"#qm c #0e0e38", +"#nH c #0e0e3c", +"#mh c #0e0e3d", +".#X c #0e0e41", +".Un c #0e0e48", +".SC c #0e0e4f", +"awg c #0e0f15", +"ayt c #0e0f16", +"aAV c #0e0f19", +"alQ c #0e0f20", +".38 c #0e0f3c", +"Qt. c #0e0f41", +".GX c #0e0f44", +".by c #0e0f46", +".MK c #0e0f47", +"#eN c #0e0f49", +".Rh c #0e0f4b", +".iZ c #0e0f51", +"azR c #0e1016", +".c1 c #0e1037", +".5G c #0e103c", +"#hN c #0e103e", +".XI c #0e104a", +"aIW c #0e1119", +".bp c #0e1138", +".Q9 c #0e113d", +".Pz c #0e113e", +".Ob c #0e1143", +".Iy c #0e1144", +".0T c #0e1148", +".l5 c #0e1151", +"aMo c #0e1219", +".N3 c #0e123d", +".Za c #0e124a", +".XB c #0e124d", +".o5 c #0e124e", +".Uc c #0e1250", +".2o c #0e1345", +".0L c #0e1348", +"#Ud c #0e142b", +"awp c #0e1a37", +".FT c #0f0000", +".Ru c #0f0001", +"a#A c #0f0002", +"#Cm c #0f0017", +"#rZ c #0f0022", +"au0 c #0f0101", +"#8i c #0f0102", +".L6 c #0f0200", +"#gm c #0f0201", +"acA c #0f0300", +"axZ c #0f0303", +"aCE c #0f0305", +"aHC c #0f0308", +"adr c #0f030b", +"ayM c #0f030d", +"ayW c #0f0310", +"#r0 c #0f0327", +"aj4 c #0f0401", +".CL c #0f0402", +".CK c #0f0404", +"atz c #0f0406", +".01 c #0f0407", +"ap4 c #0f0408", +"agB c #0f0409", +".Hr c #0f0414", +"agw c #0f0423", +"#qB c #0f0429", +".f# c #0f050b", +"ab0 c #0f050d", +"aPv c #0f050e", +".EQ c #0f0600", +"aj1 c #0f0605", +".zv c #0f0608", +"ady c #0f0609", +"aMe c #0f060c", +"#PY c #0f0612", +"#Fk c #0f0613", +"#M6 c #0f061a", +".Oj c #0f061e", +"#6# c #0f0620", +"#eP c #0f0623", +".AB c #0f062a", +".DV c #0f0635", +"ak6 c #0f0707", +".Ey c #0f0709", +"al7 c #0f070d", +"aBm c #0f0710", +"aE0 c #0f0711", +"#4s c #0f0724", +"agx c #0f0725", +"#bp c #0f073e", +".02 c #0f080b", +".Gi c #0f080d", +".PX c #0f080e", +".LF c #0f080f", +"aKe c #0f0812", +"#SV c #0f0813", +".XN c #0f0815", +".Ho c #0f0817", +"ayI c #0f081a", +".A6 c #0f0909", +".A7 c #0f090a", +"aq. c #0f090c", +".0X c #0f0910", +"azD c #0f0914", +"akF c #0f0915", +"#Lr c #0f091b", +"#7O c #0f0922", +"#a. c #0f0925", +"#Uf c #0f0927", +"#2K c #0f0928", +"#J7 c #0f092f", +".s0 c #0f0933", +"#qt c #0f0940", +".Gm c #0f0a06", +"ax5 c #0f0a0d", +".G# c #0f0a0e", +"aNd c #0f0a0f", +"aQj c #0f0a15", +"akH c #0f0a18", +"aHu c #0f0a1b", +".xv c #0f0a22", +"#a# c #0f0a2e", +"#tm c #0f0a36", +"#o8 c #0f0a40", +".l8 c #0f0a48", +"aAC c #0f0b0f", +"arO c #0f0b10", +"arP c #0f0b11", +"aO2 c #0f0b14", +"aan c #0f0b16", +".4h c #0f0b19", +".2B c #0f0b1a", +"aiz c #0f0b1c", +".zr c #0f0b2f", +".VV c #0f0b36", +"amH c #0f0c10", +"aqs c #0f0c12", +"auA c #0f0c13", +".F4 c #0f0c17", +"alM c #0f0c19", +"amT c #0f0c1a", +"ajF c #0f0c1b", +"an6 c #0f0c1c", +"#rV c #0f0c40", +"#tg c #0f0c42", +"ax7 c #0f0d11", +"anP c #0f0d12", +"axc c #0f0d17", +"anW c #0f0d18", +"anV c #0f0d19", +"aqG c #0f0d1c", +"an5 c #0f0d1d", +".7c c #0f0d42", +".nl c #0f0d43", +".nq c #0f0d47", +".ki c #0f0d50", +"aN8 c #0f0e11", +"atc c #0f0e12", +"atY c #0f0e14", +"atZ c #0f0e15", +"at0 c #0f0e16", +"at1 c #0f0e17", +"aAT c #0f0e18", +"ati c #0f0e19", +"aCe c #0f0e1a", +"aDF c #0f0e1b", +"aGb c #0f0e1d", +"aHA c #0f0e1e", +"#nF c #0f0e42", +"#m. c #0f0e43", +".Ue c #0f0e49", +".l0 c #0f0e4d", +"awJ c #0f0f15", +"azL c #0f0f16", +"azJ c #0f0f18", +"aEV c #0f0f1c", +"ahn c #0f0f1e", +"#o0 c #0f0f3a", +".DF c #0f0f3d", +".MS c #0f0f40", +".f5 c #0f0f4c", +".MM c #0f0f4e", +"aPc c #0f1014", +"axa c #0f1015", +"ayu c #0f1016", +"aAW c #0f101a", +".BK c #0f101c", +"agk c #0f1028", +"#Ct c #0f102b", +".gm c #0f1036", +"Qtb c #0f103d", +".0M c #0f1042", +".8Y c #0f104a", +".eB c #0f104b", +".hz c #0f104d", +"az1 c #0f1119", +".hr c #0f113b", +".#Z c #0f113d", +".0R c #0f113e", +".rM c #0f1142", +"#eM c #0f114f", +"ayz c #0f1214", +"#MT c #0f1230", +".#0 c #0f1238", +"Qtd c #0f1239", +".c0 c #0f123b", +".5F c #0f1241", +".37 c #0f1243", +".SF c #0f1245", +".V3 c #0f124f", +"#K# c #0f132a", +".nH c #0f1351", +".V1 c #0f135b", +".f9 c #0f1449", +"aPb c #0f1719", +"ayC c #0f171e", +"am9 c #0f192e", +"aMp c #0f1b21", +".N# c #100000", +".Wk c #100001", +"a#y c #100002", +"aGL c #10000e", +"#P4 c #100010", +"#Ye c #100100", +"azV c #100110", +"#gn c #100200", +".L5 c #100201", +"avz c #100202", +".Ec c #100205", +"aQr c #10020f", +"#OB c #100214", +"#DU c #100217", +".SO c #100224", +"aM7 c #100300", +"aot c #100306", +".CC c #10030b", +"#CA c #10031c", +"#2s c #100402", +".Zs c #100409", +"aJu c #10040c", +"#mu c #10040d", +"ajX c #100410", +"#gl c #100411", +"ahI c #10041d", +".xP c #100507", +"aIQ c #100511", +".A4 c #100512", +"#LC c #100516", +"#zq c #100520", +".CN c #100600", +"#JZ c #100617", +"ahJ c #10061e", +"#X# c #100620", +"afg c #10062a", +".i0 c #100635", +".EN c #100702", +".De c #100704", +"apu c #10070b", +"aMi c #10070d", +"aA2 c #100711", +"#k4 c #100715", +"aG. c #100719", +"#jB c #10071a", +"#.z c #100723", +"#3l c #100806", +"amd c #100807", +"#.H c #10080f", +"aOA c #100907", +"aP. c #10090b", +"aDZ c #10090d", +"a#d c #10090e", +".XQ c #100910", +"aPq c #100915", +"#.G c #100918", +"#s9 c #100937", +"#u9 c #100939", +"aMZ c #100a0a", +".B# c #100a0b", +".4i c #100a0d", +".2C c #100a0e", +"aix c #100a1a", +".vI c #100a25", +".yY c #100a26", +".Y5 c #100a29", +"#Fp c #100a30", +"##3 c #100a35", +"#nI c #100a41", +".Gc c #100b0e", +"aN. c #100b10", +"#08 c #100b18", +"aES c #100b1a", +"aF8 c #100b1c", +".bI c #100b47", +"agK c #100c0e", +"alD c #100c10", +"aPr c #100c14", +"alL c #100c1a", +"ais c #100c1b", +"#9t c #100c26", +".88 c #100c30", +"#M2 c #100c34", +".oF c #100c43", +".ac c #100c48", +".IJ c #100d0e", +"au6 c #100d0f", +"aik c #100d11", +"avc c #100d14", +"aOb c #100d15", +"#YC c #100d1d", +".MV c #100d2c", +"#bB c #100d35", +".Lz c #100d38", +"#qu c #100d40", +".kC c #100d47", +"anM c #100e13", +"ax# c #100e15", +"ar9 c #100e19", +"aqC c #100e1a", +"aqF c #100e1d", +"aqE c #100e1e", +".xK c #100e35", +".B7 c #100e36", +".Y8 c #100e3f", +"#rS c #100e44", +".mg c #100e46", +".kr c #100e53", +"aJQ c #100f11", +"asG c #100f13", +"axb c #100f14", +"azt c #100f18", +"axK c #100f19", +"ar8 c #100f1a", +"aEU c #100f1d", +"#x7 c #100f36", +"#td c #100f38", +".DH c #100f3d", +"#kI c #100f44", +"#jh c #100f45", +".MD c #100f4a", +"axE c #101016", +"ays c #101018", +"aPl c #101019", +"aAX c #10101b", +"aGd c #10101e", +"#db c #101038", +"#nz c #10103d", +"#o7 c #10103e", +"#qs c #10103f", +"#o6 c #101041", +".bZ c #101042", +".PL c #101043", +".Le c #10104a", +".iS c #10104e", +"aCb c #10111c", +".Kc c #10113b", +"Qtl c #101148", +"Qtk c #10114a", +".bw c #10114b", +"#Op c #10122c", +"Qtc c #10123b", +".bo c #10123e", +".qp c #101246", +".c7 c #10124b", +".Ud c #10124f", +".Xz c #101253", +"aPn c #10131b", +".bS c #101342", +".J7 c #101345", +".2p c #101346", +".SD c #101353", +".md c #101354", +".hC c #101451", +".d# c #10154a", +".Cz c #10172f", +".rz c #101839", +"asd c #101f42", +".Na c #110000", +"a#z c #110002", +".Ot c #110200", +"#pa c #110227", +"aa7 c #110300", +".5V c #110302", +".L4 c #110303", +"aA5 c #11030b", +"#jC c #11030f", +"#zG c #110319", +"ahO c #110402", +"aL2 c #110404", +".TB c #110407", +".gE c #110409", +"aLk c #11040b", +"aGi c #11040d", +"#G3 c #110410", +"azT c #110413", +"#.l c #110428", +".O3 c #110500", +"aPy c #11050c", +"#yf c #11050f", +"adu c #11060c", +"#2I c #110720", +"#u8 c #11073b", +"#ad c #11080f", +"#gt c #110810", +".zs c #11081a", +"#4r c #110821", +".80 c #110825", +"adV c #11082f", +".EF c #110906", +"ani c #11090f", +"#PZ c #110916", +".ku c #110929", +"#bq c #11093f", +".Ew c #110a09", +"arT c #110a0f", +"apx c #110a10", +"aIX c #110a11", +".Em c #110a13", +".Zn c #110a15", +"#eY c #110a1a", +"#Ou c #110a1d", +"#Ov c #110a28", +".Ha c #110a29", +".Ba c #110b0c", +"aam c #110b16", +"aET c #110b1b", +"ayF c #110b1c", +".rD c #110b36", +"aNg c #110c11", +"ahr c #110c1e", +"#eV c #110c31", +"#mi c #110c42", +".Q3 c #110c48", +".eL c #110c49", +"aft c #110d10", +"akx c #110d11", +"amK c #110d12", +"apm c #110d13", +"aOs c #110d17", +".pS c #110d1b", +".LE c #110d1c", +".DX c #110d1d", +"age c #110d20", +".ox c #110d23", +".Ps c #110d49", +".CF c #110e0d", +".CG c #110e0e", +"atK c #110e10", +".H3 c #110e11", +"aqr c #110e14", +"akI c #110e1c", +"aeX c #110e23", +".DN c #110e32", +".G0 c #110e41", +"#u4 c #110e42", +".no c #110e45", +".NX c #110e49", +"anL c #110f14", +"axA c #110f16", +"axJ c #110f18", +"ars c #110f1a", +"at2 c #110f1b", +"akJ c #110f1e", +"agg c #110f24", +"aeY c #110f25", +".B2 c #110f37", +".vX c #110f38", +".o8 c #110f43", +"#.x c #110f44", +"#hB c #110f46", +"av8 c #111018", +"ayb c #111019", +"aqB c #11101b", +"aGc c #11101f", +".Od c #11103f", +".ka c #111043", +"#jp c #111044", +"#jq c #111045", +"#o5 c #111046", +".np c #111049", +".dy c #11104a", +"axF c #111117", +"aI# c #11111a", +"aC. c #11111c", +".kI c #111132", +"#m# c #111140", +".Fi c #111144", +".f7 c #11114f", +"azH c #11121b", +"aC# c #11121d", +".Zc c #111247", +".#6 c #111249", +".bx c #11124a", +".PN c #11124b", +".Y1 c #111327", +".cZ c #11133f", +".iP c #111346", +".Zi c #11134b", +".kA c #111355", +".Lt c #111446", +".f8 c #111451", +".Lq c #111454", +".cb c #111539", +".In c #11153f", +".eE c #111552", +".MO c #111555", +".#9 c #11164b", +"Qtq c #11164c", +".aa c #111659", +".zq c #11183c", +".bF c #11195b", +".vW c #111a44", +"adJ c #112650", +".NB c #112752", +"#41 c #120000", +".Ns c #120105", +"aKb c #12020a", +"#Z3 c #120217", +".Bc c #120300", +".Dc c #120400", +".L3 c #120405", +".L2 c #120406", +".eX c #120423", +".r2 c #120500", +"#42 c #120505", +"aHy c #120519", +"aOS c #120602", +"ak9 c #120806", +"#6H c #120808", +"adv c #12080c", +".dL c #12081c", +".CS c #120900", +"aOk c #120907", +".ED c #12090b", +".Zr c #12090e", +"#go c #120910", +"aKd c #120913", +"aQe c #120915", +".8W c #12092d", +".AL c #120936", +"Qtz c #120937", +"#J6 c #120a28", +".C. c #120a37", +".Ev c #120b09", +"aEA c #120b10", +".Gg c #120b14", +"aP3 c #120b15", +"akE c #120b17", +"ajC c #120b18", +"agA c #120b19", +"aiw c #120b1a", +"#IC c #120b28", +"ab2 c #120c13", +"aO3 c #120c15", +".up c #120c36", +"#.y c #120c37", +"#kU c #120c43", +".Gd c #120d0f", +".jb c #120d20", +".XM c #120d24", +".Y6 c #120d32", +"aiZ c #120e11", +"as1 c #120e14", +"aPf c #120e17", +"aQh c #120e19", +".0W c #120e20", +"#Lw c #120e36", +"#c1 c #120e44", +".lW c #120e50", +"aq7 c #120f11", +"ate c #120f15", +"amj c #120f16", +"aAS c #120f1a", +"aht c #120f23", +".T7 c #120f3e", +"#nJ c #120f43", +".ni c #120f45", +"#ey c #120f4a", +"azo c #121013", +"aPT c #121014", +"aoQ c #121015", +"axI c #121018", +"arr c #12101b", +"aqA c #12101c", +"agh c #121026", +".qb c #121039", +".qq c #121041", +"#dd c #121044", +"axH c #121117", +"av7 c #121119", +"aFX c #12111a", +"ath c #12111c", +"aar c #12111f", +"aHB c #121120", +".A0 c #12112f", +".rN c #121140", +"#qr c #121143", +"#kP c #121145", +"#me c #121146", +"#kQ c #121147", +"azI c #12121b", +"ae0 c #121229", +".Og c #121247", +".XD c #121249", +".e2 c #12124a", +"##2 c #12124d", +".bs c #121250", +".hB c #121252", +".o7 c #12134a", +".N7 c #12134b", +".eD c #12134e", +".c3 c #12134f", +".V4 c #121350", +".eu c #121440", +".Zh c #121444", +"Qtn c #121447", +".Iq c #121448", +".Zj c #12144e", +".jc c #121537", +".oW c #12154a", +".d. c #121552", +".Rc c #121555", +"aQc c #121611", +".eV c #121645", +".bA c #121653", +".J5 c #121656", +"aQm c #12171e", +"aIV c #12171f", +".xJ c #121a42", +".A1 c #121b37", +".gn c #121b44", +".zp c #121c3e", +".xI c #121e42", +".vV c #121f45", +"agm c #121f46", +".YC c #122549", +".Ow c #130000", +"aQy c #13000b", +"aH7 c #13000f", +"arJ c #130100", +"#DS c #130114", +".Vf c #130200", +"aIH c #13020b", +"#Fy c #13030f", +"QtK c #130424", +"azU c #130514", +"#AU c #13051d", +".pk c #130600", +"aqX c #130608", +"acz c #130700", +"aPN c #130702", +"aP6 c #130712", +".PQ c #130723", +".xS c #13080a", +".CU c #130903", +".gD c #130910", +".Az c #130936", +".eM c #130938", +".CR c #130a00", +"am4 c #130a0f", +".7o c #130a11", +".9# c #130a12", +".MX c #130a16", +".rF c #130a1f", +"#2J c #130a26", +".s1 c #130a2c", +".gy c #130a2f", +".gf c #130a38", +"afr c #130b09", +"#bQ c #130b12", +"aOq c #130b16", +"ajB c #130b18", +"aiv c #130b19", +".Uq c #130b1b", +"#df c #130b27", +"#G8 c #130b29", +".gp c #130b3b", +".Gj c #130c0e", +"aoJ c #130c12", +"aPs c #130c15", +"aO4 c #130c16", +"aQs c #130c18", +"#Lt c #130c1a", +"ahp c #130c1c", +"#Fo c #130c2a", +"#ge c #130c4f", +"asC c #130d12", +"aDk c #130d13", +".FP c #130d1d", +".zb c #130d29", +"#jt c #130d44", +".Ge c #130e10", +"aN# c #130e13", +"aQo c #130e16", +"#wt c #130e32", +".p1 c #130e44", +".dg c #130e4a", +"atH c #130f12", +"aeO c #130f13", +"ayp c #130f19", +"aiA c #130f21", +"#x2 c #130f29", +"#gr c #130f33", +".j6 c #130f36", +"ax2 c #131011", +"atF c #131012", +"ajG c #131020", +".rC c #13103f", +".oG c #131045", +"#eA c #13104c", +".lV c #131051", +"apF c #131116", +"atR c #131117", +"axD c #131118", +"atC c #131119", +"aya c #13111b", +"aqz c #13111c", +".q. c #13113b", +".MR c #131140", +"#mj c #131144", +"#gi c #131146", +"#mf c #131147", +".nv c #13114b", +"axG c #131217", +"awy c #13121a", +".t. c #13123e", +"#kJ c #131243", +"#hJ c #131246", +"#hK c #131248", +".Ek c #131312", +"ayr c #13131c", +"aCa c #13131e", +"#wz c #13133a", +".uy c #13133e", +".Oe c #131345", +".Z. c #13134a", +"#bC c #13134e", +".#2 c #131353", +".iU c #131354", +".Ld c #131447", +".Xy c #131453", +".fX c #131541", +".JW c #131548", +".hv c #131549", +".PC c #13154c", +".Zb c #13154d", +".XC c #13154f", +"#Cr c #131629", +".am c #131645", +".Ii c #131648", +".PG c #13164e", +".Oa c #13164f", +"Qto c #131653", +".O. c #131656", +".PF c #131657", +".SE c #13174f", +".Rd c #131754", +".Ua c #131760", +".eK c #13185b", +"aBj c #14000e", +"axo c #140100", +".Nb c #140200", +"#8h c #140201", +"aIO c #14030c", +"#DT c #14041a", +"#6G c #140500", +"aHz c #140519", +".Bd c #140600", +"aLu c #14060f", +"aQz c #140610", +"#Fj c #140611", +".AV c #140618", +".ds c #140625", +"#eR c #140706", +".2G c #140708", +"ap3 c #14070a", +"#1m c #140719", +"#Ug c #14071e", +"ads c #140810", +"aIP c #140813", +"#eQ c #140815", +"aPh c #140816", +"aHx c #14081b", +"#tk c #14083d", +"aH5 c #14090d", +"aCm c #140912", +"aP5 c #140914", +"aA4 c #140a11", +".zz c #140b0d", +"aou c #140b11", +"ab1 c #140b13", +"aP4 c #140b15", +"aO5 c #140b16", +"ajA c #140b17", +"#YD c #140b1f", +"#gk c #140b27", +"##N c #140b2e", +".vF c #140b36", +".FA c #140b3b", +".Gh c #140c14", +".oZ c #140c2c", +".dw c #140c35", +"aqe c #140d11", +".vl c #140d22", +".DW c #140d2f", +".rk c #140d47", +"ayo c #140e18", +"#Iy c #140e1c", +"#gq c #140e2a", +".dz c #140e40", +"azk c #140f12", +".z9 c #140f15", +"aQf c #140f1a", +"#nT c #140f1e", +"Qty c #140f4b", +"aBW c #141014", +"#Lo c #141027", +".Ax c #141032", +".j5 c #141036", +"#gf c #14104e", +"ar1 c #141116", +"ak. c #141118", +"ajH c #141122", +"au3 c #14121a", +"arq c #14121d", +"aeZ c #141229", +".NW c #141246", +"ayq c #14131c", +"ajI c #141325", +"#ji c #141345", +"#hC c #141346", +".Y9 c #141348", +"aB9 c #14141f", +"aDA c #141420", +".DC c #141442", +".MC c #141447", +".nJ c #14144e", +".7b c #14144f", +".N9 c #141452", +".J4 c #141454", +".bQ c #14153b", +".GU c #14154a", +".Uk c #14154b", +".J2 c #14154d", +".c9 c #14154e", +".c4 c #141550", +".bt c #141552", +".MN c #141555", +".hq c #141642", +".J1 c #141649", +".#8 c #14164a", +".f1 c #14164c", +".ny c #141653", +".Ix c #141750", +".Lr c #141754", +".O# c #141755", +".Iv c #141757", +".U# c #14175d", +".J6 c #141855", +"aQg c #141c23", +"aub c #141c35", +".dd c #141c5e", +".P0 c #150000", +"aC0 c #150100", +"aQv c #150110", +".N. c #150200", +"aQw c #150210", +".Nc c #150300", +"#Vh c #150303", +"#mv c #150407", +"anh c #150505", +"aPB c #15050e", +"aMr c #15050f", +"azY c #150514", +"#qA c #150529", +".r1 c #150600", +"#ZF c #150706", +"apf c #150708", +".PR c #15071c", +"aFh c #150806", +"aLd c #15080f", +"aNn c #150811", +"aiX c #150905", +"adt c #150910", +"aPt c #150a14", +"#JY c #150a1b", +"aG# c #150a1d", +"aP# c #150b0b", +"az5 c #150b1d", +"adx c #150c0f", +"#G4 c #150c19", +"#uU c #150c2c", +".uq c #150c2e", +"#ac c #150d1d", +".xL c #150d22", +".5C c #150d26", +".rE c #150d2e", +"#gc c #150d34", +"#6f c #150e0d", +"aF9 c #150f20", +"#Lq c #150f23", +".yW c #150f2b", +"at# c #151015", +"aPd c #151017", +"adz c #151021", +".zc c #151025", +".Xw c #151039", +".DK c #15103f", +".DJ c #151041", +".Ss c #151045", +"ahs c #151123", +".Q2 c #151145", +"azq c #151216", +".Aw c #151233", +"#Fq c #151239", +"#kV c #151246", +"#ez c #151252", +"arZ c #151318", +".II c #15131c", +"aHk c #15131d", +"#wB c #151343", +"#bD c #151348", +".nr c #15134d", +"au4 c #15141c", +"aED c #15141d", +".ug c #15142d", +"#f8 c #151450", +"asH c #151518", +".mf c #151551", +".#3 c #151555", +"Qtg c #151556", +".iN c #151643", +"#f9 c #151644", +".hw c #15164b", +".bz c #15164c", +".R# c #15164e", +".ey c #151750", +".5L c #15183a", +".Ls c #151850", +".Iw c #151856", +".nx c #151953", +".gc c #151b5d", +".hN c #151f47", +".Uy c #160000", +"#Xa c #16001b", +".Rv c #160100", +".Ut c #160300", +".L9 c #160303", +"aDM c #16030b", +".rZ c #160400", +".R6 c #160404", +"#Hf c #16040e", +".SP c #16041e", +"azZ c #160515", +"aKc c #16060f", +"aPC c #160610", +".Ux c #160709", +".qE c #160800", +"aDJ c #160814", +".4l c #160908", +"aCl c #160914", +".Ct c #16091a", +".vv c #160924", +"aIY c #160a0f", +"aGa c #160a1d", +"agH c #160b09", +".EB c #160c12", +"#wp c #160c22", +".zw c #160d0f", +"aiu c #160d1a", +".qi c #160d21", +"##4 c #160d2a", +".Cj c #160d3c", +".t7 c #160d49", +"aOj c #160e09", +".Ez c #160e10", +"aqg c #160e14", +"aGh c #160e15", +".8V c #160e25", +".m. c #160e2e", +".qh c #160e2f", +"#c9 c #160e35", +".eW c #160e3f", +"aqa c #160f13", +".Xu c #160f31", +"#.w c #160f34", +"aqc c #161014", +"at. c #161015", +".C# c #161038", +"#eO c #16103b", +".gx c #16103c", +"aOu c #16111a", +"aPg c #16111c", +".E. c #161125", +".Zm c #161126", +".T2 c #16114d", +"ara c #161215", +"aPk c #16121c", +".p3 c #16123e", +"au5 c #161315", +"axB c #16131a", +".Cy c #16132c", +".Xx c #161341", +".oJ c #161342", +".qf c #161346", +"#ju c #161347", +"aqq c #161419", +"awH c #16141b", +"axs c #16141c", +"azF c #16141d", +".je c #161430", +".oN c #161446", +"#hM c #161447", +"ayc c #16151e", +".rx c #161536", +"#wA c #161540", +".kb c #16154a", +".VX c #16154b", +".Fj c #161649", +".kB c #161652", +".It c #161655", +".i6 c #16173d", +".au c #161741", +".f2 c #16174e", +".me c #161751", +".Ij c #161756", +"#DZ c #16182d", +".hD c #161a54", +"aCS c #161c3c", +".P1 c #170000", +".Ng c #170100", +".Nh c #170200", +"aGj c #17020e", +"#A7 c #170217", +"#Uh c #170311", +"aBB c #170400", +"#It c #170511", +"aos c #170605", +"aKk c #17060d", +"ap2 c #170706", +"#k5 c #170810", +".i5 c #170911", +".uv c #170a17", +".s6 c #170a18", +".hI c #170a26", +".SX c #170b11", +"aQq c #170b16", +"aCk c #170b18", +".r4 c #170c02", +".1O c #170c0a", +".jk c #170c11", +"#Iw c #170c19", +".H. c #170c46", +"adw c #170d11", +"#wq c #170d28", +"##Q c #170e33", +"aCj c #170f1c", +".e1 c #170f38", +".xs c #170f3a", +".Eu c #17100d", +"anF c #171015", +".x# c #171028", +".34 c #17102d", +"azS c #17111b", +".x. c #171128", +".7k c #17112d", +".rm c #171141", +"aFl c #17120b", +".Ga c #171215", +".as c #17122b", +".ne c #171230", +"#de c #17123c", +".Kg c #171316", +"aOr c #17131d", +"aqk c #171418", +"aEQ c #171423", +".ro c #171432", +".Y7 c #17143f", +"#c3 c #171442", +"axC c #17151c", +"axr c #17151d", +".B6 c #17153d", +"#tf c #171547", +".lX c #171554", +".ME c #171555", +".uh c #17162e", +".Lg c #171654", +".Lf c #171655", +"am6 c #171724", +"#wO c #171728", +".p7 c #171731", +".Ik c #171755", +".JX c #171756", +"Qth c #171757", +"#A1 c #171838", +".nI c #171850", +".PE c #171856", +".o6 c #17194e", +".SA c #171950", +".eF c #171b55", +".eU c #172049", +"#40 c #180000", +"#LD c #180008", +".tj c #180200", +"aQx c #180211", +"aQu c #180212", +"aOR c #180300", +"#VK c #18031f", +".Ov c #180400", +"#zH c #180414", +".Wg c #180500", +"aCD c #18050b", +"#8g c #180600", +"#3h c #180602", +"aE2 c #180713", +".eO c #180912", +".k7 c #180a11", +".vT c #180a16", +"#hZ c #180a18", +".bU c #180a29", +"aOi c #180b07", +"aPz c #180b12", +".Cs c #180b1a", +".to c #180c03", +".Ok c #180c1c", +"#JX c #180c1e", +"#AV c #180c23", +".yZ c #180c36", +".M8 c #180d0a", +".xR c #180d0f", +"apg c #180d11", +".7# c #180e31", +"ahS c #180f0d", +".dh c #180f3d", +".m# c #18101f", +"alU c #18111a", +".vY c #181128", +".nB c #181141", +"aPj c #18121d", +".p0 c #18124b", +".St c #181252", +".Q4 c #181253", +"aNf c #181318", +"agd c #181326", +".Q5 c #181352", +".Pu c #181353", +".Pt c #181354", +"azf c #181415", +"aox c #18141a", +".hW c #181437", +".Fp c #181445", +".NZ c #181453", +".NY c #181454", +"akO c #181523", +".0J c #181542", +".DY c #18161a", +".c. c #181623", +".G5 c #181642", +".lY c #181656", +".oP c #181749", +"aPo c #181821", +"aAU c #181822", +".b0 c #18184b", +".Lp c #181858", +".rr c #181924", +".Xs c #181a3b", +"#AZ c #181b32", +".qo c #181b4d", +".bB c #181c56", +"ac. c #182d51", +"aiG c #182d55", +"a#v c #190000", +".Om c #190100", +"#1U c #190108", +".Nf c #190300", +".Nr c #190407", +"aCz c #190411", +"as0 c #190502", +".zA c #190600", +"asq c #190603", +"aqW c #190604", +".Ou c #190702", +"aIZ c #19080d", +"aHs c #19080f", +"aE3 c #190813", +".Bo c #19090c", +"aOe c #190910", +".03 c #190a0d", +"agC c #190b08", +"am2 c #190b0c", +".gh c #190b14", +".rJ c #190b1b", +".nX c #190c03", +"am3 c #190c0f", +".AU c #190c1b", +".5W c #190d0d", +".CW c #190d0e", +".v3 c #190d10", +"aPa c #190e0e", +".dK c #190f23", +".VO c #190f32", +"ait c #19101d", +".s2 c #191026", +".EI c #191108", +".G7 c #19113a", +".i8 c #191142", +"#eH c #191145", +".sG c #19114c", +"aC3 c #191214", +".dI c #19131c", +".8Z c #19133e", +".e4 c #191346", +".7d c #19143e", +".DL c #191440", +"#d# c #191444", +"avD c #191518", +".DM c #19153e", +"atJ c #191619", +"azQ c #19171d", +"aDI c #191723", +"aAH c #191821", +".G6 c #191843", +".LA c #191846", +"#x5 c #191a37", +".Fh c #191a4c", +".jd c #191b3f", +"ae4 c #191c37", +".4c c #191c44", +".rL c #191c4c", +"Qtp c #191d57", +".0c c #192a4d", +".Mj c #192f5c", +"#XI c #1a0000", +"aOy c #1a0008", +"aKl c #1a0109", +".M0 c #1a0200", +"aFg c #1a0309", +".Ne c #1a0400", +"#IM c #1a040b", +"ap1 c #1a0501", +"#YE c #1a051f", +"aFA c #1a0600", +"aE4 c #1a0712", +"aPi c #1a0718", +".kD c #1a0923", +"aJ7 c #1a0a12", +".mh c #1a0a23", +".xG c #1a0b16", +".h3 c #1a0c0f", +".dn c #1a0c14", +".D5 c #1a0c1e", +".qm c #1a0c1f", +".ao c #1a0c2b", +".xg c #1a0c36", +".C1 c #1a0e0a", +".v2 c #1a0e10", +"aOv c #1a0e18", +".CJ c #1a0f10", +".xM c #1a0f11", +".T0 c #1a0f3a", +"##P c #1a0f3c", +".G9 c #1a0f47", +"aiY c #1a100d", +"apr c #1a1015", +"#tl c #1a1042", +".BI c #1a1111", +"#s8 c #1a113b", +"ayn c #1a121d", +".kv c #1a1220", +"#x1 c #1a1232", +".t8 c #1a1249", +".gu c #1a1336", +".xr c #1a1337", +"ad9 c #1a1416", +"aqd c #1a1417", +".yX c #1a1430", +".rl c #1a144a", +".Es c #1a1510", +"apy c #1a151a", +"aPe c #1a151d", +"ass c #1a161c", +".lL c #1a163a", +".kf c #1a175a", +"ata c #1a181b", +"aBY c #1a1822", +".B3 c #1a1840", +".oO c #1a184a", +"azu c #1a1922", +".Rb c #1a1a59", +"QtP c #1a1d3f", +".s8 c #1a1d4a", +".hO c #1a1e4d", +".Np c #1b0000", +".ti c #1b0400", +".Zq c #1b0600", +"aD2 c #1b0702", +"aGW c #1b0801", +"auZ c #1b0804", +".M9 c #1b0900", +"aOz c #1b090d", +".nK c #1b0b22", +".Rm c #1b0b23", +".Db c #1b0d00", +".hJ c #1b0d16", +".zj c #1b0d1f", +".Hk c #1b0e21", +".v4 c #1b1012", +"aoI c #1b1015", +".Fy c #1b1145", +".CP c #1b1206", +".zt c #1b1314", +"#J5 c #1b1327", +".vG c #1b133c", +".hP c #1b1344", +"aj5 c #1b1413", +"aqb c #1b1418", +".vE c #1b1436", +".l9 c #1b1446", +"ard c #1b151a", +".0G c #1b1536", +".2c c #1b1625", +"#c2 c #1b1650", +"arK c #1b181d", +"atM c #1b181f", +".Fr c #1b1846", +"aJR c #1b191b", +".Fv c #1b193f", +".lP c #1b1a4e", +".SB c #1b1b5a", +".J3 c #1b1c58", +".sY c #1b1d46", +".Il c #1b1d53", +".c# c #1b1f43", +".dU c #1b264a", +"apR c #1b265d", +".yt c #1b2c49", +".tg c #1c0000", +".Nq c #1c0304", +"aLv c #1c030d", +"#M7 c #1c0314", +".LI c #1c0400", +".2F c #1c0500", +"aOC c #1c0506", +".00 c #1c0600", +".Nd c #1c0601", +".uE c #1c0700", +"aI0 c #1c070b", +".rY c #1c0902", +".zn c #1c0c14", +".o9 c #1c0c20", +".vt c #1c0d35", +".qD c #1c0e05", +".o4 c #1c0e24", +".gg c #1c0f2a", +".0a c #1c1006", +".CY c #1c1014", +"aDK c #1c1019", +".uD c #1c1100", +".C4 c #1c1104", +".CT c #1c1209", +".CM c #1c120d", +"#bu c #1c1212", +"aMf c #1c1219", +"aOp c #1c121e", +"#.n c #1c1224", +".ad c #1c1241", +".CO c #1c1309", +".EO c #1c130d", +"apt c #1c1318", +"aNC c #1c131f", +".2j c #1c132b", +".AK c #1c133e", +".EM c #1c140e", +"aov c #1c151c", +"aQp c #1c151e", +"aho c #1c1525", +".DR c #1c1633", +".ng c #1c1835", +"aPp c #1c1923", +"arW c #1c1a1e", +".N0 c #1c1a52", +".p9 c #1c1b44", +".MF c #1c1b52", +".T3 c #1c1b56", +".Lh c #1c1c53", +".Fl c #1c1d4f", +".GV c #1c1d52", +"QtI c #1c204f", +"aQl c #1c2128", +".Ni c #1d0000", +"#CB c #1d0316", +"#0z c #1d070a", +"#3g c #1d0800", +".Cx c #1d0907", +".rX c #1d0a03", +"#Fi c #1d0a14", +".AZ c #1d0b0e", +"#Ck c #1d0b17", +".Br c #1d0d04", +".qr c #1d0d1f", +".pl c #1d0f06", +".D6 c #1d0f23", +".qG c #1d1007", +".C0 c #1d110f", +".h2 c #1d1218", +"aOt c #1d121f", +"#J0 c #1d1323", +"##O c #1d133f", +"#.o c #1d141e", +".EH c #1d150d", +".nC c #1d1525", +".0F c #1d1530", +".sH c #1d154c", +"#gd c #1d154f", +"#Fn c #1d1629", +"#.v c #1d1631", +"#eI c #1d1652", +"aBD c #1d1719", +"#br c #1d1742", +".Su c #1d1851", +".As c #1d193b", +".Pv c #1d1951", +"asI c #1d1b20", +".2d c #1d1b26", +".sK c #1d1b33", +".rs c #1d1b3d", +".Fs c #1d1b46", +".hV c #1d1e48", +".GS c #1d1e53", +".VS c #1d1f4d", +".cd c #1d2245", +"aKT c #1d2b4d", +".Ab c #1d2e4b", +"#A8 c #1e000f", +"#VL c #1e0013", +".Nj c #1e0100", +".Rw c #1e0300", +"aOD c #1e0608", +"aJt c #1e0708", +"aOE c #1e070a", +"aI1 c #1e070b", +".D9 c #1e0902", +"aOw c #1e0914", +"#DR c #1e0b14", +"aQi c #1e0d1d", +".Bb c #1e0f07", +".gF c #1e0f12", +".i1 c #1e0f18", +".rO c #1e0f1d", +".nG c #1e0f28", +"aPA c #1e1018", +".i9 c #1e1030", +".tn c #1e1108", +"#zp c #1e1129", +".mu c #1e1208", +".v5 c #1e1214", +".C5 c #1e1304", +"aoH c #1e1318", +".vM c #1e1319", +".EE c #1e1515", +"aJ6 c #1e151b", +"#gp c #1e1726", +".xu c #1e1737", +".Fw c #1e173c", +".Gk c #1e1817", +".na c #1e1937", +"azh c #1e1a1c", +".pT c #1e1a28", +"asE c #1e1b1f", +"agQ c #1e1b22", +".0I c #1e1b46", +".lT c #1e1c58", +".DG c #1e1e4c", +".aE c #1e1f2e", +"#AX c #1e2134", +"#WM c #1e2234", +".ce c #1e2246", +".fn c #1e2345", +"#3U c #1e283d", +".O1 c #1f0000", +".O2 c #1f0100", +"aOf c #1f060e", +"aE5 c #1f0613", +".Rp c #1f0700", +"asZ c #1f0701", +"arI c #1f0a06", +"aHD c #1f0c14", +".pA c #1f0d0c", +".SY c #1f0e10", +".TA c #1f0e12", +".r0 c #1f0f07", +"#Cl c #1f0f23", +"#pg c #1f100f", +"aCn c #1f1017", +".t# c #1f101c", +".mc c #1f102b", +".kR c #1f1109", +"aLe c #1f1219", +".bP c #1f121a", +".Hj c #1f1224", +"#.m c #1f1230", +".Xo c #1f152a", +".bJ c #1f1544", +"#7T c #1f1613", +".o0 c #1f1627", +".ur c #1f162d", +"#M1 c #1f172b", +".z. c #1f173e", +"QtJ c #1f1747", +"aL. c #1f181d", +".3V c #1f1829", +"#d. c #1f1848", +".Xv c #1f193d", +"#ga c #1f1a1d", +".lI c #1f1a3e", +"aq2 c #1f1b21", +".p4 c #1f1c42", +"#cZ c #1f1d40", +".oL c #1f1d47", +".nm c #1f1d54", +".ke c #1f1d5c", +"#t# c #1f1e46", +".GW c #1f2055", +".Ir c #1f2058", +".ca c #1f2346", +"aKm c #200109", +".Nk c #200400", +".yn c #200800", +"#Ui c #200810", +"aBd c #200817", +"#1n c #200b21", +".kz c #20102c", +"aE1 c #20121e", +".eS c #20131b", +"aPu c #20131d", +".C2 c #20140d", +"ak2 c #20141b", +".kH c #201630", +".e5 c #201641", +".EG c #201812", +".VP c #201845", +".dr c #201849", +".yS c #201a36", +"#uW c #201a45", +".5t c #201b2b", +".xq c #201c2f", +".kj c #201f61", +"#9. c #203057", +".No c #210100", +"aA7 c #21020e", +"#D8 c #210416", +".SS c #210900", +".FO c #210a00", +"aKZ c #210b03", +".pB c #210d17", +"aum c #210e09", +".Bq c #211109", +"afl c #21110d", +".Bn c #211111", +"#AT c #211122", +".uL c #21120a", +".Be c #211305", +".CZ c #211413", +".62 c #21141b", +"#9y c #211610", +".dB c #21161c", +".dA c #211642", +".2i c #211728", +"#.D c #211929", +".AC c #211935", +".rc c #211d23", +".j8 c #211d44", +"#Kd c #220107", +".Ve c #220300", +"aOg c #22040b", +"#zI c #220915", +".5U c #220a00", +"asp c #220b06", +"aAl c #220f08", +".gq c #221433", +".CX c #221619", +"#wr c #22183b", +".AA c #221842", +".f. c #221920", +"azC c #221a26", +".Et c #221b18", +".oy c #221d33", +".DD c #22214f", +".Fk c #222255", +"#1# c #22364e", +"aFk c #230000", +"aB. c #23000d", +".Nl c #230600", +"ang c #230a02", +".Hn c #230b00", +".BD c #230f00", +".ja c #231108", +".2I c #231112", +"az0 c #23121e", +".kS c #23130b", +".FL c #231529", +".Bg c #231602", +".Bl c #231606", +".D4 c #231624", +"afq c #231814", +".G8 c #231949", +"aIG c #231a20", +".DS c #231c40", +"atN c #232027", +".K0 c #233867", +"#Rv c #24000b", +"a#u c #240100", +"#6F c #240200", +"#0y c #240300", +".P2 c #240600", +"aMs c #240612", +"aOo c #240615", +"aNo c #240a14", +"aty c #24110d", +".mw c #24140c", +".fi c #24141b", +"aNH c #241423", +".tm c #24150d", +".Bm c #241510", +".vu c #241539", +"aNG c #241624", +".qF c #24170e", +"aLi c #24171e", +".Bh c #241802", +".r3 c #24180e", +"a#e c #24180f", +".jC c #24181a", +"aoG c #24191e", +".fa c #24191f", +".TZ c #241937", +"aHi c #241b21", +".7. c #241b30", +".b2 c #241b40", +".hX c #241c38", +"aNe c #241f24", +".u. c #241f41", +".Cb c #24203c", +"atL c #242123", +".G3 c #242250", +".kJ c #242642", +".dT c #242a42", +".cg c #242c4d", +"aa6 c #250000", +"aB# c #25000f", +"aBc c #250013", +"#YF c #25001b", +"#1T c #250100", +"aOh c #25020a", +".7s c #250500", +"aA6 c #250613", +".Nn c #250900", +".Nm c #250901", +".tK c #250e00", +".mM c #251524", +"#eS c #25170e", +".xC c #251729", +".BH c #251814", +".kQ c #251a10", +"##8 c #251c24", +"aL# c #251e24", +"aJS c #251e25", +".xf c #251e36", +"aw2 c #25232b", +".dS c #252536", +".cf c #25294d", +"#9U c #260000", +"#Xb c #260019", +"#OC c #260112", +".9m c #260200", +".uF c #260300", +"aKn c #26040c", +"aA9 c #260412", +"aOl c #260610", +"aA8 c #260613", +"aGk c #260f1c", +".e0 c #26111e", +"ad1 c #261611", +".k9 c #261628", +"#.k c #26193a", +".Bi c #261a02", +".Hg c #261a21", +".CV c #261b18", +".VN c #261b30", +".y0 c #261b41", +".Fz c #261c53", +".T1 c #261d54", +"#c6 c #261e1f", +".gC c #261e27", +"#bo c #261f4d", +"aNE c #26202b", +".rj c #262128", +".2m c #262145", +".0z c #262333", +".dR c #262430", +".aD c #262634", +"ajN c #26395c", +".O0 c #270503", +".3t c #27151e", +".Bp c #27161c", +".nY c #271810", +".AW c #27192d", +"agG c #271a16", +".gl c #271a22", +".Bj c #271b05", +"#Lv c #271f33", +".gT c #272034", +".y9 c #27223b", +"aEE c #27262f", +"aBt c #272d4e", +".sW c #27304c", +".EZ c #273f6d", +"#VM c #280009", +"aBb c #280211", +".9k c #280300", +"#Z4 c #280923", +".z0 c #280f00", +".Da c #281a0d", +".mv c #281a11", +".xD c #281a2e", +"#yg c #281b2e", +".eN c #281b37", +"#bx c #281f37", +".bT c #282050", +".AM c #282146", +"aAy c #282226", +"aoK c #282429", +".nd c #282441", +"apE c #28262b", +".DQ c #282643", +".hY c #282939", +".GC c #283e69", +".Ox c #290000", +"#8f c #290700", +"aJI c #29140c", +".mN c #291929", +".Bt c #291a0c", +".qC c #291a11", +".Cu c #291c2f", +".C3 c #291d13", +"#.j c #291e33", +".Fx c #29204c", +".5B c #292138", +"aND c #29222d", +"apA c #29262a", +"atO c #29262d", +".u# c #29273c", +".fm c #292b40", +".cc c #292d51", +".fo c #29315f", +".Ia c #293d66", +"aOn c #2a0616", +".v. c #2a0d00", +".u7 c #2a1200", +".wh c #2a1408", +".qU c #2a141a", +".BC c #2a1601", +".zB c #2a1705", +"#2P c #2a1719", +".Yz c #2a1809", +".fb c #2a1a21", +".dj c #2a1b24", +"aaT c #2a1d12", +".tp c #2a1f15", +"aEf c #2a2124", +".e9 c #2a222a", +"QtZ c #2a2b3b", +"avX c #2a3857", +".1P c #2a395a", +"#2z c #2a4a6c", +"#0T c #2b0000", +".6A c #2b0600", +".nN c #2b1508", +".XT c #2b1718", +".Bk c #2b1f0a", +"#xY c #2b1f2f", +".C6 c #2b2111", +"#M0 c #2b2330", +".gU c #2b2543", +"axt c #2b2931", +".rA c #2b3158", +".ul c #2b354d", +".ys c #2b364d", +"aJC c #2b385a", +".Jv c #2b4070", +"#0U c #2c0000", +"#M8 c #2c000b", +"#P5 c #2c0011", +"#jF c #2c0100", +".th c #2c0300", +"aFi c #2c0502", +"#CC c #2c0513", +"aOm c #2c0514", +"aBa c #2c0615", +"aLw c #2c0813", +".sj c #2c1100", +".u4 c #2c1212", +".kG c #2c1319", +".x8 c #2c1519", +".uJ c #2c1710", +".4m c #2c1717", +"#nV c #2c1915", +"acp c #2c1e12", +".uO c #2c2218", +"#4y c #2c231d", +"aJT c #2c252c", +".Ck c #2c2548", +".e6 c #2c262f", +"aDx c #2c2e3a", +"#7n c #2c3f69", +"#9T c #2d0000", +"aOx c #2d111d", +".qV c #2d171e", +"##T c #2d201f", +"##Z c #2d2748", +".gz c #2d2b38", +".IH c #2d2d43", +"ahz c #2d3f66", +"#2b c #2e0303", +"#4Z c #2e0d00", +".SZ c #2e1613", +".sh c #2e161a", +".zF c #2e1d00", +".ob c #2e1e20", +".nW c #2e2218", +".ii c #2e2232", +".dJ c #2e2438", +".EJ c #2e2620", +".Is c #2e2f6a", +".5M c #2e3158", +"aax c #2e3a5b", +"aFj c #2f0000", +"#LE c #2f0005", +"#0S c #2f0100", +".tF c #2f1617", +".mk c #2f1715", +"aIi c #2f1911", +".gt c #2f1a21", +".qT c #2f1b17", +".R5 c #2f1c17", +"akw c #2f2b2f", +"ato c #2f4465", +".Dl c #2f4a7c", +"aa5 c #300002", +"aNJ c #300314", +".Oy c #300400", +".9l c #300e00", +".tI c #301600", +".sk c #301707", +".sf c #301a13", +".wg c #301c06", +".hS c #301d1b", +".k8 c #302131", +".pm c #302219", +".qH c #30231a", +"ajW c #30232d", +".Xp c #302848", +"#eE c #302a2c", +".gV c #302a4f", +"aAR c #302b37", +"awz c #302d30", +".al c #303a62", +".ch c #303b60", +".aJ c #304475", +"#HA c #310000", +"#1S c #310400", +"aI2 c #310a00", +"#Z. c #310e00", +".tG c #311600", +".z1 c #311800", +"ak8 c #312623", +"aLa c #312a2f", +".8U c #312b2e", +"#2a c #320604", +"#2c c #320909", +".7t c #321700", +"aNI c #321827", +".wk c #321921", +".Zt c #321a1a", +".1N c #32221c", +".zk c #322438", +".Cr c #322630", +".EL c #322923", +"aIm c #323033", +"ari c #323035", +".B4 c #323058", +"#da c #32305c", +"aql c #323134", +".Wl c #331b0d", +".3s c #331d21", +"azd c #331f18", +"aIN c #33222b", +".xT c #33282a", +".vm c #332b40", +"anj c #332d34", +".vD c #332f40", +"Qt9 c #335e9d", +".#g c #3365a8", +".#f c #3367a8", +".#e c #3368a8", +"#2d c #340806", +"aNp c #340d19", +".7z c #341304", +".v# c #341608", +".wl c #341f08", +"#.A c #342835", +"Qt0 c #343748", +"aQF c #344d7b", +".#h c #3463a8", +"#8e c #350000", +"#Zo c #350100", +"a#t c #350103", +"aDY c #350a1e", +"#h2 c #350b00", +".7r c #350c00", +"#XH c #350d00", +"aMY c #351717", +".si c #351806", +".vP c #352739", +".xh c #35284c", +"anE c #352e33", +"#Vi c #35374b", +".d6 c #355d9e", +".aW c #3565aa", +"#Zn c #360000", +"#9S c #360005", +"#Xc c #360011", +"#SW c #360213", +"#0R c #360703", +".Rx c #361609", +"#3f c #361c0c", +".Bu c #362716", +".zi c #362938", +".0E c #362c41", +"aLb c #362f35", +".Ly c #36325a", +"#Zt c #370000", +"#Zp c #370500", +"#0V c #370600", +".9c c #370c00", +".wj c #371e22", +".yo c #37231c", +".Bv c #372916", +".aj c #372931", +".7h c #372a20", +"al6 c #372b2f", +"aoF c #372c31", +"#Fm c #372f3c", +"aAx c #373134", +"aJU c #373137", +"afA c #37343b", +".DE c #373664", +"ac# c #374d71", +".#i c #3763ac", +".aX c #3766ab", +"#9R c #38040a", +"#Zq c #380700", +".OM c #380800", +"#9Q c #38080c", +"#Wf c #380900", +"#h3 c #380b00", +"aL1 c #380e0e", +".04 c #381b1a", +".48 c #381d2d", +".tD c #382017", +".pz c #382a14", +".mL c #382a2f", +".ih c #382b37", +".us c #382e3e", +".dQ c #382e42", +".dH c #38313a", +".VR c #38376a", +"#YG c #390018", +"#Zs c #390300", +"#Zr c #390400", +"aa4 c #390608", +"#HB c #39100c", +".OZ c #39130f", +"aHE c #39202b", +".49 c #392236", +"#TV c #392626", +".z7 c #392822", +".Bs c #392a1f", +".FJ c #392c3b", +".jD c #392f31", +".Ci c #393060", +"#ws c #393157", +"#80 c #39353b", +"aAd c #393e60", +"#5H c #394b71", +".aK c #394e7f", +".#a c #395ea5", +".## c #3960a4", +".yy c #3966b3", +".wL c #3966b4", +".Oz c #3a0e02", +".P3 c #3a1a0c", +".tE c #3a2121", +".tJ c #3a220e", +".uI c #3a231d", +".wi c #3a2320", +".7g c #3a2d2c", +".vN c #3a2d38", +"aMh c #3a3137", +"alX c #3a465d", +"Qt3 c #3a5a93", +".lk c #3a5fab", +".aT c #3a62ad", +".m2 c #3a64b8", +".aV c #3a6bb0", +"#e2 c #3b0200", +"#0Q c #3b0a03", +".OK c #3b0b03", +"#D9 c #3b101d", +".nZ c #3b2b23", +".xe c #3b344b", +".cq c #3b62ad", +".a1 c #3b63a9", +".su c #3b64a9", +".cm c #3b64ac", +".oq c #3b67bc", +"#dq c #3c0400", +"aNR c #3c0408", +".OL c #3c0c04", +"#2e c #3c0d0a", +"#2# c #3c0e0b", +"#.K c #3c0f00", +"aMt c #3c121f", +".sg c #3c2427", +"#Cj c #3c2a28", +".od c #3c2a37", +".oc c #3c2b37", +"anG c #3c363b", +"#bn c #3c3759", +".At c #3c385a", +"adK c #3c517e", +"Qt2 c #3c5b95", +".ct c #3c64a6", +".cs c #3c64a9", +".cr c #3c64ac", +".#j c #3c65b0", +".#B c #3c66b0", +".cJ c #3c66b1", +"##j c #3d0000", +"#Rw c #3d0113", +".58 c #3d0900", +".OJ c #3d0d04", +"#VN c #3d0d12", +"#Wg c #3d180b", +".u2 c #3d2419", +".zO c #3d292c", +".zE c #3d2c12", +".b3 c #3d2d30", +".z8 c #3d3130", +".8O c #3d3336", +"aFB c #3d3437", +".xd c #3d364e", +".Av c #3d395b", +".aS c #3d62aa", +".#A c #3d65b2", +".#C c #3d66b1", +".vj c #3d67ae", +".#F c #3d67b1", +".a9 c #3d67b2", +".r# c #3d69b8", +"#Zu c #3e0000", +"aNO c #3e0001", +"aNK c #3e0012", +"#gw c #3e0300", +"#Zm c #3e0600", +"#Z5 c #3e0b28", +"aNS c #3e181b", +"#FW c #3e1d1d", +"#dg c #3e3340", +"#gb c #3e3749", +".t6 c #3e3936", +"ahg c #3e3b3f", +"aIc c #3e507a", +".dV c #3e507f", +".BO c #3e5d93", +".oo c #3e64af", +".a8 c #3e66b2", +".#z c #3e66b3", +".a0 c #3e67ad", +".#G c #3e67b2", +".lx c #3e68af", +".lw c #3e68b0", +".m9 c #3e68b1", +".#D c #3e68b2", +".#E c #3e68b3", +".ly c #3e69b0", +".ln c #3e69c1", +".Af c #3e6ab4", +".Dp c #3e6cab", +"#CZ c #3f0200", +"aa3 c #3f0b0d", +"#ag c #3f0f01", +"#A9 c #3f1823", +"axY c #3f2b25", +".8K c #3f3147", +".63 c #3f3333", +"#G7 c #3f3744", +".Ka c #3f3b64", +".un c #3f4268", +".g3 c #3f63af", +".jX c #3f64b1", +".jY c #3f65b0", +".a2 c #3f66ad", +".#o c #3f66ae", +".cp c #3f66b3", +".#n c #3f67ad", +".#y c #3f67b3", +".#x c #3f67b4", +".#H c #3f68b3", +".lv c #3f69b0", +".m6 c #3f69b1", +".lq c #3f69b2", +".#I c #3f69b3", +".lu c #3f6ab1", +".wN c #3f6bae", +".E4 c #3f6dab", +".Uz c #401615", +"#HC c #401a14", +"aym c #403541", +".5s c #40374b", +".dY c #4061a7", +".d2 c #4061ae", +".#d c #4062b0", +".fJ c #4065b3", +".a4 c #4067ae", +".cy c #4067af", +".#p c #4067b0", +".#k c #4067b4", +".#m c #4068ae", +".cI c #4068b3", +".a7 c #4068b4", +".#w c #4068b5", +".cu c #4069a9", +".jS c #4069af", +".wU c #4069b0", +".eg c #4069b3", +".cH c #4069b4", +".jT c #406aaf", +".iy c #406ab0", +".os c #406ab1", +".m5 c #406ab2", +".ls c #406ab3", +".eh c #406ab4", +".fI c #406ab5", +".or c #406bb2", +".lp c #406bb3", +".yA c #406caf", +".pO c #406cbe", +"aNQ c #410003", +"#UN c #410900", +"#bT c #410d01", +".6C c #411912", +".wB c #41281c", +".wm c #412b13", +"aNA c #41313f", +"#Co c #413647", +".3u c #414e6d", +".#b c #4165af", +".#L c #4166b2", +".ba c #4166b3", +".#K c #4166b4", +".#t c #4167b5", +".a3 c #4168ae", +".cx c #4168af", +".cz c #4168b1", +".#q c #4168b3", +".#v c #4168b5", +".d9 c #4169b0", +".ef c #4169b3", +".cG c #4169b4", +".a6 c #4169b5", +".#u c #4169b6", +".#. c #416aab", +".jU c #416aaf", +".iz c #416ab0", +".yD c #416ab1", +".ha c #416ab2", +".fH c #416ab3", +".ee c #416ab4", +".cF c #416ab5", +".aU c #416ab7", +".lz c #416bb2", +".pP c #416bb3", +".lr c #416bb4", +".hb c #416bb5", +".lC c #416bb6", +"Qt8 c #416caa", +".m7 c #416cb3", +".ou c #416cb4", +".aY c #416eb3", +".Do c #416fb0", +"aNN c #420004", +"#Hg c #420c17", +"#0W c #420d05", +"#Fz c #420d1a", +".59 c #420e00", +".7D c #420e02", +".ON c #42130a", +"aNq c #421320", +".OA c #42160a", +"aKQ c #421714", +".ym c #422604", +".x9 c #422b32", +".qB c #42332b", +".pj c #42352c", +"##5 c #423643", +"arY c #424245", +"#AY c #424559", +".d1 c #4261b1", +".jZ c #4267b1", +".cM c #4267b2", +".cL c #4267b3", +".fK c #4267b4", +".b# c #4267b5", +".a5 c #4268b3", +".#s c #4268b5", +".#l c #4268b6", +".cw c #4269b0", +".e. c #4269b2", +".cA c #4269b4", +".#r c #4269b5", +".cC c #4269b6", +".d8 c #426ab0", +".wW c #426ab2", +".fG c #426ab3", +".ed c #426ab4", +".cE c #426ab5", +".cD c #426ab6", +".wS c #426bb1", +".wV c #426bb2", +".fF c #426bb3", +".m4 c #426bb4", +".ec c #426bb5", +".lD c #426bb6", +".yE c #426cb2", +".lA c #426cb3", +".m8 c #426cb4", +".lt c #426cb5", +".hc c #426cb6", +".ov c #426cb7", +".lm c #426cc2", +".Dq c #426daf", +".wX c #426db2", +".ot c #426db4", +".sw c #426db5", +".E3 c #426fae", +"#9P c #430103", +"#OD c #430513", +"aLx c #431622", +".6B c #431d14", +".rW c #432215", +".eR c #432409", +".7A c #432418", +"ape c #432c26", +".bX c #432e35", +".tf c #433121", +".Bf c #433624", +"aLg c #43363d", +"aMg c #433940", +"#r4 c #433a3f", +".qj c #433a44", +"aGJ c #433b39", +"ado c #433f48", +"awd c #434047", +".ej c #4368b2", +".ei c #4368b3", +".hd c #4368b4", +".cK c #4368b5", +".#J c #4368b6", +".yw c #4369ae", +".iA c #4369b2", +".eb c #4369b6", +".fA c #436ab2", +".fB c #436ab3", +".e# c #436ab5", +".cB c #436ab6", +".h# c #436ab7", +".d5 c #436bac", +".d7 c #436bb1", +".yF c #436bb3", +".lo c #436bb5", +".yB c #436cb0", +".wR c #436cb2", +".wT c #436cb3", +".yG c #436cb4", +".jV c #436cb7", +".lB c #436db6", +".lE c #436db7", +".vk c #436db8", +".wY c #436eb4", +".E5 c #436fb0", +".BS c #4370b1", +".Ah c #4370b2", +".tV c #4370ba", +".wM c #4372b9", +"#Zl c #440a00", +"#IN c #440a12", +".OI c #44140c", +".9n c #44180a", +".44 c #441c0f", +".qW c #443124", +"aLh c #44373f", +"#bG c #443946", +".rG c #443a47", +".s3 c #443a49", +"ast c #444046", +"ay7 c #44496c", +".aP c #44629b", +".aQ c #4464a2", +".Dn c #4468ab", +".yC c #4469b1", +".cN c #4469b3", +".#M c #4469b4", +".fL c #4469b5", +".b. c #4469b7", +".j0 c #446ab1", +".iB c #446ab2", +".he c #446ab3", +".ea c #446ab6", +".fE c #446ab7", +".jR c #446ab8", +".fz c #446bb2", +".g6 c #446bb3", +".g7 c #446bb4", +".fC c #446bb6", +".h. c #446bb8", +".yH c #446cb4", +".tU c #446daf", +".wQ c #446db3", +".Aj c #446db4", +".BX c #446db5", +".ra c #446db8", +".tW c #446eb6", +".pQ c #446eb7", +".lF c #446eb8", +".n. c #446eb9", +".yx c #446eba", +".aZ c #446fb4", +".yI c #446fb5", +".vi c #4471b9", +".yz c #4472b9", +"#ay c #450000", +"aNP c #450003", +"#M9 c #450109", +"#I9 c #451200", +".OO c #45150d", +"aFf c #451528", +".9Z c #451809", +"#HD c #451a09", +".41 c #451b02", +"#6O c #451c01", +"#FX c #451e0c", +"aK# c #45353d", +".vQ c #45374b", +".Bw c #453822", +"aNB c #453845", +".d0 c #4562b5", +"Qt4 c #45649e", +"Qt6 c #45659e", +".jW c #4569b7", +".iE c #456baf", +".iD c #456bb1", +".iC c #456bb2", +".fM c #456bb3", +".fD c #456bb7", +".ix c #456bb8", +".j1 c #456caf", +".Jz c #456cb2", +".fy c #456cb3", +".it c #456cb4", +".iu c #456cb5", +".g8 c #456cb7", +".g9 c #456cb8", +".jQ c #456cb9", +".co c #456cba", +".E2 c #456db0", +".wO c #456db1", +".g5 c #456db3", +".Dr c #456db4", +".Ai c #456eb2", +".BW c #456eb4", +".Ak c #456eb5", +".Al c #456eb6", +".BT c #456fb2", +".wZ c #456fb5", +".lG c #456fb9", +".w0 c #4570b5", +".m3 c #4571c8", +".GH c #4573af", +".9O c #460500", +".6. c #461200", +"#3D c #461803", +"#zJ c #462b34", +".zM c #46342b", +".BG c #463730", +".nV c #463b31", +"#ZK c #465470", +".ci c #465580", +".aL c #465b8c", +".dZ c #4662b7", +".E1 c #4667a7", +".aR c #4669ac", +".fw c #4669b2", +".#c c #4669b5", +".hf c #466cb3", +".ek c #466cb4", +".bb c #466cb5", +".hh c #466db0", +".BU c #466db4", +".jN c #466db5", +".jO c #466db6", +".iv c #466db7", +".jP c #466db8", +".iw c #466db9", +".m1 c #466dbc", +".g4 c #466eb4", +".Am c #466eb6", +".BV c #466fb5", +".BY c #466fb6", +".BZ c #466fb7", +".r. c #466fb8", +".yJ c #4670b6", +".w6 c #4670b8", +".ow c #4670ba", +".wK c #4670be", +".w1 c #4671b6", +".w5 c #4671b8", +".sv c #4673bf", +".BR c #4674b7", +".If c #4675b1", +"#Hz c #470b00", +".OH c #47170f", +".40 c #471901", +".90 c #47190a", +".8a c #47231c", +".5X c #473131", +".di c #473a56", +"aJV c #474047", +"#bm c #47435c", +"ao6 c #47518e", +"arx c #475680", +".aI c #475b8c", +".aF c #475b8d", +".ck c #47659f", +".fv c #4766b1", +".Ac c #4769a3", +".q9 c #476aa9", +".d3 c #476ab3", +".jJ c #476bb0", +".wP c #476bb4", +".fx c #476cb5", +".iF c #476db1", +".hg c #476db3", +".BQ c #476db4", +".ir c #476dba", +".j2 c #476eb2", +".jM c #476eb5", +".is c #476fb5", +".Ae c #476fb9", +".cv c #4770af", +".Du c #4770b6", +".Dv c #4770b7", +".Dx c #4770b8", +".tX c #4770bb", +".yL c #4771b7", +".w4 c #4771b8", +".pR c #4771bb", +".yK c #4772b7", +".Dy c #4772b8", +".w3 c #4772b9", +".cn c #4772bd", +".GG c #4773b1", +".GI c #4774b4", +".Ag c #4775ba", +"a#s c #480209", +".57 c #480400", +"#KC c #480700", +"#Fl c #484453", +".li c #4866a7", +"Qt5 c #4867a1", +".g2 c #4868b0", +".st c #486aa4", +".iq c #486bb5", +".yv c #486ca9", +".wJ c #486eac", +".hi c #486eb2", +".fN c #486eb5", +".cO c #486eb6", +".iG c #486fb4", +".jL c #4870b6", +".Dw c #4870b8", +".Ds c #4871b7", +".Dt c #4871b8", +".GN c #4871b9", +".w2 c #4872b8", +".yO c #4872ba", +".rb c #4872bc", +".tY c #4872bd", +".yM c #4873b8", +".An c #4873b9", +"aNL c #490010", +"#E. c #49131d", +".OG c #491910", +".6H c #492f4b", +"#xX c #493d47", +".vJ c #493f3c", +"aPI c #496190", +".Ad c #496daf", +".#N c #496eb8", +".j3 c #496fb5", +".iH c #496fb6", +".K4 c #4970b4", +".hj c #4970b5", +".Fa c #4971b9", +".E9 c #4972b8", +".E8 c #4972b9", +".F# c #4972ba", +".Dz c #4973b9", +".w7 c #4973ba", +".Ap c #4973bb", +".sx c #4973bd", +".Fb c #4974b9", +".yN c #4974ba", +".yP c #4974bb", +".Ie c #4975b2", +"#Ke c #4a0a0f", +".6# c #4a1500", +"#J# c #4a1603", +"#0P c #4a160e", +".OB c #4a1e12", +".xW c #4a3519", +".dv c #4a3542", +".Y3 c #4a415a", +"#tw c #4a4348", +"aAI c #4a4852", +".yu c #4a5e7f", +".tS c #4a6085", +".dW c #4a639d", +".Ic c #4a69a5", +".vg c #4a6a9d", +".GE c #4a6aa7", +".hk c #4a70b6", +".iI c #4a70b7", +".hl c #4a70b8", +".hm c #4a70b9", +".JS c #4a71b8", +".K6 c #4a71b9", +".Lc c #4a72b8", +".MA c #4a72b9", +".GM c #4a72ba", +".E6 c #4a73b9", +".F. c #4a73bb", +".JK c #4a74b8", +".Ao c #4a74ba", +".Aq c #4a74bb", +".tZ c #4a74be", +".Fc c #4a75ba", +".Fe c #4a75bb", +".w8 c #4a75bc", +"aNM c #4b020d", +"#SX c #4b0617", +"#.0 c #4b0c00", +"#3C c #4b1804", +"#CD c #4b1b25", +".OP c #4b1c12", +"#1o c #4b2741", +".gk c #4b2c11", +"#G2 c #4b3a44", +".uM c #4b3e35", +".o1 c #4b424a", +"aNF c #4b424e", +"aAw c #4b4549", +"ax. c #4b494f", +".Dm c #4b69a3", +".m0 c #4b6eb7", +".iJ c #4b70ba", +".fP c #4b71b5", +".fO c #4b71b8", +".bc c #4b71b9", +".JB c #4b71ba", +".Mp c #4b72b8", +".Mn c #4b73b5", +".JR c #4b73ba", +".GL c #4b73bb", +".vh c #4b74b5", +".JL c #4b74b9", +".E7 c #4b74ba", +".GJ c #4b74bb", +".JJ c #4b75b8", +".My c #4b75b9", +".Ff c #4b75bb", +".B0 c #4b75bc", +".yQ c #4b75bd", +".Fd c #4b76bb", +".B1 c #4b76bd", +"#.2 c #4c0a00", +"#I8 c #4c1100", +"#XY c #4c1200", +"aNr c #4c1826", +"#2f c #4c1913", +"#.R c #4c1c08", +".9d c #4c2a1d", +"#FV c #4c2a2e", +".W4 c #4c2e1f", +".u3 c #4c3231", +".pa c #4c3720", +".zC c #4c3a25", +".bM c #4c3d3a", +".8J c #4c414a", +".nD c #4c4449", +".pZ c #4c4856", +"aej c #4c4a51", +"a.Q c #4c5a7e", +".aG c #4c6192", +".aN c #4c679b", +".fu c #4c67b3", +".q8 c #4c689a", +".BP c #4c6dac", +".Jy c #4c6fb5", +".d4 c #4c71b6", +".el c #4c71b9", +".hn c #4c71bc", +".jK c #4c71bd", +".GF c #4c72b3", +".fQ c #4c72b6", +".j4 c #4c72bb", +".NF c #4c73b3", +".JT c #4c73ba", +".NT c #4c74ba", +".JV c #4c74bb", +".JI c #4c75b8", +".JM c #4c75b9", +".JN c #4c75ba", +".GK c #4c75bb", +".JF c #4c76b7", +".JE c #4c76b8", +".JH c #4c76b9", +".JO c #4c76ba", +".GO c #4c76bc", +".w9 c #4c76bd", +".DA c #4c77be", +"#ax c #4d0700", +"#Bs c #4d0d00", +"#KB c #4d0e00", +".4T c #4d0f00", +"#J. c #4d1c0c", +"#0x c #4d2004", +".Qw c #4d2113", +".ma c #4d4549", +".AD c #4d475b", +".fl c #4d4c55", +".jq c #4d4e45", +".gZ c #4d5e9a", +".ip c #4d6dae", +".fT c #4d72ba", +".#V c #4d72bc", +".Id c #4d73b3", +".fR c #4d73b9", +".fS c #4d73ba", +".#W c #4d73be", +".Mz c #4d74bb", +".NH c #4d75b8", +".JU c #4d75bb", +".JQ c #4d75bc", +".ll c #4d75c6", +".L# c #4d76b8", +".JG c #4d76b9", +".JP c #4d76ba", +".Mx c #4d76bb", +".JD c #4d77b8", +".L. c #4d77b9", +".Lb c #4d77ba", +".NS c #4d77bb", +".Fg c #4d77be", +"#Xd c #4e0912", +"aNz c #4e1721", +".OF c #4e1e16", +"QtL c #4e2400", +".7C c #4e2419", +".OY c #4e241f", +".wA c #4e2f0b", +".jl c #4e3f41", +"aBU c #4e4a4e", +".av c #4e4a6d", +".sR c #4e4c68", +".mm c #4e4e65", +".fp c #4e588f", +"avp c #4e5c7e", +".fr c #4e60af", +".pM c #4e6ca7", +".pN c #4e73b8", +".#O c #4e73bc", +".fU c #4e73be", +".#U c #4e74bc", +".iK c #4e74be", +".NU c #4e75bc", +".Pd c #4e76b3", +".NV c #4e76bc", +".K9 c #4e77b9", +".La c #4e77ba", +".Mw c #4e77bc", +".K8 c #4e78b9", +".Mr c #4e78ba", +".Mu c #4e78bb", +".Mv c #4e78bc", +".GP c #4e78bf", +".GQ c #4e79c0", +"#Z6 c #4f031d", +"#Uj c #4f0b1c", +"#aq c #4f1f07", +"a.# c #4f2513", +"#6E c #4f2615", +".ky c #4f2813", +"#.T c #4f2814", +".wy c #4f300c", +".6G c #4f3148", +".TY c #4f4359", +"aoE c #4f444a", +".dP c #4f4458", +".yT c #4f4965", +"aGR c #4f608b", +".E0 c #4f6aa1", +".aO c #4f6ba1", +".dX c #4f6daf", +"Qt1 c #4f6fa8", +".on c #4f70b3", +".bj c #4f75be", +".bk c #4f75bf", +".QY c #4f76bd", +".Pp c #4f77bd", +".Mt c #4f78bb", +".NR c #4f78bc", +".NQ c #4f78bd", +".Mq c #4f79ba", +".NM c #4f79bb", +".Ms c #4f79bc", +".Pm c #4f79bd", +".op c #4f79ca", +"#9O c #500409", +"#.Z c #501200", +"#3E c #501500", +"#.L c #50291e", +".mb c #502a14", +".7B c #502e23", +"#DQ c #503d36", +"aHl c #503f46", +".oa c #504330", +".81 c #504451", +"#x0 c #504665", +"aEz c #50494e", +"ap6 c #504c52", +"ai7 c #504d54", +"#Yi c #505b79", +".gY c #505e96", +".aH c #506596", +".cj c #506699", +".Jx c #506dae", +".ep c #5075bf", +".bl c #5075c0", +".K7 c #5075c1", +".bd c #5076bd", +".#P c #5076be", +".Pf c #5077b8", +".em c #5077ba", +".QX c #5077be", +".Pq c #5078be", +".NG c #5079b9", +".NP c #5079bc", +".QR c #5079bd", +".Pn c #5079be", +".NL c #507abb", +".NK c #507abc", +".NO c #507abd", +".Po c #507abe", +".Ig c #507ac1", +".5d c #507dbb", +"#XQ c #510900", +"#.1 c #511000", +".9v c #511305", +"acy c #512320", +".OC c #512519", +".dm c #513217", +"aPM c #513327", +".BB c #513d28", +".Bx c #51442d", +"#DV c #514456", +".5. c #515d7b", +".aM c #516596", +".fs c #5165b3", +".ft c #5167b5", +"Qt7 c #5171aa", +".K3 c #5174b8", +".eq c #5176c0", +".#T c #5177bd", +".#Q c #5177be", +".cU c #5177c0", +".cV c #5177c1", +".#R c #5178bb", +".en c #5178bc", +".QZ c #5178bf", +".QK c #5179b7", +".So c #5179bf", +".TV c #5179c0", +".Pe c #517ab7", +".VF c #517aba", +".NJ c #517abc", +".NN c #517abd", +".QS c #517abe", +".QT c #517abf", +".Pj c #517bbc", +".Pi c #517bbd", +".Pl c #517bbe", +".Sm c #517bbf", +"#L4 c #520300", +".73 c #52130a", +"#.Q c #521d09", +".6a c #521e07", +"#jG c #522512", +".6D c #522928", +".ai c #523318", +".jA c #524140", +"aK. c #52424a", +".uN c #52473d", +".vw c #524756", +"aGZ c #52484c", +"#c8 c #524a64", +".85 c #524b5b", +".sO c #52506c", +"#3V c #525f79", +"#9# c #52648b", +".mX c #526ba1", +".io c #526ca3", +".K2 c #526fb0", +".mZ c #5272b4", +".Vl c #5275b6", +".cl c #5276b8", +".er c #5277c2", +".#S c #5278bd", +".eo c #5278be", +".be c #5278bf", +".cT c #5278c0", +".cW c #5278c2", +".JA c #5279bf", +".Sn c #5279c0", +".VE c #527abb", +".Mo c #527abd", +".K5 c #527abf", +".Q0 c #527ac0", +".Sp c #527ac1", +".VD c #527bbb", +".Pk c #527bbe", +".QU c #527bbf", +".QV c #527bc0", +".QJ c #527cb7", +".Ph c #527cbd", +".QP c #527cbe", +".QQ c #527cbf", +".QW c #527cc0", +"#P6 c #530a1a", +"#Et c #531705", +"#0X c #53180d", +"#0O c #531d13", +".nF c #532d15", +".i4 c #533419", +"axn c #533a30", +".xw c #534846", +".7f c #534855", +"arg c #535256", +"#Cq c #535666", +".mY c #536eaa", +".lj c #5375bb", +".JC c #5377c4", +".bf c #5379bd", +".bi c #5379bf", +".cP c #537abd", +".TT c #537ac1", +".VC c #537bbc", +".TU c #537bc2", +".TK c #537cb3", +".QI c #537cb6", +".Sg c #537cb8", +".Vw c #537cbb", +".TS c #537cc0", +".TR c #537cc1", +".TL c #537db4", +".Sf c #537db6", +".QO c #537dbe", +".QN c #537dbf", +".TQ c #537dc1", +".6O c #5380bf", +".5e c #5383c1", +"#8d c #540007", +".OE c #54271c", +".OD c #54281c", +".Zu c #543424", +".Tz c #544040", +".zQ c #544233", +".xa c #544d65", +".ak c #54567c", +"axR c #54668b", +".in c #546995", +".pL c #546c9b", +".ol c #546c9d", +".lh c #5470ac", +".W9 c #5477b8", +".Mm c #5477ba", +".X# c #5478b3", +".bg c #547abe", +".bh c #547abf", +".cS c #547ac0", +".QL c #547bbd", +".cQ c #547bbf", +".TW c #547bc2", +".Xh c #547cbb", +".VB c #547cbc", +".Sq c #547cc3", +".Se c #547db6", +".Vv c #547dbc", +".VA c #547dbd", +".Sl c #547dbf", +".QM c #547ebf", +".Sk c #547ec0", +"#X3 c #551506", +"#XX c #551904", +".9t c #551b0c", +"aLy c #552330", +".OQ c #55261b", +"#ah c #55281f", +"aLN c #553d4e", +"aIL c #55434c", +"ahR c #554843", +"aLf c #55484f", +".Hf c #554a4c", +".dM c #554b5f", +"#dk c #554e5e", +".sN c #555758", +".lg c #5570aa", +".Ml c #5572b0", +".TI c #5574a7", +".NE c #5578b8", +".Vn c #557ab4", +".NI c #557ac2", +".cR c #557bc0", +".Sh c #557cbc", +".Vz c #557dbe", +".VH c #557dbf", +".Vs c #557ebb", +".Xg c #557ebc", +".Vu c #557ebd", +".Vy c #557ebe", +".VG c #557ebf", +".Sj c #557ec0", +".Xd c #557fbc", +".Si c #557fc0", +".TP c #557fc1", +".8o c #5587bc", +"aNw c #560a13", +"#LF c #560b0f", +"aNy c #560b1b", +".74 c #56140b", +".9u c #561a0b", +".55 c #561d0c", +"#5l c #56200c", +"#.S c #562d18", +"aeo c #563726", +"aNu c #56392a", +".XU c #563b2c", +".ar c #564341", +"QtN c #56443c", +".zX c #564628", +".fe c #56464d", +".DU c #564c80", +"aAv c #564f53", +".Cl c #565165", +"aB8 c #565561", +"agn c #566790", +".wI c #566b8e", +".g0 c #566cac", +".GD c #5670a3", +".g1 c #5671b6", +".ND c #5674af", +".TJ c #567bb2", +".0h c #567cb9", +".Pg c #567cc0", +".TX c #567dc4", +".Xb c #567eb5", +".0o c #567ebd", +".Xj c #567ebe", +".Xi c #567ebf", +".VI c #567ec0", +".TM c #567fb9", +".Vr c #567fbc", +".Vt c #567fbd", +".Vx c #567fbe", +".TO c #567fc1", +".0j c #5681ba", +".8n c #5689bb", +"##k c #571002", +".9Q c #571105", +"#X4 c #571401", +"#XZ c #571f0d", +".dt c #572806", +"#FU c #572b27", +".o3 c #573116", +".8# c #57342c", +".zP c #574349", +".zL c #574636", +"aoD c #574c51", +"amk c #574c58", +"#7g c #575359", +".oC c #575369", +".Pb c #5775ae", +".Sc c #5776aa", +".tT c #5778ad", +".0f c #577aba", +".Pc c #577bb8", +".Vm c #577bba", +".X. c #577cbb", +".0g c #577cbc", +".5c c #577dbe", +".Vp c #577eb4", +".YN c #577fbe", +".Xk c #577fc1", +".0i c #5780ba", +".Vq c #5780bd", +".Xc c #5780be", +".Xf c #5780bf", +".VJ c #5780c2", +".0m c #5781be", +".1W c #5784bf", +".8l c #5787bf", +"#YH c #58081a", +"##l c #580d00", +"#WG c #581000", +"#Zv c #581502", +"#3e c #58371d", +"aOQ c #583b2f", +"aF5 c #58484f", +".i2 c #584946", +".0C c #584d5b", +"aFV c #584f55", +".kw c #585053", +".nO c #585151", +"aJW c #585158", +"avb c #58565c", +".q7 c #586b90", +".QG c #5876ad", +".1S c #587bba", +".YH c #587db9", +".1T c #587dbd", +".Xa c #587fb6", +".TN c #587fbe", +".YO c #5880c0", +".YP c #5880c1", +".Xl c #5880c2", +".Xe c #5881be", +".YK c #5881bf", +".YM c #5881c0", +".YQ c #5881c1", +".0r c #5881c3", +"a#r c #59040f", +"#XR c #590e00", +"#av c #591400", +".9M c #591803", +"#gx c #591a16", +".9Y c #592e1e", +".9j c #592f1d", +".0# c #594534", +"aLJ c #594555", +"aCo c #59464d", +".D# c #594b3e", +".gB c #59545e", +"ae5 c #59678f", +".K1 c #5971a8", +".om c #5975af", +".Vk c #597ab4", +".Sd c #597eb6", +".1Z c #5981c0", +".YR c #5981c2", +".YT c #5981c3", +".0k c #5982bf", +".0l c #5982c0", +".YL c #5982c1", +".YS c #5982c2", +".VK c #5982c3", +".0s c #5982c4", +".3B c #5988c4", +"#8# c #5a0009", +".9R c #5a1205", +"#au c #5a1501", +".56 c #5a1b0c", +"#3B c #5a200c", +".6b c #5a260f", +"aMu c #5a2635", +"#2. c #5a2a24", +"#k8 c #5a2f10", +".bO c #5a3b20", +"#Is c #5a4753", +".mt c #5a5147", +".Ib c #5a72a4", +".Mk c #5a72a7", +".Jw c #5a72aa", +".W8 c #5a7ab4", +".5b c #5a7cbb", +".QH c #5a7eb8", +".15 c #5a82c2", +".0p c #5a82c3", +".YU c #5a82c4", +".1X c #5a83c0", +".3F c #5a83c1", +".0n c #5a83c2", +".0q c #5a83c3", +".0t c #5a83c4", +".VL c #5a83c5", +".1V c #5a84bf", +".1Y c #5a84c1", +".6P c #5a8ac9", +"#Wn c #5b1000", +"aI3 c #5b1814", +"#Eu c #5b1e06", +".72 c #5b2611", +"#5m c #5b2915", +".ap c #5b3000", +".7y c #5b3323", +".ql c #5b3618", +"aor c #5b4139", +".x7 c #5b4542", +".lK c #5b577a", +"#vh c #5b5e68", +".lf c #5b6f7b", +".mW c #5b72a6", +".ss c #5b739e", +".NC c #5b74a7", +".YF c #5b7ebe", +".Vo c #5b81b8", +".3y c #5b81c1", +".YI c #5b82bb", +".1U c #5b83c0", +".3G c #5b83c2", +".3M c #5b83c3", +".14 c #5b83c4", +".3N c #5b83c5", +".YJ c #5b84bc", +".3C c #5b84c1", +".10 c #5b84c2", +".3E c #5b84c3", +".13 c #5b84c4", +".3D c #5b85c2", +".3A c #5b87c3", +".8m c #5b8ec1", +"#R0 c #5c0d00", +"aGV c #5c4032", +".zN c #5c4847", +".zR c #5c4a3a", +"#eT c #5c535a", +"amG c #5c595d", +".p6 c #5c5b77", +".jp c #5c5d55", +".Pa c #5c75a5", +".QF c #5c76a3", +".3x c #5c7fbe", +".YG c #5c81c0", +".5h c #5c84c3", +".5k c #5c84c4", +".12 c #5c84c5", +".16 c #5c84c6", +".5f c #5c85c2", +".3H c #5c85c3", +".3J c #5c85c4", +".11 c #5c85c5", +".5l c #5c85c6", +".6V c #5c85c7", +".6N c #5c86c5", +"#OE c #5d0812", +"#24 c #5d0a00", +"#az c #5d1703", +"#.Y c #5d200e", +"#1Z c #5d2700", +"#ao c #5d2c13", +".1M c #5d4539", +".3U c #5d5265", +".DT c #5d5483", +".rt c #5d5c7d", +".gX c #5d69a0", +".wH c #5d708f", +".Sb c #5d77a2", +".8p c #5d85c1", +".3I c #5d85c4", +".3L c #5d85c6", +".17 c #5d85c7", +".6Q c #5d86c3", +".5i c #5d86c4", +".5j c #5d86c5", +".3K c #5d86c6", +".9P c #5e1a10", +".Vd c #5e3325", +"#8w c #5e3822", +".dk c #5e4e4b", +".gi c #5e4f4c", +".vZ c #5e5254", +".vs c #5e576c", +"aAu c #5e595b", +"aay c #5e6c8e", +"aca c #5e749b", +".TH c #5e79a3", +".5g c #5e86c5", +".6T c #5e86c6", +".6U c #5e86c7", +".18 c #5e86c8", +".6S c #5e87c6", +".5m c #5e87c9", +".99 c #5e8abf", +"#ht c #5e91d1", +"aNx c #5f0517", +"#Uk c #5f0c1c", +"#X2 c #5f2115", +".9s c #5f2616", +"aNv c #5f312b", +".rI c #5f3a19", +".2J c #5f3e3c", +".S0 c #5f4139", +".hK c #5f504d", +"acW c #5f545a", +".oE c #5f5b71", +"aIn c #5f5d5f", +"#bd c #5f7fbf", +".8G c #5f85c5", +".8t c #5f87c3", +".3O c #5f87c9", +".3z c #5f88c6", +"#.. c #5f8bc0", +"##m c #601202", +"#XS c #601401", +"##i c #601905", +"#Zk c #602312", +".3k c #603016", +".OR c #603225", +"#49 c #603415", +"awZ c #60463c", +".u5 c #604a2c", +"#qF c #60504b", +".mK c #605545", +".k6 c #605647", +"#WN c #606986", +".fq c #606ca8", +".0e c #607eb7", +".8F c #6085c5", +".8u c #6086c4", +".8E c #6086c5", +"#.f c #6086c6", +"#.d c #6087c4", +".6M c #6087c7", +".8q c #6088c4", +".6R c #6089c7", +".3P c #6089ca", +"#Rx c #61091a", +"aKo c #611d08", +"#3F c #612209", +"#5k c #612411", +".OX c #61342e", +".8c c #613b34", +".tL c #614b3c", +".eP c #61524f", +"#dh c #615453", +".EK c #615953", +".gW c #615b84", +".wG c #616d85", +".YE c #6180b9", +".8k c #6186bf", +".8v c #6186c5", +".8D c #6186c6", +".8w c #6187c5", +"#.e c #6187c6", +".8C c #6187c7", +"#.a c #6188c4", +".8r c #6189c5", +"#hs c #618eca", +"#vR c #621400", +".6E c #623c42", +".s5 c #623d1b", +"aHF c #624554", +".wn c #624d33", +"aH4 c #625251", +".gG c #625254", +".Y0 c #626078", +".W7 c #627baa", +".Vj c #627cab", +"##D c #6282be", +".8x c #6287c6", +".8B c #6287c7", +".8y c #6288c6", +".8A c #6288c7", +".8z c #6288c8", +".8s c #6289c5", +"#.b c #6289c6", +"#.c c #628ac6", +"#rD c #628cc6", +"#X5 c #631d05", +"#X0 c #632b1a", +"#FT c #632c18", +"#ap c #63361c", +".8d c #633c36", +".uu c #633f1b", +".va c #634437", +"aJ8 c #63535c", +".8N c #635763", +"aDj c #635d62", +".AN c #635e76", +".pK c #63759a", +"asP c #63799c", +"##E c #6386bf", +"#bj c #6388c7", +"#.g c #6388c8", +"##J c #6389c7", +"##I c #6389c8", +"##K c #6389c9", +"#nu c #638aca", +"##H c #638bc7", +"#fW c #638bc8", +"#rC c #6391cf", +"#fX c #6391d2", +"#s1 c #6393d2", +"#9N c #640711", +"##n c #641603", +"#Zj c #642715", +"#a8 c #642722", +".45 c #643c34", +"#4Y c #643d1a", +".05 c #643f2e", +"aMK c #645160", +"#Iu c #645360", +".2b c #645b6b", +"#IA c #645d6a", +".2k c #645d7b", +"arN c #646066", +"aDo c #64636c", +".ru c #646384", +"axh c #64769c", +"#ba c #647cb9", +"#em c #6485c3", +".6L c #6486c4", +"#bi c #6489c8", +"#bl c #6489c9", +"#bh c #648ac8", +"#cY c #648ac9", +"#bk c #648aca", +"#rG c #648bc6", +"#be c #648bc7", +"#bf c #648cc8", +".98 c #648ec7", +"#N. c #650c0f", +"#KD c #65210f", +"#.3 c #652212", +"#XW c #652710", +".3i c #652712", +".9X c #653a2a", +".9e c #65462d", +".jn c #65504e", +"aj3 c #655a56", +".y1 c #655b7a", +"#5B c #656168", +".Fu c #65638a", +"#7o c #6579a6", +"aiH c #657da8", +".1R c #6582ba", +"#cU c #658ac9", +"#f1 c #658aca", +"#cV c #658bc9", +"#cX c #658bca", +"#cW c #658bcb", +"#bg c #658cc8", +"#cR c #658dc9", +"#kA c #658dce", +"#GG c #6593c4", +"a#q c #660714", +"#WF c #662209", +"#CY c #662c18", +".vS c #664531", +".6I c #66728f", +".im c #667696", +"aOM c #667fad", +".5a c #6681b7", +"#hq c #668abd", +"##G c #668bc7", +"#jb c #668bc8", +"#jc c #668bc9", +"#jd c #668bca", +"#f2 c #668bcb", +"apV c #668cc4", +"#er c #668cca", +"#et c #668ccb", +"#eu c #668ccc", +"#cS c #668dca", +"#oV c #668dcb", +"aoo c #668ec6", +"#cT c #668eca", +"#s0 c #6690cc", +"#38 c #6691d0", +"#8. c #670209", +"#IO c #670b16", +"#VO c #670f1e", +"#Wm c #671f08", +".75 c #67231a", +"#2g c #672d25", +"aN0 c #67493d", +"aJX c #676067", +".uV c #676250", +"aBT c #676267", +"#G6 c #676271", +"aq5 c #676368", +"aFu c #6778a3", +".0d c #677eaa", +"#ky c #6788c3", +"#Ol c #678ac8", +"#rE c #678bc1", +"#ja c #678bc8", +"#kF c #678bc9", +"#je c #678bca", +"#j# c #678cc9", +"#jf c #678cca", +"#es c #678ccb", +"#ev c #678ccc", +"#hw c #678dcb", +"#f3 c #678dcd", +"#eq c #678eca", +"#qd c #678fca", +"#eo c #678fcb", +"anc c #6790ca", +"and c #6791c8", +"#qc c #6791ce", +"#s2 c #6792cc", +"#.# c #6794cc", +"#Hh c #680d1b", +".3f c #682612", +"#e3 c #682b27", +"afX c #683115", +".3j c #68311a", +"aNs c #683326", +".UB c #68362e", +"#3o c #683a1a", +".3r c #684c48", +"aKa c #685760", +".bL c #685962", +".AT c #685c67", +".YD c #6880ae", +"#cP c #6884c3", +"#fU c #6887bd", +"#2A c #6888bc", +"#iX c #6889c5", +"#MS c #688bc9", +"#rF c #688cc4", +"#j. c #688cc9", +"#kz c #688cca", +"#jg c #688ccb", +"#i5 c #688dc7", +"#kE c #688dca", +"#l6 c #688dcb", +"#hv c #688dcc", +"#hx c #688dcd", +"aom c #688ec6", +"#kC c #688ec8", +"al3 c #688fca", +"#ep c #688fcb", +"#GC c #688fce", +"#fY c #6890cc", +"al4 c #6891c8", +"#GF c #6892c5", +"#Im c #6894c7", +"#23 c #690800", +"#SY c #690b1b", +"#cJ c #692219", +"#62 c #692d19", +".9i c #693a28", +"aeH c #693e29", +".xF c #694832", +".ag c #695957", +".8L c #695a75", +".69 c #696160", +".AJ c #696283", +"au7 c #696668", +".xp c #696768", +"#bb c #6984c0", +"#kx c #6988c3", +"#cQ c #6989cb", +"#l0 c #698ac5", +"#GV c #698bc9", +"#GU c #698cc8", +"#GT c #698cc9", +"#Ca c #698cca", +"#zk c #698dc7", +"#AI c #698dc9", +"#i9 c #698dca", +"#DG c #698dcb", +"#l7 c #698dcc", +"#i2 c #698ec8", +"#i8 c #698ecb", +"#nv c #698ecc", +"al2 c #698ecd", +"#i1 c #698fc8", +"#oW c #698fc9", +"akZ c #698fca", +"#Cb c #698fce", +"ap# c #6990c9", +"#fZ c #6990cc", +"#f0 c #6990cd", +"ak0 c #6991c8", +"#hu c #6991cd", +"#iZ c #6991d4", +"#T5 c #6994c8", +"#SE c #6996c8", +"#FA c #6a0f20", +"#Xe c #6a1d20", +"#X9 c #6a270e", +"#X1 c #6a2d23", +".OS c #6a3c2e", +".bV c #6a3d13", +".jx c #6a4e4b", +"arH c #6a5149", +".tC c #6a5637", +"aMG c #6a5b69", +"#bJ c #6a6269", +"adL c #6a80b0", +".3w c #6a87bd", +"#Ym c #6a87d4", +"#iW c #6a88c3", +"#DM c #6a8bc9", +"#DN c #6a8bca", +"#uH c #6a8cc9", +"#Cg c #6a8cca", +"#xU c #6a8dc8", +"#GS c #6a8dc9", +"#GW c #6a8dca", +"#GY c #6a8dcb", +"#GN c #6a8ec5", +"#ut c #6a8ec6", +"##F c #6a8ec7", +"#wm c #6a8ec8", +"#nt c #6a8eca", +"#i7 c #6a8ecb", +"#kD c #6a8ecc", +"#iY c #6a8ecd", +"#i3 c #6a8fc9", +"#i6 c #6a8fcc", +"#oX c #6a8fcd", +"#qi c #6a8fce", +"#DH c #6a8fcf", +"#kB c #6a90c9", +"#l4 c #6a90ca", +"#AJ c #6a90cd", +"ap. c #6a91c9", +"#uu c #6a91ca", +"#Ii c #6a91cf", +"#l3 c #6a91d2", +"#Il c #6a93c7", +"#SF c #6a98cb", +"#4O c #6b0702", +"#Kf c #6b0c12", +"#WH c #6b2000", +"#XT c #6b270d", +"#3A c #6b2916", +"ac1 c #6b2b16", +"#19 c #6b3831", +".Zv c #6b422e", +".se c #6b593c", +"#by c #6b6483", +".ok c #6b7eaa", +"#1a c #6b86ab", +"#uI c #6b8cc9", +"#AO c #6b8cca", +"#AP c #6b8ccb", +"#fV c #6b8dc6", +"asS c #6b8dc7", +"#uG c #6b8dc9", +"#uL c #6b8dca", +"#zl c #6b8dcb", +"ajT c #6b8dcd", +"aqP c #6b8ec3", +"#nr c #6b8ec9", +"#GR c #6b8eca", +"#Cc c #6b8ecb", +"#GX c #6b8ecc", +"#Io c #6b8fc6", +"#GM c #6b8fc7", +"#wf c #6b8fc8", +"#uE c #6b8fc9", +"ajU c #6b8fca", +"#GQ c #6b8fcb", +"#rI c #6b8fcc", +"#rJ c #6b8fcd", +"#qj c #6b8fce", +"ajV c #6b90c8", +"#xO c #6b90c9", +"#i0 c #6b90ca", +"apW c #6b90cc", +"#rK c #6b90cd", +"#rL c #6b90ce", +"#Fa c #6b90d0", +"#T2 c #6b90d2", +"#hr c #6b91c9", +"#rB c #6b91cb", +"#zg c #6b91cd", +"#wg c #6b92cc", +"#MI c #6b94cc", +"#Ob c #6b95ca", +"#T6 c #6b97cb", +"#Ra c #6b97ce", +"#R# c #6b98cc", +"#Z7 c #6c081a", +"#25 c #6c0a02", +"#8c c #6c0a15", +"#Wo c #6c1f09", +"#Vc c #6c2000", +"#aw c #6c2712", +".4U c #6c2a19", +"#61 c #6c2d18", +"#5o c #6c2e14", +"#1p c #6c314e", +"#0E c #6c3512", +".54 c #6c3925", +".3m c #6c442b", +".6F c #6c4a59", +"aLO c #6c5163", +".C7 c #6c5e51", +"abr c #6c6166", +"aB7 c #6c6468", +"av9 c #6c686b", +"ap9 c #6c686e", +".vf c #6c81a3", +"#cO c #6c84c1", +"#el c #6c88c3", +"#Cd c #6c8cc7", +"#Ce c #6c8dc7", +"#DJ c #6c8dc8", +"#uJ c #6c8dca", +"#xV c #6c8dcb", +"#xW c #6c8dcc", +"aiN c #6c8dce", +"anb c #6c8dd0", +"#uF c #6c8eca", +"#uK c #6c8ecb", +"#uM c #6c8ecc", +"#ns c #6c8fc8", +"#zj c #6c8fca", +"#AK c #6c8fcb", +"#GP c #6c8fcc", +"#DI c #6c8fcd", +"#GL c #6c90c7", +"#GK c #6c90c8", +"aaN c #6c90c9", +"#wl c #6c90ca", +"#uD c #6c90cb", +"#Iq c #6c90cc", +"#rM c #6c90cf", +"#PG c #6c91c3", +"#uv c #6c91c8", +"#4l c #6c91c9", +"#i4 c #6c91cb", +"#oS c #6c91cc", +"aqQ c #6c92ca", +"#xP c #6c92cd", +"#JR c #6c92cf", +"#JT c #6c93ca", +"#qg c #6c93cf", +"#JQ c #6c93d0", +"#Vr c #6c94cb", +"#Lh c #6c94ce", +"#T4 c #6c95c9", +"#JU c #6c95ca", +"#MJ c #6c95cc", +"#Oc c #6c96cc", +"#1i c #6c96cd", +"#37 c #6c96d5", +"#SD c #6c97c8", +"#SG c #6c97cd", +"#PK c #6c98cd", +"#8a c #6d0d18", +"#y1 c #6d0e00", +"#ub c #6d1700", +"aCp c #6d2238", +"abv c #6d3b2c", +"#8x c #6d3f29", +".3l c #6d4226", +"aGl c #6d5564", +".5v c #6d5d57", +".2f c #6d6064", +".uW c #6d695a", +".jr c #6d6e65", +".6K c #6d88bd", +"#qb c #6d8dc4", +"#AL c #6d8dc6", +"#DK c #6d8dc9", +"al1 c #6d8dd0", +"akY c #6d8dd1", +"#AM c #6d8ec7", +"#l1 c #6d8ec9", +"#Fc c #6d8eca", +"#uN c #6d8ecc", +"#uO c #6d8ecd", +"#uw c #6d8fc3", +"#ux c #6d8fc4", +"#JS c #6d8fc9", +"#Cf c #6d8fcb", +"#DL c #6d8fcc", +"#uP c #6d8fcd", +"asi c #6d8fcf", +"aiO c #6d90c9", +"#AN c #6d90ca", +"#uz c #6d90cb", +"#GO c #6d90cc", +"#Ip c #6d90cd", +"#Fb c #6d90ce", +"#qe c #6d91c8", +"#wh c #6d91c9", +"#56 c #6d91ca", +"#oU c #6d91cb", +"#Lj c #6d91cc", +"#JV c #6d91cd", +"#4k c #6d92c9", +"#9q c #6d92ca", +"#l5 c #6d92cc", +"#Li c #6d92cd", +"aaM c #6d93ca", +"#rH c #6d93cd", +"#s5 c #6d93ce", +"#en c #6d93d5", +"aug c #6d93dd", +"#Vq c #6d94cc", +"#Vt c #6d95cb", +"#MK c #6d95cd", +"#Rb c #6d95d0", +"#Vs c #6d96cc", +"#PI c #6d97cb", +"#Od c #6d97ce", +"#PJ c #6d98cc", +"#PL c #6d98cf", +"#R. c #6d99cc", +"#4P c #6e0000", +"#P7 c #6e0f19", +"#1q c #6e1933", +"#vS c #6e2203", +"#KA c #6e2d03", +"#XV c #6e2d15", +"#FS c #6e2f10", +".UA c #6e413e", +".9W c #6e4434", +"aNt c #6e4939", +"aFz c #6e5244", +".sl c #6e5a4b", +".FE c #6e6462", +"#Lu c #6e6773", +".jf c #6e677c", +"aAt c #6e6a6b", +"as2 c #6e6a70", +"#DW c #6e727e", +"auP c #6e7da0", +"akT c #6e7e9b", +".1Q c #6e83ae", +"#fT c #6e84b0", +"#C# c #6e8bc2", +"#DF c #6e8bc4", +"#bc c #6e8bc8", +"#AH c #6e8cc2", +"adU c #6e8ccc", +"agt c #6e8dce", +"ajS c #6e8dd1", +"#PE c #6e8eba", +"#nq c #6e8ec8", +"#Fd c #6e8eca", +"ahG c #6e8ecb", +"aiM c #6e8ed1", +"#wi c #6e8fc5", +"#zh c #6e8fc7", +"ahH c #6e8fc9", +"#Ik c #6e8fca", +"#Ij c #6e8fcb", +"#wn c #6e8fcd", +"#uQ c #6e8fce", +"#wj c #6e90c5", +"#4m c #6e90cb", +"#Fe c #6e90cd", +"#wo c #6e90ce", +"arC c #6e90d1", +"#uy c #6e91c7", +"#oT c #6e91c9", +"#Lk c #6e91cb", +"#uA c #6e91cc", +"#Lm c #6e91cd", +"#Ok c #6e91ce", +"#Ir c #6e91cf", +"#GH c #6e92c9", +"#In c #6e92ca", +"#xQ c #6e92cb", +"#uC c #6e92cc", +"#PR c #6e92ce", +"#l2 c #6e92cf", +"#Vu c #6e93c9", +"a.7 c #6e93ca", +"#qh c #6e93cd", +"#oR c #6e93cf", +"#4j c #6e94ca", +"#55 c #6e94cb", +"#T7 c #6e94cc", +"#SH c #6e94cd", +"#ML c #6e95ce", +"#Q7 c #6e96c2", +"#Q6 c #6e97c6", +"#4d c #6e97cb", +"#5Y c #6e97cd", +"#4e c #6e98cb", +"#5Z c #6e98cd", +"#Oe c #6e98d0", +"#1B c #6f0100", +"a#p c #6f0c19", +"#2T c #6f1719", +"aCq c #6f233a", +"#Y. c #6f290c", +".9S c #6f2918", +"#Hy c #6f2b02", +".46 c #6f4b49", +".zm c #6f4c32", +".yr c #6f7281", +"#ej c #6f7eb0", +"#lZ c #6f8ac1", +"#F# c #6f8cc5", +"#us c #6f8dbf", +"adT c #6f8dcc", +"afd c #6f8dce", +"#Ih c #6f8ebb", +"agu c #6f8ecb", +"adS c #6f8ecc", +"ahF c #6f8ed1", +"afc c #6f8ed2", +"ao9 c #6f8fc7", +"#xR c #6f90c6", +"#xS c #6f90c7", +"#GE c #6f90cb", +"#9r c #6f90cc", +"#uR c #6f90ce", +"#uS c #6f90cf", +"#zi c #6f91ca", +"#Oa c #6f91cc", +"#uT c #6f91cf", +"#wk c #6f92c9", +"#uB c #6f92cd", +"#W5 c #6f92ce", +"#JW c #6f92cf", +"#GJ c #6f93ca", +"#MR c #6f93cb", +"#Ll c #6f93cc", +"#U. c #6f93cf", +"#s3 c #6f94c9", +"#9p c #6f94cb", +"#s6 c #6f94ce", +"#MM c #6f94cf", +"#T3 c #6f94d4", +"#Yu c #6f95c7", +"a.6 c #6f95cb", +"#54 c #6f95cc", +"auT c #6f95de", +"#9l c #6f96ce", +"#PM c #6f96d1", +"#4c c #6f97cc", +"#SB c #6f97ce", +"#9m c #6f98d0", +"#50 c #6f99ce", +"#7C c #6f99d0", +"#Q9 c #6f9acc", +"#6w c #700005", +"#79 c #70030c", +"#L3 c #702202", +"#X6 c #70280c", +"aMv c #703746", +".XW c #704436", +".47 c #705158", +".BE c #705c47", +"ak1 c #706166", +".jB c #706263", +"aIq c #706e70", +"QtG c #707197", +"aoi c #707bba", +"#DD c #7086b3", +"aqN c #7087b8", +"#zf c #708dc2", +"afe c #708dcb", +"#we c #708ec0", +"#JP c #708ec7", +"aci c #708ecd", +"apU c #708fc5", +"agv c #708fc9", +"ach c #708fce", +"#1c c #7090c7", +".97 c #7090c9", +"#GD c #7090cd", +"#57 c #7091cd", +"adP c #7091cf", +"#7J c #7091d0", +"#xT c #7092ca", +"agr c #7092cc", +"#4n c #7092ce", +"avs c #7092d4", +"#MN c #7093cf", +"#Vz c #7093d0", +"#WZ c #7094c7", +"#MQ c #7094cb", +"#qf c #7094cc", +"#W4 c #7094d0", +"#PH c #7095c5", +"#PF c #7095c6", +"#WY c #7095c9", +"adR c #7095cc", +"#7y c #7095ce", +"#WX c #7096cc", +"#9o c #7096cd", +"#WV c #7096cf", +"#5Q c #7096d0", +"af# c #7096d2", +"#1h c #7097cf", +"#Of c #7097d1", +"#7B c #7098d0", +"#OF c #710c10", +"#LG c #710d10", +"#9H c #711215", +"##g c #711f07", +"#Zi c #712911", +"#5j c #712e1a", +".4S c #713013", +".OT c #714233", +"aJs c #714942", +".Yy c #71533e", +".rV c #715849", +".zK c #71614a", +".30 c #71656f", +"aHT c #716973", +"#eU c #716a79", +"aIp c #716f71", +"#Vj c #717692", +"#xN c #718ec2", +"aff c #718ec9", +"al0 c #718ed2", +"akX c #718ed3", +"#7N c #718fce", +"#9s c #718fcf", +"aiL c #718fd3", +"#7L c #7190ce", +"#7M c #7190cf", +"ahE c #7190d3", +"afb c #7190d4", +"#MP c #7191ce", +"ahD c #7192cb", +"#MO c #7192ce", +"ags c #7192cf", +"acg c #7192d0", +"afa c #7192d2", +"#33 c #7193ca", +"#7w c #7193cb", +"ash c #7193cd", +"#53 c #7193d1", +"av0 c #7193d3", +"#W0 c #7194c7", +"#7E c #7194cb", +"#SL c #7194d0", +"awV c #7194d4", +"#Sz c #7194d7", +"#PQ c #7195cc", +"#Vy c #7195cd", +"#7x c #7195ce", +"#WU c #7195d0", +"#WT c #7195d1", +"#WW c #7196ce", +"avt c #7196de", +"#Q5 c #7197c6", +"#7K c #7197cd", +"adQ c #7197ce", +"apa c #7197d1", +"#ZU c #7198cc", +"#4b c #7198ce", +"aon c #7198d0", +"#SA c #7198d4", +"#5X c #7199d0", +"#SC c #719bcc", +"#2H c #719cd5", +"#VP c #720d1c", +"#6C c #721819", +"#To c #72270a", +".3h c #72301c", +"#5n c #72361c", +"#am c #72361f", +".9V c #72371d", +".XV c #72493d", +".Ry c #724d3e", +"aDv c #72666c", +".zd c #726866", +".YX c #726870", +".te c #726a55", +"##M c #726b7f", +"#sZ c #728cbb", +"a.9 c #728fcf", +"aiJ c #7290cb", +"aue c #7290cc", +"a.8 c #7290cf", +"#6. c #7290d0", +"#MH c #7291d2", +"#GB c #7292b8", +"#58 c #7292cf", +"#O# c #7293cb", +"aaK c #7293d0", +"a.5 c #7293d1", +"aaL c #7293d2", +"#Vv c #7294ca", +"#SK c #7294cc", +"aaJ c #7294d0", +"#Og c #7294d1", +"#Rc c #7294d2", +"#Vp c #7294de", +"#s4 c #7295cc", +"#Vx c #7295cd", +"#34 c #7295cf", +"#Yn c #7295d1", +"auS c #7295d9", +"#GI c #7296cd", +"#W3 c #7296ce", +"av1 c #7296db", +"#Yt c #7297ca", +"#Q8 c #7298c1", +"#2G c #7299d3", +"#5R c #7299d4", +"#7A c #729ad2", +"#7D c #729cd3", +"#0f c #730202", +"#1E c #730307", +"#Kh c #730b07", +"a#o c #730f1c", +"#sE c #731d04", +".OW c #73433c", +".91 c #734436", +".OU c #734536", +"aJ# c #735468", +"aww c #735950", +".uP c #73602c", +"#AS c #736368", +"QtA c #736682", +".D3 c #736772", +".kP c #736a60", +"apq c #736a6f", +"aJY c #736c73", +".j7 c #736f96", +"aIo c #737173", +".96 c #737eaa", +".le c #738590", +"#9c c #738ab7", +"asf c #7390bc", +"ajR c #7390d2", +"aiK c #7391ce", +"#59 c #7391d0", +"#Lg c #7391d2", +"#Vn c #7391d9", +"a.V c #7392c2", +"#SI c #7393cf", +"#4o c #7393d0", +"asg c #7394c7", +"#ZO c #7394cf", +"adO c #7394d1", +"#PN c #7394d2", +"#W1 c #7395cd", +"#4g c #7395ce", +"#4h c #7395cf", +"#2F c #7395d0", +"#Yo c #7395d1", +"#52 c #7395d2", +"#4i c #7395d3", +"#ZV c #7396cd", +"#1j c #7396ce", +"aEa c #7396da", +"aCV c #7396db", +"az. c #7396de", +"#Oj c #7397ce", +"#W2 c #7397cf", +"aBw c #7397dd", +"aAg c #7397de", +"#4a c #7398d0", +"aqR c #7398d4", +"#7z c #7399d1", +"#36 c #739bd8", +"#1C c #74000a", +"#LH c #740205", +"#Kg c #74020a", +"#IP c #740210", +"#YI c #74131c", +"#Nx c #741903", +"#RZ c #741f00", +"#t1 c #742408", +"#gz c #742611", +"aCr c #74263d", +"#I7 c #742f00", +"#bU c #744139", +".7x c #744837", +"aGz c #745361", +".uH c #745b56", +".tk c #746059", +"#6g c #746a62", +"#.C c #746b72", +"#bA c #747097", +"QtQ c #74769a", +".sr c #74829d", +"#N9 c #748bb2", +"#C. c #748bbd", +"aok c #748cc7", +"ana c #748ed1", +"ajP c #748fcb", +"#Oi c #748fd0", +"akW c #748fd1", +"ajQ c #7490ce", +"ahB c #7491c3", +"#Oh c #7491d0", +"aaF c #7492c5", +"#2B c #7492c7", +"#4q c #7492d2", +"#1b c #7493c1", +"a.W c #7493c4", +"aol c #7493cd", +"#4p c #7493d2", +"atq c #7494c5", +"agq c #7495cd", +"#T8 c #7495cf", +"#Yp c #7495d0", +"#7I c #7495d2", +"#T1 c #7495da", +"#7v c #7496cd", +"#4f c #7496ce", +"#Yq c #7496cf", +"a.4 c #7496d0", +"#39 c #7496d1", +"#51 c #7496d2", +"a.3 c #7497cf", +"#5P c #7497d0", +"arA c #7497d1", +"arB c #7497d5", +"#ZW c #7498cf", +"#Yv c #7498d0", +"#9k c #749ad1", +"#5W c #749ad2", +"#35 c #749ad5", +"#Na c #750301", +"#4N c #750605", +"#t0 c #752307", +"aCs c #75253d", +"#b8 c #752811", +"#E3 c #753112", +"#3G c #753118", +".3g c #75321f", +"#0Y c #753325", +"#Ja c #753c23", +".52 c #754931", +"ajp c #754f40", +".AY c #755032", +".8b c #75514a", +".wz c #755732", +"#cM c #7582b8", +"ahA c #758bb7", +"#cN c #758bc7", +"#AG c #758cbd", +"#DE c #758cbf", +"#kw c #758cc3", +"#ur c #758dba", +"#ek c #758ec7", +"akU c #758ecb", +"#5K c #758fc3", +"akV c #758fce", +"#E9 c #7590c0", +"asQ c #7591bb", +"auR c #7592cd", +"#WR c #7592df", +"aaG c #7593c8", +"axS c #7593cc", +"#32 c #7594cb", +"atr c #7595d7", +"asR c #7596c7", +"axT c #7596d4", +"a.2 c #7597cf", +"#7F c #7597d0", +"#4. c #7597d1", +"#9n c #7597d2", +"#Yr c #7598cf", +"#4# c #7598d1", +"auf c #7598de", +"#Ys c #7599cd", +"a.0 c #7599cf", +"aFw c #7599db", +"a.1 c #759ad1", +"#5V c #759ad3", +"#0g c #76000b", +"#Hi c #760214", +"#OH c #760400", +"#Ul c #760c1b", +"#N# c #760d0d", +"#xx c #761500", +"#sG c #761902", +"#vZ c #762004", +"#Vb c #762d10", +"ah9 c #763213", +".6p c #763417", +"#XU c #76341a", +"#0N c #763f33", +".4Z c #76402b", +"#Ep c #76461c", +"#N4 c #764737", +"aid c #764a36", +"azB c #766c79", +"avM c #76737a", +".Ce c #767383", +"#b# c #7685be", +"aoj c #7686c4", +"awS c #7689b0", +".5# c #7689b1", +"#iV c #768bc1", +"#5I c #768cb8", +"avq c #768cba", +"#F. c #768cc0", +"an. c #768dca", +"#wd c #768ebb", +"#ze c #768ebd", +"alY c #768eca", +"an# c #768ecd", +"alZ c #768ece", +"#oQ c #768fc2", +".jI c #7691bc", +"#PO c #7692d3", +"arz c #7694c3", +"aqO c #7694c7", +"#Sy c #7694da", +"aaH c #7695cb", +"#WS c #7695e5", +"#Vw c #7696cd", +"#ZP c #7696d1", +"aws c #7696d4", +"#7H c #7697d3", +"#Vo c #7697e1", +"aaI c #7698d0", +"adN c #7698d1", +"#7G c #7698d2", +"#ZT c #7699d0", +"#9i c #769ad0", +"#5S c #769eda", +"#FB c #770317", +"#Q. c #770500", +"#6x c #77050d", +"#9M c #770a18", +"aCx c #77294b", +"#Zh c #772f17", +"#3z c #77301c", +"#as c #773420", +"#6D c #773a30", +"#Eo c #77411b", +".Wo c #77463a", +".uz c #77494a", +"aHL c #775a73", +".z2 c #775e42", +".u1 c #776141", +"aGI c #77635e", +".wf c #776545", +".qS c #77664d", +"##7 c #776a60", +"#.u c #77707c", +"#J3 c #77717f", +"aAQ c #77727e", +"asv c #777379", +".pC c #777469", +"aoP c #77747a", +"aJh c #77757b", +"#TX c #777da3", +".3v c #778ab4", +".8j c #778ab5", +"#WP c #778cc5", +"auQ c #778dbd", +"##C c #778dc7", +"ae8 c #778ebe", +"#xM c #778fbd", +"acf c #7790c3", +"#3Z c #7792c8", +"ae9 c #7793c7", +"#T0 c #7793d8", +"aaE c #7794c5", +"#2E c #7794cf", +"ats c #7795dc", +"#5N c #7796cb", +"axj c #7796d0", +"a.X c #7797c9", +"ahC c #7797ca", +"asj c #7797da", +"#5O c #7798ce", +"#9j c #779bd2", +"#5U c #779bd5", +"#6v c #780007", +"#1A c #780201", +"#LI c #780402", +"#4U c #780c0b", +"#P8 c #780c10", +"#Xf c #780d19", +"#dE c #782309", +"aCu c #78243d", +"#Vd c #782d09", +"ag5 c #782f10", +"#Zz c #78371d", +"#CU c #78421c", +"##t c #784225", +".06 c #784e36", +".Wm c #784f49", +".y. c #78634f", +"#PA c #786667", +"##L c #78727d", +".sE c #787474", +"#N7 c #78767e", +".oK c #7876a2", +"#B9 c #7885ab", +"#TY c #7885b8", +"aqM c #7887b7", +"acb c #788eb8", +"awT c #7892c5", +"awr c #7892c8", +"#Rd c #7892d5", +"#WQ c #7892d7", +"avZ c #7894cc", +"avr c #7894ce", +"#5M c #7895c8", +"#1d c #7896ce", +"#1g c #7896cf", +"awU c #7897d3", +"asT c #7897dc", +"#ZS c #7898d0", +"#9h c #789bd1", +"#YQ c #790304", +"#OG c #790b09", +"#8b c #791421", +"aCt c #79273f", +"#e5 c #792e18", +"#Zg c #79331b", +"#60 c #793620", +"#Tp c #793627", +"#5p c #79381e", +"#.P c #793e1d", +"#63 c #79402d", +"##u c #794125", +".8e c #79504b", +"aML c #796374", +".82 c #796c6a", +"aJZ c #797379", +"apn c #79757b", +"#Le c #7985ad", +"ao7 c #7989c4", +"ary c #798fba", +"#PP c #7990d3", +"#Lf c #7991c9", +"#Vm c #7992d3", +"#hp c #7993bd", +"#Yl c #7993d9", +"#Q4 c #7994bd", +"#MG c #7995d0", +"#SJ c #7995d4", +"aId c #7996ce", +"aGS c #7996cf", +"aFv c #7996d0", +"#31 c #7997cd", +"#ZR c #7997d1", +"#ZQ c #7998d2", +"a.Z c #799cd0", +"aGT c #799ddf", +"#YR c #7a010c", +"#RC c #7a0401", +"#P9 c #7a0805", +"#26 c #7a0907", +"#v0 c #7a1900", +"#t2 c #7a2c0f", +"afP c #7a2d0d", +"#X7 c #7a3011", +"#8J c #7a3620", +".P4 c #7a3b2b", +".9N c #7a3e2a", +"#an c #7a432c", +".hQ c #7a4e1c", +".W3 c #7a503d", +".43 c #7a5343", +"aM6 c #7a5c50", +"aHU c #7a6971", +".Y4 c #7a728c", +".vn c #7a7388", +"#Cp c #7a7e8c", +"#qa c #7a87af", +"#np c #7a8ab8", +"#9a c #7a8db6", +"#rA c #7a8fbc", +"#TZ c #7a8fcd", +"aud c #7a91c1", +"#9d c #7a94c2", +"#7r c #7a94c3", +"#5L c #7a96c9", +"aE# c #7a96d2", +"#O. c #7a98ca", +"#7u c #7a9acf", +"a.Y c #7a9ccf", +"#5T c #7a9dd7", +"#1D c #7b0b13", +"aCv c #7b253f", +"##o c #7b2f19", +"aGF c #7b2f2f", +"a#n c #7b3233", +"#Zw c #7b331d", +".76 c #7b372a", +"#.4 c #7b3828", +".9U c #7b3d25", +"#Pz c #7b4d40", +".Zw c #7b503b", +"#L# c #7b522d", +".3n c #7b533a", +".3o c #7b533c", +"aKC c #7b5f71", +".zD c #7b6a52", +".bK c #7b6e89", +"ama c #7b7270", +"#3R c #7b777e", +".jo c #7b7c74", +"#Vl c #7b8dc4", +"#9b c #7b90bb", +"avY c #7b90bc", +"#Sx c #7b91d4", +"apT c #7b92c8", +"axi c #7b95c6", +"atp c #7b97bf", +"#9f c #7b97c8", +"aCU c #7b97d4", +"#T9 c #7b98d4", +"aNX c #7b9bd0", +"#YU c #7c0102", +"#0e c #7c0202", +"#S3 c #7c0302", +"#22 c #7c0704", +"#0h c #7c0a12", +"#Ry c #7c0b19", +"#1r c #7c0d1f", +"#b9 c #7c2f18", +"#a7 c #7c350f", +".6o c #7c351a", +"#Dx c #7c3819", +"#E# c #7c4149", +".53 c #7c4e37", +".8g c #7c524d", +".Cw c #7c5633", +".jw c #7c605b", +".u8 c #7c624c", +"aKF c #7c6974", +"#3W c #7c8dae", +"aE. c #7c8dba", +"#JO c #7c90bf", +"#Re c #7c91d6", +"#7q c #7c94c3", +"aJD c #7c95c3", +"#30 c #7c98d0", +"#1f c #7c98d2", +"aON c #7c9cd1", +"af. c #7c9cd4", +"#9g c #7c9ed4", +"#Uu c #7d0302", +"#Xm c #7d0305", +"#O4 c #7d1700", +"aCw c #7d2640", +"#Wp c #7d2e19", +"#5i c #7d3621", +"#65 c #7d3a1f", +"#8K c #7d3a26", +".3e c #7d3c28", +".UC c #7d4638", +"#JJ c #7d4c2c", +".Wn c #7d5149", +".4n c #7d5956", +"aJi c #7d5d62", +".tH c #7d624a", +"#N5 c #7d6657", +".x6 c #7d685d", +"#Q1 c #7d6e7a", +"#MC c #7d6f68", +"#Iv c #7d6f7c", +"#GA c #7d8fab", +"#MF c #7d92c1", +"#PD c #7d93b6", +"#3Y c #7d95c5", +"aiI c #7d97c4", +"#ZM c #7d97ce", +"#2D c #7d98d1", +"#7s c #7d99ca", +"aBv c #7d99d7", +"#1e c #7d9ad3", +"aM3 c #7d9cd2", +"aIe c #7da1e2", +"#Uq c #7e0303", +"#Xn c #7e030d", +"#78 c #7e0410", +"#YS c #7e0810", +"#IQ c #7e221d", +".7O c #7e2d08", +"#b7 c #7e3119", +"##p c #7e371d", +"#Ib c #7e4426", +"a#S c #7e4938", +"aKG c #7e4956", +".tl c #7e6c65", +"aIK c #7e6c75", +"#.B c #7e7066", +"aIr c #7e7d7f", +"##B c #7e7faa", +"#lY c #7e86b0", +"#Sw c #7e8cc4", +"awq c #7e92bb", +"#ZL c #7e93bd", +"ao8 c #7e96cf", +"aNW c #7e97c5", +"ace c #7e97c8", +"aaA c #7e99c3", +"aaD c #7e9ac8", +"#2C c #7e9ad1", +"#7t c #7e9cd0", +"#RB c #7f0203", +"#VV c #7f0304", +"#0j c #7f0405", +"#6p c #7f0607", +"#6y c #7f0610", +"#Xg c #7f0b16", +"#Va c #7f3a22", +"#Dy c #7f3a28", +"aGq c #7f3a5b", +"#0M c #7f463a", +".Qv c #7f5142", +"#MA c #7f6043", +"#2Q c #7f6667", +".qA c #7f6f67", +".fh c #7f6f76", +"#bH c #7f7270", +".vO c #7f7281", +"aG5 c #7f7679", +".fj c #7f7a75", +"aAs c #7f7b7c", +"aur c #7f7c7f", +"azv c #7f7e87", +"#Ig c #7f92b5", +"a.U c #7f94bd", +"agp c #7f98c9", +"#9e c #7f9bcb", +"aAf c #7f9bda", +"aJE c #7f9ed4", +"aKV c #7f9fd5", +"#0i c #800e12", +"#EF c #80290f", +"#aC c #802a07", +"ag9 c #803314", +"#h5 c #80331f", +"#X8 c #803413", +"#e4 c #803e31", +"#Gv c #804025", +"#Bn c #804b24", +".OV c #804e46", +".eY c #80522f", +"aGn c #805f72", +".pn c #806a4c", +"alf c #80727f", +".8i c #807c8e", +"#WO c #808eb9", +"auc c #808fb4", +".6J c #8092ba", +"ae7 c #8093c0", +"ajO c #8094b9", +"ay9 c #809cdb", +"aPJ c #809fd5", +"#Q# c #810000", +"#4H c #810200", +"#S2 c #810304", +"#73 c #81030c", +"#Nb c #810403", +"#VW c #81040c", +"#OJ c #810708", +"#Xo c #81070e", +"#YK c #810810", +"#VQ c #810a17", +"#SZ c #810b19", +"#Xr c #810d07", +"#Qb c #811016", +"#EQ c #81240b", +"#aU c #81311d", +"aey c #813212", +"#c. c #81351d", +"#8I c #813c25", +".4V c #813e2d", +"#C0 c #81422a", +".gr c #81542b", +".51 c #81553c", +"#La c #815d3e", +"aDw c #81767b", +"arL c #817d83", +"#PC c #81879d", +"#DC c #818b9c", +"#Vk c #818cb5", +"#sY c #818db0", +"aaz c #8190b2", +"#7p c #8198c6", +"aKU c #8199c8", +"aaB c #819cc6", +"#ZN c #819fdd", +"#4T c #820005", +"#Ur c #820309", +"#VZ c #820707", +"#4Q c #82080c", +"#yI c #822905", +"#t3 c #823719", +"#aT c #823824", +"#1R c #823917", +"afG c #825740", +".7u c #82623d", +".u9 c #826653", +".zY c #82694d", +"#di c #82746b", +".dO c #82778b", +"aju c #827e83", +".sX c #8288aa", +"#ME c #828dac", +"#N8 c #828ea6", +"apS c #8292c8", +"a.R c #8293b8", +"aaC c #829dc9", +"#OI c #830200", +"#9L c #830a1b", +"#Z8 c #830c14", +"#YJ c #83181b", +"#dD c #832a14", +"#dC c #833117", +"#b6 c #83311d", +"ajm c #83442b", +"#dr c #834b45", +".4p c #83573d", +".3p c #835c4a", +"#xZ c #837790", +"aC8 c #837c7e", +".K# c #837da3", +"abV c #837e86", +"aBS c #837f83", +"#ho c #838197", +"#Sv c #8389b6", +"ase c #8399c0", +"#Yk c #8399d4", +"acc c #839ac7", +"aQG c #83a3d8", +"#6u c #84000a", +"#Z9 c #840104", +"#Up c #840305", +"#S4 c #840308", +"#4M c #840609", +"#9I c #840616", +"#Xp c #84080d", +"#Xq c #840909", +"#29 c #84110f", +"#t9 c #843516", +"#vQ c #843518", +"#5h c #843d25", +"#dt c #844027", +"ajj c #844528", +"#mw c #844913", +"aha c #844a2c", +"aHR c #844f70", +".50 c #84573f", +"##6 c #847776", +".Hi c #847786", +"aBR c #847f84", +".yV c #847f9b", +".oB c #848096", +"#Su c #8485aa", +"a.T c #8498c1", +"acd c #849ccc", +"#Xl c #850406", +"#RD c #850408", +"#Qa c #850709", +"#4E c #851318", +"#xy c #852508", +"#A# c #852f09", +"#dF c #853117", +"#b5 c #853a23", +"#WE c #853c13", +"#6Y c #853e24", +"#6Z c #853f28", +"#66 c #854026", +"#64 c #854329", +"#bW c #85492e", +"##v c #854c2f", +"#ar c #854c36", +"#6P c #855339", +"aHP c #855575", +".ta c #85574f", +".D8 c #855d35", +"aJf c #856377", +"#Yd c #856b61", +".mx c #85746c", +".dN c #857a8f", +"aAp c #858082", +"ay8 c #8595c6", +"ago c #859ac7", +"#27 c #860206", +"#RA c #860308", +"#0. c #860309", +"#6z c #860311", +"#1z c #860402", +"#VU c #860406", +"#VX c #86060d", +"#6q c #86070a", +"#9J c #860718", +"#Nc c #861414", +"#F8 c #862807", +"##h c #863b25", +"#US c #863c1d", +"a.l c #863e27", +"#3y c #863e28", +"ai# c #863f24", +"#FR c #864418", +"#b4 c #86462b", +".R4 c #86583b", +"av4 c #866c63", +".zZ c #866d51", +".jm c #867372", +".rU c #867966", +".b9 c #86838f", +"#iU c #8685a9", +"#oP c #8689a9", +"ae6 c #8695bf", +"#74 c #87040f", +"#6A c #870412", +"#VY c #87050b", +"#YL c #87070e", +"#YT c #871115", +"#6B c #87121b", +"aGC c #874349", +"#Wv c #87462a", +".9w c #87473a", +"#Zb c #874d31", +".Vc c #875242", +".9h c #875330", +".XX c #875745", +"ahb c #875a43", +"#3d c #875f39", +"akq c #876357", +"aKD c #87687b", +"#ZE c #876e64", +"#ph c #87736a", +".dF c #877e86", +"#E8 c #8796ab", +"#Yj c #8797c3", +"aAe c #8797c8", +"aCT c #8798c6", +"#3X c #879cc5", +"adM c #879ccd", +"aL7 c #879fce", +"#0d c #880403", +"#S1 c #880409", +"#Rz c #880711", +"#9K c #88091a", +"#Um c #880a17", +"#V0 c #881d14", +"#Au c #882c0d", +"#TR c #883c18", +"a.k c #884228", +"#XP c #88422d", +"#bX c #884325", +"aia c #88452a", +"#2h c #88463b", +".9r c #885140", +"#JI c #885530", +".rP c #885b45", +".uG c #885b5d", +".8f c #885f59", +"aEd c #886c5f", +"aJd c #886d7f", +"#.p c #887977", +".xB c #887b8a", +".Hd c #887e7b", +"#ei c #887e9d", +"#J4 c #88808d", +"ad8 c #888180", +"#fS c #88839c", +".Xq c #8883a9", +".rd c #88848a", +".Cf c #888496", +"aKE c #888993", +"#AF c #8889a4", +"#Q3 c #8893b2", +"#5J c #88a0d2", +"aM2 c #88a1cf", +"#1u c #890104", +"#Xh c #890711", +"#aV c #893520", +"ag6 c #894021", +".9T c #894631", +"#15 c #894723", +".mi c #895531", +"aHJ c #895a75", +"aGB c #895c61", +"aso c #896d65", +"ap0 c #897068", +"#XJ c #89736d", +"#bI c #897b71", +"aBE c #898486", +"#q# c #8986a0", +"#kv c #898bb1", +"#JN c #898daa", +"#77 c #8a0414", +"#4I c #8a0505", +"#Us c #8a050c", +"#YV c #8a0d09", +"#72 c #8a1219", +"#iP c #8a2f17", +"#ES c #8a3116", +"#pG c #8a3210", +"#BA c #8a330d", +"#eg c #8a3e2f", +"#t7 c #8a3f20", +".3c c #8a4328", +"#aA c #8a432f", +"#8G c #8a4428", +".1z c #8a4a32", +".4Y c #8a4d3a", +".kE c #8a5536", +"##s c #8a5537", +"#.U c #8a5847", +"#Y9 c #8a5b31", +".2L c #8a5f45", +".07 c #8a5f46", +".2K c #8a614f", +"aGH c #8a625d", +"aGo c #8a6378", +"aHN c #8a6581", +"#MB c #8a705b", +"aKz c #8a7484", +".we c #8a7952", +".h4 c #8a797a", +"#cL c #8a7ea3", +"aJ0 c #8a8288", +"#MD c #8a8894", +"#rz c #8a90b0", +"aBu c #8a9aca", +"#S0 c #8b0711", +"#6n c #8b0c13", +"aI4 c #8b2d3a", +"#ER c #8b3015", +"#E4 c #8b4836", +".Qh c #8b4939", +"#.X c #8b4e3c", +"#CV c #8b592e", +".4q c #8b5f45", +"#B. c #8b616a", +".qX c #8b796c", +"aG0 c #8b8184", +"#b. c #8b83ac", +".33 c #8b849e", +"#wP c #8b8d9b", +"#TW c #8b8dab", +"#4S c #8c0008", +"#2X c #8c0402", +"#YP c #8c0406", +"#0# c #8c0409", +"#Uo c #8c040a", +"aGD c #8c2c36", +"afQ c #8c3f1f", +"#1P c #8c4120", +"#t6 c #8c4222", +"#8H c #8c462c", +".3d c #8c472b", +"#02 c #8c4c32", +"#Ww c #8c4d33", +"aa2 c #8c5554", +"#Bm c #8c5a39", +"#Mz c #8c5d49", +"a#O c #8c8185", +".ij c #8c8193", +".DZ c #8c827f", +"aqo c #8c898f", +"#28 c #8d0005", +"#S5 c #8d040b", +"#75 c #8d0513", +"#1G c #8d0605", +"#1F c #8d1113", +"#EH c #8d3219", +"#EG c #8d341b", +".7N c #8d3d17", +"#Zx c #8d422a", +"#t4 c #8d4324", +".6c c #8d4827", +"##q c #8d482c", +"#UM c #8d491f", +"#Bt c #8d4b31", +".P5 c #8d4f3f", +"#18 c #8d5950", +"#Eq c #8d5b38", +".6z c #8d644c", +"aKw c #8d6e81", +"asY c #8d7167", +"aJb c #8d7586", +"aFY c #8d7d84", +"#N6 c #8d7e77", +".ae c #8d809b", +"#PB c #8d848d", +"#RE c #8e030a", +"#Xk c #8e0408", +"#VR c #8e0812", +"#2U c #8e141a", +"#71 c #8e2428", +"aGw c #8e315a", +"#Zy c #8e4127", +"#TP c #8e4428", +"#WD c #8e451e", +"#5q c #8e4930", +".4X c #8e4d3c", +".7M c #8e4f44", +".7w c #8e5e3a", +".5Y c #8e6965", +"#2r c #8e766c", +"aKA c #8e7889", +"#St c #8e7a7b", +"aF2 c #8e7e85", +".83 c #8e8077", +"aEK c #8e8086", +"#zd c #8e8394", +"aC4 c #8e878a", +"aAq c #8e898b", +"#Q2 c #8e899c", +"a.S c #8ea0c8", +"#6t c #8f020d", +"#VT c #8f0409", +"#YM c #8f050b", +"#6r c #8f060e", +"#21 c #8f0709", +"#Un c #8f0710", +"#0k c #8f0c0a", +"#vI c #8f3311", +"#u. c #8f3e21", +"ag8 c #8f4325", +"a.m c #8f4430", +"a.j c #8f4a2d", +"ac9 c #8f4c29", +"abE c #8f4c2a", +"ajl c #8f5032", +"aGG c #8f514e", +"#Gw c #8f543b", +"#HE c #8f5b3a", +".FN c #8f653a", +".3q c #8f6d61", +"aL9 c #8f7266", +"#Lb c #8f7361", +"#no c #8f879f", +"#1v c #900406", +"#76 c #900515", +"#4R c #900710", +"#EP c #903118", +"#BW c #903416", +"#ss c #903514", +"aGu c #903961", +"#aD c #903b17", +"#4D c #903d3e", +"a#2 c #90482d", +"#UR c #90492a", +"#aS c #904935", +"a#1 c #904c2c", +"aGp c #904d6d", +".4A c #90553f", +"ah2 c #905642", +"#17 c #905a51", +"#CT c #905e3d", +".qs c #906341", +"atx c #90776d", +".Ty c #907b77", +".C8 c #908275", +"aoC c #90858a", +"aJ5 c #90878d", +".AO c #908c9b", +"aIs c #908e90", +"aGE c #91202e", +"#6m c #912c2e", +"#EM c #912d16", +"#kq c #91361e", +"#su c #913a1b", +"afS c #913f20", +"aKp c #914235", +"a#3 c #91442d", +"#t5 c #914829", +"#Wl c #914c34", +"#0K c #914d2e", +".4B c #915640", +".4o c #916451", +".1K c #916b51", +"#.M c #916c56", +".jz c #917c7a", +".gL c #918482", +"aG6 c #91858b", +"ak7 c #918785", +".jj c #918a94", +"aAr c #918c8e", +".mO c #919898", +".il c #919eb6", +"#RF c #92050b", +"#Xi c #92050d", +"#1H c #921108", +"#S7 c #921817", +"#EI c #92341c", +"#F7 c #923913", +"aGs c #924569", +"#fQ c #924632", +"ai. c #924d2f", +"a.i c #924e2e", +".Qg c #925141", +"#Wx c #92533a", +"aeG c #925737", +"#.O c #925f3b", +".2M c #92674c", +".j. c #92682f", +".42 c #926d5d", +"#WL c #92766d", +"avy c #92786f", +".ff c #928289", +"aEM c #92848b", +".ID c #928cb1", +"aw3 c #928f91", +"#VS c #93050d", +"#1y c #930605", +"#2Y c #930606", +"#4L c #93070d", +"#Ut c #930c12", +"#1s c #930e17", +"#Qv c #932705", +"#Qw c #93281a", +"#EJ c #93331c", +"#5t c #934329", +"afR c #934625", +"#8N c #934a2f", +"#0v c #934b20", +"#0L c #934d2f", +"#TS c #935132", +"#8L c #93523e", +".Qf c #935241", +".Qi c #935544", +"#Es c #935947", +"#b1 c #935b3d", +"aep c #936149", +"#Vg c #93766e", +"aul c #937a70", +".7i c #938a92", +"#.h c #938c87", +"axu c #939199", +".B5 c #9391b9", +".oe c #939b94", +"#YO c #940407", +"#Xj c #94040a", +"#6s c #94040f", +"#0c c #940505", +"#6o c #94050f", +"#0a c #940609", +"#4J c #94070b", +"#sF c #943a22", +"#sv c #944122", +".1x c #944a30", +"#6X c #944e32", +"ah8 c #945031", +"aMO c #945067", +".4W c #945140", +".Qj c #945745", +"aCy c #945c76", +"agX c #945f47", +"afY c #946e5a", +"aGA c #94747b", +".0. c #94755e", +".8h c #94807d", +".zW c #948467", +".v1 c #94888b", +".uC c #948a77", +".aw c #948ca8", +"#Ld c #9492a6", +"#YN c #950409", +"#1t c #950507", +"#UT c #95492b", +"#5g c #954f35", +"#B3 c #955132", +"#2l c #95553b", +"#zY c #956442", +"#4X c #956940", +".1L c #957560", +".tq c #958254", +"aHp c #95848c", +"#eF c #958f9e", +"#EO c #96341c", +"#tX c #963918", +"#aX c #964228", +"#gD c #964826", +"afN c #964a29", +"#1O c #964b1e", +"#hm c #964b34", +"#WC c #964d2b", +"#8O c #964d31", +"#8M c #964d33", +".1y c #964e34", +"#zZ c #96603a", +"#Ic c #966147", +".4r c #966950", +".Hm c #966b3e", +".SR c #966f47", +"a#R c #966f61", +"#iT c #967781", +"auY c #967c72", +"#zK c #967e90", +"#lX c #968495", +"#Lc c #968686", +"#zo c #968897", +"aFI c #968a90", +".Hh c #968a95", +"#E7 c #968e85", +"aBJ c #969192", +"af4 c #969296", +"#4F c #970812", +"#S6 c #970c13", +"#O5 c #973227", +"#EN c #97341c", +"#xe c #973e1a", +"#Hj c #97453f", +"#3J c #97462d", +"#2k c #974735", +"#UU c #97492c", +"#0t c #974c24", +"#tK c #974f27", +"adi c #975328", +"a#0 c #975532", +"ac8 c #975630", +"##y c #975b40", +"#Dz c #975d49", +"#b3 c #976041", +".Wp c #97614f", +".nL c #97663e", +"#En c #976644", +".9f c #977450", +"#4z c #978484", +"#oO c #978899", +"aEL c #978990", +".vL c #978c8e", +".dE c #978e95", +"aIt c #979097", +"#Iz c #9791a0", +"#B8 c #9799a3", +"#0b c #980607", +"#1w c #980708", +"aH0 c #983237", +"#y0 c #98371a", +"#At c #98381b", +"#y2 c #983b1d", +"#aW c #98432b", +"abF c #984830", +"#TQ c #984a28", +"#0s c #984d1b", +"ag4 c #984f2f", +"ag7 c #984f30", +"#WB c #985032", +"#0Z c #98503f", +"#WA c #985239", +"#8F c #985435", +".Qe c #985646", +"abD c #985732", +"#ds c #985b4d", +"ajd c #986456", +".5Z c #986a57", +".IL c #987048", +".4k c #98733e", +".jy c #987f7c", +"aBK c #989094", +"aJ1 c #989095", +".2l c #9893b5", +"#2V c #99010c", +"#4K c #99080f", +"aMP c #99354f", +"#sH c #993b25", +"#Ny c #993d36", +"#EE c #994328", +"#L5 c #994940", +"#01 c #994b36", +"#dQ c #994c25", +"#t8 c #994c2d", +"ag3 c #995031", +"#3H c #995037", +"#67 c #995137", +"#3x c #995139", +"#gy c #995348", +".6q c #995c3e", +"#I3 c #99643a", +"#Bo c #996538", +"#cK c #996b75", +"#Q0 c #996c60", +".kT c #998880", +"aHq c #998890", +".fd c #998990", +".xz c #998d94", +"aEm c #998f94", +"#bw c #99909e", +"aJ2 c #999197", +"aIu c #999299", +".Cm c #99969f", +"#1x c #9a0807", +"#2Z c #9a080b", +"#20 c #9a080d", +"#aE c #9a4823", +"#7. c #9a4c30", +"#lc c #9a4d29", +"afO c #9a4d2c", +".Qk c #9a5d4a", +"#.V c #9a5f4e", +"aic c #9a644c", +"#CE c #9a656c", +"#eh c #9a6a6f", +"aac c #9a6f56", +"#JK c #9a7058", +"apd c #9a7e75", +"#Fh c #9a838a", +".r5 c #9a8560", +"aJ9 c #9a8a92", +".xi c #9a8eaa", +"aD# c #9a9196", +"#LJ c #9b3634", +"aKv c #9b385f", +"#fc c #9b3d25", +"#fd c #9b4024", +"ad# c #9b4a31", +"aex c #9b4c2c", +"#at c #9b513f", +".4R c #9b553a", +"#aB c #9b5541", +"#dx c #9b5638", +"#Wu c #9b583a", +"#0J c #9b593a", +".1A c #9b5a42", +".4z c #9b604a", +"#gu c #9b6634", +".xU c #9b866e", +"aIM c #9b8992", +"aLK c #9b8a99", +".kX c #9b8f69", +"#AE c #9b9294", +"aFH c #9b9295", +".8H c #9b9482", +".h1 c #9b949d", +"aBZ c #9b99a3", +".wF c #9b9eae", +"aMW c #9c3941", +"#jM c #9c4926", +"#3K c #9c4931", +"#jL c #9c4b28", +".3b c #9c5238", +".2X c #9c5a44", +"aji c #9c5d3f", +"#k6 c #9c632c", +"#ae c #9c6433", +"##r c #9c6749", +"#5. c #9c684b", +".2D c #9c6c31", +"#FY c #9c6c44", +".Ro c #9c754c", +"#xL c #9c858d", +".k2 c #9c908f", +"aC9 c #9c9398", +"aJ3 c #9c9499", +"aAP c #9c9699", +"ayd c #9c9aa4", +"aMC c #9d2e3f", +"#Ml c #9d4828", +"aGt c #9d4a70", +"a#4 c #9d4b37", +"aew c #9d4f2e", +"#ca c #9d5139", +"#c# c #9d513a", +"adg c #9d532c", +"#Wq c #9d5330", +"#jI c #9d533e", +"#V# c #9d5626", +".2W c #9d5b45", +"#We c #9d612e", +".71 c #9d634d", +".7E c #9d6356", +"#bR c #9d6635", +"ac0 c #9d6c5e", +".9g c #9d704a", +".Yx c #9d745b", +".Ki c #9d764d", +"abQ c #9d7760", +".2E c #9d7845", +".u6 c #9d866a", +"aDN c #9d868f", +".BF c #9d8974", +"aMH c #9d909e", +"#yh c #9d94a4", +"aEy c #9d969b", +"aDW c #9e315e", +"aKu c #9e3b5d", +"#cb c #9e533b", +"#5e c #9e542c", +"ad. c #9e5536", +"#2i c #9e5647", +"#ZA c #9e593b", +"#Wz c #9e5949", +"#bZ c #9e5b42", +"#CX c #9e674c", +"#0w c #9e6942", +".We c #9e6d3c", +".XO c #9e6e3a", +".92 c #9e6e60", +".2N c #9e7156", +"aHK c #9e849c", +".Xn c #9e949c", +"aD. c #9e9599", +"aFU c #9e959b", +"aBL c #9e969a", +"aIF c #9e969c", +"aIv c #9e969d", +"auz c #9e9ca3", +"#2W c #9f0004", +"aE7 c #9f2953", +"aMA c #9f2b33", +"#Gd c #9f3f2c", +"#rh c #9f432c", +"#sr c #9f4532", +"#fb c #9f482b", +"#h9 c #9f4a27", +"#aR c #9f4e38", +"#lk c #9f5125", +"#ch c #9f522b", +"#2j c #9f5242", +"#Y4 c #9f5422", +"#3v c #9f542d", +"aa# c #9f5430", +"afW c #9f5a36", +"#CW c #9f6b46", +".7q c #9f6b4b", +".Ol c #9f6c3a", +".MY c #9f6c3b", +"#Ea c #9f6f81", +"a.x c #9f7059", +"aHO c #9f7492", +".5T c #9f7a44", +".S1 c #9f7b6d", +"aCZ c #9f8375", +"#07 c #9f867c", +"aKy c #9f8798", +"aG4 c #9f9699", +"#OK c #a0363a", +"#F9 c #a04121", +"aeC c #a04925", +"aev c #a05131", +"#bY c #a05338", +"#Wy c #a05b4c", +"a.h c #a05c3b", +"#al c #a05d3f", +"#h0 c #a06730", +".9b c #a06c4c", +"ajo c #a06f5c", +".0Y c #a07038", +"#4W c #a07144", +".p. c #a07148", +"#Ss c #a08179", +".qz c #a08774", +"aJ4 c #a0989e", +"#dj c #a0989f", +".uf c #a09fb8", +"#4G c #a1010e", +"aMB c #a11e31", +"aDO c #a1274c", +"aLS c #a13149", +"#Gc c #a1402d", +"#q5 c #a14725", +"a#7 c #a14b2f", +"#ua c #a14c2f", +"#sD c #a14f35", +"#XA c #a1541a", +"#69 c #a15438", +"#5s c #a1543a", +"#8P c #a1573b", +"adh c #a15b31", +"#16 c #a15d3a", +".2Y c #a15f49", +"#Hw c #a16339", +"#b0 c #a16348", +"#FQ c #a1653c", +"akl c #a1674a", +"#Hv c #a16d46", +"#Sr c #a16f5b", +".Zx c #a1735c", +"aJ. c #a17f94", +"#TU c #a1827a", +"#ku c #a18693", +"aB6 c #a1999d", +".h7 c #a19b95", +"#0l c #a2261c", +"aE8 c #a22a55", +"#EK c #a24029", +"#oj c #a24e2b", +"#3u c #a25430", +"#00 c #a25642", +"#1N c #a25719", +"#e8 c #a25737", +"#0r c #a25815", +"#Hx c #a25c30", +"#a5 c #a25c36", +"#6W c #a25c3f", +"#5f c #a25c41", +"#3# c #a25e4c", +"ac7 c #a26139", +"#14 c #a2613d", +"#ak c #a26646", +"#do c #a26d3b", +".PS c #a26f3d", +"aMN c #a27c8e", +".Us c #a27f56", +"aKB c #a28a9b", +"aJc c #a28a9c", +".8M c #a294aa", +"aMI c #a295a2", +"aG7 c #a2969b", +".2h c #a296a2", +"aj2 c #a29793", +"#wR c #a2989a", +"#If c #a2a1b4", +"aLZ c #a3282d", +"aKs c #a33c50", +"#KV c #a34b2e", +"afT c #a34f2d", +"#oy c #a35030", +"#9G c #a35050", +"a.p c #a35338", +"a.q c #a35438", +"abN c #a35832", +".9L c #a35b42", +"#Ts c #a35e34", +"#dA c #a36540", +"#Ia c #a36747", +"akn c #a3694c", +".LG c #a3703e", +"#CF c #a3788a", +".zS c #a3917e", +".uK c #a39189", +".fc c #a39299", +".zJ c #a39478", +"aDu c #a3979d", +"aIw c #a39ca3", +"aLE c #a41e35", +"aLD c #a42633", +"aFd c #a4416c", +"#pC c #a4432a", +"aKH c #a44655", +"#ET c #a44c30", +"#yH c #a44d28", +"a#8 c #a44f32", +"afU c #a4502a", +"#oz c #a45131", +"#m7 c #a45338", +"aeA c #a45534", +"#du c #a4563c", +"#6U c #a4572f", +"a.n c #a45846", +"#Y3 c #a4591f", +"#3w c #a45d44", +"ah# c #a4603f", +".2Z c #a4634d", +".P6 c #a46556", +".Ql c #a46753", +"#jH c #a46a5c", +"#Ku c #a46c3f", +".7I c #a46c5f", +"#XG c #a46f3c", +"#iS c #a46f64", +".R3 c #a47356", +".08 c #a4765c", +"#fR c #a47776", +".PT c #a47d54", +".Z9 c #a47d60", +".XP c #a48054", +"aKY c #a4867a", +"#JL c #a4867d", +"anf c #a4877d", +"#nn c #a48890", +"#zn c #a49699", +"aG8 c #a4989e", +".IE c #a4a0c7", +".G4 c #a4a2cf", +"aDQ c #a5274e", +"aI9 c #a5497e", +"#Av c #a54d2d", +"#Mm c #a5512e", +"#dG c #a55137", +"#V9 c #a55619", +"afM c #a55838", +"#5r c #a55b42", +"aLR c #a55d73", +"#8E c #a56040", +"abC c #a5633c", +"a#Z c #a5633e", +".Qd c #a56353", +"#XL c #a56850", +"#L. c #a5755d", +".LH c #a57d55", +"#zc c #a59089", +".tM c #a59183", +"aF3 c #a5959c", +"aIE c #a59ca2", +".js c #a5a59d", +".rq c #a5a5b4", +"#Uv c #a64338", +"#pB c #a64729", +"#Aa c #a64c29", +"#Mh c #a64f32", +"abG c #a6503c", +"aeB c #a65232", +"#5u c #a6543b", +"a.o c #a6553b", +"#aY c #a65639", +"#So c #a65836", +"aeF c #a65b34", +"ag2 c #a65d3d", +"#03 c #a66244", +".20 c #a6644e", +"#Br c #a66852", +".7p c #a66d3d", +".Rn c #a67341", +"aGx c #a6738e", +"##z c #a67975", +"#.N c #a67c5a", +".7v c #a67e56", +".w. c #a6906e", +"aHr c #a6959c", +".95 c #a69aab", +"azA c #a69ca8", +"aID c #a69da3", +"#vi c #a69ea2", +"aBQ c #a69fa3", +"aDP c #a72c52", +"aI5 c #a7485f", +"#Nw c #a74b1e", +"#F6 c #a74f29", +"a#6 c #a74f34", +"#Y# c #a75e3a", +"aGr c #a75e80", +"#dB c #a76040", +"#a4 c #a7633c", +"#.W c #a76455", +"#I4 c #a76539", +".6w c #a76b5a", +"#h4 c #a76b5e", +".4C c #a76c56", +"#jD c #a76e37", +".Kh c #a77442", +".Qu c #a77565", +".Ur c #a77747", +"aGy c #a77c92", +".wu c #a78863", +".yg c #a78a69", +"anD c #a79ba1", +"aDa c #a79ea3", +".uU c #a7a088", +"aDR c #a82850", +"aLF c #a83048", +"aKO c #a8312d", +"#Ge c #a84931", +"aKq c #a84a49", +"#xf c #a84d2b", +"#Mi c #a85035", +"#Mk c #a85334", +"#5d c #a85a34", +"#3I c #a85a42", +"#sC c #a85b3f", +"#68 c #a85e43", +"ah7 c #a86345", +".Zp c #a88456", +"#tx c #a89190", +"afp c #a89b96", +"ahX c #a8a5ac", +".h0 c #a8a5b1", +".jv c #a8a8a0", +"aE9 c #a92e5a", +"aMV c #a92f39", +"aKr c #a9434d", +"#EL c #a94730", +"#xz c #a94d2e", +"#KW c #a95133", +"#Mj c #a95137", +"aez c #a95a39", +"#0n c #a95b3b", +"a.t c #a95e3c", +"#KE c #a96252", +"#Zf c #a9664d", +"#z4 c #a9664e", +".2V c #a96751", +"ajk c #a96a4c", +"#Y8 c #a96d33", +"akg c #a96f52", +".7J c #a96f62", +"ako c #a97059", +".Vb c #a97060", +".9a c #a97140", +"a.a c #a9715d", +".9o c #a97365", +".SQ c #a97644", +"#yt c #a97755", +"#h1 c #a9784a", +".Zo c #a97943", +"#s# c #a98161", +"#7Y c #a98178", +"aKx c #a98d9f", +"aHo c #a9989f", +".gO c #a99c9a", +".tu c #a99e86", +"ad7 c #a9a09e", +"aIx c #a9a2a9", +".7j c #a9a2b1", +".ri c #a9a5ab", +"aF. c #aa2b58", +"#ie c #aa4a24", +"#tW c #aa4e2d", +"abJ c #aa4f33", +"#oi c #aa5532", +".7X c #aa5b2b", +"#vT c #aa5d3e", +"aaa c #aa613b", +".Qc c #aa6858", +".ZZ c #aa6c4f", +".IK c #aa7746", +"#hn c #aa7f7b", +".MZ c #aa835a", +"akf c #aa8b81", +".xV c #aa957b", +"aiV c #aa9c97", +".qQ c #aa9d6e", +"#7U c #aa9f94", +"aHf c #aaa1a7", +".y8 c #aaa7af", +".Xr c #aaaacf", +"#uc c #ab4a2d", +"#aO c #ab4e33", +"abI c #ab4e34", +"#y3 c #ab5333", +"#Bz c #ab572f", +"#aQ c #ab5741", +"#1J c #ab5b3b", +"#UV c #ab5c3f", +"#cG c #ab5e33", +"#Y2 c #ab6118", +"a.g c #ab6744", +".6s c #ab6859", +".77 c #ab6959", +"abw c #ab6b55", +".78 c #ab6e5a", +"#e0 c #ab7644", +"#b2 c #ab7757", +".5S c #ab7b3d", +"#a9 c #ab8594", +".Wf c #ab875d", +"aEn c #aba1a6", +"aEl c #aba3a5", +".oD c #aba6bd", +"awA c #aba7aa", +"alC c #aba7ab", +"#G. c #ac4d2f", +"ada c #ac523e", +"##c c #ac5426", +"#kr c #ac583f", +"a.r c #ac5e40", +"#FC c #ac6159", +"#Y6 c #ac632b", +"#Tt c #ac643b", +"#Wt c #ac6647", +"#TO c #ac664e", +"#R3 c #ac6737", +"#OV c #ac6a36", +".Qb c #ac6a5a", +"#sT c #ac6c46", +"#No c #ac6d3b", +"alu c #ac7659", +"#Id c #ac8373", +".0Z c #ac8757", +"asn c #ac8d80", +"aBA c #ac9083", +"aME c #ac96a7", +".qJ c #ac9983", +"#zL c #ac99a8", +".zT c #ac9a85", +"aG9 c #aca0a6", +"aH. c #aca1a6", +"aHh c #aca4aa", +"aus c #aca9ab", +"#KU c #ad523b", +"abK c #ad5537", +"#C6 c #ad5631", +"a#5 c #ad5947", +"aMD c #ad5964", +"adf c #ad5d39", +"#I6 c #ad622a", +"#8Q c #ad6244", +"abz c #ad6431", +"#6V c #ad6439", +".6n c #ad6449", +"#Kz c #ad652f", +"#B4 c #ad6753", +"#LV c #ad7242", +".4y c #ad725c", +"##w c #ad7357", +"#gv c #ad7657", +".XY c #ad7963", +".4j c #ad7c40", +"akp c #ad7c69", +".Z8 c #ad8161", +"#B6 c #ad8266", +"#lW c #ad8584", +"aPL c #ad897b", +"aE6 c #ad8f9d", +"#GZ c #ad949b", +"#r5 c #ad9d95", +"#DB c #ada194", +".vx c #ada5a6", +"aEw c #ada6ac", +"aDh c #ada7ac", +"aoy c #ada9af", +"#Gz c #ada9b4", +".fk c #adaaaa", +"#O3 c #ae470f", +"#gK c #ae4f33", +"#ow c #ae573f", +"#A. c #ae5a32", +"#3t c #ae5b3a", +"#UE c #ae5d24", +"#.7 c #ae5d38", +"#8S c #ae6142", +".1w c #ae6148", +"a.s c #ae6242", +"#8R c #ae6243", +"a.u c #ae6341", +"#dw c #ae6348", +"abO c #ae663e", +"#a6 c #ae6841", +"#7a c #ae6b4b", +"#Sq c #ae6b4d", +"#0I c #ae6e4e", +"aib c #ae7056", +".9p c #ae7163", +".4D c #ae745d", +"#.I c #ae7645", +"alv c #ae7a64", +"#aj c #ae7b5d", +"#Gx c #ae7e6c", +"#6k c #ae7e79", +"#tF c #ae8266", +"#Z# c #aea09e", +"#JM c #aea0a9", +"aFC c #aea4a7", +"aEg c #aea5a8", +"aDb c #aea5aa", +"aIC c #aea5ab", +"aIB c #aea6ac", +"#yj c #aea9af", +"#bz c #aea9cd", +"awG c #aeabb2", +"aJg c #aebdc2", +"aMS c #af1d30", +"aKI c #af2a39", +"aDS c #af2b54", +"aF# c #af2c5a", +"aKt c #af4a66", +"#st c #af5534", +"##f c #af583c", +"#hj c #af6045", +"#WI c #af6641", +"#Wr c #af6643", +"#8D c #af6738", +"a#X c #af6932", +"ah6 c #af6a4b", +"aMX c #af6d71", +"#3N c #af7048", +"#8U c #af7456", +"#9D c #af9084", +"QtC c #af9f9c", +"aHe c #afa6ac", +"aIy c #afa7ae", +"aDi c #afa9ae", +"aFa c #b02959", +"#pF c #b04e3d", +"#v1 c #b05033", +"#Dq c #b05436", +"#70 c #b05c5b", +"#3L c #b06541", +".4Q c #b0674c", +"ac5 c #b06b30", +".4E c #b06c4c", +"#Ve c #b06f50", +"afH c #b07054", +".Qm c #b0735e", +"akk c #b07659", +"#dp c #b0795a", +"alo c #b0795d", +"#4C c #b07977", +"#wc c #b08f90", +"aeI c #b09081", +"#AD c #b0947b", +"aGm c #b095a5", +".z6 c #b09c93", +"#B7 c #b09d8b", +"#AR c #b0a097", +"aHa c #b0a4aa", +"aBP c #b0a9ad", +"aMR c #b1243c", +"aDU c #b12752", +"aGv c #b1567f", +"aeD c #b15830", +"#iO c #b1593d", +"#.6 c #b15e3a", +"#7# c #b16145", +"#L2 c #b16332", +"#ec c #b1633a", +"aa. c #b16341", +"aKP c #b1635e", +"#Nz c #b16737", +"ah. c #b16746", +"#Sn c #b1674a", +"#ip c #b16c49", +"#UQ c #b16d4c", +"a#Y c #b16e48", +".P7 c #b17263", +"#bS c #b17b5c", +".09 c #b18165", +".Rz c #b18a78", +"aOP c #b18d7f", +"aLQ c #b18d9e", +"arG c #b19387", +"ad2 c #b19d93", +".qy c #b19f8d", +"acq c #b1a08c", +".sm c #b1a193", +"ahP c #b1a29b", +"al5 c #b1a2a4", +".vb c #b1a38a", +"#zm c #b1a39f", +".gS c #b1aab3", +".sy c #b1adad", +"avE c #b1aeb0", +"axz c #b1afb6", +".mV c #b1b9c6", +"aDT c #b22a55", +"#RG c #b23836", +"#pE c #b2503e", +"#pD c #b2513b", +"#G# c #b25237", +"#Gb c #b2523c", +"#QW c #b25937", +"#cw c #b25a40", +"#Mg c #b25c3c", +"#h8 c #b25f3d", +".7P c #b2623d", +"#m6 c #b26448", +"#fp c #b2653e", +"#XE c #b2660f", +"#Xz c #b26627", +"#Qm c #b26d39", +"#XO c #b26e58", +"#a3 c #b27048", +"#Gu c #b27156", +"#nW c #b27740", +"als c #b27b5f", +"#yu c #b27c56", +"#FP c #b27f59", +".p# c #b28e6d", +"avx c #b29181", +"#Ba c #b294a3", +"#Bd c #b2a39b", +"aEJ c #b2a5ab", +"aH# c #b2a6ac", +"#98 c #b2a7a9", +"aFJ c #b2a7ac", +"aHg c #b2a9af", +"#wQ c #b2b2ba", +"#lT c #b35a42", +"#xA c #b35b3b", +"#dv c #b35d46", +"#Mn c #b35e39", +"#sw c #b36447", +".9K c #b36546", +"#id c #b3663b", +"#cF c #b3683c", +"#1Q c #b36949", +"#kn c #b36a44", +"#Kv c #b36b3e", +"#Ws c #b36b4a", +"#f# c #b37047", +"#.J c #b37f5f", +".W2 c #b3806b", +"#uq c #b38d8a", +"auX c #b39283", +"aAk c #b39789", +"aqV c #b3978d", +"afm c #b3a097", +"#zM c #b3a5b0", +"#9z c #b3a698", +"aIz c #b3abb2", +"#yi c #b3adb9", +"aFc c #b4295a", +"#e7 c #b45843", +"abH c #b45948", +"add c #b45a3c", +"#KT c #b45a41", +"#gQ c #b46040", +"#ld c #b46440", +"#vP c #b46448", +"a.d c #b4663f", +"#8C c #b4683d", +"a#V c #b46840", +"aby c #b4693f", +"#YY c #b46949", +"ac4 c #b46c37", +"#Px c #b46d54", +"abA c #b46e35", +".Qa c #b47262", +"#Jb c #b47557", +"ajn c #b47a63", +".7H c #b47e6f", +"ams c #b48064", +".1H c #b48969", +"awv c #b49383", +"aJH c #b4968a", +".z5 c #b49c80", +"#vj c #b49c9a", +"aMM c #b4a1b0", +"a#f c #b4a594", +".61 c #b4a5b6", +"agF c #b4a6a0", +"aiW c #b4a7a3", +".gN c #b4a7a5", +"#zN c #b4aaad", +".31 c #b4aabb", +".t0 c #b4afad", +".hZ c #b4b4c2", +"aFb c #b52b5c", +"aMU c #b52f39", +"#fe c #b55b3f", +"#xw c #b55e43", +"#n8 c #b5633c", +"#Tw c #b56640", +"#oM c #b5684d", +"a.e c #b56a3c", +"#V. c #b56d40", +"#U3 c #b5714f", +"abB c #b5734b", +"#13 c #b57752", +"ajh c #b57759", +"#af c #b58060", +"#bV c #b5806f", +"a.w c #b58267", +"#r8 c #b58366", +"#JH c #b58469", +"#kt c #b5857d", +".2O c #b5866b", +"#my c #b58869", +"aH3 c #b59591", +".wC c #b59f97", +".ph c #b5a99f", +"aEo c #b5aaaf", +"aIA c #b5aeb5", +"#pT c #b65c44", +"#KX c #b65e3e", +".9H c #b66330", +"#5c c #b66342", +"#6T c #b66542", +"a#9 c #b66545", +"#UL c #b66625", +"#UD c #b6662d", +"#V8 c #b66829", +"#fM c #b66847", +".7Y c #b66941", +"#Qy c #b66a40", +"#ll c #b66b3f", +"#cc c #b66b53", +".6d c #b67150", +"#z5 c #b67154", +"#Y7 c #b6732e", +".6t c #b67364", +"#C1 c #b67655", +".P8 c #b67768", +"#uo c #b67d4c", +"#Er c #b68167", +"#qJ c #b68468", +"#jE c #b68557", +"#B# c #b693a5", +"#E6 c #b6947e", +"auk c #b69585", +"awY c #b69586", +"azc c #b69a8c", +"aLH c #b69bad", +"#Be c #b6a290", +"agD c #b6a49c", +".pg c #b6aba1", +".y2 c #b6adc3", +"aEx c #b6aeb4", +"aEv c #b6afb4", +".t5 c #b6b2af", +"aHY c #b72a40", +"adc c #b7593d", +"#KY c #b7603d", +"#Tf c #b76330", +"#cv c #b7634a", +"abM c #b76845", +"#mW c #b76846", +"#Np c #b76936", +"#jS c #b76a3f", +"#lC c #b76c50", +"#Xy c #b76d20", +"a.v c #b76d4a", +"a#W c #b76e3d", +"ag1 c #b76e4f", +".ZY c #b77155", +"abP c #b77249", +"#5w c #b7734f", +"aHW c #b7747e", +"ac6 c #b7754c", +".ZH c #b7785c", +".UD c #b77e67", +"#N3 c #b77e6a", +".9q c #b78070", +"#k7 c #b78457", +".Zy c #b7856a", +"#TT c #b78672", +".Yw c #b7886c", +"atw c #b79687", +"asX c #b79689", +"aHG c #b798a9", +"aJe c #b798ac", +"axX c #b79b8e", +"#Ef c #b7a48d", +"aG1 c #b7adb1", +".dG c #b7b0b9", +"auy c #b7b5bc", +".l. c #b7b8bd", +".jH c #b7cce7", +"aMQ c #b83a54", +"#YW c #b84538", +"#jT c #b86339", +"#e6 c #b8634a", +"#mI c #b8643e", +"#mH c #b86540", +"#5b c #b86548", +"#lU c #b8664d", +"#mV c #b86743", +"#cu c #b86950", +"#I5 c #b86c3f", +"#sg c #b86c43", +"#LW c #b86d3c", +"a.f c #b8703e", +"#5v c #b8704d", +".ZX c #b87055", +"#Sm c #b8715a", +".70 c #b8765c", +".1B c #b8765e", +"#Ev c #b8795a", +".Q# c #b8796a", +"#0H c #b87a59", +"#2S c #b87d7d", +"#e1 c #b88162", +"#8V c #b88165", +"#3p c #b88263", +"alt c #b88266", +".Zz c #b88364", +".4s c #b88a71", +"#vr c #b88b70", +"alw c #b88d7d", +"aNZ c #b89385", +"axm c #b89788", +"#G0 c #b8a0a8", +"#G1 c #b8a3ad", +"#CK c #b8a58f", +"aaU c #b8a894", +"aLP c #b8a9b7", +".zI c #b8aa8b", +".zh c #b8acb7", +".py c #b8ad83", +"aDf c #b8b2b7", +"aAJ c #b8b6c0", +".q6 c #b8c3db", +"aMT c #b92435", +"adb c #b95a49", +"#Ki c #b9605d", +"ade c #b96443", +"#tZ c #b9654a", +"#4V c #b96a5f", +"#Sp c #b96c49", +"#sx c #b96d51", +".3a c #b96d54", +"#Tu c #b96f47", +".Z0 c #b97a5d", +".P9 c #b97a6b", +"#T# c #b97d60", +".W1 c #b9826d", +".Qt c #b98371", +"#w1 c #b98766", +"#k9 c #b98d7a", +".j# c #b99872", +"#yl c #b9a399", +".r6 c #b9a584", +"#Ci c #b9a796", +"#CJ c #b9a99e", +".af c #b9aab3", +"#zO c #b9aca7", +"#Za c #b9acaa", +".v6 c #b9aeb0", +"#KS c #ba6045", +"#aP c #ba6148", +"#LX c #ba6432", +"#RQ c #ba6435", +"#.5 c #ba6743", +"#gC c #ba6749", +"#Kw c #ba6839", +"#gV c #ba6843", +"#OW c #ba6934", +"#pu c #ba6a43", +"#n7 c #ba6a44", +"#nl c #ba6a51", +"aeu c #ba6b4a", +"#qT c #ba6d44", +"afK c #ba6d4d", +"#8T c #ba6d4e", +"#ls c #ba6e4b", +"agY c #ba7158", +"#un c #ba723f", +"aab c #ba734c", +"#l# c #ba755e", +".1f c #ba775f", +"#Dw c #ba794b", +".ZI c #ba7a5e", +".Q. c #ba7b6b", +".Qn c #ba7d66", +".Qr c #ba7e6a", +".4x c #ba7f69", +"aln c #ba8467", +"alr c #ba8468", +"amA c #ba9385", +".qt c #ba9778", +"#7X c #ba9d94", +"a#i c #baa695", +".Cq c #baaeb5", +"#yk c #baafad", +"aEs c #bab0b4", +"aHd c #bab1b7", +"aBM c #bab2b6", +".p5 c #bab8d8", +".sP c #bab9d4", +"aLY c #bb1c26", +"#Ga c #bb5b43", +"#ri c #bb5e48", +"#F5 c #bb653f", +"#Th c #bb681e", +"#ND c #bb693b", +".9J c #bb6943", +"#UG c #bb6b1e", +"#p8 c #bb6b4f", +"aet c #bb6c4b", +"#fa c #bb6f4c", +"#tL c #bb6f4e", +"#V7 c #bb7024", +"#L6 c #bb723e", +"#Gm c #bb723f", +"#R4 c #bb7344", +".Yl c #bb7b5e", +".Qs c #bb816e", +".1. c #bb8769", +"#ai c #bb8f7b", +"aHM c #bb9ab5", +"##A c #bba1b2", +"a#j c #bba493", +"#6h c #bba7a4", +"aIJ c #bba9b2", +"aaY c #bbaa97", +"aDg c #bbb5ba", +".yq c #bbb5bc", +"aHS c #bbb5c2", +"aLT c #bc2641", +"aDV c #bc305c", +"#cx c #bc6246", +"#ox c #bc634b", +"aeE c #bc663b", +"#Da c #bc674d", +"afI c #bc684b", +"#xo c #bc684d", +"#ov c #bc684f", +"afV c #bc6c45", +"#aZ c #bc6d4e", +"#UC c #bc6e2b", +"#qU c #bc6e45", +"#mE c #bc744d", +"#Mx c #bc7454", +"#N2 c #bc7458", +"#QY c #bc755d", +"#sR c #bc7a50", +"#a1 c #bc7c53", +".ZG c #bc7c61", +".Qq c #bc7e6a", +"akm c #bc8265", +"#My c #bc836a", +"#E5 c #bc8371", +"#tC c #bc8a5d", +".93 c #bc8b7d", +".1J c #bc9274", +"aoq c #bc9e93", +"a#g c #bca493", +"afn c #bcaca5", +"ad6 c #bcb0ad", +"aHc c #bcb3b9", +"aBN c #bcb5b9", +".ik c #bcc6da", +"#mN c #bd6e44", +"#gH c #bd6e48", +"#pw c #bd7045", +"##. c #bd704a", +"#U4 c #bd705e", +"#XC c #bd732f", +"#U1 c #bd7552", +"ac2 c #bd7555", +"#gE c #bd764e", +"#U2 c #bd7755", +".2U c #bd7b65", +"#Zd c #bd8065", +"##x c #bd8166", +"#12 c #bd825c", +"#QZ c #bd8574", +".1G c #bd8e70", +"aIg c #bd998a", +"aM5 c #bd998b", +"#6j c #bd9d98", +"aIh c #bd9f93", +"#Gy c #bda19c", +".uX c #bda594", +"#9C c #bda597", +"#zP c #bda89a", +"#Bb c #bda8b2", +"#0D c #bdb0aa", +"aEq c #bdb3b8", +"aDd c #bdb5b9", +"aEu c #bdb5bb", +"#lf c #be6743", +"#C5 c #be6a42", +".9z c #be6d48", +"aL0 c #be6e6f", +"#sh c #be7147", +"afL c #be7150", +"#cE c #be7347", +"#0u c #be744d", +".4P c #be745a", +"#TN c #be7843", +"#gG c #be794d", +"#p4 c #be7950", +"ah5 c #be7a5b", +"#Uy c #be7e61", +"#Ze c #be7e63", +".4u c #be8671", +"#Py c #be8674", +"#sa c #be895c", +".Yv c #be8a6e", +"#vo c #be8d60", +"#I# c #be8d6f", +".Z7 c #be8f6e", +"#qM c #be9374", +"aad c #be9784", +"aBz c #be9a86", +"aAj c #be9a87", +"#DP c #beac95", +"aLL c #beacbb", +"aaX c #bead99", +"aHb c #beb2b7", +"aDc c #beb5b9", +"aFT c #beb6bc", +".DP c #bebcd9", +"aKN c #bf1f1f", +"aes c #bf5b3a", +"aLz c #bf614a", +"#Qo c #bf662d", +".9I c #bf6b3e", +"abL c #bf6c4b", +"#Qn c #bf6e37", +"#tN c #bf6f4a", +"#pv c #bf7047", +"#8B c #bf704b", +"#8A c #bf7051", +"#O8 c #bf7145", +"#Qz c #bf7148", +"#eb c #bf7249", +"#W# c #bf7327", +"#ct c #bf735a", +"#XB c #bf7436", +"aeq c #bf785b", +"#R1 c #bf7e4c", +"ajg c #bf8062", +"#7b c #bf8160", +".Qo c #bf836b", +"#RK c #bf8468", +".Hl c #bf8553", +"#tG c #bf8d6c", +"#vs c #bf8e6c", +"#pk c #bf8e71", +"#Eb c #bf97a5", +"asW c #bf9988", +"aJG c #bf9a8c", +"aCY c #bf9b88", +"apZ c #bfa196", +"#FH c #bfa89d", +"#Ie c #bfa8a7", +"#FF c #bfaaa4", +".x0 c #bfac86", +"aEF c #bfb1b7", +"QtF c #bfb2ba", +"aEr c #bfb5ba", +".6W c #bfb89d", +".0y c #bfb8c9", +".oj c #bfd1f9", +"#Wb c #c06701", +"#cr c #c0684f", +"#cm c #c06942", +"#RS c #c06b24", +"#W. c #c0722e", +"#tM c #c07251", +"#O7 c #c07447", +"#qV c #c07449", +"#sk c #c07548", +"ag0 c #c07757", +"#04 c #c07955", +"#e9 c #c07c56", +"#rs c #c07d53", +"#Bu c #c07e5b", +"#WJ c #c08060", +"#5x c #c08560", +"#FZ c #c08952", +"alq c #c08a6e", +"aCX c #c0967d", +"aEc c #c0967e", +"aKX c #c09c8e", +"#Ee c #c0afa3", +".zG c #c0b091", +".AS c #c0b4bb", +"aDp c #c0b5ba", +".o. c #c0b78f", +"aBO c #c0b9bd", +"#vJ c #c16443", +"#Dc c #c1684e", +"#vY c #c16b4f", +"#xB c #c16d4c", +"#F4 c #c16e46", +"#vz c #c1724d", +"#aG c #c1734d", +"#O6 c #c17649", +"a#U c #c17657", +"#lQ c #c17a54", +"#Tr c #c17f54", +"#oI c #c18057", +"#XN c #c18069", +".Yn c #c18362", +".Qp c #c1856c", +".FM c #c18858", +".6x c #c18c78", +".Tx c #c1936c", +"#Bl c #c19479", +"aBy c #c1977f", +".nM c #c19a7d", +"azb c #c19d89", +"aFy c #c19d8a", +"QtE c #c1a387", +"#CH c #c1a6ae", +"#Hn c #c1a89c", +".xX c #c1ad8f", +"#Hm c #c1ada5", +"acu c #c1b4a0", +".v0 c #c1b5b8", +".zg c #c1b5bb", +"aEp c #c1b7bc", +"aFS c #c1b9be", +".84 c #c1b9c0", +".Cc c #c1bdd4", +"#Nq c #c26935", +"#Aw c #c26e4c", +"#oA c #c2704f", +"#ih c #c2714e", +"#6S c #c27153", +"#o. c #c2754a", +"#lP c #c2754e", +"a.c c #c27557", +"#5a c #c27559", +"##a c #c2764f", +"#nm c #c27a5f", +"#2m c #c27f60", +".1g c #c27f67", +".6v c #c28071", +"#dy c #c28361", +".7L c #c28478", +".7K c #c28579", +"#0G c #c28866", +".6r c #c28868", +"#K9 c #c2886c", +"#FD c #c28880", +"#11 c #c28a63", +"#Nl c #c28c4f", +"#7c c #c28c6c", +".Z6 c #c29271", +"#w0 c #c2967a", +".1I c #c29777", +".mj c #c29983", +"av3 c #c29a85", +"aM4 c #c29a8b", +"#2R c #c29d9d", +"aqT c #c29e8e", +"#Ec c #c2a4ac", +"#IU c #c2aaa2", +"#r6 c #c2ab9c", +"#7V c #c2aca5", +".By c #c2ae99", +"#FG c #c2afa8", +"#CI c #c2b0af", +"aII c #c2b0b9", +"ad3 c #c2b1aa", +"anC c #c2b5bb", +"aEt c #c2b7bc", +".D2 c #c2b7bd", +"aDe c #c2b9bd", +".5A c #c2b9cb", +"aBF c #c2bcbe", +"#EU c #c36245", +"#gM c #c3674a", +"#KR c #c36a4c", +"#xd c #c36c46", +"#Db c #c36d52", +"#oh c #c36e59", +"#3s c #c36f52", +"aH1 c #c36f70", +"#Te c #c37040", +"#n9 c #c3734a", +"#3r c #c37559", +"#Y5 c #c37946", +"#ng c #c37951", +"#U8 c #c37956", +"#ZB c #c37c56", +".1e c #c38068", +".ZF c #c38367", +"#Yb c #c38565", +"#sS c #c3875d", +".Va c #c38778", +".XZ c #c38c70", +"#wX c #c39164", +"#zX c #c3967b", +"#CS c #c3977b", +"#ZD c #c39781", +".kF c #c39989", +"awX c #c39b86", +"avw c #c39b87", +"aL8 c #c39b8b", +"axl c #c39c87", +"aNY c #c39c8c", +"axW c #c39f8c", +"#Hl c #c3aba5", +"#4A c #c3abaa", +"#7W c #c3aca4", +"ahQ c #c3b5af", +"ayl c #c3b7c4", +".VM c #c3b8c5", +"aEh c #c3bbbe", +"azw c #c3c2cb", +"aHX c #c45b6b", +"#gJ c #c4624a", +"#h# c #c46942", +"#OX c #c46b34", +"#cy c #c46c4d", +"#R7 c #c47548", +"#xp c #c47556", +".ZW c #c47960", +"#R5 c #c47a4c", +"#fu c #c47b56", +"#yA c #c47b5c", +"#6l c #c47b79", +"#U9 c #c47c53", +"#2n c #c47d59", +".6u c #c48071", +"#Wk c #c48168", +"a#T c #c4826b", +".Ym c #c48867", +"#8y c #c48871", +"#2p c #c48968", +"#HF c #c48a5d", +"akj c #c48a6d", +".Wq c #c48b72", +".D7 c #c48c5f", +"#qN c #c48c61", +"#Ej c #c49366", +"#5y c #c49574", +"aAi c #c49a82", +".S2 c #c49b8a", +"arE c #c49e8c", +".94 c #c4a6a3", +"#IV c #c4a99c", +"#wS c #c4ada7", +"#Ed c #c4afae", +"aF1 c #c4b4bb", +"#.i c #c4bcc1", +".8T c #c4bdaf", +".oz c #c4c0d6", +"QtY c #c4c2cf", +"#gI c #c56b4d", +"#KK c #c57141", +"#N1 c #c5714e", +"#D# c #c57156", +"#pt c #c57850", +"#Tv c #c57851", +"ac3 c #c57a4f", +"#sB c #c57b5e", +"#lV c #c57b61", +"#oH c #c57e55", +"#Xu c #c57e5f", +"ah3 c #c57f6c", +"#g2 c #c5825c", +".4w c #c58372", +"#6Q c #c5876d", +".4v c #c58774", +"#LS c #c59057", +".Cv c #c59068", +".R2 c #c59274", +"#CP c #c59466", +"#3O c #c5946f", +"#AC c #c59472", +"#nY c #c59679", +"aKW c #c59d8d", +"aOO c #c59e8e", +"acx c #c59e98", +".eZ c #c5a191", +"#4B c #c5a29f", +"#CM c #c5a884", +"aJa c #c5aabc", +".z4 c #c5ac90", +"acs c #c5b09b", +".Bz c #c5b19d", +".ws c #c5b28c", +"#Bc c #c5b6b7", +"aFK c #c5b9bf", +"aFO c #c5babf", +".vr c #c5bed3", +".xc c #c5bed6", +".kK c #c5c4d4", +"#Tj c #c66002", +"#q4 c #c66554", +"#Nd c #c6686a", +"#jN c #c66f4c", +"#gP c #c67051", +"#Mo c #c6734b", +"#cA c #c6734f", +"#QA c #c6754c", +"#lj c #c6794e", +"#hi c #c6795e", +"#w8 c #c67a58", +"#ii c #c67c5b", +"#sy c #c67d63", +"#ij c #c68363", +".Yk c #c68367", +".1C c #c6846c", +"#Wi c #c6866c", +"ajf c #c68769", +"#rt c #c6895f", +".2Q c #c68e74", +"#OS c #c68f50", +"#10 c #c69068", +"alp c #c69074", +"#FM c #c69164", +".Yu c #c69175", +"#Hs c #c69263", +"aHQ c #c693b3", +"#yq c #c69467", +"#Bi c #c69567", +"#Gt c #c69576", +"#ys c #c6997e", +"#Yc c #c69984", +"aza c #c69c84", +"auW c #c69e89", +"asl c #c69e8b", +"ajc c #c69e8f", +"asm c #c6a292", +"apY c #c6a496", +"#Kl c #c6a79f", +"#Eg c #c6ae8e", +"ad4 c #c6b6af", +".FH c #c6bbc1", +".pY c #c6c2d0", +"aye c #c6c4ce", +"#Di c #c7674e", +"#vH c #c76d4a", +"#ha c #c76e44", +"#jO c #c76e4a", +"#ig c #c76f4b", +"#BX c #c76f4f", +"#KZ c #c7704a", +"#iQ c #c7715a", +"#Tn c #c7723e", +"#QV c #c77250", +"#oF c #c77750", +"#oG c #c77751", +"#K4 c #c7794d", +"#cH c #c7794f", +"#si c #c77b50", +"### c #c77b54", +"#px c #c77c50", +"#U7 c #c77c5e", +"#sf c #c77d55", +"#K8 c #c77e5b", +"#2o c #c7825c", +"#E2 c #c78558", +"#XF c #c7863f", +".ZE c #c7886c", +"#B5 c #c78973", +"akh c #c78d70", +".V# c #c78f68", +".79 c #c79078", +".AX c #c79471", +".2P c #c79478", +".4t c #c7947d", +"#zU c #c79568", +".Z5 c #c79674", +"#WK c #c79984", +"axk c #c79a81", +"#Kt c #c79b6e", +"#Em c #c79b7f", +"axV c #c79d85", +"aHI c #c79eb6", +"aPK c #c79f8f", +".aq c #c7a585", +"apb c #c7a698", +"#zQ c #c7a893", +"aaV c #c7ae9b", +".wo c #c7b396", +".yp c #c7b9b8", +"#0C c #c7bcbc", +"aB0 c #c7bec3", +"aFR c #c7bec4", +".DO c #c7c4e5", +"aLU c #c81831", +"#Tm c #c8681e", +"#pA c #c86947", +"#Gf c #c86a51", +"#g9 c #c86f4c", +"#EB c #c8714c", +"#cd c #c8714e", +"#gR c #c87554", +".9y c #c87651", +"#NU c #c8774b", +"#NT c #c8774d", +"#p2 c #c87851", +"#Tx c #c87852", +".9A c #c87853", +"#H5 c #c87946", +"#JB c #c87949", +"#QU c #c87b5a", +".3# c #c87d63", +"#KF c #c87f4c", +"#nh c #c8845d", +"#Az c #c88464", +"#a2 c #c8865e", +"#v7 c #c88a5f", +"#p5 c #c88a60", +"#XM c #c88a72", +".2R c #c88b71", +".Z4 c #c89272", +"#w2 c #c8936c", +"#I0 c #c89462", +"#3c c #c89a6a", +"#up c #c89b81", +"#I2 c #c89d73", +"#06 c #c89e87", +"asV c #c89f8b", +"auj c #c8a08c", +"aJF c #c8a090", +".bW c #c8a48e", +".hR c #c8a686", +"#xK c #c8a898", +"#Km c #c8a89a", +"#9A c #c8b1a5", +".v9 c #c8b293", +"ak# c #c8b8c5", +"aFQ c #c8c0c6", +"##Y c #c8c2da", +".ld c #c8d8e2", +"#Xs c #c96253", +"#ED c #c96c4b", +"#ob c #c96f48", +"aer c #c96f50", +"#LZ c #c97024", +"#HN c #c97048", +"#3. c #c97166", +"#Mf c #c97350", +"#kp c #c97357", +"#mD c #c97547", +"#yG c #c9754d", +"#mJ c #c9774f", +"#UF c #c97834", +"#aF c #c97853", +"#Td c #c9793f", +".7W c #c97a44", +"#NB c #c97c4c", +"#w9 c #c97c52", +"#Jc c #c97f4e", +"#sW c #c98265", +"#QT c #c98364", +"#tJ c #c98553", +".RO c #c98569", +"#ix c #c98666", +"#V3 c #c98668", +"#R2 c #c98755", +"#ry c #c9896b", +".ZD c #c9896d", +"#y8 c #c98a5e", +"#JG c #c98e6e", +".W0 c #c9907b", +".ZA c #c9916e", +"#Bp c #c9926c", +"#Kr c #c9955f", +"atu c #c99c83", +"#CG c #c9a4b3", +".FI c #c9bdc8", +"#8u c #c9bfbf", +"aah c #c9c2c7", +".Cg c #c9c3df", +"aw. c #c9c5c8", +".rp c #c9c8de", +".tR c #c9d4e7", +"aKM c #ca1115", +"#gL c #ca6c50", +"#if c #ca6d48", +"#S8 c #ca6e61", +"#BB c #ca704d", +"##b c #ca7342", +"#lg c #ca744f", +"#Jh c #ca7548", +"#nf c #ca7752", +"#QX c #ca7758", +"#ef c #ca7952", +"#NC c #ca7b4c", +"#um c #ca7b60", +"#o# c #ca7f53", +"#qW c #ca8054", +"abx c #ca8162", +"#cB c #ca8255", +"#3M c #ca835d", +"#q. c #ca886a", +".Yq c #ca8969", +".9x c #ca8a7d", +"#Hk c #ca8a83", +"aje c #ca8b6d", +".ZJ c #ca8b6f", +"#Zc c #ca8f73", +".WZ c #ca936d", +".Yt c #ca9571", +"amt c #ca9579", +"#Qg c #ca9a7c", +"#Vf c #ca9a85", +"awW c #ca9d84", +"atv c #caa28d", +"aIf c #caa292", +"#zb c #caa483", +"apc c #caab9e", +"#9B c #cab3a6", +"aHn c #cab9c0", +"aF4 c #cabac1", +".ig c #cabcc3", +".dC c #cabfc5", +".YY c #cac2d5", +"aux c #cac7ce", +"#O2 c #cb6211", +"#BV c #cb6b4e", +"#v2 c #cb6e50", +"##e c #cb714e", +"#i. c #cb7350", +"#HL c #cb754c", +"#EA c #cb774f", +"#NV c #cb7b4c", +"#UW c #cb7b50", +"#Ky c #cb7c3d", +"#vD c #cb7c50", +"#tQ c #cb7d51", +"#KG c #cb804d", +"#qS c #cb8058", +"#IR c #cb807b", +"#L7 c #cb814e", +"#he c #cb8265", +"#cC c #cb8356", +"#sz c #cb8369", +"#05 c #cb845f", +"#tI c #cb8850", +"#gF c #cb885c", +".21 c #cb8968", +"#9F c #cb8a86", +"#B2 c #cb8b5e", +"#a0 c #cb8b62", +"#dz c #cb906b", +"#wa c #cb9567", +".zl c #cb9a7b", +".Tw c #cb9b74", +".hL c #cb9d7c", +"aEb c #cb9d82", +"aui c #cb9e85", +".kx c #cba085", +".nE c #cba183", +".o2 c #cba281", +"#IX c #cba68e", +".du c #cba797", +"#LN c #cba798", +"apX c #cba899", +"#ym c #cbaa99", +"#Hp c #cbac94", +"#FJ c #cbb098", +"aLI c #cbb3c4", +"act c #cbbca6", +".AE c #cbc6d2", +"aq3 c #cbc7cd", +"aut c #cbc8ca", +"aij c #cbc8cc", +"#Qu c #cc5e1e", +"aLG c #cc677c", +"#Nv c #cc6f2c", +"#BC c #cc704e", +"#oc c #cc724f", +"#Pv c #cc7450", +"#gA c #cc745c", +"#ib c #cc7750", +"#xu c #cc775a", +"#NK c #cc7954", +".9B c #cc7c57", +"#vA c #cc7d50", +"#vU c #cc7e5f", +"#L8 c #cc7f4d", +"#y6 c #cc7f5b", +"#fL c #cc805e", +"#hh c #cc8064", +"#Qx c #cc8156", +"#UZ c #cc8159", +"#sA c #cc8366", +"#Ya c #cc845f", +"#oN c #cc876b", +".4F c #cc8867", +".Yo c #cc8b6b", +".2S c #cc8d73", +"#uk c #cc8e63", +"#Gs c #cc916d", +"#8W c #cc9d85", +".eQ c #cc9f7d", +"aFx c #cc9f84", +".S3 c #cca18e", +".rH c #cca37d", +".qk c #cca37f", +".s4 c #cca47c", +"QtM c #ccab85", +"#IW c #ccab98", +"#IT c #ccaca7", +"#Ho c #ccb09d", +".qI c #ccb698", +"#qG c #ccb7aa", +"ad5 c #ccbeb8", +".AR c #ccc1c3", +".FG c #ccc1c4", +"aFF c #ccc3c6", +"aC5 c #ccc5c7", +".Ft c #cccaf2", +"aKL c #cd0a13", +"aKJ c #cd2935", +"aJm c #cd2e2a", +"#Qp c #cd6820", +"#RT c #cd6917", +"afJ c #cd6e50", +"#fl c #cd7250", +"#KP c #cd7452", +"#KQ c #cd7454", +"#ff c #cd7458", +"agZ c #cd775e", +"#cz c #cd7855", +"#xn c #cd795e", +"#O9 c #cd7c51", +"#fy c #cd7d5a", +"#dK c #cd7e62", +"#mK c #cd7f54", +"#U5 c #cd806c", +"#RN c #cd813e", +"#sj c #cd8256", +"#U0 c #cd845e", +".4O c #cd866b", +"#Uz c #cd8823", +"#3q c #cd8a6d", +".1h c #cd8a72", +".2T c #cd8b75", +"#Ay c #cd8d60", +"#C2 c #cd8d67", +".Yr c #cd906f", +".Ys c #cd9673", +"#OP c #cd9c7f", +".ah c #cda07e", +"aCW c #cda085", +"awu c #cda087", +"#Ko c #cda28a", +".ut c #cda47b", +"#yn c #cda490", +"#LM c #cda49d", +"#ty c #cda59f", +".gs c #cda993", +"#FE c #cdaaa3", +"aop c #cdaea1", +".wx c #cdaf8a", +"a#h c #cdb7a6", +"aaZ c #cdb7a7", +".BA c #cdbaa5", +"aMF c #cdbac9", +"#Ch c #cdbca1", +".qY c #cdbdb2", +".sc c #cdbe8c", +"aEG c #cdbfc5", +"aoL c #cdcace", +".sQ c #cdcce8", +"aLX c #ce1c28", +"aJo c #ce1f1a", +"#OZ c #ce6915", +"#fE c #ce744c", +"#y4 c #ce7a59", +"#ne c #ce7f57", +"#QF c #ce8053", +"#p3 c #ce8059", +"#iN c #ce805f", +"#B1 c #ce825e", +"#Tc c #ce833a", +"#UB c #ce8431", +"#V6 c #ce852a", +"a.b c #ce8871", +"#Qj c #ce9655", +".1# c #ce9676", +"#Qe c #ce9b8c", +"#ON c #ce9e8f", +"#LU c #ce9f6f", +"axU c #cea085", +"aGU c #cea186", +"auV c #cea188", +"#Ng c #cea394", +"#vk c #cea79f", +"aqU c #ceada0", +"#Eh c #ceb28d", +"#6i c #ceb6b2", +".uY c #ceb995", +"afo c #cebfb8", +".uS c #cec29c", +"aFP c #cec2c8", +".xA c #cec2cc", +"aFN c #cec3c8", +"#bv c #cec4c9", +".pU c #cec9d7", +".lJ c #cec9ed", +".aC c #cecbd7", +".ua c #cecdd6", +".pJ c #cedaf8", +"aLV c #cf0f26", +"#OY c #cf6a1f", +"#Ns c #cf6f1d", +"#cq c #cf745a", +"#Dd c #cf745b", +"#i# c #cf7552", +"#KO c #cf7651", +"#C7 c #cf7652", +"aMw c #cf7662", +"#M# c #cf7a4a", +"#cs c #cf7a61", +"#jU c #cf7c53", +"#NJ c #cf7c55", +"#oL c #cf7c61", +"#dH c #cf7c62", +"#XD c #cf7e1e", +"#yF c #cf7e54", +"#NS c #cf7e57", +"#.8 c #cf7f59", +".9F c #cf825b", +"#mL c #cf8358", +"#Pt c #cf8360", +"#rr c #cf845c", +"#iM c #cf8560", +"#j3 c #cf8561", +"#yB c #cf865e", +"#hf c #cf8669", +"#Wd c #cf873f", +".6m c #cf886d", +"#z7 c #cf895e", +".RA c #cf8d77", +"#xE c #cf9165", +"#RI c #cf9178", +"#ZC c #cf9372", +".Z3 c #cf9476", +".1E c #cf967b", +"#Ni c #cf9f83", +"#LP c #cfa087", +".bN c #cfa180", +".i3 c #cfa280", +"ask c #cfa691", +"#wb c #cfa791", +"#zR c #cfaa90", +"#Kn c #cfaa96", +"arF c #cfac9d", +"#FI c #cfb5a2", +"acr c #cfb6a1", +"aaW c #cfbaa6", +".xZ c #cfbc98", +".uQ c #cfbe8d", +"aEk c #cfc7c9", +".k1 c #cfcbbc", +"axv c #cfccce", +"QtR c #cfcde9", +"aKK c #d01721", +"#Nt c #d0711c", +"#ud c #d07154", +"#Gl c #d0744d", +"#Jw c #d07653", +"#dZ c #d0785a", +"#RR c #d07b3f", +"#qX c #d07b51", +"#Tg c #d07c3d", +"#hb c #d07c4d", +"#Mr c #d07d50", +"#NH c #d07d53", +"#u# c #d07d60", +"#Wc c #d07e24", +"#fo c #d07e5a", +"#vW c #d07e61", +"#D. c #d07e62", +"#tO c #d07f51", +"#TB c #d07f52", +"#xr c #d07f61", +"#ic c #d08056", +"#xC c #d0805e", +"#UY c #d08259", +"#ul c #d08a6a", +".1d c #d08c74", +"#vv c #d08d5c", +"aJr c #d08d80", +".RZ c #d09270", +"#Ql c #d09561", +"aH2 c #d09695", +"#0F c #d09775", +"#z0 c #d09969", +"#RJ c #d0997c", +"#n1 c #d0997e", +"#pn c #d0a083", +"avv c #d0a38a", +"arD c #d0a895", +".my c #d0bc8f", +".vq c #d0c8dd", +".kZ c #d0c9b2", +"auv c #d0cdd0", +".um c #d0d7f5", +"#RY c #d17138", +"#UJ c #d17211", +"#d6 c #d17654", +"#Dl c #d1775c", +"#Qc c #d1777b", +"#Jx c #d17852", +"#pH c #d17b59", +"#le c #d17d59", +"#v4 c #d17d5c", +"#Pw c #d17d5d", +"#M. c #d17f4f", +"#mM c #d18055", +"#Gn c #d1814d", +"#ed c #d18159", +"#y5 c #d1815e", +"#km c #d1825d", +"#R6 c #d18456", +"#rv c #d18666", +"#hg c #d1866a", +"#U6 c #d1866d", +"#sQ c #d1875f", +"#TM c #d18a58", +"#iF c #d18a64", +"#V4 c #d18c1f", +".RM c #d18d71", +".Z1 c #d18f73", +"#sX c #d19274", +"#po c #d1956b", +".xE c #d1a085", +"az# c #d1a489", +"#wU c #d1a495", +"#Hu c #d1a57e", +"#DA c #d1ab93", +"#wT c #d1aca0", +"#Bf c #d1b59b", +"aHm c #d1c0c8", +".b4 c #d1c3c6", +"aFM c #d1c6cb", +".Cn c #d1c7c4", +"#c7 c #d1c9d3", +".Ch c #d1c9f1", +".Au c #d1cdef", +"auw c #d1cfd5", +"aHZ c #d2304a", +"#EV c #d27255", +"#Nu c #d27423", +"#LY c #d27628", +"##d c #d2784f", +"#rg c #d27961", +"#V1 c #d27966", +"#HM c #d27a51", +"#v3 c #d27a5a", +"#Mw c #d27d57", +"#rf c #d27d64", +"#NI c #d27f56", +"#fk c #d28062", +"#TG c #d2806b", +"#L1 c #d28143", +"aMz c #d28177", +"#R8 c #d28255", +"#UX c #d28257", +".7Q c #d2825d", +"#xa c #d28357", +"#.9 c #d2845e", +"#NA c #d28757", +"#HG c #d2875a", +"#V2 c #d2886e", +".RN c #d28d72", +"#S9 c #d28f77", +"#Wj c #d29177", +"#Ew c #d2936e", +".ZC c #d29870", +".UE c #d29879", +"#mx c #d29d71", +".7G c #d29e8f", +"#UO c #d2a098", +"#Qf c #d2a38a", +"aBx c #d2a58a", +"aqS c #d2ad9b", +"#3P c #d2b096", +"#Bg c #d2b192", +"a#k c #d2b1a4", +".yj c #d2b594", +".v8 c #d2bb9e", +".ty c #d2bbab", +".64 c #d2c0bd", +"aFZ c #d2c2c9", +"##U c #d2c6c6", +"#1X c #d2c6c7", +"au8 c #d2ced1", +"aLW c #d30c20", +"#Tk c #d36b0c", +"#O0 c #d36b0f", +"#UI c #d3710a", +"#Dj c #d3745b", +"#sI c #d3765a", +"#h. c #d37853", +"#Dm c #d37a5f", +"#NG c #d37b4f", +"#m8 c #d37f66", +"#TA c #d38051", +"#xt c #d38063", +".9G c #d3814a", +"#NR c #d3815d", +"#NQ c #d3815e", +"#re c #d38267", +"#vV c #d38365", +"#tR c #d38459", +"#dL c #d38468", +"#cI c #d3855a", +"#lN c #d3855d", +"#vy c #d38667", +".1v c #d3866d", +"#Dv c #d38763", +".Yp c #d39071", +"#qP c #d3986e", +"#I. c #d39876", +".R0 c #d39978", +"#nX c #d39c70", +"#vt c #d39d77", +"#Nf c #d39f99", +"#IS c #d3a19c", +"aAh c #d3a68b", +"adj c #d3b09b", +"a.. c #d3b1a0", +".tN c #d3bfb2", +".x5 c #d3c0ac", +".ax c #d3c0c0", +"#XK c #d3c1c0", +".uR c #d3c397", +".6Y c #d3c7c9", +"#6M c #d3c9c7", +".e7 c #d3cdd6", +"axy c #d3d0d7", +"#RX c #d46a1d", +"#RU c #d46c15", +"#C8 c #d47957", +"#Jj c #d47b50", +"#gO c #d47d5e", +"#QC c #d47e57", +"#NZ c #d47f58", +"#BY c #d4805f", +"#Mq c #d48154", +"#P. c #d48157", +"#xb c #d48359", +"#uh c #d48462", +"#vB c #d48553", +"#E0 c #d48764", +"#HH c #d4885b", +"#sl c #d4885d", +".ZV c #d4886f", +"#Jd c #d48959", +"#Ms c #d48d66", +"#Ta c #d48e32", +"#z6 c #d48e69", +"#Ps c #d48e6d", +"#xF c #d48f6f", +".RL c #d48f74", +"#ka c #d49071", +"#xI c #d49467", +".RY c #d49470", +"#pq c #d4956c", +".WY c #d49a75", +"aki c #d49a7d", +"#OU c #d49c68", +".X0 c #d49c79", +"#Nn c #d4a16f", +".Tv c #d4a179", +".1F c #d4a184", +"#Kk c #d4aaa6", +".wt c #d4b590", +"ane c #d4b5a9", +".wp c #d4c0a0", +".kU c #d4c18d", +"#DO c #d4c2a2", +"acv c #d4c3b2", +"##W c #d4cbd2", +"aw9 c #d4d2d8", +"aJn c #d52b26", +"#O1 c #d56d0f", +"#Qq c #d56d1a", +"#HO c #d57856", +"#Dr c #d57c5d", +"#uf c #d57d5d", +"#g8 c #d57e5b", +"#yZ c #d57e62", +"#RP c #d58156", +"#Xt c #d58168", +"#lh c #d5825a", +"#xs c #d58265", +"#fP c #d58464", +"#jR c #d5855b", +"#iL c #d58560", +"#li c #d5865c", +"#Mt c #d5875e", +"#Ax c #d58865", +"#Uw c #d58872", +"#y7 c #d58965", +".1u c #d58970", +"#g7 c #d58a63", +"#E1 c #d58a66", +"#p9 c #d58a6f", +".ZU c #d58b71", +"#8z c #d58d74", +".RQ c #d59175", +".RR c #d59275", +".1i c #d5927a", +".RB c #d5937c", +"#5# c #d59478", +".S5 c #d5967b", +"#OM c #d59993", +"aMy c #d59a82", +"#zT c #d59c60", +"#3a c #d59e62", +".R1 c #d59e7f", +"#tH c #d59f79", +"#vl c #d5a499", +".vR c #d5a58b", +"#OO c #d5a78f", +"#LO c #d5ac96", +".yh c #d5b896", +"agE c #d5c6bf", +"aFL c #d5cacf", +".e8 c #d5cdd6", +".gA c #d5d2de", +"apB c #d5d4d7", +".sM c #d5d5da", +".sL c #d5d5e2", +"#HP c #d67958", +"#Gi c #d67959", +"#Dk c #d67a60", +"#q0 c #d67c5d", +"#L0 c #d68139", +"#Me c #d6815b", +"#NL c #d68260", +"#OL c #d68285", +"#q8 c #d68465", +"#By c #d6855b", +"#x. c #d68958", +"#ui c #d68965", +"#Xx c #d68e31", +".RP c #d69277", +"#Qd c #d6948f", +".S6 c #d6977c", +"#Bq c #d69c7f", +"#CO c #d69d61", +"awt c #d6a68b", +".dl c #d6a887", +"#9E c #d6a89f", +"asU c #d6ab96", +"#Ks c #d6ac7b", +".z3 c #d6bda2", +".8P c #d6c6c3", +"anK c #d6d4d9", +"#gB c #d77864", +"#yJ c #d77b59", +"#Jk c #d77c58", +"#dM c #d77d5b", +"#dY c #d77e5f", +"#vG c #d7805b", +"#Kx c #d7833d", +"#sO c #d7855f", +"#yE c #d7885d", +"#TD c #d78c62", +"#iG c #d78d66", +"#JF c #d78d67", +"#K3 c #d78f64", +"#NX c #d7916d", +"#vu c #d7965f", +"#Nk c #d79753", +"a#m c #d79895", +"#mB c #d79980", +"#yp c #d79e62", +".Tu c #d7a177", +"av2 c #d7a78c", +"#LT c #d7aa76", +"QtD c #d7aa88", +"#Nh c #d7ab94", +".y# c #d7c2ad", +".uZ c #d7c590", +"#47 c #d7ceca", +".n6 c #d7cfcd", +".jg c #d7dbec", +"#Qr c #d86d0d", +"#UH c #d8750c", +"#Dh c #d8765e", +"#aL c #d87e54", +"#ce c #d8815e", +"#Mu c #d88258", +"#jQ c #d8835c", +"#cf c #d8845f", +"#Mp c #d8855a", +"#rp c #d88760", +"#NW c #d88859", +"#rq c #d88860", +"#gW c #d88c65", +"#ks c #d88c72", +"#H9 c #d88e65", +"#H4 c #d88f5e", +"#yC c #d88f61", +".3. c #d89176", +"#vw c #d8926a", +"#j4 c #d8926f", +"#n4 c #d8946c", +"#Tq c #d8976c", +".1j c #d89979", +"#7Z c #d89c97", +".ZB c #d89e79", +"#Ei c #d89f62", +".Wr c #d89f7e", +"avu c #d8a88d", +".gj c #d8ab89", +"#mz c #d8ac99", +".zH c #d8c8a7", +"#AQ c #d8c8b7", +".5r c #d8cbe1", +"#3m c #d8ceca", +".AP c #d8cecb", +"aC7 c #d8d1d4", +".nb c #d8d4f1", +".vC c #d8d7d4", +"#Qt c #d96a12", +"#RV c #d96d11", +"aJk c #d96d70", +"#As c #d9785b", +"#lb c #d97f53", +"#KN c #d9815b", +"#jP c #d9825d", +"#Pu c #d98560", +"#yU c #d98568", +"#ro c #d98661", +"#la c #d98664", +"#Pe c #d98960", +"#xq c #d9896b", +"#lM c #d98c64", +"#v5 c #d98c68", +"#ea c #d98d63", +"#e# c #d98e65", +"#lR c #d98e6c", +"#6R c #d98e74", +"#TF c #d99068", +"#Xv c #d99325", +"#oJ c #d9946f", +"ah4 c #d99476", +"#Sl c #d9955d", +".RS c #d99678", +".1c c #d9967e", +"#sb c #d99965", +"#Hr c #d99a65", +".S4 c #d99b7f", +".1b c #d99c7b", +".V. c #d99e78", +".1a c #d99e7d", +"#wW c #d9a064", +"#pj c #d9a176", +"#tz c #d9a79e", +"auU c #d9a98e", +"#nZ c #d9ae9b", +"#CL c #d9c0a1", +".ve c #d9e2f1", +"#jK c #da7c51", +"#HQ c #da7c5e", +"#tV c #da805d", +"#Jy c #da8159", +"#yV c #da8569", +"#og c #da856f", +"#Tz c #da8655", +"#vO c #da876c", +"#z9 c #da895f", +"#sm c #da8b64", +"#rw c #da8b6f", +"#F0 c #da8f64", +"#Ne c #da9393", +".Yj c #da9478", +"#y9 c #da9576", +".RX c #da9874", +"#OR c #da9a54", +".U9 c #da9c76", +"#T. c #da9f83", +".Tt c #daa075", +"#LL c #daa2a0", +"#FO c #daac88", +"#wZ c #dab096", +"#yr c #dab097", +"#vq c #dab197", +".w# c #dac5a0", +".ye c #dac7a6", +".sd c #daca9a", +".px c #dacfa3", +".68 c #dad4bf", +"aAO c #dad4d8", +"anr c #dad6dc", +"#Qs c #db6d09", +"#EC c #db7f5d", +"#q1 c #db7f64", +"#H2 c #db805a", +"#Jl c #db805d", +"#ia c #db825d", +"#Go c #db834d", +"#QD c #db835d", +"#lO c #db8561", +"#Sa c #db8857", +"#RO c #db8a56", +"#ee c #db8a62", +"#cg c #db8a64", +"#Pd c #db8b60", +"#p1 c #db8b64", +"#fN c #db8b6b", +"#ko c #db8e6c", +"#e. c #db9167", +"#sU c #db9271", +"#ni c #db9370", +".1t c #db9479", +"#v8 c #db9575", +"#se c #db986e", +".RC c #db9882", +"#LR c #db995a", +"#Bv c #db9970", +".WV c #db9976", +"#Ux c #db997e", +"#IZ c #db9a63", +"#f. c #db9b71", +"#FL c #db9c68", +"#vn c #dba265", +"#tB c #dba266", +"#za c #dba27b", +"auh c #dbab90", +"#xJ c #dbac84", +"att c #dbac91", +"#tE c #dbb198", +".ie c #dbcac7", +".pi c #dbcfc5", +"abs c #dbcfd4", +".xy c #dbd0d2", +"aFD c #dbd1d4", +"aFE c #dbd2d5", +".n7 c #dbd5d5", +".mn c #dbdcef", +"#Nr c #dc7b2c", +"#Do c #dc7b5f", +"#aN c #dc7d5d", +"#Mv c #dc8258", +"#QE c #dc835d", +"#sq c #dc836e", +"#xv c #dc866a", +"#xc c #dc8961", +"#C4 c #dc8b61", +"#fH c #dc8e5d", +"#kk c #dc8e66", +"#LK c #dc8e8d", +"#KH c #dc8f5d", +"#n3 c #dc8f5f", +"#Ex c #dc8f62", +"#iR c #dc8f75", +"#v6 c #dc916d", +"#ru c #dc9c76", +"#qQ c #dc9d73", +"#sd c #dc9f74", +"#Qk c #dca56c", +"#Nm c #dcac76", +"#FN c #dcae87", +"#I1 c #dcb184", +"#Ek c #dcb191", +"#zW c #dcb298", +"acw c #dcc1b5", +".0D c #dcd2e2", +".k0 c #dcd7c5", +"aAK c #dcd7da", +"azz c #dcd8db", +"aJq c #dd7467", +"#Tl c #dd761d", +"#Wa c #dd8014", +"#Gk c #dd815c", +"#Jm c #dd8161", +"#ue c #dd8163", +"#Jv c #dd8363", +"#yX c #dd876b", +"#Ty c #dd8857", +"#d9 c #dd8b60", +"#Bx c #dd8f63", +"#fK c #dd916f", +"#TK c #dd926a", +"#mF c #dd926c", +"#RL c #dd9740", +"#Kq c #dd9c60", +"#Kj c #dd9d9a", +".WX c #dd9f7b", +"#ik c #dd9f81", +"#pi c #dda06a", +".rQ c #ddbba4", +".pp c #ddcab4", +".r7 c #ddccaf", +"#1Y c #ddcec6", +"ayk c #ddd0dd", +".8I c #ddd4cd", +".mJ c #ddd5b1", +"auu c #ddd9dc", +"aJp c #de4c43", +"#Ti c #de7a1e", +"#h7 c #de7d54", +"#Gh c #de8063", +"#cp c #de8064", +"#aM c #de815b", +"#nk c #de886e", +"#NE c #de895c", +"#ug c #de8968", +"#tY c #de896e", +"#QB c #de8a62", +"#vX c #de8a6e", +"#vN c #de8a6f", +"#HK c #de8b60", +"#tT c #de8b63", +"#tS c #de8d63", +"#p0 c #de8d67", +"#tP c #de8e5a", +"#QG c #de8f63", +"#vC c #de9064", +"#TC c #de9065", +"#NY c #de916c", +"#fJ c #de9371", +"#fI c #de9471", +"#TL c #de9567", +"#cD c #de9569", +".6e c #de9978", +"#Qh c #de9a4d", +".4G c #de9a79", +".RD c #de9b85", +".RE c #de9c86", +"#UP c #de9d7c", +".UF c #dea57f", +".UG c #dea77c", +"#OT c #deaa72", +"amz c #dead98", +"#Ht c #deb289", +"aHV c #debac1", +".gK c #ded1cf", +"acX c #ded1d8", +".uT c #ded4b6", +".5z c #ded4e0", +"aB5 c #ded5da", +".uc c #dedfda", +"aJl c #df5454", +"#C9 c #df8262", +"#fD c #df845f", +"#h6 c #df8464", +"#H3 c #df855d", +"#UK c #df8631", +"#iK c #df8663", +"#BT c #df876c", +"#xk c #df876d", +"#vM c #df886e", +"#K7 c #df895f", +"#Md c #df8a63", +"#rn c #df8a67", +"#vF c #df8b63", +"#fx c #df8c6a", +"#ok c #df8d6c", +"#hl c #df8f74", +"#hk c #df9075", +"aFe c #df91b3", +"#B0 c #df926e", +"#hd c #df945f", +"#TE c #df956d", +"#YZ c #df982e", +"#Sk c #df9964", +"#iE c #df9974", +"#p6 c #df9c77", +".RT c #df9c7d", +".RF c #df9d87", +"#wV c #dfa15b", +"#r7 c #dfa57a", +".X2 c #dfa67c", +".Wt c #dfa77d", +"a#l c #dfb0a8", +"#Bk c #dfb59b", +"#r9 c #dfb6a2", +"#Fg c #dfc5cb", +"aG3 c #dfd5d8", +"aEi c #dfd6d9", +".k5 c #dfd8b5", +"#.t c #dfd8d6", +".pX c #dfdbe9", +"awB c #dfdcde", +"aek c #dfdce3", +"avL c #dfdde4", +".jt c #dfe0d7", +"#Jn c #e08466", +"#xi c #e0856c", +"#Dn c #e0886d", +"#Ar c #e0896d", +"#Ma c #e08a5b", +"#S# c #e08b59", +"#P# c #e08b61", +"#Jg c #e08d60", +"#sN c #e08d68", +"#BZ c #e08f6d", +"#dJ c #e09074", +".7V c #e09258", +"#mO c #e0926b", +"#io c #e09773", +"#kb c #e0977a", +"#FK c #e09c63", +".RJ c #e09c80", +".U7 c #e09d7a", +".22 c #e09e7e", +".WW c #e09f7c", +"#Qi c #e0a159", +"#vm c #e0a15b", +".1D c #e0a188", +".S7 c #e0a286", +".Ts c #e0a376", +".ZR c #e0a384", +"#qI c #e0a77b", +"#CQ c #e0b494", +".id c #e0cdc6", +"aMJ c #e0d1df", +".tt c #e0d2b3", +".D. c #e0d2c5", +".wE c #e0d9e0", +"aeN c #e0dde1", +"#1I c #e17a64", +"#Dp c #e18164", +"#xg c #e18464", +"#EW c #e18567", +"#K6 c #e18759", +"#gN c #e1876a", +"#RH c #e1887a", +"#Mb c #e1895a", +"#KL c #e18b5c", +"#JE c #e18b5d", +"#yT c #e18c70", +"#Pc c #e19165", +"#QH c #e19167", +".9D c #e1936d", +"#TJ c #e19371", +"#yD c #e19467", +"#xD c #e19571", +"#Kp c #e19657", +"#uj c #e19672", +"#JA c #e1986a", +"#rx c #e1997d", +"#qR c #e19c73", +".25 c #e19f7f", +".RG c #e19f89", +"#w4 c #e1a172", +"#yo c #e1a25c", +"#iw c #e1a281", +"#tA c #e1a35d", +"#CR c #e1b89e", +"ah1 c #e1bba7", +".yi c #e1c5a3", +".qK c #e1d1c2", +"aDt c #e1d6db", +".xj c #e1d7e8", +".o# c #e1d8b1", +".y3 c #e1d9e5", +".nc c #e1dcfa", +"azx c #e1dee1", +"apD c #e1dee4", +"ava c #e1dfe6", +"#RW c #e2741a", +"#BU c #e28164", +"#pz c #e2845f", +"#sL c #e28a6a", +"#An c #e28a6f", +"#kl c #e28b67", +"#K2 c #e28c61", +"#q7 c #e28c6c", +"#xm c #e28c71", +"#pZ c #e28f6c", +"#NP c #e2906f", +"#TH c #e29079", +"#oB c #e2916e", +"#vE c #e29267", +"#sP c #e2926a", +"#mG c #e2926d", +"#EZ c #e2926f", +".9C c #e2936d", +"#q9 c #e29376", +".9E c #e2946e", +"#x# c #e29568", +"#LQ c #e29754", +"#dR c #e29870", +"#z# c #e29c7e", +"#n2 c #e29e77", +".RK c #e29e82", +".4I c #e29f7e", +".RH c #e2a08a", +"#CN c #e2a45e", +"amu c #e2ae92", +"#pl c #e2b8a5", +".v7 c #e2cbaf", +".sa c #e2cdbe", +".n1 c #e2ceac", +".6Z c #e2d4e4", +"#.q c #e2d5cf", +".Cp c #e2d7d9", +"ael c #e2ddde", +"agR c #e2dfe6", +"arh c #e2e2e5", +".ju c #e2e3da", +"aLC c #e37d79", +"#q3 c #e38370", +"#Gj c #e38663", +"#YX c #e3866e", +"#R9 c #e38b57", +"#jJ c #e38b6a", +"#K5 c #e38c5e", +"#fF c #e38c61", +"#yY c #e38d71", +"#d1 c #e39072", +"#L9 c #e39463", +"#jV c #e3956d", +"#w. c #e39571", +"#n# c #e39573", +"#Du c #e39672", +"#lJ c #e39873", +"#rc c #e3997c", +"#g3 c #e39a75", +"#Y1 c #e39b41", +".RW c #e3a07e", +".RU c #e3a180", +"#zS c #e3a55f", +".X9 c #e3a687", +".Ws c #e3aa83", +"#l. c #e3ad9e", +"#wY c #e3b797", +"#vp c #e3b898", +"#El c #e3b99f", +"#qK c #e3baa6", +".u0 c #e3d19d", +".sb c #e3d1b1", +".zU c #e3d2ba", +".5w c #e3d4d0", +".n4 c #e3d6c8", +".qZ c #e3d6cb", +"aG2 c #e3dadd", +".8S c #e3dbc6", +".k4 c #e3ddb8", +"anH c #e3dee3", +"avF c #e3dfe2", +"ap7 c #e3dfe5", +"amm c #e3e2e8", +".ub c #e3e4e4", +"#Jt c #e4896e", +"#od c #e48c6b", +"#S. c #e48d59", +"#KM c #e48d5e", +"#Ji c #e48d61", +"#pS c #e48d74", +"#K0 c #e48e66", +"#yR c #e48e73", +"#Ds c #e4906f", +"#sn c #e49370", +"#BE c #e49377", +"#Sc c #e49668", +"#na c #e49673", +"#F1 c #e4986e", +"aI8 c #e498c4", +"#0o c #e49b38", +"aJj c #e49da2", +"#w# c #e49f6e", +".4H c #e4a080", +".RV c #e4a17f", +".24 c #e4a282", +".WU c #e4a381", +".UI c #e4a583", +"#Bh c #e4a660", +".X1 c #e4ab84", +"#Bj c #e4b898", +"#tD c #e4b998", +"#7d c #e4ba9f", +"#qL c #e4bfaa", +".n2 c #e4d3b6", +"a#P c #e4d8dc", +"aDq c #e4d9de", +"aka c #e4dae2", +"apo c #e4e0e6", +"awc c #e4e1e8", +"ayf c #e4e2e4", +"aqm c #e4e3e7", +"#yO c #e58e74", +"#NO c #e59074", +"#Ac c #e59277", +"#KJ c #e59363", +"#oE c #e5956e", +"#kj c #e59770", +"#aH c #e59972", +"#IY c #e59b60", +"#ps c #e59d75", +"#Hq c #e59e64", +"#OQ c #e59f54", +".4N c #e5a084", +".1s c #e5a386", +".Tr c #e5a577", +".UJ c #e5a584", +".UH c #e5a684", +".Wu c #e5a887", +".X8 c #e5a889", +".Ye c #e5a98a", +".7F c #e5b1a2", +"#zV c #e5ba9a", +"aEI c #e5d8de", +".re c #e5e1e7", +".tQ c #e5e6e8", +"#pU c #e68b73", +"#yL c #e68d74", +"#EX c #e68e6e", +"#vL c #e68e75", +"#NF c #e68f63", +"#yW c #e69074", +"#dW c #e6926f", +"#dP c #e69570", +"#Dt c #e69673", +"#dV c #e69772", +"#fz c #e69874", +"#Nj c #e69e56", +"#qO c #e6a46f", +".Tq c #e6a576", +".Wv c #e6a988", +"#sc c #e6ad81", +"#s. c #e6c2ad", +"aa0 c #e6c4b9", +".wd c #e6d5ab", +"aF0 c #e6d6dd", +"anB c #e6dae0", +".D1 c #e6dbde", +".D0 c #e6dcdb", +".n5 c #e6ddd5", +".h6 c #e6e0da", +".t1 c #e6e2df", +"aeJ c #e6e3e7", +".ue c #e6e4fd", +".y7 c #e6e5e1", +".mU c #e6eefa", +"#Dg c #e7846c", +"#Gg c #e7896e", +"#yK c #e78a69", +"#H1 c #e78c68", +"#xh c #e78c73", +"#fC c #e78d6b", +"#qZ c #e78f6a", +"#sM c #e7916e", +"#NM c #e79373", +"#fj c #e79476", +"aDX c #e794b7", +"#dI c #e7967b", +"#Bw c #e79a6d", +"#lL c #e79a73", +"#1K c #e79c3f", +"#iH c #e79c74", +"#n6 c #e79c75", +"#Gr c #e79d72", +"#pp c #e7a06d", +"#yz c #e7a085", +".23 c #e7a484", +"#pr c #e7a57c", +".ZK c #e7a98a", +".WR c #e7af8f", +".n9 c #e7dbc4", +"#3n c #e7ddd7", +".Co c #e7dddc", +".nU c #e7ded4", +".b8 c #e7e3ed", +".mR c #e7eef3", +".jE c #e7f1eb", +"#0m c #e8846d", +"#Ju c #e88d6f", +"#N0 c #e88f68", +"#d7 c #e88f6a", +"#BS c #e89075", +"#tU c #e8916b", +"#m9 c #e89379", +"#TI c #e8987c", +"#Ey c #e8996d", +"#iJ c #e89a72", +"#n. c #e89b79", +"#p7 c #e89c7c", +"#mX c #e89d7c", +".7Z c #e89f7f", +".Yi c #e8a086", +".29 c #e8a488", +".RI c #e8a489", +"#AB c #e8a68c", +".UK c #e8a988", +"#w3 c #e8aa75", +"#qH c #e8ab75", +".U5 c #e8ab85", +"#3b c #e8b47c", +"#n0 c #e8bba9", +".yl c #e8cba9", +".qu c #e8d5ba", +".5q c #e8d8e8", +".66 c #e8dcc5", +".2a c #e8dce6", +".8R c #e8ddca", +".32 c #e8dff5", +"alg c #e8e0e9", +".YZ c #e8e3fb", +"af3 c #e8e4e8", +".AI c #e8e4f5", +"aeM c #e8e5e9", +"awF c #e8e5ec", +"#HR c #e98b6f", +"#Pb c #e99068", +"#yM c #e99077", +"#Pa c #e99169", +"#Aq c #e99276", +"#K1 c #e99368", +"#qY c #e9936b", +"#fG c #e99768", +"#BG c #e9977b", +"#Sb c #e99869", +"#Jf c #e9996a", +"#rd c #e99d80", +"#rb c #e9a284", +".ZT c #e9a488", +".Ww c #e9ab8a", +".X7 c #e9ad8d", +"#pm c #e9c1ad", +"afF c #e9cfbf", +".n0 c #e9d4b0", +".po c #e9d4b9", +".zV c #e9d8be", +".qR c #e9dbae", +"aEH c #e9dbe1", +".2g c #e9dce3", +".sn c #e9ddd0", +"a.B c #e9dfe1", +"aFG c #e9e0e3", +".0x c #e9e0eb", +"##X c #e9e2f0", +"afB c #e9e7ee", +"#py c #ea8c65", +"#q2 c #ea8c76", +"#Jr c #ea8c77", +"#HX c #ea8d73", +"#oa c #ea8f67", +"#Ao c #ea9277", +"#gU c #ea9370", +"#d8 c #ea956c", +"#z8 c #ea9c6f", +"#sV c #ea9d80", +"#lK c #ea9e78", +"#ci c #ea9f78", +"#r. c #ea9f84", +"#1M c #eaa051", +"#vx c #eaa07f", +"#0q c #eaa14d", +".Yh c #eaa287", +".6l c #eaa589", +"#ft c #eaa780", +"#yx c #eaaa87", +".Z2 c #eaaa8e", +"#z2 c #eaad8e", +"#yv c #eaaf7c", +".WQ c #eaaf90", +"#z1 c #eab087", +"amy c #eab69a", +".wc c #ead6ab", +".tz c #ead6b3", +"ai8 c #ead9e4", +"ahY c #eadae3", +".C9 c #eadccf", +".0u c #eaddc8", +".YW c #eadfd9", +"#48 c #eadfdb", +".zf c #eadfe1", +"aEj c #eae2e5", +".tv c #eae3d2", +"QtS c #eae3f8", +"#De c #eb8e76", +"#d5 c #eb9070", +"#Js c #eb9076", +"#JC c #eb9362", +"#aK c #eb9367", +"#xl c #eb947a", +"#Sf c #eb957e", +"#Mc c #eb966e", +"#Pf c #eb9a74", +"#oC c #eb9a75", +"#hc c #eb9d6a", +"#lD c #eb9d81", +"#Sj c #eba372", +"#kg c #eba37d", +".4J c #eba787", +".U8 c #eba985", +".26 c #eba989", +".1k c #ebab8b", +".1r c #ebad8f", +".X3 c #ebaf8f", +".Yc c #ebb192", +".WJ c #ebb294", +"#mA c #ebbaa9", +"#2q c #ebc1ab", +"ai9 c #ebe0e6", +".He c #ebe1e0", +"abU c #ebe1ea", +".rT c #ebe5cf", +".h8 c #ebe5df", +".sz c #ebe6e7", +"au9 c #ebe8eb", +".sq c #ebf1fc", +".jG c #ebfcff", +"#Ab c #ec8f6e", +"#xj c #ec9279", +"#Jz c #ec936a", +"#pW c #ec9577", +"#NN c #ec977a", +"#C3 c #ec9d71", +"#HI c #ec9e72", +"#ol c #ec9f7f", +"#w7 c #eca285", +"#iq c #ecab89", +".Th c #ecac88", +".S8 c #ecad92", +".WH c #ecb395", +"abu c #ecc8bd", +".mz c #ecd9af", +".8Q c #ecddd3", +".r9 c #ece0d2", +"afC c #ece3e6", +".67 c #ece4c7", +".yU c #ece7ff", +"ans c #ece8ed", +"axw c #ece9ec", +".Cd c #ece9fc", +"#Jo c #ed9075", +"#q6 c #ed9574", +"#Ap c #ed957a", +"#rm c #ed9675", +"aLB c #ed9988", +"#pR c #ed9b80", +"#pJ c #ed9d7d", +".7U c #ed9f79", +"#Pr c #eda06e", +"#QR c #eda06f", +"#iI c #eda078", +"#mC c #eda27d", +"aMx c #eda890", +".Tb c #edad89", +"#z3 c #edad94", +".Wx c #edb08f", +".UW c #edb192", +".WS c #edb28d", +".Y. c #edb293", +".8. c #edb99f", +".pf c #eddccb", +".mA c #edddb7", +"QtB c #eddee7", +"#8Z c #ede1df", +"anz c #ede1e6", +"#6N c #ede2df", +".mH c #ede3cf", +".vK c #ede3e2", +".mI c #ede5bf", +"aml c #ede6f0", +"aii c #ede9ed", +"aw8 c #edebf2", +".xo c #edede2", +".vB c #edeede", +"#HU c #ee8f7a", +"#co c #ee9071", +"#Ag c #ee997e", +"#Ae c #ee9a7f", +"#Sg c #ee9a81", +"#Pq c #eea070", +"#nb c #eea07c", +"#Je c #eea171", +"#pK c #eea284", +"#lI c #eea47f", +"#w5 c #eeaa85", +".U6 c #eead89", +".ZS c #eead90", +".Te c #eeae8a", +".UR c #eeb698", +"aen c #eedcd3", +".2# c #eee1df", +"anA c #eee2e8", +"#.s c #eee6dc", +"avK c #eeebf2", +".ud c #eeedff", +".lc c #eefaff", +"#BD c #ef9271", +"#sK c #ef9576", +"#H6 c #ef9763", +"#yQ c #ef997e", +"#BI c #ef9a7f", +"#mU c #ef9c78", +"#oD c #ef9f79", +"#KI c #efa06f", +"#xG c #efa289", +".UL c #efaf8e", +".1n c #efaf8f", +".U0 c #efaf92", +".X6 c #efb293", +".X4 c #efb393", +".WI c #efb798", +"a.y c #efc8b7", +".xx c #efe5e4", +".b7 c #efe8f0", +".sD c #efeaeb", +"aeL c #efebf0", +"awb c #efecf3", +".q4 c #eff1fa", +"#sJ c #f09477", +"#vK c #f0977d", +"#yP c #f0997f", +"#lS c #f09b7f", +"#ou c #f0a085", +"#F2 c #f0a279", +"#lm c #f0a87f", +"#lt c #f0a886", +"#lB c #f0aa8c", +".Tf c #f0b08c", +".Yf c #f0b193", +".WC c #f0b799", +".UU c #f0b89a", +".wa c #f0dbb4", +"#7f c #f0e1db", +".3Y c #f0e1e1", +"#99 c #f0e5e7", +".aB c #f0e9f2", +".rh c #f0ebf1", +".rg c #f0ecf2", +"#yN c #f1997f", +"#d0 c #f19b7d", +"#fh c #f19c7e", +"aLA c #f19c85", +"#d2 c #f1a083", +"#QM c #f1a27d", +"#QS c #f1a472", +"#g4 c #f1a480", +"#ki c #f1a57e", +"#kh c #f1a780", +"#n5 c #f1aa83", +".Tk c #f1ad89", +".Tj c #f1ad8a", +".Yg c #f1ad91", +"#j5 c #f1af8e", +".1l c #f1b191", +".1m c #f1b192", +"#ir c #f1b393", +".ZL c #f1b495", +".Yd c #f1b697", +".UT c #f1b99b", +"#Ff c #f1d5da", +".5n c #f1decd", +".60 c #f1e2f8", +"agT c #f1e6e7", +".oA c #f1ecff", +".rf c #f1edf3", +"af1 c #f1eef2", +".AH c #f1eff5", +"#H0 c #f29674", +"#HY c #f2967a", +"#BR c #f2987d", +"#dN c #f29a77", +"#fB c #f29b7a", +"#pV c #f29b7d", +"#fg c #f29b7e", +"#yS c #f29d81", +"#F3 c #f2a179", +"#BF c #f2a185", +"aI6 c #f2a5c0", +"#xH c #f2a786", +"#jW c #f2aa84", +"#ra c #f2ab91", +"#iD c #f2ae89", +".1q c #f2b292", +".WP c #f2b295", +".S9 c #f2b498", +".ZO c #f2b596", +".Y# c #f2b798", +".WD c #f2b99b", +".WE c #f2ba9b", +".US c #f2ba9c", +"amx c #f2bea2", +".U3 c #f2c09e", +"aa1 c #f2c3be", +"aaf c #f2dcda", +".x1 c #f2dfb8", +"adm c #f2e3ec", +".3T c #f2e3f0", +".gH c #f2e4e2", +"#8v c #f2e6e6", +".0w c #f2e7e7", +"adn c #f2eaf6", +".vA c #f2edd5", +".y6 c #f2eee4", +"asu c #f2eef4", +".pW c #f2eefb", +"axx c #f2eff6", +".pD c #f2f0e6", +"#HT c #f3947d", +"#Df c #f3967e", +"#H8 c #f39c6c", +"#oK c #f3a486", +"#fq c #f3aa82", +".Tl c #f3af8a", +".Ti c #f3af8d", +".28 c #f3b191", +"#mT c #f3b296", +".Tc c #f3b38f", +"#yw c #f3b68a", +".Wy c #f3b695", +".ZM c #f3b697", +".X5 c #f3b797", +"#g0 c #f3ba92", +"amv c #f3bfa3", +".3Q c #f3e2ce", +"#5A c #f3e3da", +".if c #f3e3e4", +".n3 c #f3e4ce", +".2. c #f3e4d6", +"aag c #f3e5e9", +".b5 c #f3e7eb", +"aDs c #f3e8ed", +".xb c #f3ecff", +".vz c #f3edda", +"ahe c #f3eff3", +".pc c #f3f2e8", +"alh c #f3f2f6", +".og c #f3f9f6", +"#rj c #f4997c", +"#Al c #f49a7f", +"#so c #f4a083", +"#Af c #f4a084", +"#Ad c #f4a185", +"#HJ c #f4a479", +"#Pl c #f4a480", +"#Po c #f4a579", +"#kf c #f4ad88", +".Ta c #f4b48f", +".Tg c #f4b490", +".1o c #f4b495", +".T. c #f4b69a", +".Ya c #f4b99a", +"abR c #f4d3c3", +".ww c #f4d5b0", +"alx c #f4d9d2", +"#3Q c #f4e2d8", +"abT c #f4e2e7", +".3S c #f4e3e5", +".AQ c #f4e9e8", +".kY c #f4eacc", +"aB1 c #f4ebf0", +".xl c #f4ede7", +".h9 c #f4efe8", +".sA c #f4f0f0", +"af0 c #f4f0f4", +"aoA c #f4f0f5", +"aoz c #f4f0f6", +"av# c #f4f1f8", +".vd c #f4f3ef", +".la c #f4f9ff", +".lb c #f4fdff", +"#pY c #f5a17f", +"#nj c #f5a386", +"#Pn c #f5a67d", +"#fv c #f5a684", +"#nd c #f5a780", +"#QQ c #f5a879", +"#r# c #f5ad92", +".4M c #f5b296", +".UM c #f5b594", +"#j6 c #f5b797", +".T# c #f5b79b", +".ZQ c #f5b899", +".tb c #f5d2c6", +"agW c #f5d5c0", +".wv c #f5d6b2", +"#8Y c #f5ddd3", +"akr c #f5ded8", +"aLM c #f5e0f0", +".65 c #f5e6da", +"any c #f5e9ee", +".YV c #f5eadc", +"#.r c #f5eae0", +"QtW c #f5eaef", +"afD c #f5ebea", +".b6 c #f5ebf1", +".6X c #f5ecdd", +".vy c #f5efe4", +".xn c #f5f1de", +".y5 c #f5f1eb", +"ahf c #f5f1f5", +"akb c #f5f3f5", +"awE c #f5f3fa", +".jF c #f5ffff", +"#Am c #f69d82", +"#Gq c #f69f6d", +"#pX c #f6a080", +"#Pp c #f6a87a", +".7T c #f6a882", +"#v9 c #f6a88e", +"#Sd c #f6a97e", +"#mY c #f6af8f", +".ZN c #f6b99a", +".U4 c #f6bb95", +".WG c #f6bd9e", +".UQ c #f6bea0", +"#j0 c #f6c1a1", +".xY c #f6e2c1", +"a.A c #f6e4e1", +".r8 c #f6e7d1", +".ze c #f6ebeb", +"##V c #f6ecee", +"QtX c #f6eff9", +"aAL c #f6f0f3", +".pG c #f6f1ef", +"aeK c #f6f2f6", +"arM c #f6f2f8", +".oi c #f6f3fe", +"apC c #f6f5f9", +".q5 c #f6fbff", +".mT c #f6ffff", +"#HS c #f7997f", +"#HW c #f79a81", +"#HZ c #f79b7c", +"#cn c #f79c79", +"#fm c #f79e7b", +"#Pm c #f7a781", +"#QN c #f7a982", +"#nc c #f7a984", +"#Si c #f7ac81", +"#om c #f7ad8e", +"#pL c #f7ae92", +"#w6 c #f7af91", +"#AA c #f7af9a", +".6h c #f7b291", +".6f c #f7b292", +".UZ c #f7b699", +".Td c #f7b793", +"ajq c #f7dbd0", +".wq c #f7e3c1", +".yd c #f7e4c4", +".n8 c #f7e8e4", +".3Z c #f7eaee", +"agS c #f7eaf0", +".Xm c #f7ecec", +".qx c #f7efdc", +".tP c #f7f0e5", +"aBI c #f7f2f4", +".t2 c #f7f3f0", +"app c #f7f3f9", +"aw5 c #f7f4f7", +"alz c #f7f5f9", +".mP c #f7fefe", +"#HV c #f89985", +"#Jq c #f89a84", +"#BP c #f89c82", +"#d4 c #f8a082", +"#BJ c #f8a287", +"#QI c #f8a883", +"#g6 c #f8ab86", +"#Se c #f8ac81", +"#z. c #f8ae96", +"#RM c #f8af60", +"#aJ c #f8af87", +"#m5 c #f8b092", +".6g c #f8b392", +".Tm c #f8b48e", +".Tp c #f8b58b", +".Wz c #f8ba99", +"#lp c #f8c4a4", +".3R c #f8e7dc", +".ts c #f8e8c2", +".kW c #f8e9bd", +"alm c #f8eae3", +".5y c #f8ebf2", +".FF c #f8eded", +"aDr c #f8edf2", +".vc c #f8eedd", +".nT c #f8f0e6", +".gR c #f8f1f0", +".t4 c #f8f3f0", +".sC c #f8f3f4", +"aAM c #f8f3f6", +".kL c #f8f3fa", +"avI c #f8f4f7", +".AF c #f8f4fa", +"amF c #f8f5f9", +"ap8 c #f8f5fa", +".oh c #f8f7fe", +"aqn c #f8f8fb", +"amp c #f8fbf6", +"amD c #f8fefe", +"amC c #f8ffff", +"#fi c #f9a688", +"#fO c #f9a988", +"#pQ c #f9aa8f", +"#lu c #f9b494", +"#pM c #f9b499", +".WO c #f9b69a", +"#fs c #f9b991", +".UN c #f9b998", +"#m1 c #f9b99d", +".UX c #f9bb9d", +".WT c #f9bc98", +"#il c #f9bda0", +"amw c #f9c5a9", +"afZ c #f9dcce", +".5p c #f9e7eb", +".fg c #f9e8ef", +".aA c #f9eef4", +".td c #f9efd9", +".dD c #f9eff5", +"aB4 c #f9f1f5", +"ahZ c #f9f2ed", +".vo c #f9f2ff", +"aAN c #f9f4f7", +"azy c #f9f5f8", +"af2 c #f9f5f9", +"avJ c #f9f6fd", +"alj c #f9fcf7", +"amE c #f9fcf8", +"amo c #f9fefb", +".mQ c #f9ffff", +"#JD c #fa9f6d", +"#sp c #faa38a", +"#cl c #faa67d", +"#pI c #faa686", +"#Pi c #faa889", +"#in c #faaf89", +"#pP c #fab093", +"#iy c #fab395", +"#V5 c #fab44d", +".To c #fab68d", +"#yy c #fab69b", +".Tn c #fab78f", +".WA c #fabd9c", +".Yb c #fabfa1", +"#it c #fac0a1", +".UP c #fac2a4", +"#5z c #fadac3", +"abS c #fae0dc", +".5x c #faecec", +".0v c #faede0", +".vp c #faf3ff", +"ant c #faf6fc", +".AG c #faf7f9", +"anJ c #faf7fd", +"aoM c #faf9fc", +"aoN c #fafafd", +"alB c #fafcfb", +"amn c #fafeff", +".of c #fafffc", +"#BO c #fb9e84", +"#Jp c #fb9e85", +"#H7 c #fb9f6b", +"#oe c #fba487", +"#of c #fba58c", +"#fA c #fba685", +"#Ah c #fba78c", +"#BH c #fba88d", +".7R c #fbac86", +"#Sh c #fbac88", +"#QP c #fbad81", +"#kc c #fbaf92", +".6k c #fbb89c", +".27 c #fbb999", +".UO c #fbbb9a", +".WL c #fbbd9f", +".WB c #fbbe9d", +".ZP c #fbbe9f", +".WK c #fbbfa0", +"#iu c #fbc19f", +".WF c #fbc2a3", +".UV c #fbc3a5", +"ahc c #fbdbca", +"adk c #fbddd0", +".yk c #fbdebc", +".uB c #fbe6d9", +"anx c #fbeef4", +"aem c #fbf2f0", +".xk c #fbf3f8", +".ia c #fbf6ef", +".t3 c #fbf6f4", +"aq4 c #fbf7fd", +"aw7 c #fbf8fa", +"ayh c #fbf9fb", +".jh c #fbfdff", +".mS c #fbffff", +"#BQ c #fca187", +"#rl c #fca384", +"#dX c #fca484", +"#fw c #fcaa88", +"#Ez c #fcab81", +"#QO c #fcae85", +"#1L c #fcb258", +"#gX c #fcb48c", +"#dS c #fcb58c", +"#os c #fcb597", +"#mS c #fcba9d", +"#op c #fcba9e", +"#jX c #fcbb97", +"#g1 c #fcbf98", +"#j7 c #fcc0a1", +".tA c #fcebb7", +".x2 c #fcecc5", +"QtV c #fcecee", +".so c #fcf3e7", +".nS c #fcf5ea", +"akt c #fcf5f2", +"aC6 c #fcf5f7", +".pu c #fcf5fc", +".nQ c #fcf6f2", +".xm c #fcf7e8", +"awC c #fcf8fb", +"aih c #fcf8fc", +".pV c #fcf8ff", +"avG c #fcf9fb", +"awD c #fcf9fc", +"akv c #fcf9fd", +"ayi c #fcfafc", +"aj# c #fcfbf2", +"anI c #fcfcff", +"akc c #fcfef9", +"ali c #fcffff", +"#rk c #fda385", +"#QK c #fdac8d", +"#Tb c #fdb660", +"#Xw c #fdb74e", +".4L c #fdb999", +".WM c #fdbb9e", +".UY c #fdbc9f", +".1p c #fdbd9e", +"#lz c #fdbe9d", +"#k. c #fdc09f", +"#is c #fdc2a3", +"aie c #fdddcc", +"a.z c #fde0d7", +"adl c #fde5e4", +".tc c #fde8d6", +".19 c #fdeed8", +"afE c #fdeee6", +".az c #fdeff2", +".s. c #fdf5f3", +"aB3 c #fdf5f9", +".mD c #fdf6e5", +".tx c #fdf8ef", +".mq c #fdf9ed", +"aw# c #fdf9fc", +"ajt c #fdf9fd", +"anv c #fdf9ff", +"aw4 c #fdfafc", +"alk c #fdfbf4", +"ayj c #fdfbfd", +"aoO c #fdfbff", +"alA c #fdffff", +"#d3 c #feaa8d", +"#Pj c #feac8e", +"#UA c #feb758", +"#on c #feb89b", +"#ke c #feb995", +"#or c #feb99a", +".4K c #feba9a", +"#mZ c #feba9c", +"#iC c #febb96", +"#ly c #fec09f", +"#lw c #fec0a2", +"#iv c #fec2a0", +".yf c #fee2c0", +"acY c #fee5de", +".wb c #fee9c0", +".yb c #feebd0", +".tB c #feedba", +".k3 c #fef5e1", +".tw c #fef8ec", +".i# c #fef8f1", +".y4 c #fef9fa", +".sB c #fefafa", +"aw6 c #fefafd", +"anu c #fefaff", +"av. c #fefbfd", +"ayg c #fefcfe", +"awa c #fefcff", +".nP c #fefeff", +"#BN c #ffa288", +"#Ak c #ffa68b", +"#Gp c #ffa872", +"#BL c #ffa88e", +"#BM c #ffa88f", +"#gT c #ffaa88", +"#dO c #ffab87", +"#EY c #ffad8c", +"#Aj c #ffad92", +"#lF c #ffad93", +"#fn c #ffae8b", +"#QJ c #ffae8d", +"#Pk c #ffaf8c", +"#gS c #ffaf8d", +"#lE c #ffaf95", +"#lr c #ffb08c", +"#Pg c #ffb08d", +"#Ph c #ffb08f", +"#BK c #ffb096", +"#QL c #ffb395", +".7S c #ffb48e", +"#iA c #ffb499", +"#j1 c #ffb58f", +"#Ai c #ffb59a", +"#aI c #ffb68e", +"#lH c #ffb693", +"#iz c #ffb699", +"#dU c #ffb790", +"#im c #ffb892", +"#mP c #ffb994", +"#g5 c #ffb995", +"#0p c #ffba5c", +"#pO c #ffba9c", +".6j c #ffbb9b", +"#pN c #ffbb9c", +"#ot c #ffbb9e", +"#kd c #ffbba0", +"#ck c #ffbc94", +"#lG c #ffbc99", +".WN c #ffbca0", +"#iB c #ffbca1", +"#Y0 c #ffbd58", +".6i c #ffbd9d", +"#gZ c #ffbe94", +"#cj c #ffbe96", +"#lv c #ffbe9f", +"#m0 c #ffbea1", +"#fr c #ffbf96", +"#lq c #ffbf9a", +"#mQ c #ffbf9d", +"#lA c #ffbf9f", +"#oq c #ffc0a1", +"#dT c #ffc197", +"#mR c #ffc1a2", +"#m4 c #ffc2a3", +"#gY c #ffc39a", +"#j2 c #ffc39e", +"#m3 c #ffc3a3", +"#k# c #ffc4a4", +".U1 c #ffc4a5", +"#oo c #ffc4a7", +"#m2 c #ffc5a5", +"#j9 c #ffc6a4", +"#lo c #ffc7a4", +"#lx c #ffc7aa", +"#jY c #ffc8a6", +"#j8 c #ffc9aa", +"#ln c #ffcaa3", +".U2 c #ffcaaa", +"#jZ c #ffcdac", +"aI7 c #ffd2f3", +".6y c #ffdbc4", +"amr c #ffdcc0", +".uA c #ffddd8", +"#8X c #ffdfcd", +"#Wh c #ffdfdd", +"aae c #ffe0d6", +"#7e c #ffe6d5", +"abt c #ffe7de", +"aHH c #ffe7fb", +"a#Q c #ffe8db", +"ahd c #ffebde", +"acZ c #ffebe1", +".wr c #ffecc8", +".rR c #ffecd4", +"amB c #ffece7", +".tr c #ffedc3", +"agV c #ffeddf", +".x3 c #ffeecb", +".yc c #ffeed1", +"aif c #ffeee1", +".qP c #ffefd2", +".ic c #ffefe5", +".ay c #ffeff0", +"QtU c #fff0ef", +"ake c #fff1e7", +".5o c #fff2e9", +"ajb c #fff3e3", +".wD c #fff3f2", +"aly c #fff3f3", +".kV c #fff4c2", +".pw c #fff4da", +".pq c #fff4e4", +"QtT c #fff4f2", +".nR c #fff5eb", +"ajs c #fff5ec", +".pr c #fff5ed", +".ps c #fff5f4", +".mB c #fff6d7", +".pe c #fff6e8", +".qO c #fff6eb", +".pt c #fff6fa", +"aoB c #fff6fc", +".x4 c #fff7db", +".rS c #fff7df", +"ah0 c #fff7e5", +"all c #fff7ef", +".gJ c #fff7f6", +".mC c #fff8e1", +"ajr c #fff8ef", +".q0 c #fff8f0", +".mG c #fff8f6", +".h5 c #fff8f7", +".mp c #fff8f8", +".tO c #fff9e8", +".ms c #fff9ee", +".pv c #fff9f1", +"aks c #fff9f6", +".gI c #fff9f7", +"aBH c #fff9fb", +".qM c #fff9fd", +"anw c #fff9ff", +".ya c #fffae2", +".qv c #fffae3", +"aja c #fffaed", +".q1 c #fffaf4", +".q2 c #fffaf5", +".qL c #fffaf9", +".pI c #fffafd", +"aB2 c #fffafe", +".kO c #fffbef", +"akd c #fffbf0", +".gQ c #fffbf3", +".ib c #fffbf5", +".gM c #fffbf9", +".s# c #fffbfb", +".pH c #fffbfc", +"aBG c #fffbfd", +".qN c #fffbff", +".qw c #fffce8", +".pb c #fffcee", +".mE c #fffcf0", +".sp c #fffcf6", +"avH c #fffcfe", +".mo c #fffcff", +".kN c #fffdf0", +".gP c #fffdf1", +".pE c #fffdf5", +"aig c #fffdff", +".mr c #fffef3", +".pd c #fffef4", +"amq c #fffef8", +".pF c #fffef9", +"aj. c #fffefc", +".ji c #fffeff", +".kM c #fffff4", +".mF c #fffff7", +"agU c #fffff8", +"aku c #fffffa", +".q3 c #fffffb", +".i. c #fffffc", +".l# c #ffffff", +"Qt.Qt#QtaQtbQtcQtdQteQtfQtgQthQtiQtjQtkQtlQtmQtnQtoQtpQtqQtrQtrQtsQttQtuQtvQtwQtxQtyQtzQtAQtBQtCQtDQtEQtFQtGQtHQtIQtJQtKQtLQtMQtNQtOQtPQtQQtRQtSQtTQtUQtVQtWQtXQtYQtZQt0Qt1Qt2Qt3Qt4Qt5Qt6Qt7Qt5Qt8Qt9.#..##.#a.#b.#c.#d.#e.#f.#g.#h.#i.#j.#k.#l.#m.#n.#o.#p.#q.#r.#s.#t.#u.#v.#w.#x.#y.#z.#A.#A.#B.#C.#D.#E.#E.#D.#C.#B.#D.#D.#F.#F.#F.#G.#H.#I.#J.#K.#L.#M.#N.#O.#P.#Q.#R.#S.#S.#T.#U.#V.#V.#W", +".#XQt#.#Y.#ZQtc.#0.#1Qtf.#2.#3.#4.#5Qtk.#6.#7.#8QtoQtp.#9QtrQtrQtsQtt.a..a#.aa.ab.ac.ad.ae.af.ag.ah.ai.aj.ak.al.am.an.ao.ap.aq.ar.as.at.au.av.aw.ax.ay.az.aA.aB.aC.aD.aE.aF.aG.aH.aI.aJ.aK.aL.aM.aN.aO.aP.aQ.aR.aS.aT.aU.aV.aW.aX.aY.aZ.a0.a1.a2.a3.a4.#o.#p.a5.#r.#s.#t.#u.#u.a6.a7.#x.#y.#z.a8.#D.#G.a9.#F.#F.a9.#G.#D.#E.#D.#F.#F.#F.#G.#E.#I.b..b#.ba.a5.bb.bc.bd.be.bf.bg.bh.bi.#P.bj.bk.bl", +".bmQt#.bn.boQtc.bp.bq.br.bs.bt.bu.bv.bw.bx.by.bz.bA.bB.#9.bC.bCQts.bD.bE.bF.bG.bH.bI.bJ.bK.bL.bM.bN.bO.bP.bQ.bR.bS.bT.bU.bV.bW.bX.bY.bZ.b0.b1.b2.b3.b4.b5.b6.b7.b8.b9.c..c#.ca.c#.cb.cc.cd.ce.cf.cg.ch.ci.cj.ck.cl.cm.cn.co.cp.cq.cr.cs.ct.cu.cv.cw.cx.cy.cz.cA.cB.cC.#v.cD.cE.cF.cG.cH.cI.#H.#y.cH.#I.a9.cJ.cJ.a9.#I.cH.#I.#E.#D.#F.#F.#G.#E.#I.b..cK.cL.cM.cN.cO.#U.bi.cP.cQ.cR.cS.cT.cU.cV.cW", +".cXQt#.cY.cZ.c0.c1.bq.c2.c3.c4.c5.c6.bw.c7.c8.c9.d..bB.d#.da.da.db.bD.dc.dd.de.df.dg.dh.di.dj.dk.dl.dm.dn.do.dp.dq.dr.ds.dt.du.dv.dw.dx.dy.dz.dA.dB.dC.dD.dE.dF.dG.dH.dI.dJ.dK.dL.dM.dN.dO.dP.dQ.dR.dS.dT.dU.dV.dW.dX.dY.dZ.d0.d1.d2.d3.d4.d5.d6.d7.d8.d9.e..e#.ea.eb.#l.ec.ec.ed.ed.ee.ef.ef.eg.eh.#I.#E.#D.#D.#E.#I.eh.eh.#I.#D.a9.#F.#D.#E.#I.b#.cK.cK.ei.ej.ek.el.bd.em.en.#S.eo.#P.ep.eq.er", +".esQt#.et.eu.c0.ev.ew.ex.c9.ey.ez.eA.eB.eB.eC.eD.eE.eF.d#.eG.eG.db.eH.eI.eJ.eK.ab.eL.eM.eN.eO.eP.eQ.eR.eS.eT.eU.eV.eW.eX.eY.eZ.e0.e1.e2.e3.e4.e5.e6.e7.e8.e9.f..f#.fa.dB.fb.fc.fd.fe.ff.fg.fh.fi.fj.fk.fl.fm.fn.fo.fp.fq.fr.fs.ft.fu.fv.fw.fx.cO.fy.fz.fA.fB.fC.fD.fE.eb.fF.fF.fF.fG.fG.fH.fH.fH.#I.#I.eh.fI.fI.eh.#I.#I.fI.eh.#I.#D.a9.#D.#D.#H.fJ.fK.fL.#M.cN.fM.fN.fO.fP.fQ.fR.fS.fT.#V.fU.bk", +".fVQt#.fW.fX.c0.fY.fZ.f0.f1.f2.f3.f4.eB.f5.f6.f7.f8.eF.f9.g..g..g#.eH.ga.gb.gc.gd.ge.gf.gg.gh.gi.gj.gk.gl.gm.gn.go.gp.gq.gr.gs.gt.gu.gv.gw.gx.gy.gz.gA.gB.gC.gD.gE.gF.gG.gH.gI.gJ.gK.gL.gM.gN.gO.gP.gQ.gR.gS.gT.gU.gV.gW.gX.gY.gZ.g0.g1.g2.g3.b#.g4.g5.g6.g7.g8.g9.h..h#.ha.ha.ha.ha.ha.ha.ha.ha.#I.eh.hb.hc.hc.hb.eh.#I.hb.fI.#I.#D.#G.#D.#D.#E.fJ.#K.hd.#M.he.fM.hf.hg.hh.hi.hj.hk.hl.hm.hn.fU", +".hoQt#.hp.hq.hr.hs.ht.hu.hv.hw.hx.hy.eB.hz.hA.hB.hC.hD.f9.g..hE.g#.hF.hGQtvQtw.hH.ge.eM.hI.hJ.hK.hL.gk.dn.hM.hN.hO.hP.eX.hQ.hR.hS.hT.hU.hV.hW.hX.hY.hZ.h0.h1.h2.h3.h4.h5.h6.h7.h8.h9.i..i#.ia.ib.ic.id.ie.if.ig.ih.ii.ij.ik.il.im.in.io.ip.iq.ir.is.g4.it.iu.iv.iw.ix.fE.iy.iy.iz.iz.iz.iz.iz.iz.hb.hb.hb.hb.hb.hb.hb.hb.hc.hb.cH.#E.#D.#D.#D.#E.b#.#K.ba.cM.iA.iB.iC.iD.iE.iF.iG.iH.iI.iJ.#V.iK", +".iLQt#.iM.iN.hr.iO.ht.hu.iP.hv.iQ.iR.eB.iS.iT.iU.hC.hD.f9.hE.hE.iV.iW.hG.iX.iY.iZ.eL.i0.gg.i1.i2.i3.i4.i5.i6.eU.i7.i8.i9.j..j#.ja.jb.jc.jd.je.jf.jg.jh.ji.jj.jk.jl.jm.jn.jo.jp.jq.jr.js.jt.ju.jv.jw.jx.jy.jz.jA.jB.jC.jD.jE.jF.jG.jH.jIQt1.jJ.jK.jL.jM.jN.jO.jP.iw.jQ.jR.jS.jS.jS.jS.jT.jU.iz.iz.jV.hc.hb.eh.eh.hb.hc.jV.hc.hb.eh.#E.#D.#D.#D.#D.jW.fK.jX.jY.jZ.iB.iC.j0.j1.j2.j3.iI.bc.j4.fU.bl", +".j5.j6.j7.j8.j6.j9.k..k#.ka.kb.kc.kd.ke.kf.kg.kh.ki.kj.kk.kl.km.kl.kn.ko.kp.kq.kr.ks.kt.ku.kv.kw.kx.ky.kz.kA.kB.kB.kC.kD.kE.kF.kG.kH.kI.kJ.kK.kL.kM.kN.kO.kP.kQ.kR.kS.kT.kU.kV.kW.kX.kY.kZ.k0.k1.k2.k3.k4.k5.k6.k7.k8.k9.l..l#.la.lb.lc.ld.le.lf.lg.lh.li.lj.lk.ll.lm.ln.lo.lo.lo.lo.lo.lo.lo.lo.lp.lq.lq.lr.lr.ls.ls.lt.lu.lv.lw.lx.ly.lu.lz.lA.lp.lt.lB.lt.ls.lq.ls.lt.hc.lC.hb.hb.lD.lE.lF.lG", +".lH.lI.lJ.lK.lL.lM.lN.lO.lP.lQ.lR.lS.lT.lU.lV.lW.lX.lY.lZ.l0.l1.l2.l3.l4.l5.l6.l7.l8.l9.m..m#.ma.kx.mb.mc.md.me.mf.mg.mh.mi.mj.mk.ml.mm.mn.mo.mp.mq.mr.ms.mt.mu.mv.mw.mx.my.mz.mA.mB.mC.mD.mE.mF.mG.mH.mI.mJ.mK.mL.mM.mN.mO.mP.mQ.mR.mS.mT.mU.mV.mW.mX.mY.mZ.m0.m1.m2.m3.m4.m4.m4.m4.m4.m4.m4.m4.lp.lq.lq.lr.lr.ls.ls.lt.m5.lu.lv.lv.m6.m5.m7.m8.lq.ls.ls.m5.m6.m9.m9.lq.hc.lC.hb.hb.hc.lE.n..lG", +".n#.na.nb.nc.nd.ne.nf.ng.nh.ni.nj.nk.nl.nm.nn.no.np.nq.nr.ns.nt.nu.nv.nw.nx.ny.nz.nA.nB.m..nC.nD.nE.nF.nG.nH.nI.nJ.no.nK.nL.nM.nN.nO.nP.l#.nQ.nR.nS.nT.nU.nV.nW.nX.nY.nZ.n0.n1.n2.n3.n4.n5.n6.n7.n8.n9.o..o#.oa.ob.oc.od.oe.of.og.l#.l#.oh.ji.oi.oj.ok.ol.om.on.oo.op.oq.fH.fH.fH.fH.fH.fH.fH.fH.lp.lq.lq.lr.lr.ls.ls.lt.lz.or.m5.os.m5.lz.lA.ot.lt.ou.lr.lr.lr.lr.lp.ls.hc.lC.lC.lC.ov.lF.lG.ow", +".ox.oy.oz.oA.oB.oC.oD.oE.oF.oG.oH.oI.oJ.oK.oL.oM.oN.oO.oP.oQ.oR.oS.oT.oU.oV.oW.e3.oX.oY.oZ.o0.o1.o2.o3.o4.o5.o6.o7.o8.o9.p..p#.pa.pb.pc.pd.pe.pf.pg.ph.pi.pj.pk.nX.pl.pm.pn.po.pp.pq.pr.ps.pt.pu.pv.pw.px.py.pz.pA.pB.pB.pC.pD.pE.pF.pG.pH.pI.mo.mT.pJ.pK.pL.pM.pN.aU.pO.ef.ef.ef.ef.ef.ef.ef.ef.lp.lq.lq.lr.lr.ls.ls.lt.m7.m7.pP.pP.pP.m7.lA.lA.pQ.lt.lr.lr.lB.lB.lt.ls.hc.hc.lD.hc.lE.n..ow.pR", +".pS.pT.pU.pV.pW.pX.pY.pZ.p0.p1.p2.p3.p4.p5.p6.p7.p8.p9.q..q#.oM.q..qa.qb.qc.qd.qe.qf.qg.qh.qi.qj.qk.ql.qm.qn.qo.qp.qq.qr.qs.qt.qu.qv.qw.qx.qy.qz.qA.qB.qC.qD.qE.qF.qG.qH.qI.po.qJ.qK.pv.qL.qM.qN.qO.qP.qQ.qR.qS.qT.qU.qV.qW.qX.qY.qZ.q0.q1.q2.q3.q4.q5.q6.q7.q8.q9.r..r#.ef.ef.ef.ef.ef.ef.ef.ef.lp.lq.lq.lr.lr.ls.ls.lt.m7.m7.m7.lA.m7.m7.pP.lz.lt.ls.lq.lq.lr.lr.ls.m9.hc.hc.hc.ov.ra.lG.pR.rb", +".rc.rd.re.rf.rg.rh.ri.rj.rk.rl.rm.rn.ro.rp.rq.rr.rs.rs.rt.ru.rv.rw.rx.ry.rz.rA.rB.rC.rD.rE.rF.rG.rH.rI.rJ.rK.rL.rM.rN.rO.rP.rQ.rR.rS.rT.rU.rV.rW.rX.rY.rZ.r0.r1.r2.r3.r4.r5.r6.r7.r8.r9.q0.s..s#.sa.sb.sc.sd.se.sf.sg.sh.si.sj.sk.sl.sm.sn.so.gP.sp.l#.sq.sr.ss.st.su.sv.fH.fH.fH.fH.fH.fH.fH.fH.lp.lq.lq.lr.lr.ls.ls.lt.lz.pP.m7.lA.m7.lz.os.lu.sw.lt.lr.lr.ou.ou.lr.ls.hc.hc.hc.lE.lF.ow.rb.sx", +".sy.sz.sA.sB.sC.sD.sE.sF.sG.sH.sI.sJ.sK.sL.sM.sN.sO.sP.sQ.sR.sS.sT.sU.sV.sW.sX.sY.sZ.s0.s1.s2.s3.s4.s5.s6.s7.s8.s9.t..t#.ta.tb.tc.td.te.tf.tg.th.ti.tj.tk.tl.tm.tn.to.tp.tq.tr.ts.tt.tu.tv.tw.tx.ty.tz.tA.tB.tC.tD.tE.tF.tG.tH.tI.tJ.tK.tL.tM.tN.tO.tP.tQ.tR.tS.tT.tU.tV.m4.m4.m4.m4.m4.m4.m4.m4.lp.lq.lq.lr.lr.ls.ls.lt.os.or.pP.m7.lz.os.lv.lx.lB.pQ.pQ.tW.sw.lt.lt.lB.hc.hc.ov.lE.lG.tX.tY.tZ", +".t0.t1.t2.t2.t3.t4.t5.t6.t7.t8.t9.u..u#.ua.ub.uc.ud.ue.uf.ug.uh.ui.uj.uk.ul.um.un.uo.up.uq.ur.us.ut.uu.uv.uw.s8.ux.uy.t#.uz.uA.uB.uC.uD.uE.uF.uG.uH.uI.uJ.uK.uL.uM.uN.uO.uP.uQ.uR.uS.uT.uU.uV.uW.uX.uY.uZ.u0.u1.u2.u3.u4.u5.u6.u7.u8.u9.v..v#.va.vb.vc.vd.ve.vf.vg.vh.vi.lo.lo.lo.lo.lo.lo.lo.lo.lp.lq.lq.lr.lr.ls.ls.lt.lu.os.lz.lz.or.lu.lx.vj.ls.lt.lB.lt.ls.lq.ls.lt.hc.hc.ov.vk.lG.pR.sx.tZ", +".vl.vm.vn.vo.vp.vq.vr.vs.vt.vu.vv.vw.vx.vy.vz.vA.vB.vC.vD.vE.vF.vG.vH.vI.vJ.vK.vL.vM.vN.vO.vP.vQ.vR.vS.vT.vU.vV.vW.vX.vY.vZ.v0.v1.v2.v3.v4.v5.v6.v7.v8.v9.w..w#.wa.wb.wc.wd.we.wf.wg.wh.wi.wj.wk.wl.wm.wn.wo.wp.wq.wr.ws.wt.wu.wv.ww.wx.wy.wz.wA.wB.wC.wD.wE.wF.wG.wH.wI.wJ.cx.wK.wL.wM.wN.wO.wP.wQ.wR.iz.wS.wR.wT.wS.wU.wV.wV.wV.wV.wW.wW.wW.wW.wX.wY.wZ.w0.wZ.w0.w1.w2.w3.w4.w5.w6.w4.w7.w8.w9", +".x..x#.xa.xb.xc.xd.xe.xf.xg.xh.xi.xj.xk.xl.xm.xn.xo.xp.xq.xr.xs.xt.xu.xv.xw.xx.xy.xz.xA.xB.xC.xD.xE.xF.xG.xH.xI.xJ.xK.xL.xM.xN.xO.xP.xQ.xR.xS.xT.xU.xV.xW.xX.xY.xZ.x0.x1.x2.x3.x4.x5.x6.x7.x8.x9.y..y#.ya.yb.yc.yd.ye.x3.yf.yg.yh.yi.yj.yk.yl.ym.yn.yo.yp.yq.yr.ys.yt.yu.yv.yw.yx.yy.yz.yA.yB.yC.wQ.wS.iz.yD.yE.wR.wS.jS.ha.ha.wW.fF.yF.yG.yG.yH.yI.yJ.yK.yK.yL.yL.yM.yN.w4.w4.w4.w3.yO.yP.w8.yQ", +".yR.yS.yT.yU.yV.yW.yX.yY.yZ.y0.y1.y2.y3.y4.y5.y6.y7.y8.y9.z..z#.za.zb.zc.zd.ze.zf.zg.zh.zi.zj.zk.zl.zm.zn.zo.zp.zq.zr.zs.zt.zu.zv.zw.zx.zv.zy.zz.zA.zB.zC.zD.zE.zF.zG.zH.zI.zJ.zK.zL.zM.zN.zO.zP.zQ.zR.zS.zT.zU.zV.zW.zX.zY.zZ.z0.z1.z2.z3.z4.z5.z6.z7.z8.z9.A..A#.Aa.Ab.Ac.Ad.Ae.Af.Ag.Ah.Ai.iB.wQ.wR.wS.wS.wR.wR.wS.iz.ha.wV.fF.yG.Aj.Ak.Al.Am.w0.yL.w2.w2.yK.yK.An.Ao.w5.w4.yO.Ap.Aq.w8.w8.w8", +".Ar.As.At.Au.Av.Aw.Ax.Ay.Az.AA.AB.AC.AD.AE.AF.AG.AH.AI.AJ.AK.AL.AM.AN.AO.AP.AQ.AR.AS.AT.AU.AV.AW.AX.AY.AZ.A0.A1.A2.A3.A4.A5.A6.A7.A8.A9.B..B#.Ba.Bb.Bc.Bd.Be.Bf.Bg.Bh.Bi.Bj.Bk.Bl.qE.Bm.Bn.Bo.Bp.Bq.Br.Bs.Bt.Bu.Bv.Bw.Bx.By.Bz.BA.BB.BC.BD.BE.BF.BG.BH.BI.BJ.BK.BL.BM.BN.BO.BP.BQ.ou.BR.BS.BT.BU.BV.BW.BW.BW.BW.BW.wQ.wQ.Aj.BX.BX.Ak.Al.Am.BY.BZ.wZ.w1.yK.yK.w1.w1.yK.An.w6.w3.Ap.B0.w9.B1.B0.w8", +".B2.B3.B4.B5.B6.B3.B7.B8.B9.C..C#.Ca.Cb.Cc.Cd.Ce.Cf.Cg.Ch.Ci.Cj.Ck.Cl.Cm.Cn.Co.Cp.Cq.Cr.Cs.Ct.Cu.Cv.Cw.Cx.Cy.Cz.CA.CB.CC.CD.CE.CE.CF.CG.CH.CI.CE.CJ.CK.CL.CM.CN.CO.CP.CQ.CR.CS.CT.CU.CV.CW.CX.CY.CZ.C0.C1.C2.C3.C4.C5.C6.C7.C8.C9.D..D#.Da.Db.Dc.Dd.De.Df.Dg.Dh.Di.Dj.Dk.Dl.Dm.Dn.jU.Do.Dp.Dq.Dr.Ds.Dt.w2.Ds.yL.Du.Du.Dv.Dt.Dw.Dx.Dv.BZ.BZ.BY.Am.w1.yK.yM.w2.yL.yL.Dy.Dz.w4.yO.Aq.w9.DA.DA.w9.yQ", +".DB.DC.DD.DE.DF.DG.DH.DI.DJ.DK.DL.DM.DN.DO.DP.DQ.DR.DS.DT.DU.DV.DW.DX.DY.DZ.D0.D1.D2.D3.D4.D5.D6.D7.D8.D9.E..E#.Ea.Eb.Ec.Ed.Ee.Ef.Eg.Eh.Ei.Ej.Ek.El.Em.En.Eo.Ep.Eq.Er.Es.Et.Eu.Ev.Ew.Ex.Ey.Ez.EA.EB.EC.ED.EE.EF.EG.EH.EI.EJ.EK.EL.EM.EN.EO.EP.EQ.ER.ES.ET.EU.EV.EW.EX.EY.EZ.E0.E1.E2.E3.E4.E5.yL.E6.E7.E7.E7.E8.E9.E9.E6.F..F..F#.Fa.Dt.Dx.BZ.BZ.Fb.Fc.Fd.Fe.yN.Dz.Ao.Ff.w7.yP.w8.B1.DA.Fg.Fg.Fg", +"Qt..Fh.Fi.Fj.Fk.Fl.Fm.Fn.Fo.Fp.Fq.Fr.Fs.Ft.Fu.Fv.Fw.Fx.Fy.Fz.FA.FB.FC.FD.FE.FF.FG.FH.FI.FJ.FK.FL.FM.FN.FO.FP.FQ.FR.FS.FT.FU.FV.FW.FX.FY.FU.FZ.F0.F1.F2.F3.F4.F5.F6.F7.F8.F9.G..G#.Ga.Gb.Gc.Gd.Ge.Gf.Gg.Gh.Gi.Gj.Gk.Gl.Gm.Gn.Go.Gp.Gq.Gr.Gs.Gq.Gt.Gu.Gv.Gw.Gx.Gy.Gz.GA.GB.GC.GD.GE.GF.GG.GH.GI.Fb.E6.GJ.GK.GJ.E6.E9.E6.E7.GL.F..F..GM.F#.Fa.GN.GN.Fc.Fd.GO.Fd.Ao.Fb.Fc.Fd.w8.w8.w8.B0.w9.Fg.GP.GQ", +".GR.GS.GT.GU.GV.GW.GX.GY.GZ.G0.G1.G2.G3.G4.G5.G6.G7.G8.G9.H..H#.Ha.Hb.Hc.Hd.He.Hf.Hg.Hh.Hi.Hj.Hk.Hl.Hm.Hn.Ho.Hp.Hq.Hr.Hs.FU.Ht.Hu.Hv.Hw.Hx.Hy.Hz.HA.HB.HC.HD.HE.HF.HG.HH.HI.HJ.HK.HL.HM.HN.HO.HP.HQ.HR.HS.HT.HU.HV.HW.HX.HY.HZ.F0.H0.H1.H2.H3.H4.H5.F0.H6.H7.H8.H9.I..I#.Ia.Ib.Ic.Id.Ie.If.GI.yK.E6.E7.GK.E7.E9.Dt.E8.E7.F..F..F..F..F..F..F..F..Dz.Ao.Fe.Ao.An.w2.An.yN.w9.yQ.w8.w8.yQ.Fg.GQ.Ig", +".Ih.Ii.bw.Ij.Ik.Il.Ih.Im.In.Io.Ip.Iq.Ir.Is.It.Iu.Iv.Iw.Ix.Iy.Iz.IA.IB.IC.ID.IE.IF.IG.zo.IH.II.IJ.IK.IL.Hn.IM.IN.IO.IP.IQ.IR.IS.IT.IU.IV.IW.IX.IY.IZ.I0.I1.I2.I3.I4.I5.HF.I6.I7.I8.I8.I9.J..J#.I7.Ja.Ja.Jb.Gb.Jc.Jd.Je.Je.Jf.Jg.Jh.Ji.Jj.Jk.Jl.Jm.Jn.Jo.Jp.Jq.Jr.Js.Jt.Ju.Jv.Jw.Jx.Jy.Jz.JA.JB.JC.JD.JD.JD.JD.JE.JF.JF.JF.JG.JH.JH.JH.JI.JJ.JJ.JJ.JK.JK.JL.JL.JM.JN.JO.JP.JQ.JR.JS.JS.JT.JU.JU.JV", +".Io.JW.bw.JX.Ik.Il.Io.Im.JY.JZ.J0.J1.J2.J3.f7.J4.J5.J6.Ix.J7.J8Qte.J9.K..K#.Ka.Kb.Kc.Kd.Ke.Kf.Kg.Kh.Ki.yn.Kj.Kk.IO.Kl.Km.Kn.Ko.Kp.Kq.Kr.Ks.Kt.Ku.Kv.Kw.Kx.Jk.Ky.Kz.Kw.KA.KB.KC.KD.KB.KE.KF.KG.KB.KH.KH.KI.KJ.KK.ES.Gl.ER.KL.KM.KN.KO.KP.KQ.KR.KS.KT.KU.KV.KW.F5.KX.KY.KZ.K0.K1.K2.K3.K4.K5.K6.K7.K8.K8.K9.L..JD.JD.L#.JE.La.Lb.Lb.Lb.JG.JH.JH.JH.JO.JN.JN.JN.JN.JN.JN.JM.JU.JR.JS.Lc.JT.JU.JU.JV", +".Io.Ld.Le.Lf.Lg.Lh.Li.Lj.Lk.Ll.J0.iP.Lm.Ln.Lo.Lp.Lq.Lr.Ls.Lt.Lu.Lv.Lw.Lx.Ly.Lz.LA.LB.LC.LD.LE.LF.LG.LH.LI.LJ.Jq.IO.I0.Km.LK.LL.LM.LN.LO.LP.F0.F0.LQ.LR.LS.LQ.LT.LQ.LU.LV.LW.LX.LY.LZ.LY.LY.L0.L1.L2.L3.L4.L5.L6.L7.L8.L8.L9.FT.M..M#.Ma.I9.Mb.Mc.Md.Me.Mf.Mg.KD.Jk.Mh.Mi.Mj.Mk.Ml.Mm.Mn.Mo.Mp.eq.Mq.Mq.Mq.Mr.K8.K8.K8.K8.Ms.Ms.Mt.Mt.Mu.Mu.La.La.Mv.Mw.Mw.Mx.JP.JN.JN.My.Mz.JT.MA.MA.JT.Mz.JU.Mz", +".MB.MC.MD.ME.lX.MF.MG.MH.MI.Ll.Ip.MJ.MK.ML.MM.MN.MO.Lr.Ix.J7.J8Qte.Lw.MP.MQ.MR.MS.MT.MU.MV.MW.MX.MY.MZ.M0.M1.M2.IO.I2.Km.H4.M3.M4.M5.M6.M6.M7.M8.M9.N..N#.Na.Nb.Nc.Nb.Nb.Nd.Ne.Nf.tj.Ng.Nh.Nf.Nf.Ni.Ni.Nj.Nk.Nl.Nm.Nm.Nn.No.Ni.Np.Nq.Nr.Ns.Nt.Nu.Jf.Gq.Nv.Nw.Nx.Ny.Nz.NA.NB.NC.ND.NE.NF.NG.NH.NI.NJ.NK.NK.NL.NL.NM.NM.Mq.NN.NN.NO.NO.NP.NP.Ms.Ms.NQ.NQ.NR.Mv.Mw.Mw.NS.NS.JU.JV.JT.JR.NT.Mz.NU.NV", +".MG.NW.NX.NY.NZ.N0.N1.N2.N3.N4.N5.N6.N7.N8.N9.J4.O..O#.Oa.Ob.Oc.bq.J9.K..Od.Oe.Of.Og.Oh.Oi.Oj.Ok.Ol.MZ.Om.On.Oo.Op.H9.L0.Oq.Or.zv.Os.Ot.Ou.Ov.Ow.Ox.Oy.Oz.OA.OB.OC.OD.OE.OF.OG.OH.OI.OJ.OK.OL.OM.ON.OO.OP.OQ.OR.OS.OT.OU.OV.OW.OX.OY.OZ.O0.O1.O2.O3.O4.O5.O6.O7.O8.O9.P..P#.Pa.Pb.Pc.Pd.Pe.Pf.Pg.Ph.Ph.Ph.Ph.Pi.Pj.Pj.Pj.Pk.Pk.Pl.Pl.Pl.NN.NN.NO.NQ.NQ.NQ.Pm.Pn.Pn.Po.Po.NV.NU.NU.JU.JU.NU.Pp.Pq", +".Pr.NW.Ps.Pt.Pu.Pv.Pw.Px.Py.Pz.PA.PB.PC.PD.PEQth.PF.O#.PG.PH.PI.PJ.J9.PK.PL.PM.PN.PO.oS.PP.PQ.PR.PS.PT.LI.PU.Kr.PV.H9.PW.PX.PY.PZ.Na.P0.P1.P2.P3.P4.P5.P6.P7.P8.P9.Q..Q#.Qa.Qb.Qc.Qd.Qe.Qf.Qg.Qh.Qi.Qj.Qk.Ql.Qm.Qn.Qo.Qp.Qq.Qr.Qs.Qt.Qu.Qv.Qw.Ox.N#.Qx.Qy.Qz.QA.QB.QC.QD.QE.QF.QG.QH.QI.QJ.QK.QL.QM.QM.QN.QO.QO.QO.QP.QP.QQ.QQ.QQ.Pk.Pl.Pl.Pl.NN.Pm.QR.Pn.QS.QT.QU.QV.QW.QX.QX.QX.Pp.QY.Pp.QZ.Q0", +".Q1.Q2.Q3.Q4.Q5.Pv.Q6.Q7.Q8.Q9.R..MJ.R#.Ra.Rb.#3.Rc.Rd.Oa.Ob.rB.bq.Re.MP.Rf.Rg.Rh.Ri.Rj.Rk.Rl.Rm.Rn.Ro.Rp.Rq.Rr.PV.I2.PW.Rs.Rt.Ru.Rv.Rw.Rx.Ry.Rz.RA.RB.RC.RD.RE.RF.RG.RH.RI.RJ.RK.RJ.RL.RM.RN.RO.RP.RQ.RR.RS.RT.RU.RV.RW.RX.RY.RZ.R0.R1.R2.R3.R4.R5.R6.M6.R7.R8.R9.S..S#.Sa.Sb.Sc.Sd.Se.Sf.Sg.Sh.Si.Si.Sj.Sk.QM.QM.QM.Sl.QQ.QQ.QP.Pk.Pl.Pl.Pl.NN.Sm.QU.QU.QV.QV.QV.QV.QV.QZ.Sn.Q0.So.Pq.QZ.Sp.Sq", +".Sr.Ss.Q3.Q4.St.Su.Sv.Sw.Sx.Io.Sy.Sz.SA.c5.SB.SC.SDQto.SE.SF.SG.bq.SH.SI.f3.SJ.SK.SL.SM.SN.SO.SP.SQ.SR.SS.ST.SU.SV.I2.PW.SW.SX.SY.SZ.S0.S1.S2.S3.S4.S5.S6.S7.S8.S9.T..T#.Ta.Tb.Tc.Td.Te.Tf.Tg.Th.Ti.Tj.Tk.Tl.Tm.Tn.To.Tp.Tq.Tr.Ts.Tt.Tu.Tv.Tw.Tx.Ty.Tz.TA.TB.TC.TD.TE.TF.TG.TH.TI.TJ.TK.TL.TM.TN.TO.TP.Si.Si.Si.Sk.Sk.QM.QQ.QQ.Pk.Pk.Pl.Pl.NN.NN.TQ.TQ.TR.TS.QV.QV.Sm.QT.Sn.TT.TU.TT.So.TV.TW.TX", +".TY.TZ.T0.T1.T2.T3.T4.T5.T6.T7.T8.T9.U..U#.Ua.Ub.Uc.nH.Ud.Ue.Uf.Ug.Uh.Ui.Uj.Uk.Ul.Um.Un.Uo.Up.Uq.Ur.Us.Ut.Uu.Uv.Uw.Ux.Uy.Uz.UA.UB.UC.UD.UE.UF.UG.UH.UI.UJ.UK.UL.UM.UN.UO.UP.UQ.UR.US.UT.UU.UV.UP.UW.UX.UY.UZ.U0.U1.U2.U3.U4.U5.U6.U7.U8.U9.V..V#.Va.Vb.Vc.Vd.Ve.Vf.Vg.Vh.Vi.Vj.Vk.Vl.Vm.Vn.Vo.Vp.Vq.Vr.Vs.Vr.Vq.Vq.Vt.Vs.Vu.Vv.Vw.Vv.Vu.Vx.Vu.Vv.Vy.Vz.VA.VB.VC.VD.VE.VF.VG.VH.VH.VH.VI.VJ.VK.VL", +".VM.VN.VO.VP.VQ.VR.VS.VT.VU.VV.VW.VX.VY.VZ.V0.V1.V2.V3.V4.dy.V5.V6.V7.V8.V9.GU.W..W#.Wa.Wb.Wc.Wd.We.Wf.Wg.Wh.Wi.Wj.Wk.Wl.Wm.Wn.Wo.Wp.Wq.Wr.Ws.Wt.Wu.Wv.Ww.Wx.Wy.Wz.WA.WB.WC.WD.WE.WF.WG.WH.WI.WJ.WK.WL.WM.WN.WO.WP.WQ.WR.WS.WT.WU.WV.WW.WX.WY.WZ.W0.W1.W2.W3.W4.Nb.W5.Jj.W6.W7.W8.W9.X..X#.Xa.Xb.Xc.Vt.Xd.Vr.Vq.Xe.Vq.Vr.Xf.Vt.Xg.Xg.Vt.Vt.Xg.Xh.Xi.Xi.Xi.Xj.Vy.Vy.Vy.Vy.VI.VI.Xk.Xk.Xl.Xl.Xl.Xl", +".Xm.Xn.Xo.Xp.Xq.Xr.Xs.Xt.Xu.Xv.Xw.Xx.nh.bw.Xy.Xz.XA.XB.XC.XD.XE.XF.XG.XH.MB.Ld.XI.XJ.XK.XL.XM.XN.XO.XP.Ov.XQ.XR.XS.XT.XU.XV.XW.XX.XY.XZ.X0.X1.X2.X3.X4.X5.X5.X6.X7.X8.X9.Y..Y#.Ya.Yb.Ya.Yc.Yd.Y..Ye.Yf.Yg.Yh.Yi.Yj.Yk.Yl.Ym.Yn.Yo.Yp.Yq.Yr.Ys.Yt.Yu.Yv.Yw.Yx.Yy.Yz.YA.YB.YC.YD.YE.YF.YG.YH.YI.YJ.Xe.Vq.Vr.Vq.Xe.YK.Xe.Vt.YL.YM.YN.Vx.YN.Vx.Xg.Xh.YO.YP.YP.YQ.YQ.YR.YR.YS.Xk.VJ.YT.YU.YU.YT.VJ.TO", +".YV.YW.YX.YY.YZ.Y0.Y1.Y2.Y3.Y4.Y5.Y6.Y7.Y8.Y9.Z..Z#.Za.Zb.Zc.Zd.Ze.Zf.Zg.hr.Zh.Zi.Zj.Zk.Zl.Zm.Zn.Zo.Zp.Zq.Zr.Wj.Zs.Zt.Zu.Zv.Zw.Zx.Zy.Zz.ZA.ZB.ZC.ZD.ZE.ZF.ZG.ZH.ZI.ZF.ZJ.ZK.ZL.ZM.ZN.ZO.ZO.ZP.ZQ.ZR.ZS.ZT.ZU.ZV.ZW.ZX.ZY.ZZ.Z0.Z1.Z1.Z2.Z3.Z4.Z5.Z6.Z7.Z8.Z9.0..0#.0a.0b.0c.0d.0e.0f.0g.0h.0i.0j.0k.Xe.Vq.Xe.0k.0l.0k.0m.0n.YL.YK.YK.YM.YM.Xf.0o.0p.0p.0p.0p.0q.0q.0q.0q.Xl.0r.0s.YU.0t.YU.0s.YT", +".0u.0v.0w.0x.0y.0z.0A.0B.0C.0D.0E.0F.0G.0H.0I.0J.0K.0L.oW.0M.0N.0O.0P.0Q.ev.0R.0S.0T.0U.0V.0W.0X.0Y.0Z.00.01.02.03.04.05.06.07.08.09.1..1#.1a.1b.1c.1d.1e.1f.1f.1g.1h.1i.1j.1k.1l.1m.1n.1o.1p.1q.1r.1s.1t.1u.1v.1w.1x.1y.1z.1A.1B.1C.1D.1E.1F.1G.1H.1I.1J.1K.1L.1M.1N.1O.1P.1Q.1R.1S.1T.1U.1V.1W.1X.0k.YK.0k.1X.1Y.1X.0k.0n.YL.1Z.YL.0n.10.0n.0l.11.12.13.14.0q.15.YS.YS.0s.YT.YT.0s.0t.16.17.18", +".19.2..2#.2a.2b.2c.2d.2e.2f.2g.2h.2i.2j.2k.2l.2m.2n.2o.2p.2q.2r.2s.2t.2u.2v.2w.2x.2y.2z.2A.2B.2C.2D.2E.2F.2G.2H.2I.2J.2K.2L.2M.2N.2O.2P.2Q.2R.2S.2T.2U.2V.2W.2X.2Y.2Z.20.21.22.23.24.25.26.27.28.U0.29.3..3#.3a.3b.3c.3d.3e.3f.3g.3h.3i.3j.3k.3l.3m.3n.3o.3p.3q.3r.3s.3t.3u.3v.3w.3x.3y.3z.3A.3B.10.1X.0l.1X.3C.3D.3C.1X.3E.0n.3F.3G.3H.3I.3J.3E.3K.3L.11.13.3M.0p.YS.YR.3N.0t.YU.YU.3N.17.3O.3P", +".3Q.3R.3S.3T.3U.3V.3W.3X.i2.3Y.3Z.30.31.32.33.34.35.36.37.38.39.4..4#.4a.4b.4c.4d.4e.4f.4g.4h.4i.4j.4k.FO.4l.A9.4m.4n.4o.4p.4q.4r.4s.4t.4u.4v.4w.4x.4y.4z.4A.4B.4z.4C.4D.4E.4F.4G.4H.4I.4J.4K.4L.4M.4N.4O.4P.4Q.4R.4S.4T.4U.4V.4W.4X.4Y.4Z.40.41.42.43.44.45.46.47.48.49.5..5#.5a.5b.5c.YM.5d.5e.5f.3C.1X.3C.5f.5f.10.3F.5g.3I.5h.3H.5i.5j.3J.3E.3K.3K.3L.11.11.5k.13.13.16.16.16.5l.17.18.5m.3O", +".5n.5o.5p.5q.5r.5s.5t.5u.5v.5w.5x.5y.5z.5A.5B.5C.5D.5E.5F.5G.5H.5I.5J.5K.5L.5MQtI.5N.5O.5P.5Q.5R.5S.5T.5U.5V.5W.5X.5Y.5Z.50.51.52.53.54.55.56.57.58.59.6..6..6..6#.6a.6b.6c.6d.6e.6f.6g.6h.6i.6j.6k.6l.6m.6n.6o.6p.6q.6r.6s.6t.6u.6v.6w.6x.6y.6z.6A.6B.6C.6D.6E.6F.6G.6H.6I.6J.6K.6L.6M.6N.6O.6P.6Q.10.3C.3C.5f.6Q.5f.3C.6R.6S.5j.5i.5j.5j.5h.0n.3L.3L.3K.3K.3K.6T.6U.6U.16.17.18.3O.3O.18.17.6V", +".6W.6X.6Y.6Z.60.61.62.63.64.65.66.67.68.69.7..7#.7a.7b.7c.7d.7e.7f.7g.7h.7i.7j.7k.zr.7l.7m.7n.7o.7p.7q.7r.7s.7t.7u.7v.7w.7x.7y.7z.7A.7B.7C.7D.7E.7F.7G.7H.7I.7J.7K.7L.7M.7N.7O.7P.7Q.7R.7S.7T.7U.7V.7W.7X.7Y.7Z.70.71.72.73.74.75.76.77.78.79.8..8#.8a.8b.8c.8d.8e.8f.8g.8h.8i.8j.8k.8l.8m.8n.8o.8p.8p.8p.8p.8p.8p.8p.8p.8q.8q.8r.8s.8s.8r.8q.8t.8u.8u.8u.8v.8w.8x.8y.8y.8z.8A.8B.8C.8D.8E.8F.8G", +".8H.8I.8J.8K.8L.8M.8N.8O.8P.8Q.8R.8S.8T.8U.8V.8W.8X.8Y.T8.8Z.80.81.82.83.84.85.86.87.88.89.9..9#.9a.9b.9c.9d.9e.9f.9g.9h.9i.9j.9k.9l.9m.9n.9o.9p.9q.9r.9s.9t.9u.9v.9w.9x.9y.9z.9A.9B.9C.9D.9E.9F.9G.9H.9I.9J.9K.9L.9M.9N.9O.9P.9Q.9R.9S.9T.9U.9V.9W.9X.9Y.9Z.90.91.92.93.94.95.96.97.98.99#..#.#.8t.8t.8t.8t.8t.8t.8t.8t#.a.8r#.b#.c#.c.8r.8q#.d.8x#.e#.e.8w.8w.8w.8w.8w.8E#.f#.f#.e.8B.8A.8z#.g", +"#.h#.i#.j#.k#.l#.m#.n#.o#.p#.q#.r#.s#.t#.u#.v#.w.SK.7b#.x#.y#.z#.A.82#.B#.C#.D.86#.E#.F.89#.G#.H#.I#.J#.K#.L#.M#.N#.O#.P#.Q#.R#.S#.T.7r#.U#.V#.W#.X#.Y#.Z#.0#.1#.2#.3#.4#.5#.6#.7#.8#.9##.#####a##b##c##d##e##f##g##h##i##j##k##l##m##n##o##p##q##r##s##t##u##v##w##x##y##z##A##B##C##D##E##F##G.8r.8r.8r.8r.8r.8r.8r.8r#.c#.c##H##H##H#.c.8r.8q##I##J##J.8y.8y.8w.8w.8v.8D#.e.8C.8B.8z#.g##K##K", +"##L##M##N##O##P##Q##R##S##T##U##V##W##X##Y##Z##0##1##2.NW##3##4##5##6##7##8##9#a.#a##aa#ab#ac#ad#ae#af#ag#ah#ai#aj#ak#al#am#an#ao#ap#aq#ar#as#at#au#av#aw#ax#ay#az#aA#aB#aC#aD#aE#aF#aG#aH#aI#aJ#aK#aL#aM#aN#aO#aP#aQ#aR#aS#aT#aU#aV#aW#aX#aY#aZ#a0#a1#a2#a3#a4#a5#a6#a7#a8#a9#b.#b##ba#bb#bc#bd##H##H##H##H##H##H##H##H#be#bf#bf#bg#bf##H#.c.8r#bh#bh#bi##J##J#bj.8y.8y#bk#bk#bl##K#.g.8z.8A.8B", +"#bm#bn#bo#bp#bq#br#bs#bt#bu#bv#bw#bx#by#bz#bA#bB.Ud#bC#bD#bE#bF#bG#bH#bI#bJ#bK#bL#bM#bN#bO#bP#bQ#bR#bS#bT#bU#bV#bW#bX#bY#bZ#b0#b1#b2#b3#b4#b5#b6#b7#b8#b9#c.#c##ca#cb#cc#cd#ce#cf#cg#ch#ci#cj#ck#cl#cm#cn#co#cp#cq#cr#cs#ct#cu#cv#cw#cx#cy#cz#cA#cB#cC#cD#cE#cF#cG#cH#cI#cJ#cK#cL#cM#cN#cO#cP#cQ#cR#cR#cR#cR#cR#cR#cR#cR#cR#cS#cT#cT#cR#bf##H#.c##J##I#bh#bh#bh#cU#cV#cV#cW#cW#cX#bk#cY##K##I#.g", +"#cZ#c0#c1.dg#c2#c3#c4#c5#c6#c7#c8#c9#d.#d##da#db.7a#dc#dd#de#df#dg#dh#di#dj#dk#ab#dl#dm#a.#dn#.H#do#dp#dq#dr#ds#dt#du#dv#dw#dx#dy#dz#dA#dB#dC#dD#dE#dF#dG#dH#dI#dJ#dK#dL#dM#dN#dO#dP#dQ#dR#dS#dT#dU#dV#dW#dX#dY#dZ#d0#d1#d2#d3#d4#d5#d6#d7#d8#d9#e.#e##ea#eb#ec#ed#ee#ef#eg#eh#ei#ej#ek#el#em#en#cT#cT#cT#cT#cT#cT#cT#cT#eo#eo#ep#eo#eq#cR#bf##H#bh#bh#bh#cV#cX#er#er#es#bl#cY#bk#cX#cW#et#eu#ev", +"#ew#ex#ey#ez#eA#eB#eC#eD#eE#eF#eG#eH#eI#eJ#eK#eL#eM#eN.oG#eO#eP#eQ#eR#eS#eT#eU.yY#eV#eW#eX#eY#eZ#e0#e1#e2#e3#e4#e5#e6#e7#aY#e8#e9#f.#f##fa#fb#fc#fd#fe#ff#fg#fh#fi#fj#fk#fl#fm#fn#fo#fp#fq#fr.To#fs#ft#fu#fv#fw#fx#fy#fz#fA#fB#fC#fD#fE#fF#fG#fH#fI#fJ#fK#fL#fM#fN#fO#fP#fQ#fR#fS#fT#fU#fV#fW#fX#eo#eo#eo#eo#eo#eo#eo#eo#fY#fZ#f0#fY#eo#cT#bg#be#er#er#er#er#er#er#er#er#f1#f1#cW#f2#eu#ev#f3#f3", +"#f4#f5#f6#f7#f8#f9#g.#g##ga#gb#gc#gd#ge#gf.gv#gg#gh.c4#gi#gj#gk#gl#gm#gn#go#gp#gq#gr#dl#ab#gs#gt#gu#gv#gw#gx#gy#gz#gA#gB#gC#gD#gE#gF#gG#gH#gI#gJ#gK#gL#gM#gN#gO#gP#gQ#gR#gS#gT#gU#gV#gW#gX#gY#gZ#g0#g1#g2#g3#g4#g5#g6#g7#g8#g9#h.#h##ha#hb#hc#hd#he#hf#hg#hh#hi#hj#hk#hl#hm#hn#ho#hp#hq#hr#hs#ht#eo#eo#eo#eo#eo#eo#eo#eo#hu#hu#hu#hu#fY#cT#cR#bf#hv#hw#hw#es#er#er#cV#cV#hx#hx#f3#ev#eu#f2#cW#cX", +"#hy#hz#hA#hB#hC#hD#hE#hF#hG#hH#hI#hJ#hK#hL.XE.DF.p1#hM#hN#hO#hP#hQ#hR.uj#hS#hT#hU#hV#hW#hX#hY#hZ#h0#h1#h2#h3#h4#h5#h6#h7#h8#h9#i.#i##ia#ib#ic#id#ie#if#ig#ih#ii#ij#ik#il#im#in#io#ip#iq#ir#is#it#iu#iv#iw#ix#iy#iz#iA#iB#iC#iD#iE#iF#iG#iH#iI#iJ#iK#iL#iM#iN#iO#iP#iQ#iR#iS#iT#iU#iV#iW#iX#iY#iZ#i0#i1#i2#i2#i3#i0#i3#i2#i3#i2#i3#i4#i4#i2#i5#i2#i6#i7#i8#i9#j.#j##ja#jb#jc#jd#je#je#jf#es#jg#jg", +"#hy#hz#hA#jh#ji#jj#jk#jl#jm#jn#jo#jp#jq#jr#js.V9#jt#ju.ux#jv#hP#hQ#hR.uj#jw.IF#jx#jy#jz#jA#jB#jC#jD#jE#jF#jG#jH#jI#jJ#jK#jL#jM#jN#jO#jP#jQ#jR#jS#jT#jU#jV#jW#jX#jY#jZ#j0#j1#j2#j3#j4#j5#j6#j7#j8#j9#k.#k##ka#kb#kc#kd#iA#ke#g5#kf#kg#kh#ki#kj#kk#kl#km#kn#ko#kp#kq#kr#ks#kt#ku#kv#kw#kx#ky#kz#kA#i0#i3#i2#i2#kB#i0#i3#i2#i4#i3#i2#i0#i3#kC#i2#i3#i6#kD#i8#i9#kE#j##kF#ja#je#je#je#jf#jg#jg#jg#jg", +"#kG.MG#kH#kI#kJ#hG#kK#kL#kM#kN#kO#kP#kQ#kR#kS#kT#kU#kV#kW#kX#hP#kY#hR.uj#kZ#k0.Lk#k1#k2#k3#k4#k5#k6#k7#k8#k9#l.#l##la#lb#lc#ld#le#lf#lg#lh#li#lj#lk#ll#lm#ln#j2#lo#jZ#lp#lq#lr#ls#lt#lu#lv#lw#lx#ly#lz#lA#lB#lC#lD#lE#lF#lG#lH#lI#lJ#lK#lL#lM#lN#lO#lP#lQ#lR#lS#lT#lU#lV#lW#lX#lY#lZ#l0#l1#l2#l3#i0#i3#i2#i3#i0#i0#l4#i2#l5#i0#i2#i3#i2#i5#i1#i4#i6#kD#i7#i8#kE#kE#j.#j##jf#jf#jg#jg#jg#l6#l7#l7", +"#l8.MG#l9#m.#m##ma#mb#mc#hy#kN#md#me#mf#m.#mg#mh#mi#mj#mk#ml#hP#mm#mn.uj#mo#mp#mq#mr#ms#mt#mu#mv#mw#mx#my#mz#mA#mB#mC#mD#mE#mF#mG#mH#mI#mJ#mK#mL#mM#mN#mO#mP#mQ#mR#mS#mT#mU#mV#mW#mX#mY#mZ#m0#m1#m2#m3#m4#m5#m6#m7#m8#m9#n.#n##na#nb#nc#nd#jV#ne#nf#ng#nh#ni#nj#nk#nl#nm#nn#no#np#nq#nr#ns#nt#nu#i4#i0#i3#i3#i0#i4#i0#i3#l5#i0#i3#i0#i0#i2#i3#i4#i6#i6#i7#i8#i8#i9#kE#kE#jg#jg#l6#l7#l7#l7#nv#nv", +"#nw#nx#ny#hL#nz#nA#nB#nC#nD#nE.Uj#nF#jh#nF#nG#nH#nI#nJ#nK#nL#hP#nM#nN.uj#nO#nP#nQ#nR#nS#nT#nU#nV#nW#nX#nY#nZ#n0#n1#n2#n3#n4#n5#n6#n7#n8#n9#o.#o##oa#ob#oc#od#oe#of#og#oh#oi#oj#ok#ol#om#on#oo#op#oq#or#os#ot#ou#ov#ow#ox#oy#oz#oA#oB#oC#oD#oE#oF#oG#oH#oI#oJ#oK#oL#oM#oN#oO#oP#oQ#oR#oS#oT#oU#oV#i4#i0#i3#i0#i4#i4#i4#kB#i4#i0#i0#l5#l5#i3#oW#i0#i6#i6#i6#kD#i7#i7#i8#i8#l7#l7#l7#nv#iY#iY#iY#oX", +"#oY#jo#ny#oZ#o0#o1#o2#o3#o4#nE#jo#nF#o5#jp#o6#o7#o8.G0#o9#p.#hP#p##nN.uj#pa#pb#pc#pd#pe#pf#pg#ph#pi#pj#pk#pl#pm#pn#po#pp#pq#pr#ps#pt#pu#pv#pw#px#py#pz#pA#pB#pC#pD#pE#pF#pG#pH#pI#pJ#pK#pL.WN#pM#pN#pO#pP#pQ#pR#pS#pT#pU#pV#pW#pX#pY#pZ#p0#p1#p2#p3#p4#p5#p6#p7#p8#p9#q.#q##qa#qb#qc#qd#qe#qf#qg#l5#i4#i0#i0#i4#l5#i4#i0#i4#i0#i4#qh#qh#i0#i3#kB#i6#i6#i6#i6#i6#i6#i6#i6#iY#iY#iY#iY#qi#qj#qj#qj", +"#qk#jo#ny#ql#qm#qn#qo#qp#qq#hH#kO#kI#kQ#hJ#qr#qs#qt#qu#qv#qw#hP#qx#qy#qz#qA#qB#qC#qD.5J#qE#qF#qG#qH#qI#qJ#qK#qL#qM#qN#qO#qP#qQ#qR#qS#qT#qU#qV#qW#qX#qY#qZ#q0#q1#q2#q3#q4#q5#q6#q7#q8#q9#r.#r##ra#rb#rc#rd#re#rf#rg#rh#ri#rj#rk#rl#rm#rn#ro#rp#rq#rr#rs#rt#ru#rv#rw#rx#ry#rz#rA#rB#rC#rD#rE#rF#rG#l5#i4#i0#i4#l5#l5#i4#i0#rH#i4#i4#qh#l5#i0#i0#i4#i6#i6#rI#rI#rI#rJ#rK#rK#oX#oX#qj#qj#qj#rL#rM#rM", +"#qk#jo#ny#ql#rN#rO#rP#rQ#rR#jn#hI.lQ#rS#kI.MS#rT#rU#rV#qv#rW#hP#rX#rY#qz#rZ#r0#r1#r2#r3#r4#r5#r6#pi#r7#r8#r9#s.#s##sa#sb#sc#sd#se#sf#sg#sh#si#sj#sk#sl#sm#sn#so#sp#sq#sr#ss#st#su#sv#sw#sx#sy#sz#sA#sB#sC#sD#sE#sF#sG#sH#sI#sJ#sK#sL#sM#sN#sO#sP#sQ#sR#sS#sT#sU#sV#sW#sX#sY#sZ#s0#s1#s2#s3#s4#s5#l5#i4#i0#i4#l5#rH#l5#i4#s6#l5#i0#i4#i4#i0#i4#qh#i6#i6#rI#rJ#rK#rK#rK#rK#qj#qj#qj#qj#rL#rM#rM#rM", +"#s7#s8#s9#t.#t##ta#tb#tc#td.qa#te#tf#tg#th#ti#tj#tk#tl#tm#tn#to#tp#tq#tr#ts#tt#tu#tv#tw#tx#ty#tz#tA#tB#tC#tD#tE#tF#tG#tH#tI#tJ#tK#tL#tM#tN#tO#tP#tQ#tR#tS#tT#tU#tV#tW#tX#tY#tZ#t0#t1#t2#t3#t4#t5#t6#t7#t8#t9#u.#u##ua#ub#uc#ud#ue#uf#ug#uh#ui#uj#uk#ul#um#tN#un#uo#up#uq#ur#us#ut#uu#uv#uw#ux#uy#uz#uA#uB#uC#uz#uD#oU#uC#uB#uA#uz#uE#nr#nr#nr#uE#uF#uG#uH#uI#uJ#uK#uL#uH#uM#uN#uO#uP#uQ#uR#uS#uT", +"#uU.8W#uV#uW#uX#uY#uZ.HF#u0#u1#u2#u3#u4#u5#u6#u7#u8#u9#v.#v##va#vb#vc#vd#ve#vf#vg#vh#vi#vj#vk#vl#vm#vn#vo#vp#vq#vr#vs#vt#vu#vv#vw#vx#vy#vz#vA#vB#vC#vD#vE#vF#vG#vH#vI#vJ#vK#vL#vM#vN#vO#vP#vQ#vR#vS#vT#vU#vV#vW#vX#vY#vZ#v0#v1#v2#v3#v4#uh#v5#v6#v7#v8#v9#w.#w##wa#wb#wc#wd#we#wf#wg#wh#wi#wj#wk#uz#uA#uC#uA#uz#wl#oU#uC#uC#oU#wl#nr#wm#wm#wm#nr#uF#uG#uH#uI#uJ#uK#uL#uH#uM#uM#uN#uP#wn#wo#uR#uS", +"#wp#wq#wr#ws#wt#wu#wv#ww#wx#wy#wz#wA#wB#wC#wD#wE#wF#wG#wH#wI#wJ#wK#wL#wM#wN#wO#wP#wQ#wR#wS#wT#wU#wV#wW#wX#wY#wZ#w0#w1#w2#w3#w4#w5#w6#w7#w8#w9#x.#x##xa#xb#xc#xd#xe#xf#xg#xh#xi#xj#xk#xl#xm#xn#xo#xp#xq#xr#xs#xt#xu#xv#xw#xx#xy#xz#xA#xB#xC#v5#xD#xE#xF#xG#xH#xI#xJ#xK#xL#xM#xN#xO#xP#xQ#xR#xS#xT#wl#oU#uC#uA#uz#wl#oU#uC#oU#wl#uE#wm#xU#xU#wm#nr#uF#uG#uH#uI#uJ#uK#uL#uH#xV#xW#uM#uO#uP#uQ#wo#uR", +"#xX#xY#xZ#x0#x1#x2#x3#x4#x5#x6.zo#x7#x8.Zd#x9#y.#y##ya#yb#yc#yd.zs#ye#yf#yg#yh#yi#yj#yk#yl#ym#yn#yo#yp#yq#tD#yr#ys#yt#yu#yv#yw#yx#yy#yz#yA#yB#yC#yD#yE#yF#yG#yH#yI#yJ#yK#yL#yM#yN#yO#xl#yP#yQ#yR#yS#yT#yU#yV#yW#yX#yY#yZ#y0#y1#y2#y3#y4#y5#y6#y7#y8#y9#z.#z##za#zb#zc#zd#ze#zf#uE#zg#oU#zh#zh#zi#wl#oU#uA#uA#wl#zj#uz#uA#uD#zj#nr#xU#zk#xU#wm#nr#uF#uG#uH#uI#uJ#uK#uL#uH#zl#zl#xV#uM#uO#uP#wn#uQ", +"#zm#zn#zo#zp#zq#zr#zs#zt#zu#zv#zw#zx#zy#zz#zA#zB#zC#zD#zE#zF#zG#zH#zI#zJ#zK#zL#zM#zN#zO#zP#zQ#zR#zS#zT#zU#zV#zW#zX#zY#zZ#z0#z1#z2#z3#z4#z5#z6#z7#z8#xa#z9#A.#A##Aa#yJ#Ab#Ac#Ad#Ae#Af#Ag#Ah#Ai#Aj#Ak#Al#Am#An#Ao#Ap#Aq#Ar#As#At#Au#Av#Aw#fy#Ax#xD#Ay#Az#AA#AB#AC#AD#AE#AF#AG#AH#AI#AJ#AK#AL#AM#AN#zj#uz#uA#oU#wl#zj#wl#uA#wl#zj#nr#xU#xU#wm#nr#uE#uF#uG#uH#uI#uJ#uK#uL#uH#AO#AP#zl#xV#uM#uO#uP#uP", +"#AQ#AR#AS#AT#AU#AV#AW.7n#AX#AY#AZ#A0#A1#A2#u0#A3#A4#A5#A6#zq#A7#A8#A9#B.#B##Ba#Bb#Bc#Bd#Be#Bf#Bg#Bh#zT#Bi#Bj#Bk#Bl#Bm#Bn#Bo#Bp#Bq#Br#Bs#Bt#Bu#Bv#Bw#Bx#By#Bz#BA#BB#BC#BD#BE#BF#BG#BH#BI#BJ#BK#BL#BM#BN#BO#BP#BQ#BR#BS#BT#BU#BV#BW#BX#BY#BZ#B0#B1#B2#B3#B4#B5#B6#B7#B8#B9#C.#C##Ca#Cb#Cc#Cd#Ce#Cf#zj#uz#oU#oU#zj#uE#wl#oU#uz#wl#uE#nr#nr#uE#wl#uz#uF#uG#uH#uI#uJ#uK#uL#uH#Cg#Cg#AO#zl#xV#uM#uN#uO", +"#Ch#Ci#Cj#Ck#Cl#Cm#Cn#Co#Cp#Cq#Cr#Cs#Ct#Cu#Cv#Cw#Cx#Cy#Cz#CA#CB#CC#CD#CE#CF#CG#CH#CI#CJ#CK#CL#CM#CN#CO#CP#CQ#CR#CS#CT#CU#CV#CW#CX#CY#CZ#C0#C1#C2#Bx#C3#C4#C5#C6#C7#C8#C9#D.#D##Da#Db#Dc#Dd#De#Df#Dg#Dh#Di#Dj#Dk#Dl#Dm#Dn#Do#Dp#Dq#Dr#Ds#Dt#Du#Dv#Dw#Dx#Dy#Dz#DA#DB#DC#DD#DE#DF#DG#DH#DI#DJ#DK#DL#uE#wl#oU#uz#zj#uE#wl#oU#oU#uz#wl#zj#zj#wl#oU#uA#uF#uG#uH#uI#uJ#uK#uL#uH#DM#DN#Cg#AP#zl#xW#uM#uN", +"#DO#DP#DQ#DR#DS#DT#DU#DV#DW#DX#DY#DZ#D0#D1#D2#D3#D4#D5#D6#D7#D8#D9#E.#E##Ea#Eb#Ec#Ed#Ee#Ef#Eg#Eh#wV#Ei#Ej#Ek#El#Em#En#Eo#Ep#Eq#Er#Es#Et#Eu#Ev#Ew#Ex#Ey#Ez#EA#EB#C7#EC#ED#EE#EF#EG#EH#EI#EJ#EK#EL#EM#EN#EO#EP#EQ#ER#ES#ET#EU#EV#EW#EX#EY#EZ#E0#E1#E2#E3#E4#E5#E6#E7#E8#E9#F.#F##kD#Fa#Fb#Fc#Fd#Fe#uE#wl#oU#uz#zj#uE#zj#uz#uA#oU#uz#wl#uD#oU#uC#uB#uF#uG#uH#uI#uJ#uK#uL#uH#DM#DM#Cg#AO#zl#xV#uM#uM", +"#Ff#Fg#Fh#Fi#Fj#Fk.9.#Fl#Fm#Fn#Fo#Fp#Fq#Fr#Fs#Ft#Fu#Fv#Fw#Fx#Fy#Fz#FA#FB#FC#FD#FE#FF#FG#FH#FI#FJ#FK#FL#FM#FN#FO#FP#FQ#FR#FS#FT#FU#FV#FW#FX#FY#FZ#F0#F1#F2#F3#F4#F5#F6#F7#F8#F9#G.#G##Ga#Gb#Gc#Gd#Ge#Gf#Gg#Gh#Gi#Gj#Gk#Gl#Gm#Gn#Go#Gp#Gq#Gr#Gs#Gt#Gu#Gv#Gw#Gx#Gy#Gz#GA#GB#GC#qj#uQ#GD#GE#uv#GF#GG#GH#GH#GH#wh#wh#qe#qe#qe#GI#GJ#GK#GL#GL#GK#GM#GN#GO#GO#GP#GQ#GR#GS#GT#GU#GV#GW#GX#GX#GY#Cc#GP#Fb", +"#GZ#G0#G1#G2#G3#G4#G5#G6#G7#hY#G8#G9#H.#H##Ha#Hb.2r#Hc#Hd#He#Hf#Hg#Hh#Hi#Hj#Hk#ty#Hl#Hm#Hn#Ho#Hp#Hq#Hr#Hs#Ht#Hu#Hv#Hw#Hx#Hy#Hz#HA#HB#HC#HD#HE#HF#HG#HH#HI#HJ#HK#HL#HM#HN#HO#HP#HQ#HR#HS#HT#HU#HV#HW#HX#HY#HZ#H0#H1#H2#H3#H4#H5#H6#H7#H8#H9#I.#I##Ia#Ib#Ic#Id#Ie#If#Ig#Ih#Ii#rM#uP#Ij#Ik#uv#Il#Im#In#GH#GH#GH#wh#qe#qe#qe#In#GH#qe#GL#Io#Io#GL#qe#Ip#GO#Iq#AK#GR#GS#AI#GU#Ca#Cc#DI#DI#GX#uM#Ip#Ir", +"#Is#It#Iu#Iv#Iw#Ix#Iy#Iz#IA#IB#IC#ID#IE#IF#IG#IH#II#IJ#IK#IL#IM#IN#IO#IP#IQ#IR#IS#IT#IU#IV#IW#IX#IY#IZ#I0#I1#I2#I3#I4#I5#I6#I7#I8#I9#J.#J##Ja#Jb#Jc#Jd#Je#Jf#Jg#Jh#Ji#Jj#Jk#Jl#Jm#Jn#Jo#Jp#Jq#Jr#Js#Jt#Ju#Jv#Jw#Jx#Jy#Jz#JA#JB#JC#JD#JE#JF#JG#JH#JI#JJ#JK#JL#JM#JN#JO#JP#JQ#JR#Ip#Cf#JS#wh#JT#JU#GJ#GJ#In#GH#GH#GH#wh#wh#GL#qe#wh#GL#GN#GN#GL#In#JV#GO#Iq#AK#GR#GS#GS#GT#GY#GX#Ip#Ip#DI#DI#Fb#JW", +"#JX#JY#JZ#J0#J1#J1#J2#J3#J4#J5#J6#J7#J8#J9#K.#K##Ka#rY#Kb#Kc#Kd#Ke#Kf#Kg#Kh#Ki#Kj#Kk#Kl#Km#Kn#Ko#Kp#Kq#Kr#Ks#Kt#Ku#Kv#Kw#Kx#Ky#Kz#KA#KB#KC#KD#KE#KF#KG#KH#KI#KJ#KK#KL#KM#KN#KO#KP#KQ#KR#KS#KT#KU#KV#KW#KX#KY#KZ#K0#K1#K2#K3#K4#K5#K6#K7#K8#K9#L.#L##La#Lb#Lc#Ld#Le#Lf#Lg#Lh#Lh#Li#Lj#Lk#uC#Ll#Ll#GJ#GJ#GJ#GJ#In#GH#GH#GH#qe#qe#qe#GL#ut#ut#GL#qe#Lm#JV#GO#AK#GR#nt#GS#AI#Ca#Cc#DI#DI#GX#GX#Ip#Ir", +"#Ln#Lo#Lp#Lq#Lr##S#Ls#Lt#Lu#Lv#J6#Fp#Lw#Lx#Ly#Lz#LA#LB#LC#LD#LE#LF#LG#LH#LI#LJ#LK#LL#LM#LN#LO#LP#LQ#LR#LS#LT#LU#LV#LW#LX#LY#LZ#L0#L1#L2#L3#L4#L5#L6#L7#L8#L9#M.#M##Ma#Mb#Mc#Md#Me#Mf#Mg#Mh#Mi#Mj#Mk#Ml#Mm#Mn#Mo#Mp#Mq#Mr#Ms#Mt#Mu#Mv#Mw#Mx#My#Mz#MA#MB#MC#MD#ME#MF#MG#MH#MI#MJ#MK#ML#MM#MN#MO#MP#MQ#MQ#MR#GJ#GJ#GJ#In#In#qf#GJ#GK#GM#GL#GL#GM#Io#Lm#Lm#GO#GP#GQ#GR#GS#GS#MS#Ca#GY#Cc#GY#GY#GX#DI", +"#MT#MU#MV#MW#MX#MY#.G#MZ#M0#M1#IC#ID#M2#M3#rO#M4#M5#M6#M7#M8#M9#N.#N##Na#Nb#Nc#Nd#Ne#Nf#Ng#Nh#Ni#Nj#Nk#Nl#Nm#Nn#No#Np#Nq#Nr#Ns#Nt#Nu#Nv#Nw#Nx#Ny#Nz#NA#NB#NC#ND#NE#NF#NG#NH#NI#NJ#NK#NL#NM#NN#NO#NP#NQ#NR#NS#NT#NU#NV#NW#NX#NY#NZ#N0#N1#N2#N3#N4#N5#N6#N7#N8#N9#O.#O##Oa#Ob#Oc#Od#Oe#Of#Og#Oh#Oi#qf#MQ#MQ#MQ#MR#GJ#GJ#GJ#Oj#MQ#qe#GK#qe#wh#GL#ut#Ok#Lm#Ip#GO#AK#GR#nt#GS#Ol#GT#GW#GY#Ca#Ca#zl#GP", +"#Om#On#Oo#Op#Oq#Or.I.#Os#Ot#Ou#Ov#Ow.0H#Ox#Oy#Oz#OA#OB#OC#OD#OE#OF#OG#OH#OI#OJ#OK#OL#OM#ON#OO#OP#OQ#OR#OS#OT#OU#OV#OW#OX#OY#OZ#O0#O1#O2#O3#O4#O5#O6#O7#O8#O9#P.#P##Pa#Pb#Pc#Pd#Pe#Pf#Pg#Ph#Pi#Pj#Pk#Pl#Pm#Pn#Po#Pp#Pq#Pr#Ps#Pt#Pu#Pv#Pw#Px#Py#Pz#PA#PB#PC#PD#PE#PF#PG#PH#PI#PJ#PK#PL#PM#PN#PO#PP#PQ#qf#qf#MQ#MQ#MR#MR#MR#GI#PQ#GJ#GH#GH#GH#GH#GH#PR#Lm#JV#GO#AK#GR#GR#GW#MS#Ca#GY#GY#GY#GY#GX#DI", +"#A5#PS#PT#PU#PV#PW#PX#PY#PZ#P0#Ov#ID#J8#P1#P2#Hb#P3#P4#P5#P6#P7#P8#P9#Q.#Q##Qa#Qb#Qc#Qd#Qe#Qf#Qg#Qh#Qi#Qj#Qk#Ql#Qm#Qn#Qo#Qp#Qq#Qr#Qs#Qt#Qu#Qv#Qw#Qx#Qy#Qz#QA#QB#QC#QD#QE#QF#QG#QH#oE#QI#QJ#QK#QL#Pg#QM#QN#QO#QP#QQ#QR#QS#QT#QU#QV#QW#QX#QY#QZ#Q0#Q1#Q2#Q3#Q4#Q5#Q6#Q7#Q8#Q9#R.#R##Ra#Rb#Rc#Rd#Re#PQ#PQ#qf#qf#MQ#MQ#MQ#MR#MQ#qf#PQ#MR#GH#GH#MQ#GI#PR#Lm#Lm#GO#AK#Cc#GR#nt#Ca#GY#GX#GX#Cc#GX#DI#Fb", +"#Rf#Rg#Rh#Ri#Rj#Rk#Rl#Rm#Rn#Ro#Rp#Rq#Rr#Rs#Rt#Ru#Rv#Rw#Rx#Ry#Rz#RA#RB#RC#RD#RE#RF#RG#RH#RI#RJ#RK#RL#RM#RN#RO#RP#RQ#RR#RS#RT#RU#RV#RW#RX#RY#RZ#R0#R1#R2#R3#R4#R5#R6#R7#R8#R9#S.#S##Sa#Sb#Sc#Sd#Se#Sf#Sg#nj#Sh#Si#Sj#Sk#Sl#Sm#Sn#So#Sp#Sq#Sr#Ss#St#Su#Sv#Sw#Sx#Sy#Sz#SA#SB#SC#SD#SE#SF#SG#SH#SI#SJ#SK#SK#SK#SK#SK#SK#SK#SK#In#GJ#qf#PQ#PQ#MQ#GJ#GH#SL#MN#JW#Lm#GO#GQ#GR#GS#GP#GP#GP#GP#GP#GP#GP#GP", +"#Rf#Rg#Rh#Ri#Rj#SM#Rl#SN#SO#SP#SQ#SR#SS#ST#SU#SV#SW#SX#SY#SZ#S0#S1#S2#S3#S4#S5#S6#S7#S8#S9#T.#T##Ta#Tb#Tc#Td#Te#Tf#Tg#Th#Ti#Tj#Tk#Tl#Tm#Tn#To#Tp#Tq#Tr#Ts#Tt#Tu#Tv#Tw#Tx#Ty#Tz#TA#TB#TC#TD#TE#TF#TG#TH#TI#TJ#TK#TL#TM#TN#TO#TP#TQ#TR#TS#TT#TU#TV#TW#TX#TY#TZ#T0#T1#T2#T3#T4#Il#T5#T6#Oc#T7#T8#T9#s4#s4#s4#s4#s4#s4#s4#s4#MQ#MQ#qf#qf#qf#MQ#MR#GJ#SL#MN#U.#Lm#GO#AK#GR#nt#DI#DI#DI#DI#DI#DI#DI#DI", +"#Rf#Rg#Rh#U##Ua#Ub#Rl#SN#Uc#Ud#Ue#pd#Uf#Ug#Uh#Ui#Uj#Uk#Ul#Um#Un#Uo#Up#Uq#Ur#Us#Ut#Uu#Uv#Uw#Ux#Uy#Uz#UA#UB#UC#UD#UE#UF#UG#UH#UI#UJ#UK#UL#UM#UN#UO#UP.21#UQ#UR#US#UT#UU#UV#UW#UX#UY#UZ#U0#U1#U2#U3#U4#U5#U6#U7#U8#U9#V.#V##Va#Vb#Vc#Vd#Ve#Vf#Vg#Vh#Vi#Vj#Vk#Vl#Vm#Vn#Vo#Vp#Vq#Vr#MJ#Vs#Vt#Vu#Vv#Vw#Vx#Vx#Vx#Vx#Vx#Vx#Vx#Vx#Vy#PQ#qf#MQ#MR#MQ#MQ#MQ#SL#Vz#U.#PR#JV#Iq#AK#Cc#DI#DI#DI#DI#DI#DI#DI#DI", +"#VA#VB#VC#VD#VE#Ub#Rl#VF#VG#VH#VI#VJ#VK#VL#VM#VN#VO#VP#VQ#VR#VS#VT#VU#VV#VW#VX#VY#VZ#V0#V1#V2#V3#V4#V5#V6#V7#V8#V9#W.#W##Wa#Wb#Wc#Wd#We#Wf#Wg#Wh#Wi#Wj#Wk#Wl#Wm#Wn#Wo#Wp#Wq#Wr#Ws#Wt#Wu#Wv#Ww#Wx#Wy#Wz#aB#WA#WB#WC#WD#WE#WF#WG#WH#WI#WJ#WK#WL.N#.BK#WM#WN#WO#WP#WQ#WR#WS#WT#WU#WV#WW#WX#WY#WZ#W0#W1#W1#W1#W1#W1#W1#W1#W1#W2#W3#PQ#MR#GJ#MR#qf#PQ#SL#W4#MN#W5#Lm#GO#Iq#AK#Fb#Fb#Fb#Fb#Fb#Fb#Fb#Fb", +"#VA#VB#VC#W6#W7#Ub#W8#VF#W9#X.#X##Xa#Xb#Xc#Xd#Xe#Xf#Xg#Xh#Xi#Xj#Xk#Xl#Xm#Xn#Xo#Xp#Xq#Xr#Xs#Xt#Xu#Xv#Xw#Xx#Xy#Xz#XA#XB#XC#XD#XE#XF#XG#XH#XI#XJ#XK#XL#XM#XN#XO#XP#XQ#XR#XS#XT#XU#XV#XW#XX#XY#XZ#X0#X1#X2#X3#X4#X5#X6#X7#X8#X9#Y.#Y##Ya#Yb#Yc#Yd#Ye#Yf#Yg#Yh#Yi#Yj#Yk#Yl#Ym#Yn#Yo#Yp#Yq#Yr#Ys#Yt#Yu#W1#W1#W1#W1#W1#W1#W1#W1#Yv#W2#GI#MQ#MR#MQ#qf#PQ#SL#SL#MN#U.#W5#Lm#JV#GO#Fb#Fb#Fb#Fb#Fb#Fb#Fb#Fb", +"#Yw.lN#Yx#Yy#Yz#Ub#YA#YB#YC#YD#YE#YF#YG#YH#YI#YJ#YK#YL#YM#YN#YO#YP#S2#YQ#YR#YS#YT#YU#YV#YW#YX#YY#YZ#Y0#Y1#Y2#Y3#Y4#Y5#Y6#Y7#Y8#Y9#Z..Om.FT#Z##Za#Zb#Zc#Zd#Ze#Zf#Zg#Zh#Zi#Zj#Zk#Zl#Zm#Zn#Zo#Zp#Zq#Zr#Zs#Zt#Zu#Zv#Zw#Zx#Zy#Zz#ZA#K8#ZB#ZC#ZD#ZE#ZF#ZG#ZH#ZI#ZJ#ZK#ZL#ZM#ZN#ZO#ZP#ZQ#ZR#ZS#ZT#ZU#T4#ZV#ZV#ZV#ZV#ZV#ZV#ZV#ZV#Yv#ZW#Oj#GI#PQ#qf#qf#qf#SL#SL#W4#MN#JW#W5#PR#Lm#Ir#Ir#Ir#Ir#Ir#Ir#Ir#Ir", +"#Yw#ZX#ZY#ZZ#Z0#Ub#Z1#YB#Z2#Z3#Z4#Z5#Z6#Z7#Z8#Z9#0.#0##0a#0b#0c#0d#0e#0f#0g#0h#0i#0j#0k#0l#0m#0n#0o#0p#0q#0r#0s#0t#0u#0v#0w#0x#0y#0z#0A#0B#0C#0D#0E#0F#0G#0H#0I#0J#0K#0L#0M#0N#0O#0P#0Q#0R#0S#0T#0U#0V#0W#0X#0Y#0Z#00#01#02#03#04#05.Ym#06#07.L8#08#09.XR#1..HC#1##1a#1b#1c#1d#1e#1f#1g#Yq#1h#1i#1j#1j#1j#1j#1j#1j#1j#1j#ZW#ZW#ZW#W2#W3#PQ#MQ#GJ#SL#SL#W4#MN#MN#JW#W5#W5#Ir#Ir#Ir#Ir#Ir#Ir#Ir#Ir", +"#pc#ZX#ZY#1k.5J#1l#Z1#YB#1m#1n#1o#1p#1q#1r#1s#1t#1u#1v#1w#1x#1y#1z#1A#1B#1C#1D#1E#1F#1G#1H#1I#1J#1K#1L#1M#1N#1O#1P#1Q#1R#1S#1T#1U#1V#hY#1W#1X#1Y#1Z#10#11#12#13#14#15#16#17#18#19#2.#2##2a#2b#2c#2d#2e#2f#2g#2h#2i#2j#2k#2l#2m#2n#2o#2p#2q#2r#2s##9#2t#2u#2v#2w#2x#2y#2z#2A#2B#2C#2D#2E#2F#2G#2H#1j#1j#1j#1j#1j#1j#1j#1j#W2#ZW#Yv#Yv#W2#Vy#MQ#In#SL#SL#SL#Vz#MN#MN#U.#JW#JW#JW#JW#JW#JW#JW#JW#JW", +"#2I#2J#2K#2L#2M#2N#2O.Jf#2P#2Q#2R#2S#2T#2U#2V#2W#2X#2Y#2Z#20#21#22#23#24#25#26#27#28#YM#29#3.#3##3a#3b#3c#3d#3e#3f#3g#3h#3i#Rm#3j#3k.B.#3l#3m#3n#3o#3p#3q#3r#3s#3t#3u#3v#3w#3x#3y#3z#3A#3B#3C#3D#XY#3E#3F#3G#3H#3I#3J#3K#3L#3M#3N#3O#3P#3Q#3R#ve#3S#3S#3S#3S#3S#3S#3S#3S#3T#3U#3V#3W#3X#3Y#3Z#30#31#32#33#34#35#36#37#38#39#4.#4##4a#4b#4c#4d#4e#4f#1j#4g#4h#2F#PN#4i#4i#4j#4k#4l#4m#4n#4o#4p#4q", +"#4r#4s#4t#4u#4v#4w#4x#4y#4z#4A#4B#4C#4D#4E#4F#4G#4H#4I#4J#4K#4L#4M#4N#4O#4P#4Q#4R#4S#4T#4U#4V.6w#4W#4X#4Y#4Z#40#41.Hs#42#43.I4#44#45.B.#46#47#48#49#5.#5##5a#5b#5c#5d#5e#5f#5g#5h#5i#5j#5k#5l#5m#5n#5o#5p#5q#5r#5s#5t#5u#5v#5w#5x#5y#5z#5A#5B#5C#3S#3S#3S#3S#3S#3S#3S#3S#5D#5E#5F#5G#5H#5I#5J#5K#5L#5M#5N#5O#5P#5Q#5R#5S#5T#5U#5V#5W#5X#5Y#5Z#50#s4#W1#Yq#39#51#52#Rc#53#54#55#56#Lk#57#58#59#6.", +"#6##6a#6b#6c#6d#6e#6f#6g#6h#6i#6j#6k#6l#6m#6n#6o#6p#6q#6r#6s#6t#6u#6v#6w#6x#6y#6z#6A#6B#6C#6D#6E#6F.7s.Np#41#6G#6H.F9.HK.O9.Mb#6I#6J#6K#6L#6M#6N#6O#6P#6Q#6R#6S#6T#6U#6V#6W#6X#6Y#6Z#60#61#62#63#64#65#66#67#68#69#7.#7##7a#7b#7c#7d#7e#7f#7g#7h#3S#3S#3S#3S#3S#3S#3S#3S#7i#7j#7k#7l#Rr#7m#7n#7o#7p#7q#7r#7s#7t#7u#7v#7w#5P#7x#7y#7z#7A#7B#7C#7D#7E#W1#7F#7G#7H#7I#53#7J#7K#54#In#Lk#57#7L#7M#7N", +"#7O.yR#7P#7Q#7R#7S#7T#7U#7V#7W#7X#7Y#7Z#70#71#72#73#74#75#76#77#78#79#8.#8##8a#8b#8c#8d#8e#8f#8g.N.#8h#8i#8j#8k#8l#8m#8n#8o#8p#8q#8r#8s#8t#8u#8v#8w#8x#8y#8z#8A#8B#8C#8D#8E#8F#8G#8H#8I#8J#8K#8L#8M#8N#8O#8P#8Q#8R#8S#8T#8U#8V#8W#8X#8Y#8Z#80#81#3S#3S#3S#3S#3S#3S#3S#3S#82#83#84#85#86#87#88#89#9.#9##9a#9b#9c#9d#9e#9f#9g#9h#9i#9j#9k#9l#MK#9m#s4#1j#7F#9n#9n#52#53#7J#9o#9p#9q#4m#9r#7L#9s#7N", +"#9t#9u#9v#9w#9x.LF#9y#9z#9A#9B#9C#9D#9E#9F#9G#9H#9I#9J#9K#9L#9M#9N#9O#9P#9Q#9R#9S#9T#9U.Np.Jg.Hv#9V#9W#9X#9Y#9Z#90#91#92#93#94#95#96.Ep#97#98#99a..a.#a.aa.ba.ca.da.ea.fa.ga.ha.ia.ja.ka.la.ma.na.oa.pa.qa.ra.sa.ta.ua.va.wa.xa.ya.za.Aa.Ba.Ca.Da.Ea.Ea.Ea.Ea.Ea.Ea.Ea.Ea.Fa.Ga.Ha.Ia.Ja.Ka.La.Ma.Na.Oa.Pa.Qa.Ra.Sa.Ta.Ua.Va.Wa.Xa.Ya.Za.0#ZWa.1a.2a.3#Yqa.4#2F#Oga.5#53a.6a.7#wh#AN#9r#MPa.8a.9", +"a#.a##a#aa#ba#ca#da#ea#fa#ga#ha#ia#ja#ka#la#ma#na#oa#pa#qa#ra#sa#ta#ua#va#wa#xa#ya#za#Aa#Ba#Ca#Da#Ea#E#YAa#Fa#Ga#H.SV.SUa#Ia#Ja#Ka#La#Ma#Na#Oa#Pa#Qa#Ra#Sa#Ta#Ua#Va#Wa#Xa#Ya#Za#0a#1a#2a#3a#4a#5a#6a#7a#8a#9aa.aa#aaaaabaacaadaaeaafaagaahaaiaaja.Ea.Ea.Ea.Ea.Ea.Ea.Ea.EaakaalaamaanaaoaapaaqaaraasaataauaavaawaaxaayaazaaAaaBaaCaaDaaEaaFaaGaaHaaI#7F#Yq#4haaJaaKa.5aaL#55aaMaaN#AN#9r#MPa.8#9s", +"aaOaaP#zwaaQaaRaaSaaTaaUaaVaaWaaXaaYaaZaa0aa1aa2aa3aa4aa5aa6.Npaa7aa8aa9ab.ab#.S.abaabbabcabdabeabfabgabhabiabjabk.FVablablabmabnaboabpabqabrabsabtabuabvabwabxabyabzabAabBabCabDabE#WBabFabGabHabIabJabKabLabMabNabOabPabQabRabSabTabUabVabWabXa.Ea.Ea.Ea.Ea.Ea.Ea.Ea.EabYabZab0ab1.Gh.XQ.0Xab2#VEab3ab4ab5ab6ab7ab8ab9ac.ac#acaacbaccacdaceacfa.2a.2#7F#39#Yo#Ogacg#7J#WX#55#wh#AN#9r#GDachaci", +"acjackaclacmacnacoacpacqacracsactacuacvacwacxacyacz.O3acAacBacCacDacEacEacF.KW.KSacGacH.I2acIacJacKacLacMacNacOacPacPacQ.JnacRacSacTacUacVacWacXacYacZac0ac1ac2ac3ac4ac5ac6ac7ac8ac9ad.ad#adaadb#G#adcaddadeadfadgadhadiadjadkadladmadnadoadpadqa.Ea.Ea.Ea.Ea.Ea.Ea.Ea.EadradsadtaduadvadwadxadyadzadA#RjadBadCadDadEadFadGadHadI.ViadJadKadLadM#ZVa.3adN#7G#9nadOadP#woadQadR#In#AN#IjadSadTadU", +"adVadWadXadYadZad0ad1ad2ad3ad4ad5ad6ad7ad8ad9ae.ae#ae#aeaaeb.HM.HMaecaecaedaedaedaedaedaedaedaedaeeaeeaeeaeeaeeaeeaeeaeeaefaegaehaeiaefaefaejaekaelaemaenaeoaepaeqaeraesaetaeuaevaewaexaeyaezaeAaeBaeCaeDaeEaeFaeGaeHaeIaeJaeKaeLaeMaeNaeOaecaea#3Sa.EaePaeQaeQaePa.E#3SaeRaeSaeT.0AaeUaeVaeWaeW.LE.LE.LE.LE.LE.LE.LE.LEaeXaeYaeZae0ae1ae2ae3ae4ae5ae6ae7ae8ae9af.#wlaf##Ll#MNafaafbafcafdafeaff", +"afgafhafiafj.Hoafkaflafmafnafoafpafqafrafs.KIaft.Nwafuafuae#aeaaebaeb.HMaedaedaedaedaedaedaedaed.O8.O8.O8.O8.O8.O8.O8.O8afvafvafwafxafyafzafAafBafCafDafEafFafGafHafIafJafKafLafMafNafOafPafQafRafSafTafUafVafWafXafYafZaf0af1af2af3af4af5af6af6#3S#3Sa.EaeQaeQa.E#3S#3Saf7af8af9ag.ag#agaagbagc.LE.LE.LE.LE.LE.LE.LE.LEagdageagfaggaghagiagjagkaglagmagnagoagp#2Bagq#nsagragsafaafbafcagtaguagv", +"agwagxagyagzagAagBagCagDagEagFagGagHagIagJagKagLagMagMagNagN.Nwafuae#ae#agOagOagOagOagOagOagOagO#44#44#44#44#44#44#44#44afzaegagPafxaefagPagQagRagSagTagUagVagWagXagYagZag0ag1ag2ag3ag4ag5ag6ag7ag8ag9ah.ah#ahaahbahcahd.moaheahfaeJahg.H2ahhaf6ahi#3Sahja.Ea.Eahj#3SahiahkaeTaf9.EWahlahmagbahn.pS.pS.pS.pS.pS.pS.pS.pSahoahpahqahrahsahtahuahvahwahxahyahzahA#7rahBahCahD#58#MHahEahFagtahGahH", +"ahIahJahKahLahMahNahOahPahQahRacAahS.JfahT.F7.Mfaf5af5ahUagMagNagN.Nw.NwagOagOagOagOagOagOagOagO.H9.H9.H9.H9.H9.H9.H9.H9agPafx#3jafzaefahVahWahXahY.ptahZ.mEah0ah1ah2ah3ah4ah5ah6ah7ah8ah9ah8ai.ai#aiaaibaicaidaieaif.nRaigaihaiiaijaikaaiail.HMaimahi#3S#3S#3S#3Sahiaimainainaioaipag#ahmaiqairaisaisaisaisaisaisaisaisaitaiuaivaiwaixaiyaizaiAaiBaiCaiDaiEaiFaiGaiHaiIaiJaiK#LgaiLaiMaiN#uFaiO", +".CtaiPaiQaiRaiSaiTaiUaiVaiWaiXaiY.xN.xNaiZai0ai1af5af5ahUagMagNagN.Nw.Nwai2ai2ai2ai2ai2ai2ai2ai2.H9.H9.H9.H9.H9.H9.H9.H9ai3afzai4ai5ai6ai5afxai7ai8ai9aj.aj#ajaajbajcajdajeajfajgajhajiajjajkajlajmajnajoajpajqajrajs.q0ajt.l#ahfajuajvaecaf6aeaabhabhahiajwajwahiabhabhahkaio.DhajxahmajyajyajzaisaisaisaisaisaisaisaisajAajAajBajCajDajEajFajGajHajIajJajKajLajMajNajOajPajQajRaiLajSajTajUajV", +"ajWajXajYajZaj0.F9aj1aj2aj3aj4.Jgaj5.HVaj6aj7aj8agMagMagNagN.Nwafuae#ae#ai2ai2ai2ai2ai2ai2ai2ai2#44#44#44#44#44#44#44#44aeiaj9aegagPaj9ai6aehak.ak#akaakbakcagUakdakeakfakgakhakiakjakkaklakmaknakoakpakqakraksaktaku.ibakvaigaiiakwakxajvahUafu#8oakyabhahiahiabhaky#8oainakzakAakBaeUajyakCakDaisaisaisaisaisaisaisaisakEakFakGakHakIakJakKakLakMakNakOakPakQakRakSakTakUakVakWakXakY#iYakZak0", +"ak1ak2.IMak3ak4ak5ak6ak7ak8ak9al.al#.HNalaalbabf.Nwafuafuae#aeaaebaeb.HMalcalcalcalcalcalcalcalc.O8.O8.O8.O8.O8.O8.O8.O8aefafzaldaleaefai4afwaj9alfalgalhalialjalkallalmalnaloalpalqalralsaltalualvalwalxalyalzalAalBaku.moaihalCagNalDafualEalFalG#8oabhabhabhabh#8oalGalHalIakAajxalJahmalKakCalLalLalLalLalLalLalLalLalMalNalOalPagcalQalRalS.CYalTalUa#E.DjalValWalXalYalZakWal0al1al2al3al4", +"al5al6al7al8al9am.am#amaambamcamdameamfamgamh.Nzae#ae#aeaaeb.HM.HMaecaecalcalcalcalcalcalcalcalcaeeaeeaeeaeeaeeaeeaeeaeeamiaegafwai4aldaegafzamjamkamlammamnamoampamqakuamramsamtamuamvamwamxamyamzamAamB.l#.mSamCamDamE.jiamFamGamHamIamJamKamLalGamM#8oabhabh#8oamMalGamNamOalIamPamQamRajyamSamTamTamTamTamTamTamTamTamUamVamWamXamYamZam0am1am2am3am4am5am6am7am8am9an.an#anaal0anbal2ancand", +"aneanfang.Nhanh.R7anianjaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankank.KC.KC.O8.O8anlanmannannanoanoanoanoanoanoanoano#44#44anpanp.Mg.Mganqanq.rcanransantanuanv.jianuanwanxanyanzanAanBanCanDanEanFanGanH.mo.ji.l#anIanJanKanLanLanManNanOanPanQanRanR.HGanSanTanUanUanVanWanXanY.KfanZan0an1#YCan2an3an4an5#YCan6an7an8an8an9ao.ao#aoaaobaobaocaodaoeaofaoc.Klaogaohaoiaojaokaolaomaonaoo#ML", +"aopaoqaor.Neaosaotaouaovaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankank.KC.KC.KC.O8anlanmanmannanoanoanoanoanoanoanoano#44#44anpanp.Mg.Mgaowanqaoxaoy.rgaozaoA.l#.AF.qNaoBaoCaoDaoEaoFaoGaoHaoIaoJanF.JbaoKaoLaoM.l#aoNaoOaoPaoQaoRanOaoSaoTaoUaoVaoWaoXaoYaoZao0anZanZao1anYanYanYanYanYanYanYan4an3an9ao2an4an4ao3an9an7an3an8ao.ao#aobao4#SUaocaodaoeaofaf7.Klaogao5ao6ao7ao8ao9#9qap.ap#apa", +"apbapcapdape.Ncapfapgaphaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankankapiapi.KC.O8.O8anlanmanmapjapjapjapjapjapjapjapj#44#44apkanp.S..Mgaplanqapmapnapoaoz.l#appaiganuapqaprapsaptapuahNam4apvapwapxapyapzapAapB.jiapCapDapEapFapGanNanLapGaoUaoWaoWaoXaoYapHao0anZapIanWanWanWanWanWanWanWanW#YCan6apJan6#YC#YCan5apJapJan4an3an9apKao4apLapMapNapOapPaof#VF.KlaogapQapRapSapTapU#GHapV#rHapW", +"apXapYapZap0ap1ap2ap3ap4aeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankankankankapi.KC.O8.O8anlanlapjapjapjapjapjapjapjapj#6I#44#44anpanp.Mg.Mg.Mgap5ap6ap7.moanuanvap8ap9aq.aq#aqaaqbaqcaqdaqeaqfaqgaqhaqiaqjaqkaqlaqmaqnaqoapFaqpaqqapGaqraqsaqpaqtaquaqvaqwaqxaqy.Kf.KfaqzaqAaqBaqCanWanY.KfaqDaqEaqF#YC#YCaqEaqEaqE#YCaqGapJan7an9aoa#SUapMaqHapNapOaqIao5aqJaqKaogapQaqLaqMaqNaqOaqPaqQaqR#Ol", +"aqSaqTaqUaqV.uIaqW.FTaqXaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankankaqYankankapi.KC.O8.O8.O8#6I#6I#6I#6I#6I#6I#6I#6IaqZaq0#44apkanp.S..Mg.Mgaq1aq2aq3.rfaq4.moaq5.BJaq6aftaq7aq8aq9ar.ar#araarbarcardakxarearfargarhariarjarkanPapGaoRanParlaquaquaqvarmarnaro.KfarparqaqzarrarsanWanY.KfapIan7an4an4an4an4an7an4an4aqGapJan7an9aoa#SUapMaqHartaruaqIarvaqJ.I2#VFapQarwarxaryarz#uwarAarBarC", +"arDarEarFarGarHarIarJanhaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankankaqYaqYankankapi.KC.KC.O8arfarfarfarfarfarfarfarfaqZaqZ#44#44anpanp.Mg.Mg.HKarKarLarM.l#arNarOarP.LOarQapiarRanlarS.LN.LOarTarUarVagNaecarWarXarYapFanOarZanOanMar0ar1anNar2ar3ar4ar5ar6ar7anYanYarsarsar8ar9anVanWanXanYan3an4an6an2an7an3an4apJapJan4an3an9apKao4apLapMaqJaruas.as#asa.I2#YAasbascasdaseasfasgashasiasj", +"askaslasmasnasoaspasqarJaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankank.LL.LLaqYankankapi.KC.KC.KP.KP.KP.KP.KP.KP.KP.KPaqZaqZaq0#44apkanp.S..Mgasrassastasuasva.C.HL.HLaswasxasyaszasAaswaswasBaqhasCasDaf5asEasFasGasHaqq.F6asIarjaqsarjaoQanPar3ar3ar4asJasKar7anY.HEarsar8ar9anVanWanYao1.Kfan7an6#YCan6an4ao3an4an5an7an3an8ao.ao#aobao4#SU.KlasLaohas##SN.I2#YAasMasNasOasPasQasRasSaiNasT", +"asUasVasWasXasYasZas0.Hsaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankank.LL.LLaqYaqYankapi.KC.KC.KP.KP.KP.KP.KP.KP.KP.KPaqZaqZaqZ#44apkanpanp.S.as1adpaoxas2a.Cap5arPadpas3as4as5as6as7abjas6as8aqhas9at.at#aaiataatbatcanManPanMaqratdateanOanLatfar3ar4asJasKatganY.HEaqzatharsatianYanZan1atjapKan9an3an8ao#aoaao.an3an8an8an9ao.ao#aoaaobaob.KlasLatkas##SNatl#YAasMatmatnatoatpatq#Ceatrats", +"attatuatvatwatxaty.Hsatzapjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapiankankapi.KC.O8anlanmanmarfarfarfarfarfarfarfarfanpanpanpanpanpanpanpanpatAatBatCatDatDarn.McatEatFatGatHatIatJ.OqatKatLatMatNatOatPatQahWahVatRatSa.DatTaj8atUatVatWatSatfatXatYatZat0at1anWanVatianXanYatiarsat2ar9anYat3at4an0at5at6at7at3at8at7at7at7an0at6at6at5at5at9au.au##9Y.XR.S..TEaqY#YAauaaubaucaudaueaufaug", +"auhauiaujaukaulaum.Hsatzapjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapi.KC.KC.KC.KC.O8.O8.O8.O8arfarfarfarfarfarfarfarfanpanpanpanpanpanpanpanpatEacHarn#ZHaunauoaupauoauqatI.HYaurausautauuauvauwauxauyauzatMauAatQaldaj8atTauBa.DauCatVauDauCauEaoWaoXauF.HGauGauHat5arp.KfanYar9aqBarrar9anYarpapIat6auIauIan0at4auJarpat4apIan0at5auIaapaapau.au.auKauLauM.S..TEaqYauN#wLauOauPauQauRauSauT", +"auUauVauWauXauYauZau0au1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapi.O8.O8.O8.KC.KCapiankank#6I#6I#6I#6I#6I#6I#6I#6IanpanpanpanpanpanpanpanpatAau2arnatBauoau3au4aunau5au6au7au8au9av.aig.ji.mo.moav#avaavbavcaj9ai4avdalbalbaveavfauDauDatWanQ.H8avgavh#ZHaviavjau#anUanZanYar9aqBarsanW.Kfat4at7auIaapaapauIat7at4at3arpat4an0auIaapau.avkau#auK#9YauL.I0avlanpanpavmavnavoavpavqavravsavt", +"avuavvavwavxavy.Utavzau1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapianl.O8.O8.KCankankaqYaqYapjapjapjapjapjapjapjapjanpanpanpanpanpanpanpanpatE.HGavAavhavBaoYatCavC#6JavDavEavFavGavHavI.l#avJavJavKavLavMai5afzavcavNavNavda.DatSatUatVatVaqtavOaoX.HGavPavQatjatjanZ.KfanWar8ar8anWanZatjapIan0avR.3Wau.aapat5at7at4apIat7at6avRavSau.avT#9YauLauLavBavB.I0avlaegavUavVavWavXavYavZav0av1", +"av2avvav3avxav4.Ow.5Vau1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapi.KCapiapiankankankaqYaqYanoanoanoanoanoanoanoanoanpanpanpanpanpanpanpanpau2av5av6av7av8avAav8atCau6av9aw.aw#.l#aig.jiaw#.l#awaawbawcawdafwaweaehalbauBa.DawfatSatUatVawgaquaquaqvarmarnawh.KfarpanYanWar9ar9anYanUawiawjat4an0avRau.au.avSauIat6at6at6at5auIavRaapavSavS.XRauM.I0avBavBawkawlawlawmawnawoawpawqawraws#Sz", +"awtawuav3awvaww#41avzau1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapiawxawx.LLaqYankapi.KC.KCanoanoanoanoanoanoanoanoanpanpanpanpanpanpanpanpatAatBatBapHawyawyar5apHawzawAawBaigawCawD.l#.moawEav#awFawGavcaweawHawIaj8atSatUatUauCauCauDawJaquaquaqvar5asKatganXanWar9arsar8anXanUawKauKawLarpat7auIavSau.aapauIat6auIauIavRaapaapaapavSavS.S..S.avl.I0awkawMawNa#EawOawPawQawRawSawTawUawV", +"awtawWawXawYawZ.Owavzau1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapiaw0aw1.LNawxank.O8anmannamgamgamgamgamgamgamgamganpanpanpanpanpanpanpanpacHarnar5av6atBatBatEaw2aw3au8aw4aw5aig.l#aw6aw7aoOaw8aw9ax.afwax#ak.auAatUawgaxaawgatWauCauDawJasGaxbatYatZat0axcanWanWarsarsatiaqDawiauKauKavj.HEat4at6aapavSaapauIat6at5auIauIaapavSau.avkavk.TE.TEanpavlawlawNa#Eaxdaxearkaxfaxgaxhaxiaxj#4i", +"awtaxkaxlaxmaxnaxoau0au1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapiarSaxpaw1awxankanlarR.KF.TE.TE.TE.TE.TE.TE.TE.TEanpanpanpanpanpanpanpanp#ZHaxqarnaxraxsacHaxtaxuaxvaxw.l#.moaw4aig.l#.l#axxaxyaxzaxAaxBaxCafzaxDawJaxEaxFaxEatVauCauDawJaxGaxGaxHaxIaxJaxKar9atiaqCar9anXan1avjawLavjatjat8at3an0auIaapavRat5an0at7axLat5aapau.avkaxdaxMaqYaqYanpaegawla#EaxdaxNaxO#2uaxPaxQaxRaceaxSaxT", +"axUaxVaxWaxXaxYaxZas9ax0apiapiapiapiapiapiapiapi.KO.KOax1.F0ax2ax3a#La#L.Gbax4ax4ax5.KHax6.Gb.Oqaj6ax7#2uadpax8ax9ay.#8q.XR.XR.I0avlavl.HH.HH.HHaroay#ayaaybaycar7aydayeayfaygayhaigaigayiawDayjaykaylaymaynayoaypavPayqayraysaytayuatWayvaszaywayxayyayzayAayBayCayDayEayFayGahqayHayIayJayKayLayMayNat1ayOayPayQ#2xayRaySayTayUayVayWayXayYayZay0ahiay1aqKasa#VFaogay2ay3ay4ay5ay6ay7ay8ay9az.", +"az#azaazbazcazdaze.Maaeiapiapiapiapiapiapiapiapi.IJ.KOacTazfazgazgazhacTaziazjax5azk.KHazl.Gbaziazm.F0abbaznazoax7azpazq.XR.XR.I0avlavl.H9.HH.HHay#aqyazrazsaztazuazvazwazx.l#avGawCaw6aigazyazzazAazBazCazDazEazFazGazHazIazJazKazLatXazMayvazNazOazPar1azQawgazRaxJazS.CsazTazUazVazWazXazYazZaz0akMaz1az2az3az4alOaz5#wMaz6.HRaz7az8az9.QCaA.#3S#3SaA#.I2#Rm#YA#Z1awLaAaaAbaAcay6aAdaAeaAfaAg", +"aAhaAiaAjaAkaAl.QxaAmafxapiapiapiapiapiapiapiapiaAnaAoaApaAqaAraAsaAtaAuaAvaAwaAxaAyaAzaAA.F9aABareahUaACalEailalEaADafu.XR.XR.I0.I0avlavlavl.H9aAEayaaAFaAGauGaAHaAIaAJaAKaALaAMaig.qNaANaAOaAPaAQaARaASanYaATaAUaAVaAWaAXaAYaAZaytatVamhaA0aA1aqxazSaA2aA2aA3ab2aA4aA5aA6aA7aA8aA9aB.aB#aBaaBbaBcaBdaBeaBfaBgaBhaBiaBjaBkaBlaBkaBmaBn.XR.KkaBoaq0aBpaBqaA#aeR#SN#YA#Z1.MiaBra.HaBsaBtaBuaBvaBw", +"aBxaByaBzaBAaBBaiUaBCaj9apiapiapiapiapiapiapiapiaBD#2OaBEaBFaBGaBHaBIaBJaBKaBL#viaBMaBNaBOaBPaBQaBRaBSaBTaBUaBV.H0aBWaAC.XR.XR.I0.I0.I0.I0avlavlaAGaroawlaBXao0aroaBYaBZaB0aB1aB2aB3aB4aB5aB6aB7aB8aB9aC.aC#aCaaCbaCcaCdaCeaCfaAYaAZaCgatUavfaChaCiaCjaCkaClaCmaA4aCnaCoaCpaCqaCraCsaCtaCuaCvaCwaCxaCyaCzaCAaCBaCCabpaCDaCEaCFaCGaCHaCIaCJaCKaCLa.Eahj.Mb.EVaCMaCN#W8#W8aCOaCPaCQaCRaCSaCTaCUaCV", +"aCWaCXaCYaCZaC0aC1.F9ai4apiapiapiapiapiapiapiapiaC2aC3aC4aC5aC6aBHaC7aC8aC9aD.aD#aDaaDbaDcaDdaDeaDfaDgaDhaDiaDjaDk.HOaDl.I0.I0auM.XR.XR.XR.XR.XRaDmaAEaDmaBXaxcaqyaDnaDoaDpaDqaDraDsaDtaDuaDvaDwaDxaDyaeUaDzaDAaDBaDCaDDaDEaDFaDGanXaDHazKaCgauDaDI.HQayWaDJaDKaDLaDMaDNaDOaDPaDQaDRaDSaDTaDUaDVaDWaDXaDYaDZaD0aD1.EN.RvaD2#6GaD3aD4aD5aD6.I3axqaq0aBp.MbaA#aeR#SN#YA#Z1aD7aeTasLaD8aD9aE.aE#aEa", +"aEbaEcazbaEdaBBaxZaEeawIapiapiapiapiapiapiapiapiacoaEfaEgaEhaEiaEjaEkaElaEmaEnaEoaEpaEqaEraEsaEtaEuaEvaEwaExaEyaEzaEAarU.I0.I0auM.XR.XR.IP.IPaEBar7ay#aqyaECat1aEDar7aEEaEFaEGaEHaEIaEJaEKaELaEMaENamRaEOaEPaEQaERaESaETamVaEUaEVaEWaEXaAZazKaEYaEZaE0aE1aE2aE3aE4aE5aE6aE7aE8aE9aF.aF#aFaaFbaFcaFdaFeaFfaFgaFh.UyaFiaFjaFk#6F#8gaFlaFm#94aFnaFo#3SaBqaFp.I2#Rm#YAawLawLaFqaf8aFraFsaFtaFuaFvaFw", +"aFxaAiaFyaFzaFAaxZaEeauMapiapiapiapiapiapiapiapiadxaFBaFCaFDaFEaFFaFGaFHaFIaFJaFKaFLaFMaFNaFOaFPaFQaFRaFSaFTaFRaFUaFVaFWavl.I0auM.XR.IPaEBaEBaEBaqyanSauGanSaroayaaFXanSaFYaFZaF0aF1aF2aF3aF4aF5aF6aF7#YCaF8aF9aG.aG#aGaaGbaGcaGdaGeaGfaAYaAZaGgaGhaGiaGjaGkaGlaGmaGnaGoaGpaGqaGraGsaGtaGuaGvaGwaGxaGyaGzaGAaGBaGCaGDaGEaGFaGGaGHaGIaGJ.4iaGKaGLahiay1aGM.KlaqJaogawjawjaGNaGOatgaGPaGQaGRaGSaGT", +"aGUaxVaFyaGVaGWaC1aGXagPapiapiapiapiapiapiapiapiaGYaGZaG0aG1aG2aG3aG4aG5aG6aG7aG8aG9aH.aH#aHaaHbaHcaHdaHeaHfaHgaHhaFUaHiavlavlauM.XR.IPaEBaHjaHjay#anSawlaHkaqyaECayaaztaHlaHmaHnaHoaHpaHqaHraHsan7aHtaHuaHvaHwaHxaHyaHzaHAaHBaGcaGeaGfaCfaAZ.GAaHCaHDaHEaHFaHGaHHaHIaHJaHKaHLaHMaHNaHOaHPaHQaHRaHSaHTaHUaHVaHWaHXaHYaHZaH0aH1aH2aH3aH4aH5aH6aH7abhabhaH8apNaocaH9aruaruaGNaI.aI#aIaaIbaIcaIdaIe", +"aIfaIgaIhaIi.L8aIj.NyaIkai2agOagOaedaedabnabnabnaIlaImaInaIoaIpaIqaIraIsaItaIuaIvaIwaIxaIyaIzaIAaIBaHgaHfaICaIDaIEaIFaIG#44#44apkanpanpanp.S..S.acHauoatB#ZHatDavA.HG.HGaIHaIIaIJaIKaILaIMaINaIOaIPaIQaIRaISaITar5aCgaIUaIVaIWarmaIXaIYaIZaI0aI1aI2aI3aI4aI5aI6aI7aI8aI9aJ.aJ#aJaaJbaJcaJdaJeaJfaJgaJhaJiaJjaJkaJlaJmaJnaJoaJpaJqaJraJsaJtaJuaJvaJwatEaJxaviaruaBraJyaJzaJAaJBar5aap#RnaJCaJDaJE", +"aJFaJGaJHaJIaJJaIjaJKaJLai2ai2agOaJMaedaedabnabnaJNaJO.HZ.LLaJPanlaJQaJRaJSaJTaJUaJVaJWaJXaJYaJZaJ0aJ1aJ2aFUaJ3aJ4aJ5aJ6#44#44#44apkanpanpanp.S.acHauoatBatB#ZH#ZHatDavAaJ7aJ8aJ9aK.aK#aKaaKbaKcaKdaKeaKfaJwatDacGaKgaKhaKiaKjaqw.7oaKkaKlaKmaKnaKoaKpaKqaKraKsaKtaKuaKvaKwaKxaKyaKzaKAaKBaKCaKDaKEaKFaKGaKHaKIaKJaKKaKLaKMaKNaKOaKPaKQ.N#aKR.GAaJwatEaJxaviaruaBraJyaJzaKS.Nz.HGat7#M4aKTaKUaKV", +"aKWaKXaKYaKZ.L7.GpaK0.IUai2ai2ai2agOaedaedaedabn.F7apiaJNaJNanlaK1aK2arRaK3aK3aK4aK5aK6aKRaK7aK8aK9aqha#daL.aL#aLaaLbaLc#44#44#44#44anpanpanpanpacHacHauoauoauoauoauoauoaLdaLeaLfaLgaLhaLiaLjaLkaLlaLmatBaocaLnaLoaLpaLqaLraLsaLtaLuaLvaLwaLxaLyaLzaLAaLBaLCaLDaLEaLFaLGaLHaLIaLJaLKaLLaLMaLNaLOaLPaLQaLRaLSaLTaLUaLVaLWaLXaLYaLZaL0aL1aL2aL3.KtaJwatEaJxaviaruaBraJyaJzaL4aGOatEaEWaL5aL6aL7aJE", +"aL8aKXaL9.Nf.L6agJaD6aM.alcai2ai2ai2agOaedaedaedaJN.GwaK1aK1aK1apiaK1aznaM#aM#.HKaMaaMb.Ja.BJaMcarcaMdaMdaBCaqiaMdaGX.HOaq0#44#44#44apkanpanpanpacHacHacHacHaunavBavBavBaphaMeaMfaMgaMhaFWaMiaMeaMjaI.aMkaMlaMmaLoaMnaMoaMpaMqahWaMraMsaMtaMuaMvaMwaMxaMyaMzaMAaMBaMCaMDaMEaMFaMGaMHaMIaMJaMKaMLaMMaMNaMOaMPaMQaMRaMSaMTaMUaMVaMWaMXaMYaMZ.Jk.KRaJwatEaJxaviaruaBraJyaJzaL4.NzauoanVaM0aM1aM2aM3", +"aM4aM5aM6.P0aM7.GraD6aM.alcalc.Gvai2ai2agOaJMaedaJOazmaJO.GwaaiaaiaK2aM8.I7.I7.I7.I7.I7.I7.I7.I7.OqaM9aN..HM.HMaN#aNaaNbaqZaq0aq0#44#44#44apkanpacHacHacHaunavBavBavBavBaf5aNcaNdaNeaNfakx.HUaNgaNhaNiaNjaNjaMmaJBacIaNkaNlaNmatDaNnaNoaNpaNqaNraNsaNtaNuaNvaNwaNxaNyaNzaNAaNBaNCaNDaNEaNFaNGaNH.ihaNIaNJaNKaNLaNMaNNaNOaNPaNQaNRaNS.L8aAnaNTaNUaJwatEaJxaviaruaBraJyaJzaFqaJBatEaNVaL5ascaNWaNX", +"aNYaNZaN0.P0.O3aN1#93aN2aN3aN3alc.Gvai2ai2agOagOaK1aK2aK1.H1aN4.LO.HZazmanNanNanNanNanNanNaN5aN5amIaN6aikaADazpawmailaecaqZaqZaqZaq0#44#44#44apkaunaunaun.IPacHacHacHacHano.TEaN7ano.KPaN8aN9aO.aO#aNiaNjaOaacGapHar5aObaOcaOd.O9.LFaOeaOfaOgaOhaOiaOjaOk.SYaOlaOmaOnaOoaOpaOqaOraEDaHkaOsaynaOtaOuaOvaOwaOxaOyaOzaOAaOBaOCaODaOE.FT.NuaaiaOFaOGaJwatEaJxaviaruaBraJyaJzaOHaOIavAaOJaOKaOLaOMaON", +"aOOaOPaOQaORaOS.QAaOTaIkaN3aN3aN3alcai2ai2ai2agOaN4aM8.GwaJN.GwaK1aK2.Gwanpanpamg#44#6IaqZaqZ.H7azn#94#2uaOU.Nwaaiax8alEaqZaqZaqZaq0#44#44#44#44avBavBaunacHauoatBatBatEaOVaOWaOXaJKaOVaOYaOZ.Kx.NzaLnaO0aO1aO2aO3aO4aO5aO6aO7aO8azMaO9aP.aP#aPaaPbaPcaPdaPeaPfaPgaPhaPiaPjaPkaPlaPmaPnaPoaPpaPqaPraPsaPtaPuaPv.H8aPwaPxaPyaPzaPAaPBaPC.LJajwaPDaJwatEaJxaviaruaBraJyaJzaPEaI.aPFau.aPGaPHaPIaPJ", +"aPKaPLaPM.ZqaPNaPO.Kp.RraN3aN3aN3alcalcai2ai2ai2aJOaM8aM8aPPaJNarR.LNaPQanpanpanpanp.S..S.aimaimaPRano.EUasFaPSai0aPTabbaqZaqZaqZaqZaq0#44#44#44avBavBaunacHatB#ZHatDavAaPUaPVaPWaPXaPYaPZ.KAaP0aGgaP1aO1aP2aP3aP4aP5aP6aP7aP8aP9aQ.aQ#aQaaQbaQcaQdaEYaQeaQfaKSaQgaQhaQiaQjaFXaQkaQlaQmaQnay#aOsaQoaQpaQqaQrayWaQsazGaQtaQuaQvaQwaQxaQyaQzaQAaQBaJwatEaJxaviaruaBraJyaJzaQCaQDaPFavkaQEaOLaQFaQG"}; diff --git a/Tests/images/imagedraw/discontiguous_corners_polygon.png b/Tests/images/imagedraw/discontiguous_corners_polygon.png index 509c42b26e0..8992a165758 100644 Binary files a/Tests/images/imagedraw/discontiguous_corners_polygon.png and b/Tests/images/imagedraw/discontiguous_corners_polygon.png differ diff --git a/Tests/images/imagedraw_rectangle_I.tiff b/Tests/images/imagedraw_rectangle_I.tiff index 9b9eda883a3..f0cb534b63e 100644 Binary files a/Tests/images/imagedraw_rectangle_I.tiff and b/Tests/images/imagedraw_rectangle_I.tiff differ diff --git a/Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png b/Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png new file mode 100644 index 00000000000..b225afc2dc1 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_radius.png b/Tests/images/imagedraw_rounded_rectangle_radius.png new file mode 100644 index 00000000000..e2acf7be1b1 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_radius.png differ diff --git a/Tests/images/imagedraw_stroke_float.png b/Tests/images/imagedraw_stroke_float.png new file mode 100644 index 00000000000..a590f8af0b6 Binary files /dev/null and b/Tests/images/imagedraw_stroke_float.png differ diff --git a/Tests/images/jfif_unit_cm.jpg b/Tests/images/jfif_unit_cm.jpg new file mode 100644 index 00000000000..78b50e60a23 Binary files /dev/null and b/Tests/images/jfif_unit_cm.jpg differ diff --git a/Tests/images/morph_a.png b/Tests/images/morph_a.png index 19f6b777ffe..035fbc4bb84 100644 Binary files a/Tests/images/morph_a.png and b/Tests/images/morph_a.png differ diff --git a/Tests/images/multiline_text_justify.png b/Tests/images/multiline_text_justify.png new file mode 100644 index 00000000000..32eed34cd21 Binary files /dev/null and b/Tests/images/multiline_text_justify.png differ diff --git a/Tests/images/multiline_text_justify_anchor.png b/Tests/images/multiline_text_justify_anchor.png new file mode 100644 index 00000000000..6d3fb421d18 Binary files /dev/null and b/Tests/images/multiline_text_justify_anchor.png differ diff --git a/Tests/images/no_palette_with_transparency_after_rgb.gif b/Tests/images/no_palette_with_transparency_after_rgb.gif new file mode 100644 index 00000000000..41357c1471d Binary files /dev/null and b/Tests/images/no_palette_with_transparency_after_rgb.gif differ diff --git a/Tests/images/op_index.qoi b/Tests/images/op_index.qoi new file mode 100644 index 00000000000..e626aafe6a4 Binary files /dev/null and b/Tests/images/op_index.qoi differ diff --git a/Tests/images/p_4_planes.pcx b/Tests/images/p_4_planes.pcx new file mode 100644 index 00000000000..8c5743a9855 Binary files /dev/null and b/Tests/images/p_4_planes.pcx differ diff --git a/Tests/images/pal8_offset.bmp b/Tests/images/pal8_offset.bmp deleted file mode 100644 index 24be65f22c3..00000000000 Binary files a/Tests/images/pal8_offset.bmp and /dev/null differ diff --git a/Tests/images/pal8rletrns.png b/Tests/images/pal8rletrns.png new file mode 100644 index 00000000000..2362266ef02 Binary files /dev/null and b/Tests/images/pal8rletrns.png differ diff --git a/Tests/images/psd-oob-write-overflow.psd b/Tests/images/psd-oob-write-overflow.psd new file mode 100644 index 00000000000..c2bb10d614e Binary files /dev/null and b/Tests/images/psd-oob-write-overflow.psd differ diff --git a/Tests/images/psd-oob-write-x.psd b/Tests/images/psd-oob-write-x.psd new file mode 100644 index 00000000000..86359f4cb7e Binary files /dev/null and b/Tests/images/psd-oob-write-x.psd differ diff --git a/Tests/images/psd-oob-write-y.psd b/Tests/images/psd-oob-write-y.psd new file mode 100644 index 00000000000..73498266a7d Binary files /dev/null and b/Tests/images/psd-oob-write-y.psd differ diff --git a/Tests/images/psd-oob-write.psd b/Tests/images/psd-oob-write.psd new file mode 100644 index 00000000000..65a4472cf26 Binary files /dev/null and b/Tests/images/psd-oob-write.psd differ diff --git a/Tests/images/separate_planar_extra_samples.tiff b/Tests/images/separate_planar_extra_samples.tiff new file mode 100644 index 00000000000..be51a7570ee Binary files /dev/null and b/Tests/images/separate_planar_extra_samples.tiff differ diff --git a/Tests/images/sugarshack_frame_size.mpo b/Tests/images/sugarshack_frame_size.mpo deleted file mode 100644 index abff98ea57d..00000000000 Binary files a/Tests/images/sugarshack_frame_size.mpo and /dev/null differ diff --git a/Tests/images/test_combine_multiline_ttb.png b/Tests/images/test_combine_multiline_ttb.png new file mode 100644 index 00000000000..d9c4aa2a1ae Binary files /dev/null and b/Tests/images/test_combine_multiline_ttb.png differ diff --git a/Tests/images/test_extents_transparency.gif b/Tests/images/test_extents_transparency.gif new file mode 100644 index 00000000000..739c82ad47c Binary files /dev/null and b/Tests/images/test_extents_transparency.gif differ diff --git a/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli b/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli index ce4607d2dd0..73da81dcb62 100644 Binary files a/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli and b/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli differ diff --git a/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli b/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli index 77a94b87a3a..abe642e6a9d 100644 Binary files a/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli and b/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli differ diff --git a/Tests/images/trailer_loop.pdf b/Tests/images/trailer_loop.pdf new file mode 100644 index 00000000000..7bf27ca370e Binary files /dev/null and b/Tests/images/trailer_loop.pdf differ diff --git a/Tests/images/unimplemented_pixel_format.dds b/Tests/images/unimplemented_pixel_format.dds new file mode 100644 index 00000000000..9092df8b1b5 Binary files /dev/null and b/Tests/images/unimplemented_pixel_format.dds differ diff --git a/Tests/images/zero_mask_totals.dds b/Tests/images/zero_mask_totals.dds new file mode 100644 index 00000000000..31e329e4f26 Binary files /dev/null and b/Tests/images/zero_mask_totals.dds differ diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py index 8788d7021d3..f4e40ea3671 100755 --- a/Tests/oss-fuzz/fuzz_font.py +++ b/Tests/oss-fuzz/fuzz_font.py @@ -16,8 +16,9 @@ import atheris +from atheris.import_hook import instrument_imports -with atheris.instrument_imports(): +with instrument_imports(): import sys import fuzzers diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index 9137391b656..d58a6f0150d 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -14,8 +14,9 @@ import atheris +from atheris.import_hook import instrument_imports -with atheris.instrument_imports(): +with instrument_imports(): import sys import fuzzers diff --git a/Tests/oss-fuzz/python.supp b/Tests/oss-fuzz/python.supp index 94cc87db97d..4803497adeb 100644 --- a/Tests/oss-fuzz/python.supp +++ b/Tests/oss-fuzz/python.supp @@ -1,5 +1,5 @@ { - + Memcheck:Cond ... fun:encode_current_locale @@ -14,3 +14,23 @@ fun:_TIFFReadEncodedTileAndAllocBuffer ... } + +{ + + Memcheck:Leak + match-leak-kinds: all + fun:malloc + fun:_PyMem_RawMalloc + fun:PyObject_Malloc + ... +} + +{ + + Memcheck:Leak + match-leak-kinds: all + fun:malloc + fun:_PyMem_RawRealloc + fun:PyMem_Realloc + ... +} diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 90eb8713a8b..37d11e0ba4c 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -7,11 +7,12 @@ import packaging import pytest -from PIL import Image, UnidentifiedImageError, features +from PIL import Image, features from Tests.helper import skip_unless_feature -if sys.platform.startswith("win32"): - pytest.skip("Fuzzer is linux only", allow_module_level=True) +if sys.platform.startswith("win32") or sys.platform == "ios": + pytest.skip("Fuzzer doesn't run on Windows or iOS", allow_module_level=True) + libjpeg_turbo_version = features.version("libjpeg_turbo") if libjpeg_turbo_version is not None: version = packaging.version.parse(libjpeg_turbo_version) @@ -32,21 +33,17 @@ def test_fuzz_images(path: str) -> None: fuzzers.fuzz_image(f.read()) assert True except ( + # Known exceptions from Pillow OSError, SyntaxError, MemoryError, ValueError, NotImplementedError, OverflowError, - ): - # Known exceptions that are through from Pillow - assert True - except ( + # Known Image.* exceptions Image.DecompressionBombError, Image.DecompressionBombWarning, - UnidentifiedImageError, ): - # Known Image.* exceptions assert True finally: fuzzers.disable_decompressionbomb_error() diff --git a/Tests/test_arro3.py b/Tests/test_arro3.py new file mode 100644 index 00000000000..42d032f7660 --- /dev/null +++ b/Tests/test_arro3.py @@ -0,0 +1,275 @@ +from __future__ import annotations + +import json +from typing import Any, NamedTuple + +import pytest + +from PIL import Image + +from .helper import ( + assert_deep_equal, + assert_image_equal, + hopper, + is_big_endian, +) + +TYPE_CHECKING = False +if TYPE_CHECKING: + from arro3 import compute + from arro3.core import ( + Array, + DataType, + Field, + fixed_size_list_array, + ) +else: + arro3 = pytest.importorskip("arro3", reason="Arro3 not installed") + from arro3 import compute + from arro3.core import Array, DataType, Field, fixed_size_list_array + +TEST_IMAGE_SIZE = (10, 10) + + +def _test_img_equals_pyarray( + img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1 +) -> None: + assert img.height * img.width * elts_per_pixel == len(arr) + px = img.load() + assert px is not None + if elts_per_pixel > 1 and mask is None: + # have to do element-wise comparison when we're comparing + # flattened r,g,b,a to a pixel. + mask = list(range(elts_per_pixel)) + 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)): + if mask: + pixel = px[x, y] + assert isinstance(pixel, tuple) + for ix, elt in enumerate(mask): + if elts_per_pixel == 1: + assert pixel[ix] == arr[y * img.width + x].as_py()[elt] + else: + assert ( + pixel[ix] + == arr[(y * img.width + x) * elts_per_pixel + elt].as_py() + ) + else: + assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) + + +def _test_img_equals_int32_pyarray( + img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1 +) -> None: + assert img.height * img.width * elts_per_pixel == len(arr) + px = img.load() + assert px is not None + if mask is None: + # have to do element-wise comparison when we're comparing + # flattened rgba in an uint32 to a pixel. + mask = list(range(elts_per_pixel)) + 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)): + pixel = px[x, y] + assert isinstance(pixel, tuple) + arr_pixel_int = arr[y * img.width + x].as_py() + arr_pixel_tuple = ( + arr_pixel_int % 256, + (arr_pixel_int // 256) % 256, + (arr_pixel_int // 256**2) % 256, + (arr_pixel_int // 256**3), + ) + if is_big_endian(): + arr_pixel_tuple = arr_pixel_tuple[::-1] + + for ix, elt in enumerate(mask): + assert pixel[ix] == arr_pixel_tuple[elt] + + +fl_uint8_4_type = DataType.list(Field("_", DataType.uint8()).with_nullable(False), 4) + + +@pytest.mark.parametrize( + "mode, dtype, mask", + ( + ("L", DataType.uint8(), None), + ("I", DataType.int32(), None), + ("F", DataType.float32(), None), + ("LA", fl_uint8_4_type, [0, 3]), + ("RGB", fl_uint8_4_type, [0, 1, 2]), + ("RGBA", fl_uint8_4_type, None), + ("RGBX", fl_uint8_4_type, None), + ("CMYK", fl_uint8_4_type, None), + ("YCbCr", fl_uint8_4_type, [0, 1, 2]), + ("HSV", fl_uint8_4_type, [0, 1, 2]), + ), +) +def test_to_array(mode: str, dtype: DataType, mask: list[int] | None) -> None: + img = hopper(mode) + + # Resize to non-square + img = img.crop((3, 0, 124, 127)) + assert img.size == (121, 127) + + arr = Array(img) + _test_img_equals_pyarray(img, arr, mask) + assert arr.type == dtype + + reloaded = Image.fromarrow(arr, mode, img.size) + assert_image_equal(img, reloaded) + + +def test_lifetime() -> None: + # valgrind shouldn't error out here. + # arrays should be accessible after the image is deleted. + + img = hopper("L") + + arr_1 = Array(img) + arr_2 = Array(img) + + del img + + assert compute.sum(arr_1).as_py() > 0 + del arr_1 + + assert compute.sum(arr_2).as_py() > 0 + del arr_2 + + +def test_lifetime2() -> None: + # valgrind shouldn't error out here. + # img should remain after the arrays are collected. + + img = hopper("L") + + arr_1 = Array(img) + arr_2 = Array(img) + + assert compute.sum(arr_1).as_py() > 0 + del arr_1 + + assert compute.sum(arr_2).as_py() > 0 + del arr_2 + + img2 = img.copy() + px = img2.load() + assert px # make mypy happy + assert isinstance(px[0, 0], int) + + +class DataShape(NamedTuple): + dtype: DataType + # Strictly speaking, elt should be a pixel or pixel component, so + # list[uint8][4], float, int, uint32, uint8, etc. But more + # correctly, it should be exactly the dtype from the line above. + elt: Any + elts_per_pixel: int + + +UINT_ARR = DataShape( + dtype=fl_uint8_4_type, + elt=[1, 2, 3, 4], # array of 4 uint8 per pixel + elts_per_pixel=1, # only one array per pixel +) + +UINT = DataShape( + dtype=DataType.uint8(), + elt=3, # one uint8, + elts_per_pixel=4, # but repeated 4x per pixel +) + +UINT32 = DataShape( + dtype=DataType.uint32(), + elt=0xABCDEF45, # one packed int, doesn't fit in a int32 > 0x80000000 + elts_per_pixel=1, # one per pixel +) + +INT32 = DataShape( + dtype=DataType.uint32(), + elt=0x12CDEF45, # one packed int + elts_per_pixel=1, # one per pixel +) + + +@pytest.mark.parametrize( + "mode, data_tp, mask", + ( + ("L", DataShape(DataType.uint8(), 3, 1), None), + ("I", DataShape(DataType.int32(), 1 << 24, 1), None), + ("F", DataShape(DataType.float32(), 3.14159, 1), None), + ("LA", UINT_ARR, [0, 3]), + ("LA", UINT, [0, 3]), + ("RGB", UINT_ARR, [0, 1, 2]), + ("RGBA", UINT_ARR, None), + ("CMYK", UINT_ARR, None), + ("YCbCr", UINT_ARR, [0, 1, 2]), + ("HSV", UINT_ARR, [0, 1, 2]), + ("RGB", UINT, [0, 1, 2]), + ("RGBA", UINT, None), + ("CMYK", UINT, None), + ("YCbCr", UINT, [0, 1, 2]), + ("HSV", UINT, [0, 1, 2]), + ), +) +def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: + dtype, elt, elts_per_pixel = data_tp + + ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] + if dtype == fl_uint8_4_type: + tmp_arr = Array(elt * (ct_pixels * elts_per_pixel), type=DataType.uint8()) + arr = fixed_size_list_array(tmp_arr, 4) + else: + arr = Array([elt] * (ct_pixels * elts_per_pixel), type=dtype) + img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) + + _test_img_equals_pyarray(img, arr, mask, elts_per_pixel) + + +@pytest.mark.parametrize( + "mode, mask", + ( + ("LA", [0, 3]), + ("RGB", [0, 1, 2]), + ("RGBA", None), + ("CMYK", None), + ("YCbCr", [0, 1, 2]), + ("HSV", [0, 1, 2]), + ), +) +@pytest.mark.parametrize("data_tp", (UINT32, INT32)) +def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) -> None: + dtype, elt, elts_per_pixel = data_tp + + ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] + arr = Array([elt] * (ct_pixels * elts_per_pixel), type=dtype) + img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) + + _test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel) + + +@pytest.mark.parametrize( + "mode, metadata", + ( + ("LA", ["L", "X", "X", "A"]), + ("RGB", ["R", "G", "B", "X"]), + ("RGBX", ["R", "G", "B", "X"]), + ("RGBA", ["R", "G", "B", "A"]), + ("CMYK", ["C", "M", "Y", "K"]), + ("YCbCr", ["Y", "Cb", "Cr", "X"]), + ("HSV", ["H", "S", "V", "X"]), + ), +) +def test_image_metadata(mode: str, metadata: list[str]) -> None: + img = hopper(mode) + + arr = Array(img) + + assert arr.type.value_field + assert arr.type.value_field.metadata + assert arr.type.value_field.metadata[b"image"] + + parsed_metadata = json.loads(arr.type.value_field.metadata[b"image"].decode("utf8")) + + assert "bands" in parsed_metadata + assert parsed_metadata["bands"] == metadata diff --git a/Tests/test_arrow.py b/Tests/test_arrow.py new file mode 100644 index 00000000000..9f84a75a472 --- /dev/null +++ b/Tests/test_arrow.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +import pytest + +from PIL import Image + +from .helper import hopper + + +@pytest.mark.parametrize( + "mode, dest_modes", + ( + ("L", ["I", "F", "LA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr", "HSV"]), + ("I", ["L", "F"]), # Technically I;32 can work for any 4x8bit storage. + ("F", ["I", "L", "LA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr", "HSV"]), + ("LA", ["L", "F"]), + ("RGB", ["L", "F"]), + ("RGBA", ["L", "F"]), + ("RGBX", ["L", "F"]), + ("CMYK", ["L", "F"]), + ("YCbCr", ["L", "F"]), + ("HSV", ["L", "F"]), + ), +) +def test_invalid_array_type(mode: str, dest_modes: list[str]) -> None: + img = hopper(mode) + for dest_mode in dest_modes: + with pytest.raises(ValueError): + Image.fromarrow(img, dest_mode, img.size) + + +def test_invalid_array_size() -> None: + img = hopper("RGB") + + assert img.size != (10, 10) + with pytest.raises(ValueError): + Image.fromarrow(img, "RGB", (10, 10)) + + +def test_release_schema() -> None: + # these should not error out, valgrind should be clean + img = hopper("L") + schema = img.__arrow_c_schema__() + del schema + + +def test_release_array() -> None: + # these should not error out, valgrind should be clean + img = hopper("L") + array, schema = img.__arrow_c_array__() + del array + del schema + + +def test_readonly() -> None: + img = hopper("L") + reloaded = Image.fromarrow(img, img.mode, img.size) + assert reloaded.readonly == 1 + reloaded._readonly = 0 + assert reloaded.readonly == 1 + + +def test_multiblock_l_image() -> None: + block_size = Image.core.get_block_size() + + # check a 2 block image in single channel mode + size = (4096, 2 * block_size // 4096) + img = Image.new("L", size, 128) + + with pytest.raises(ValueError): + schema, arr = img.__arrow_c_array__() + + +def test_multiblock_rgba_image() -> None: + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, (block_size // 4096) // 2) + img = Image.new("RGBA", size, (128, 127, 126, 125)) + + with pytest.raises(ValueError): + schema, arr = img.__arrow_c_array__() + + +def test_multiblock_l_schema() -> None: + block_size = Image.core.get_block_size() + + # check a 2 block image in single channel mode + size = (4096, 2 * block_size // 4096) + img = Image.new("L", size, 128) + + with pytest.raises(ValueError): + img.__arrow_c_schema__() + + +def test_multiblock_rgba_schema() -> None: + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, (block_size // 4096) // 2) + img = Image.new("RGBA", size, (128, 127, 126, 125)) + + with pytest.raises(ValueError): + img.__arrow_c_schema__() + + +def test_singleblock_l_image() -> None: + Image.core.set_use_block_allocator(1) + + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, 2 * (block_size // 4096)) + img = Image.new("L", size, 128) + assert img.im.isblock() + + schema, arr = img.__arrow_c_array__() + assert schema + assert arr + + Image.core.set_use_block_allocator(0) + + +def test_singleblock_rgba_image() -> None: + Image.core.set_use_block_allocator(1) + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, (block_size // 4096) // 2) + img = Image.new("RGBA", size, (128, 127, 126, 125)) + assert img.im.isblock() + + schema, arr = img.__arrow_c_array__() + assert schema + assert arr + Image.core.set_use_block_allocator(0) + + +def test_singleblock_l_schema() -> None: + Image.core.set_use_block_allocator(1) + block_size = Image.core.get_block_size() + + # check a 2 block image in single channel mode + size = (4096, 2 * block_size // 4096) + img = Image.new("L", size, 128) + assert img.im.isblock() + + schema = img.__arrow_c_schema__() + assert schema + Image.core.set_use_block_allocator(0) + + +def test_singleblock_rgba_schema() -> None: + Image.core.set_use_block_allocator(1) + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, (block_size // 4096) // 2) + img = Image.new("RGBA", size, (128, 127, 126, 125)) + assert img.im.isblock() + + schema = img.__arrow_c_schema__() + assert schema + Image.core.set_use_block_allocator(0) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 7f848792131..ea0853100cc 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -22,6 +22,8 @@ def test_bad() -> None: for f in get_files("b"): # Assert that there is no unclosed file warning with warnings.catch_warnings(): + warnings.simplefilter("error") + try: with Image.open(f) as im: im.load() @@ -54,7 +56,7 @@ def test_questionable() -> None: im.load() if os.path.basename(f) not in supported: print(f"Please add {f} to the partially supported bmp specs.") - except Exception: # as msg: + except Exception: # noqa: PERF203 if os.path.basename(f) in supported: raise @@ -70,7 +72,7 @@ def test_good() -> None: "pal8-0.bmp": "pal8.png", "pal8rle.bmp": "pal8.png", "pal8topdown.bmp": "pal8.png", - "pal8nonsquare.bmp": "pal8nonsquare-v.png", + "pal8nonsquare.bmp": "pal8nonsquare-e.png", "pal8os2.bmp": "pal8.png", "pal8os2sp.bmp": "pal8.png", "pal8os2v2.bmp": "pal8.png", @@ -93,18 +95,18 @@ def get_compare(f: str) -> str: for f in get_files("g"): try: with Image.open(f) as im: - im.load() 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: + # assert image similar doesn't really work + # with paletized image, since the palette might + # be differently ordered for an equivalent image. + im_converted = im.convert("RGBA") if im.mode == "P" else im + compare_converted = ( + compare.convert("RGBA") if im.mode == "P" else compare + ) + + assert_image_similar(im_converted, compare_converted, 5) + + except Exception as msg: # noqa: PERF203 # there are three here that are unsupported: unsupported = ( os.path.join(base, "g", "rgb32bf.bmp"), diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index 1f6ed61277a..07e62db8ced 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -28,9 +28,13 @@ def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image: def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None: - it = iter(im.getdata()) + it = iter(im.get_flattened_data()) for data_row in data: - im_row = [next(it) for _ in range(im.size[0])] + im_row = [] + for _ in range(im.width): + im_v = next(it) + assert isinstance(im_v, (int, float)) + im_row.append(im_v) 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): @@ -71,6 +75,11 @@ def test_color_modes() -> None: box_blur(sample.convert("YCbCr")) +@pytest.mark.parametrize("size", ((0, 1), (1, 0))) +def test_zero_dimension(size: tuple[int, int]) -> None: + assert box_blur(Image.new("L", size)).size == size + + def test_radius_0() -> None: assert_blur( sample, diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 00c8995b08d..26945ae1a69 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -19,7 +19,7 @@ class TestColorLut3DCoreAPI: def generate_identity_table( self, channels: int, size: int | tuple[int, int, int] - ) -> tuple[int, int, int, int, list[float]]: + ) -> tuple[int, tuple[int, int, int], list[float]]: if isinstance(size, tuple): size_1d, size_2d, size_3d = size else: @@ -39,9 +39,7 @@ def generate_identity_table( ] return ( channels, - size_1d, - size_2d, - size_3d, + (size_1d, size_2d, size_3d), [item for sublist in table for item in sublist], ) @@ -89,107 +87,81 @@ def test_wrong_args(self) -> None: 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 + "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 + "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 + "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) -> None: + im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), 16) + + @pytest.mark.parametrize( + "lut_mode, table_channels, table_size", + [ + ("RGB", 3, 3), + ("CMYK", 4, 3), + ("RGB", 3, (2, 3, 3)), + ("RGB", 3, (65, 3, 3)), + ("RGB", 3, (3, 65, 3)), + ("RGB", 3, (2, 3, 65)), + ], + ) + def test_correct_args( + self, lut_mode: str, table_channels: int, table_size: int | tuple[int, int, int] + ) -> None: 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", + lut_mode, 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) -> None: + *self.generate_identity_table(table_channels, table_size), + ) + + @pytest.mark.parametrize( + "image_mode, lut_mode, table_channels, table_size", + [ + ("L", "RGB", 3, 3), + ("RGB", "L", 3, 3), + ("L", "L", 3, 3), + ("RGB", "RGBA", 3, 3), + ("RGB", "RGB", 4, 3), + ], + ) + def test_wrong_mode( + self, image_mode: str, lut_mode: str, table_channels: int, table_size: int + ) -> None: with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("L", (10, 10), 0) + im = Image.new(image_mode, (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) + lut_mode, + Image.Resampling.BILINEAR, + *self.generate_identity_table(table_channels, table_size), ) - def test_correct_mode(self) -> None: - im = Image.new("RGBA", (10, 10), 0) + @pytest.mark.parametrize( + "image_mode, lut_mode, table_channels, table_size", + [ + ("RGBA", "RGBA", 3, 3), + ("RGBA", "RGBA", 4, 3), + ("RGB", "HSV", 3, 3), + ("RGB", "RGBA", 4, 3), + ], + ) + def test_correct_mode( + self, image_mode: str, lut_mode: str, table_channels: int, table_size: int + ) -> None: + im = Image.new(image_mode, (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) + lut_mode, + Image.Resampling.BILINEAR, + *self.generate_identity_table(table_channels, table_size), ) def test_identities(self) -> None: @@ -290,7 +262,7 @@ def test_channels_order(self) -> None: assert_image_equal( Image.merge('RGB', im.split()[::-1]), im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, - 3, 2, 2, 2, [ + 3, (2, 2, 2), [ 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, @@ -312,7 +284,7 @@ def test_overflow(self) -> None: # fmt: off transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, - 3, 2, 2, 2, + 3, (2, 2, 2), [ -1, -1, -1, 2, -1, -1, -1, 2, -1, 2, 2, -1, @@ -321,6 +293,7 @@ def test_overflow(self) -> None: -1, 2, 2, 2, 2, 2, ])).load() # fmt: on + assert transformed is not None assert transformed[0, 0] == (0, 0, 255) assert transformed[50, 50] == (0, 0, 255) assert transformed[255, 0] == (0, 255, 255) @@ -332,7 +305,7 @@ def test_overflow(self) -> None: # fmt: off transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, - 3, 2, 2, 2, + 3, (2, 2, 2), [ -3, -3, -3, 5, -3, -3, -3, 5, -3, 5, 5, -3, @@ -341,6 +314,7 @@ def test_overflow(self) -> None: -3, 5, 5, 5, 5, 5, ])).load() # fmt: on + assert transformed is not None assert transformed[0, 0] == (0, 0, 255) assert transformed[50, 50] == (0, 0, 255) assert transformed[255, 0] == (0, 255, 255) @@ -412,10 +386,12 @@ def test_numpy_sources(self) -> None: 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.shape == (table.size,) 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.shape == (table.size,) # Check application diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 2c1de8bc3d2..2a22f805d8d 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -188,5 +188,5 @@ def test_units(self) -> None: ), ) def test_warnings(self, var: dict[str, str]) -> None: - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match=list(var)[0]): Image._apply_env_variables(var) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index c140156f9ea..98d833736fa 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -12,19 +12,16 @@ class TestDecompressionBomb: - def teardown_method(self) -> None: - Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT - def test_no_warning_small_file(self) -> None: # Implicit assert: no warning. # A warning would cause a failure. with Image.open(TEST_FILE): pass - def test_no_warning_no_limit(self) -> None: + def test_no_warning_no_limit(self, monkeypatch: pytest.MonkeyPatch) -> None: # Arrange # Turn limit off - Image.MAX_IMAGE_PIXELS = None + monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None) assert Image.MAX_IMAGE_PIXELS is None # Act / Assert @@ -33,18 +30,18 @@ def test_no_warning_no_limit(self) -> None: with Image.open(TEST_FILE): pass - def test_warning(self) -> None: + def test_warning(self, monkeypatch: pytest.MonkeyPatch) -> None: # Set limit to trigger warning on the test file - Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 + monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 128 * 128 - 1) assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1 with pytest.warns(Image.DecompressionBombWarning): with Image.open(TEST_FILE): pass - def test_exception(self) -> None: + def test_exception(self, monkeypatch: pytest.MonkeyPatch) -> None: # Set limit to trigger exception on the test file - Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 + monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 64 * 128 - 1) assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1 with pytest.raises(Image.DecompressionBombError): @@ -66,9 +63,9 @@ def test_exception_gif_extents(self) -> None: with pytest.raises(Image.DecompressionBombError): im.seek(1) - def test_exception_gif_zero_width(self) -> None: + def test_exception_gif_zero_width(self, monkeypatch: pytest.MonkeyPatch) -> None: # Set limit to trigger exception on the test file - Image.MAX_IMAGE_PIXELS = 4 * 64 * 128 + monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 4 * 64 * 128) assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128 with pytest.raises(Image.DecompressionBombError): diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 584d8f91d67..1e98ecfff37 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -9,9 +9,9 @@ "version, expected", [ ( - 11, - "Old thing is deprecated and will be removed in Pillow 11 " - r"\(2024-10-15\)\. Use new thing instead\.", + 13, + "Old thing is deprecated and will be removed in Pillow 13 " + r"\(2026-10-15\)\. Use new thing instead\.", ), ( None, @@ -47,25 +47,24 @@ def test_unknown_version() -> None: ], ) def test_old_version(deprecated: str, plural: bool, expected: str) -> None: - expected = r"" with pytest.raises(RuntimeError, match=expected): _deprecate.deprecate(deprecated, 1, plural=plural) def test_plural() -> None: expected = ( - r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " + r"Old things are deprecated and will be removed in Pillow 13 \(2026-10-15\)\. " r"Use new thing instead\." ) with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old things", 11, "new thing", plural=True) + _deprecate.deprecate("Old things", 13, "new thing", plural=True) def test_replacement_and_action() -> None: expected = "Use only one of 'replacement' and 'action'" with pytest.raises(ValueError, match=expected): _deprecate.deprecate( - "Old thing", 11, replacement="new thing", action="Upgrade to new thing" + "Old thing", 13, replacement="new thing", action="Upgrade to new thing" ) @@ -78,16 +77,16 @@ def test_replacement_and_action() -> None: ) def test_action(action: str) -> None: expected = ( - r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " + r"Old thing is deprecated and will be removed in Pillow 13 \(2026-10-15\)\. " r"Upgrade to new thing\." ) with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old thing", 11, action=action) + _deprecate.deprecate("Old thing", 13, action=action) def test_no_replacement_or_action() -> None: expected = ( - r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)" + r"Old thing is deprecated and will be removed in Pillow 13 \(2026-10-15\)" ) with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old thing", 11) + _deprecate.deprecate("Old thing", 13) diff --git a/Tests/test_features.py b/Tests/test_features.py index b7eefa09ae1..93d803fc133 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -2,7 +2,6 @@ import io import re -from typing import Callable import pytest @@ -10,10 +9,9 @@ from .helper import skip_unless_feature -try: - from PIL import _webp -except ImportError: - pass +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable def test_check() -> None: @@ -37,10 +35,11 @@ def test(name: str, function: Callable[[str], str | None]) -> None: else: assert function(name) == version if name != "PIL": - if name == "zlib" and version is not None: - version = re.sub(".zlib-ng$", "", version) - elif name == "libtiff" and version is not None: - version = re.sub("t$", "", version) + if version is not None: + if name == "zlib" and features.check_feature("zlib_ng"): + version = re.sub(".zlib-ng$", "", version) + elif name == "libtiff": + version = re.sub("t$", "", version) assert version is None or re.search(r"\d+(\.\d+)*$", version) for module in features.modules: @@ -51,22 +50,6 @@ def test(name: str, function: Callable[[str], str | None]) -> None: test(feature, features.version_feature) -@skip_unless_feature("webp") -def test_webp_transparency() -> None: - assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha() - assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY - - -@skip_unless_feature("webp") -def test_webp_mux() -> None: - assert features.check("webp_mux") == _webp.HAVE_WEBPMUX - - -@skip_unless_feature("webp") -def test_webp_anim() -> None: - assert features.check("webp_anim") == _webp.HAVE_WEBPANIM - - @skip_unless_feature("libjpeg_turbo") def test_libjpeg_turbo_version() -> None: version = features.version("libjpeg_turbo") @@ -92,10 +75,9 @@ def test_check_codecs(feature: str) -> None: def test_check_warns_on_nonexistent() -> None: - with pytest.warns(UserWarning) as cm: + with pytest.warns(UserWarning, match="Unknown feature 'typo'."): has_feature = features.check("typo") assert has_feature is False - assert str(cm[-1].message) == "Unknown feature 'typo'." def test_supported_modules() -> None: @@ -125,6 +107,25 @@ def test_unsupported_module() -> None: features.version_module(module) +def test_unsupported_feature() -> None: + # Arrange + feature = "unsupported_feature" + # Act / Assert + with pytest.raises(ValueError): + features.check_feature(feature) + with pytest.raises(ValueError): + features.version_feature(feature) + + +def test_unsupported_version() -> None: + assert features.version("unsupported_version") is None + + +def test_modulenotfound(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(features, "features", {"test": ("PIL._test", "", "")}) + assert features.check_feature("test") is None + + @pytest.mark.parametrize("supported_formats", (True, False)) def test_pilinfo(supported_formats: bool) -> None: buf = io.StringIO() diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index e95850212a5..b57a1d1ad8c 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -1,5 +1,6 @@ from __future__ import annotations +from io import BytesIO from pathlib import Path import pytest @@ -12,6 +13,7 @@ # (referenced from https://wiki.mozilla.org/APNG_Specification) def test_apng_basic() -> None: with Image.open("Tests/images/apng/single_frame.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated assert im.n_frames == 1 assert im.get_format_mimetype() == "image/apng" @@ -20,6 +22,7 @@ def test_apng_basic() -> None: assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/single_frame_default.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.is_animated assert im.n_frames == 2 assert im.get_format_mimetype() == "image/apng" @@ -34,8 +37,11 @@ def test_apng_basic() -> None: with pytest.raises(EOFError): im.seek(2) - # test rewind support im.seek(0) + with pytest.raises(ValueError, match="cannot seek to frame 2"): + im._seek(2) + + # test rewind support assert im.getpixel((0, 0)) == (255, 0, 0, 255) assert im.getpixel((64, 32)) == (255, 0, 0, 255) im.seek(1) @@ -49,6 +55,7 @@ def test_apng_basic() -> None: ) def test_apng_fdat(filename: str) -> None: with Image.open(filename) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -56,31 +63,37 @@ def test_apng_fdat(filename: str) -> None: def test_apng_dispose() -> None: with Image.open("Tests/images/apng/dispose_op_none.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) 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: + assert isinstance(im, PngImagePlugin.PngImageFile) 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: + assert isinstance(im, PngImagePlugin.PngImageFile) 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: + assert isinstance(im, PngImagePlugin.PngImageFile) 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: + assert isinstance(im, PngImagePlugin.PngImageFile) 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: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) @@ -88,21 +101,25 @@ def test_apng_dispose() -> None: def test_apng_dispose_region() -> None: with Image.open("Tests/images/apng/dispose_op_none_region.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) 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: + assert isinstance(im, PngImagePlugin.PngImageFile) 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: + assert isinstance(im, PngImagePlugin.PngImageFile) 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: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -129,6 +146,7 @@ def test_apng_dispose_op_previous_frame() -> None: # ], # ) with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (255, 0, 0, 255) @@ -142,26 +160,31 @@ def test_apng_dispose_op_background_p_mode() -> None: def test_apng_blend() -> None: with Image.open("Tests/images/apng/blend_op_source_solid.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) 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: + assert isinstance(im, PngImagePlugin.PngImageFile) 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: + assert isinstance(im, PngImagePlugin.PngImageFile) 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: + assert isinstance(im, PngImagePlugin.PngImageFile) 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: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 97) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -175,6 +198,7 @@ def test_apng_blend_transparency() -> None: def test_apng_chunk_order() -> None: with Image.open("Tests/images/apng/fctl_actl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -230,100 +254,115 @@ def test_apng_num_plays() -> None: def test_apng_mode() -> None: with Image.open("Tests/images/apng/mode_16bit.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) 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_grayscale.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) 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_grayscale_alpha.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) 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 isinstance(im, PngImagePlugin.PngImageFile) 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) + im_rgb = im.convert("RGB") + assert im_rgb.getpixel((0, 0)) == (0, 255, 0) + assert im_rgb.getpixel((64, 32)) == (0, 255, 0) with Image.open("Tests/images/apng/mode_palette_alpha.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) 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) + im_rgba = im.convert("RGBA") + assert im_rgba.getpixel((0, 0)) == (0, 255, 0, 255) + assert im_rgba.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) 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) + im_rgba = im.convert("RGBA") + assert im_rgba.getpixel((0, 0)) == (0, 0, 255, 128) + assert im_rgba.getpixel((64, 32)) == (0, 0, 255, 128) def test_apng_chunk_errors() -> None: with Image.open("Tests/images/apng/chunk_no_actl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated - with pytest.warns(UserWarning): - with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: - im.load() - assert not im.is_animated + with pytest.warns(UserWarning, match="Invalid APNG"): + im = Image.open("Tests/images/apng/chunk_multi_actl.png") + assert isinstance(im, PngImagePlugin.PngImageFile) + assert not im.is_animated + im.close() with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with Image.open("Tests/images/apng/chunk_no_fctl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(SyntaxError): im.seek(im.n_frames - 1) with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(SyntaxError): im.seek(im.n_frames - 1) with Image.open("Tests/images/apng/chunk_no_fdat.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(SyntaxError): im.seek(im.n_frames - 1) def test_apng_syntax_errors() -> None: - with pytest.warns(UserWarning): - with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: - assert not im.is_animated - with pytest.raises(OSError): - im.load() - - with pytest.warns(UserWarning): - with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: - assert not im.is_animated - im.load() + with pytest.warns(UserWarning, match="Invalid APNG"): + im = Image.open("Tests/images/apng/syntax_num_frames_zero.png") + assert isinstance(im, PngImagePlugin.PngImageFile) + assert not im.is_animated + with pytest.raises(OSError): + im.load() + im.close() + + with pytest.warns(UserWarning, match="Invalid APNG"): + im = Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") + assert isinstance(im, PngImagePlugin.PngImageFile) + assert not im.is_animated + im.load() + im.close() # 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 + assert isinstance(im, PngImagePlugin.PngImageFile) + im.seek(im.n_frames - 1) with pytest.raises(OSError): with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) im.load() - with pytest.warns(UserWarning): - with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: - assert not im.is_animated - im.load() + with pytest.warns(UserWarning, match="Invalid APNG"): + im = Image.open("Tests/images/apng/syntax_num_frames_invalid.png") + assert isinstance(im, PngImagePlugin.PngImageFile) + assert not im.is_animated + im.load() + im.close() @pytest.mark.parametrize( @@ -341,16 +380,18 @@ def test_apng_syntax_errors() -> None: def test_apng_sequence_errors(test_file: str) -> None: with pytest.raises(SyntaxError): with Image.open(f"Tests/images/apng/{test_file}") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) im.load() def test_apng_save(tmp_path: Path) -> None: with Image.open("Tests/images/apng/single_frame.png") as im: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file, save_all=True) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.load() assert not im.is_animated assert im.n_frames == 1 @@ -366,6 +407,7 @@ def test_apng_save(tmp_path: Path) -> None: ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.load() assert im.is_animated assert im.n_frames == 2 @@ -377,7 +419,7 @@ def test_apng_save(tmp_path: Path) -> None: def test_apng_save_alpha(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127)) @@ -395,7 +437,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None: # 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") + test_file = 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( @@ -405,17 +447,13 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None: 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 + assert isinstance(im, PngImagePlugin.PngImageFile) + im.seek(im.n_frames - 1) + im.load() def test_apng_save_duration_loop(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" with Image.open("Tests/images/apng/delay.png") as im: frames = [] durations = [] @@ -452,6 +490,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150] ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.n_frames == 1 assert "duration" not in im.info @@ -463,6 +502,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: duration=[500, 100, 150], ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.n_frames == 2 assert im.info["duration"] == 600 @@ -473,12 +513,31 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: frame.info["duration"] = 300 frame.save(test_file, save_all=True, append_images=[frame, different_frame]) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.n_frames == 2 assert im.info["duration"] == 600 +def test_apng_save_duration_float(tmp_path: Path) -> None: + test_file = tmp_path / "temp.png" + im = Image.new("1", (1, 1)) + im2 = Image.new("1", (1, 1), 1) + im.save(test_file, save_all=True, append_images=[im2], duration=0.5) + + with Image.open(test_file) as reloaded: + assert reloaded.info["duration"] == 0.5 + + +def test_apng_save_large_duration(tmp_path: Path) -> None: + test_file = tmp_path / "temp.png" + im = Image.new("1", (1, 1)) + im2 = Image.new("1", (1, 1), 1) + with pytest.raises(ValueError, match="cannot write duration"): + im.save(test_file, save_all=True, append_images=[im2], duration=65536000) + + def test_apng_save_disposal(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = 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)) @@ -579,7 +638,7 @@ def test_apng_save_disposal(tmp_path: Path) -> None: def test_apng_save_disposal_previous(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = 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)) @@ -601,7 +660,7 @@ def test_apng_save_disposal_previous(tmp_path: Path) -> None: def test_apng_save_blend(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = 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)) @@ -669,7 +728,7 @@ def test_apng_save_blend(tmp_path: Path) -> None: def test_apng_save_size(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im = Image.new("L", (100, 100)) im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))]) @@ -678,6 +737,25 @@ def test_apng_save_size(tmp_path: Path) -> None: assert reloaded.size == (200, 200) +def test_compress_level() -> None: + compress_level_sizes = {} + for compress_level in (0, 9): + out = BytesIO() + + im = Image.new("L", (100, 100)) + im.save( + out, + "PNG", + save_all=True, + append_images=[Image.new("L", (200, 200))], + compress_level=compress_level, + ) + + compress_level_sizes[compress_level] = len(out.getvalue()) + + assert compress_level_sizes[0] > compress_level_sizes[9] + + def test_seek_after_close() -> None: im = Image.open("Tests/images/apng/delay.png") im.seek(1) @@ -693,7 +771,7 @@ def test_seek_after_close() -> None: def test_different_modes_in_later_frames( mode: str, default_image: bool, duplicate: bool, tmp_path: Path ) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im = Image.new("L", (1, 1)) im.save( @@ -707,7 +785,7 @@ def test_different_modes_in_later_frames( def test_different_durations(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" with Image.open("Tests/images/apng/different_durations.png") as im: for _ in range(3): diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py new file mode 100644 index 00000000000..a25f7717797 --- /dev/null +++ b/Tests/test_file_avif.py @@ -0,0 +1,787 @@ +from __future__ import annotations + +import gc +import os +import re +import warnings +from collections.abc import Generator, Sequence +from contextlib import contextmanager +from io import BytesIO +from pathlib import Path +from typing import Any + +import pytest + +from PIL import ( + AvifImagePlugin, + GifImagePlugin, + Image, + ImageDraw, + ImageFile, + UnidentifiedImageError, + features, +) + +from .helper import ( + PillowLeakTestCase, + assert_image, + assert_image_similar, + assert_image_similar_tofile, + hopper, + skip_unless_feature, +) + +try: + from PIL import _avif + + HAVE_AVIF = True +except ImportError: + HAVE_AVIF = False + + +TEST_AVIF_FILE = "Tests/images/avif/hopper.avif" + + +def assert_xmp_orientation(xmp: bytes, expected: int) -> None: + assert int(xmp.split(b'tiff:Orientation="')[1].split(b'"')[0]) == expected + + +def roundtrip(im: ImageFile.ImageFile, **options: Any) -> ImageFile.ImageFile: + out = BytesIO() + im.save(out, "AVIF", **options) + return Image.open(out) + + +def skip_unless_avif_decoder(codec_name: str) -> pytest.MarkDecorator: + reason = f"{codec_name} decode not available" + return pytest.mark.skipif( + not HAVE_AVIF or not _avif.decoder_codec_available(codec_name), reason=reason + ) + + +def skip_unless_avif_encoder(codec_name: str) -> pytest.MarkDecorator: + reason = f"{codec_name} encode not available" + return pytest.mark.skipif( + not HAVE_AVIF or not _avif.encoder_codec_available(codec_name), reason=reason + ) + + +def is_docker_qemu() -> bool: + try: + init_proc_exe = os.readlink("/proc/1/exe") + except (FileNotFoundError, PermissionError): + return False + return "qemu" in init_proc_exe + + +class TestUnsupportedAvif: + def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) + + with pytest.raises(UnidentifiedImageError): + with pytest.warns(UserWarning, match="AVIF support not installed"): + with Image.open(TEST_AVIF_FILE): + pass + + def test_unsupported_open(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) + + with pytest.raises(SyntaxError): + AvifImagePlugin.AvifImageFile(TEST_AVIF_FILE) + + +@skip_unless_feature("avif") +class TestFileAvif: + def test_version(self) -> None: + version = features.version_module("avif") + assert version is not None + assert re.search(r"^\d+\.\d+\.\d+$", version) + + def test_codec_version(self) -> None: + assert AvifImagePlugin.get_codec_version("unknown") is None + + for codec_name in ("aom", "dav1d", "rav1e", "svt"): + codec_version = AvifImagePlugin.get_codec_version(codec_name) + if _avif.decoder_codec_available( + codec_name + ) or _avif.encoder_codec_available(codec_name): + assert codec_version is not None + assert re.search(r"^v?\d+\.\d+\.\d+(-([a-z\d])+)*$", codec_version) + else: + assert codec_version is None + + def test_read(self) -> None: + """ + Can we read an AVIF file without error? + Does it have the bits we expect? + """ + + with Image.open(TEST_AVIF_FILE) as image: + assert image.mode == "RGB" + assert image.size == (128, 128) + assert image.format == "AVIF" + assert image.get_format_mimetype() == "image/avif" + + # generated with: + # avifdec hopper.avif hopper_avif_write.png + assert_image_similar_tofile( + image, "Tests/images/avif/hopper_avif_write.png", 11.5 + ) + + def test_write_rgb(self, tmp_path: Path) -> None: + """ + Can we write a RGB mode file to avif without error? + Does it have the bits we expect? + """ + + temp_file = tmp_path / "temp.avif" + + im = hopper() + im.save(temp_file) + with Image.open(temp_file) as reloaded: + assert reloaded.mode == "RGB" + assert reloaded.size == (128, 128) + assert reloaded.format == "AVIF" + + # avifdec hopper.avif avif/hopper_avif_write.png + assert_image_similar_tofile( + reloaded, "Tests/images/avif/hopper_avif_write.png", 6.88 + ) + + # 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. + assert_image_similar(reloaded, im, 9.28) + + def test_AvifEncoder_with_invalid_args(self) -> None: + """ + Calling encoder functions with no arguments should result in an error. + """ + with pytest.raises(TypeError): + _avif.AvifEncoder() + + def test_AvifDecoder_with_invalid_args(self) -> None: + """ + Calling decoder functions with no arguments should result in an error. + """ + with pytest.raises(TypeError): + _avif.AvifDecoder() + + def test_invalid_dimensions(self, tmp_path: Path) -> None: + test_file = tmp_path / "temp.avif" + im = Image.new("RGB", (0, 0)) + with pytest.raises(ValueError): + im.save(test_file) + + def test_encoder_finish_none_error( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """Save should raise an OSError if AvifEncoder.finish returns None""" + + class _mock_avif: + class AvifEncoder: + def __init__(self, *args: Any) -> None: + pass + + def add(self, *args: Any) -> None: + pass + + def finish(self) -> None: + return None + + monkeypatch.setattr(AvifImagePlugin, "_avif", _mock_avif) + + im = Image.new("RGB", (150, 150)) + test_file = tmp_path / "temp.avif" + with pytest.raises(OSError): + im.save(test_file) + + def test_no_resource_warning(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + with warnings.catch_warnings(): + warnings.simplefilter("error") + + im.save(tmp_path / "temp.avif") + + @pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"]) + def test_accept_ftyp_brands(self, major_brand: bytes) -> None: + data = b"\x00\x00\x00\x1cftyp%s\x00\x00\x00\x00" % major_brand + assert AvifImagePlugin._accept(data) is True + + def test_file_pointer_could_be_reused(self) -> None: + with open(TEST_AVIF_FILE, "rb") as blob: + with Image.open(blob) as im: + im.load() + with Image.open(blob) as im: + im.load() + + def test_background_from_gif(self, tmp_path: Path) -> None: + with Image.open("Tests/images/chi.gif") as im: + original_value = im.convert("RGB").getpixel((1, 1)) + assert isinstance(original_value, tuple) + + # Save as AVIF + out_avif = tmp_path / "temp.avif" + im.save(out_avif, save_all=True) + + # Save as GIF + out_gif = tmp_path / "temp.gif" + with Image.open(out_avif) as im: + im.save(out_gif) + + with Image.open(out_gif) as reread: + reread_value = reread.convert("RGB").getpixel((1, 1)) + assert isinstance(reread_value, tuple) + difference = sum([abs(original_value[i] - reread_value[i]) for i in range(3)]) + assert difference <= 6 + + def test_save_single_frame(self, tmp_path: Path) -> None: + temp_file = tmp_path / "temp.avif" + with Image.open("Tests/images/chi.gif") as im: + im.save(temp_file) + with Image.open(temp_file) as im: + assert isinstance(im, AvifImagePlugin.AvifImageFile) + assert im.n_frames == 1 + + def test_invalid_file(self) -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + AvifImagePlugin.AvifImageFile(invalid_file) + + def test_load_transparent_rgb(self) -> None: + test_file = "Tests/images/avif/transparency.avif" + with Image.open(test_file) as im: + assert_image(im, "RGBA", (64, 64)) + + # image has 876 transparent pixels + colors = im.getchannel("A").getcolors() + assert colors is not None + assert colors[0] == (876, 0) + + def test_save_transparent(self, tmp_path: Path) -> None: + im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) + assert im.getcolors() == [(100, (0, 0, 0, 0))] + + test_file = tmp_path / "temp.avif" + im.save(test_file) + + # check if saved image contains the same transparency + with Image.open(test_file) as im: + assert_image(im, "RGBA", (10, 10)) + assert im.getcolors() == [(100, (0, 0, 0, 0))] + + def test_save_icc_profile(self) -> None: + with Image.open("Tests/images/avif/icc_profile_none.avif") as im: + assert "icc_profile" not in im.info + + with Image.open("Tests/images/avif/icc_profile.avif") as with_icc: + expected_icc = with_icc.info["icc_profile"] + assert expected_icc is not None + + im = roundtrip(im, icc_profile=expected_icc) + assert im.info["icc_profile"] == expected_icc + + def test_discard_icc_profile(self) -> None: + with Image.open("Tests/images/avif/icc_profile.avif") as im: + im = roundtrip(im, icc_profile=None) + assert "icc_profile" not in im.info + + def test_roundtrip_icc_profile(self) -> None: + with Image.open("Tests/images/avif/icc_profile.avif") as im: + expected_icc = im.info["icc_profile"] + + im = roundtrip(im) + assert im.info["icc_profile"] == expected_icc + + def test_roundtrip_no_icc_profile(self) -> None: + with Image.open("Tests/images/avif/icc_profile_none.avif") as im: + assert "icc_profile" not in im.info + + im = roundtrip(im) + assert "icc_profile" not in im.info + + def test_exif(self) -> None: + # With an EXIF chunk + with Image.open("Tests/images/avif/exif.avif") as im: + exif = im.getexif() + assert exif[274] == 1 + + with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: + exif = im.getexif() + assert exif[274] == 3 + + @pytest.mark.parametrize("use_bytes", [True, False]) + @pytest.mark.parametrize("orientation", [1, 2, 3, 4, 5, 6, 7, 8]) + def test_exif_save( + self, + tmp_path: Path, + use_bytes: bool, + orientation: int, + ) -> None: + exif = Image.Exif() + exif[274] = orientation + exif_data = exif.tobytes() + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, exif=exif_data if use_bytes else exif) + + with Image.open(test_file) as reloaded: + if orientation == 1: + assert "exif" not in reloaded.info + else: + assert reloaded.getexif()[274] == orientation + assert reloaded.info["exif"] == exif_data + + def test_exif_without_orientation(self, tmp_path: Path) -> None: + exif = Image.Exif() + exif[272] = b"test" + exif_data = exif.tobytes() + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, exif=exif) + + with Image.open(test_file) as reloaded: + assert reloaded.info["exif"] == exif_data + + def test_exif_invalid(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(SyntaxError): + im.save(test_file, exif=b"invalid") + + @pytest.mark.parametrize( + "rot, mir, exif_orientation", + [ + (0, 0, 4), + (0, 1, 2), + (1, 0, 5), + (1, 1, 7), + (2, 0, 2), + (2, 1, 4), + (3, 0, 7), + (3, 1, 5), + ], + ) + def test_rot_mir_exif( + self, rot: int, mir: int, exif_orientation: int, tmp_path: Path + ) -> None: + with Image.open(f"Tests/images/avif/rot{rot}mir{mir}.avif") as im: + exif = im.getexif() + assert exif[274] == exif_orientation + + test_file = tmp_path / "temp.avif" + im.save(test_file, exif=exif) + with Image.open(test_file) as reloaded: + assert reloaded.getexif()[274] == exif_orientation + + def test_xmp(self) -> None: + with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: + xmp = im.info["xmp"] + assert_xmp_orientation(xmp, 3) + + def test_xmp_save(self, tmp_path: Path) -> None: + xmp_arg = "\n".join( + [ + '', + '', + ' ', + ' ', + " ", + "", + '', + ] + ) + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, xmp=xmp_arg) + + with Image.open(test_file) as reloaded: + xmp = reloaded.info["xmp"] + assert_xmp_orientation(xmp, 1) + + def test_tell(self) -> None: + with Image.open(TEST_AVIF_FILE) as im: + assert im.tell() == 0 + + def test_seek(self) -> None: + with Image.open(TEST_AVIF_FILE) as im: + im.seek(0) + + with pytest.raises(EOFError): + im.seek(1) + + @pytest.mark.parametrize("subsampling", ["4:4:4", "4:2:2", "4:2:0", "4:0:0"]) + def test_encoder_subsampling(self, tmp_path: Path, subsampling: str) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, subsampling=subsampling) + + def test_encoder_subsampling_invalid(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, subsampling="foo") + + @pytest.mark.parametrize("value", ["full", "limited"]) + def test_encoder_range(self, tmp_path: Path, value: str) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, range=value) + + def test_encoder_range_invalid(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, range="foo") + + @skip_unless_avif_encoder("aom") + def test_encoder_codec_param(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, codec="aom") + + def test_encoder_codec_invalid(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, codec="foo") + + @skip_unless_avif_decoder("dav1d") + def test_decoder_codec_cannot_encode(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, codec="dav1d") + + @skip_unless_avif_encoder("aom") + @pytest.mark.parametrize( + "advanced", + [ + {"tune": "psnr"}, + (("tune", "psnr"),), + [("tune", "psnr")], + ], + ) + def test_encoder_advanced_codec_options( + self, advanced: dict[str, str] | Sequence[tuple[str, str]] + ) -> None: + with Image.open(TEST_AVIF_FILE) as im: + ctrl_buf = BytesIO() + im.save(ctrl_buf, "AVIF", codec="aom") + test_buf = BytesIO() + im.save( + test_buf, + "AVIF", + codec="aom", + advanced=advanced, + ) + assert ctrl_buf.getvalue() != test_buf.getvalue() + + @skip_unless_avif_encoder("aom") + @pytest.mark.parametrize("advanced", [{"foo": "bar"}, {"foo": 1234}, 1234]) + def test_encoder_advanced_codec_options_invalid( + self, tmp_path: Path, advanced: dict[str, str] | int + ) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, codec="aom", advanced=advanced) + + @skip_unless_avif_decoder("aom") + def test_decoder_codec_param(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "aom") + + with Image.open(TEST_AVIF_FILE) as im: + assert im.size == (128, 128) + + @skip_unless_avif_encoder("rav1e") + def test_encoder_codec_cannot_decode( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "rav1e") + + with pytest.raises(ValueError): + with Image.open(TEST_AVIF_FILE): + pass + + def test_decoder_codec_invalid(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "foo") + + with pytest.raises(ValueError): + with Image.open(TEST_AVIF_FILE): + pass + + @skip_unless_avif_encoder("aom") + def test_encoder_codec_available(self) -> None: + assert _avif.encoder_codec_available("aom") is True + + def test_encoder_codec_available_bad_params(self) -> None: + with pytest.raises(TypeError): + _avif.encoder_codec_available() + + @skip_unless_avif_decoder("dav1d") + def test_encoder_codec_available_cannot_decode(self) -> None: + assert _avif.encoder_codec_available("dav1d") is False + + def test_encoder_codec_available_invalid(self) -> None: + assert _avif.encoder_codec_available("foo") is False + + def test_encoder_quality_valueerror(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, quality="invalid") + + @skip_unless_avif_decoder("aom") + def test_decoder_codec_available(self) -> None: + assert _avif.decoder_codec_available("aom") is True + + def test_decoder_codec_available_bad_params(self) -> None: + with pytest.raises(TypeError): + _avif.decoder_codec_available() + + @skip_unless_avif_encoder("rav1e") + def test_decoder_codec_available_cannot_decode(self) -> None: + assert _avif.decoder_codec_available("rav1e") is False + + def test_decoder_codec_available_invalid(self) -> None: + assert _avif.decoder_codec_available("foo") is False + + def test_p_mode_transparency(self, tmp_path: Path) -> None: + im = Image.new("P", size=(64, 64)) + draw = ImageDraw.Draw(im) + draw.rectangle(xy=[(0, 0), (32, 32)], fill=255) + draw.rectangle(xy=[(32, 32), (64, 64)], fill=255) + + out_png = tmp_path / "temp.png" + im.save(out_png, transparency=0) + with Image.open(out_png) as im_png: + out_avif = tmp_path / "temp.avif" + im_png.save(out_avif, quality=100) + + with Image.open(out_avif) as expected: + assert_image_similar(im_png.convert("RGBA"), expected, 0.17) + + def test_decoder_strict_flags(self) -> None: + # This would fail if full avif strictFlags were enabled + with Image.open("Tests/images/avif/hopper-missing-pixi.avif") as im: + assert im.size == (128, 128) + + @skip_unless_avif_encoder("aom") + @pytest.mark.parametrize("speed", [-1, 1, 11]) + def test_aom_optimizations(self, tmp_path: Path, speed: int) -> None: + test_file = tmp_path / "temp.avif" + hopper().save(test_file, codec="aom", speed=speed) + + @skip_unless_avif_encoder("svt") + def test_svt_optimizations(self, tmp_path: Path) -> None: + test_file = tmp_path / "temp.avif" + hopper().save(test_file, codec="svt", speed=1) + + +@skip_unless_feature("avif") +class TestAvifAnimation: + @contextmanager + def star_frames(self) -> Generator[list[Image.Image], None, None]: + with Image.open("Tests/images/avif/star.png") as f: + yield [f, f.rotate(90), f.rotate(180), f.rotate(270)] + + def test_n_frames(self) -> None: + """ + Ensure that AVIF format sets n_frames and is_animated attributes + correctly. + """ + + with Image.open(TEST_AVIF_FILE) as im: + assert isinstance(im, AvifImagePlugin.AvifImageFile) + assert im.n_frames == 1 + assert not im.is_animated + + with Image.open("Tests/images/avif/star.avifs") as im: + assert isinstance(im, AvifImagePlugin.AvifImageFile) + assert im.n_frames == 5 + assert im.is_animated + + def test_write_animation_P(self, tmp_path: Path) -> None: + """ + Convert an animated GIF to animated AVIF, then compare the frame + count, and ensure the frames are visually similar to the originals. + """ + + with Image.open("Tests/images/avif/star.gif") as original: + assert isinstance(original, GifImagePlugin.GifImageFile) + assert original.n_frames > 1 + + temp_file = tmp_path / "temp.avif" + original.save(temp_file, save_all=True) + with Image.open(temp_file) as im: + assert isinstance(im, AvifImagePlugin.AvifImageFile) + assert im.n_frames == original.n_frames + + # Compare first frame in P mode to frame from original GIF + assert_image_similar(im, original.convert("RGBA"), 2) + + # Compare later frames in RGBA mode to frames from original GIF + for frame in range(1, original.n_frames): + original.seek(frame) + im.seek(frame) + assert_image_similar(im, original, 2.54) + + def test_write_animation_RGBA(self, tmp_path: Path) -> None: + """ + Write an animated AVIF from RGBA frames, and ensure the frames + are visually similar to the originals. + """ + + def check(temp_file: Path) -> None: + with Image.open(temp_file) as im: + assert isinstance(im, AvifImagePlugin.AvifImageFile) + assert im.n_frames == 4 + + # Compare first frame to original + assert_image_similar(im, frame1, 2.7) + + # Compare second frame to original + im.seek(1) + assert_image_similar(im, frame2, 4.1) + + with self.star_frames() as frames: + frame1 = frames[0] + frame2 = frames[1] + temp_file1 = tmp_path / "temp.avif" + frames[0].copy().save(temp_file1, save_all=True, append_images=frames[1:]) + check(temp_file1) + + # Test appending using a generator + def imGenerator( + ims: list[Image.Image], + ) -> Generator[Image.Image, None, None]: + yield from ims + + temp_file2 = tmp_path / "temp_generator.avif" + frames[0].copy().save( + temp_file2, + save_all=True, + append_images=imGenerator(frames[1:]), + ) + check(temp_file2) + + def test_sequence_dimension_mismatch_check(self, tmp_path: Path) -> None: + temp_file = tmp_path / "temp.avif" + frame1 = Image.new("RGB", (100, 100)) + frame2 = Image.new("RGB", (150, 150)) + with pytest.raises(ValueError): + frame1.save(temp_file, save_all=True, append_images=[frame2]) + + def test_heif_raises_unidentified_image_error(self) -> None: + with pytest.raises(UnidentifiedImageError): + with Image.open("Tests/images/avif/hopper.heif"): + pass + + @pytest.mark.parametrize("alpha_premultiplied", [False, True]) + def test_alpha_premultiplied( + self, tmp_path: Path, alpha_premultiplied: bool + ) -> None: + temp_file = tmp_path / "temp.avif" + color = (200, 200, 200, 1) + im = Image.new("RGBA", (1, 1), color) + im.save(temp_file, alpha_premultiplied=alpha_premultiplied) + + expected = (255, 255, 255, 1) if alpha_premultiplied else color + with Image.open(temp_file) as reloaded: + assert reloaded.getpixel((0, 0)) == expected + + def test_timestamp_and_duration(self, tmp_path: Path) -> None: + """ + Try passing a list of durations, and make sure the encoded + timestamps and durations are correct. + """ + + durations = [1, 10, 20, 30, 40] + temp_file = tmp_path / "temp.avif" + with self.star_frames() as frames: + frames[0].save( + temp_file, + save_all=True, + append_images=(frames[1:] + [frames[0]]), + duration=durations, + ) + + with Image.open(temp_file) as im: + assert isinstance(im, AvifImagePlugin.AvifImageFile) + assert im.n_frames == 5 + assert im.is_animated + + # Check that timestamps and durations match original values specified + timestamp = 0 + for frame in range(im.n_frames): + im.seek(frame) + im.load() + assert im.info["duration"] == durations[frame] + assert im.info["timestamp"] == timestamp + timestamp += durations[frame] + + def test_seeking(self, tmp_path: Path) -> None: + """ + Create an animated AVIF file, and then try seeking through frames in + reverse-order, verifying the timestamps and durations are correct. + """ + + duration = 33 + temp_file = tmp_path / "temp.avif" + with self.star_frames() as frames: + frames[0].save( + temp_file, + save_all=True, + append_images=(frames[1:] + [frames[0]]), + duration=duration, + ) + + with Image.open(temp_file) as im: + assert isinstance(im, AvifImagePlugin.AvifImageFile) + assert im.n_frames == 5 + assert im.is_animated + + # Traverse frames in reverse, checking timestamps and durations + timestamp = duration * (im.n_frames - 1) + for frame in reversed(range(im.n_frames)): + im.seek(frame) + im.load() + assert im.info["duration"] == duration + assert im.info["timestamp"] == timestamp + timestamp -= duration + + def test_seek_errors(self) -> None: + with Image.open("Tests/images/avif/star.avifs") as im: + with pytest.raises(EOFError): + im.seek(-1) + + with pytest.raises(EOFError): + im.seek(42) + + +MAX_THREADS = os.cpu_count() or 1 + + +@skip_unless_feature("avif") +class TestAvifLeaks(PillowLeakTestCase): + mem_limit = MAX_THREADS * 3 * 1024 + iterations = 100 + + @pytest.mark.skipif( + is_docker_qemu(), reason="Skipping on cross-architecture containers" + ) + def test_leak_load(self) -> None: + with open(TEST_AVIF_FILE, "rb") as f: + im_data = f.read() + + def core() -> None: + with Image.open(BytesIO(im_data)) as im: + im.load() + gc.collect() + + self._test_leak(core) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 1e2f20c407b..5f6b263a1e3 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -4,12 +4,11 @@ import pytest -from PIL import Image +from PIL import BlpImagePlugin, Image from .helper import ( - assert_image_equal, assert_image_equal_tofile, - assert_image_similar, + assert_image_similar_tofile, hopper, ) @@ -19,6 +18,7 @@ def test_load_blp1() -> None: assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png") with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im: + assert im.mode == "RGBA" im.load() @@ -37,25 +37,30 @@ def test_load_blp2_dxt1a() -> None: assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(BlpImagePlugin.BLPFormatError): + BlpImagePlugin.BlpImageFile(invalid_file) + + def test_save(tmp_path: Path) -> None: - f = str(tmp_path / "temp.blp") + f = 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) + assert_image_equal_tofile(im.convert("RGB"), f) with Image.open("Tests/images/transparent.png") as im: - f = str(tmp_path / "temp.blp") + f = tmp_path / "temp.blp" im.convert("P").save(f, blp_version=version) - with Image.open(f) as reloaded: - assert_image_similar(im, reloaded, 8) + assert_image_similar_tofile(im, f, 8) im = hopper() - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Unsupported BLP image mode"): im.save(f) diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 2ff4160bd71..c8ac4652434 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -6,6 +6,8 @@ import pytest from PIL import BmpImagePlugin, Image, _binary +from PIL._binary import o16le as o16 +from PIL._binary import o32le as o32 from .helper import ( assert_image_equal, @@ -15,25 +17,19 @@ ) -def test_sanity(tmp_path: Path) -> None: - def roundtrip(im: Image.Image) -> None: - outfile = str(tmp_path / "temp.bmp") +@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) +def test_sanity(mode: str, tmp_path: Path) -> None: + outfile = tmp_path / "temp.bmp" - im.save(outfile, "BMP") + im = hopper(mode) + im.save(outfile, "BMP") - 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" - - roundtrip(hopper()) - - roundtrip(hopper("1")) - roundtrip(hopper("L")) - roundtrip(hopper("P")) - 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() -> None: @@ -46,7 +42,7 @@ def test_fallback_if_mmap_errors() -> None: # 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") + assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp") def test_save_to_bytes() -> None: @@ -66,7 +62,7 @@ def test_small_palette(tmp_path: Path) -> None: colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] im.putpalette(colors) - out = str(tmp_path / "temp.bmp") + out = tmp_path / "temp.bmp" im.save(out) with Image.open(out) as reloaded: @@ -74,7 +70,7 @@ def test_small_palette(tmp_path: Path) -> None: def test_save_too_large(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.bmp") + outfile = tmp_path / "temp.bmp" with Image.new("RGB", (1, 1)) as im: im._size = (37838, 37838) with pytest.raises(ValueError): @@ -96,7 +92,7 @@ def test_dpi() -> None: def test_save_bmp_with_dpi(tmp_path: Path) -> None: # Test for #1301 # Arrange - outfile = str(tmp_path / "temp.jpg") + outfile = tmp_path / "temp.jpg" with Image.open("Tests/images/hopper.bmp") as im: assert im.info["dpi"] == (95.98654816726399, 95.98654816726399) @@ -112,7 +108,7 @@ def test_save_bmp_with_dpi(tmp_path: Path) -> None: def test_save_float_dpi(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.bmp") + outfile = 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: @@ -120,7 +116,7 @@ def test_save_float_dpi(tmp_path: Path) -> None: def test_load_dib() -> None: - # test for #1293, Imagegrab returning Unsupported Bitfields Format + # 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" @@ -152,7 +148,7 @@ def test_dib_header_size(header_size: int, path: str) -> None: def test_save_dib(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.dib") + outfile = tmp_path / "temp.dib" with Image.open("Tests/images/clipboard.dib") as im: im.save(outfile) @@ -169,9 +165,9 @@ def test_rgba_bitfields() -> None: 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)) + im_rgb = Image.merge("RGB", (r, g, b)) - assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp") + assert_image_equal_tofile(im_rgb, "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 @@ -196,9 +192,9 @@ def test_rle8() -> None: # 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() + with Image.open(io.BytesIO(data)) as im: + with pytest.raises(ValueError): + im.load() def test_rle4() -> None: @@ -220,13 +216,50 @@ def test_rle4() -> None: def test_rle8_eof(file_name: str, length: int) -> None: 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_rle_delta() -> None: + with Image.open("Tests/images/bmp/q/pal8rletrns.bmp") as im: + assert_image_equal_tofile(im, "Tests/images/pal8rletrns.png") + + +def test_unsupported_bmp_bitfields_layout() -> None: + fp = io.BytesIO( + o32(40) # header size + + b"\x00" * 10 + + o16(1) # bits + + o32(3) # BITFIELDS compression + + b"\x00" * 32 + ) + with pytest.raises(OSError, match="Unsupported BMP bitfields layout"): + Image.open(fp) + + +@pytest.mark.parametrize( + "offset, path", + ( + (26, "pal8os2.bmp"), + (54, "pal8.bmp"), + ), +) +def test_offset(offset: int, path: str) -> None: + image_path = "Tests/images/bmp/g/" + path + # Exclude the palette size from the pixel data offset + with open(image_path, "rb") as fp: + data = fp.read() + data = data[:10] + o32(offset) + data[14:] with Image.open(io.BytesIO(data)) as im: - with pytest.raises(ValueError): - im.load() + assert_image_equal_tofile(im, image_path) -def test_offset() -> None: - # 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") +def test_use_raw_alpha(monkeypatch: pytest.MonkeyPatch) -> None: + with Image.open("Tests/images/bmp/g/rgb32.bmp") as im: + assert im.info["compression"] == BmpImagePlugin.BmpImageFile.COMPRESSIONS["RAW"] + assert im.mode == "RGB" + + monkeypatch.setattr(BmpImagePlugin, "USE_RAW_ALPHA", True) + with Image.open("Tests/images/bmp/g/rgb32.bmp") as im: + assert im.mode == "RGBA" diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 77ee5b0ea12..8c6bb1a69f7 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -43,7 +43,7 @@ def test_load() -> None: def test_save(tmp_path: Path) -> None: # Arrange im = hopper() - tmpfile = str(tmp_path / "temp.bufr") + tmpfile = tmp_path / "temp.bufr" # Act / Assert: stub cannot save without an implemented handler with pytest.raises(OSError): @@ -61,6 +61,7 @@ def open(self, im: ImageFile.StubImageFile) -> None: def load(self, im: ImageFile.StubImageFile) -> Image.Image: self.loaded = True + assert im.fp is not None im.fp.close() return Image.new("RGB", (1, 1)) @@ -79,8 +80,8 @@ def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: im.load() assert handler.is_loaded() - temp_file = str(tmp_path / "temp.bufr") + temp_file = tmp_path / "temp.bufr" im.save(temp_file) assert handler.saved - BufrStubImagePlugin._handler = None + BufrStubImagePlugin.register_handler(None) diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 7f76fb47a6a..c73f2a40cba 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -1,13 +1,9 @@ from __future__ import annotations -from typing import Literal - import pytest from PIL import ContainerIO, Image -from .helper import hopper - TEST_FILE = "Tests/images/dummy.container" @@ -17,12 +13,19 @@ def test_sanity() -> None: def test_isatty() -> None: - with hopper() as im: - container = ContainerIO.ContainerIO(im, 0, 0) + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 0, 0) assert container.isatty() is False +def test_seekable() -> None: + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 0, 0) + + assert container.seekable() is True + + @pytest.mark.parametrize( "mode, expected_position", ( @@ -31,7 +34,7 @@ def test_isatty() -> None: (2, 100), ), ) -def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None: +def test_seek_mode(mode: int, expected_position: int) -> None: # Arrange with open(TEST_FILE, "rb") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -44,6 +47,14 @@ def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None: assert container.tell() == expected_position +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_readable(bytesmode: bool) -> None: + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 0, 120) + + assert container.readable() is True + + @pytest.mark.parametrize("bytesmode", (True, False)) def test_read_n0(bytesmode: bool) -> None: # Arrange @@ -51,7 +62,7 @@ def test_read_n0(bytesmode: bool) -> None: container = ContainerIO.ContainerIO(fh, 22, 100) # Act - container.seek(81) + assert container.seek(81) == 81 data = container.read() # Assert @@ -67,7 +78,7 @@ def test_read_n(bytesmode: bool) -> None: container = ContainerIO.ContainerIO(fh, 22, 100) # Act - container.seek(81) + assert container.seek(81) == 81 data = container.read(3) # Assert @@ -83,7 +94,7 @@ def test_read_eof(bytesmode: bool) -> None: container = ContainerIO.ContainerIO(fh, 22, 100) # Act - container.seek(100) + assert container.seek(100) == 100 data = container.read() # Assert @@ -94,21 +105,65 @@ def test_read_eof(bytesmode: bool) -> None: @pytest.mark.parametrize("bytesmode", (True, False)) def test_readline(bytesmode: bool) -> None: - # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 0, 120) - # Act data = container.readline() - - # Assert if bytesmode: data = data.decode() assert data == "This is line 1\n" + data = container.readline(4) + if bytesmode: + data = data.decode() + assert data == "This" + @pytest.mark.parametrize("bytesmode", (True, False)) def test_readlines(bytesmode: bool) -> None: + 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) + + data = container.readlines() + if bytesmode: + data = [line.decode() for line in data] + assert data == expected + + assert container.seek(0) == 0 + + data = container.readlines(2) + if bytesmode: + data = [line.decode() for line in data] + assert data == expected[:2] + + +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_write(bytesmode: bool) -> None: + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 0, 120) + + assert container.writable() is False + + with pytest.raises(NotImplementedError): + container.write(b"" if bytesmode else "") + with pytest.raises(NotImplementedError): + container.writelines([]) + with pytest.raises(NotImplementedError): + container.truncate() + + +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_iter(bytesmode: bool) -> None: # Arrange expected = [ "This is line 1\n", @@ -124,9 +179,19 @@ def test_readlines(bytesmode: bool) -> None: container = ContainerIO.ContainerIO(fh, 0, 120) # Act - data = container.readlines() + data = list(container) # Assert if bytesmode: data = [line.decode() for line in data] assert data == expected + + +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_file(bytesmode: bool) -> None: + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 0, 120) + + assert isinstance(container.fileno(), int) + container.flush() + container.close() diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index dbf1b866d7f..4b3e3afcb43 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -1,8 +1,13 @@ from __future__ import annotations +from io import BytesIO + import pytest from PIL import CurImagePlugin, Image +from PIL._binary import o8 +from PIL._binary import o16le as o16 +from PIL._binary import o32le as o32 TEST_FILE = "Tests/images/deerstalker.cur" @@ -17,6 +22,24 @@ def test_sanity() -> None: assert im.getpixel((16, 16)) == (84, 87, 86, 255) +def test_largest_cursor() -> None: + magic = b"\x00\x00\x02\x00" + sizes = ((1, 1), (8, 8), (4, 4)) + data = magic + o16(len(sizes)) + for w, h in sizes: + image_offset = 6 + len(sizes) * 16 if (w, h) == max(sizes) else 0 + data += o8(w) + o8(h) + o8(0) * 10 + o32(image_offset) + data += ( + o32(12) # header size + + o16(8) # width + + o16(16) # height + + o16(0) # planes + + o16(1) # bits + ) + with Image.open(BytesIO(data)) as im: + assert im.size == (8, 8) + + def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" @@ -26,6 +49,7 @@ def test_invalid_file() -> None: no_cursors_file = "Tests/images/no_cursors.cur" cur = CurImagePlugin.CurImageFile(TEST_FILE) + assert cur.fp is not None cur.fp.close() with open(no_cursors_file, "rb") as cur.fp: with pytest.raises(TypeError): diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 65337cad9b1..e9d88dd3964 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -26,16 +26,18 @@ def test_sanity() -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(TEST_FILE) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(TEST_FILE) im.load() im.close() @@ -43,6 +45,8 @@ def test_closed_file() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(TEST_FILE) as im: im.load() @@ -65,12 +69,14 @@ def test_tell() -> None: def test_n_frames() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, DcxImagePlugin.DcxImageFile) assert im.n_frames == 1 assert not im.is_animated def test_eoferror() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, DcxImagePlugin.DcxImageFile) n_frames = im.n_frames # Test seeking past the last frame diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index ebc0e89a1c5..931ff02f1fb 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -9,7 +9,13 @@ from PIL import DdsImagePlugin, Image -from .helper import assert_image_equal, assert_image_equal_tofile, hopper +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + assert_image_similar_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" @@ -51,7 +57,7 @@ def test_sanity_dxt1_bc1(image_path: str) -> None: """Check DXT1 and BC1 images can be opened""" with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target: - target = target.convert("RGBA") + target_rgba = target.convert("RGBA") with Image.open(image_path) as im: im.load() @@ -59,7 +65,7 @@ def test_sanity_dxt1_bc1(image_path: str) -> None: assert im.mode == "RGBA" assert im.size == (256, 256) - assert_image_equal(im, target) + assert_image_equal(im, target_rgba) def test_sanity_dxt3() -> None: @@ -109,6 +115,32 @@ def test_sanity_ati1_bc4u(image_path: str) -> None: assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) +def test_dx10_bc2(tmp_path: Path) -> None: + out = tmp_path / "temp.dds" + with Image.open(TEST_FILE_DXT3) as im: + im.save(out, pixel_format="BC2") + + with Image.open(out) as reloaded: + assert reloaded.format == "DDS" + assert reloaded.mode == "RGBA" + assert reloaded.size == (256, 256) + + assert_image_similar(im, reloaded, 3.81) + + +def test_dx10_bc3(tmp_path: Path) -> None: + out = tmp_path / "temp.dds" + with Image.open(TEST_FILE_DXT5) as im: + im.save(out, pixel_format="BC3") + + with Image.open(out) as reloaded: + assert reloaded.format == "DDS" + assert reloaded.mode == "RGBA" + assert reloaded.size == (256, 256) + + assert_image_similar(im, reloaded, 3.69) + + @pytest.mark.parametrize( "image_path", ( @@ -152,7 +184,7 @@ def test_sanity_ati2_bc5u(image_path: str) -> None: @pytest.mark.parametrize( - ("image_path", "expected_path"), + "image_path, expected_path", ( # hexeditted to be typeless (TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM), @@ -248,7 +280,7 @@ def test_dx10_r8g8b8a8_unorm_srgb() -> None: @pytest.mark.parametrize( - ("mode", "size", "test_file"), + "mode, size, test_file", [ ("L", (128, 128), TEST_FILE_UNCOMPRESSED_L), ("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA), @@ -331,11 +363,13 @@ def test_dxt5_colorblock_alpha_issue_4142() -> None: with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im: px = im.getpixel((0, 0)) + assert isinstance(px, tuple) assert px[0] != 0 assert px[1] != 0 assert px[2] != 0 px = im.getpixel((1, 0)) + assert isinstance(px, tuple) assert px[0] != 0 assert px[1] != 0 assert px[2] != 0 @@ -346,34 +380,46 @@ def test_palette() -> None: assert_image_equal_tofile(im, "Tests/images/transparent.gif") +def test_zero_mask_totals() -> None: + with Image.open("Tests/images/zero_mask_totals.dds") as im: + im.load() + + +def test_unsupported_header_size() -> None: + with pytest.raises(OSError, match="Unsupported header size 0"): + with Image.open(BytesIO(b"DDS " + b"\x00" * 4)): + pass + + def test_unsupported_bitcount() -> None: - with pytest.raises(OSError): + with pytest.raises(OSError, match="Unsupported bitcount 24 for 131072"): with Image.open("Tests/images/unsupported_bitcount.dds"): pass @pytest.mark.parametrize( - "test_file", + "test_file, message", ( - "Tests/images/unimplemented_dxgi_format.dds", - "Tests/images/unimplemented_pfflags.dds", + ("Tests/images/unimplemented_dxgi_format.dds", "Unimplemented DXGI format 93"), + ("Tests/images/unimplemented_pixel_format.dds", "Unimplemented pixel format 0"), + ("Tests/images/unimplemented_pfflags.dds", "Unknown pixel format flags 8"), ), ) -def test_not_implemented(test_file: str) -> None: - with pytest.raises(NotImplementedError): +def test_not_implemented(test_file: str, message: str) -> None: + with pytest.raises(NotImplementedError, match=message): with Image.open(test_file): pass def test_save_unsupported_mode(tmp_path: Path) -> None: - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" im = hopper("HSV") - with pytest.raises(OSError): + with pytest.raises(OSError, match="cannot write mode HSV as DDS"): im.save(out) @pytest.mark.parametrize( - ("mode", "test_file"), + "mode, test_file", [ ("L", "Tests/images/linear_gradient.png"), ("LA", "Tests/images/uncompressed_la.png"), @@ -382,10 +428,115 @@ def test_save_unsupported_mode(tmp_path: Path) -> None: ], ) def test_save(mode: str, test_file: str, tmp_path: Path) -> None: - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" with Image.open(test_file) as im: assert im.mode == mode im.save(out) - with Image.open(out) as reloaded: - assert_image_equal(im, reloaded) + assert_image_equal_tofile(im, out) + + +def test_save_unsupported_pixel_format(tmp_path: Path) -> None: + out = tmp_path / "temp.dds" + im = hopper() + with pytest.raises(OSError, match="cannot write pixel format UNKNOWN"): + im.save(out, pixel_format="UNKNOWN") + + +def test_save_dxt1(tmp_path: Path) -> None: + # RGB + out = tmp_path / "temp.dds" + with Image.open(TEST_FILE_DXT1) as im: + im.convert("RGB").save(out, pixel_format="DXT1") + assert_image_similar_tofile(im, out, 1.84) + + # RGBA + im_alpha = im.copy() + im_alpha.putpixel((0, 0), (0, 0, 0, 0)) + im_alpha.save(out, pixel_format="DXT1") + with Image.open(out) as reloaded: + assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0) + + # L + im_l = im.convert("L") + im_l.save(out, pixel_format="DXT1") + assert_image_similar_tofile(im_l.convert("RGBA"), out, 6.07) + + # LA + im_alpha.convert("LA").save(out, pixel_format="DXT1") + with Image.open(out) as reloaded: + assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0) + + +def test_save_dxt3(tmp_path: Path) -> None: + # RGB + out = tmp_path / "temp.dds" + with Image.open(TEST_FILE_DXT3) as im: + im_rgb = im.convert("RGB") + im_rgb.save(out, pixel_format="DXT3") + assert_image_similar_tofile(im_rgb.convert("RGBA"), out, 1.26) + + # RGBA + im.save(out, pixel_format="DXT3") + assert_image_similar_tofile(im, out, 3.81) + + # L + im_l = im.convert("L") + im_l.save(out, pixel_format="DXT3") + assert_image_similar_tofile(im_l.convert("RGBA"), out, 5.89) + + # LA + im_la = im.convert("LA") + im_la.save(out, pixel_format="DXT3") + assert_image_similar_tofile(im_la.convert("RGBA"), out, 8.44) + + +def test_save_dxt5(tmp_path: Path) -> None: + # RGB + out = tmp_path / "temp.dds" + with Image.open(TEST_FILE_DXT1) as im: + im.convert("RGB").save(out, pixel_format="DXT5") + assert_image_similar_tofile(im, out, 1.84) + + # RGBA + with Image.open(TEST_FILE_DXT5) as im_rgba: + im_rgba.save(out, pixel_format="DXT5") + assert_image_similar_tofile(im_rgba, out, 3.69) + + # L + im_l = im.convert("L") + im_l.save(out, pixel_format="DXT5") + assert_image_similar_tofile(im_l.convert("RGBA"), out, 6.07) + + # LA + im_la = im_rgba.convert("LA") + im_la.save(out, pixel_format="DXT5") + assert_image_similar_tofile(im_la.convert("RGBA"), out, 8.32) + + +def test_save_dx10_bc5(tmp_path: Path) -> None: + out = tmp_path / "temp.dds" + with Image.open(TEST_FILE_DX10_BC5_TYPELESS) as im: + im.save(out, pixel_format="BC5") + assert_image_similar_tofile(im, out, 9.56) + + im_l = hopper("L") + with pytest.raises(OSError, match="only RGB mode can be written as BC5"): + im_l.save(out, pixel_format="BC5") + + +@pytest.mark.parametrize( + "pixel_format, mode", + ( + ("DXT1", "RGBA"), + ("DXT3", "RGBA"), + ("DXT5", "RGBA"), + ("BC2", "RGBA"), + ("BC3", "RGBA"), + ("BC5", "RGB"), + ), +) +def test_save_large_file(tmp_path: Path, pixel_format: str, mode: str) -> None: + im = hopper(mode).resize((440, 440)) + # should not error in valgrind + im.save(tmp_path / "img.dds", pixel_format=pixel_format) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 1c21aa8ca67..d4e8db4f43c 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -8,29 +8,31 @@ from PIL import EpsImagePlugin, Image, UnidentifiedImageError, features from .helper import ( + assert_image_equal_tofile, assert_image_similar, assert_image_similar_tofile, hopper, is_win32, mark_if_feature_version, skip_unless_feature, + timeout_unless_slower_valgrind, ) 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/eps/zero_bb.eps" +FILE2 = "Tests/images/eps/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/eps/zero_bb.png" +FILE1_COMPARE_SCALE2 = "Tests/images/eps/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/eps/non_zero_bb.png" +FILE2_COMPARE_SCALE2 = "Tests/images/eps/non_zero_bb_scale2.png" # EPS test files with binary preview -FILE3 = "Tests/images/binary_preview_map.eps" +FILE3 = "Tests/images/eps/binary_preview_map.eps" # Three unsigned 32bit little-endian values: # 0xC6D3D0C5 magic number @@ -80,13 +82,13 @@ @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -@pytest.mark.parametrize( - ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) -) +@pytest.mark.parametrize("filename, size", ((FILE1, (460, 352)), (FILE2, (360, 252)))) @pytest.mark.parametrize("scale", (1, 2)) def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: expected_size = tuple(s * scale for s in size) with Image.open(filename) as image: + assert isinstance(image, EpsImagePlugin.EpsImageFile) + image.load(scale=scale) assert image.mode == "RGB" assert image.size == expected_size @@ -96,10 +98,14 @@ def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_load() -> None: with Image.open(FILE1) as im: - assert im.load()[0, 0] == (255, 255, 255) + px = im.load() + assert px is not None + assert px[0, 0] == (255, 255, 255) # Test again now that it has already been loaded once - assert im.load()[0, 0] == (255, 255, 255) + px = im.load() + assert px is not None + assert px[0, 0] == (255, 255, 255) def test_binary() -> None: @@ -128,6 +134,15 @@ def test_binary_header_only() -> None: EpsImagePlugin.EpsImageFile(data) +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_simple_eps_file(prefix: bytes) -> None: + data = io.BytesIO(prefix + b"\n".join(simple_eps_file)) + with Image.open(data) as img: + assert img.mode == "RGB" + assert img.size == (100, 100) + assert img.format == "EPS" + + @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) def test_missing_version_comment(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) @@ -143,23 +158,21 @@ def test_missing_boundingbox_comment(prefix: bytes) -> None: @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment(prefix: bytes) -> None: - data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) +@pytest.mark.parametrize( + "file_lines", + ( + simple_eps_file_with_invalid_boundingbox, + simple_eps_file_with_invalid_boundingbox_valid_imagedata, + ), +) +def test_invalid_boundingbox_comment( + prefix: bytes, file_lines: tuple[bytes, ...] +) -> None: + data = io.BytesIO(prefix + b"\n".join(file_lines)) with pytest.raises(OSError, match="cannot determine EPS bounding box"): EpsImagePlugin.EpsImageFile(data) -@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix: bytes) -> None: - data = io.BytesIO( - prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata) - ) - with Image.open(data) as img: - assert img.mode == "RGB" - assert img.size == (100, 100) - assert img.format == "EPS" - - @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) def test_ascii_comment_too_long(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) @@ -179,17 +192,25 @@ def test_load_long_binary_data(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) with Image.open(data) as img: img.load() - assert img.mode == "RGB" + assert img.mode == "1" assert img.size == (100, 100) assert img.format == "EPS" +def test_begin_binary() -> None: + with open("Tests/images/eps/binary_preview_map.eps", "rb") as fp: + data = bytearray(fp.read()) + data[76875 : 76875 + 11] = b"%" * 11 + with Image.open(io.BytesIO(data)) as img: + assert img.size == (399, 480) + + @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() -> None: - with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: + with Image.open("Tests/images/eps/pil_sample_cmyk.eps") as cmyk_image: assert cmyk_image.mode == "CMYK" assert cmyk_image.size == (100, 100) assert cmyk_image.format == "EPS" @@ -206,8 +227,8 @@ def test_cmyk() -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_showpage() -> None: # 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: + with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image: + with Image.open("Tests/images/eps/reqd_showpage.png") as target: # should not crash/hang plot_image.load() # fonts could be slightly different @@ -216,11 +237,13 @@ def test_showpage() -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_transparency() -> None: - with Image.open("Tests/images/reqd_showpage.eps") as plot_image: + with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image: + assert isinstance(plot_image, EpsImagePlugin.EpsImageFile) + plot_image.load(transparency=True) assert plot_image.mode == "RGBA" - with Image.open("Tests/images/reqd_showpage_transparency.png") as target: + with Image.open("Tests/images/eps/reqd_showpage_transparency.png") as target: # fonts could be slightly different assert_image_similar(plot_image, target, 6) @@ -229,7 +252,7 @@ def test_transparency() -> None: def test_file_object(tmp_path: Path) -> None: # issue 479 with Image.open(FILE1) as image1: - with open(str(tmp_path / "temp.eps"), "wb") as fh: + with open(tmp_path / "temp.eps", "wb") as fh: image1.save(fh, "EPS") @@ -242,19 +265,29 @@ def test_bytesio_object() -> None: img.load() with Image.open(FILE1_COMPARE) as image1_scale1_compare: - image1_scale1_compare = image1_scale1_compare.convert("RGB") - image1_scale1_compare.load() - assert_image_similar(img, image1_scale1_compare, 5) + image1_scale1_compare_rgb = image1_scale1_compare.convert("RGB") + image1_scale1_compare_rgb.load() + assert_image_similar(img, image1_scale1_compare_rgb, 5) -def test_1_mode() -> None: - with Image.open("Tests/images/1.eps") as im: - assert im.mode == "1" +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@pytest.mark.parametrize( + # These images have an "ImageData" descriptor. + "filename", + ( + "Tests/images/eps/1.eps", + "Tests/images/eps/1_boundingbox_after_imagedata.eps", + "Tests/images/eps/1_second_imagedata.eps", + ), +) +def test_1(filename: str) -> None: + with Image.open(filename) as im: + assert_image_equal_tofile(im, "Tests/images/eps/1.bmp") def test_image_mode_not_supported(tmp_path: Path) -> None: im = hopper("RGBA") - tmpfile = str(tmp_path / "temp.eps") + tmpfile = tmp_path / "temp.eps" with pytest.raises(ValueError): im.save(tmpfile) @@ -268,17 +301,17 @@ def test_render_scale1() -> None: with Image.open(FILE1) as image1_scale1: image1_scale1.load() with Image.open(FILE1_COMPARE) as image1_scale1_compare: - image1_scale1_compare = image1_scale1_compare.convert("RGB") - image1_scale1_compare.load() - assert_image_similar(image1_scale1, image1_scale1_compare, 5) + image1_scale1_compare_rgb = image1_scale1_compare.convert("RGB") + image1_scale1_compare_rgb.load() + assert_image_similar(image1_scale1, image1_scale1_compare_rgb, 5) # Non-zero bounding box with Image.open(FILE2) as image2_scale1: image2_scale1.load() with Image.open(FILE2_COMPARE) as image2_scale1_compare: - image2_scale1_compare = image2_scale1_compare.convert("RGB") - image2_scale1_compare.load() - assert_image_similar(image2_scale1, image2_scale1_compare, 10) + image2_scale1_compare_rgb = image2_scale1_compare.convert("RGB") + image2_scale1_compare_rgb.load() + assert_image_similar(image2_scale1, image2_scale1_compare_rgb, 10) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @@ -288,28 +321,30 @@ def test_render_scale2() -> None: # Zero bounding box with Image.open(FILE1) as image1_scale2: + assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile) image1_scale2.load(scale=2) with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare: - image1_scale2_compare = image1_scale2_compare.convert("RGB") - image1_scale2_compare.load() - assert_image_similar(image1_scale2, image1_scale2_compare, 5) + image1_scale2_compare_rgb = image1_scale2_compare.convert("RGB") + assert_image_similar(image1_scale2, image1_scale2_compare_rgb, 5) # Non-zero bounding box with Image.open(FILE2) as image2_scale2: + assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile) image2_scale2.load(scale=2) with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: - image2_scale2_compare = image2_scale2_compare.convert("RGB") - image2_scale2_compare.load() - assert_image_similar(image2_scale2, image2_scale2_compare, 10) + image2_scale2_compare_rgb = image2_scale2_compare.convert("RGB") + assert_image_similar(image2_scale2, image2_scale2_compare_rgb, 10) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) +@pytest.mark.parametrize( + "filename", (FILE1, FILE2, "Tests/images/eps/illu10_preview.eps") +) def test_resize(filename: str) -> None: with Image.open(filename) as im: new_size = (100, 100) - im = im.resize(new_size) - assert im.size == new_size + im_resized = im.resize(new_size) + assert im_resized.size == new_size @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @@ -329,46 +364,6 @@ def test_read_binary_preview() -> None: pass -def test_readline_psfile(tmp_path: Path) -> None: - # 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: EpsImagePlugin.PSFile, ending: str) -> None: - ending = f"Failure with line ending: {''.join(str(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: str, ending: str) -> None: - f = io.BytesIO(test_string.encode("latin-1")) - with pytest.warns(DeprecationWarning): - t = EpsImagePlugin.PSFile(f) - _test_readline(t, ending) - - def _test_readline_file_psfile(test_string: str, ending: str) -> None: - 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: - with pytest.warns(DeprecationWarning): - t = EpsImagePlugin.PSFile(r) - _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) - - -def test_psfile_deprecation() -> None: - with pytest.warns(DeprecationWarning): - EpsImagePlugin.PSFile(None) - - @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize( "line_ending", @@ -386,10 +381,10 @@ def test_readline(prefix: bytes, line_ending: bytes) -> None: @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", + "Tests/images/eps/illu10_no_preview.eps", + "Tests/images/eps/illu10_preview.eps", + "Tests/images/eps/illuCS6_no_preview.eps", + "Tests/images/eps/illuCS6_preview.eps", ), ) def test_open_eps(filename: str) -> None: @@ -401,7 +396,7 @@ def test_open_eps(filename: str) -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_emptyline() -> None: # Test file includes an empty line in the header data - emptyline_file = "Tests/images/zero_bb_emptyline.eps" + emptyline_file = "Tests/images/eps/zero_bb_emptyline.eps" with Image.open(emptyline_file) as image: image.load() @@ -410,10 +405,10 @@ def test_emptyline() -> None: assert image.format == "EPS" -@pytest.mark.timeout(timeout=5) +@timeout_unless_slower_valgrind(5) @pytest.mark.parametrize( "test_file", - ["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], + ["Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], ) def test_timeout(test_file: str) -> None: with open(test_file, "rb") as f: @@ -425,20 +420,21 @@ def test_timeout(test_file: str) -> None: def test_bounding_box_in_trailer() -> None: # Check bounding boxes are parsed in the same way # when specified in the header and the trailer - with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open( - FILE1 - ) as header_image: + with ( + Image.open("Tests/images/eps/zero_bb_trailer.eps") as trailer_image, + Image.open(FILE1) as header_image, + ): assert trailer_image.size == header_image.size def test_eof_before_bounding_box() -> None: with pytest.raises(OSError): - with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"): + with Image.open("Tests/images/eps/zero_bb_eof_before_boundingbox.eps"): pass def test_invalid_data_after_eof() -> None: - with open("Tests/images/illuCS6_preview.eps", "rb") as f: + with open("Tests/images/eps/illuCS6_preview.eps", "rb") as f: img_bytes = io.BytesIO(f.read() + b"\r\n%" + (b" " * 255)) with Image.open(img_bytes) as img: diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index f86fb8d0925..13c6a43239b 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,12 +1,18 @@ from __future__ import annotations +import io import warnings import pytest from PIL import FliImagePlugin, Image, ImageFile -from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + is_pypy, + timeout_unless_slower_valgrind, +) # created as an export of a palette image from Gimp2.6 # save as...-> hopper.fli, default options. @@ -21,6 +27,8 @@ def test_sanity() -> None: with Image.open(static_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) + im.load() assert im.mode == "P" assert im.size == (128, 128) @@ -28,6 +36,8 @@ def test_sanity() -> None: assert not im.is_animated with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) + assert im.mode == "P" assert im.size == (320, 200) assert im.format == "FLI" @@ -35,36 +45,37 @@ def test_sanity() -> None: assert im.is_animated -def test_prefix_chunk() -> None: - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - with Image.open(animated_test_file_with_prefix_chunk) as im: - assert im.mode == "P" - assert im.size == (320, 200) - assert im.format == "FLI" - assert im.info["duration"] == 171 - assert im.is_animated +def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + with Image.open(animated_test_file_with_prefix_chunk) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) + assert im.mode == "P" + assert im.size == (320, 200) + assert im.format == "FLI" + assert im.info["duration"] == 171 + assert im.is_animated - palette = im.getpalette() - assert palette[3:6] == [255, 255, 255] - assert palette[381:384] == [204, 204, 12] - assert palette[765:] == [252, 0, 0] - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + palette = im.getpalette() + assert palette is not None + assert palette[3:6] == [255, 255, 255] + assert palette[381:384] == [204, 204, 12] + assert palette[765:] == [252, 0, 0] @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(static_test_file) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(static_test_file) im.load() im.close() @@ -81,6 +92,8 @@ def test_seek_after_close() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(static_test_file) as im: im.load() @@ -110,16 +123,19 @@ def test_palette_chunk_second() -> None: def test_n_frames() -> None: with Image.open(static_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) assert im.n_frames == 1 assert not im.is_animated with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) assert im.n_frames == 384 assert im.is_animated def test_eoferror() -> None: with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -131,6 +147,15 @@ def test_eoferror() -> None: im.seek(n_frames - 1) +def test_missing_frame_size() -> None: + with open(animated_test_file, "rb") as fp: + data = fp.read() + data = data[:6188] + with Image.open(io.BytesIO(data)) as im: + with pytest.raises(EOFError, match="missing frame size"): + im.seek(1) + + def test_seek_tell() -> None: with Image.open(animated_test_file) as im: layer_number = im.tell() @@ -155,10 +180,14 @@ def test_seek_tell() -> None: def test_seek() -> None: with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) im.seek(50) assert_image_equal_tofile(im, "Tests/images/a_fli.png") + with pytest.raises(ValueError, match="cannot seek to frame 52"): + im._seek(52) + @pytest.mark.parametrize( "test_file", @@ -167,7 +196,7 @@ def test_seek() -> None: "Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli", ], ) -@pytest.mark.timeout(timeout=3) +@timeout_unless_slower_valgrind(3) def test_timeouts(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index e32f30a0123..8d8064692e5 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -22,10 +22,11 @@ def test_sanity() -> None: def test_close() -> None: with Image.open("Tests/images/input_bw_one_band.fpx") as im: - pass + assert isinstance(im, FpxImagePlugin.FpxImageFile) assert im.ole.fp.closed im = Image.open("Tests/images/input_bw_one_band.fpx") + assert isinstance(im, FpxImagePlugin.FpxImageFile) im.close() assert im.ole.fp.closed diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index 0c544245a9f..fdd7b375762 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -1,5 +1,8 @@ from __future__ import annotations +import io +import struct + import pytest from PIL import FtexImagePlugin, Image @@ -23,3 +26,15 @@ def test_invalid_file() -> None: with pytest.raises(SyntaxError): FtexImagePlugin.FtexImageFile(invalid_file) + + +def test_invalid_texture() -> None: + with open("Tests/images/ftex_dxt1.ftc", "rb") as fp: + data = fp.read() + + # Change texture compression format + data = data[:24] + struct.pack(" None: def test_load() -> None: with Image.open("Tests/images/gbr.gbr") as im: - assert im.load()[0, 0] == (0, 0, 0, 0) + px = im.load() + assert px is not None + assert px[0, 0] == (0, 0, 0, 0) # Test again now that it has already been loaded once - assert im.load()[0, 0] == (0, 0, 0, 0) + px = im.load() + assert px is not None + assert px[0, 0] == (0, 0, 0, 0) def test_multiple_load_operations() -> None: @@ -27,8 +33,49 @@ def test_multiple_load_operations() -> None: assert_image_equal_tofile(im, "Tests/images/gbr.png") +def create_gbr_image(info: dict[str, int] = {}, magic_number: bytes = b"") -> BytesIO: + return BytesIO( + b"".join( + _binary.o32be(i) + for i in [ + info.get("header_size", 20), + info.get("version", 1), + info.get("width", 1), + info.get("height", 1), + info.get("color_depth", 1), + ] + ) + + magic_number + ) + + def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" + for f in [ + create_gbr_image({"header_size": 0}), + create_gbr_image({"width": 0}), + create_gbr_image({"height": 0}), + ]: + with pytest.raises(SyntaxError, match="not a GIMP brush"): + GbrImagePlugin.GbrImageFile(f) - with pytest.raises(SyntaxError): + invalid_file = "Tests/images/flower.jpg" + with pytest.raises(SyntaxError, match="Unsupported GIMP brush version"): GbrImagePlugin.GbrImageFile(invalid_file) + + +def test_unsupported_gimp_brush() -> None: + f = create_gbr_image({"color_depth": 2}) + with pytest.raises(SyntaxError, match="Unsupported GIMP brush color depth: 2"): + GbrImagePlugin.GbrImageFile(f) + + +def test_bad_magic_number() -> None: + f = create_gbr_image({"version": 2}, magic_number=b"badm") + with pytest.raises(SyntaxError, match="not a GIMP brush, bad magic number"): + GbrImagePlugin.GbrImageFile(f) + + +def test_L() -> None: + f = create_gbr_image() + with Image.open(f) as im: + assert im.mode == "L" diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index d512df284e1..8a49fd4fa53 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -1,9 +1,13 @@ from __future__ import annotations +from io import BytesIO + import pytest from PIL import GdImageFile, UnidentifiedImageError +from .helper import assert_image_similar_tofile + TEST_GD_FILE = "Tests/images/hopper.gd" @@ -11,6 +15,15 @@ def test_sanity() -> None: with GdImageFile.open(TEST_GD_FILE) as im: assert im.size == (128, 128) assert im.format == "GD" + assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.jpg", 14) + + +def test_transparency() -> None: + with open(TEST_GD_FILE, "rb") as fp: + data = bytearray(fp.read()) + data[7:11] = b"\x00\x00\x00\x05" + with GdImageFile.open(BytesIO(data)) as im: + assert im.info["transparency"] == 5 def test_bad_mode() -> None: diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index e19c88a47c4..7b504233d56 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,9 +1,10 @@ from __future__ import annotations import warnings +from collections.abc import Generator from io import BytesIO from pathlib import Path -from typing import Generator +from typing import Any import pytest @@ -21,9 +22,6 @@ # sample gif stream TEST_GIF = "Tests/images/hopper.gif" -with open(TEST_GIF, "rb") as f: - data = f.read() - def test_sanity() -> None: with Image.open(TEST_GIF) as im: @@ -36,16 +34,18 @@ def test_sanity() -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(TEST_GIF) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(TEST_GIF) im.load() im.close() @@ -67,6 +67,8 @@ def test_seek_after_close() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(TEST_GIF) as im: im.load() @@ -81,12 +83,12 @@ def test_invalid_file() -> None: def test_l_mode_transparency() -> None: with Image.open("Tests/images/no_palette_with_transparency.gif") as im: assert im.mode == "L" - assert im.load()[0, 0] == 128 + assert im.getpixel((0, 0)) == 128 assert im.info["transparency"] == 255 im.seek(1) assert im.mode == "L" - assert im.load()[0, 0] == 128 + assert im.getpixel((0, 0)) == 128 def test_l_mode_after_rgb() -> None: @@ -98,13 +100,25 @@ def test_l_mode_after_rgb() -> None: assert im.mode == "RGB" +def test_l_mode_transparency_after_rgb() -> None: + with Image.open("Tests/images/no_palette_with_transparency_after_rgb.gif") as im: + expected = im.convert("RGB") + d = ImageDraw.Draw(expected) + d.rectangle([(0, 0), (64, 128)], fill="#000") + + im.seek(1) + assert im.mode == "RGB" + + assert_image_equal(im, expected) + + def test_palette_not_needed_for_second_frame() -> None: 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() -> None: +def test_strategy(monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/iss634.gif") as im: expected_rgb_always = im.convert("RGB") @@ -114,35 +128,36 @@ def test_strategy() -> None: 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) + monkeypatch.setattr( + 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) + 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" + monkeypatch.setattr( + 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) + im.seek(1) + assert im.mode == "P" + assert_image_equal(im.convert("RGB"), expected_different) - # Change to RGB mode when a frame has an individual palette - with Image.open("Tests/images/iss634.gif") as im: - assert im.mode == "P" + # Change to RGB mode when a frame has an individual palette + with Image.open("Tests/images/iss634.gif") as im: + assert im.mode == "P" - im.seek(1) - assert im.mode == "RGB" - finally: - GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST + im.seek(1) + assert im.mode == "RGB" def test_optimize() -> None: @@ -221,11 +236,12 @@ def test_optimize_if_palette_can_be_reduced_by_half() -> None: out = BytesIO() im.save(out, "GIF", optimize=optimize) with Image.open(out) as reloaded: + assert reloaded.palette is not None assert len(reloaded.palette.palette) // 3 == colors def test_full_palette_second_frame(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("P", (1, 256)) full_palette_im = Image.new("P", (1, 256)) @@ -246,7 +262,7 @@ def test_full_palette_second_frame(tmp_path: Path) -> None: def test_roundtrip(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = hopper() im.save(out) with Image.open(out) as reread: @@ -255,7 +271,7 @@ def test_roundtrip(tmp_path: Path) -> None: def test_roundtrip2(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/403 - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open(TEST_GIF) as im: im2 = im.copy() im2.save(out) @@ -265,7 +281,7 @@ def test_roundtrip2(tmp_path: Path) -> None: def test_roundtrip_save_all(tmp_path: Path) -> None: # Single frame image - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = hopper() im.save(out, save_all=True) with Image.open(out) as reread: @@ -273,15 +289,16 @@ def test_roundtrip_save_all(tmp_path: Path) -> None: # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, save_all=True) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 5 def test_roundtrip_save_all_1(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("1", (1, 1)) im2 = Image.new("1", (1, 1), 1) im.save(out, save_all=True, append_images=[im2]) @@ -293,6 +310,14 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None: assert reloaded.getpixel((0, 0)) == 255 +@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0))) +def test_save_zero(size: tuple[int, int]) -> None: + b = BytesIO() + im = Image.new("RGB", size) + with pytest.raises(ValueError, match="cannot write empty image"): + im.save(b, "GIF") + + @pytest.mark.parametrize( "path, mode", ( @@ -304,19 +329,19 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None: def test_loading_multiple_palettes(path: str, mode: str) -> None: with Image.open(path) as im: assert im.mode == "P" + assert im.palette is not None first_frame_colors = im.palette.colors.keys() - original_color = im.convert("RGB").load()[0, 0] + original_color = im.convert("RGB").getpixel((0, 0)) im.seek(1) assert im.mode == mode - if mode == "RGBA": - im = im.convert("RGB") + im_rgb = im.convert("RGB") if mode == "RGBA" else im # Check a color only from the old palette - assert im.load()[0, 0] == original_color + assert im_rgb.getpixel((0, 0)) == original_color # Check a color from the new palette - assert im.load()[24, 24] not in first_frame_colors + assert im_rgb.getpixel((24, 24)) not in first_frame_colors def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: @@ -325,7 +350,7 @@ def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: info = im.info.copy() - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, save_all=True) with Image.open(out) as reread: for header in important_headers: @@ -336,24 +361,24 @@ def test_palette_handling(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/513 with Image.open(TEST_GIF) as im: - im = im.convert("RGB") + im_rgb = im.convert("RGB") - im = im.resize((100, 100), Image.Resampling.LANCZOS) - im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) + im_rgb = im_rgb.resize((100, 100), Image.Resampling.LANCZOS) + im_p = im_rgb.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) - f = str(tmp_path / "temp.gif") - im2.save(f, optimize=True) + f = tmp_path / "temp.gif" + im_p.save(f, optimize=True) with Image.open(f) as reloaded: - assert_image_similar(im, reloaded.convert("RGB"), 10) + assert_image_similar(im_rgb, reloaded.convert("RGB"), 10) def test_palette_434(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/434 def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: - out = str(tmp_path / "temp.gif") - im.copy().save(out, **kwargs) + out = tmp_path / "temp.gif" + im.copy().save(out, "GIF", **kwargs) reloaded = Image.open(out) return reloaded @@ -365,39 +390,41 @@ def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: with roundtrip(im, optimize=True) as reloaded: assert_image_similar(im, reloaded, 1) - im = im.convert("RGB") - # check automatic P conversion - with roundtrip(im) as reloaded: - reloaded = reloaded.convert("RGB") - assert_image_equal(im, reloaded) + im_rgb = im.convert("RGB") + + # check automatic P conversion + with roundtrip(im_rgb) as reloaded: + reloaded = reloaded.convert("RGB") + assert_image_equal(im_rgb, reloaded) @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") def test_save_netpbm_bmp_mode(tmp_path: Path) -> None: with Image.open(TEST_GIF) as img: - img = img.convert("RGB") + img_rgb = img.convert("RGB") - tempfile = str(tmp_path / "temp.gif") - b = BytesIO() - GifImagePlugin._save_netpbm(img, b, tempfile) - with Image.open(tempfile) as reloaded: - assert_image_similar(img, reloaded.convert("RGB"), 0) + tempfile = str(tmp_path / "temp.gif") + b = BytesIO() + GifImagePlugin._save_netpbm(img_rgb, b, tempfile) + with Image.open(tempfile) as reloaded: + assert_image_equal(img_rgb, reloaded.convert("RGB")) @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") def test_save_netpbm_l_mode(tmp_path: Path) -> None: with Image.open(TEST_GIF) as img: - img = img.convert("L") + img_l = img.convert("L") tempfile = str(tmp_path / "temp.gif") b = BytesIO() - GifImagePlugin._save_netpbm(img, b, tempfile) + GifImagePlugin._save_netpbm(img_l, b, tempfile) with Image.open(tempfile) as reloaded: - assert_image_similar(img, reloaded.convert("L"), 0) + assert_image_equal(img_l, reloaded.convert("L")) def test_seek() -> None: with Image.open("Tests/images/dispose_none.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) frame_count = 0 try: while True: @@ -406,6 +433,10 @@ def test_seek() -> None: except EOFError: assert frame_count == 5 + img.seek(0) + with pytest.raises(ValueError, match="cannot seek to frame 2"): + img._seek(2) + def test_seek_info() -> None: with Image.open("Tests/images/iss634.gif") as im: @@ -438,10 +469,12 @@ def test_seek_rewind() -> None: def test_n_frames(path: str, n_frames: int) -> None: # Test is_animated before n_frames with Image.open(path) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.is_animated == (n_frames != 1) # Test is_animated after n_frames with Image.open(path) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) @@ -451,6 +484,7 @@ def test_no_change() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: im.seek(1) expected = im.copy() + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.n_frames == 5 assert_image_equal(im, expected) @@ -458,17 +492,20 @@ def test_no_change() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: im.seek(3) expected = im.copy() + assert isinstance(im, GifImagePlugin.GifImageFile) 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 isinstance(im, GifImagePlugin.GifImageFile) assert not im.is_animated assert_image_equal(im, expected) def test_eoferror() -> None: with Image.open(TEST_GIF) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -482,12 +519,12 @@ def test_eoferror() -> None: def test_first_frame_transparency() -> None: with Image.open("Tests/images/first_frame_transparency.gif") as im: - px = im.load() - assert px[0, 0] == im.info["transparency"] + assert im.getpixel((0, 0)) == im.info["transparency"] def test_dispose_none() -> None: with Image.open("Tests/images/dispose_none.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) try: while True: img.seek(img.tell() + 1) @@ -511,6 +548,7 @@ def test_dispose_none_load_end() -> None: def test_dispose_background() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) try: while True: img.seek(img.tell() + 1) @@ -523,7 +561,10 @@ def test_dispose_background_transparency() -> None: with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img: img.seek(2) px = img.load() - assert px[35, 30][3] == 0 + assert px is not None + value = px[35, 30] + assert isinstance(value, tuple) + assert value[3] == 0 @pytest.mark.parametrize( @@ -550,21 +591,20 @@ def test_dispose_background_transparency() -> None: def test_transparent_dispose( loading_strategy: GifImagePlugin.LoadingStrategy, expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]], + monkeypatch: pytest.MonkeyPatch, ) -> None: - 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 + monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy) + with Image.open("Tests/images/transparent_dispose.gif") as img: + for frame in range(3): + img.seek(frame) + for x in range(3): + color = img.getpixel((x, 0)) + assert color == expected_colors[frame][x] def test_dispose_previous() -> None: with Image.open("Tests/images/dispose_prev.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) try: while True: img.seek(img.tell() + 1) @@ -593,15 +633,16 @@ def test_previous_frame_loaded() -> None: def test_save_dispose(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = 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): + for method in range(4): im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method) with Image.open(out) as img: + assert isinstance(img, GifImagePlugin.GifImageFile) for _ in range(2): img.seek(img.tell() + 1) assert img.disposal_method == method @@ -615,13 +656,14 @@ def test_save_dispose(tmp_path: Path) -> None: ) with Image.open(out) as img: + assert isinstance(img, GifImagePlugin.GifImageFile) for i in range(2): img.seek(img.tell() + 1) assert img.disposal_method == i + 1 def test_dispose2_palette(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" # Four colors: white, gray, black, red circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] @@ -655,7 +697,7 @@ def test_dispose2_palette(tmp_path: Path) -> None: def test_dispose2_diff(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" # 4 frames: red/blue, red/red, blue/blue, red/blue circles = [ @@ -697,7 +739,7 @@ def test_dispose2_diff(tmp_path: Path) -> None: def test_dispose2_background(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_list = [] @@ -723,7 +765,7 @@ def test_dispose2_background(tmp_path: Path) -> None: def test_dispose2_background_frame(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_list = [Image.new("RGBA", (1, 20))] @@ -737,11 +779,12 @@ def test_dispose2_background_frame(tmp_path: Path) -> None: im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) with Image.open(out) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.n_frames == 3 def test_dispose2_previous_frame(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("P", (100, 100)) im.info["transparency"] = 0 @@ -759,8 +802,23 @@ def test_dispose2_previous_frame(tmp_path: Path) -> None: assert im.getpixel((0, 0)) == (0, 0, 0, 255) +def test_dispose2_without_transparency(tmp_path: Path) -> None: + out = tmp_path / "temp.gif" + + im = Image.new("P", (100, 100)) + + im2 = Image.new("P", (100, 100), (0, 0, 0)) + im2.putpixel((50, 50), (255, 0, 0)) + + im.save(out, save_all=True, append_images=[im2], disposal=2) + + with Image.open(out) as reloaded: + reloaded.seek(1) + assert reloaded.tile[0].extents == (0, 0, 100, 100) + + def test_transparency_in_second_frame(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open("Tests/images/different_transparency.gif") as im: assert im.info["transparency"] == 0 @@ -790,7 +848,7 @@ def test_no_transparency_in_second_frame() -> None: def test_remapped_transparency(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("P", (1, 2)) im2 = im.copy() @@ -808,7 +866,7 @@ def test_remapped_transparency(tmp_path: Path) -> None: def test_duration(tmp_path: Path) -> None: duration = 1000 - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") # Check that the argument has priority over the info settings @@ -822,7 +880,7 @@ def test_duration(tmp_path: Path) -> None: def test_multiple_duration(tmp_path: Path) -> None: duration_list = [1000, 2000, 3000] - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_list = [ Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#111"), @@ -857,7 +915,7 @@ def test_multiple_duration(tmp_path: Path) -> None: def test_roundtrip_info_duration(tmp_path: Path) -> None: duration_list = [100, 500, 500] - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open("Tests/images/transparent_dispose.gif") as im: assert [ frame.info["duration"] for frame in ImageSequence.Iterator(im) @@ -872,7 +930,7 @@ def test_roundtrip_info_duration(tmp_path: Path) -> None: def test_roundtrip_info_duration_combined(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open("Tests/images/duplicate_frame.gif") as im: assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [ 1000, @@ -890,7 +948,7 @@ def test_roundtrip_info_duration_combined(tmp_path: Path) -> None: def test_identical_frames(tmp_path: Path) -> None: duration_list = [1000, 1500, 2000, 4000] - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_list = [ Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"), @@ -903,6 +961,8 @@ def test_identical_frames(tmp_path: Path) -> None: out, save_all=True, append_images=im_list[1:], duration=duration_list ) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) + # Assert that the first three frames were combined assert reread.n_frames == 2 @@ -923,7 +983,7 @@ def test_identical_frames(tmp_path: Path) -> None: def test_identical_frames_to_single_frame( duration: int | list[int], tmp_path: Path ) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_list = [ Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"), @@ -932,6 +992,8 @@ def test_identical_frames_to_single_frame( im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) + # Assert that all frames were combined assert reread.n_frames == 1 @@ -940,7 +1002,7 @@ def test_identical_frames_to_single_frame( def test_loop_none(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") im.save(out, loop=None) with Image.open(out) as reread: @@ -950,7 +1012,7 @@ def test_loop_none(tmp_path: Path) -> None: def test_number_of_loops(tmp_path: Path) -> None: number_of_loops = 2 - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") im.save(out, loop=number_of_loops) with Image.open(out) as reread: @@ -966,7 +1028,7 @@ def test_number_of_loops(tmp_path: Path) -> None: def test_background(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") im.info["background"] = 1 im.save(out) @@ -975,42 +1037,42 @@ def test_background(tmp_path: Path) -> None: def test_webp_background(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" # Test opaque WebP background - if features.check("webp") and features.check("webp_anim"): + if features.check("webp"): with Image.open("Tests/images/hopper.webp") as im: assert im.info["background"] == (255, 255, 255, 255) im.save(out) # Test non-opaque WebP background - im = Image.new("L", (100, 100), "#000") - im.info["background"] = (0, 0, 0, 0) - im.save(out) + im2 = Image.new("L", (100, 100), "#000") + im2.info["background"] = (0, 0, 0, 0) + im2.save(out) def test_comment(tmp_path: Path) -> None: 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) + out = tmp_path / "temp.gif" + im2 = Image.new("L", (100, 100), "#000") + im2.info["comment"] = b"Test comment text" + im2.save(out) with Image.open(out) as reread: - assert reread.info["comment"] == im.info["comment"] + assert reread.info["comment"] == im2.info["comment"] - im.info["comment"] = "Test comment text" - im.save(out) + im2.info["comment"] = "Test comment text" + im2.save(out) with Image.open(out) as reread: - assert reread.info["comment"] == im.info["comment"].encode() + assert reread.info["comment"] == im2.info["comment"].encode() # Test that GIF89a is used for comments assert reread.info["version"] == b"GIF89a" def test_comment_over_255(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") comment = b"Test comment text" while len(comment) < 256: @@ -1036,7 +1098,7 @@ def test_read_multiple_comment_blocks() -> None: def test_empty_string_comment(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open("Tests/images/chi.gif") as im: assert "comment" in im.info @@ -1070,7 +1132,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None: assert "comment" not in im.info # Test that a saved image keeps the comment - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open("Tests/images/dispose_prev.gif") as im: im.save(out, save_all=True, comment="Test") @@ -1080,7 +1142,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None: def test_version(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" def assert_version_after_save(im: Image.Image, version: bytes) -> None: im.save(out) @@ -1110,7 +1172,7 @@ def assert_version_after_save(im: Image.Image, version: bytes) -> None: def test_append_images(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" # Test appending single frame images im = Image.new("RGB", (100, 100), "#f00") @@ -1118,6 +1180,14 @@ def test_append_images(tmp_path: Path) -> None: im.copy().save(out, save_all=True, append_images=ims) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) + assert reread.n_frames == 3 + + # Test append_images without save_all + im.copy().save(out, append_images=ims) + + with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 3 # Tests appending using a generator @@ -1127,6 +1197,7 @@ def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: im.save(out, save_all=True, append_images=im_generator(ims)) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 3 # Tests appending single and multiple frame images @@ -1135,11 +1206,12 @@ def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: im.save(out, save_all=True, append_images=[im2]) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 10 def test_append_different_size_image(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("RGB", (100, 100)) bigger_im = Image.new("RGB", (200, 200), "#f00") @@ -1166,7 +1238,7 @@ def test_transparent_optimize(tmp_path: Path) -> None: im.frombytes(data) im.putpalette(palette) - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, transparency=im.getpixel((252, 0))) with Image.open(out) as reloaded: @@ -1174,14 +1246,16 @@ def test_transparent_optimize(tmp_path: Path) -> None: def test_removed_transparency(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("RGB", (256, 1)) for x in range(256): im.putpixel((x, 0), (x, 0, 0)) im.info["transparency"] = (255, 255, 255) - with pytest.warns(UserWarning): + with pytest.warns( + UserWarning, match="Couldn't allocate palette entry for transparency" + ): im.save(out) with Image.open(out) as reloaded: @@ -1189,7 +1263,7 @@ def test_removed_transparency(tmp_path: Path) -> None: def test_rgb_transparency(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" # Single frame im = Image.new("RGB", (1, 1)) @@ -1203,7 +1277,7 @@ def test_rgb_transparency(tmp_path: Path) -> None: im = Image.new("RGB", (1, 1)) im.info["transparency"] = b"" ims = [Image.new("RGB", (1, 1))] - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="should be converted to RGBA images"): im.save(out, save_all=True, append_images=ims) with Image.open(out) as reloaded: @@ -1211,7 +1285,7 @@ def test_rgb_transparency(tmp_path: Path) -> None: def test_rgba_transparency(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = hopper("P") im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)]) @@ -1221,25 +1295,26 @@ def test_rgba_transparency(tmp_path: Path) -> None: assert_image_equal(hopper("P").convert("RGB"), reloaded) -def test_background_outside_palettte(tmp_path: Path) -> None: +def test_background_outside_palettte() -> None: with Image.open("Tests/images/background_outside_palette.gif") as im: im.seek(1) assert im.info["background"] == 255 def test_bbox(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = 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 isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 2 def test_bbox_alpha(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("RGBA", (1, 2), (255, 0, 0, 255)) im.putpixel((0, 1), (255, 0, 0, 0)) @@ -1247,6 +1322,7 @@ def test_bbox_alpha(tmp_path: Path) -> None: im.save(out, save_all=True, append_images=[im2]) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 2 @@ -1258,7 +1334,7 @@ def test_palette_save_L(tmp_path: Path) -> None: palette = im.getpalette() assert palette is not None - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_l.save(out, palette=bytes(palette)) with Image.open(out) as reloaded: @@ -1269,7 +1345,7 @@ def test_palette_save_P(tmp_path: Path) -> None: im = Image.new("P", (1, 2)) im.putpixel((0, 1), 1) - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, palette=bytes((1, 2, 3, 4, 5, 6))) with Image.open(out) as reloaded: @@ -1285,7 +1361,7 @@ def test_palette_save_duplicate_entries(tmp_path: Path) -> None: im.putpalette((0, 0, 0, 0, 0, 0)) - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1]) with Image.open(out) as reloaded: @@ -1300,14 +1376,17 @@ def test_palette_save_all_P(tmp_path: Path) -> None: frame.putpalette(color) frames.append(frame) - out = str(tmp_path / "temp.gif") + out = 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 isinstance(im, GifImagePlugin.GifImageFile) assert_image_equal(im.convert("RGB"), frames[0].convert("RGB")) + assert im.palette is not None + assert im.global_palette is not None assert im.palette.palette == im.global_palette.palette im.seek(1) @@ -1322,7 +1401,7 @@ def test_palette_save_ImagePalette(tmp_path: Path) -> None: im = hopper("P") palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3) - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, palette=palette) with Image.open(out) as reloaded: @@ -1335,51 +1414,71 @@ def test_save_I(tmp_path: Path) -> None: im = hopper("I") - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out) with Image.open(out) as reloaded: assert_image_equal(reloaded.convert("L"), im.convert("L")) -def test_getdata() -> None: +def test_getdata(monkeypatch: pytest.MonkeyPatch) -> None: # 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 = Image.linear_gradient(mode="L").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) + monkeypatch.setattr(GifImagePlugin, "_FORCE_OPTIMIZE", True) + + h = GifImagePlugin.getheader(im, passed_palette) + d = GifImagePlugin.getdata(im) - import pickle + 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) + # 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 + assert h == h_target + assert d == d_target def test_lzw_bits() -> None: # 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 + args = im.tile[0][3] + assert isinstance(args, tuple) + assert args[0] == 11 # LZW bits # codec error prepatch im.load() -def test_extents() -> None: - with Image.open("Tests/images/test_extents.gif") as im: +@pytest.mark.parametrize( + "test_file, loading_strategy", + ( + ("test_extents.gif", GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST), + ( + "test_extents.gif", + GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY, + ), + ( + "test_extents_transparency.gif", + GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST, + ), + ), +) +def test_extents( + test_file: str, + loading_strategy: GifImagePlugin.LoadingStrategy, + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy) + with Image.open("Tests/images/" + test_file) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.size == (100, 100) # Check that n_frames does not change the size @@ -1389,6 +1488,9 @@ def test_extents() -> None: im.seek(1) assert im.size == (150, 150) + im.load() + assert im.im.size == (150, 150) + def test_missing_background() -> None: # The Global Color Table Flag isn't set, so there is no background color index, @@ -1399,10 +1501,34 @@ def test_missing_background() -> None: def test_saving_rgba(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = 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 + px = reloaded_rgba.load() + assert px is not None + value = px[0, 0] + assert isinstance(value, tuple) + assert value[3] == 0 + + +@pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False})) +def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None: + out = tmp_path / "temp.gif" + + im1 = Image.new("P", (100, 100)) + d = ImageDraw.Draw(im1) + d.ellipse([(40, 40), (60, 60)], fill=1) + data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254 + im1.putpalette(data, "RGBA") + + im2 = Image.new("P", (100, 100)) + im2.putpalette(data, "RGBA") + + im1.save(out, save_all=True, append_images=[im2], **params) + + with Image.open(out) as reloaded: + assert isinstance(reloaded, GifImagePlugin.GifImageFile) + assert reloaded.n_frames == 2 diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index e8d5f170506..08862113b15 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -1,5 +1,7 @@ from __future__ import annotations +from io import BytesIO + import pytest from PIL.GimpPaletteFile import GimpPaletteFile @@ -14,17 +16,20 @@ def test_sanity() -> None: GimpPaletteFile(fp) with open("Tests/images/bad_palette_file.gpl", "rb") as fp: - with pytest.raises(SyntaxError): + with pytest.raises(SyntaxError, match="bad palette file"): GimpPaletteFile(fp) with open("Tests/images/bad_palette_entry.gpl", "rb") as fp: - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="bad palette entry"): GimpPaletteFile(fp) -def test_get_palette() -> None: +@pytest.mark.parametrize( + "filename, size", (("custom_gimp_palette.gpl", 8), ("full_gimp_palette.gpl", 256)) +) +def test_get_palette(filename: str, size: int) -> None: # Arrange - with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: + with open("Tests/images/" + filename, "rb") as fp: palette_file = GimpPaletteFile(fp) # Act @@ -32,3 +37,36 @@ def test_get_palette() -> None: # Assert assert mode == "RGB" + assert len(palette) / 3 == size + + +def test_frombytes() -> None: + # Test that __init__ stops reading after 260 lines + with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: + custom_data = fp.read() + custom_data += b"#\n" * 300 + b" 0 0 0 Index 12" + b = BytesIO(custom_data) + palette = GimpPaletteFile(b) + assert len(palette.palette) / 3 == 8 + + # Test that __init__ only reads 256 entries + with open("Tests/images/full_gimp_palette.gpl", "rb") as fp: + full_data = fp.read() + data = full_data.replace(b"#\n", b"") + b" 0 0 0 Index 256" + b = BytesIO(data) + palette = GimpPaletteFile(b) + assert len(palette.palette) / 3 == 256 + + # Test that frombytes() can read beyond that + palette = GimpPaletteFile.frombytes(data) + assert len(palette.palette) / 3 == 257 + + # Test that __init__ raises an error if a comment is too long + data = full_data[:-1] + b"a" * 100 + b = BytesIO(data) + with pytest.raises(SyntaxError, match="bad palette file"): + palette = GimpPaletteFile(b) + + # Test that frombytes() can read the data regardless + palette = GimpPaletteFile.frombytes(data) + assert len(palette.palette) / 3 == 256 diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index aba473d24d0..05925d50202 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -43,7 +43,7 @@ def test_load() -> None: def test_save(tmp_path: Path) -> None: # Arrange im = hopper() - tmpfile = str(tmp_path / "temp.grib") + tmpfile = tmp_path / "temp.grib" # Act / Assert: stub cannot save without an implemented handler with pytest.raises(OSError): @@ -59,8 +59,9 @@ class TestHandler(ImageFile.StubHandler): def open(self, im: Image.Image) -> None: self.opened = True - def load(self, im: Image.Image) -> Image.Image: + def load(self, im: ImageFile.ImageFile) -> Image.Image: self.loaded = True + assert im.fp is not None im.fp.close() return Image.new("RGB", (1, 1)) @@ -79,8 +80,8 @@ def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: im.load() assert handler.is_loaded() - temp_file = str(tmp_path / "temp.grib") + temp_file = tmp_path / "temp.grib" im.save(temp_file) assert handler.saved - GribStubImagePlugin._handler = None + GribStubImagePlugin.register_handler(None) diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 8275bd0d890..e1a56309b90 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -43,7 +43,7 @@ def test_save() -> None: # Arrange with Image.open(TEST_FILE) as im: dummy_fp = BytesIO() - dummy_filename = "dummy.filename" + dummy_filename = "dummy.h5" # Act / Assert: stub cannot save without an implemented handler with pytest.raises(OSError): @@ -61,8 +61,9 @@ class TestHandler(ImageFile.StubHandler): def open(self, im: Image.Image) -> None: self.opened = True - def load(self, im: Image.Image) -> Image.Image: + def load(self, im: ImageFile.ImageFile) -> Image.Image: self.loaded = True + assert im.fp is not None im.fp.close() return Image.new("RGB", (1, 1)) @@ -81,8 +82,8 @@ def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: im.load() assert handler.is_loaded() - temp_file = str(tmp_path / "temp.h5") + temp_file = tmp_path / "temp.h5" im.save(temp_file) assert handler.saved - Hdf5StubImagePlugin._handler = None + Hdf5StubImagePlugin.register_handler(None) diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 488984aef70..b9b81850665 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -21,6 +21,8 @@ def test_sanity() -> None: with Image.open(TEST_FILE) as im: # Assert that there is no unclosed file warning with warnings.catch_warnings(): + warnings.simplefilter("error") + im.load() assert im.mode == "RGBA" @@ -30,14 +32,18 @@ def test_sanity() -> None: def test_load() -> None: with Image.open(TEST_FILE) as im: - assert im.load()[0, 0] == (0, 0, 0, 0) + px = im.load() + assert px is not None + assert px[0, 0] == (0, 0, 0, 0) # Test again now that it has already been loaded once - assert im.load()[0, 0] == (0, 0, 0, 0) + px = im.load() + assert px is not None + assert px[0, 0] == (0, 0, 0, 0) def test_save(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.icns") + temp_file = tmp_path / "temp.icns" with Image.open(TEST_FILE) as im: im.save(temp_file) @@ -54,7 +60,7 @@ def test_save(tmp_path: Path) -> None: def test_save_append_images(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.icns") + temp_file = tmp_path / "temp.icns" provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) with Image.open(TEST_FILE) as im: @@ -63,8 +69,9 @@ def test_save_append_images(tmp_path: Path) -> None: assert_image_similar_tofile(im, temp_file, 1) with Image.open(temp_file) as reread: - reread.size = (16, 16, 2) - reread.load() + assert isinstance(reread, IcnsImagePlugin.IcnsImageFile) + reread.size = (16, 16) + reread.load(2) assert_image_equal(reread, provided_im) @@ -84,17 +91,17 @@ def test_sizes() -> None: # 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: + assert isinstance(im, IcnsImagePlugin.IcnsImageFile) for w, h, r in im.info["sizes"]: - wr = w * r - hr = h * r - im.size = (w, h, r) - im.load() + # Test using load() with scale + im.size = (w, h) + im.load(scale=r) assert im.mode == "RGBA" - assert im.size == (wr, hr) + assert im.size == (w * r, h * r) # Check that we cannot load an incorrect size with pytest.raises(ValueError): - im.size = (1, 1) + im.size = (1, 2) def test_older_icon() -> None: @@ -105,8 +112,9 @@ def test_older_icon() -> None: wr = w * r hr = h * r with Image.open("Tests/images/pillow2.icns") as im2: - im2.size = (w, h, r) - im2.load() + assert isinstance(im2, IcnsImagePlugin.IcnsImageFile) + im2.size = (w, h) + im2.load(r) assert im2.mode == "RGBA" assert im2.size == (wr, hr) @@ -122,8 +130,9 @@ def test_jp2_icon() -> None: wr = w * r hr = h * r with Image.open("Tests/images/pillow3.icns") as im2: - im2.size = (w, h, r) - im2.load() + assert isinstance(im2, IcnsImagePlugin.IcnsImageFile) + im2.size = (w, h) + im2.load(r) assert im2.mode == "RGBA" assert im2.size == (wr, hr) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index fa8c11d5a95..36b608a0a98 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -6,7 +6,7 @@ import pytest -from PIL import IcoImagePlugin, Image, ImageDraw +from PIL import IcoImagePlugin, Image, ImageDraw, ImageFile from .helper import assert_image_equal, assert_image_equal_tofile, hopper @@ -24,7 +24,9 @@ def test_sanity() -> None: def test_load() -> None: with Image.open(TEST_ICO_FILE) as im: - assert im.load()[0, 0] == (1, 1, 9, 255) + px = im.load() + assert px is not None + assert px[0, 0] == (1, 1, 9, 255) def test_mask() -> None: @@ -39,7 +41,7 @@ def test_black_and_white() -> None: def test_palette(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.ico") + temp_file = tmp_path / "temp.ico" im = Image.new("P", (16, 16)) im.save(temp_file) @@ -75,6 +77,7 @@ def test_save_to_bytes() -> None: # The other one output.seek(0) with Image.open(output) as reloaded: + assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) reloaded.size = (32, 32) assert im.mode == reloaded.mode @@ -86,21 +89,23 @@ def test_save_to_bytes() -> None: def test_getpixel(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.ico") + temp_file = tmp_path / "temp.ico" im = hopper() im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)]) with Image.open(temp_file) as reloaded: + assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) reloaded.load() reloaded.size = (32, 32) + assert reloaded.load() is not None assert reloaded.getpixel((0, 0)) == (18, 20, 62) def test_no_duplicates(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.ico") - temp_file2 = str(tmp_path / "temp2.ico") + temp_file = tmp_path / "temp.ico" + temp_file2 = tmp_path / "temp2.ico" im = hopper() sizes = [(32, 32), (64, 64)] @@ -113,8 +118,8 @@ def test_no_duplicates(tmp_path: Path) -> None: def test_different_bit_depths(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.ico") - temp_file2 = str(tmp_path / "temp2.ico") + temp_file = tmp_path / "temp.ico" + temp_file2 = tmp_path / "temp2.ico" im = hopper() im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)]) @@ -130,8 +135,8 @@ def test_different_bit_depths(tmp_path: Path) -> None: 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") + temp_file3 = tmp_path / "temp3.ico" + temp_file4 = tmp_path / "temp4.ico" im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)]) im.save( @@ -165,6 +170,7 @@ def test_save_to_bytes_bmp(mode: str) -> None: # The other one output.seek(0) with Image.open(output) as reloaded: + assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) reloaded.size = (32, 32) assert "RGBA" == reloaded.mode @@ -176,6 +182,7 @@ def test_save_to_bytes_bmp(mode: str) -> None: def test_incorrect_size() -> None: with Image.open(TEST_ICO_FILE) as im: + assert isinstance(im, IcoImagePlugin.IcoImageFile) with pytest.raises(ValueError): im.size = (1, 1) @@ -184,7 +191,7 @@ def test_save_256x256(tmp_path: Path) -> None: """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") + outfile = tmp_path / "temp_saved_hopper_256x256.ico" # Act im.save(outfile) @@ -200,7 +207,7 @@ def test_only_save_relevant_sizes(tmp_path: Path) -> None: """ # Arrange with Image.open("Tests/images/python.ico") as im: # 16x16, 32x32, 48x48 - outfile = str(tmp_path / "temp_saved_python.ico") + outfile = tmp_path / "temp_saved_python.ico" # Act im.save(outfile) @@ -213,10 +220,11 @@ def test_save_append_images(tmp_path: Path) -> None: # 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") + outfile = 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 isinstance(reread, IcoImagePlugin.IcoImageFile) assert_image_equal(reread, hopper("RGBA")) reread.size = (32, 32) @@ -226,14 +234,14 @@ def test_save_append_images(tmp_path: Path) -> None: def test_unexpected_size() -> None: # This image has been manually hexedited to state that it is 16x32 # while the image within is still 16x16 - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Image was not the expected size"): with Image.open("Tests/images/hopper_unexpected.ico") as im: assert im.size == (16, 16) def test_draw_reloaded(tmp_path: Path) -> None: with Image.open(TEST_ICO_FILE) as im: - outfile = str(tmp_path / "temp_saved_hopper_draw.ico") + outfile = tmp_path / "temp_saved_hopper_draw.ico" draw = ImageDraw.Draw(im) draw.line((0, 0) + im.size, "#f00") @@ -241,3 +249,25 @@ def test_draw_reloaded(tmp_path: Path) -> None: with Image.open(outfile) as im: assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico") + + +def test_truncated_mask(monkeypatch: pytest.MonkeyPatch) -> None: + # 1 bpp + with open("Tests/images/hopper_mask.ico", "rb") as fp: + data = fp.read() + + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + data = data[:-3] + + with Image.open(io.BytesIO(data)) as im: + assert im.mode == "1" + + # 32 bpp + output = io.BytesIO() + expected = hopper("RGBA") + expected.save(output, "ico", bitmap_format="bmp") + + data = output.getvalue()[:-1] + + with Image.open(io.BytesIO(data)) as im: + assert im.mode == "RGB" diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 036965bf5d8..55c6b730599 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -23,7 +23,7 @@ def test_sanity() -> None: def test_name_limit(tmp_path: Path) -> None: - out = str(tmp_path / ("name_limit_test" * 7 + ".im")) + out = 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") @@ -31,16 +31,18 @@ def test_name_limit(tmp_path: Path) -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(TEST_IM) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(TEST_IM) im.load() im.close() @@ -48,6 +50,8 @@ def test_closed_file() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(TEST_IM) as im: im.load() @@ -64,12 +68,14 @@ def test_tell() -> None: def test_n_frames() -> None: with Image.open(TEST_IM) as im: + assert isinstance(im, ImImagePlugin.ImImageFile) assert im.n_frames == 1 assert not im.is_animated def test_eoferror() -> None: with Image.open(TEST_IM) as im: + assert isinstance(im, ImImagePlugin.ImImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -83,7 +89,7 @@ def test_eoferror() -> None: @pytest.mark.parametrize("mode", ("RGB", "P", "PA")) def test_roundtrip(mode: str, tmp_path: Path) -> None: - out = str(tmp_path / "temp.im") + out = tmp_path / "temp.im" im = hopper(mode) im.save(out) assert_image_equal_tofile(im, out) @@ -94,7 +100,7 @@ def test_small_palette(tmp_path: Path) -> None: colors = [0, 1, 2] im.putpalette(colors) - out = str(tmp_path / "temp.im") + out = tmp_path / "temp.im" im.save(out) with Image.open(out) as reloaded: @@ -102,7 +108,7 @@ def test_small_palette(tmp_path: Path) -> None: def test_save_unsupported_mode(tmp_path: Path) -> None: - out = str(tmp_path / "temp.im") + out = tmp_path / "temp.im" im = hopper("HSV") with pytest.raises(ValueError): im.save(out) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 88c30d46895..3eb5cde8e50 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -1,32 +1,91 @@ from __future__ import annotations -import sys -from io import BytesIO, StringIO +from io import BytesIO import pytest -from PIL import Image, IptcImagePlugin +from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags -from .helper import assert_image_equal, hopper +from .helper import assert_image_equal TEST_FILE = "Tests/images/iptc.jpg" +def create_iptc_image(info: dict[str, int] = {}) -> BytesIO: + def field(tag: tuple[int, int], value: bytes) -> bytes: + return bytes((0x1C,) + tag + (0, len(value))) + value + + data = field((3, 60), bytes((info.get("layers", 1), info.get("component", 0)))) + data += field((3, 120), bytes((info.get("compression", 1),))) + if "band" in info: + data += field((3, 65), bytes((info["band"] + 1,))) + data += field((3, 20), b"\x01") # width + data += field((3, 30), b"\x01") # height + data += field( + (8, 10), + bytes((info.get("data", 0),)), + ) + + return BytesIO(data) + + def test_open() -> None: expected = Image.new("L", (1, 1)) - f = BytesIO( - b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01" - b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00" - ) + f = create_iptc_image() with Image.open(f) as im: - assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")] + assert im.tile == [("iptc", (0, 0, 1, 1), 25, ("raw", None))] assert_image_equal(im, expected) + with Image.open(f) as im: + assert im.load() is not None + + +def test_field_length() -> None: + f = create_iptc_image() + f.seek(28) + f.write(b"\xff") + with pytest.raises(OSError, match="illegal field length in IPTC/NAA file"): + with Image.open(f): + pass + + +@pytest.mark.parametrize("layers, mode", ((3, "RGB"), (4, "CMYK"))) +def test_layers(layers: int, mode: str) -> None: + for band in range(-1, layers): + info = {"layers": layers, "component": 1, "data": 5} + if band != -1: + info["band"] = band + f = create_iptc_image(info) + with Image.open(f) as im: + assert im.mode == mode + + data = [0] * layers + data[max(band, 0)] = 5 + assert im.getpixel((0, 0)) == tuple(data) + + +def test_unknown_compression() -> None: + f = create_iptc_image({"compression": 2}) + with pytest.raises(OSError, match="Unknown IPTC image compression"): + with Image.open(f): + pass + + +def test_getiptcinfo() -> None: + f = create_iptc_image() + with Image.open(f) as im: + assert IptcImagePlugin.getiptcinfo(im) == { + (3, 60): b"\x01\x00", + (3, 120): b"\x01", + (3, 20): b"\x01", + (3, 30): b"\x01", + } + def test_getiptcinfo_jpg_none() -> None: # Arrange - with hopper() as im: + with Image.open("Tests/images/hopper.jpg") as im: # Act iptc = IptcImagePlugin.getiptcinfo(im) @@ -57,10 +116,8 @@ def test_getiptcinfo_fotostation() -> None: iptc = IptcImagePlugin.getiptcinfo(im) # Assert - for tag in iptc.keys(): - if tag[0] == 240: - return - pytest.fail("FotoStation tag not found") + assert iptc is not None + assert 240 in (tag[0] for tag in iptc.keys()), "FotoStation tag not found" def test_getiptcinfo_zero_padding() -> None: @@ -76,43 +133,28 @@ def test_getiptcinfo_zero_padding() -> None: assert len(iptc) == 3 -def test_getiptcinfo_tiff_none() -> None: - # Arrange - with Image.open("Tests/images/hopper.tif") as im: - # Act - iptc = IptcImagePlugin.getiptcinfo(im) - - # Assert - assert iptc is None +def test_getiptcinfo_tiff() -> None: + expected = {(1, 90): b"\x1b%G", (2, 0): b"\xcf\xc0"} + with Image.open("Tests/images/hopper.Lab.tif") as im: + iptc = IptcImagePlugin.getiptcinfo(im) -def test_i() -> None: - # Arrange - c = b"a" + assert iptc == expected - # Act - with pytest.warns(DeprecationWarning): - ret = IptcImagePlugin.i(c) + # Test with LONG tag type + with Image.open("Tests/images/hopper.Lab.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + im.tag_v2.tagtype[TiffImagePlugin.IPTC_NAA_CHUNK] = TiffTags.LONG + iptc = IptcImagePlugin.getiptcinfo(im) - # Assert - assert ret == 97 + assert iptc == expected -def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: +def test_getiptcinfo_tiff_none() -> None: # Arrange - c = b"abc" - # Temporarily redirect stdout - mystdout = StringIO() - monkeypatch.setattr(sys, "stdout", mystdout) - - # Act - with pytest.warns(DeprecationWarning): - IptcImagePlugin.dump(c) + with Image.open("Tests/images/hopper.tif") as im: + # Act + iptc = IptcImagePlugin.getiptcinfo(im) # Assert - assert mystdout.getvalue() == "61 62 63 \n" - - -def test_pad_deprecation() -> None: - with pytest.warns(DeprecationWarning): - assert IptcImagePlugin.PAD == b"\0\0\0\0" + assert iptc is None diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 68705094bab..5103a767278 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -26,12 +26,12 @@ 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, + timeout_unless_slower_valgrind, ) ElementTree: ModuleType | None @@ -83,14 +83,15 @@ def test_sanity(self) -> None: @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im = Image.new("RGB", size) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="cannot write empty image"): im.save(f) def test_app(self) -> None: # Test APP/COM reader (@PIL135) with Image.open(TEST_FILE) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") assert im.applist[1] == ( "COM", @@ -128,33 +129,29 @@ def test_comment_write(self) -> None: def test_cmyk(self) -> None: # 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" - 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))) + def check(im: ImageFile.ImageFile) -> None: + cmyk = im.getpixel((0, 0)) + assert isinstance(cmyk, tuple) + c, m, y, k = (x / 255.0 for x in cmyk) 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)) - ) + cmyk = im.getpixel((im.size[0] - 1, im.size[1] - 1)) + assert isinstance(cmyk, tuple) + k = cmyk[3] / 255.0 assert k > 0.9 + + with Image.open("Tests/images/pil_sample_cmyk.jpg") as im: + # the source image has red pixels in the upper left corner. + check(im) + # 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 + check(self.roundtrip(im)) def test_rgb(self) -> None: - def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]: + def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, ...]: return tuple(v[0] for v in im.layer) im = hopper() @@ -181,6 +178,10 @@ def test(xdpi: int, ydpi: int | None = None) -> tuple[int, int] | None: assert test(100, 200) == (100, 200) assert test(0) is None # square pixels + def test_dpi_jfif_cm(self) -> None: + with Image.open("Tests/images/jfif_unit_cm.jpg") as im: + assert im.info["dpi"] == (2.54, 5.08) + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) @@ -190,7 +191,7 @@ def test_icc(self, tmp_path: Path) -> None: icc_profile = im1.info["icc_profile"] assert len(icc_profile) == 3144 # Roundtrip via physical file. - f = str(tmp_path / "temp.jpg") + f = 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 @@ -234,7 +235,7 @@ def test_large_icc_meta(self, tmp_path: Path) -> None: # Sometimes the meta data on the icc_profile block is bigger than # Image.MAXBLOCK or the image size. with Image.open("Tests/images/icc_profile_big.jpg") as im: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" icc_profile = im.info["icc_profile"] # Should not raise OSError for image with icc larger than image size. im.save( @@ -246,11 +247,11 @@ def test_large_icc_meta(self, tmp_path: Path) -> None: ) with Image.open("Tests/images/flower2.jpg") as im: - f = str(tmp_path / "temp2.jpg") + f = tmp_path / "temp2.jpg" im.save(f, progressive=True, quality=94, icc_profile=b" " * 53955) with Image.open("Tests/images/flower2.jpg") as im: - f = str(tmp_path / "temp3.jpg") + f = tmp_path / "temp3.jpg" im.save(f, progressive=True, quality=94, exif=b" " * 43668) def test_optimize(self) -> None: @@ -264,7 +265,7 @@ def test_optimize(self) -> None: def test_optimize_large_buffer(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" # this requires ~ 1.5x Image.MAXBLOCK im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", optimize=True) @@ -277,17 +278,20 @@ def test_progressive(self) -> None: assert not im2.info.get("progressive") assert im3.info.get("progressive") - assert_image_equal(im1, im3) + if features.check_feature("mozjpeg"): + assert_image_similar(im1, im3, 9.39) + else: + assert_image_equal(im1, im3) assert im1_bytes >= im3_bytes def test_progressive_large_buffer(self, tmp_path: Path) -> None: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" # this requires ~ 1.5x Image.MAXBLOCK im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", progressive=True) def test_progressive_large_buffer_highest_quality(self, tmp_path: Path) -> None: - f = str(tmp_path / "temp.jpg") + f = 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) @@ -300,7 +304,7 @@ def test_progressive_cmyk_buffer(self) -> None: def test_large_exif(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im = hopper() im.save(f, "JPEG", quality=90, exif=b"1" * 65533) @@ -309,6 +313,8 @@ def test_large_exif(self, tmp_path: Path) -> None: def test_exif_typeerror(self) -> None: with Image.open("Tests/images/exif_typeerror.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + # Should not raise a TypeError im._getexif() @@ -324,18 +330,22 @@ def test_exif_gps(self, tmp_path: Path) -> None: # Reading with Image.open("Tests/images/exif_gps.jpg") as im: - exif = im._getexif() - assert exif[gps_index] == expected_exif_gps + assert isinstance(im, JpegImagePlugin.JpegImageFile) + exif_data = im._getexif() + assert exif_data is not None + assert exif_data[gps_index] == expected_exif_gps # Writing - f = str(tmp_path / "temp.jpg") + f = 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 + assert isinstance(reloaded, JpegImagePlugin.JpegImageFile) + exif_data = reloaded._getexif() + assert exif_data is not None + assert exif_data[gps_index] == expected_exif_gps def test_empty_exif_gps(self) -> None: with Image.open("Tests/images/empty_gps_ifd.jpg") as im: @@ -349,7 +359,6 @@ def test_empty_exif_gps(self) -> None: assert exif.get_ifd(0x8825) == {} transposed = ImageOps.exif_transpose(im) - assert transposed is not None exif = transposed.getexif() assert exif.get_ifd(0x8825) == {} @@ -363,6 +372,7 @@ def test_exif_equality(self) -> None: exifs = [] for i in range(2): with Image.open("Tests/images/exif-200dpcm.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) exifs.append(im._getexif()) assert exifs[0] == exifs[1] @@ -396,13 +406,17 @@ def test_exif_rollback(self) -> None: } with Image.open("Tests/images/exif_gps.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) exif = im._getexif() + assert exif is not None for tag, value in expected_exif.items(): assert value == exif[tag] def test_exif_gps_typeerror(self) -> None: with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + # Should not raise a TypeError im._getexif() @@ -420,8 +434,12 @@ def test_progressive_compat(self) -> None: im2 = self.roundtrip(hopper(), progressive=1) im3 = self.roundtrip(hopper(), progression=1) # compatibility - assert_image_equal(im1, im2) - assert_image_equal(im1, im3) + if features.check_feature("mozjpeg"): + assert_image_similar(im1, im2, 9.39) + assert_image_similar(im1, im3, 9.39) + else: + 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") @@ -478,7 +496,9 @@ def getsampling( def test_exif(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) info = im._getexif() + assert info is not None assert info[305] == "Adobe Photoshop CS Macintosh" def test_get_child_images(self) -> None: @@ -490,20 +510,21 @@ def test_get_child_images(self) -> None: def test_mp(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert im._getmp() is None def test_quality_keep(self, tmp_path: Path) -> None: # RGB with Image.open("Tests/images/hopper.jpg") as im: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im.save(f, quality="keep") # Grayscale with Image.open("Tests/images/hopper_gray.jpg") as im: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im.save(f, quality="keep") # CMYK with Image.open("Tests/images/pil_sample_cmyk.jpg") as im: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im.save(f, quality="keep") def test_junk_jpeg_header(self) -> None: @@ -520,12 +541,13 @@ def test_ff00_jpeg_header(self) -> None: @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) -> None: + def test_truncated_jpeg_should_read_all_the_data( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: filename = "Tests/images/truncated_jpeg.jpg" - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(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) -> None: @@ -541,18 +563,20 @@ def test_truncated_jpeg_throws_oserror(self) -> None: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_qtables(self, tmp_path: Path) -> None: + def test_qtables(self) -> None: def _n_qtables_helper(n: int, test_file: str) -> None: + b = BytesIO() 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: + im.save(b, "JPEG", qtables=[[n] * 64] * n) + with Image.open(b) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert len(im.quantization) == n reloaded = self.roundtrip(im, qtables="keep") assert im.quantization == reloaded.quantization assert max(reloaded.quantization[0]) <= 255 with Image.open("Tests/images/hopper.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) qtables = im.quantization reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) assert im.quantization == reloaded.quantization @@ -566,9 +590,7 @@ def _n_qtables_helper(n: int, test_file: str) -> None: assert im2.quantization == {0: bounds_qtable} # values from wizard.txt in jpeg9-a src package. - standard_l_qtable = [ - int(s) - for s in """ + 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 @@ -577,14 +599,9 @@ def _n_qtables_helper(n: int, test_file: str) -> None: 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 - ) - ] + """.split(None)] - standard_chrominance_qtable = [ - int(s) - for s in """ + 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 @@ -593,10 +610,25 @@ def _n_qtables_helper(n: int, test_file: str) -> None: 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 - ) - ] + """.split(None)] + + for quality in range(101): + qtable_from_qtable_quality = self.roundtrip( + im, + qtables={0: standard_l_qtable, 1: standard_chrominance_qtable}, + quality=quality, + ).quantization + + qtable_from_quality = self.roundtrip(im, quality=quality).quantization + + if features.check_feature("libjpeg_turbo"): + assert qtable_from_qtable_quality == qtable_from_quality + else: + assert qtable_from_qtable_quality[0] == qtable_from_quality[0] + assert ( + qtable_from_qtable_quality[1][1:] == qtable_from_quality[1][1:] + ) + # list of qtable lists assert_image_similar( im, @@ -652,17 +684,20 @@ def _n_qtables_helper(n: int, test_file: str) -> None: def test_load_16bit_qtables(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert len(im.quantization) == 2 assert len(im.quantization[0]) == 64 assert max(im.quantization[0]) > 255 def test_save_multiple_16bit_qtables(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) im2 = self.roundtrip(im, qtables="keep") assert im.quantization == im2.quantization def test_save_single_16bit_qtable(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) im2 = self.roundtrip(im, qtables={0: im.quantization[0]}) assert len(im2.quantization) == 1 assert im2.quantization[0] == im.quantization[0] @@ -694,17 +729,10 @@ def test_restart_markers(self, blocks: int, rows: int, markers: int) -> None: @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") def test_load_djpeg(self) -> None: with Image.open(TEST_FILE) as img: + assert isinstance(img, JpegImagePlugin.JpegImageFile) 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: Path) -> None: - with Image.open(TEST_FILE) as img: - tempfile = str(tmp_path / "temp.jpg") - JpegImagePlugin._save_cjpeg(img, BytesIO(), 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) -> None: # Arrange tag_ids = {v: k for k, v in ExifTags.TAGS.items()} @@ -715,7 +743,7 @@ def test_no_duplicate_0x1001_tag(self) -> None: def test_MAXBLOCK_scaling(self, tmp_path: Path) -> None: im = self.gen_random_image((512, 512)) - f = str(tmp_path / "temp.jpeg") + f = tmp_path / "temp.jpeg" im.save(f, quality=100, optimize=True) with Image.open(f) as reloaded: @@ -730,10 +758,13 @@ def test_bad_mpo_header(self) -> None: # Act # Shouldn't raise error - fn = "Tests/images/sugarshack_bad_mpo_header.jpg" - with pytest.warns(UserWarning, Image.open, fn) as im: - # Assert - assert im.format == "JPEG" + with pytest.warns(UserWarning, match="malformed MPO file"): + im = Image.open("Tests/images/sugarshack_bad_mpo_header.jpg") + + # Assert + assert im.format == "JPEG" + + im.close() @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) def test_save_correct_modes(self, mode: str) -> None: @@ -751,7 +782,7 @@ def test_save_wrong_modes(self, mode: str) -> None: def test_save_tiff_with_dpi(self, tmp_path: Path) -> None: # Arrange - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/hopper.tif") as im: # Act im.save(outfile, "JPEG", dpi=im.info["dpi"]) @@ -762,7 +793,7 @@ def test_save_tiff_with_dpi(self, tmp_path: Path) -> None: assert im.info["dpi"] == reloaded.info["dpi"] def test_save_dpi_rounding(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.jpg") + outfile = tmp_path / "temp.jpg" with Image.open("Tests/images/hopper.jpg") as im: im.save(outfile, dpi=(72.2, 72.2)) @@ -829,7 +860,7 @@ def test_no_dpi_in_exif(self) -> None: with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: # Act / Assert # "When the image resolution is unknown, 72 [dpi] is designated." - # https://web.archive.org/web/20240227115053/https://exiv2.org/tags.html + # https://exiv2.org/tags.html assert im.info.get("dpi") == (72, 72) def test_invalid_exif(self) -> None: @@ -848,8 +879,10 @@ def test_exif_x_resolution(self, tmp_path: Path) -> None: exif = im.getexif() assert exif[282] == 180 - out = str(tmp_path / "out.jpg") + out = tmp_path / "out.jpg" with warnings.catch_warnings(): + warnings.simplefilter("error") + im.save(out, exif=exif) with Image.open(out) as reloaded: @@ -868,7 +901,10 @@ def test_ifd_offset_exif(self) -> None: # 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" + assert isinstance(im, JpegImagePlugin.JpegImageFile) + exif = im._getexif() + assert exif is not None + assert exif[306] == "2017:03:13 23:03:09" def test_multiple_exif(self) -> None: with Image.open("Tests/images/multiple_exif.jpg") as im: @@ -896,6 +932,7 @@ def test_photoshop(self) -> None: def test_photoshop_malformed_and_multiple(self) -> None: with Image.open("Tests/images/app13-multiple.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) 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"] @@ -921,7 +958,7 @@ def test_icc_after_SOF(self) -> None: def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None: size = 4097 - buffer = BytesIO(b"\xFF" * size) # Many xFF bytes + buffer = BytesIO(b"\xff" * size) # Many xff bytes max_pos = 0 orig_read = buffer.read @@ -991,23 +1028,44 @@ def test_getxmp_padded(self) -> None: else: assert im.getxmp() == {"xmpmeta": None} - @pytest.mark.timeout(timeout=1) - def test_eof(self) -> None: + def test_save_xmp(self, tmp_path: Path) -> None: + f = tmp_path / "temp.jpg" + im = hopper() + im.save(f, xmp=b"XMP test") + with Image.open(f) as reloaded: + assert reloaded.info["xmp"] == b"XMP test" + + # Check that XMP is not saved from image info + reloaded.save(f) + + with Image.open(f) as reloaded: + assert "xmp" not in reloaded.info + + im.save(f, xmp=b"1" * 65504) + with Image.open(f) as reloaded: + assert reloaded.info["xmp"] == b"1" * 65504 + + with pytest.raises(ValueError): + im.save(f, xmp=b"1" * 65505) + + @timeout_unless_slower_valgrind(1) + def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None: # 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: bytes) -> tuple[int, int]: + def decode( + self, buffer: bytes | Image.SupportsArrayInterface + ) -> tuple[int, int]: return 0, 0 Image.register_decoder("INFINITE", InfiniteMockPyDecoder) with Image.open(TEST_FILE) as im: im.tile = [ - ("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), + ImageFile._Tile("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), ] - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) im.load() - ImageFile.LOAD_TRUNCATED_IMAGES = False def test_separate_tables(self) -> None: im = hopper() @@ -1019,13 +1077,22 @@ def test_separate_tables(self) -> None: # SOI, EOI for marker in b"\xff\xd8", b"\xff\xd9": - assert marker in data[1] and marker in data[2] - # DHT, DQT - for marker in b"\xff\xc4", b"\xff\xdb": - assert marker in data[1] and marker not in data[2] + assert marker in data[1] + assert marker in data[2] + + # DQT + markers = [b"\xff\xdb"] + if features.check_feature("libjpeg_turbo"): + # DHT + markers.append(b"\xff\xc4") + for marker in markers: + assert marker in data[1] + assert marker not in data[2] + # SOF0, SOS, APP0 (JFIF header) for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0": - assert marker not in data[1] and marker in data[2] + assert marker not in data[1] + assert marker in data[2] with Image.open(BytesIO(data[0])) as interchange_im: with Image.open(BytesIO(data[1] + data[2])) as combined_im: @@ -1050,14 +1117,15 @@ def test_repr_jpeg_error_returns_none(self) -> None: @skip_unless_feature("jpg") class TestFileCloseW32: def test_fd_leak(self, tmp_path: Path) -> None: - tmpfile = str(tmp_path / "temp.jpg") + tmpfile = tmp_path / "temp.jpg" with Image.open("Tests/images/hopper.jpg") as im: im.save(tmpfile) im = Image.open(tmpfile) + assert im.fp is not None + assert not im.fp.closed fp = im.fp - assert not fp.closed with pytest.raises(OSError): os.remove(tmpfile) im.load() diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index a5cfa7c6c7a..0e60b59f5d2 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -2,6 +2,7 @@ import os import re +from collections.abc import Generator from io import BytesIO from pathlib import Path from typing import Any @@ -29,8 +30,16 @@ pytestmark = skip_unless_feature("jpg_2000") -test_card = Image.open("Tests/images/test-card.png") -test_card.load() + +@pytest.fixture +def card() -> Generator[ImageFile.ImageFile, None, None]: + with Image.open("Tests/images/test-card.png") as im: + im.load() + try: + yield im + finally: + im.close() + # OpenJPEG 2.0.0 outputs this debugging message sometimes; we should # ignore it---it doesn't represent a test failure. @@ -54,6 +63,7 @@ def test_sanity() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: px = im.load() + assert px is not None assert px[0, 0] == (0, 0, 0) assert im.mode == "RGB" assert im.size == (640, 480) @@ -74,76 +84,92 @@ def test_invalid_file() -> None: Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) -def test_bytesio() -> None: +def test_bytesio(card: ImageFile.ImageFile) -> None: with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = BytesIO(f.read()) with Image.open(data) as im: im.load() - assert_image_similar(im, test_card, 1.0e-3) + assert_image_similar(im, 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_lossless(tmp_path: Path) -> None: +def test_lossless(card: ImageFile.ImageFile, tmp_path: Path) -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: im.load() - outfile = str(tmp_path / "temp_test-card.png") + outfile = tmp_path / "temp_test-card.png" im.save(outfile) - assert_image_similar(im, test_card, 1.0e-3) + assert_image_similar(im, card, 1.0e-3) -def test_lossy_tiled() -> None: - assert_image_similar_tofile( - test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0 - ) +def test_lossy_tiled(card: ImageFile.ImageFile) -> None: + assert_image_similar_tofile(card, "Tests/images/test-card-lossy-tiled.jp2", 2.0) -def test_lossless_rt() -> None: - im = roundtrip(test_card) - assert_image_equal(im, test_card) +def test_lossless_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card) + assert_image_equal(im, card) -def test_lossy_rt() -> None: - im = roundtrip(test_card, quality_layers=[20]) - assert_image_similar(im, test_card, 2.0) +def test_lossy_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, quality_layers=[20]) + assert_image_similar(im, card, 2.0) -def test_tiled_rt() -> None: - im = roundtrip(test_card, tile_size=(128, 128)) - assert_image_equal(im, test_card) +def test_tiled_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, tile_size=(128, 128)) + assert_image_equal(im, card) -def test_tiled_offset_rt() -> None: - 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_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) + assert_image_equal(im, card) -def test_tiled_offset_too_small() -> None: +def test_tiled_offset_too_small(card: ImageFile.ImageFile) -> None: with pytest.raises(ValueError): - roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) + roundtrip(card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) + + +def test_irreversible_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, irreversible=True, quality_layers=[20]) + assert_image_similar(im, card, 2.0) + + +def test_prog_qual_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, quality_layers=[60, 40, 20], progression="LRCP") + assert_image_similar(im, card, 2.0) + +def test_prog_res_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, num_resolutions=8, progression="RLCP") + assert_image_equal(im, card) -def test_irreversible_rt() -> None: - im = roundtrip(test_card, irreversible=True, quality_layers=[20]) - assert_image_similar(im, test_card, 2.0) +def test_unknown_progression(tmp_path: Path) -> None: + outfile = tmp_path / "temp.jp2" -def test_prog_qual_rt() -> None: - im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") - assert_image_similar(im, test_card, 2.0) + im = Image.new("1", (1, 1)) + with pytest.raises(ValueError, match="unknown progression"): + im.save(outfile, progression="invalid") -def test_prog_res_rt() -> None: - im = roundtrip(test_card, num_resolutions=8, progression="RLCP") - assert_image_equal(im, test_card) +def test_unknown_cinema_mode(tmp_path: Path) -> None: + outfile = tmp_path / "temp.jp2" + + im = Image.new("1", (1, 1)) + with pytest.raises(ValueError, match="unknown cinema mode"): + im.save(outfile, cinema_mode="invalid") @pytest.mark.parametrize("num_resolutions", range(2, 6)) -def test_default_num_resolutions(num_resolutions: int) -> None: +def test_default_num_resolutions( + card: ImageFile.ImageFile, num_resolutions: int +) -> None: d = 1 << (num_resolutions - 1) - im = test_card.resize((d - 1, d - 1)) + im = card.resize((d - 1, d - 1)) with pytest.raises(OSError): roundtrip(im, num_resolutions=num_resolutions) reloaded = roundtrip(im) @@ -154,7 +180,7 @@ def test_reduce() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: assert callable(im.reduce) - im.reduce = 2 + im.reduce = 2 # type: ignore[assignment, method-assign] assert im.reduce == 2 im.load() @@ -172,14 +198,20 @@ def test_load_dpi() -> None: assert "dpi" not in im.info -def test_restricted_icc_profile() -> None: - 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_restricted_icc_profile(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + # 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" + + +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +def test_unknown_colorspace() -> None: + with Image.open(f"{EXTRA_DIR}/file8.jp2") as im: + im.load() + assert im.mode == "L" def test_header_errors() -> None: @@ -196,31 +228,33 @@ def test_header_errors() -> None: pass -def test_layers_type(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp_layers.jp2") +def test_layers_type(card: ImageFile.ImageFile, tmp_path: Path) -> None: + outfile = tmp_path / "temp_layers.jp2" for quality_layers in [[100, 50, 10], (100, 50, 10), None]: - test_card.save(outfile, quality_layers=quality_layers) + card.save(outfile, quality_layers=quality_layers) for quality_layers_str in ["quality_layers", ("100", "50", "10")]: with pytest.raises(ValueError): - test_card.save(outfile, quality_layers=quality_layers_str) + card.save(outfile, quality_layers=quality_layers_str) -def test_layers() -> None: +def test_layers(card: ImageFile.ImageFile) -> None: out = BytesIO() - test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") + card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") out.seek(0) with Image.open(out) as im: + assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile) im.layers = 1 im.load() - assert_image_similar(im, test_card, 13) + assert_image_similar(im, card, 13) out.seek(0) with Image.open(out) as im: + assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile) im.layers = 3 im.load() - assert_image_similar(im, test_card, 0.4) + assert_image_similar(im, card, 0.4) @pytest.mark.parametrize( @@ -233,27 +267,33 @@ def test_layers() -> None: ("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"), + (None, {"no_jp2": False}, 4, b"jP"), ), ) -def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: +def test_no_jp2( + card: ImageFile.ImageFile, + name: str, + args: dict[str, bool], + offset: int, + data: bytes, +) -> None: out = BytesIO() if name: out.name = name - test_card.save(out, "JPEG2000", **args) + card.save(out, "JPEG2000", **args) out.seek(offset) assert out.read(2) == data -def test_mct() -> None: +def test_mct(card: ImageFile.ImageFile) -> None: # Three component for val in (0, 1): out = BytesIO() - test_card.save(out, "JPEG2000", mct=val, no_jp2=True) + 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) + assert_image_similar(im, card, 1.0e-3) # Single component should have MCT disabled for val in (0, 1): @@ -267,7 +307,7 @@ def test_mct() -> None: def test_sgnd(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.jp2") + outfile = tmp_path / "temp.jp2" im = Image.new("L", (1, 1)) im.save(outfile) @@ -291,6 +331,18 @@ def test_rgba(ext: str) -> None: assert im.mode == "RGBA" +def test_grayscale_four_channels() -> None: + with open("Tests/images/rgb_trns_ycbc.jp2", "rb") as fp: + data = fp.read() + + # Change color space to OPJ_CLRSPC_GRAY + data = data[:76] + b"\x11" + data[77:] + + with Image.open(BytesIO(data)) as im: + im.load() + assert im.mode == "RGBA" + + @pytest.mark.skipif( not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) @@ -301,6 +353,18 @@ def test_cmyk() -> None: assert im.getpixel((0, 0)) == (185, 134, 0, 0) +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +@skip_unless_feature_version("jpg_2000", "2.5.3") +def test_cmyk_save() -> None: + with Image.open(f"{EXTRA_DIR}/issue205.jp2") as jp2: + assert jp2.mode == "CMYK" + + im = roundtrip(jp2) + assert_image_equal(im, jp2) + + @pytest.mark.parametrize("ext", (".j2k", ".jp2")) def test_16bit_monochrome_has_correct_mode(ext: str) -> None: with Image.open("Tests/images/16bit.cropped" + ext) as im: @@ -388,37 +452,55 @@ def test_subsampling_decode(name: str) -> None: def test_pclr() -> None: with Image.open(f"{EXTRA_DIR}/issue104_jpxstream.jp2") as im: assert im.mode == "P" + assert im.palette is not None assert len(im.palette.colors) == 256 assert im.palette.colors[(255, 255, 255)] == 0 + for enumcs in (0, 15, 17): + with open(f"{EXTRA_DIR}/issue104_jpxstream.jp2", "rb") as fp: + data = bytearray(fp.read()) + data[114:115] = bytes([enumcs]) + with Image.open(BytesIO(data)) as im: + assert im.mode == "L" + + with Image.open( + f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2" + ) as im: + assert im.mode == "P" + assert im.palette is not None + assert im.palette.mode == "CMYK" + assert len(im.palette.colors) == 139 + assert im.palette.colors[(0, 0, 0, 0)] == 0 + def test_comment() -> None: - with Image.open("Tests/images/comment.jp2") as im: - assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" + for path in ("Tests/images/9bit.j2k", "Tests/images/comment.jp2"): + with Image.open(path) as im: + assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" # Test an image that is truncated partway through a codestream with open("Tests/images/comment.jp2", "rb") as fp: b = BytesIO(fp.read(130)) - with Image.open(b) as im: - pass + with Image.open(b) as im: + pass -def test_save_comment() -> None: +def test_save_comment(card: ImageFile.ImageFile) -> None: for comment in ("Created by Pillow", b"Created by Pillow"): out = BytesIO() - test_card.save(out, "JPEG2000", comment=comment) + card.save(out, "JPEG2000", comment=comment) with Image.open(out) as im: assert im.info["comment"] == b"Created by Pillow" out = BytesIO() long_comment = b" " * 65531 - test_card.save(out, "JPEG2000", comment=long_comment) + card.save(out, "JPEG2000", comment=long_comment) with Image.open(out) as im: assert im.info["comment"] == long_comment with pytest.raises(ValueError): - test_card.save(out, "JPEG2000", comment=long_comment + b" ") + card.save(out, "JPEG2000", comment=long_comment + b" ") @pytest.mark.parametrize( @@ -441,15 +523,14 @@ def test_crashes(test_file: str) -> None: @skip_unless_feature_version("jpg_2000", "2.4.0") -def test_plt_marker() -> None: +def test_plt_marker(card: ImageFile.ImageFile) -> None: # Search the start of the codesteam for PLT out = BytesIO() - test_card.save(out, "JPEG2000", no_jp2=True, plt=True) + card.save(out, "JPEG2000", no_jp2=True, plt=True) out.seek(0) while True: marker = out.read(2) - if not marker: - pytest.fail("End of stream without PLT") + assert marker, "End of stream without PLT" jp2_boxid = _binary.i16be(marker) if jp2_boxid == 0xFF4F: diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index f491f9875d9..ca3c055f91a 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -11,7 +11,15 @@ import pytest -from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features +from PIL import ( + Image, + ImageFile, + ImageFilter, + ImageOps, + TiffImagePlugin, + TiffTags, + features, +) from PIL.TiffImagePlugin import OSUBFILETYPE, SAMPLEFORMAT, STRIPOFFSETS, SUBIFD from .helper import ( @@ -27,23 +35,19 @@ @skip_unless_feature("libtiff") class LibTiffTestCase: - def _assert_noerr(self, tmp_path: Path, im: TiffImagePlugin.TiffImageFile) -> None: + def _assert_noerr(self, tmp_path: Path, im: ImageFile.ImageFile) -> None: """Helper tests that assert basic sanity about the g4 tiff reading""" # 1 bit assert im.mode == "1" # Does the data actually load im.load() - im.getdata() - try: - assert im._compression == "group4" - except AttributeError: - print("No _compression") - print(dir(im)) + assert isinstance(im, TiffImagePlugin.TiffImageFile) + assert im._compression == "group4" # can we write it back out, in a different form. - out = str(tmp_path / "temp.png") + out = tmp_path / "temp.png" im.save(out) out_bytes = io.BytesIO() @@ -84,7 +88,7 @@ def test_g4_tiff_bytesio(self, tmp_path: Path) -> None: s = io.BytesIO() with open(test_file, "rb") as f: s.write(f.read()) - s.seek(0) + s.seek(0) with Image.open(s) as im: assert im.size == (500, 500) self._assert_noerr(tmp_path, im) @@ -92,11 +96,22 @@ def test_g4_tiff_bytesio(self, tmp_path: Path) -> None: def test_g4_non_disk_file_object(self, tmp_path: Path) -> None: """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) + data = f.read() + + class NonBytesIO(io.RawIOBase): + def read(self, size: int = -1) -> bytes: + nonlocal data + if size == -1: + size = len(data) + result = data[:size] + data = data[size:] + return result + + def readable(self) -> bool: + return True + + r = io.BufferedReader(NonBytesIO()) with Image.open(r) as im: assert im.size == (500, 500) self._assert_noerr(tmp_path, im) @@ -116,7 +131,7 @@ def test_g4_write(self, tmp_path: Path) -> None: """Checking to see that the saved image is the same as what we wrote""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" rot = orig.transpose(Image.Transpose.ROTATE_90) assert rot.size == (500, 500) rot.save(out) @@ -144,8 +159,9 @@ def test_adobe_deflate_tiff(self) -> None: @pytest.mark.parametrize("legacy_api", (False, True)) def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None: """Test metadata writing through libtiff""" - f = str(tmp_path / "temp.tiff") + f = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper_g4.tif") as img: + assert isinstance(img, TiffImagePlugin.TiffImageFile) img.save(f, tiffinfo=img.tag) if legacy_api: @@ -163,6 +179,7 @@ def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None: ] with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) if legacy_api: reloaded = loaded.tag.named() else: @@ -205,11 +222,9 @@ def test_additional_metadata( # Exclude ones that have special meaning # that we're already testing them with Image.open("Tests/images/hopper_g4.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) for tag in im.tag_v2: - try: - del core_items[tag] - except KeyError: - pass + core_items.pop(tag, None) del core_items[320] # colormap is special, tested below # Type codes: @@ -229,9 +244,10 @@ def test_additional_metadata( new_ifd = TiffImagePlugin.ImageFileDirectory_v2() for tag, info in core_items.items(): + assert info.type is not None if info.length == 1: new_ifd[tag] = values[info.type] - if info.length == 0: + elif not info.length: new_ifd[tag] = tuple(values[info.type] for _ in range(3)) else: new_ifd[tag] = tuple(values[info.type] for _ in range(info.length)) @@ -239,24 +255,12 @@ def test_additional_metadata( # Extra samples really doesn't make sense in this application. del new_ifd[338] - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) im.save(out, tiffinfo=new_ifd) - @pytest.mark.parametrize( - "libtiff", - ( - pytest.param( - True, - marks=pytest.mark.skipif( - not getattr(Image.core, "libtiff_support_custom_tags", False), - reason="Custom tags not supported by older libtiff", - ), - ), - False, - ), - ) + @pytest.mark.parametrize("libtiff", (True, False)) def test_custom_metadata( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool ) -> None: @@ -301,14 +305,15 @@ class Tc(NamedTuple): } def check_tags( - tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str] + tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str], ) -> None: im = hopper() - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out, tiffinfo=tiffinfo) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) for tag, value in tiffinfo.items(): reloaded_value = reloaded.tag_v2[tag] if ( @@ -339,35 +344,88 @@ def check_tags( ) def test_osubfiletype(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/g4_orientation_6.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.tag_v2[OSUBFILETYPE] = 1 im.save(outfile) def test_subifd(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/g4_orientation_6.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.tag_v2[SUBIFD] = 10000 # Should not segfault im.save(outfile) + @pytest.mark.parametrize("tagtype", (TiffTags.SIGNED_RATIONAL, TiffTags.IFD)) + def test_tag_type( + self, tagtype: int, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) + + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[37000] = 100 + ifd.tagtype[37000] = tagtype + + out = tmp_path / "temp.tif" + im = Image.new("L", (1, 1)) + im.save(out, tiffinfo=ifd) + + with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) + assert reloaded.tag_v2[37000] == 100 + + def test_inknames_tag( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) + + out = tmp_path / "temp.tif" + hopper("L").save(out, tiffinfo={333: "name\x00"}) + + with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) + assert reloaded.tag_v2[333] in ("name", "name\x00") + + def test_whitepoint_tag( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) + + out = tmp_path / "temp.tif" + hopper().save(out, tiffinfo={318: (0.3127, 0.3289)}) + + with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) + assert reloaded.tag_v2[318] == pytest.approx((0.3127, 0.3289)) + + # Save tag by default + out = tmp_path / "temp2.tif" + with Image.open("Tests/images/rdf.tif") as im: + im.save(out) + + with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) + assert reloaded.tag_v2[318] == pytest.approx((0.3127, 0.3289999)) + def test_xmlpacket_tag( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) with Image.open(out) as reloaded: - if 700 in reloaded.tag_v2: - assert reloaded.tag_v2[700] == b"xmlpacket tag" + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) + assert reloaded.tag_v2[700] == b"xmlpacket tag" def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: # issue #1765 im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) im.save(out, dpi=(72, 72)) with Image.open(out) as reloaded: @@ -375,7 +433,7 @@ def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: def test_g3_compression(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper_g4_500.tif") as i: - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" i.save(out, compression="group3") with Image.open(out) as reread: @@ -392,7 +450,7 @@ def test_little_endian(self, tmp_path: Path) -> None: assert b[0] == ord(b"\xe0") assert b[1] == ord(b"\x01") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" # out = "temp.le.tif" im.save(out) with Image.open(out) as reread: @@ -412,7 +470,7 @@ def test_big_endian(self, tmp_path: Path) -> None: assert b[0] == ord(b"\x01") assert b[1] == ord(b"\xe0") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out) with Image.open(out) as reread: assert reread.info["compression"] == im.info["compression"] @@ -422,12 +480,15 @@ def test_g4_string_info(self, tmp_path: Path) -> None: """Tests String data in info directory""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: - out = str(tmp_path / "temp.tif") + assert isinstance(orig, TiffImagePlugin.TiffImageFile) + + out = tmp_path / "temp.tif" orig.tag[269] = "temp.tif" orig.save(out) with Image.open(out) as reread: + assert isinstance(reread, TiffImagePlugin.TiffImageFile) assert "temp.tif" == reread.tag_v2[269] assert "temp.tif" == reread.tag[269][0] @@ -449,20 +510,20 @@ def test_12bit_rawmode(self, monkeypatch: pytest.MonkeyPatch) -> None: def test_blur(self, tmp_path: Path) -> None: # test case from irc, how to do blur on b/w image # and save to compressed tif. - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" with Image.open("Tests/images/pport_g4.tif") as im: - im = im.convert("L") + im_l = im.convert("L") - im = im.filter(ImageFilter.GaussianBlur(4)) - im.save(out, compression="tiff_adobe_deflate") + im_l = im_l.filter(ImageFilter.GaussianBlur(4)) + im_l.save(out, compression="tiff_adobe_deflate") - assert_image_equal_tofile(im, out) + assert_image_equal_tofile(im_l, out) def test_compressions(self, tmp_path: Path) -> None: # Test various tiff compressions and assert similar image content but reduced # file sizes. im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out) size_raw = os.path.getsize(out) @@ -486,7 +547,7 @@ def test_compressions(self, tmp_path: Path) -> None: def test_tiff_jpeg_compression(self, tmp_path: Path) -> None: im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out, compression="tiff_jpeg") with Image.open(out) as reloaded: @@ -494,7 +555,7 @@ def test_tiff_jpeg_compression(self, tmp_path: Path) -> None: def test_tiff_deflate_compression(self, tmp_path: Path) -> None: im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out, compression="tiff_deflate") with Image.open(out) as reloaded: @@ -502,7 +563,7 @@ def test_tiff_deflate_compression(self, tmp_path: Path) -> None: def test_quality(self, tmp_path: Path) -> None: im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" with pytest.raises(ValueError): im.save(out, compression="tiff_lzw", quality=50) @@ -517,7 +578,7 @@ def test_quality(self, tmp_path: Path) -> None: def test_cmyk_save(self, tmp_path: Path) -> None: im = hopper("CMYK") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out, compression="tiff_adobe_deflate") assert_image_equal_tofile(im, out) @@ -526,26 +587,28 @@ def test_cmyk_save(self, tmp_path: Path) -> None: def test_palette_save( self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) im.save(out) with Image.open(out) as reloaded: # colormap/palette tag + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert len(reloaded.tag_v2[320]) == 768 @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None: im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" with pytest.raises(OSError): im.save(out, compression=compression) def test_fp_leak(self) -> None: - im: Image.Image | None = Image.open("Tests/images/hopper_g4_500.tif") + im: ImageFile.ImageFile | None = Image.open("Tests/images/hopper_g4_500.tif") assert im is not None + assert im.fp is not None fn = im.fp.fileno() os.fstat(fn) @@ -564,6 +627,7 @@ def test_multipage(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/multipage.tiff") as im: # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.seek(0) assert im.size == (10, 10) assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) @@ -583,6 +647,7 @@ def test_multipage_nframes(self, monkeypatch: pytest.MonkeyPatch) -> None: # issue #862 monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/multipage.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) frames = im.n_frames assert frames == 3 for _ in range(frames): @@ -602,6 +667,7 @@ def test_multipage_seek_backwards(self, monkeypatch: pytest.MonkeyPatch) -> None def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/hopper.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert not im.tag.next im.load() assert not im.tag.next @@ -669,7 +735,7 @@ def save_bytesio(compression: str | None = None) -> None: buffer_io.seek(0) with Image.open(buffer_io) as saved_im: - assert_image_similar(pilim, saved_im, 0) + assert_image_equal(pilim, saved_im) save_bytesio() save_bytesio("raw") @@ -678,34 +744,37 @@ def save_bytesio(compression: str | None = None) -> None: def test_save_ycbcr(self, tmp_path: Path) -> None: im = hopper("YCbCr") - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im.save(outfile, compression="jpeg") with Image.open(outfile) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) def test_exif_ifd(self) -> None: out = io.BytesIO() with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2[34665] == 125456 im.save(out, "TIFF") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert 34665 not in reloaded.tag_v2 im.save(out, "TIFF", tiffinfo={34665: 125456}) with Image.open(out) as reloaded: - if Image.core.libtiff_support_custom_tags: - assert reloaded.tag_v2[34665] == 125456 + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) + assert reloaded.tag_v2[34665] == 125456 def test_crashing_metadata( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: # issue 1597 with Image.open("Tests/images/rdf.tif") as im: - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) # this shouldn't crash @@ -716,7 +785,7 @@ def test_page_number_x_0(self, tmp_path: Path) -> None: # 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 = str(tmp_path / "temp.tif") + outfile = 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 @@ -728,7 +797,7 @@ def test_page_number_x_0(self, tmp_path: Path) -> None: def test_fd_duplication(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/1651 - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" with open(tmpfile, "wb") as f: with open("Tests/images/g4-multi.tiff", "rb") as src: f.write(src.read()) @@ -750,19 +819,7 @@ def test_read_icc(self, monkeypatch: pytest.MonkeyPatch) -> None: assert icc_libtiff is not None assert icc == icc_libtiff - @pytest.mark.parametrize( - "libtiff", - ( - pytest.param( - True, - marks=pytest.mark.skipif( - not getattr(Image.core, "libtiff_support_custom_tags", False), - reason="Custom tags not supported by older libtiff", - ), - ), - False, - ), - ) + @pytest.mark.parametrize("libtiff", (True, False)) def test_write_icc( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool ) -> None: @@ -771,13 +828,14 @@ def test_write_icc( with Image.open("Tests/images/hopper.iccprofile.tif") as img: icc_profile = img.info["icc_profile"] - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" img.save(out, icc_profile=icc_profile) with Image.open(out) as reloaded: assert icc_profile == reloaded.info["icc_profile"] def test_multipage_compression(self) -> None: with Image.open("Tests/images/compression.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.seek(0) assert im._compression == "tiff_ccitt" assert im.size == (10, 10) @@ -794,7 +852,7 @@ def test_multipage_compression(self) -> None: def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None: # Arrange - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" # Created with ImageMagick: convert hopper.jpg hopper_jpg.tif # Contains JPEGTables (347) tag @@ -856,7 +914,7 @@ def test_sampleformat_write( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: im = Image.new("F", (1, 1)) - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) im.save(out) @@ -870,8 +928,8 @@ def test_lzma(self, capfd: pytest.CaptureFixture[str]) -> None: assert im.mode == "RGB" assert im.size == (128, 128) assert im.format == "TIFF" - im2 = hopper() - assert_image_similar(im, im2, 5) + with hopper() as im2: + assert_image_similar(im, im2, 5) except OSError: captured = capfd.readouterr() if "LZMA compression support is not configured" in captured.err: @@ -997,10 +1055,19 @@ def test_strip_planar_16bit_RGBa(self) -> None: 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") + def test_separate_planar_extra_samples(self, tmp_path: Path) -> None: + out = tmp_path / "temp.tif" + with Image.open("Tests/images/separate_planar_extra_samples.tiff") as im: + assert im.mode == "L" + + im.save(out) + with Image.open(out) as reloaded: + assert reloaded.mode == "L" + @pytest.mark.parametrize("compression", (None, "jpeg")) def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None: im = hopper() - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" tags = { TiffImagePlugin.TILEWIDTH: 256, @@ -1018,6 +1085,19 @@ def test_old_style_jpeg(self) -> None: 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_old_style_jpeg_orientation(self) -> None: + with open("Tests/images/old-style-jpeg-compression.tif", "rb") as fp: + data = fp.read() + + # Set EXIF Orientation to 2 + data = data[:102] + b"\x02" + data[103:] + + with Image.open(io.BytesIO(data)) as im: + im_transposed = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + assert_image_equal_tofile( + im_transposed, "Tests/images/old-style-jpeg-compression.png" + ) + def test_open_missing_samplesperpixel(self) -> None: with Image.open( "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif" @@ -1048,7 +1128,11 @@ def test_open_missing_samplesperpixel(self) -> None: ], ) def test_wrong_bits_per_sample( - self, file_name: str, mode: str, size: tuple[int, int], tile + self, + file_name: str, + mode: str, + size: tuple[int, int], + tile: list[tuple[str, tuple[int, int, int, int], int, tuple[Any, ...]]], ) -> None: with Image.open("Tests/images/" + file_name) as im: assert im.mode == mode @@ -1067,6 +1151,7 @@ def test_orientation(self) -> None: 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: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert 274 in im.tag_v2 im.load() @@ -1078,9 +1163,30 @@ def test_exif_transpose(self) -> None: 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 = ImageOps.exif_transpose(im) + im_transposed = ImageOps.exif_transpose(im) - assert_image_similar(base_im, im, 0.7) + assert_image_similar(base_im, im_transposed, 0.7) + + @pytest.mark.parametrize( + "test_file", + [ + "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif", + "Tests/images/old-style-jpeg-compression.tif", + ], + ) + def test_buffering(self, test_file: str) -> None: + # load exif first + with open(test_file, "rb", buffering=1048576) as f: + with Image.open(f) as im: + exif = dict(im.getexif()) + + # load image before exif + with open(test_file, "rb", buffering=1048576) as f: + with Image.open(f) as im2: + im2.load() + exif_after_load = dict(im2.getexif()) + + assert exif == exif_after_load @pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") def test_sampleformat_not_corrupted(self) -> None: @@ -1107,16 +1213,14 @@ def test_sampleformat_not_corrupted(self) -> None: def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(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" + with pytest.raises(OSError, match="decoder error -9"): + im.load() @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: im = hopper("RGB").resize((256, 256)) - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out, compression=compression) with Image.open(out) as im: @@ -1125,34 +1229,33 @@ def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: assert len(im.tag_v2[STRIPOFFSETS]) > 1 @pytest.mark.parametrize("argument", (True, False)) - def test_save_single_strip(self, argument: bool, tmp_path: Path) -> None: + def test_save_single_strip( + self, argument: bool, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: im = hopper("RGB").resize((256, 256)) - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" if not argument: - TiffImagePlugin.STRIP_SIZE = 2**18 - try: - arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} - if argument: - arguments["strip_size"] = 2**18 - im.save(out, **arguments) + monkeypatch.setattr(TiffImagePlugin, "STRIP_SIZE", 2**18) + arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} + if argument: + arguments["strip_size"] = 2**18 + im.save(out, "TIFF", **arguments) - with Image.open(out) as im: - assert isinstance(im, TiffImagePlugin.TiffImageFile) - assert len(im.tag_v2[STRIPOFFSETS]) == 1 - finally: - TiffImagePlugin.STRIP_SIZE = 65536 + with Image.open(out) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + assert len(im.tag_v2[STRIPOFFSETS]) == 1 @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: im = Image.new("RGB", (0, 0)) - out = str(tmp_path / "temp.tif") - with pytest.raises(SystemError): + out = tmp_path / "temp.tif" + with pytest.raises(ValueError, match="cannot write empty image"): im.save(out, compression=compression) def test_save_many_compressed(self, tmp_path: Path) -> None: im = hopper() - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" for _ in range(10000): im.save(out, compression="jpeg") diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 617e1e89c72..65ba80c2076 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -32,7 +32,7 @@ def test_g4_hopper_bytesio(self, tmp_path: Path) -> None: s = BytesIO() with open(test_file, "rb") as f: s.write(f.read()) - s.seek(0) + s.seek(0) with Image.open(s) 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 e11e6bb5284..7e236028d18 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -27,6 +27,6 @@ def test_valid_file() -> None: # Assert assert im.format == "MCIDAS" - assert im.mode == "I" + assert im.mode == "I;16B" 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 9a6f13ea366..0706af4c07d 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -22,19 +22,21 @@ def test_sanity() -> None: # 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()]) + im1 = Image.merge("RGBA", [chan.point(lut) for chan in im.split()]) im2 = hopper("RGBA") - assert_image_similar(im, im2, 10) + assert_image_similar(im1, im2, 10) def test_n_frames() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, MicImagePlugin.MicImageFile) assert im.n_frames == 1 def test_is_animated() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, MicImagePlugin.MicImageFile) assert not im.is_animated @@ -55,10 +57,11 @@ def test_seek() -> None: def test_close() -> None: with Image.open(TEST_FILE) as im: - pass + assert isinstance(im, MicImagePlugin.MicImageFile) assert im.ole.fp.closed im = Image.open(TEST_FILE) + assert isinstance(im, MicImagePlugin.MicImageFile) im.close() assert im.ole.fp.closed diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 39b9c60b756..7f35693f504 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -2,11 +2,18 @@ import warnings from io import BytesIO -from typing import Any, cast +from typing import Any import pytest -from PIL import Image, MpoImagePlugin +from PIL import ( + Image, + ImageFile, + JpegImagePlugin, + MpoImagePlugin, + TiffImagePlugin, + _binary, +) from .helper import ( assert_image_equal, @@ -20,34 +27,41 @@ pytestmark = skip_unless_feature("jpg") -def roundtrip(im: Image.Image, **options: Any) -> MpoImagePlugin.MpoImageFile: +def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile: out = BytesIO() im.save(out, "MPO", **options) out.seek(0) - return cast(MpoImagePlugin.MpoImageFile, Image.open(out)) + return Image.open(out) @pytest.mark.parametrize("test_file", test_files) def test_sanity(test_file: str) -> None: - with Image.open(test_file) as im: + def check(im: ImageFile.ImageFile) -> None: im.load() assert im.mode == "RGB" assert im.size == (640, 480) assert im.format == "MPO" + with Image.open(test_file) as im: + check(im) + with MpoImagePlugin.MpoImageFile(test_file) as im: + check(im) + @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(test_files[0]) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(test_files[0]) im.load() im.close() @@ -63,6 +77,8 @@ def test_seek_after_close() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(test_files[0]) as im: im.load() @@ -71,10 +87,11 @@ def test_context_manager() -> None: def test_app(test_file: str) -> None: # Test APP/COM reader (@PIL135) with Image.open(test_file) as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) 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 im.applist[1][1].startswith( + b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00" ) assert len(im.applist) == 2 @@ -85,32 +102,36 @@ def test_exif(test_file: str) -> None: im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) for im in (im_original, im_reloaded): + assert isinstance(im, MpoImagePlugin.MpoImageFile) info = im._getexif() + assert info is not None assert info[272] == "Nintendo 3DS" assert info[296] == 2 assert info[34665] == 188 def test_frame_size() -> None: - # This image has been hexedited to contain a different size - # in the SOF marker of the second frame - with Image.open("Tests/images/sugarshack_frame_size.mpo") as im: - assert im.size == (640, 480) + with Image.open("Tests/images/frame_size.mpo") as im: + assert im.size == (56, 70) + im.load() im.seek(1) - assert im.size == (680, 480) + assert im.size == (349, 434) + im.load() im.seek(0) - assert im.size == (640, 480) + assert im.size == (56, 70) def test_ignore_frame_size() -> None: # 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 isinstance(im, MpoImagePlugin.MpoImageFile) assert im.size == (64, 64) im.seek(1) + assert im.mpinfo is not None assert ( im.mpinfo[0xB002][1]["Attribute"]["MPType"] == "Multi-Frame Image: (Disparity)" @@ -131,6 +152,32 @@ def test_parallax() -> None: assert exif.get_ifd(0x927C)[0xB211] == -3.125 +def test_truncated_makernote() -> None: + def check(ifd: TiffImagePlugin.ImageFileDirectory_v2) -> None: + fp = BytesIO() + ifd.save(fp) + + e = Image.Exif() + e.load(fp.getvalue()) + assert e.get_ifd(37500) == {} + + # Nintendo + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[271] = "Nintendo" + ifd[34665] = {37500: b" "} + check(ifd) + + # Fujifilm + for data in ( + b"FUJIFILM", + b"FUJIFILM" + _binary.o32le(50), + b"FUJIFILM" + _binary.o32le(0), + ): + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[34665] = {37500: data} + check(ifd) + + def test_reload_exif_after_seek() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: exif = im.getexif() @@ -143,7 +190,9 @@ def test_reload_exif_after_seek() -> None: @pytest.mark.parametrize("test_file", test_files) def test_mp(test_file: str) -> None: with Image.open(test_file) as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) mpinfo = im._getmp() + assert mpinfo is not None assert mpinfo[45056] == b"0100" assert mpinfo[45057] == 2 @@ -152,7 +201,9 @@ def test_mp_offset() -> None: # 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: + assert isinstance(im, MpoImagePlugin.MpoImageFile) mpinfo = im._getmp() + assert mpinfo is not None assert mpinfo[45056] == b"0100" assert mpinfo[45057] == 2 @@ -168,7 +219,9 @@ def test_mp_no_data() -> None: @pytest.mark.parametrize("test_file", test_files) def test_mp_attribute(test_file: str) -> None: with Image.open(test_file) as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) mpinfo = im._getmp() + assert mpinfo is not None for frame_number, mpentry in enumerate(mpinfo[0xB002]): mpattr = mpentry["Attribute"] if frame_number: @@ -209,12 +262,14 @@ def test_seek(test_file: str) -> None: def test_n_frames() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) assert im.n_frames == 2 assert im.is_animated def test_eoferror() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -226,6 +281,14 @@ def test_eoferror() -> None: im.seek(n_frames - 1) +def test_adopt_jpeg() -> None: + with Image.open("Tests/images/hopper.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + + with pytest.raises(ValueError): + MpoImagePlugin.MpoImageFile.adopt(im) + + def test_ultra_hdr() -> None: with Image.open("Tests/images/ultrahdr.jpg") as im: assert im.format == "JPEG" @@ -270,16 +333,46 @@ def test_save_all() -> None: 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]) + im_rgb = Image.new("RGB", (1, 1)) + for colors in (("#f00",), ("#f00", "#0f0")): + append_images = [Image.new("RGB", (1, 1), color) for color in colors] + im_reloaded = roundtrip(im_rgb, save_all=True, append_images=append_images) - assert_image_equal(im, im_reloaded) - assert im_reloaded.mpinfo[45056] == b"0100" + assert_image_equal(im_rgb, im_reloaded) + assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile) + assert im_reloaded.mpinfo is not None + assert im_reloaded.mpinfo[45056] == b"0100" - im_reloaded.seek(1) - assert_image_similar(im2, im_reloaded, 1) + for im_expected in append_images: + im_reloaded.seek(im_reloaded.tell() + 1) + assert_image_similar(im_reloaded, im_expected, 1) # Test that a single frame image will not be saved as an MPO - jpg = roundtrip(im, save_all=True) + jpg = roundtrip(im_rgb, save_all=True) assert "mp" not in jpg.info + + +def test_save_xmp() -> None: + im = Image.new("RGB", (1, 1)) + im2 = Image.new("RGB", (1, 1), "#f00") + + def roundtrip_xmp() -> list[Any]: + im_reloaded = roundtrip(im, xmp=b"Default", save_all=True, append_images=[im2]) + xmp = [im_reloaded.info["xmp"]] + im_reloaded.seek(1) + return xmp + [im_reloaded.info["xmp"]] + + # Use the save parameters for all frames by default + assert roundtrip_xmp() == [b"Default", b"Default"] + + # Specify a value for the first frame + im.encoderinfo = {"xmp": b"First frame"} + assert roundtrip_xmp() == [b"First frame", b"Default"] + del im.encoderinfo + + # Specify value for the second frame + im2.encoderinfo = {"xmp": b"Second frame"} + assert roundtrip_xmp() == [b"Default", b"Second frame"] + + # Test that encoderinfo is unchanged + assert im2.encoderinfo == {"xmp": b"Second frame"} diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index b0964aabe12..8c91922bd0b 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -15,7 +15,7 @@ def test_sanity(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.msp") + test_file = tmp_path / "temp.msp" hopper("1").save(test_file) @@ -84,7 +84,7 @@ def test_msp_v2() -> None: def test_cannot_save_wrong_mode(tmp_path: Path) -> None: # Arrange im = hopper() - filename = str(tmp_path / "temp.msp") + filename = tmp_path / "temp.msp" # Act/Assert with pytest.raises(OSError): diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index 194f39b30b8..58208ba99fa 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -14,7 +14,7 @@ def helper_save_as_palm(tmp_path: Path, mode: str) -> None: # Arrange im = hopper(mode) - outfile = str(tmp_path / ("temp_" + mode + ".palm")) + outfile = tmp_path / ("temp_" + mode + ".palm") # Act im.save(outfile) @@ -25,7 +25,7 @@ def helper_save_as_palm(tmp_path: Path, mode: str) -> None: def open_with_magick(magick: list[str], tmp_path: Path, f: str) -> Image.Image: - outfile = str(tmp_path / "temp.png") + outfile = tmp_path / "temp.png" rc = subprocess.call( magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT ) @@ -43,6 +43,11 @@ def roundtrip(tmp_path: Path, mode: str) -> None: im.save(outfile) converted = open_with_magick(magick, tmp_path, outfile) + if mode == "P": + assert converted.mode == "P" + + im = im.convert("RGB") + converted = converted.convert("RGB") assert_image_equal(converted, im) @@ -55,7 +60,6 @@ def test_monochrome(tmp_path: Path) -> None: roundtrip(tmp_path, mode) -@pytest.mark.xfail(reason="Palm P image is wrong") def test_p_mode(tmp_path: Path) -> None: # Arrange mode = "P" diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index 81a316fc14a..15dd7f116db 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -1,10 +1,17 @@ from __future__ import annotations +from io import BytesIO + +import pytest + from PIL import Image +from .helper import assert_image_equal + def test_load_raw() -> None: with Image.open("Tests/images/hopper.pcd") as im: + assert im.size == (768, 512) im.load() # should not segfault. # Note that this image was created with a resized hopper @@ -15,3 +22,18 @@ def test_load_raw() -> None: # target = hopper().resize((768,512)) # assert_image_similar(im, target, 10) + + +@pytest.mark.parametrize("orientation", (1, 3)) +def test_rotated(orientation: int) -> None: + with open("Tests/images/hopper.pcd", "rb") as fp: + data = bytearray(fp.read()) + data[2048 + 1538] = orientation + f = BytesIO(data) + with Image.open(f) as im: + assert im.size == (512, 768) + + with Image.open("Tests/images/hopper.pcd") as expected: + assert_image_equal( + im, expected.rotate(90 if orientation == 1 else 270, expand=True) + ) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index ab9f9663e1b..509d93469e6 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,5 +1,6 @@ from __future__ import annotations +import io from pathlib import Path import pytest @@ -10,7 +11,7 @@ def _roundtrip(tmp_path: Path, im: Image.Image) -> None: - f = str(tmp_path / "temp.pcx") + f = tmp_path / "temp.pcx" im.save(f) with Image.open(f) as im2: assert im2.mode == im.mode @@ -30,12 +31,47 @@ def test_sanity(tmp_path: Path) -> None: _roundtrip(tmp_path, im) # Test an unsupported mode - f = str(tmp_path / "temp.pcx") + f = tmp_path / "temp.pcx" im = hopper("RGBA") with pytest.raises(ValueError): im.save(f) +@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0))) +def test_save_zero(size: tuple[int, int]) -> None: + b = io.BytesIO() + im = Image.new("1", size) + with pytest.raises(ValueError): + im.save(b, "PCX") + + +def test_p_4_planes() -> None: + with Image.open("Tests/images/p_4_planes.pcx") as im: + assert im.getpixel((0, 0)) == 3 + + +def test_bad_image_size() -> None: + with open("Tests/images/pil184.pcx", "rb") as fp: + data = fp.read() + data = data[:4] + b"\xff\xff" + data[6:] + + b = io.BytesIO(data) + with pytest.raises(SyntaxError, match="bad PCX image size"): + with PcxImagePlugin.PcxImageFile(b): + pass + + +def test_unknown_mode() -> None: + with open("Tests/images/pil184.pcx", "rb") as fp: + data = fp.read() + data = data[:3] + b"\xff" + data[4:] + + b = io.BytesIO(data) + with pytest.raises(OSError, match="unknown PCX mode"): + with Image.open(b): + pass + + def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" @@ -76,6 +112,7 @@ def test_pil184() -> None: def test_1px_width(tmp_path: Path) -> None: im = Image.new("L", (1, 256)) px = im.load() + assert px is not None for y in range(256): px[0, y] = y _roundtrip(tmp_path, im) @@ -84,77 +121,84 @@ def test_1px_width(tmp_path: Path) -> None: def test_large_count(tmp_path: Path) -> None: im = Image.new("L", (256, 1)) px = im.load() + assert px is not None for x in range(256): px[x, 0] = x // 67 * 67 _roundtrip(tmp_path, im) -def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None: - _last = ImageFile.MAXBLOCK - ImageFile.MAXBLOCK = size - try: - _roundtrip(tmp_path, im) - finally: - ImageFile.MAXBLOCK = _last +def _test_buffer_overflow( + tmp_path: Path, im: Image.Image, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr(ImageFile, "MAXBLOCK", 1024) + _roundtrip(tmp_path, im) -def test_break_in_count_overflow(tmp_path: Path) -> None: +def test_break_in_count_overflow( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: im = Image.new("L", (256, 5)) px = im.load() + assert px is not None for y in range(4): for x in range(256): px[x, y] = x % 128 - _test_buffer_overflow(tmp_path, im) + _test_buffer_overflow(tmp_path, im, monkeypatch) -def test_break_one_in_loop(tmp_path: Path) -> None: +def test_break_one_in_loop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: im = Image.new("L", (256, 5)) px = im.load() + assert px is not None for y in range(5): for x in range(256): px[x, y] = x % 128 - _test_buffer_overflow(tmp_path, im) + _test_buffer_overflow(tmp_path, im, monkeypatch) -def test_break_many_in_loop(tmp_path: Path) -> None: +def test_break_many_in_loop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: im = Image.new("L", (256, 5)) px = im.load() + assert px is not None 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) + _test_buffer_overflow(tmp_path, im, monkeypatch) -def test_break_one_at_end(tmp_path: Path) -> None: +def test_break_one_at_end(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: im = Image.new("L", (256, 5)) px = im.load() + assert px is not None 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) + _test_buffer_overflow(tmp_path, im, monkeypatch) -def test_break_many_at_end(tmp_path: Path) -> None: +def test_break_many_at_end(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: im = Image.new("L", (256, 5)) px = im.load() + assert px is not None 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 - _test_buffer_overflow(tmp_path, im) + _test_buffer_overflow(tmp_path, im, monkeypatch) -def test_break_padding(tmp_path: Path) -> None: +def test_break_padding(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: im = Image.new("L", (257, 5)) px = im.load() + assert px is not None 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) + _test_buffer_overflow(tmp_path, im, monkeypatch) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index d39a86565ce..a2218673b44 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -5,14 +5,20 @@ import os.path import tempfile import time +from collections.abc import Generator from pathlib import Path -from typing import Any, Generator +from typing import Any import pytest from PIL import Image, PdfParser, features -from .helper import hopper, mark_if_feature_version, skip_unless_feature +from .helper import ( + hopper, + mark_if_feature_version, + skip_unless_feature, + timeout_unless_slower_valgrind, +) def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str: @@ -54,7 +60,7 @@ def test_save_alpha(tmp_path: Path, mode: str) -> None: def test_p_alpha(tmp_path: Path) -> None: # Arrange - outfile = str(tmp_path / "temp.pdf") + outfile = tmp_path / "temp.pdf" with Image.open("Tests/images/pil123p.png") as im: assert im.mode == "P" assert isinstance(im.info["transparency"], bytes) @@ -79,7 +85,7 @@ def test_monochrome(tmp_path: Path) -> None: def test_unsupported_mode(tmp_path: Path) -> None: im = hopper("PA") - outfile = str(tmp_path / "temp_PA.pdf") + outfile = tmp_path / "temp_PA.pdf" with pytest.raises(ValueError): im.save(outfile) @@ -88,7 +94,7 @@ def test_unsupported_mode(tmp_path: Path) -> None: def test_resolution(tmp_path: Path) -> None: im = hopper() - outfile = str(tmp_path / "temp.pdf") + outfile = tmp_path / "temp.pdf" im.save(outfile, resolution=150) with open(outfile, "rb") as fp: @@ -116,8 +122,8 @@ def test_resolution(tmp_path: Path) -> None: def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None: im = hopper() - outfile = str(tmp_path / "temp.pdf") - im.save(outfile, **params) + outfile = tmp_path / "temp.pdf" + im.save(outfile, "PDF", **params) with open(outfile, "rb") as fp: contents = fp.read() @@ -143,7 +149,7 @@ def test_save_all(tmp_path: Path) -> None: # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = str(tmp_path / "temp.pdf") + outfile = tmp_path / "temp.pdf" im.save(outfile, save_all=True) assert os.path.isfile(outfile) @@ -176,7 +182,7 @@ def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: def test_multiframe_normal_save(tmp_path: Path) -> None: # Test saving a multiframe image without save_all with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = str(tmp_path / "temp.pdf") + outfile = tmp_path / "temp.pdf" im.save(outfile) assert os.path.isfile(outfile) @@ -228,6 +234,7 @@ def test_pdf_append_fails_on_nonexistent_file() -> None: def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None: + assert pdf.pages_ref is not None pages_info = pdf.read_indirect(pdf.pages_ref) assert b"Parent" not in pages_info assert b"Kids" in pages_info @@ -262,7 +269,7 @@ def test_pdf_append(tmp_path: Path) -> None: # append some info pdf.info.Title = "abc" pdf.info.Author = "def" - pdf.info.Subject = "ghi\uABCD" + pdf.info.Subject = "ghi\uabcd" pdf.info.Keywords = "qw)e\\r(ty" pdf.info.Creator = "hopper()" pdf.start_writing() @@ -290,7 +297,7 @@ def test_pdf_append(tmp_path: Path) -> None: 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 pdf.info.Subject == "ghi\uabcd" assert b"CreationDate" in pdf.info assert b"ModDate" in pdf.info check_pdf_pages_consistency(pdf) @@ -337,8 +344,7 @@ def test_pdf_append_to_bytesio() -> None: assert len(f.getvalue()) > initial_size -@pytest.mark.timeout(1) -@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") +@timeout_unless_slower_valgrind(1) @pytest.mark.parametrize("newline", (b"\r", b"\n")) def test_redos(newline: bytes) -> None: malicious = b" trailer<<>>" + newline * 3456 diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index dfe8f9e9927..3f08d1ad3aa 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -41,7 +41,7 @@ def chunk(cid: bytes, *data: bytes) -> bytes: test_file = BytesIO() - PngImagePlugin.putchunk(*(test_file, cid) + data) + PngImagePlugin.putchunk(test_file, cid, *data) return test_file.getvalue() @@ -68,7 +68,7 @@ def roundtrip(im: Image.Image, **options: Any) -> PngImagePlugin.PngImageFile: @skip_unless_feature("zlib") class TestFilePng: - def get_chunks(self, filename: str) -> list[bytes]: + def get_chunks(self, filename: Path) -> list[bytes]: chunks = [] with open(filename, "rb") as fp: fp.read(8) @@ -89,7 +89,7 @@ def test_sanity(self, tmp_path: Path) -> None: assert version is not None assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version) - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" hopper("RGB").save(test_file) @@ -100,13 +100,14 @@ def test_sanity(self, tmp_path: Path) -> None: assert im.format == "PNG" assert im.get_format_mimetype() == "image/png" - for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]: - im = hopper(mode) - im.save(test_file) + for mode in ["1", "L", "P", "RGB", "I;16", "I;16B"]: + im1 = hopper(mode) + im1.save(test_file) with Image.open(test_file) as reloaded: - if mode in ("I", "I;16B"): - reloaded = reloaded.convert(mode) - assert_image_equal(reloaded, im) + converted_reloaded = ( + reloaded.convert(mode) if mode == "I;16B" else reloaded + ) + assert_image_equal(converted_reloaded, im1) def test_invalid_file(self) -> None: invalid_file = "Tests/images/flower.jpg" @@ -225,11 +226,13 @@ def test_load_transparent_p(self) -> None: test_file = "Tests/images/pil123p.png" with Image.open(test_file) as im: assert_image(im, "P", (162, 150)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (162, 150)) + im_rgba = im.convert("RGBA") + assert_image(im_rgba, "RGBA", (162, 150)) # image has 124 unique alpha values - assert len(im.getchannel("A").getcolors()) == 124 + colors = im_rgba.getchannel("A").getcolors() + assert colors is not None + assert len(colors) == 124 def test_load_transparent_rgb(self) -> None: test_file = "Tests/images/rgb_trns.png" @@ -237,11 +240,13 @@ def test_load_transparent_rgb(self) -> None: assert im.info["transparency"] == (0, 255, 52) assert_image(im, "RGB", (64, 64)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (64, 64)) + im_rgba = im.convert("RGBA") + assert_image(im_rgba, "RGBA", (64, 64)) # image has 876 transparent pixels - assert im.getchannel("A").getcolors()[0][0] == 876 + colors = im_rgba.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == 876 def test_save_p_transparent_palette(self, tmp_path: Path) -> None: in_file = "Tests/images/pil123p.png" @@ -250,7 +255,7 @@ def test_save_p_transparent_palette(self, tmp_path: Path) -> None: # each palette entry assert len(im.info["transparency"]) == 256 - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file) # check if saved image contains same transparency @@ -258,11 +263,13 @@ def test_save_p_transparent_palette(self, tmp_path: Path) -> None: assert len(im.info["transparency"]) == 256 assert_image(im, "P", (162, 150)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (162, 150)) + im_rgba = im.convert("RGBA") + assert_image(im_rgba, "RGBA", (162, 150)) # image has 124 unique alpha values - assert len(im.getchannel("A").getcolors()) == 124 + colors = im_rgba.getchannel("A").getcolors() + assert colors is not None + assert len(colors) == 124 def test_save_p_single_transparency(self, tmp_path: Path) -> None: in_file = "Tests/images/p_trns_single.png" @@ -271,7 +278,7 @@ def test_save_p_single_transparency(self, tmp_path: Path) -> None: assert im.info["transparency"] == 164 assert im.getpixel((31, 31)) == 164 - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file) # check if saved image contains same transparency @@ -279,13 +286,15 @@ def test_save_p_single_transparency(self, tmp_path: Path) -> None: 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)) + im_rgba = im.convert("RGBA") + assert_image(im_rgba, "RGBA", (64, 64)) - assert im.getpixel((31, 31)) == (0, 255, 52, 0) + assert im_rgba.getpixel((31, 31)) == (0, 255, 52, 0) # image has 876 transparent pixels - assert im.getchannel("A").getcolors()[0][0] == 876 + colors = im_rgba.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == 876 def test_save_p_transparent_black(self, tmp_path: Path) -> None: # check if solid black image with full transparency @@ -294,7 +303,7 @@ def test_save_p_transparent_black(self, tmp_path: Path) -> None: assert im.getcolors() == [(100, (0, 0, 0, 0))] im = im.convert("P") - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file) # check if saved image contains same transparency @@ -313,9 +322,11 @@ def test_save_grayscale_transparency(self, tmp_path: Path) -> None: assert im.info["transparency"] == 255 im_rgba = im.convert("RGBA") - assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent + colors = im_rgba.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == num_transparent - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file) with Image.open(test_file) as test_im: @@ -324,12 +335,23 @@ def test_save_grayscale_transparency(self, tmp_path: Path) -> None: assert_image_equal(im, test_im) test_im_rgba = test_im.convert("RGBA") - assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent + colors = test_im_rgba.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == num_transparent + + def test_save_1_transparency(self, tmp_path: Path) -> None: + out = tmp_path / "temp.png" + + im = Image.new("1", (1, 1), 1) + im.save(out, transparency=1) + + with Image.open(out) as reloaded: + assert reloaded.info["transparency"] == 255 def test_save_rgb_single_transparency(self, tmp_path: Path) -> None: in_file = "Tests/images/caption_6_33_22.png" with Image.open(in_file) as im: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file) def test_load_verify(self) -> None: @@ -338,6 +360,8 @@ def test_load_verify(self) -> None: with Image.open(TEST_PNG_FILE) as im: # Assert that there is no unclosed file warning with warnings.catch_warnings(): + warnings.simplefilter("error") + im.verify() with Image.open(TEST_PNG_FILE) as im: @@ -361,7 +385,7 @@ def test_verify_struct_error(self) -> None: with pytest.raises((OSError, SyntaxError)): im.verify() - def test_verify_ignores_crc_error(self) -> None: + def test_verify_ignores_crc_error(self, monkeypatch: pytest.MonkeyPatch) -> None: # check ignores crc errors in ancillary chunks chunk_data = chunk(b"tEXt", b"spam") @@ -371,24 +395,20 @@ def test_verify_ignores_crc_error(self) -> None: with pytest.raises(SyntaxError): PngImagePlugin.PngImageFile(BytesIO(image_data)) - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - im = load(image_data) - assert im is not None - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + im = load(image_data) + assert im is not None - def test_verify_not_ignores_crc_error_in_required_chunk(self) -> None: + def test_verify_not_ignores_crc_error_in_required_chunk( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: # check does not ignore crc errors in required chunks image_data = MAGIC + IHDR[:-1] + b"q" + TAIL - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - with pytest.raises(SyntaxError): - PngImagePlugin.PngImageFile(BytesIO(image_data)) - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + with pytest.raises(SyntaxError): + PngImagePlugin.PngImageFile(BytesIO(image_data)) def test_roundtrip_dpi(self) -> None: # Check dpi roundtripping @@ -424,8 +444,10 @@ def test_roundtrip_itxt(self) -> None: im = roundtrip(im, pnginfo=info) assert im.info == {"spam": "Eggs", "eggs": "Spam"} assert im.text == {"spam": "Eggs", "eggs": "Spam"} + assert isinstance(im.text["spam"], PngImagePlugin.iTXt) assert im.text["spam"].lang == "en" assert im.text["spam"].tkey == "Spam" + assert isinstance(im.text["eggs"], PngImagePlugin.iTXt) assert im.text["eggs"].lang == "en" assert im.text["eggs"].tkey == "Eggs" @@ -488,7 +510,7 @@ def test_trns_p(self, tmp_path: Path) -> None: im = hopper("P") im.info["transparency"] = 0 - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im.save(f) with Image.open(f) as im2: @@ -549,7 +571,7 @@ def test_repr_png_error_returns_none(self) -> None: def test_chunk_order(self, tmp_path: Path) -> None: with Image.open("Tests/images/icc_profile.png") as im: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.convert("P").save(test_file, dpi=(100, 100)) chunks = self.get_chunks(test_file) @@ -576,6 +598,7 @@ def test_getchunks(self) -> None: def test_read_private_chunks(self) -> None: with Image.open("Tests/images/exif.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.private_chunks == [(b"orNT", b"\x01")] def test_roundtrip_private_chunk(self) -> None: @@ -596,8 +619,9 @@ def test_roundtrip_private_chunk(self) -> None: (b"prIV", b"VALUE3", True), ] - def test_textual_chunks_after_idat(self) -> None: + def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/hopper.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert "comment" in im.text for k, v in { "date:create": "2014-09-04T09:37:08+03:00", @@ -607,76 +631,92 @@ def test_textual_chunks_after_idat(self) -> None: # Raises a SyntaxError in load_end with Image.open("Tests/images/broken_data_stream.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(OSError): assert isinstance(im.text, dict) + # Raises an EOFError in load_end + with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) + assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} + # Raises a UnicodeDecodeError in load_end with Image.open("Tests/images/truncated_image.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) + # The file is truncated with pytest.raises(OSError): - im.text() - ImageFile.LOAD_TRUNCATED_IMAGES = True + im.text + monkeypatch.setattr(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_unknown_compression_method(self) -> None: with pytest.raises(SyntaxError, match="Unknown compression method"): PngImagePlugin.PngImageFile("Tests/images/unknown_compression_method.png") - def test_padded_idat(self) -> None: + def test_padded_idat(self, monkeypatch: pytest.MonkeyPatch) -> None: # 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 + monkeypatch.setattr(ImageFile, "MAXBLOCK", 45) + monkeypatch.setattr(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: bytes) -> None: + def test_truncated_chunks( + self, cid: bytes, monkeypatch: pytest.MonkeyPatch + ) -> None: fp = BytesIO() with PngImagePlugin.PngStream(fp) as png: with pytest.raises(ValueError): png.call(cid, 0, 0) - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) png.call(cid, 0, 0) - ImageFile.LOAD_TRUNCATED_IMAGES = False @pytest.mark.parametrize("save_all", (True, False)) def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None: im = hopper("P") - out = str(tmp_path / "temp.png") + out = tmp_path / "temp.png" im.save(out, bits=4, save_all=save_all) with Image.open(out) as reloaded: + assert isinstance(reloaded, PngImagePlugin.PngImageFile) + assert reloaded.png is not None + assert reloaded.png.im_palette is not None assert len(reloaded.png.im_palette[1]) == 48 def test_plte_length(self, tmp_path: Path) -> None: im = Image.new("P", (1, 1)) im.putpalette((1, 1, 1)) - out = str(tmp_path / "temp.png") - im.save(str(tmp_path / "temp.png")) + out = tmp_path / "temp.png" + im.save(out) with Image.open(out) as reloaded: + assert isinstance(reloaded, PngImagePlugin.PngImageFile) + assert reloaded.png is not None + assert reloaded.png.im_palette is not None assert len(reloaded.png.im_palette[1]) == 3 + def test_plte_cmyk(self, tmp_path: Path) -> None: + im = Image.new("P", (1, 1)) + im.putpalette((0, 100, 150, 200), "CMYK") + + out = tmp_path / "temp.png" + im.save(out) + + with Image.open(out) as reloaded: + assert reloaded.convert("CMYK").getpixel((0, 0)) == (200, 222, 232, 0) + def test_getxmp(self) -> None: with Image.open("Tests/images/color_snakes.png") as im: if ElementTree is None: @@ -696,13 +736,17 @@ def test_getxmp(self) -> None: def test_exif(self) -> None: # With an EXIF chunk with Image.open("Tests/images/exif.png") as im: - exif = im._getexif() - assert exif[274] == 1 + assert isinstance(im, PngImagePlugin.PngImageFile) + exif_data = im._getexif() + assert exif_data is not None + assert exif_data[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 isinstance(im, PngImagePlugin.PngImageFile) + exif_data = im._getexif() + assert exif_data is not None + assert exif_data[274] == 1 # Assert that info still can be extracted # when the image is no longer a PngImageFile instance @@ -711,8 +755,10 @@ def test_exif(self) -> None: # With a tEXt chunk with Image.open("Tests/images/exif_text.png") as im: - exif = im._getexif() - assert exif[274] == 1 + assert isinstance(im, PngImagePlugin.PngImageFile) + exif_data = im._getexif() + assert exif_data is not None + assert exif_data[274] == 1 # With XMP tags with Image.open("Tests/images/xmp_tags_orientation.png") as im: @@ -721,11 +767,12 @@ def test_exif(self) -> None: def test_exif_save(self, tmp_path: Path) -> None: # Test exif is not saved from info - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" with Image.open("Tests/images/exif.png") as im: im.save(test_file) with Image.open(test_file) as reloaded: + assert isinstance(reloaded, PngImagePlugin.PngImageFile) assert reloaded._getexif() is None # Test passing in exif @@ -733,24 +780,28 @@ def test_exif_save(self, tmp_path: Path) -> None: im.save(test_file, exif=im.getexif()) with Image.open(test_file) as reloaded: - exif = reloaded._getexif() - assert exif[274] == 1 + assert isinstance(reloaded, PngImagePlugin.PngImageFile) + exif_data = reloaded._getexif() + assert exif_data is not None + assert exif_data[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: Path) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file, exif=im.getexif()) with Image.open(test_file) as reloaded: + assert isinstance(reloaded, PngImagePlugin.PngImageFile) exif = reloaded._getexif() + assert exif is not None assert exif[305] == "Adobe Photoshop CS Macintosh" def test_exif_argument(self, tmp_path: Path) -> None: with Image.open(TEST_PNG_FILE) as im: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file, exif=b"exifstring") with Image.open(test_file) as reloaded: @@ -768,38 +819,41 @@ def test_seek(self) -> None: im.seek(1) @pytest.mark.parametrize("buffer", (True, False)) - def test_save_stdout(self, buffer: bool) -> None: - old_stdout = sys.stdout + def test_save_stdout(self, buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None: class MyStdOut: buffer = BytesIO() mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() - sys.stdout = mystdout # type: ignore[assignment] + monkeypatch.setattr(sys, "stdout", mystdout) with Image.open(TEST_PNG_FILE) as im: - im.save(sys.stdout, "PNG") - - # Reset stdout - sys.stdout = old_stdout + im.save(sys.stdout, "PNG") # type: ignore[arg-type] if isinstance(mystdout, MyStdOut): mystdout = mystdout.buffer with Image.open(mystdout) as reloaded: assert_image_equal_tofile(reloaded, TEST_PNG_FILE) - def test_truncated_end_chunk(self) -> None: + def test_truncated_end_chunk(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/truncated_end_chunk.png") as im: with pytest.raises(OSError): im.load() - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - with Image.open("Tests/images/truncated_end_chunk.png") as im: - assert_image_equal_tofile(im, "Tests/images/hopper.png") - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + with Image.open("Tests/images/truncated_end_chunk.png") as im: + assert_image_equal_tofile(im, "Tests/images/hopper.png") + + def test_deprecation(self, tmp_path: Path) -> None: + test_file = tmp_path / "out.png" + + im = hopper("I") + with pytest.warns(DeprecationWarning, match="Saving I mode images as PNG"): + im.save(test_file) + + with Image.open(test_file) as reloaded: + assert_image_equal(im, reloaded.convert("I")) @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") @@ -808,11 +862,11 @@ 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) -> None: + def test_leak_load(self, monkeypatch: pytest.MonkeyPatch) -> None: with open("Tests/images/hopper.png", "rb") as f: DATA = BytesIO(f.read(16 * 1024)) - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) with Image.open(DATA) as im: im.load() @@ -820,7 +874,4 @@ def core() -> None: with Image.open(DATA) as im: im.load() - try: - self._test_leak(core) - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + self._test_leak(core) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index d6451ec18b5..fbca46be513 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -49,7 +49,7 @@ def test_sanity() -> None: (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", + b"P6 3 1 17 \x00\x01\x02\x08\x09\x0a\x0f\x10\x11", "RGB", ( (0, 15, 30), @@ -60,7 +60,7 @@ def test_sanity() -> None: # 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", + b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xff\xff", "RGB", ( (0, 1, 2), @@ -79,6 +79,7 @@ def test_arbitrary_maxval( assert im.mode == mode px = im.load() + assert px is not None assert tuple(px[x, 0] for x in range(3)) == pixels @@ -91,11 +92,20 @@ def test_16bit_pgm() -> None: assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.tiff") +def test_p4_save(tmp_path: Path) -> None: + with Image.open("Tests/images/hopper_1bit.pbm") as im: + filename = tmp_path / "temp.pbm" + im.save(filename) + assert_image_equal_tofile(im, filename) + + def test_16bit_pgm_write(tmp_path: Path) -> None: with Image.open("Tests/images/16_bit_binary.pgm") as im: - filename = str(tmp_path / "temp.pgm") + filename = tmp_path / "temp.pgm" im.save(filename, "PPM") + assert_image_equal_tofile(im, filename) + im.convert("I;16").save(filename, "PPM") assert_image_equal_tofile(im, filename) @@ -103,7 +113,7 @@ def test_pnm(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.pnm") as im: assert_image_similar(im, hopper(), 0.0001) - filename = str(tmp_path / "temp.pnm") + filename = tmp_path / "temp.pnm" im.save(filename) assert_image_equal_tofile(im, filename) @@ -114,7 +124,7 @@ def test_pfm(tmp_path: Path) -> None: assert im.info["scale"] == 1.0 assert_image_equal(im, hopper("F")) - filename = str(tmp_path / "tmp.pfm") + filename = tmp_path / "tmp.pfm" im.save(filename) assert_image_equal_tofile(im, filename) @@ -125,12 +135,18 @@ def test_pfm_big_endian(tmp_path: Path) -> None: assert im.info["scale"] == 2.5 assert_image_equal(im, hopper("F")) - filename = str(tmp_path / "tmp.pfm") + filename = tmp_path / "tmp.pfm" im.save(filename) assert_image_equal_tofile(im, filename) +def test_save_unsupported_mode(tmp_path: Path) -> None: + im = hopper("P") + with pytest.raises(OSError, match="cannot write mode P as PPM"): + im.save(tmp_path / "out.ppm") + + @pytest.mark.parametrize( "data", [ @@ -191,8 +207,8 @@ def test_16bit_plain_pgm() -> None: def test_plain_data_with_comment( tmp_path: Path, header: bytes, data: bytes, comment_count: int ) -> None: - path1 = str(tmp_path / "temp1.ppm") - path2 = str(tmp_path / "temp2.ppm") + path1 = tmp_path / "temp1.ppm" + path2 = 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) @@ -204,7 +220,7 @@ def test_plain_data_with_comment( @pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(data) @@ -215,7 +231,7 @@ def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None: @pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(data) @@ -232,7 +248,7 @@ def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None: ), ) def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(data) @@ -242,7 +258,7 @@ def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None: def test_plain_ppm_value_negative(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P3\n128 128\n255\n-1") @@ -252,7 +268,7 @@ def test_plain_ppm_value_negative(tmp_path: Path) -> None: def test_plain_ppm_value_too_large(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P3\n128 128\n255\n256") @@ -267,7 +283,7 @@ def test_magic() -> None: def test_header_with_comments(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") @@ -276,7 +292,7 @@ def test_header_with_comments(tmp_path: Path) -> None: def test_non_integer_token(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P6\nTEST") @@ -285,30 +301,28 @@ def test_non_integer_token(tmp_path: Path) -> None: pass -def test_header_token_too_long(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") +@pytest.mark.parametrize("data", (b"P3\x0cAAAAAAAAAA\xee", b"P6\n 01234567890")) +def test_header_token_too_long(tmp_path: Path, data: bytes) -> None: + path = tmp_path / "temp.ppm" with open(path, "wb") as f: - f.write(b"P6\n 01234567890") + f.write(data) with pytest.raises(ValueError) as e: with Image.open(path): pass - - assert str(e.value) == "Token too long in file header: 01234567890" + assert "Token too long in file header: " in repr(e) def test_truncated_file(tmp_path: Path) -> None: # Test EOF in header - path = str(tmp_path / "temp.pgm") + path = tmp_path / "temp.pgm" with open(path, "wb") as f: f.write(b"P6") - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="Reached EOF while reading header"): 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: @@ -317,7 +331,7 @@ def test_truncated_file(tmp_path: Path) -> None: def test_not_enough_image_data(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P2 1 2 255 255") @@ -328,16 +342,16 @@ def test_not_enough_image_data(tmp_path: Path) -> None: @pytest.mark.parametrize("maxval", (b"0", b"65536")) def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P6\n3 1 " + maxval) - with pytest.raises(ValueError) as e: + with pytest.raises( + ValueError, match="maxval must be greater than 0 and less than 65536" + ): with Image.open(path): pass - assert str(e.value) == "maxval must be greater than 0 and less than 65536" - def test_neg_ppm() -> None: # Storage.c accepted negative values for xsize, ysize. the @@ -351,7 +365,7 @@ def test_neg_ppm() -> None: def test_mimetypes(tmp_path: Path) -> None: - path = str(tmp_path / "temp.pgm") + path = tmp_path / "temp.pgm" with open(path, "wb") as f: f.write(b"P4\n128 128\n255") @@ -365,21 +379,17 @@ def test_mimetypes(tmp_path: Path) -> None: @pytest.mark.parametrize("buffer", (True, False)) -def test_save_stdout(buffer: bool) -> None: - old_stdout = sys.stdout +def test_save_stdout(buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None: class MyStdOut: buffer = BytesIO() mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() - sys.stdout = mystdout # type: ignore[assignment] + monkeypatch.setattr(sys, "stdout", mystdout) with Image.open(TEST_FILE) as im: - im.save(sys.stdout, "PPM") - - # Reset stdout - sys.stdout = old_stdout + im.save(sys.stdout, "PPM") # type: ignore[arg-type] if isinstance(mystdout, MyStdOut): mystdout = mystdout.buffer diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index e6c79e40b6e..538b1406b36 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,12 +1,18 @@ from __future__ import annotations +import sys import warnings import pytest from PIL import Image, PsdImagePlugin -from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy +from .helper import ( + assert_image_equal_tofile, + assert_image_similar, + hopper, + is_pypy, +) test_file = "Tests/images/hopper.psd" @@ -25,16 +31,18 @@ def test_sanity() -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(test_file) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(test_file) im.load() im.close() @@ -42,6 +50,8 @@ def test_closed_file() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(test_file) as im: im.load() @@ -55,17 +65,21 @@ def test_invalid_file() -> None: def test_n_frames() -> None: with Image.open("Tests/images/hopper_merged.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) 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 isinstance(im, PsdImagePlugin.PsdImageFile) assert im.n_frames == 2 assert im.is_animated def test_eoferror() -> None: with Image.open(test_file) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) + # PSD seek index starts at 1 rather than 0 n_frames = im.n_frames + 1 @@ -77,6 +91,11 @@ def test_eoferror() -> None: # Test that seeking to the last frame does not raise an error im.seek(n_frames - 1) + # Test seeking past the last frame without calling n_frames first + with Image.open(test_file) as im: + with pytest.raises(EOFError): + im.seek(3) + def test_seek_tell() -> None: with Image.open(test_file) as im: @@ -92,7 +111,7 @@ def test_seek_tell() -> None: im.seek(2) layer_number = im.tell() - assert layer_number == 2 + assert layer_number == 2 def test_seek_eoferror() -> None: @@ -115,11 +134,13 @@ def test_rgba() -> None: def test_negative_top_left_layer() -> None: with Image.open("Tests/images/negative_top_left_layer.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.layers[0][2] == (-50, -50, 50, 50) def test_layer_skip() -> None: with Image.open("Tests/images/five_channels.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.n_frames == 1 @@ -128,7 +149,7 @@ def test_icc_profile() -> None: assert "icc_profile" in im.info icc_profile = im.info["icc_profile"] - assert len(icc_profile) == 3144 + assert len(icc_profile) == 3144 def test_no_icc_profile() -> None: @@ -148,17 +169,16 @@ def test_combined_larger_than_size() -> None: @pytest.mark.parametrize( - "test_file,raises", + "test_file", [ - ("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError), - ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), + "Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", + "Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", ], ) -def test_crashes(test_file: str, raises: type[Exception]) -> None: - with open(test_file, "rb") as f: - with pytest.raises(raises): - with Image.open(f): - pass +def test_crashes(test_file: str) -> None: + with pytest.raises(OSError): + with Image.open(test_file): + pass @pytest.mark.parametrize( @@ -169,7 +189,38 @@ def test_crashes(test_file: str, raises: type[Exception]) -> None: ], ) def test_layer_crashes(test_file: str) -> None: - with open(test_file, "rb") as f: - with Image.open(f) as im: - with pytest.raises(SyntaxError): - im.layers + with Image.open(test_file) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) + with pytest.raises(SyntaxError): + im.layers + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/psd-oob-write.psd", + "Tests/images/psd-oob-write-x.psd", + "Tests/images/psd-oob-write-y.psd", + ], +) +def test_bounds_crash(test_file: str) -> None: + with Image.open(test_file) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) + im.seek(im.n_frames) + + with pytest.raises(ValueError): + im.load() + + +def test_bounds_crash_overflow() -> None: + with Image.open("Tests/images/psd-oob-write-overflow.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) + im.load() + if sys.maxsize <= 2**32: + with pytest.raises(OverflowError): + im.seek(im.n_frames) + else: + im.seek(im.n_frames) + + with pytest.raises(ValueError): + im.load() diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py index fd4b981ce93..b9becb24f77 100644 --- a/Tests/test_file_qoi.py +++ b/Tests/test_file_qoi.py @@ -1,10 +1,12 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, QoiImagePlugin -from .helper import assert_image_equal_tofile +from .helper import assert_image_equal_tofile, hopper def test_sanity() -> None: @@ -28,3 +30,28 @@ def test_invalid_file() -> None: with pytest.raises(SyntaxError): QoiImagePlugin.QoiImageFile(invalid_file) + + +def test_op_index() -> None: + # QOI_OP_INDEX as the first chunk + with Image.open("Tests/images/op_index.qoi") as im: + assert im.getpixel((0, 0)) == (0, 0, 0, 0) + + +def test_save(tmp_path: Path) -> None: + f = tmp_path / "temp.qoi" + + im = hopper() + im.save(f, colorspace="sRGB") + + assert_image_equal_tofile(im, f) + + for path in ("Tests/images/default_font.png", "Tests/images/pil123rgba.png"): + with Image.open(path) as im: + im.save(f) + + assert_image_equal_tofile(im, f) + + im = hopper("P") + with pytest.raises(ValueError, match="Unsupported QOI image mode"): + im.save(f) diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index e13a8019ed4..abf424dbf11 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,5 +1,6 @@ from __future__ import annotations +from io import BytesIO from pathlib import Path import pytest @@ -71,31 +72,42 @@ def test_invalid_file() -> None: SgiImagePlugin.SgiImageFile(invalid_file) -def test_write(tmp_path: Path) -> None: - def roundtrip(img: Image.Image) -> None: - out = str(tmp_path / "temp.sgi") - img.save(out, format="sgi") +def test_unsupported_image_mode() -> None: + with open("Tests/images/hopper.rgb", "rb") as fp: + data = fp.read() + data = data[:3] + b"\x03" + data[4:] + with pytest.raises(ValueError, match="Unsupported SGI image mode"): + with Image.open(BytesIO(data)): + pass + + +def roundtrip(img: Image.Image, tmp_path: Path) -> None: + out = tmp_path / "temp.sgi" + img.save(out, format="sgi") + assert_image_equal_tofile(img, out) + + out = tmp_path / "fp.sgi" + with open(out, "wb") as fp: + img.save(fp) assert_image_equal_tofile(img, out) - out = str(tmp_path / "fp.sgi") - with open(out, "wb") as fp: - img.save(fp) - assert_image_equal_tofile(img, out) + assert not fp.closed + - assert not fp.closed +@pytest.mark.parametrize("mode", ("L", "RGB", "RGBA")) +def test_write(mode: str, tmp_path: Path) -> None: + roundtrip(hopper(mode), tmp_path) - 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_write_L_mode_1_dimension(tmp_path: Path) -> None: + roundtrip(Image.new("L", (10, 1)), tmp_path) def test_write16(tmp_path: Path) -> None: test_file = "Tests/images/hopper16.rgb" with Image.open(test_file) as im: - out = str(tmp_path / "temp.sgi") + out = tmp_path / "temp.sgi" im.save(out, format="sgi", bpc=2) assert_image_equal_tofile(im, out) @@ -103,7 +115,15 @@ def test_write16(tmp_path: Path) -> None: def test_unsupported_mode(tmp_path: Path) -> None: im = hopper("LA") - out = str(tmp_path / "temp.sgi") + out = tmp_path / "temp.sgi" with pytest.raises(ValueError): im.save(out, format="sgi") + + +def test_unsupported_number_of_bytes_per_pixel(tmp_path: Path) -> None: + im = hopper() + out = tmp_path / "temp.sgi" + + with pytest.raises(ValueError, match="Unsupported number of bytes per pixel"): + im.save(out, bpc=3) diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 66c88e9d8eb..71fb434ccf4 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -7,7 +7,7 @@ import pytest -from PIL import Image, ImageSequence, SpiderImagePlugin +from PIL import Image, SpiderImagePlugin from .helper import assert_image_equal, hopper, is_pypy @@ -24,16 +24,18 @@ def test_sanity() -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(TEST_FILE) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(TEST_FILE) im.load() im.close() @@ -41,13 +43,15 @@ def test_closed_file() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(TEST_FILE) as im: im.load() def test_save(tmp_path: Path) -> None: # Arrange - temp = str(tmp_path / "temp.spider") + temp = tmp_path / "temp.spider" im = hopper() # Act @@ -59,6 +63,16 @@ def test_save(tmp_path: Path) -> None: assert im2.size == (128, 128) assert im2.format == "SPIDER" + del Image.EXTENSION[".spider"] + + +@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0))) +def test_save_zero(size: tuple[int, int]) -> None: + b = BytesIO() + im = Image.new("1", size) + with pytest.raises(ValueError, match="cannot write empty image"): + im.save(b, "SPIDER") + def test_tempfile() -> None: # Arrange @@ -92,6 +106,7 @@ def test_tell() -> None: def test_n_frames() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, SpiderImagePlugin.SpiderImageFile) assert im.n_frames == 1 assert not im.is_animated @@ -149,8 +164,8 @@ def test_nonstack_file() -> None: def test_nonstack_dos() -> None: with Image.open(TEST_FILE) as im: - for i, frame in enumerate(ImageSequence.Iterator(im)): - assert i <= 1, "Non-stack DOS file test failed" + with pytest.raises(EOFError): + im.seek(0) # for issue #4093 diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 6cfff87309c..78534e15411 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -1,10 +1,11 @@ from __future__ import annotations +import io import os import pytest -from PIL import Image, SunImagePlugin +from PIL import Image, SunImagePlugin, _binary from .helper import assert_image_equal_tofile, assert_image_similar, hopper @@ -33,6 +34,60 @@ def test_im1() -> None: assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png") +def _sun_header( + depth: int = 0, file_type: int = 0, palette_length: int = 0 +) -> io.BytesIO: + return io.BytesIO( + _binary.o32be(0x59A66A95) + + b"\x00" * 8 + + _binary.o32be(depth) + + b"\x00" * 4 + + _binary.o32be(file_type) + + b"\x00" * 4 + + _binary.o32be(palette_length) + ) + + +def test_unsupported_mode_bit_depth() -> None: + with pytest.raises(SyntaxError, match="Unsupported Mode/Bit Depth"): + with SunImagePlugin.SunImageFile(_sun_header()): + pass + + +def test_unsupported_color_palette_length() -> None: + with pytest.raises(SyntaxError, match="Unsupported Color Palette Length"): + with SunImagePlugin.SunImageFile(_sun_header(depth=1, palette_length=1025)): + pass + + +def test_unsupported_palette_type() -> None: + with pytest.raises(SyntaxError, match="Unsupported Palette Type"): + with SunImagePlugin.SunImageFile(_sun_header(depth=1, palette_length=1)): + pass + + +def test_unsupported_file_type() -> None: + with pytest.raises(SyntaxError, match="Unsupported Sun Raster file type"): + with SunImagePlugin.SunImageFile(_sun_header(depth=1, file_type=6)): + pass + + +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +def test_rgbx() -> None: + with open(os.path.join(EXTRA_DIR, "32bpp.ras"), "rb") as fp: + data = fp.read() + + # Set file type to 3 + data = data[:20] + _binary.o32be(3) + data[24:] + + with Image.open(io.BytesIO(data)) as im: + r, g, b = im.split() + im_rgb = Image.merge("RGB", (b, g, r)) + assert_image_equal_tofile(im_rgb, os.path.join(EXTRA_DIR, "32bpp.png")) + + @pytest.mark.skipif( not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 6217ebedd8a..084d0f2880d 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,6 +1,7 @@ from __future__ import annotations import warnings +from pathlib import Path import pytest @@ -29,6 +30,22 @@ def test_sanity(codec: str, test_path: str, format: str) -> None: assert im.format == format +def test_unexpected_end(tmp_path: Path) -> None: + tmpfile = str(tmp_path / "temp.tar") + with open(tmpfile, "w"): + pass + + with pytest.raises(OSError, match="unexpected end of tar file"): + with TarIO.TarIO(tmpfile, "test"): + pass + + +def test_cannot_find_subfile() -> None: + with pytest.raises(OSError, match="cannot find subfile"): + with TarIO.TarIO(TEST_TAR_FILE, "test"): + pass + + @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: with pytest.warns(ResourceWarning): @@ -37,11 +54,15 @@ def test_unclosed_file() -> None: def test_close() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") tar.close() def test_contextmanager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): pass diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index a03a6a6e10a..7ec562342e1 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -1,13 +1,12 @@ from __future__ import annotations import os -from glob import glob -from itertools import product +from io import BytesIO from pathlib import Path import pytest -from PIL import Image, UnidentifiedImageError +from PIL import Image, UnidentifiedImageError, _binary from .helper import assert_image_equal, assert_image_equal_tofile, hopper @@ -15,16 +14,27 @@ _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: str, tmp_path: Path) -> None: +@pytest.mark.parametrize( + "size_mode", + ( + ("1x1", "L"), + ("200x32", "L"), + ("200x32", "LA"), + ("200x32", "P"), + ("200x32", "RGB"), + ("200x32", "RGBA"), + ), +) +@pytest.mark.parametrize("origin", _ORIGIN_TO_ORIENTATION) +@pytest.mark.parametrize("rle", (True, False)) +def test_sanity( + size_mode: tuple[str, str], origin: str, rle: str, tmp_path: Path +) -> None: def roundtrip(original_im: Image.Image) -> None: - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" original_im.save(out, rle=rle) with Image.open(out) as saved_im: @@ -36,46 +46,40 @@ def roundtrip(original_im: Image.Image) -> None: 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 + size, mode = size_mode + png_path = os.path.join(_TGA_DIR_COMMON, size + "_" + mode.lower() + ".png") + 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" - ) + path_no_ext = os.path.splitext(png_path)[0] + 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() + 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() - assert_image_equal(original_im, reference_im) + assert_image_equal(original_im, reference_im) - roundtrip(original_im) + roundtrip(original_im) -def test_palette_depth_8(tmp_path: Path) -> None: +def test_palette_depth_8() -> None: with pytest.raises(UnidentifiedImageError): Image.open("Tests/images/p_8.tga") def test_palette_depth_16(tmp_path: Path) -> None: with Image.open("Tests/images/p_16.tga") as im: + assert im.palette is not None assert im.palette.mode == "RGBA" assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png") - out = str(tmp_path / "temp.png") + out = tmp_path / "temp.png" im.save(out) with Image.open(out) as reloaded: assert_image_equal_tofile(reloaded.convert("RGBA"), "Tests/images/p_16.png") @@ -89,6 +93,25 @@ def test_rgba_16() -> None: assert im.getpixel((1, 0)) == (0, 255, 82, 0) +def test_v2_no_alpha() -> None: + test_file = "Tests/images/tga/common/200x32_rgba_tl_rle.tga" + with open(test_file, "rb") as fp: + data = fp.read() + data += ( + b"\x00" * 495 + + _binary.o32le(len(data)) + + _binary.o32le(0) + + b"TRUEVISION-XFILE.\x00" + ) + with Image.open(BytesIO(data)) as im: + with Image.open(test_file) as im2: + r, g, b = im2.split()[:3] + a = Image.new("L", im2.size, 255) + expected = Image.merge("RGBA", (r, g, b, a)) + + assert_image_equal(im, expected) + + def test_id_field() -> None: # tga file with id field test_file = "Tests/images/tga_id_field.tga" @@ -121,7 +144,7 @@ def test_cross_scan_line() -> None: def test_save(tmp_path: Path) -> None: test_file = "Tests/images/tga_id_field.tga" with Image.open(test_file) as im: - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" # Save im.save(out) @@ -140,7 +163,7 @@ def test_small_palette(tmp_path: Path) -> None: colors = [0, 0, 0] im.putpalette(colors) - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" im.save(out) with Image.open(out) as reloaded: @@ -154,7 +177,7 @@ def test_missing_palette() -> None: def test_save_wrong_mode(tmp_path: Path) -> None: im = hopper("PA") - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" with pytest.raises(OSError): im.save(out) @@ -171,7 +194,7 @@ def test_save_mapdepth() -> None: def test_save_id_section(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" with Image.open(test_file) as im: - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" # Check there is no id section im.save(out) @@ -185,7 +208,9 @@ def test_save_id_section(tmp_path: Path) -> None: # Save with custom id section greater than 255 characters id_section = b"Test content" * 25 - with pytest.warns(UserWarning): + with pytest.warns( + UserWarning, match="id_section has been trimmed to 255 characters" + ): im.save(out, id_section=id_section) with Image.open(out) as test_im: @@ -201,7 +226,7 @@ def test_save_id_section(tmp_path: Path) -> None: def test_save_orientation(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" with Image.open(test_file) as im: assert im.info["orientation"] == -1 @@ -213,10 +238,18 @@ def test_save_orientation(tmp_path: Path) -> None: def test_horizontal_orientations() -> None: # 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) + px = im.load() + assert px is not None + value = px[90, 90] + assert isinstance(value, tuple) + assert value[:3] == (0, 0, 0) with Image.open("Tests/images/rgb32rle_bottom_right.tga") as im: - assert im.load()[90, 90][:3] == (0, 255, 0) + px = im.load() + assert px is not None + value = px[90, 90] + assert isinstance(value, tuple) + assert value[:3] == (0, 255, 0) def test_save_rle(tmp_path: Path) -> None: @@ -224,7 +257,7 @@ def test_save_rle(tmp_path: Path) -> None: with Image.open(test_file) as im: assert im.info["compression"] == "tga_rle" - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" # Save im.save(out) @@ -259,13 +292,17 @@ def test_save_l_transparency(tmp_path: Path) -> None: 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 + colors = im.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == num_transparent - out = str(tmp_path / "temp.tga") + out = 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 + colors = test_im.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == num_transparent assert_image_equal(im, test_im) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index c06d110a75e..e442471d1ca 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -2,14 +2,22 @@ import os import warnings +from collections.abc import Generator from io import BytesIO from pathlib import Path from types import ModuleType -from typing import Generator import pytest -from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError +from PIL import ( + Image, + ImageFile, + JpegImagePlugin, + TiffImagePlugin, + TiffTags, + UnidentifiedImageError, + _binary, +) from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION from .helper import ( @@ -20,6 +28,7 @@ hopper, is_pypy, is_win32, + timeout_unless_slower_valgrind, ) ElementTree: ModuleType | None @@ -31,7 +40,7 @@ class TestFileTiff: def test_sanity(self, tmp_path: Path) -> None: - filename = str(tmp_path / "temp.tif") + filename = tmp_path / "temp.tif" hopper("RGB").save(filename) @@ -41,43 +50,31 @@ def test_sanity(self, tmp_path: Path) -> None: assert im.size == (128, 128) assert im.format == "TIFF" - hopper("1").save(filename) - with Image.open(filename): - pass - - hopper("L").save(filename) - with Image.open(filename): - pass - - hopper("P").save(filename) - with Image.open(filename): - pass - - hopper("RGB").save(filename) - with Image.open(filename): - pass - - hopper("I").save(filename) - with Image.open(filename): - pass + for mode in ("1", "L", "P", "RGB", "I", "I;16", "I;16L"): + hopper(mode).save(filename) + with Image.open(filename): + pass @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(self) -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open("Tests/images/multipage.tiff") im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file(self) -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open("Tests/images/multipage.tiff") im.load() im.close() def test_seek_after_close(self) -> None: im = Image.open("Tests/images/multipage.tiff") + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.close() with pytest.raises(ValueError): @@ -87,6 +84,8 @@ def test_seek_after_close(self) -> None: def test_context_manager(self) -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open("Tests/images/multipage.tiff") as im: im.load() @@ -107,25 +106,39 @@ def test_bigtiff(self, tmp_path: Path) -> None: assert_image_equal_tofile(im, "Tests/images/hopper.tif") with Image.open("Tests/images/hopper_bigtiff.tif") as im: - # multistrip support not yet implemented - del im.tag_v2[273] - - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) + def test_bigtiff_save(self, tmp_path: Path) -> None: + outfile = tmp_path / "temp.tif" + im = hopper() + im.save(outfile, big_tiff=True) + + with Image.open(outfile) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) + assert reloaded.tag_v2._bigtiff is True + + im.save(outfile, save_all=True, append_images=[im], big_tiff=True) + + with Image.open(outfile) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) + assert reloaded.tag_v2._bigtiff is True + def test_seek_too_large(self) -> None: with pytest.raises(ValueError, match="Unable to seek to frame"): Image.open("Tests/images/seek_too_large.tif") def test_set_legacy_api(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() - with pytest.raises(Exception) as e: + with pytest.raises(Exception, match="Not allowing setting of legacy api"): ifd.legacy_api = False - assert str(e.value) == "Not allowing setting of legacy api" def test_xyres_tiff(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # legacy api assert isinstance(im.tag[X_RESOLUTION][0], tuple) assert isinstance(im.tag[Y_RESOLUTION][0], tuple) @@ -139,6 +152,8 @@ def test_xyres_tiff(self) -> None: def test_xyres_fallback_tiff(self) -> None: filename = "Tests/images/compression.tif" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # v2 api assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) @@ -153,6 +168,8 @@ def test_xyres_fallback_tiff(self) -> None: def test_int_resolution(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # Try to read a file where X,Y_RESOLUTION are ints im.tag_v2[X_RESOLUTION] = 71 im.tag_v2[Y_RESOLUTION] = 71 @@ -167,11 +184,12 @@ def test_load_float_dpi(self, resolution_unit: int | None, dpi: float) -> None: with Image.open( "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" ) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit assert im.info["dpi"] == (dpi, dpi) def test_save_float_dpi(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/hopper.tif") as im: dpi = (72.2, 72.2) im.save(outfile, dpi=dpi) @@ -184,6 +202,7 @@ def test_save_setting_missing_resolution(self) -> None: 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 isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2[X_RESOLUTION] == 123.45 assert im.tag_v2[Y_RESOLUTION] == 123.45 @@ -199,19 +218,21 @@ def test_invalid_file(self) -> None: TiffImagePlugin.PREFIXES.pop() def test_bad_exif(self) -> None: - with Image.open("Tests/images/hopper_bad_exif.jpg") as i: + with Image.open("Tests/images/hopper_bad_exif.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + # Should not raise struct.error. - with pytest.warns(UserWarning): - i._getexif() + with pytest.warns(UserWarning, match="Corrupt EXIF data"): + im._getexif() def test_save_rgba(self, tmp_path: Path) -> None: im = hopper("RGBA") - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im.save(outfile) def test_save_unsupported_mode(self, tmp_path: Path) -> None: im = hopper("HSV") - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with pytest.raises(OSError): im.save(outfile) @@ -293,11 +314,13 @@ def test_unknown_pixel_mode(self) -> None: ) def test_n_frames(self, path: str, n_frames: int) -> None: with Image.open(path) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) def test_eoferror(self) -> None: with Image.open("Tests/images/multipage-lastframe.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -341,19 +364,24 @@ def test_multipage_last_frame(self) -> None: def test_frame_order(self) -> None: # A frame can't progress to itself after reading with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) 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 isinstance(im, TiffImagePlugin.TiffImageFile) 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 isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == 3 def test___str__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # Act ret = str(im.ifd) @@ -364,6 +392,8 @@ def test_dict(self) -> None: # Arrange filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # v2 interface v2_tags = { 256: 55, @@ -403,6 +433,7 @@ def test_dict(self) -> None: def test__delitem__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) len_before = len(dict(im.ifd)) del im.ifd[256] len_after = len(dict(im.ifd)) @@ -424,17 +455,18 @@ def test_load_string(self) -> None: def test_load_float(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdabcd" - ret = ifd.load_float(data, False) + ret = getattr(ifd, "load_float")(data, False) assert ret == (1.6777999408082104e22, 1.6777999408082104e22) def test_load_double(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdefghabcdefgh" - ret = ifd.load_double(data, False) + ret = getattr(ifd, "load_double")(data, False) assert ret == (8.540883223036124e194, 8.540883223036124e194) def test_ifd_tag_type(self) -> None: with Image.open("Tests/images/ifd_tag_type.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert 0x8825 in im.tag_v2 def test_exif(self, tmp_path: Path) -> None: @@ -471,14 +503,14 @@ def check_exif(exif: Image.Exif) -> None: assert gps[0] == b"\x03\x02\x00\x00" assert gps[18] == "WGS-84" - outfile = str(tmp_path / "temp.tif") + outfile = 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") + outfile2 = tmp_path / "temp2.tif" with Image.open(outfile) as im: exif = im.getexif() check_exif(exif) @@ -490,7 +522,7 @@ def check_exif(exif: Image.Exif) -> None: check_exif(exif) def test_modify_exif(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/ifd_tag_type.tiff") as im: exif = im.getexif() exif[264] = 100 @@ -519,10 +551,11 @@ def test_exif_frames(self) -> None: @pytest.mark.parametrize("mode", ("1", "L")) def test_photometric(self, mode: str, tmp_path: Path) -> None: - filename = str(tmp_path / "temp.tif") + filename = tmp_path / "temp.tif" im = hopper(mode) im.save(filename, tiffinfo={262: 0}) with Image.open(filename) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[262] == 0 assert_image_equal(im, reloaded) @@ -598,9 +631,11 @@ def test_gray_semibyte_per_pixel(self) -> None: def test_with_underscores(self, tmp_path: Path) -> None: kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} - filename = str(tmp_path / "temp.tif") - hopper("RGB").save(filename, **kwargs) + filename = tmp_path / "temp.tif" + hopper("RGB").save(filename, "TIFF", **kwargs) with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # legacy interface assert im.tag[X_RESOLUTION][0][0] == 72 assert im.tag[Y_RESOLUTION][0][0] == 36 @@ -616,26 +651,29 @@ def test_roundtrip_tiff_uint16(self, tmp_path: Path) -> None: with Image.open(infile) as im: assert im.getpixel((0, 0)) == pixel_value - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" im.save(tmpfile) assert_image_equal_tofile(im, tmpfile) def test_iptc(self, tmp_path: Path) -> None: # Do not preserve IPTC_NAA_CHUNK by default if type is LONG - outfile = str(tmp_path / "temp.tif") - im = hopper() - ifd = TiffImagePlugin.ImageFileDirectory_v2() - ifd[33723] = 1 - ifd.tagtype[33723] = 4 - im.tag_v2 = ifd - im.save(outfile) + outfile = tmp_path / "temp.tif" + with Image.open("Tests/images/hopper.tif") as im: + im.load() + assert isinstance(im, TiffImagePlugin.TiffImageFile) + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[33723] = 1 + ifd.tagtype[33723] = 4 + im.tag_v2 = ifd + im.save(outfile) with Image.open(outfile) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert 33723 not in im.tag_v2 def test_rowsperstrip(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im = hopper() im.save(outfile, tiffinfo={278: 256}) @@ -643,6 +681,23 @@ def test_rowsperstrip(self, tmp_path: Path) -> None: assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2[278] == 256 + im = hopper() + im.encoderinfo = {"tiffinfo": {278: 100}} + im2 = Image.new("L", (128, 128)) + im3 = im2.copy() + im3.encoderinfo = {"tiffinfo": {278: 300}} + im.save(outfile, save_all=True, tiffinfo={278: 200}, append_images=[im2, im3]) + + with Image.open(outfile) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + assert im.tag_v2[278] == 100 + + im.seek(1) + assert im.tag_v2[278] == 200 + + im.seek(2) + assert im.tag_v2[278] == 300 + def test_strip_raw(self) -> None: infile = "Tests/images/tiff_strip_raw.tif" with Image.open(infile) as im: @@ -672,17 +727,25 @@ def test_tiled_planar_raw(self) -> None: def test_planar_configuration_save(self, tmp_path: Path) -> None: infile = "Tests/images/tiff_tiled_planar_raw.tif" with Image.open(infile) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im._planar_configuration == 2 - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im.save(outfile) with Image.open(outfile) as reloaded: assert_image_equal_tofile(reloaded, infile) + def test_invalid_tiled_dimensions(self) -> None: + with open("Tests/images/tiff_tiled_planar_raw.tif", "rb") as fp: + data = fp.read() + b = BytesIO(data[:144] + b"\x02" + data[145:]) + with pytest.raises(ValueError): + Image.open(b) + @pytest.mark.parametrize("mode", ("P", "PA")) def test_palette(self, mode: str, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im = hopper(mode) im.save(outfile) @@ -697,16 +760,18 @@ def test_tiff_save_all(self) -> None: mp.seek(0, os.SEEK_SET) with Image.open(mp) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == 3 # Test appending images mp = BytesIO() - im = Image.new("RGB", (100, 100), "#f00") + im_rgb = 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) + im_rgb.copy().save(mp, format="TIFF", save_all=True, append_images=ims) mp.seek(0, os.SEEK_SET) with Image.open(mp) as reread: + assert isinstance(reread, TiffImagePlugin.TiffImageFile) assert reread.n_frames == 3 # Test appending using a generator @@ -714,12 +779,60 @@ def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims mp = BytesIO() - im.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims)) + im_rgb.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 isinstance(reread, TiffImagePlugin.TiffImageFile) assert reread.n_frames == 3 + def test_fixoffsets(self) -> None: + b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00") + with TiffImagePlugin.AppendingTiffWriter(b) as a: + b.seek(0) + a.fixOffsets(1, isShort=True) + + b.seek(0) + a.fixOffsets(1, isLong=True) + + # Neither short nor long + b.seek(0) + with pytest.raises(RuntimeError): + a.fixOffsets(1) + + b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00") + with TiffImagePlugin.AppendingTiffWriter(b) as a: + a.offsetOfNewPage = 2**16 + + b.seek(0) + a.fixOffsets(1, isShort=True) + + b = BytesIO(b"II\x2b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") + with TiffImagePlugin.AppendingTiffWriter(b) as a: + a.offsetOfNewPage = 2**32 + + b.seek(0) + a.fixOffsets(1, isShort=True) + + b.seek(0) + a.fixOffsets(1, isLong=True) + + def test_appending_tiff_writer_writelong(self) -> None: + data = b"II\x2a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b = BytesIO(data) + with TiffImagePlugin.AppendingTiffWriter(b) as a: + a.seek(-4, os.SEEK_CUR) + a.writeLong(2**32 - 1) + assert b.getvalue() == data[:-4] + b"\xff\xff\xff\xff" + + def test_appending_tiff_writer_rewritelastshorttolong(self) -> None: + data = b"II\x2a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b = BytesIO(data) + with TiffImagePlugin.AppendingTiffWriter(b) as a: + a.seek(-2, os.SEEK_CUR) + a.rewriteLastShortToLong(2**32 - 1) + assert b.getvalue() == data[:-4] + b"\xff\xff\xff\xff" + def test_saving_icc_profile(self, tmp_path: Path) -> None: # Tests saving TIFF with icc_profile set. # At the time of writing this will only work for non-compressed tiffs @@ -729,7 +842,7 @@ def test_saving_icc_profile(self, tmp_path: Path) -> None: im.info["icc_profile"] = "Dummy value" # Try save-load round trip to make sure both handle icc_profile. - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" im.save(tmpfile, "TIFF", compression="raw") with Image.open(tmpfile) as reloaded: assert b"Dummy value" == reloaded.info["icc_profile"] @@ -738,7 +851,7 @@ def test_save_icc_profile(self, tmp_path: Path) -> None: im = hopper() assert "icc_profile" not in im.info - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" icc_profile = b"Dummy value" im.save(outfile, icc_profile=icc_profile) @@ -749,11 +862,11 @@ def test_save_bmp_compression(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper.bmp") as im: assert im.info["compression"] == 0 - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im.save(outfile) def test_discard_icc_profile(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/icc_profile.png") as im: assert "icc_profile" in im.info @@ -779,8 +892,32 @@ def test_getxmp(self) -> None: assert description[0]["format"] == "image/tiff" assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] + def test_getxmp_undefined(self, tmp_path: Path) -> None: + tmpfile = tmp_path / "temp.tif" + im = Image.new("L", (1, 1)) + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd.tagtype[700] = TiffTags.UNDEFINED + with Image.open("Tests/images/lab.tif") as im_xmp: + ifd[700] = im_xmp.info["xmp"] + im.save(tmpfile, tiffinfo=ifd) + + with Image.open(tmpfile) as im_reloaded: + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + assert im_reloaded.getxmp() == {} + else: + assert "xmp" in im_reloaded.info + xmp = im_reloaded.getxmp() + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description[0]["format"] == "image/tiff" + def test_get_photoshop_blocks(self) -> None: with Image.open("Tests/images/lab.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert list(im.get_photoshop_blocks().keys()) == [ 1061, 1002, @@ -805,8 +942,17 @@ def test_get_photoshop_blocks(self) -> None: 4001, ] + def test_truncated_photoshop_blocks(self) -> None: + with Image.open("Tests/images/hopper.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + im.tag_v2[34377] = b"8BIM" + assert im.get_photoshop_blocks() == {} + + im.tag_v2[34377] = b"8BIM" + _binary.o16be(0) + _binary.o8(2) + b" " * 5 + assert im.get_photoshop_blocks() == {} + def test_tiff_chunks(self, tmp_path: Path) -> None: - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" im = hopper() with open(tmpfile, "wb") as fp: @@ -828,19 +974,20 @@ def test_tiff_chunks(self, tmp_path: Path) -> None: def test_close_on_load_exclusive(self, tmp_path: Path) -> None: # similar to test_fd_leak, but runs on unixlike os - tmpfile = str(tmp_path / "temp.tif") + tmpfile = 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 + assert fp is not None assert not fp.closed im.load() assert fp.closed def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None: - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" with Image.open("Tests/images/uint16_1_4660.tif") as im: im.save(tmpfile) @@ -848,6 +995,7 @@ def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None: with open(tmpfile, "rb") as f: im = Image.open(f) fp = im.fp + assert fp is not None assert not fp.closed im.load() assert not fp.closed @@ -867,13 +1015,12 @@ def test_string_dimension(self) -> None: with pytest.raises(OSError): im.load() - @pytest.mark.timeout(6) + @timeout_unless_slower_valgrind(6) @pytest.mark.filterwarnings("ignore:Truncated File Read") - def test_timeout(self) -> None: + def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/timeout-6646305047838720") as im: - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) im.load() - ImageFile.LOAD_TRUNCATED_IMAGES = False @pytest.mark.parametrize( "test_file", @@ -881,10 +1028,10 @@ def test_timeout(self) -> None: "Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif", ], ) - @pytest.mark.timeout(2) + @timeout_unless_slower_valgrind(2) def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Corrupt EXIF data"): with Image.open(test_file): pass @@ -892,15 +1039,16 @@ def test_oom(self, test_file: str) -> None: @pytest.mark.skipif(not is_win32(), reason="Windows only") class TestFileTiffW32: def test_fd_leak(self, tmp_path: Path) -> None: - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" # this is an mmaped file. with Image.open("Tests/images/uint16_1_4660.tif") as im: im.save(tmpfile) im = Image.open(tmpfile) + assert im.fp is not None + assert not im.fp.closed fp = im.fp - assert not fp.closed with pytest.raises(OSError): os.remove(tmpfile) im.load() diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 1e0310001a5..322ef5abcc5 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -56,11 +56,12 @@ def test_rt_metadata(tmp_path: Path) -> None: info[ImageDescription] = text_data - f = str(tmp_path / "temp.tif") + f = tmp_path / "temp.tif" img.save(f, tiffinfo=info) with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),) @@ -80,12 +81,14 @@ def test_rt_metadata(tmp_path: Path) -> None: info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8) img.save(f, tiffinfo=info) with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) def test_read_metadata() -> None: with Image.open("Tests/images/hopper_g4.tif") as img: + assert isinstance(img, TiffImagePlugin.TiffImageFile) assert { "YResolution": IFDRational(4294967295, 113653537), "PlanarConfiguration": 1, @@ -128,13 +131,15 @@ def test_read_metadata() -> None: def test_write_metadata(tmp_path: Path) -> None: """Test metadata writing through the python code""" with Image.open("Tests/images/hopper.tif") as img: - f = str(tmp_path / "temp.tiff") + assert isinstance(img, TiffImagePlugin.TiffImageFile) + f = tmp_path / "temp.tiff" del img.tag[278] img.save(f, tiffinfo=img.tag) original = img.tag_v2.named() with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) reloaded = loaded.tag_v2.named() ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"] @@ -163,31 +168,56 @@ def test_write_metadata(tmp_path: Path) -> None: def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) info = im.tag_v2 del info[278] # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT - im = im.resize((500, 500)) - info[TiffImagePlugin.IMAGEWIDTH] = im.width + im_resized = im.resize((500, 500)) + info[TiffImagePlugin.IMAGEWIDTH] = im_resized.width # STRIPBYTECOUNTS can be a SHORT or a LONG info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT - im.save(out, tiffinfo=info) + im_resized.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG +def test_save_multiple_stripoffsets() -> None: + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[TiffImagePlugin.STRIPOFFSETS] = (123, 456) + assert ifd.tagtype[TiffImagePlugin.STRIPOFFSETS] == TiffTags.LONG + + # all values are in little-endian + assert ifd.tobytes() == ( + # number of tags == 1 + b"\x01\x00" + # tag id (2 bytes), type (2 bytes), count (4 bytes), value (4 bytes) + # TiffImagePlugin.STRIPOFFSETS, TiffTags.LONG, 2, 18 + # where STRIPOFFSETS is 273, LONG is 4 + # and 18 is the offset of the tag data + b"\x11\x01\x04\x00\x02\x00\x00\x00\x12\x00\x00\x00" + # end of entries + b"\x00\x00\x00\x00" + # 26 is the total number of bytes output, + # the offset for any auxiliary strip data that will then be appended + # (123 + 26, 456 + 26) == (149, 482) + b"\x95\x00\x00\x00\xe2\x01\x00\x00" + ) + + def test_no_duplicate_50741_tag() -> None: assert TAG_IDS["MakerNoteSafety"] == 50741 assert TAG_IDS["BestQualityScale"] == 50780 def test_iptc(tmp_path: Path) -> None: - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper.Lab.tif") as im: im.save(out) @@ -204,10 +234,11 @@ def test_writing_other_types_to_ascii( info[271] = value im = hopper() - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[271] == expected @@ -221,10 +252,11 @@ def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) info[700] = value - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[700] == b"\x01" @@ -240,10 +272,11 @@ def test_writing_other_types_to_undefined( info[33723] = value - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[33723] == b"1" @@ -267,13 +300,13 @@ def test_empty_metadata() -> None: head = f.read(8) info = TiffImagePlugin.ImageFileDirectory(head) # Should not raise struct.error. - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Corrupt EXIF data"): info.load(f) def test_iccprofile(tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/1462 - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper.iccprofile.tif") as im: im.save(out) @@ -288,19 +321,20 @@ def test_iccprofile_binary() -> None: # but probably won't be able to save it. with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2.tagtype[34675] == 1 assert im.info["icc_profile"] def test_iccprofile_save_png(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.iccprofile.tif") as im: - outfile = str(tmp_path / "temp.png") + outfile = tmp_path / "temp.png" im.save(outfile) def test_iccprofile_binary_save_png(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: - outfile = str(tmp_path / "temp.png") + outfile = tmp_path / "temp.png" im.save(outfile) @@ -309,10 +343,11 @@ def test_exif_div_zero(tmp_path: Path) -> None: info = TiffImagePlugin.ImageFileDirectory_v2() info[41988] = TiffImagePlugin.IFDRational(0, 0) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert 0 == reloaded.tag_v2[41988].numerator assert 0 == reloaded.tag_v2[41988].denominator @@ -328,10 +363,11 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: info[41493] = TiffImagePlugin.IFDRational(numerator, 1) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert max_long == reloaded.tag_v2[41493].numerator assert 1 == reloaded.tag_v2[41493].denominator @@ -340,10 +376,11 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: info[41493] = TiffImagePlugin.IFDRational(numerator, 1) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert max_long == reloaded.tag_v2[41493].numerator assert 1 == reloaded.tag_v2[41493].denominator @@ -358,10 +395,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert numerator == reloaded.tag_v2[37380].numerator assert denominator == reloaded.tag_v2[37380].denominator @@ -370,10 +408,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert numerator == reloaded.tag_v2[37380].numerator assert denominator == reloaded.tag_v2[37380].denominator @@ -383,10 +422,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert 2**31 - 1 == reloaded.tag_v2[37380].numerator assert -1 == reloaded.tag_v2[37380].denominator @@ -397,10 +437,11 @@ def test_ifd_signed_long(tmp_path: Path) -> None: info[37000] = -60000 - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[37000] == -60000 @@ -421,11 +462,13 @@ def test_empty_values() -> None: def test_photoshop_info(tmp_path: Path) -> None: with Image.open("Tests/images/issue_2278.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert len(im.tag_v2[34377]) == 70 assert isinstance(im.tag_v2[34377], bytes) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert len(reloaded.tag_v2[34377]) == 70 assert isinstance(reloaded.tag_v2[34377], bytes) @@ -438,7 +481,7 @@ def test_too_many_entries() -> None: ifd.tagtype[277] = TiffTags.SHORT # Should not raise ValueError. - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Metadata Warning"): assert ifd[277] == 4 @@ -457,7 +500,7 @@ def test_tag_group_data() -> None: def test_empty_subifd(tmp_path: Path) -> None: - out = str(tmp_path / "temp.jpg") + out = tmp_path / "temp.jpg" im = hopper() exif = im.getexif() diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index b34975e8380..549d47054e7 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -1,5 +1,7 @@ from __future__ import annotations +from io import BytesIO + from PIL import WalImageFile from .helper import assert_image_equal_tofile @@ -13,15 +15,29 @@ def test_open() -> None: assert im.format_description == "Quake2 Texture" assert im.mode == "P" assert im.size == (128, 128) + assert "next_name" not in im.info assert isinstance(im, WalImageFile.WalImageFile) assert_image_equal_tofile(im, "Tests/images/hopper_wal.png") +def test_next_name() -> None: + with open(TEST_FILE, "rb") as fp: + data = bytearray(fp.read()) + data[56:60] = b"Test" + f = BytesIO(data) + with WalImageFile.open(f) as im: + assert im.info["next_name"] == b"Test" + + def test_load() -> None: with WalImageFile.open(TEST_FILE) as im: - assert im.load()[0, 0] == 122 + px = im.load() + assert px is not None + assert px[0, 0] == 122 # Test again now that it has already been loaded once - assert im.load()[0, 0] == 122 + px = im.load() + assert px is not None + assert px[0, 0] == 122 diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index cbc905de4e7..f996cce675c 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -28,19 +28,16 @@ class TestUnsupportedWebp: - def test_unsupported(self) -> None: + def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: if HAVE_WEBP: - WebPImagePlugin.SUPPORTED = False + monkeypatch.setattr(WebPImagePlugin, "SUPPORTED", False) file_path = "Tests/images/hopper.webp" - with pytest.warns(UserWarning): - with pytest.raises(OSError): + with pytest.raises(OSError): + with pytest.warns(UserWarning, match="WEBP support not installed"): with Image.open(file_path): pass - if HAVE_WEBP: - WebPImagePlugin.SUPPORTED = True - @skip_unless_feature("webp") class TestFileWebp: @@ -48,8 +45,6 @@ def setup_method(self) -> None: self.rgb_mode = "RGB" def test_version(self) -> None: - _webp.WebPDecoderVersion() - _webp.WebPDecoderBuggyAlpha() version = features.version_module("webp") assert version is not None assert re.search(r"\d+\.\d+\.\d+$", version) @@ -65,7 +60,6 @@ def test_read_rgb(self) -> None: 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 @@ -74,7 +68,7 @@ def test_read_rgb(self) -> None: def _roundtrip( self, tmp_path: Path, mode: str, epsilon: float, args: dict[str, Any] = {} ) -> None: - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" hopper(mode).save(temp_file, **args) with Image.open(temp_file) as image: @@ -82,7 +76,6 @@ def _roundtrip( 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 @@ -117,9 +110,8 @@ def test_write_method(self, tmp_path: Path) -> None: hopper().save(buffer_method, format="WEBP", method=6) assert buffer_no_args.getbuffer() != buffer_method.getbuffer() - @skip_unless_feature("webp_anim") def test_save_all(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") + temp_file = 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]) @@ -130,12 +122,16 @@ def test_save_all(self, tmp_path: Path) -> None: reloaded.seek(1) assert_image_similar(im2, reloaded, 1) + def test_unsupported_image_mode(self) -> None: + im = Image.new("1", (1, 1)) + with pytest.raises(ValueError): + _webp.WebPEncode(im.getim(), False, 0, 0, "", 4, 0, b"", "") + def test_icc_profile(self, tmp_path: Path) -> None: 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} - ) + self._roundtrip( + tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} + ) def test_write_unsupported_mode_L(self, tmp_path: Path) -> None: """ @@ -155,40 +151,43 @@ def test_write_unsupported_mode_P(self, tmp_path: Path) -> None: @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_message(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (15000, 15000)) + with pytest.raises(ValueError, match="encoding error 6"): + im.save(tmp_path / "temp.webp", method=0) + + @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") + def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None: + im = Image.new("L", (16384, 16384)) with pytest.raises(ValueError) as e: - im.save(temp_file, method=0) - assert str(e.value) == "encoding error 6" + im.save(tmp_path / "temp.webp") + assert ( + str(e.value) + == "encoding error 5: Image size exceeds WebP limit of 16383 pixels" + ) def test_WebPEncode_with_invalid_args(self) -> None: """ Calling encoder functions with no arguments should result in an error. """ - - if _webp.HAVE_WEBPANIM: - with pytest.raises(TypeError): - _webp.WebPAnimEncoder() + with pytest.raises(TypeError): + _webp.WebPAnimEncoder() with pytest.raises(TypeError): _webp.WebPEncode() - def test_WebPDecode_with_invalid_args(self) -> None: + def test_WebPAnimDecoder_with_invalid_args(self) -> None: """ Calling decoder functions with no arguments should result in an error. """ - - if _webp.HAVE_WEBPANIM: - with pytest.raises(TypeError): - _webp.WebPAnimDecoder() with pytest.raises(TypeError): - _webp.WebPDecode() + _webp.WebPAnimDecoder() def test_no_resource_warning(self, tmp_path: Path) -> None: 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) + warnings.simplefilter("error") + + image.save(tmp_path / "temp.webp") def test_file_pointer_could_be_reused(self) -> None: file_path = "Tests/images/hopper.webp" @@ -200,46 +199,45 @@ def test_file_pointer_could_be_reused(self) -> None: "background", (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)), ) - @skip_unless_feature("webp_anim") def test_invalid_background( self, background: int | tuple[int, ...], tmp_path: Path ) -> None: - temp_file = str(tmp_path / "temp.webp") + temp_file = 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: Path) -> None: + out_webp = tmp_path / "temp.webp" + # 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)) + assert isinstance(original_value, tuple) # 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") + out_gif = 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 isinstance(reread_value, tuple) + difference = sum(abs(original_value[i] - reread_value[i]) for i in range(3)) assert difference < 5 - @skip_unless_feature("webp_anim") def test_duration(self, tmp_path: Path) -> None: + out_webp = tmp_path / "temp.webp" + 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: @@ -247,9 +245,10 @@ def test_duration(self, tmp_path: Path) -> None: assert reloaded.info["duration"] == 1000 def test_roundtrip_rgba_palette(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" im = Image.new("RGBA", (1, 1)).convert("P") assert im.mode == "P" + assert im.palette is not None assert im.palette.mode == "RGBA" im.save(temp_file) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index c7445212110..b1aa45f6b52 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -13,12 +13,7 @@ hopper, ) -_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") - - -def setup_module() -> None: - if _webp.WebPDecoderBuggyAlpha(): - pytest.skip("Buggy early version of WebP installed, not testing transparency") +pytest.importorskip("PIL._webp", reason="WebP support not installed") def test_read_rgba() -> None: @@ -34,7 +29,6 @@ def test_read_rgba() -> None: assert image.size == (200, 150) assert image.format == "WEBP" image.load() - image.getdata() image.tobytes() @@ -47,7 +41,7 @@ def test_write_lossless_rgb(tmp_path: Path) -> None: Does it have the bits we expect? """ - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" # temp_file = "temp.webp" pil_image = hopper("RGBA") @@ -65,7 +59,6 @@ def test_write_lossless_rgb(tmp_path: Path) -> None: assert image.size == pil_image.size assert image.format == "WEBP" image.load() - image.getdata() assert_image_equal(image, pil_image) @@ -76,14 +69,11 @@ def test_write_rgba(tmp_path: Path) -> None: Does it have the bits we expect? """ - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) pil_image.save(temp_file) - if _webp.WebPDecoderBuggyAlpha(): - return - with Image.open(temp_file) as image: image.load() @@ -91,14 +81,8 @@ def test_write_rgba(tmp_path: Path) -> None: assert image.size == (10, 10) assert image.format == "WEBP" image.load() - image.getdata() - # Early versions of WebP are known to produce higher deviations: - # deal with it - if _webp.WebPDecoderVersion() <= 0x201: - assert_image_similar(image, pil_image, 3.0) - else: - assert_image_similar(image, pil_image, 1.0) + assert_image_similar(image, pil_image, 1.0) def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None: @@ -117,7 +101,7 @@ def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None: half_transparent_image.putalpha(new_alpha) # save with transparent area preserved - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" half_transparent_image.save(temp_file, exact=True, lossless=True) with Image.open(temp_file) as reloaded: @@ -136,7 +120,7 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None: should work, and be similar to the original file. """ - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" file_path = "Tests/images/transparent.gif" with Image.open(file_path) as im: im.save(temp_file) @@ -146,7 +130,6 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None: assert image.format == "WEBP" image.load() - image.getdata() with Image.open(file_path) as im: target = im.convert("RGBA") @@ -155,10 +138,10 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None: def test_alpha_quality(tmp_path: Path) -> None: with Image.open("Tests/images/transparent.png") as im: - out = str(tmp_path / "temp.webp") + out = tmp_path / "temp.webp" im.save(out) - out_quality = str(tmp_path / "quality.webp") + out_quality = tmp_path / "quality.webp" im.save(out_quality, alpha_quality=50) with Image.open(out) as reloaded: with Image.open(out_quality) as reloaded_quality: diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 882dccb32f9..600448fb9e1 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -1,33 +1,33 @@ from __future__ import annotations +from collections.abc import Generator from pathlib import Path import pytest -from packaging.version import parse as parse_version -from PIL import Image, features +from PIL import GifImagePlugin, Image, WebPImagePlugin from .helper import ( assert_image_equal, assert_image_similar, + has_feature_version, is_big_endian, skip_unless_feature, ) -pytestmark = [ - skip_unless_feature("webp"), - skip_unless_feature("webp_anim"), -] +pytestmark = skip_unless_feature("webp") def test_n_frames() -> None: """Ensure that WebP format sets n_frames and is_animated attributes correctly.""" with Image.open("Tests/images/hopper.webp") as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 1 assert not im.is_animated with Image.open("Tests/images/iss634.webp") as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 42 assert im.is_animated @@ -39,11 +39,13 @@ def test_write_animation_L(tmp_path: Path) -> None: """ with Image.open("Tests/images/iss634.gif") as orig: + assert isinstance(orig, GifImagePlugin.GifImageFile) assert orig.n_frames > 1 - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" orig.save(temp_file, save_all=True) with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == orig.n_frames # Compare first and last frames to the original animated GIF @@ -51,11 +53,8 @@ def test_write_animation_L(tmp_path: Path) -> None: im.load() assert_image_similar(im, orig.convert("RGBA"), 32.9) - if is_big_endian(): - version = features.version_module("webp") - assert version is not None - if parse_version(version) < parse_version("1.2.2"): - pytest.skip("Fails with libwebp earlier than 1.2.2") + if is_big_endian() and not has_feature_version("webp", "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() @@ -69,8 +68,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None: are visually similar to the originals. """ - def check(temp_file: str) -> None: + def check(temp_file: Path) -> None: with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 2 # Compare first frame to original @@ -78,28 +78,27 @@ def check(temp_file: str) -> None: assert_image_equal(im, frame1.convert("RGBA")) # Compare second frame to original - if is_big_endian(): - version = features.version_module("webp") - assert version is not None - if parse_version(version) < parse_version("1.2.2"): - pytest.skip("Fails with libwebp earlier than 1.2.2") + if is_big_endian() and not has_feature_version("webp", "1.2.2"): + pytest.skip("Fails with libwebp earlier than 1.2.2") im.seek(1) im.load() 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") + temp_file1 = 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): + def im_generator( + ims: list[Image.Image], + ) -> Generator[Image.Image, None, None]: yield from ims - temp_file2 = str(tmp_path / "temp_generator.webp") + temp_file2 = tmp_path / "temp_generator.webp" frame1.copy().save( temp_file2, save_all=True, @@ -116,7 +115,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None: """ durations = [0, 10, 20, 30, 40] - temp_file = str(tmp_path / "temp.webp") + temp_file = 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( @@ -127,6 +126,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None: ) with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 5 assert im.is_animated @@ -141,7 +141,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None: def test_float_duration(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" with Image.open("Tests/images/iss634.apng") as im: assert im.info["duration"] == 70.0 @@ -159,7 +159,7 @@ def test_seeking(tmp_path: Path) -> None: """ dur = 33 - temp_file = str(tmp_path / "temp.webp") + temp_file = 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( @@ -170,6 +170,7 @@ def test_seeking(tmp_path: Path) -> None: ) with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 5 assert im.is_animated @@ -196,10 +197,10 @@ def test_alpha_quality(tmp_path: Path) -> None: with Image.open("Tests/images/transparent.png") as im: first_frame = Image.new("L", im.size) - out = str(tmp_path / "temp.webp") + out = tmp_path / "temp.webp" first_frame.save(out, save_all=True, append_images=[im]) - out_quality = str(tmp_path / "quality.webp") + out_quality = tmp_path / "quality.webp" first_frame.save( out_quality, save_all=True, append_images=[im], alpha_quality=50 ) diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 32e29de56ad..b4c0448ac75 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -8,15 +8,12 @@ from .helper import assert_image_equal, hopper -_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") +pytest.importorskip("PIL._webp", reason="WebP support not installed") RGB_MODE = "RGB" def test_write_lossless_rgb(tmp_path: Path) -> None: - if _webp.WebPDecoderVersion() < 0x0200: - pytest.skip("lossless not included") - - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" hopper(RGB_MODE).save(temp_file, lossless=True) @@ -27,6 +24,5 @@ def test_write_lossless_rgb(tmp_path: Path) -> None: assert image.size == (128, 128) assert image.format == "WEBP" image.load() - image.getdata() assert_image_equal(image, hopper(RGB_MODE)) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index c3df4ad7bcb..3de412b832b 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -6,14 +6,11 @@ import pytest -from PIL import Image +from PIL import Image, WebPImagePlugin from .helper import mark_if_feature_version, skip_unless_feature -pytestmark = [ - skip_unless_feature("webp"), - skip_unless_feature("webp_mux"), -] +pytestmark = skip_unless_feature("webp") ElementTree: ModuleType | None try: @@ -25,11 +22,13 @@ def test_read_exif_metadata() -> None: file_path = "Tests/images/flower.webp" with Image.open(file_path) as image: + assert isinstance(image, WebPImagePlugin.WebPImageFile) assert image.format == "WEBP" exif_data = image.info.get("exif", None) assert exif_data exif = image._getexif() + assert exif is not None # Camera make assert exif[271] == "Canon" @@ -43,7 +42,7 @@ def test_read_exif_metadata() -> None: def test_read_exif_metadata_without_prefix() -> None: with Image.open("Tests/images/flower2.webp") as im: # Assert prefix is not present - assert im.info["exif"][:6] != b"Exif\x00\x00" + assert not im.info["exif"].startswith(b"Exif\x00\x00") exif = im.getexif() assert exif[305] == "Adobe Photoshop CS6 (Macintosh)" @@ -113,13 +112,22 @@ def test_read_no_exif() -> None: test_buffer.seek(0) with Image.open(test_buffer) as webp_image: + assert isinstance(webp_image, WebPImagePlugin.WebPImageFile) assert not webp_image._getexif() def test_getxmp() -> None: with Image.open("Tests/images/flower.webp") as im: assert "xmp" not in im.info - assert im.getxmp() == {} + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + xmp = im.getxmp() + else: + xmp = im.getxmp() + assert xmp == {} with Image.open("Tests/images/flower2.webp") as im: if ElementTree is None: @@ -136,13 +144,12 @@ def test_getxmp() -> None: ) -@skip_unless_feature("webp_anim") def test_write_animated_metadata(tmp_path: Path) -> None: iccp_data = b"" exif_data = b"" xmp_data = b"" - temp_file = str(tmp_path / "temp.webp") + temp_file = 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( diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 79e707263d6..56901f46b07 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,5 +1,6 @@ from __future__ import annotations +from io import BytesIO from pathlib import Path from typing import IO @@ -7,7 +8,7 @@ from PIL import Image, ImageFile, WmfImagePlugin -from .helper import assert_image_similar_tofile, hopper +from .helper import assert_image_equal_tofile, assert_image_similar_tofile, hopper def test_load_raw() -> None: @@ -17,7 +18,7 @@ def test_load_raw() -> None: # Currently, support for WMF/EMF is Windows-only im.load() # Compare to reference rendering - assert_image_similar_tofile(im, "Tests/images/drawing_emf_ref.png", 0) + assert_image_equal_tofile(im, "Tests/images/drawing_emf_ref.png") # Test basic WMF open and rendering with Image.open("Tests/images/drawing.wmf") as im: @@ -31,7 +32,37 @@ def test_load_raw() -> None: def test_load() -> None: with Image.open("Tests/images/drawing.emf") as im: if hasattr(Image.core, "drawwmf"): - assert im.load()[0, 0] == (255, 255, 255) + px = im.load() + assert px is not None + assert px[0, 0] == (255, 255, 255) + + +def test_load_zero_inch() -> None: + b = BytesIO(b"\xd7\xcd\xc6\x9a\x00\x00" + b"\x00" * 10) + with pytest.raises(ValueError): + with Image.open(b): + pass + + +def test_load_unsupported_wmf() -> None: + b = BytesIO(b"\xd7\xcd\xc6\x9a\x00\x00" + b"\x01" * 10) + with pytest.raises(SyntaxError, match="Unsupported WMF file format"): + WmfImagePlugin.WmfStubImageFile(b) + + +def test_load_unsupported() -> None: + b = BytesIO(b"\x01\x00\x00\x00") + with pytest.raises(SyntaxError, match="Unsupported file format"): + WmfImagePlugin.WmfStubImageFile(b) + + +def test_render() -> None: + with open("Tests/images/drawing.emf", "rb") as fp: + data = fp.read() + b = BytesIO(data[:808] + b"\x00" + data[809:]) + with Image.open(b) as im: + if hasattr(Image.core, "drawwmf"): + assert_image_equal_tofile(im, "Tests/images/drawing.emf") def test_register_handler(tmp_path: Path) -> None: @@ -49,7 +80,7 @@ def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: WmfImagePlugin.register_handler(handler) im = hopper() - tmpfile = str(tmp_path / "temp.wmf") + tmpfile = tmp_path / "temp.wmf" im.save(tmpfile) assert handler.methodCalled @@ -61,9 +92,16 @@ def test_load_float_dpi() -> None: with Image.open("Tests/images/drawing.emf") as im: assert im.info["dpi"] == 1423.7668161434979 + with open("Tests/images/drawing.emf", "rb") as fp: + data = fp.read() + b = BytesIO(data[:8] + b"\x06\xfa" + data[10:]) + with Image.open(b) as im: + assert im.info["dpi"][0] == 2540 + def test_load_set_dpi() -> None: with Image.open("Tests/images/drawing.wmf") as im: + assert isinstance(im, WmfImagePlugin.WmfStubImageFile) assert im.size == (82, 82) if hasattr(Image.core, "drawwmf"): @@ -72,11 +110,27 @@ def test_load_set_dpi() -> None: assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1) + with Image.open("Tests/images/drawing.emf") as im: + assert im.size == (1625, 1625) + + if not hasattr(Image.core, "drawwmf"): + return + assert isinstance(im, WmfImagePlugin.WmfStubImageFile) + im.load(im.info["dpi"]) + assert im.size == (1625, 1625) + + with Image.open("Tests/images/drawing.emf") as im: + assert isinstance(im, WmfImagePlugin.WmfStubImageFile) + im.load((72, 144)) + assert im.size == (82, 164) + + assert_image_equal_tofile(im, "Tests/images/drawing_emf_ref_72_144.png") + @pytest.mark.parametrize("ext", (".wmf", ".emf")) def test_save(ext: str, tmp_path: Path) -> None: im = hopper() - tmpfile = str(tmp_path / ("temp" + ext)) + tmpfile = 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 44dd2541fb5..154f3dcc061 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -73,7 +73,7 @@ def test_invalid_file() -> None: def test_save_wrong_mode(tmp_path: Path) -> None: im = hopper() - out = str(tmp_path / "temp.xbm") + out = tmp_path / "temp.xbm" with pytest.raises(OSError): im.save(out) @@ -81,7 +81,7 @@ def test_save_wrong_mode(tmp_path: Path) -> None: def test_hotspot(tmp_path: Path) -> None: im = hopper("1") - out = str(tmp_path / "temp.xbm") + out = tmp_path / "temp.xbm" hotspot = (0, 7) im.save(out, hotspot=hotspot) diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 26afe93f450..86d86602f17 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,5 +1,7 @@ from __future__ import annotations +from io import BytesIO + import pytest from PIL import Image, XpmImagePlugin @@ -17,7 +19,45 @@ def test_sanity() -> None: assert im.format == "XPM" # large error due to quantization->44 colors. - assert_image_similar(im.convert("RGB"), hopper("RGB"), 60) + assert_image_similar(im.convert("RGB"), hopper(), 23) + + +def test_bpp2() -> None: + with Image.open("Tests/images/hopper_bpp2.xpm") as im: + assert_image_similar(im.convert("RGB"), hopper(), 11) + + +def test_rgb() -> None: + with Image.open("Tests/images/hopper_rgb.xpm") as im: + assert im.mode == "RGB" + assert_image_similar(im, hopper(), 16) + + +def test_truncated_header() -> None: + data = b"/* XPM */" + with pytest.raises(SyntaxError, match="broken XPM file"): + with XpmImagePlugin.XpmImageFile(BytesIO(data)): + pass + + +def test_cannot_read_color() -> None: + with open(TEST_FILE, "rb") as fp: + data = fp.read().split(b"#")[0] + with pytest.raises(ValueError, match="cannot read this XPM file"): + with Image.open(BytesIO(data)): + pass + + with pytest.raises(ValueError, match="cannot read this XPM file"): + with Image.open(BytesIO(data + b"invalid")): + pass + + +def test_not_enough_image_data() -> None: + with open(TEST_FILE, "rb") as fp: + data = fp.read().split(b"/* pixels */")[0] + with Image.open(BytesIO(data)) as im: + with pytest.raises(ValueError, match="not enough image data"): + im.load() def test_invalid_file() -> None: @@ -30,6 +70,7 @@ def test_invalid_file() -> None: def test_load_read() -> None: # Arrange with Image.open(TEST_FILE) as im: + assert isinstance(im, XpmImagePlugin.XpmImageFile) dummy_bytes = 1 # Act diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index 136070f9e7d..b4155c8793b 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -1,5 +1,7 @@ from __future__ import annotations +import io + import pytest from PIL import BdfFontFile, FontFile @@ -8,13 +10,20 @@ def test_sanity() -> None: - with open(filename, "rb") as test_file: - font = BdfFontFile.BdfFontFile(test_file) + with open(filename, "rb") as fp: + font = BdfFontFile.BdfFontFile(fp) assert isinstance(font, FontFile.FontFile) assert len([_f for _f in font.glyph if _f]) == 190 +def test_zero_width_chars() -> None: + with open(filename, "rb") as fp: + data = fp.read() + data = data[:2650] + b"\x00\x00" + data[2652:] + BdfFontFile.BdfFontFile(io.BytesIO(data)) + + def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index b82340ef70b..72a0f353477 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -1,15 +1,12 @@ from __future__ import annotations -import pytest - from PIL import Image, ImageDraw, ImageFont -from .helper import skip_unless_feature - class TestFontCrash: def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None: - # from fuzzers.fuzz_font + # Copy of the code from fuzz_font() in Tests/oss-fuzz/fuzzers.py + # that triggered a problem when fuzzing font.getbbox("ABC") font.getmask("test text") with Image.new(mode="RGBA", size=(200, 200)) as im: @@ -17,8 +14,6 @@ def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None: draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2) draw.text((10, 10), "Test Text", font=font, fill="#000") - @skip_unless_feature("freetype2") def test_segfault(self) -> None: - with pytest.raises(OSError): - font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") - self._fuzz_font(font) + font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") + self._fuzz_font(font) diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index ab8a7f9ecca..a5da76faa0b 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + from PIL import Image, ImageDraw, ImageFont, _util from .helper import PillowLeakTestCase, features, skip_unless_feature @@ -7,11 +9,7 @@ original_core = ImageFont.core -class TestTTypeFontLeak(PillowLeakTestCase): - # fails at iteration 3 in main - iterations = 10 - mem_limit = 4096 # k - +class TestFontLeak(PillowLeakTestCase): def _test_font(self, font: ImageFont.FreeTypeFont | ImageFont.ImageFont) -> None: im = Image.new("RGB", (255, 255), "white") draw = ImageDraw.ImageDraw(im) @@ -21,23 +19,29 @@ def _test_font(self, font: ImageFont.FreeTypeFont | ImageFont.ImageFont) -> None ) ) + +class TestTTypeFontLeak(TestFontLeak): + # fails at iteration 3 in main + iterations = 10 + mem_limit = 4096 # k + @skip_unless_feature("freetype2") def test_leak(self) -> None: ttype = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) self._test_font(ttype) -class TestDefaultFontLeak(TestTTypeFontLeak): +class TestDefaultFontLeak(TestFontLeak): # fails at iteration 37 in main iterations = 100 mem_limit = 1024 # k - def test_leak(self) -> None: + def test_leak(self, monkeypatch: pytest.MonkeyPatch) -> None: if features.check_module("freetype2"): - ImageFont.core = _util.DeferredError(ImportError("Disabled for testing")) - try: - default_font = ImageFont.load_default() - finally: - ImageFont.core = original_core - + monkeypatch.setattr( + ImageFont, + "core", + _util.DeferredError(ImportError("Disabled for testing")), + ) + default_font = ImageFont.load_default() self._test_font(default_font) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 997809e463b..321dd85603a 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -2,6 +2,7 @@ import os from pathlib import Path +from typing import AnyStr import pytest @@ -9,7 +10,6 @@ from .helper import ( assert_image_equal_tofile, - assert_image_similar_tofile, skip_unless_feature, ) @@ -72,14 +72,24 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None: 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) + assert_image_equal_tofile(im, "Tests/images/test_draw_pbm_target.png") + + +def test_to_imagefont() -> None: + with open(fontname, "rb") as test_file: + pcffont = PcfFontFile.PcfFontFile(test_file) + imagefont = pcffont.to_imagefont() + im = Image.new("L", (130, 30), "white") + draw = ImageDraw.Draw(im) + draw.text((0, 0), message, "black", font=imagefont) + assert_image_equal_tofile(im, "Tests/images/test_draw_pbm_target.png") def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) for i in range(255): - (ox, oy, dx, dy) = font.getbbox(chr(i)) + ox, oy, dx, dy = font.getbbox(chr(i)) assert ox == 0 assert oy == 0 assert dy == 20 @@ -92,14 +102,14 @@ def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None: def _test_high_characters( - request: pytest.FixtureRequest, tmp_path: Path, message: str | bytes + request: pytest.FixtureRequest, tmp_path: Path, message: AnyStr ) -> None: 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) + assert_image_equal_tofile(im, "Tests/images/high_ascii_chars.png") def test_high_characters(request: pytest.FixtureRequest, tmp_path: Path) -> None: diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index 895458d9d82..6ebaa35ffa8 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -10,7 +10,6 @@ from .helper import ( assert_image_equal_tofile, - assert_image_similar_tofile, skip_unless_feature, ) @@ -85,7 +84,7 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> 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) + assert_image_equal_tofile(im, charsets[encoding]["image1"]) @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) @@ -95,7 +94,7 @@ def test_textsize( tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) for i in range(255): - (ox, oy, dx, dy) = font.getbbox(bytearray([i])) + ox, oy, dx, dy = font.getbbox(bytearray([i])) assert ox == 0 assert oy == 0 assert dy == 20 diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py index 206499a047f..1a9069fd892 100644 --- a/Tests/test_fontfile.py +++ b/Tests/test_fontfile.py @@ -1,15 +1,44 @@ from __future__ import annotations +from io import BytesIO from pathlib import Path import pytest -from PIL import FontFile +from PIL import FontFile, Image + + +def test_puti16() -> None: + fp = BytesIO() + FontFile.puti16(fp, (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) + assert fp.getvalue() == ( + b"\x00\x00\x00\x01\x00\x02\x00\x03\x00\x04" + b"\x00\x05\x00\x06\x00\x07\x00\x08\x00\t" + ) + + +def test_compile() -> None: + font = FontFile.FontFile() + font.glyph[0] = ((0, 0), (0, 0, 0, 0), (0, 0, 0, 1), Image.new("L", (0, 0))) + font.compile() + assert font.ysize == 1 + + font.ysize = 2 + font.compile() + + # Assert that compiling again did not change anything + assert font.ysize == 2 def test_save(tmp_path: Path) -> None: tempname = str(tmp_path / "temp.pil") font = FontFile.FontFile() - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="No bitmap created"): font.save(tempname) + + +def test_to_imagefont() -> None: + font = FontFile.FontFile() + with pytest.raises(ValueError, match="No bitmap created"): + font.to_imagefont() diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index c07024a2c5d..2aeff5c34c3 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -2,12 +2,15 @@ import colorsys import itertools -from typing import Callable from PIL import Image from .helper import assert_image_similar, hopper +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + def int_to_float(i: int) -> float: return i / 255 @@ -22,28 +25,26 @@ def test_sanity() -> None: Image.new("HSV", (100, 100)) -def wedge() -> Image.Image: - w = Image._wedge() - w90 = w.rotate(90) +def linear_gradient() -> Image.Image: + im = Image.linear_gradient(mode="L") + im90 = im.rotate(90) - (px, h) = w.size + px, h = im.size r = Image.new("L", (px * 3, h)) g = r.copy() b = r.copy() - r.paste(w, (0, 0)) - r.paste(w90, (px, 0)) - - g.paste(w90, (0, 0)) - g.paste(w, (2 * px, 0)) + r.paste(im, (0, 0)) + r.paste(im90, (px, 0)) - b.paste(w, (px, 0)) - b.paste(w90, (2 * px, 0)) + g.paste(im90, (0, 0)) + g.paste(im, (2 * px, 0)) - img = Image.merge("RGB", (r, g, b)) + b.paste(im, (px, 0)) + b.paste(im90, (2 * px, 0)) - return img + return Image.merge("RGB", (r, g, b)) def to_xxx_colorsys( @@ -53,7 +54,7 @@ def to_xxx_colorsys( ) -> Image.Image: # convert the hard way using the library colorsys routines. - (r, g, b) = im.split() + r, g, b = im.split() conv_func = int_to_float @@ -79,8 +80,8 @@ def to_rgb_colorsys(im: Image.Image) -> Image.Image: return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") -def test_wedge() -> None: - src = wedge().resize((3 * 32, 32), Image.Resampling.BILINEAR) +def test_linear_gradient() -> None: + src = linear_gradient().resize((3 * 32, 32), Image.Resampling.BILINEAR) im = src.convert("HSV") comparable = to_hsv_colorsys(src) diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index 4fcc37e88cc..5f4a704f213 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -13,15 +13,15 @@ def test_white() -> None: k = i.getpixel((0, 0)) - L = i.getdata(0) - a = i.getdata(1) - b = i.getdata(2) + L = i.get_flattened_data(0) + a = i.get_flattened_data(1) + b = i.get_flattened_data(2) assert k == (255, 128, 128) - assert list(L) == [255] * 100 - assert list(a) == [128] * 100 - assert list(b) == [128] * 100 + assert L == (255,) * 100 + assert a == (128,) * 100 + assert b == (128,) * 100 def test_green() -> None: diff --git a/Tests/test_image.py b/Tests/test_image.py index 93f9b9833ce..32c79919595 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -19,6 +19,7 @@ ImageDraw, ImageFile, ImagePalette, + ImageShow, UnidentifiedImageError, features, ) @@ -30,10 +31,10 @@ assert_image_similar_tofile, assert_not_all_same, hopper, - is_big_endian, is_win32, mark_if_feature_version, skip_unless_feature, + timeout_unless_slower_valgrind, ) ElementTree: ModuleType | None @@ -42,38 +43,34 @@ except ImportError: ElementTree = None - -# Deprecation helper -def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): - return Image.new(mode, size) - else: - return Image.new(mode, size) +PrettyPrinter: type | None +try: + from IPython.lib.pretty import PrettyPrinter +except ImportError: + PrettyPrinter = None class TestImage: - @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) + @pytest.mark.parametrize("mode", Image.MODES) def test_image_modes_success(self, mode: str) -> None: - helper_image_new(mode, (1, 1)) + Image.new(mode, (1, 1)) @pytest.mark.parametrize("mode", ("", "bad", "very very long")) def test_image_modes_fail(self, mode: str) -> None: - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="unrecognized image mode"): Image.new(mode, (1, 1)) - assert str(e.value) == "unrecognized image mode" def test_exception_inheritance(self) -> None: assert issubclass(UnidentifiedImageError, OSError) def test_sanity(self) -> None: im = Image.new("L", (100, 100)) - assert repr(im)[:45] == " None: # with pytest.raises(MemoryError): # Image.new("L", (1000000, 1000000)) + @pytest.mark.skipif(PrettyPrinter is None, reason="IPython is not installed") def test_repr_pretty(self) -> None: - class Pretty: - def text(self, text: str) -> None: - self.pretty_output = text - im = Image.new("L", (100, 100)) - p = Pretty() - im._repr_pretty_(p, None) - assert p.pretty_output == "" + output = io.StringIO() + assert PrettyPrinter is not None + p = PrettyPrinter(output) + im._repr_pretty_(p, False) + assert output.getvalue() == "" def test_open_formats(self) -> None: PNGFILE = "Tests/images/hopper.png" @@ -136,8 +132,8 @@ def test_open_verbose_failure(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(Image, "WARN_POSSIBLE_FORMATS", True) im = io.BytesIO(b"") - with pytest.warns(UserWarning): - with pytest.raises(UnidentifiedImageError): + with pytest.raises(UnidentifiedImageError): + with pytest.warns(UserWarning, match="opening failed"): with Image.open(im): pass @@ -155,6 +151,10 @@ def test_set_mode(self) -> None: with pytest.raises(AttributeError): im.mode = "P" # type: ignore[misc] + def test_empty_path(self) -> None: + with pytest.raises(FileNotFoundError): + Image.open("") + def test_invalid_image(self) -> None: im = io.BytesIO(b"") with pytest.raises(UnidentifiedImageError): @@ -171,6 +171,13 @@ def test_stringio(self) -> None: with Image.open(io.StringIO()): # type: ignore[arg-type] pass + def test_string(self, tmp_path: Path) -> None: + out = str(tmp_path / "temp.png") + im = hopper() + im.save(out) + with Image.open(out) as reloaded: + assert_image_equal(im, reloaded) + def test_pathlib(self, tmp_path: Path) -> None: with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: assert im.mode == "P" @@ -183,16 +190,13 @@ def test_pathlib(self, tmp_path: Path) -> None: 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)) + im.save(tmp_path / ("temp." + ext)) def test_fp_name(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.jpg") + temp_file = tmp_path / "temp.jpg" class FP(io.BytesIO): - name: str + name: Path if sys.version_info >= (3, 12): from collections.abc import Buffer @@ -222,10 +226,10 @@ def test_tempfile(self) -> None: assert_image_similar(im, reloaded, 20) def test_unknown_extension(self, tmp_path: Path) -> None: - im = hopper() - temp_file = str(tmp_path / "temp.unknown") - with pytest.raises(ValueError): - im.save(temp_file) + temp_file = tmp_path / "temp.unknown" + with hopper() as im: + with pytest.raises(ValueError): + im.save(temp_file) def test_internals(self) -> None: im = Image.new("L", (100, 100)) @@ -243,13 +247,22 @@ def test_internals(self) -> None: reason="Test requires opening an mmaped file for writing", ) def test_readonly_save(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.bmp") + temp_file = 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_save_without_changing_readonly(self, tmp_path: Path) -> None: + temp_file = tmp_path / "temp.bmp" + + with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: + assert im.readonly + + im.save(temp_file) + assert im.readonly + def test_dump(self, tmp_path: Path) -> None: im = Image.new("L", (10, 10)) im._dump(str(tmp_path / "temp_L.ppm")) @@ -271,33 +284,6 @@ def test_comparison_with_other_type(self) -> None: assert item is not None assert item != num - def test_expand_x(self) -> None: - # Arrange - im = hopper() - orig_size = im.size - xmargin = 5 - - # Act - im = im._expand(xmargin) - - # Assert - assert im.size[0] == orig_size[0] + 2 * xmargin - assert im.size[1] == orig_size[1] + 2 * xmargin - - def test_expand_xy(self) -> None: - # Arrange - im = hopper() - orig_size = im.size - xmargin = 5 - ymargin = 3 - - # Act - im = im._expand(xmargin, ymargin) - - # Assert - assert im.size[0] == orig_size[0] + 2 * xmargin - assert im.size[1] == orig_size[1] + 2 * ymargin - def test_getbands(self) -> None: # Assert assert hopper("RGB").getbands() == ("R", "G", "B") @@ -372,8 +358,40 @@ def test_alpha_composite(self) -> None: img = Image.alpha_composite(dst, src) # Assert - img_colors = sorted(img.getcolors()) - assert img_colors == expected_colors + img_colors = img.getcolors() + assert img_colors is not None + assert sorted(img_colors) == expected_colors + + def test_alpha_composite_la(self) -> None: + # Arrange + expected_colors = sorted( + [ + (3300, (255, 255)), + (1156, (170, 192)), + (1122, (128, 255)), + (1089, (0, 0)), + (1122, (255, 128)), + (1122, (0, 128)), + (1089, (0, 255)), + ] + ) + + dst = Image.new("LA", size=(100, 100), color=(0, 255)) + draw = ImageDraw.Draw(dst) + draw.rectangle((0, 33, 100, 66), fill=(0, 128)) + draw.rectangle((0, 67, 100, 100), fill=(0, 0)) + src = Image.new("LA", size=(100, 100), color=(255, 255)) + draw = ImageDraw.Draw(src) + draw.rectangle((33, 0, 66, 100), fill=(255, 128)) + draw.rectangle((67, 0, 100, 100), fill=(255, 0)) + + # Act + img = Image.alpha_composite(dst, src) + + # Assert + img_colors = img.getcolors() + assert img_colors is not None + assert sorted(img_colors) == expected_colors def test_alpha_inplace(self) -> None: src = Image.new("RGBA", (128, 128), "blue") @@ -438,9 +456,11 @@ def test_register_open_duplicates(self) -> None: # Assert assert len(Image.ID) == id_length - def test_registered_extensions_uninitialized(self) -> None: + def test_registered_extensions_uninitialized( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: # Arrange - Image._initialized = 0 + monkeypatch.setattr(Image, "_initialized", 0) # Act Image.registered_extensions() @@ -448,6 +468,9 @@ def test_registered_extensions_uninitialized(self) -> None: # Assert assert Image._initialized == 2 + for extension in Image.EXTENSION: + assert extension in Image._EXTENSION_PLUGIN + def test_registered_extensions(self) -> None: # Arrange # Open an image to trigger plugin registration @@ -554,10 +577,7 @@ def test_check_size(self) -> None: i = Image.new("RGB", [1, 1]) assert isinstance(i.size, tuple) - @pytest.mark.timeout(0.75) - @pytest.mark.skipif( - "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" - ) + @timeout_unless_slower_valgrind(0.75) @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) def test_empty_image(self, size: tuple[int, int]) -> None: Image.new("RGB", size) @@ -574,8 +594,7 @@ def test_storage_neg(self) -> None: def test_one_item_tuple(self) -> None: for mode in ("I", "F", "L"): im = Image.new(mode, (100, 100), (5,)) - px = im.load() - assert px[0, 0] == 5 + assert im.getpixel((0, 0)) == 5 def test_linear_gradient_wrong_mode(self) -> None: # Arrange @@ -599,8 +618,8 @@ def test_linear_gradient(self, mode: str) -> None: 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) + im_target = target.convert(mode) + assert_image_equal(im, im_target) def test_radial_gradient_wrong_mode(self) -> None: # Arrange @@ -624,8 +643,8 @@ def test_radial_gradient(self, mode: str) -> None: 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) + im_target = target.convert(mode) + assert_image_equal(im, im_target) def test_register_extensions(self) -> None: test_format = "a" @@ -649,18 +668,20 @@ def test_remap_palette(self) -> None: assert_image_equal(im, im.remap_palette(list(range(256)))) # Test identity transform with an RGBA palette - im = Image.new("P", (256, 1)) + im_p = 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 + im_p.putpixel((x, 0), x) + im_p.putpalette(list(range(256)) * 4, "RGBA") + im_remapped = im_p.remap_palette(list(range(256))) + assert_image_equal(im_p, im_remapped) + assert im_p.palette is not None + assert im_remapped.palette is not None + assert im_p.palette.palette == im_remapped.palette.palette # Test illegal image mode - with hopper() as im: + with hopper() as im_hopper: with pytest.raises(ValueError): - im.remap_palette(None) + im_hopper.remap_palette([]) def test_remap_palette_transparency(self) -> None: im = Image.new("P", (1, 2), (0, 0, 0)) @@ -669,7 +690,9 @@ def test_remap_palette_transparency(self) -> None: im_remapped = im.remap_palette([1, 0]) assert im_remapped.info["transparency"] == 1 - assert len(im_remapped.getpalette()) == 6 + palette = im_remapped.getpalette() + assert palette is not None + assert len(palette) == 6 # Test unused transparency im.info["transparency"] = 2 @@ -696,11 +719,12 @@ def _make_new( assert new_image.size == image.size assert new_image.info == base_image.info if palette_result is not None: + assert new_image.palette is not None assert new_image.palette.tobytes() == palette_result.tobytes() else: assert new_image.palette is None - _make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3)) + _make_new(im, im_p, ImagePalette.ImagePalette("RGB")) _make_new(im_p, im, None) _make_new(im, blank_p, ImagePalette.ImagePalette()) _make_new(im, blank_pa, ImagePalette.ImagePalette()) @@ -722,15 +746,17 @@ def test_no_resource_warning_on_save(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/835 # Arrange test_file = "Tests/images/hopper.png" - temp_file = str(tmp_path / "temp.jpg") + temp_file = tmp_path / "temp.jpg" # Act/Assert with Image.open(test_file) as im: with warnings.catch_warnings(): + warnings.simplefilter("error") + im.save(temp_file) def test_no_new_file_on_error(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.jpg") + temp_file = tmp_path / "temp.jpg" im = Image.new("RGB", (0, 0)) with pytest.raises(ValueError): @@ -758,13 +784,33 @@ def test_empty_exif(self) -> None: assert dict(exif) # Test that exif data is cleared after another load - exif.load(None) + exif.load(b"") assert not dict(exif) # Test loading just the EXIF header exif.load(b"Exif\x00\x00") assert not dict(exif) + def test_duplicate_exif_header(self) -> None: + with Image.open("Tests/images/exif.png") as im: + im.load() + im.info["exif"] = b"Exif\x00\x00" + im.info["exif"] + + exif = im.getexif() + assert exif[274] == 1 + + def test_empty_get_ifd(self) -> None: + exif = Image.Exif() + ifd = exif.get_ifd(0x8769) + assert ifd == {} + + ifd[36864] = b"0220" + assert exif.get_ifd(0x8769) == {36864: b"0220"} + + reloaded_exif = Image.Exif() + reloaded_exif.load(exif.tobytes()) + assert reloaded_exif.get_ifd(0x8769) == {36864: b"0220"} + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) @@ -777,7 +823,7 @@ def test_exif_jpeg(self, tmp_path: Path) -> None: assert exif[296] == 2 assert exif[11] == "gThumb 3.0.1" - out = str(tmp_path / "temp.jpg") + out = tmp_path / "temp.jpg" exif[258] = 8 del exif[274] del exif[282] @@ -799,7 +845,7 @@ def test_exif_jpeg(self, tmp_path: Path) -> None: assert exif[274] == 1 assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)" - out = str(tmp_path / "temp.jpg") + out = tmp_path / "temp.jpg" exif[258] = 8 del exif[306] exif[274] = 455 @@ -813,13 +859,12 @@ def test_exif_jpeg(self, tmp_path: Path) -> None: assert reloaded_exif[305] == "Pillow test" @skip_unless_feature("webp") - @skip_unless_feature("webp_anim") def test_exif_webp(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper.webp") as im: exif = im.getexif() assert exif == {} - out = str(tmp_path / "temp.webp") + out = tmp_path / "temp.webp" exif[258] = 8 exif[40963] = 455 exif[305] = "Pillow test" @@ -841,7 +886,7 @@ def test_exif_png(self, tmp_path: Path) -> None: exif = im.getexif() assert exif == {274: 1} - out = str(tmp_path / "temp.png") + out = tmp_path / "temp.png" exif[258] = 8 del exif[274] exif[40963] = 455 @@ -887,6 +932,17 @@ def test_exif_ifd(self) -> None: reloaded_exif.load(exif.tobytes()) assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769) + def test_delete_ifd_tag(self) -> None: + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + exif.get_ifd(0x8769) + assert 0x8769 in exif + del exif[0x8769] + + reloaded_exif = Image.Exif() + reloaded_exif.load(exif.tobytes()) + assert 0x8769 not in reloaded_exif + def test_exif_load_from_fp(self) -> None: with Image.open("Tests/images/flower.jpg") as im: data = im.info["exif"] @@ -933,15 +989,28 @@ def test_exif_hide_offsets(self) -> None: assert tag not in exif.get_ifd(0x8769) assert exif.get_ifd(0xA005) + def test_exif_from_xmp_bytes(self) -> None: + im = Image.new("RGB", (1, 1)) + im.info["xmp"] = b'\xff tiff:Orientation="2"' + assert im.getexif()[274] == 2 + def test_empty_xmp(self) -> None: with Image.open("Tests/images/hopper.gif") as im: - assert im.getxmp() == {} + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + xmp = im.getxmp() + else: + xmp = im.getxmp() + assert xmp == {} def test_getxmp_padded(self) -> None: im = Image.new("RGB", (1, 1)) im.info["xmp"] = ( b'\n' - b'\n\x00\x00' + b'\n\x00\x00 ' ) if ElementTree is None: with pytest.warns( @@ -952,6 +1021,18 @@ def test_getxmp_padded(self) -> None: else: assert im.getxmp() == {"xmpmeta": None} + def test_get_child_images(self) -> None: + im = Image.new("RGB", (1, 1)) + with pytest.warns(DeprecationWarning, match="Image.Image.get_child_images"): + assert im.get_child_images() == [] + + def test_show(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(ImageShow, "_viewers", []) + + im = Image.new("RGB", (1, 1)) + with pytest.warns(DeprecationWarning, match="Image._show"): + Image._show(im) + @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) def test_zero_tobytes(self, size: tuple[int, int]) -> None: im = Image.new("RGB", size) @@ -986,12 +1067,14 @@ def test_has_transparency_data(self) -> None: # P mode with RGBA palette im = Image.new("RGBA", (1, 1)).convert("P") assert im.mode == "P" + assert im.palette is not None assert im.palette.mode == "RGBA" assert im.has_transparency_data def test_apply_transparency(self) -> None: im = Image.new("P", (1, 1)) im.putpalette((0, 0, 0, 1, 1, 1)) + assert im.palette is not None assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1} # Test that no transformation is applied without transparency @@ -1009,15 +1092,24 @@ def test_apply_transparency(self) -> None: im.putpalette((0, 0, 0, 255, 1, 1, 1, 128), "RGBA") im.info["transparency"] = 0 im.apply_transparency() + assert im.palette is not None 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 is not None assert im.palette.colors[(27, 35, 6)] == 24 im.apply_transparency() + assert im.palette is not None assert im.palette.colors[(27, 35, 6, 214)] == 24 + def test_merge_pa(self) -> None: + p = hopper("P") + a = Image.new("L", p.size) + pa = Image.merge("PA", (p, a)) + assert p.getpalette() == pa.getpalette() + def test_constants(self) -> None: for enum in ( Image.Transpose, @@ -1048,22 +1140,17 @@ def test_overrun(self, path: str) -> None: 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: + with pytest.raises(OSError) as e: im.load() - pytest.fail() - except OSError as e: - buffer_overrun = str(e) == "buffer overrun when reading image file" - truncated = "image file is truncated" in str(e) + buffer_overrun = str(e.value) == "buffer overrun when reading image file" + truncated = "image file is truncated" in str(e.value) - assert buffer_overrun or truncated + assert buffer_overrun or truncated def test_fli_overrun2(self) -> None: with Image.open("Tests/images/fli_overrun2.bin") as im: - try: + with pytest.raises(OSError, match="buffer overrun when reading image file"): im.seek(1) - pytest.fail() - except OSError as e: - assert str(e) == "buffer overrun when reading image file" def test_exit_fp(self) -> None: with Image.new("L", (1, 1)) as im: @@ -1081,34 +1168,28 @@ def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None: class TestImageBytes: - @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) + @pytest.mark.parametrize("mode", Image.MODES) def test_roundtrip_bytes_constructor(self, mode: str) -> None: im = hopper(mode) source_bytes = im.tobytes() - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): - reloaded = Image.frombytes(mode, im.size, source_bytes) - else: - reloaded = Image.frombytes(mode, im.size, source_bytes) + reloaded = Image.frombytes(mode, im.size, source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) + @pytest.mark.parametrize("mode", Image.MODES) def test_roundtrip_bytes_method(self, mode: str) -> None: im = hopper(mode) source_bytes = im.tobytes() - reloaded = helper_image_new(mode, im.size) + reloaded = Image.new(mode, im.size) reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) - def test_getdata_putdata(self, mode: str) -> None: - if is_big_endian() and mode == "BGR;15": - pytest.xfail("Known failure of BGR;15 on big-endian") + @pytest.mark.parametrize("mode", Image.MODES) + def test_get_flattened_data_putdata(self, mode: str) -> None: im = hopper(mode) - reloaded = helper_image_new(mode, im.size) - reloaded.putdata(im.getdata()) + reloaded = Image.new(mode, im.size) + reloaded.putdata(im.get_flattened_data()) assert_image_equal(im, reloaded) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 8abb1f69fc1..6470ac9fc6f 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -12,19 +12,6 @@ from .helper import assert_image_equal, hopper, is_win32 -# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 -# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 -cffi: ModuleType | None -if os.environ.get("PYTHONOPTIMIZE") == "2": - cffi = None -else: - try: - import cffi - - from PIL import PyAccess - except ImportError: - cffi = None - numpy: ModuleType | None try: import numpy @@ -32,21 +19,7 @@ numpy = None -class AccessTest: - # Initial value - _init_cffi_access = Image.USE_CFFI_ACCESS - _need_cffi_access = False - - @classmethod - def setup_class(cls) -> None: - Image.USE_CFFI_ACCESS = cls._need_cffi_access - - @classmethod - def teardown_class(cls) -> None: - Image.USE_CFFI_ACCESS = cls._init_cffi_access - - -class TestImagePutPixel(AccessTest): +class TestImagePutPixel: def test_sanity(self) -> None: im1 = hopper() im2 = Image.new(im1.mode, im1.size, 0) @@ -54,7 +27,9 @@ def test_sanity(self) -> None: for y in range(im1.size[1]): for x in range(im1.size[0]): pos = x, y - im2.putpixel(pos, im1.getpixel(pos)) + value = im1.getpixel(pos) + assert value is not None + im2.putpixel(pos, value) assert_image_equal(im1, im2) @@ -64,7 +39,9 @@ def test_sanity(self) -> None: for y in range(im1.size[1]): for x in range(im1.size[0]): pos = x, y - im2.putpixel(pos, im1.getpixel(pos)) + value = im1.getpixel(pos) + assert value is not None + im2.putpixel(pos, value) assert not im2.readonly assert_image_equal(im1, im2) @@ -74,10 +51,12 @@ def test_sanity(self) -> None: pix1 = im1.load() pix2 = im2.load() + assert pix1 is not None + assert pix2 is not None with pytest.raises(TypeError): - pix1[0, "0"] + pix1[0, "0"] # type: ignore[index] with pytest.raises(TypeError): - pix1["0", 0] + pix1["0", 0] # type: ignore[index] for y in range(im1.size[1]): for x in range(im1.size[0]): @@ -96,7 +75,9 @@ def test_sanity_negative_index(self) -> None: 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)) + value = im1.getpixel(pos) + assert value is not None + im2.putpixel(pos, value) assert_image_equal(im1, im2) @@ -106,7 +87,9 @@ def test_sanity_negative_index(self) -> None: 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)) + value = im1.getpixel(pos) + assert value is not None + im2.putpixel(pos, value) assert not im2.readonly assert_image_equal(im1, im2) @@ -116,6 +99,8 @@ def test_sanity_negative_index(self) -> None: pix1 = im1.load() pix2 = im2.load() + assert pix1 is not None + assert pix2 is not None 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] @@ -125,28 +110,22 @@ def test_sanity_negative_index(self) -> None: @pytest.mark.skipif(numpy is None, reason="NumPy not installed") def test_numpy(self) -> None: im = hopper() - pix = im.load() + px = im.load() + assert px is not None assert numpy is not None - assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59) + assert px[numpy.int32(1), numpy.int32(2)] == (18, 20, 59) -class TestImageGetPixel(AccessTest): +class TestImageGetPixel: @staticmethod def color(mode: str) -> int | tuple[int, ...]: bands = Image.getmodebands(mode) if bands == 1: return 1 - if mode in ("BGR;15", "BGR;16"): - # These modes have less than 8 bits per band, - # so (1, 2, 3) cannot be roundtripped. - return (16, 32, 49) return tuple(range(1, bands + 1)) def check(self, mode: str, expected_color_int: int | None = None) -> None: - if self._need_cffi_access and mode.startswith("BGR;"): - pytest.skip("Support not added to deprecated module for BGR;* modes") - expected_color = ( self.color(mode) if expected_color_int is None else expected_color_int ) @@ -171,15 +150,14 @@ def check(self, mode: str, expected_color_int: int | None = None) -> None: # Check 0x0 image with None initial color im = Image.new(mode, (0, 0), None) assert im.load() is not None - error = ValueError if self._need_cffi_access else IndexError - with pytest.raises(error): + with pytest.raises(IndexError): im.putpixel((0, 0), expected_color) - with pytest.raises(error): + with pytest.raises(IndexError): im.getpixel((0, 0)) # Check negative index - with pytest.raises(error): + with pytest.raises(IndexError): im.putpixel((-1, -1), expected_color) - with pytest.raises(error): + with pytest.raises(IndexError): im.getpixel((-1, -1)) # Check initial color @@ -199,21 +177,16 @@ def check(self, mode: str, expected_color_int: int | None = None) -> None: # Check 0x0 image with initial color im = Image.new(mode, (0, 0), expected_color) - with pytest.raises(error): + with pytest.raises(IndexError): im.getpixel((0, 0)) # Check negative index - with pytest.raises(error): + with pytest.raises(IndexError): im.getpixel((-1, -1)) @pytest.mark.parametrize("mode", Image.MODES) def test_basic(self, mode: str) -> None: self.check(mode) - @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) - def test_deprecated(self, mode: str) -> None: - with pytest.warns(DeprecationWarning): - self.check(mode) - def test_list(self) -> None: im = hopper() assert im.getpixel([0, 0]) == (20, 20, 70) @@ -235,127 +208,8 @@ def test_p_putpixel_rgb_rgba(self, mode: str, color: tuple[int, ...]) -> None: assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) -@pytest.mark.filterwarnings("ignore::DeprecationWarning") -@pytest.mark.skipif(cffi is None, reason="No CFFI") -class TestCffiPutPixel(TestImagePutPixel): - _need_cffi_access = True - - -@pytest.mark.filterwarnings("ignore::DeprecationWarning") -@pytest.mark.skipif(cffi is None, reason="No CFFI") -class TestCffiGetPixel(TestImageGetPixel): - _need_cffi_access = True - - -@pytest.mark.skipif(cffi is None, reason="No CFFI") -class TestCffi(AccessTest): - _need_cffi_access = True - - def _test_get_access(self, im: Image.Image) -> None: - """Do we get the same thing as the old pixel access - - Using private interfaces, forcing a capi access and - a pyaccess for the same image""" - caccess = im.im.pixel_access(False) - with pytest.warns(DeprecationWarning): - access = PyAccess.new(im, False) - assert access is not None - - w, h = im.size - for x in range(0, w, 10): - for y in range(0, h, 10): - assert access[(x, y)] == caccess[(x, y)] - - # Access an out-of-range pixel - with pytest.raises(ValueError): - access[(access.xsize + 1, access.ysize + 1)] - - def test_get_vs_c(self) -> None: - with pytest.warns(DeprecationWarning): - rgb = hopper("RGB") - rgb.load() - self._test_get_access(rgb) - for mode in ("RGBA", "L", "LA", "1", "P", "F"): - self._test_get_access(hopper(mode)) - - for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): - im = Image.new(mode, (10, 10), 40000) - self._test_get_access(im) - - def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None: - """Are we writing the correct bits into the image? - - Using private interfaces, forcing a capi access and - a pyaccess for the same image""" - caccess = im.im.pixel_access(False) - with pytest.warns(DeprecationWarning): - access = PyAccess.new(im, False) - assert access is not None - - w, h = im.size - for x in range(0, w, 10): - for y in range(0, h, 10): - access[(x, y)] = color - assert color == caccess[(x, y)] - - # Attempt to set the value on a read-only image - with pytest.warns(DeprecationWarning): - access = PyAccess.new(im, True) - assert access is not None - - with pytest.raises(ValueError): - access[(0, 0)] = color - - def test_set_vs_c(self) -> None: - rgb = hopper("RGB") - with pytest.warns(DeprecationWarning): - 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("PA"), (128, 128)) - self._test_set_access(hopper("F"), 1024.0) - - for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): - im = Image.new(mode, (10, 10), 40000) - self._test_set_access(im, 45000) - - @pytest.mark.filterwarnings("ignore::DeprecationWarning") - def test_not_implemented(self) -> None: - assert PyAccess.new(hopper("BGR;15")) is None - - # Ref https://github.com/python-pillow/Pillow/pull/2009 - def test_reference_counting(self) -> None: - size = 10 - - for _ in range(10): - # Do not save references to the image, only to the access object - with pytest.warns(DeprecationWarning): - px = Image.new("L", (size, 1), 0).load() - for i in range(size): - # Pixels can contain garbage if image is released - assert px[i, 0] == 0 - - @pytest.mark.parametrize("mode", ("P", "PA")) - def test_p_putpixel_rgb_rgba(self, mode: str) -> None: - for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)): - im = Image.new(mode, (1, 1)) - with pytest.warns(DeprecationWarning): - access = PyAccess.new(im, False) - assert access is not None - - access.putpixel((0, 0), color) - - if len(color) == 3: - color += (255,) - assert im.convert("RGBA").getpixel((0, 0)) == color - - -class TestImagePutPixelError(AccessTest): - IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"] +class TestImagePutPixelError: + IMAGE_MODES1 = ["LA", "RGB", "RGBA"] IMAGE_MODES2 = ["L", "I", "I;16"] INVALID_TYPES = ["foo", 1.0, None] @@ -364,18 +218,13 @@ def test_putpixel_type_error1(self, mode: str) -> None: 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) + im.putpixel((0, 0), v) # type: ignore[arg-type] @pytest.mark.parametrize( - ("mode", "band_numbers", "match"), + "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"), - ( - "BGR;15", - (0, 2), - "color must be int, or tuple of one or three elements", - ), ( "RGB", (0, 2, 5), @@ -398,7 +247,7 @@ def test_putpixel_type_error2(self, mode: str) -> None: with pytest.raises( TypeError, match="color must be int or single-element tuple" ): - im.putpixel((0, 0), v) + im.putpixel((0, 0), v) # type: ignore[arg-type] @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) def test_putpixel_overflow_error(self, mode: str) -> None: @@ -408,17 +257,28 @@ def test_putpixel_overflow_error(self, mode: str) -> None: class TestEmbeddable: - @pytest.mark.xfail(reason="failing test") + @pytest.mark.xfail(not (sys.version_info >= (3, 13)), reason="failing test") @pytest.mark.skipif(not is_win32(), reason="requires Windows") def test_embeddable(self) -> None: import ctypes from setuptools.command import build_ext + compiler = getattr(build_ext, "new_compiler")() + compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY")) + + libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var( + "INCLUDEPY" + ).replace("include", "libs") + compiler.add_library_dir(libdir) + try: + compiler.initialize() + except Exception: + pytest.skip("Compiler could not be initialized") + with open("embed_pil.c", "w", encoding="utf-8") as fh: home = sys.prefix.replace("\\", "\\\\") - fh.write( - f""" + fh.write(f""" #include "Python.h" int main(int argc, char* argv[]) @@ -439,16 +299,8 @@ def test_embeddable(self) -> None: return 0; }} - """ - ) + """) - compiler = getattr(build_ext, "new_compiler")() - compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY")) - - 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") @@ -461,3 +313,10 @@ def test_embeddable(self) -> None: process = subprocess.Popen(["embed_pil.exe"], env=env) process.communicate() assert process.returncode == 0 + + def teardown_method(self) -> None: + try: + os.remove("embed_pil.c") + except FileNotFoundError: + # If the test was skipped or failed, the file won't exist + pass diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index bb606488286..220e128d168 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import pytest from packaging.version import parse as parse_version @@ -13,6 +13,7 @@ im = hopper().resize((128, 100)) +TYPE_CHECKING = False if TYPE_CHECKING: import numpy.typing as npt @@ -24,7 +25,7 @@ def test(mode: str) -> tuple[tuple[int, ...], str, int]: def test_with_dtype(dtype: npt.DTypeLike) -> None: ai = numpy.array(im, dtype=dtype) - assert ai.dtype == dtype + assert ai.dtype.type is dtype # assert test("1") == ((100, 128), '|b1', 1600)) assert test("L") == ((100, 128), "|u1", 12800) @@ -47,7 +48,7 @@ def test_with_dtype(dtype: npt.DTypeLike) -> None: with pytest.raises(OSError): numpy.array(im_truncated) else: - with pytest.warns(UserWarning): + with pytest.warns(DeprecationWarning, match="__array_interface__"): numpy.array(im_truncated) @@ -77,7 +78,7 @@ def test(mode: str) -> tuple[str, tuple[int, int], bool]: }, ) out = Image.fromarray(wrapped) - return out.mode, out.size, list(i.getdata()) == list(out.getdata()) + return out.mode, out.size, i.get_flattened_data() == out.get_flattened_data() # assert test("1") == ("1", (128, 100), True) assert test("L") == ("L", (128, 100), True) @@ -100,7 +101,7 @@ def __init__(self, arr_params: dict[str, Any]) -> None: self.__array_interface__ = arr_params with pytest.raises(ValueError): - wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)}) + wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1), "typestr": "|u1"}) Image.fromarray(wrapped, "L") @@ -113,4 +114,13 @@ def test_fromarray_palette() -> None: out = Image.fromarray(a, "P") # Assert that the Python and C palettes match + assert out.palette is not None assert len(out.palette.colors) == len(out.im.getpalette()) / 3 + + +def test_deprecation() -> None: + a = numpy.array(im.convert("L")) + with pytest.warns( + DeprecationWarning, match="'mode' parameter for changing data types" + ): + Image.fromarray(a, "1") diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 2fb45854adf..547a6c2c678 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -80,8 +80,8 @@ def test_16bit() -> None: _test_float_conversion(im) for color in (65535, 65536): - im = Image.new("I", (1, 1), color) - im_i16 = im.convert("I;16") + im_i = Image.new("I", (1, 1), color) + im_i16 = im_i.convert("I;16") assert im_i16.getpixel((0, 0)) == 65535 @@ -97,6 +97,13 @@ def test_opaque() -> None: assert_image_equal(alpha, solid) +def test_rgba() -> None: + with Image.open("Tests/images/transparent.png") as im: + assert im.mode == "RGBA" + + assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5) + + def test_rgba_p() -> None: im = hopper("RGBA") im.putalpha(hopper("L")) @@ -107,18 +114,26 @@ def test_rgba_p() -> None: assert_image_similar(im, comparable, 20) -def test_rgba() -> None: - with Image.open("Tests/images/transparent.png") as im: - assert im.mode == "RGBA" +def test_rgba_pa() -> None: + im = hopper("RGBA").convert("PA").convert("RGB") + expected = hopper("RGB") - assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5) + assert_image_similar(im, expected, 9.3) + + +def test_pa() -> None: + im = hopper().convert("PA") + + palette = im.palette + assert palette is not None + assert palette.colors != {} def test_trns_p(tmp_path: Path) -> None: im = hopper("P") im.info["transparency"] = 0 - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im_l = im.convert("L") assert im_l.info["transparency"] == 0 @@ -154,7 +169,7 @@ def test_trns_l(tmp_path: Path) -> None: im = hopper("L") im.info["transparency"] = 128 - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im_la = im.convert("LA") assert "transparency" not in im_la.info @@ -177,7 +192,7 @@ def test_trns_RGB(tmp_path: Path) -> None: im = hopper("RGB") im.info["transparency"] = im.getpixel((0, 0)) - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im_l = im.convert("L") assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone @@ -203,7 +218,10 @@ def test_trns_RGB(tmp_path: Path) -> None: assert "transparency" not in im_rgba.info assert im_rgba.getpixel((0, 0)) == (0, 0, 0, 0) - im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.Palette.ADAPTIVE) + with pytest.warns( + UserWarning, match="Couldn't allocate palette entry for transparency" + ): + im_p = im.convert("P", palette=Image.Palette.ADAPTIVE) assert "transparency" not in im_p.info im_p.save(f) @@ -218,12 +236,13 @@ def test_trns_RGB(tmp_path: Path) -> None: def test_l_macro_rounding(convert_mode: str) -> None: for mode in ("P", "PA"): im = Image.new(mode, (1, 1)) + assert im.palette is not None im.palette.getcolor((0, 1, 2)) converted_im = im.convert(convert_mode) - px = converted_im.load() - converted_color = px[0, 0] + converted_color = converted_im.getpixel((0, 0)) if convert_mode == "LA": + assert isinstance(converted_color, tuple) converted_color = converted_color[0] assert converted_color == 1 @@ -233,6 +252,7 @@ def test_gif_with_rgba_palette_to_p() -> None: with Image.open("Tests/images/hopper.gif") as im: im.info["transparency"] = 255 im.load() + assert im.palette is not None assert im.palette.mode == "RGB" im_p = im.convert("P") diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index 027e5338b7a..a0b829cc565 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -49,5 +49,7 @@ def test_copy_zero() -> None: @skip_unless_feature("libtiff") def test_deepcopy() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: + assert im.size == (590, 88) + out = copy.deepcopy(im) assert out.size == (590, 88) diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index 07fec2e64af..9df8883a4f0 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -78,13 +78,13 @@ def test_crop_crash() -> None: extents = (1, 1, 10, 10) # works prepatch with Image.open(test_img) as img: - img2 = img.crop(extents) - img2.load() + img1 = img.crop(extents) + img1.load() # fail prepatch with Image.open(test_img) as img: - img = img.crop(extents) - img.load() + img2 = img.crop(extents) + img2.load() def test_crop_zero() -> None: @@ -95,10 +95,10 @@ def test_crop_zero() -> None: cropped = im.crop((10, 10, 20, 20)) assert cropped.size == (10, 10) - assert cropped.getdata()[0] == (0, 0, 0) + assert cropped.getpixel((0, 0)) == (0, 0, 0) im = Image.new("RGB", (0, 0)) cropped = im.crop((10, 10, 20, 20)) assert cropped.size == (10, 10) - assert cropped.getdata()[2] == (0, 0, 0) + assert cropped.getpixel((2, 0)) == (0, 0, 0) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 412ab44c3c1..e566cd0557a 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -35,16 +35,25 @@ ImageFilter.UnsharpMask(10), ), ) -@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None: +@pytest.mark.parametrize( + "mode", ("L", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK") +) +def test_sanity( + filter_to_apply: ImageFilter.Filter | type[ImageFilter.Filter], mode: str +) -> None: im = hopper(mode) - if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter): + if mode[0] != "I" or ( + callable(filter_to_apply) + and issubclass(filter_to_apply, ImageFilter.BuiltinFilter) + ): out = im.filter(filter_to_apply) assert out.mode == im.mode assert out.size == im.size -@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) +@pytest.mark.parametrize( + "mode", ("L", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK") +) def test_sanity_error(mode: str) -> None: im = hopper(mode) with pytest.raises(TypeError): @@ -145,7 +154,9 @@ def test_kernel_not_enough_coefficients() -> None: ImageFilter.Kernel((3, 3), (0, 0)) -@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) +@pytest.mark.parametrize( + "mode", ("L", "LA", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK") +) def test_consistency_3x3(mode: str) -> None: with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper_emboss.bmp") as reference: @@ -161,7 +172,9 @@ def test_consistency_3x3(mode: str) -> None: assert_image_equal(source.filter(kernel), reference) -@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) +@pytest.mark.parametrize( + "mode", ("L", "LA", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK") +) def test_consistency_5x5(mode: str) -> None: with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py index 8f8870f4fec..8dbe82b29b0 100644 --- a/Tests/test_image_getcolors.py +++ b/Tests/test_image_getcolors.py @@ -54,17 +54,21 @@ def test_pack() -> None: assert A is None A = im.getcolors(maxcolors=3) + assert A is not None A.sort() assert A == expected A = im.getcolors(maxcolors=4) + assert A is not None A.sort() assert A == expected A = im.getcolors(maxcolors=8) + assert A is not None A.sort() assert A == expected A = im.getcolors(maxcolors=16) + assert A is not None A.sort() assert A == expected diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index dd3d70b3450..94d6cbaa2e7 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -1,23 +1,23 @@ from __future__ import annotations +import pytest + from PIL import Image from .helper import hopper def test_sanity() -> None: - data = hopper().getdata() - - len(data) - list(data) + data = hopper().get_flattened_data() + assert len(data) == 128 * 128 assert data[0] == (20, 20, 70) def test_mode() -> None: - def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]: + def getdata(mode: str) -> tuple[float | tuple[int, ...] | None, int, int]: im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST) - data = im.getdata() + data = im.get_flattened_data() return data[0], len(data), len(list(data)) assert getdata("1") == (0, 960, 960) @@ -28,3 +28,13 @@ def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]: 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) + + +def test_deprecation() -> None: + im = hopper() + with pytest.warns(DeprecationWarning, match="getdata"): + data = im.getdata() + + assert len(data) == 128 * 128 + assert data[0] == (20, 20, 70) + assert list(data)[0] == (20, 20, 70) diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index 9afa02b0a8b..07612e5872e 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -5,7 +5,6 @@ def test_sanity() -> None: im = hopper() - type_repr = repr(type(im.getim())) + type_repr = repr(type(im.getim())) assert "PyCapsule" in type_repr - assert isinstance(im.im.id, int) diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index dbd55d4c2d3..436eb78a26e 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -10,9 +10,12 @@ def histogram(mode: str) -> tuple[int, int, int]: assert histogram("1") == (256, 0, 10994) assert histogram("L") == (256, 0, 662) + assert histogram("LA") == (512, 0, 16384) + assert histogram("La") == (512, 0, 16384) assert histogram("I") == (256, 0, 662) assert histogram("F") == (256, 0, 662) assert histogram("P") == (256, 0, 1551) + assert histogram("PA") == (512, 0, 16384) assert histogram("RGB") == (768, 4, 675) assert histogram("RGBA") == (1024, 0, 16384) assert histogram("CMYK") == (1024, 0, 16384) diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 0605821e0ad..1d5f0d17cd6 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -12,9 +12,10 @@ def test_sanity() -> None: im = hopper() - pix = im.load() + px = im.load() - assert pix[0, 0] == (20, 20, 70) + assert px is not None + assert px[0, 0] == (20, 20, 70) def test_close() -> None: @@ -37,6 +38,7 @@ def test_close_after_load(caplog: pytest.LogCaptureFixture) -> None: def test_contextmanager() -> None: fn = None with Image.open("Tests/images/hopper.gif") as im: + assert im.fp is not None fn = im.fp.fileno() os.fstat(fn) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 01cb880cb31..37e4df10370 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -14,6 +14,7 @@ def assert_9points_image( self, im: Image.Image, expected: list[tuple[int, int, int, int]] ) -> None: px = im.load() + assert px is not None actual = [ px[0, 0], px[self.size // 2, 0], @@ -48,6 +49,7 @@ def assert_9points_paste( def mask_1(self) -> Image.Image: mask = Image.new("1", (self.size, self.size)) px = mask.load() + assert px is not None for y in range(mask.height): for x in range(mask.width): px[y, x] = (x + y) % 2 @@ -61,6 +63,7 @@ def mask_L(self) -> Image.Image: def gradient_L(self) -> Image.Image: gradient = Image.new("L", (self.size, self.size)) px = gradient.load() + assert px is not None for y in range(gradient.height): for x in range(gradient.width): px[y, x] = (x + y) % 255 @@ -121,6 +124,21 @@ def test_image_solid(self, mode: str) -> None: im = im.crop((12, 23, im2.width + 12, im2.height + 23)) assert_image_equal(im, im2) + @pytest.mark.parametrize("y", [10, -10]) + @pytest.mark.parametrize("mode", ["L", "RGB"]) + @pytest.mark.parametrize("mask_mode", ["", "1", "L", "LA", "RGBa"]) + def test_image_self(self, y: int, mode: str, mask_mode: str) -> None: + im = getattr(self, "gradient_" + mode) + mask = Image.new(mask_mode, im.size, 0xFFFFFFFF) if mask_mode else None + + im_self = im.copy() + im_self.paste(im_self, (0, y), mask) + + im_copy = im.copy() + im_copy.paste(im_copy.copy(), (0, y), mask) + + assert_image_equal(im_self, im_copy) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_image_mask_1(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index dad26ef144c..226cb4c14d8 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -2,6 +2,7 @@ import sys from array import array +from typing import cast import pytest @@ -12,26 +13,24 @@ def test_sanity() -> None: im1 = hopper() + for data in (im1.get_flattened_data(), im1.im): + im2 = Image.new(im1.mode, im1.size, 0) + im2.putdata(data) - data = list(im1.getdata()) + assert_image_equal(im1, im2) - im2 = Image.new(im1.mode, im1.size, 0) - im2.putdata(data) + # readonly + im2 = Image.new(im1.mode, im2.size, 0) + im2.readonly = 1 + 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) + assert not im2.readonly + assert_image_equal(im1, im2) def test_long_integers() -> None: # see bug-200802-systemerror - def put(value: int) -> tuple[int, int, int, int]: + def put(value: int) -> float | tuple[int, ...] | None: im = Image.new("RGBA", (1, 1)) im.putdata([value]) return im.getpixel((0, 0)) @@ -60,32 +59,22 @@ def test_mode_with_L_with_float() -> None: @pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B")) def test_mode_i(mode: str) -> None: src = hopper("L") - data = list(src.getdata()) + data = src.get_flattened_data() im = Image.new(mode, src.size, 0) im.putdata(data, 2, 256) - target = [2 * elt + 256 for elt in data] - assert list(im.getdata()) == target + target = tuple(2 * elt + 256 for elt in cast(tuple[int, ...], data)) + assert im.get_flattened_data() == target def test_mode_F() -> None: src = hopper("L") - data = list(src.getdata()) + data = src.get_flattened_data() im = Image.new("F", src.size, 0) im.putdata(data, 2.0, 256.0) - target = [2.0 * float(elt) + 256.0 for elt in data] - assert list(im.getdata()) == target - - -@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) -def test_mode_BGR(mode: str) -> None: - data = [(16, 32, 49), (32, 32, 98)] - with pytest.warns(DeprecationWarning): - im = Image.new(mode, (1, 2)) - im.putdata(data) - - assert list(im.getdata()) == data + target = tuple(2.0 * float(elt) + 256.0 for elt in cast(tuple[int, ...], data)) + assert im.get_flattened_data() == target def test_array_B() -> None: @@ -96,7 +85,7 @@ def test_array_B() -> None: im = Image.new("L", (150, 100)) im.putdata(arr) - assert len(im.getdata()) == len(arr) + assert len(im.get_flattened_data()) == len(arr) def test_array_F() -> None: @@ -107,7 +96,7 @@ def test_array_F() -> None: arr = array("f", [0.0]) * 15000 im.putdata(arr) - assert len(im.getdata()) == len(arr) + assert len(im.get_flattened_data()) == len(arr) def test_not_flattened() -> None: diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 75d9d2fc116..237de63305a 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -62,6 +62,7 @@ def test_putpalette_with_alpha_values() -> None: expected = im.convert("RGBA") palette = im.getpalette() + assert palette is not None transparency = im.info.pop("transparency") palette_with_alpha_values = [] @@ -86,6 +87,22 @@ def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None: im = Image.new("P", (1, 1)) im.putpalette(palette, mode) assert im.getpalette() == [1, 2, 3] + assert im.palette is not None + assert im.palette.colors == {(1, 2, 3, 4): 0} + + +@pytest.mark.parametrize( + "mode, palette", + ( + ("CMYK", (1, 2, 3, 4)), + ("CMYKX", (1, 2, 3, 4, 0)), + ), +) +def test_cmyk_palette(mode: str, palette: tuple[int, ...]) -> None: + im = Image.new("P", (1, 1)) + im.putpalette(palette, mode) + assert im.getpalette() == [250, 249, 248] + assert im.palette is not None assert im.palette.colors == {(1, 2, 3, 4): 0} diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 2d461d985dd..8876285605a 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,11 +1,16 @@ from __future__ import annotations import pytest -from packaging.version import parse as parse_version -from PIL import Image, features +from PIL import Image -from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature +from .helper import ( + assert_image_similar, + has_feature_version, + hopper, + is_ppc64le, + skip_unless_feature, +) def test_sanity() -> None: @@ -23,15 +28,14 @@ def test_sanity() -> None: @skip_unless_feature("libimagequant") def test_libimagequant_quantize() -> None: image = hopper() - if is_ppc64le(): - version = features.version_feature("libimagequant") - assert version is not None - if parse_version(version) < parse_version("4"): - pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le") + if is_ppc64le() and not has_feature_version("libimagequant", "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 + colors = converted.getcolors() + assert colors is not None + assert len(colors) == 100 def test_octree_quantize() -> None: @@ -39,7 +43,9 @@ def test_octree_quantize() -> None: converted = image.quantize(100, Image.Quantize.FASTOCTREE) assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 20) - assert len(converted.getcolors()) == 100 + colors = converted.getcolors() + assert colors is not None + assert len(colors) == 100 def test_rgba_quantize() -> None: @@ -52,8 +58,8 @@ def test_rgba_quantize() -> None: def test_quantize() -> None: with Image.open("Tests/images/caption_6_33_22.png") as image: - image = image.convert("RGB") - converted = image.quantize() + converted = image.convert("RGB") + converted = converted.quantize() assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 1) @@ -61,11 +67,13 @@ def test_quantize() -> None: def test_quantize_no_dither() -> None: image = hopper() with Image.open("Tests/images/caption_6_33_22.png") as palette: - palette = palette.convert("P") + palette_p = palette.convert("P") - converted = image.quantize(dither=Image.Dither.NONE, palette=palette) + converted = image.quantize(dither=Image.Dither.NONE, palette=palette_p) assert converted.mode == "P" - assert converted.palette.palette == palette.palette.palette + assert converted.palette is not None + assert palette_p.palette is not None + assert converted.palette.palette == palette_p.palette.palette def test_quantize_no_dither2() -> None: @@ -77,9 +85,11 @@ def test_quantize_no_dither2() -> None: palette.putpalette(data) quantized = im.quantize(dither=Image.Dither.NONE, palette=palette) + assert quantized.palette is not None assert tuple(quantized.palette.palette) == data px = quantized.load() + assert px is not None for x in range(9): assert px[x, 0] == (0 if x < 5 else 1) @@ -87,10 +97,10 @@ def test_quantize_no_dither2() -> None: def test_quantize_dither_diff() -> None: image = hopper() with Image.open("Tests/images/caption_6_33_22.png") as palette: - palette = palette.convert("P") + palette_p = palette.convert("P") - dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette) - nodither = image.quantize(dither=Image.Dither.NONE, palette=palette) + dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette_p) + nodither = image.quantize(dither=Image.Dither.NONE, palette=palette_p) assert dither.tobytes() != nodither.tobytes() @@ -108,20 +118,32 @@ def test_quantize_kmeans(method: Image.Quantize) -> None: im.quantize(kmeans=-1, method=method) +@skip_unless_feature("libimagequant") +def test_resize() -> None: + im = hopper().resize((100, 100)) + converted = im.quantize(100, Image.Quantize.LIBIMAGEQUANT) + colors = converted.getcolors() + assert colors is not None + assert len(colors) == 100 + + def test_colors() -> None: im = hopper() colors = 2 converted = im.quantize(colors) + assert converted.palette is not None assert len(converted.palette.palette) == colors * len("RGB") def test_transparent_colors_equal() -> None: im = Image.new("RGBA", (1, 2), (0, 0, 0, 0)) px = im.load() + assert px is not None px[0, 1] = (255, 255, 255, 0) converted = im.quantize() converted_px = converted.load() + assert converted_px is not None assert converted_px[0, 0] == converted_px[0, 1] @@ -138,8 +160,8 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None: 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] + assert converted.palette is not None + assert converted.getpixel((0, 0)) == converted.palette.colors[color] def test_small_palette() -> None: @@ -154,4 +176,6 @@ def test_small_palette() -> None: im = im.quantize(palette=p) # Assert - assert len(im.getcolors()) == 2 + quantized_colors = im.getcolors() + assert quantized_colors is not None + assert len(quantized_colors) == 2 diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index d6055b577e3..73b25ed51b2 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,7 +1,7 @@ from __future__ import annotations +from collections.abc import Generator from contextlib import contextmanager -from typing import Generator import pytest @@ -74,6 +74,7 @@ def make_sample(self, data: str, size: tuple[int, int]) -> Image.Image: data = data.replace(" ", "") sample = Image.new("L", size) s_px = sample.load() + assert s_px is not None w, h = size[0] // 2, size[1] // 2 for y in range(h): for x in range(w): @@ -87,6 +88,8 @@ def make_sample(self, data: str, size: tuple[int, int]) -> Image.Image: def check_case(self, case: Image.Image, sample: Image.Image) -> None: s_px = sample.load() c_px = case.load() + assert s_px is not None + assert c_px is not None for y in range(case.size[1]): for x in range(case.size[0]): if c_px[x, y] != s_px[x, y]: @@ -98,6 +101,7 @@ def check_case(self, case: Image.Image, sample: Image.Image) -> None: def serialize_image(self, image: Image.Image) -> str: s_px = image.load() + assert s_px is not None return "\n".join( " ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0])) for y in range(image.size[1]) @@ -233,13 +237,16 @@ def test_box_filter_correct_range(self) -> None: class TestCoreResampleConsistency: def make_case( self, mode: str, fill: tuple[int, int, int] | float - ) -> tuple[Image.Image, tuple[int, ...]]: + ) -> tuple[Image.Image, float | tuple[int, ...]]: im = Image.new(mode, (512, 9), fill) - return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] + px = im.load() + assert px is not None + return im.resize((9, 512), Image.Resampling.LANCZOS), px[0, 0] - def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None: + def run_case(self, case: tuple[Image.Image, float | tuple[int, ...]]) -> None: channel, color = case px = channel.load() + assert px is not None for x in range(channel.size[0]): for y in range(channel.size[1]): if px[x, y] != color: @@ -249,6 +256,7 @@ def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None: def test_8u(self) -> None: im, color = self.make_case("RGB", (0, 64, 255)) r, g, b = im.split() + assert isinstance(color, tuple) self.run_case((r, color[0])) self.run_case((g, color[1])) self.run_case((b, color[2])) @@ -271,6 +279,7 @@ class TestCoreResampleAlphaCorrect: def make_levels_case(self, mode: str) -> Image.Image: i = Image.new(mode, (256, 16)) px = i.load() + assert px is not None for y in range(i.size[1]): for x in range(i.size[0]): pix = [x] * len(mode) @@ -280,8 +289,13 @@ def make_levels_case(self, mode: str) -> Image.Image: def run_levels_case(self, i: Image.Image) -> None: px = i.load() + assert px is not None for y in range(i.size[1]): - used_colors = {px[x, y][0] for x in range(i.size[0])} + used_colors = set() + for x in range(i.size[0]): + value = px[x, y] + assert isinstance(value, tuple) + used_colors.add(value[0]) assert 256 == len(used_colors), ( "All colors should be present in resized image. " f"Only {len(used_colors)} on line {y}." @@ -310,6 +324,7 @@ def make_dirty_case( ) -> Image.Image: i = Image.new(mode, (64, 64), dirty_pixel) px = i.load() + assert px is not None xdiv4 = i.size[0] // 4 ydiv4 = i.size[1] // 4 for y in range(ydiv4 * 2): @@ -319,14 +334,16 @@ def make_dirty_case( def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None: px = i.load() + assert px is not None 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: + value = px[x, y] + assert isinstance(value, tuple) + if value[-1] != 0 and value[:-1] != clean_pixel: message = ( - f"pixel at ({x}, {y}) is different:\n" - f"{px[x, y]}\n{clean_pixel}" + f"pixel at ({x}, {y}) is different:\n{value}\n{clean_pixel}" ) - assert px[x, y][:3] == clean_pixel, message + assert value[:3] == clean_pixel, message def test_dirty_pixels_rgba(self) -> None: case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0)) @@ -406,6 +423,7 @@ def test_reduce(self) -> None: draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color) px = i.resize((5, i.size[1]), Image.Resampling.BICUBIC).load() + assert px is not None if px[2, 0] != test_color // 2: assert test_color // 2 == px[2, 0] @@ -444,7 +462,7 @@ def test_wrong_arguments(self, resample: Image.Resampling) -> None: 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"): + with pytest.raises(TypeError, match="must be (sequence|tuple) of length 4"): im.resize((32, 32), resample, (im.width, im.height)) # type: ignore[arg-type] with pytest.raises(ValueError, match="can't be negative"): diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 64098f80fb1..3e8979a5b11 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -4,13 +4,13 @@ from __future__ import annotations +from collections.abc import Generator from itertools import permutations from pathlib import Path -from typing import Generator import pytest -from PIL import Image +from PIL import Image, ImageFile from .helper import ( assert_image_equal, @@ -44,9 +44,19 @@ def test_convolution_modes(self) -> None: 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"]: + for mode in [ + "L", + "I", + "I;16", + "I;16L", + "I;16B", + "I;16N", + "F", + "RGB", + "RGBA", + "CMYK", + "YCbCr", + ]: im = hopper(mode) r = self.resize(im, (15, 12), Image.Resampling.BILINEAR) assert r.mode == mode @@ -150,7 +160,7 @@ def test_enlarge_zero(self, resample: Image.Resampling) -> None: 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) + assert r.getpixel((0, 0)) == (0, 0, 0) def test_unknown_filter(self) -> None: with pytest.raises(ValueError): @@ -161,7 +171,7 @@ def test_cross_platform(self, tmp_path: Path) -> None: # 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") + temp_file = tmp_path / "temp.gif" im.save(temp_file) with Image.open(temp_file) as reloaded: @@ -169,7 +179,7 @@ def test_cross_platform(self, tmp_path: Path) -> None: @pytest.fixture -def gradients_image() -> Generator[Image.Image, None, None]: +def gradients_image() -> Generator[ImageFile.ImageFile, None, None]: with Image.open("Tests/images/radial_gradients.png") as im: im.load() try: @@ -179,7 +189,7 @@ def gradients_image() -> Generator[Image.Image, None, None]: class TestReducingGapResize: - def test_reducing_gap_values(self, gradients_image: Image.Image) -> None: + def test_reducing_gap_values(self, gradients_image: ImageFile.ImageFile) -> None: ref = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, reducing_gap=None ) @@ -200,7 +210,7 @@ def test_reducing_gap_values(self, gradients_image: Image.Image) -> None: ) def test_reducing_gap_1( self, - gradients_image: Image.Image, + gradients_image: ImageFile.ImageFile, box: tuple[float, float, float, float], epsilon: float, ) -> None: @@ -220,7 +230,7 @@ def test_reducing_gap_1( ) def test_reducing_gap_2( self, - gradients_image: Image.Image, + gradients_image: ImageFile.ImageFile, box: tuple[float, float, float, float], epsilon: float, ) -> None: @@ -240,7 +250,7 @@ def test_reducing_gap_2( ) def test_reducing_gap_3( self, - gradients_image: Image.Image, + gradients_image: ImageFile.ImageFile, box: tuple[float, float, float, float], epsilon: float, ) -> None: @@ -256,7 +266,9 @@ def test_reducing_gap_3( @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: Image.Image, box: tuple[float, float, float, float] + self, + gradients_image: ImageFile.ImageFile, + box: tuple[float, float, float, float], ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( @@ -271,7 +283,7 @@ def test_reducing_gap_8( ) def test_box_filter( self, - gradients_image: Image.Image, + gradients_image: ImageFile.ImageFile, box: tuple[float, float, float, float], epsilon: float, ) -> None: @@ -285,36 +297,34 @@ def test_box_filter( class TestImageResize: def test_resize(self) -> None: - def resize(mode: str, size: tuple[int, int]) -> None: + def resize(mode: str, size: tuple[int, int] | list[int]) -> None: out = hopper(mode).resize(size) assert out.mode == mode - assert out.size == size + assert out.size == tuple(size) for mode in "1", "P", "L", "RGB", "I", "F": resize(mode, (112, 103)) - resize(mode, (188, 214)) + resize(mode, [188, 214]) # Test unknown resampling filter with hopper() as im: with pytest.raises(ValueError): - im.resize((10, 10), "unknown") + im.resize((10, 10), -1) @skip_unless_feature("libtiff") - def test_load_first(self) -> None: - # load() may change the size of the image - # Test that resize() is calling it before getting the size + def test_transposed(self) -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: - im = im.resize((64, 64)) - assert im.size == (64, 64) + im_resized = im.resize((64, 64)) + assert im_resized.size == (64, 64) - @pytest.mark.parametrize("mode", ("L", "RGB", "I", "F")) + @pytest.mark.parametrize( + "mode", ("L", "RGB", "I", "I;16", "I;16L", "I;16B", "I;16N", "F") + ) def test_default_filter_bicubic(self, mode: str) -> None: im = hopper(mode) assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) - @pytest.mark.parametrize( - "mode", ("1", "P", "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16") - ) + @pytest.mark.parametrize("mode", ("1", "P")) def test_default_filter_nearest(self, mode: str) -> None: 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 252a15db742..c3ff52f5767 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -43,8 +43,8 @@ def test_angle(angle: int) -> None: 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)) + im_hopper = hopper() + assert_image_equal(im_hopper.rotate(angle), im_hopper.rotate(angle, expand=1)) @pytest.mark.parametrize("angle", (0, 45, 90, 180, 270)) @@ -76,9 +76,9 @@ def test_center_0() -> None: 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_target = target.crop((0, target_origin, 128, target_origin + 128)) - assert_image_similar(im, target, 15) + assert_image_similar(im, im_target, 15) def test_center_14() -> None: @@ -87,22 +87,22 @@ def test_center_14() -> None: 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_target = target.crop((6, target_origin, 128 + 6, target_origin + 128)) - assert_image_similar(im, target, 10) + assert_image_similar(im, im_target, 10) def test_translate() -> None: im = hopper() with Image.open("Tests/images/hopper_45.png") as target: target_origin = (target.size[1] / 2 - 64) - 5 - target = target.crop( + im_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) + assert_image_similar(im, im_target, 1) def test_fastpath_center() -> None: diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index 3385f81f527..43068535e6e 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -45,9 +45,9 @@ def test_split_merge(mode: str) -> None: def test_split_open(tmp_path: Path) -> None: if features.check("zlib"): - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" else: - test_file = str(tmp_path / "temp.pcx") + test_file = tmp_path / "temp.pcx" def split_open(mode: str) -> int: hopper(mode).save(test_file) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index bdbf09c407e..2ae230f3df1 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -92,34 +92,32 @@ def test_no_resize() -> None: @skip_unless_feature("libtiff") -def test_load_first() -> None: - # load() may change the size of the image - # Test that thumbnail() is calling it before performing size calculations +def test_transposed() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: + assert im.size == (590, 88) + 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() -> None: +def test_load_first_unless_jpeg(monkeypatch: pytest.MonkeyPatch) -> None: # Test that thumbnail() still uses draft() for JPEG with Image.open("Tests/images/hopper.jpg") as im: - draft = im.draft + original_draft = im.draft def im_draft( - mode: str, size: tuple[int, int] + mode: str | None, size: tuple[int, int] | None ) -> tuple[str, tuple[int, int, float, float]] | None: - result = draft(mode, size) + result = original_draft(mode, size) assert result is not None return result - im.draft = im_draft + monkeypatch.setattr(im, "draft", im_draft) im.thumbnail((64, 64)) @@ -161,9 +159,9 @@ def test_reducing_gap_for_DCT_scaling() -> None: 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) + im_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) + assert_image_similar(im_ref, im, 1.4) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 638d1224710..12a05ec18b1 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,7 +1,6 @@ from __future__ import annotations import math -from typing import Callable import pytest @@ -9,6 +8,10 @@ from .helper import assert_image_equal, assert_image_similar, hopper +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + class TestImageTransform: def test_sanity(self) -> None: @@ -47,11 +50,13 @@ def test_palette(self) -> None: transformed = im.transform( im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0] ) + assert im.palette is not None + assert transformed.palette is not None assert im.palette.palette == transformed.palette.palette def test_extent(self) -> None: im = hopper("RGB") - (w, h) = im.size + w, h = im.size transformed = im.transform( im.size, Image.Transform.EXTENT, @@ -67,7 +72,7 @@ def test_extent(self) -> None: def test_quad(self) -> None: # one simple quad transform, equivalent to scale & crop upper left quad im = hopper("RGB") - (w, h) = im.size + w, h = im.size transformed = im.transform( im.size, Image.Transform.QUAD, @@ -94,7 +99,7 @@ def test_quad(self) -> None: ) def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None: im = hopper(mode) - (w, h) = im.size + w, h = im.size transformed = im.transform( im.size, Image.Transform.EXTENT, @@ -107,7 +112,7 @@ def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None: def test_mesh(self) -> None: # this should be a checkerboard of halfsized hoppers in ul, lr im = hopper("RGBA") - (w, h) = im.size + w, h = im.size transformed = im.transform( im.size, Image.Transform.MESH, @@ -169,7 +174,7 @@ def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: def test_alpha_premult_transform(self) -> None: def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: - (w, h) = im.size + w, h = im.size return im.transform( sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR ) @@ -192,8 +197,9 @@ def _test_nearest( im = op(im, (40, 10)) - colors = sorted(im.getcolors()) - assert colors == sorted( + colors = im.getcolors() + assert colors is not None + assert sorted(colors) == sorted( ( (20 * 10, opaque), (20 * 10, transparent), @@ -210,7 +216,7 @@ def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: @pytest.mark.parametrize("mode", ("RGBA", "LA")) def test_nearest_transform(self, mode: str) -> None: def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: - (w, h) = im.size + w, h = im.size return im.transform( sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST ) @@ -244,14 +250,14 @@ def test_blank_fill(self) -> None: def test_missing_method_data(self) -> None: with hopper() as im: with pytest.raises(ValueError): - im.transform((100, 100), None) + im.transform((100, 100), None) # type: ignore[arg-type] @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None: with hopper() as im: - (w, h) = im.size + w, h = im.size with pytest.raises(ValueError): - im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) + im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) # type: ignore[arg-type] class TestImageTransformAffine: diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 7e2290c156e..61812ca7dc9 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,11 +1,13 @@ from __future__ import annotations -from typing import Callable - from PIL import Image, ImageChops from .helper import assert_image_equal, hopper +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + BLACK = (0, 0, 0) BROWN = (127, 64, 0) CYAN = (0, 255, 255) @@ -391,23 +393,26 @@ def test_overlay() -> None: def test_logical() -> None: def table( op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int - ) -> tuple[int, int, int, int]: + ) -> list[float]: 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) + value = op(imx, imy).getpixel((0, 0)) + assert not isinstance(value, tuple) + assert value is not None + out.append(value) + return 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 c51f1155d37..a30fb18b80a 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -7,7 +7,7 @@ import sys from io import BytesIO from pathlib import Path -from typing import Any, Literal, cast +from typing import Literal, cast import pytest @@ -31,6 +31,9 @@ # Skipped via setup_module() pass +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Any SRGB = "Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc" HAVE_PROFILE = os.path.exists(SRGB) @@ -54,10 +57,6 @@ def skip_missing() -> None: def test_sanity() -> None: # basic smoke test. # this mostly follows the cms_test outline. - with pytest.warns(DeprecationWarning): - v = ImageCms.versions() # should return four strings - assert v[0] == "1.0.0 pil" - assert list(map(type, v)) == [str, str, str, str] # internal version number version = features.version_module("littlecms2") @@ -212,9 +211,10 @@ def test_exceptions() -> None: ImageCms.getProfileName(None) # type: ignore[arg-type] 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"): + with pytest.raises( + ImageCms.PyCMSError, + match="'NoneType' object cannot be interpreted as an integer", + ): ImageCms.isIntentSupported(SRGB, None, None) # type: ignore[arg-type] @@ -274,13 +274,13 @@ def test_simple_lab() -> None: # 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) + l_data = i_lab.get_flattened_data(0) + a_data = i_lab.get_flattened_data(1) + b_data = i_lab.get_flattened_data(2) - assert list(l_data) == [137] * 100 - assert list(a_data) == [128] * 100 - assert list(b_data) == [128] * 100 + assert l_data == (137,) * 100 + assert a_data == (128,) * 100 + assert b_data == (128,) * 100 def test_lab_color() -> None: @@ -677,12 +677,6 @@ def test_auxiliary_channels_isolated() -> None: assert_image_equal(test_image.convert(dst_format[2]), reference_image) -def test_long_modes() -> None: - p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc") - with pytest.warns(DeprecationWarning): - ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI") - - @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) def test_rgb_lab(mode: str) -> None: im = Image.new(mode, (1, 1)) @@ -691,19 +685,26 @@ def test_rgb_lab(mode: str) -> None: im = Image.new("LAB", (1, 1), (255, 0, 0)) converted_im = im.convert(mode) - assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) + value = converted_im.getpixel((0, 0)) + assert isinstance(value, tuple) + assert value[:3] == (0, 255, 255) -def test_deprecation() -> None: - with pytest.warns(DeprecationWarning): - assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") - with pytest.warns(DeprecationWarning): - assert ImageCms.VERSION == "1.0.0 pil" - with pytest.warns(DeprecationWarning): - assert isinstance(ImageCms.FLAGS, dict) +def test_cmyk_lab() -> None: + im = Image.new("CMYK", (1, 1)) + converted_im = im.convert("LAB") + assert converted_im.getpixel((0, 0)) == (255, 128, 128) + +def test_deprecation() -> None: profile = ImageCmsProfile(ImageCms.createProfile("sRGB")) - with pytest.warns(DeprecationWarning): - ImageCms.ImageCmsTransform(profile, profile, "RGBA;16B", "RGB") - with pytest.warns(DeprecationWarning): - ImageCms.ImageCmsTransform(profile, profile, "RGB", "RGBA;16B") + with pytest.warns( + DeprecationWarning, match="ImageCms.ImageCmsProfile.product_name" + ): + profile.product_name + with pytest.warns( + DeprecationWarning, match="ImageCms.ImageCmsProfile.product_info" + ): + profile.product_info + with pytest.raises(AttributeError): + profile.this_attribute_does_not_exist diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 1466ab1bac8..3bcb7b90178 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,13 +1,10 @@ from __future__ import annotations -import contextlib import os.path -from typing import Sequence import pytest from PIL import Image, ImageColor, ImageDraw, ImageFont, features -from PIL._typing import Coords from .helper import ( assert_image_equal, @@ -17,6 +14,12 @@ skip_unless_feature, ) +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + + from PIL._typing import Coords + BLACK = (0, 0, 0) WHITE = (255, 255, 255) GRAY = (190, 190, 190) @@ -39,6 +42,8 @@ POINTS = ( ((10, 10), (20, 40), (30, 30)), [(10, 10), (20, 40), (30, 30)], + ([10, 10], [20, 40], [30, 30]), + [[10, 10], [20, 40], [30, 30]], (10, 10, 20, 40, 30, 30), [10, 10, 20, 40, 30, 30], ) @@ -46,6 +51,8 @@ KITE_POINTS = ( ((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)), [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)], + ([10, 50], [70, 10], [90, 50], [70, 90], [10, 50]), + [[10, 50], [70, 10], [90, 50], [70, 90], [10, 50]], ) @@ -61,10 +68,22 @@ def test_sanity() -> None: draw.rectangle(list(range(4))) -def test_valueerror() -> None: +def test_new_color() -> None: with Image.open("Tests/images/chi.gif") as im: draw = ImageDraw.Draw(im) + assert im.palette is not None + assert len(im.palette.colors) == 249 + + # Test drawing a new color onto the palette draw.line((0, 0), fill=(0, 0, 0)) + assert im.palette is not None + assert len(im.palette.colors) == 250 + assert im.palette.dirty + + # Test drawing another new color, now that the palette is dirty + draw.point((0, 0), fill=(1, 0, 0)) + assert len(im.palette.colors) == 251 + assert im.convert("RGB").getpixel((0, 0)) == (1, 0, 0) def test_mode_mismatch() -> None: @@ -191,10 +210,10 @@ def test_bitmap() -> None: 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) + small_resized = small.resize((50, 50), Image.Resampling.NEAREST) # Act - draw.bitmap((10, 10), small) + draw.bitmap((10, 10), small_resized) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_bitmap.png") @@ -448,7 +467,6 @@ def test_shape1() -> None: x3, y3 = 95, 5 # Act - assert ImageDraw.Outline is not None s = ImageDraw.Outline() s.move(x0, y0) s.curve(x1, y1, x2, y2, x3, y3) @@ -470,7 +488,6 @@ def test_shape2() -> None: x3, y3 = 5, 95 # Act - assert ImageDraw.Outline is not None s = ImageDraw.Outline() s.move(x0, y0) s.curve(x1, y1, x2, y2, x3, y3) @@ -489,7 +506,6 @@ def test_transform() -> None: draw = ImageDraw.Draw(im) # Act - assert ImageDraw.Outline is not None s = ImageDraw.Outline() s.line(0, 0) s.transform((0, 0, 0, 0, 0, 0)) @@ -782,9 +798,10 @@ def test_rectangle_I16(bbox: Coords) -> None: draw = ImageDraw.Draw(im) # Act - draw.rectangle(bbox, outline=0xFFFF) + draw.rectangle(bbox, outline=0xCDEF) # Assert + assert im.getpixel((X0, Y0)) == 0xCDEF assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff") @@ -812,7 +829,7 @@ def test_rounded_rectangle( tuple[int, int, int, int] | tuple[list[int]] | tuple[tuple[int, int], tuple[int, int]] - ) + ), ) -> None: # Arrange im = Image.new("RGB", (200, 200)) @@ -857,6 +874,39 @@ def test_rounded_rectangle_corners( ) +def test_rounded_rectangle_joined_x_different_corners() -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") + + # Act + draw.rounded_rectangle( + (20, 10, 80, 90), + 30, + fill="red", + outline="green", + width=5, + corners=(True, False, False, False), + ) + + # Assert + assert_image_equal_tofile( + im, "Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png" + ) + + +def test_rounded_rectangle_radius() -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGB") + + # Act + draw.rounded_rectangle((25, 25, 75, 75), 24, fill="red", outline="green", width=5) + + # Assert + assert_image_equal_tofile(im, "Tests/images/imagedraw_rounded_rectangle_radius.png") + + @pytest.mark.parametrize( "xy, radius, type", [ @@ -1026,8 +1076,8 @@ def create_base_image_draw( background2: tuple[int, int, int] = GRAY, ) -> tuple[Image.Image, ImageDraw.ImageDraw]: img = Image.new(mode, size, background1) - for x in range(0, size[0]): - for y in range(0, size[1]): + for x in range(size[0]): + for y in range(size[1]): if (x + y) % 2 == 0: img.putpixel((x, y), background2) return img, ImageDraw.Draw(img) @@ -1347,6 +1397,20 @@ def test_stroke() -> None: ) +@skip_unless_feature("freetype2") +def test_stroke_float() -> None: + # Arrange + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + draw.text((12, 12), "A", "#f00", font, stroke_width=0.5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_float.png", 3.1) + + @skip_unless_feature("freetype2") def test_stroke_descender() -> None: # Arrange @@ -1361,6 +1425,28 @@ def test_stroke_descender() -> None: assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76) +@skip_unless_feature("freetype2") +def test_stroke_inside_gap() -> None: + # Arrange + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + draw.text((12, 12), "i", "#f00", font, stroke_width=20) + + # Assert + for y in range(im.height): + glyph = "" + for x in range(im.width): + if im.getpixel((x, y)) == (0, 0, 0): + if glyph == "started": + glyph = "ended" + else: + assert glyph != "ended", "Gap inside stroked glyph" + glyph = "started" + + @skip_unless_feature("freetype2") def test_split_word() -> None: # Arrange @@ -1399,21 +1485,15 @@ def test_stroke_multiline() -> None: @skip_unless_feature("freetype2") -def test_setting_default_font() -> None: - # Arrange +def test_setting_default_font(monkeypatch: pytest.MonkeyPatch) -> None: im = Image.new("RGB", (100, 250)) draw = ImageDraw.Draw(im) - font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) - - # Act - ImageDraw.ImageDraw.font = font + assert isinstance(draw.getfont(), ImageFont.load_default().__class__) - # Assert - try: - assert draw.getfont() == font - finally: - ImageDraw.ImageDraw.font = None - assert isinstance(draw.getfont(), ImageFont.load_default().__class__) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + monkeypatch.setattr(ImageDraw.ImageDraw, "font", font) + assert draw.getfont() == font def test_default_font_size() -> None: @@ -1422,25 +1502,48 @@ def test_default_font_size() -> None: im = Image.new("RGB", (220, 25)) draw = ImageDraw.Draw(im) - with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): + + def check(func: Callable[[], None]) -> None: + if freetype_support: + func() + else: + with pytest.raises(ImportError): + func() + + def draw_text() -> None: draw.text((0, 0), text, font_size=16) - assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png") + assert_image_similar_tofile( + im, "Tests/images/imagedraw_default_font_size.png", 1 + ) + + check(draw_text) - with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): + def draw_textlength() -> None: assert draw.textlength(text, font_size=16) == 216 - with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): + check(draw_textlength) + + def draw_textbbox() -> None: assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19) + check(draw_textbbox) + im = Image.new("RGB", (220, 25)) draw = ImageDraw.Draw(im) - with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): + + def draw_multiline_text() -> None: draw.multiline_text((0, 0), text, font_size=16) - assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png") + assert_image_similar_tofile( + im, "Tests/images/imagedraw_default_font_size.png", 1 + ) - with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): + check(draw_multiline_text) + + def draw_multiline_textbbox() -> None: assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19) + check(draw_multiline_textbbox) + @pytest.mark.parametrize("bbox", BBOX) def test_same_color_outline(bbox: Coords) -> None: @@ -1450,7 +1553,6 @@ def test_same_color_outline(bbox: Coords) -> None: x2, y2 = 95, 50 x3, y3 = 95, 5 - assert ImageDraw.Outline is not None s = ImageDraw.Outline() s.move(x0, y0) s.curve(x1, y1, x2, y2, x3, y3) @@ -1554,7 +1656,7 @@ def test_compute_regular_polygon_vertices( 0, ValueError, "bounding_circle should contain 2D coordinates " - "and a radius (e.g. (x, y, r) or ((x, y), r) )", + r"and a radius \(e.g. \(x, y, r\) or \(\(x, y\), r\) \)", ), ( 3, @@ -1568,7 +1670,7 @@ def test_compute_regular_polygon_vertices( ((50, 50, 50), 25), 0, ValueError, - "bounding_circle centre should contain 2D coordinates (e.g. (x, y))", + r"bounding_circle centre should contain 2D coordinates \(e.g. \(x, y\)\)", ), ( 3, @@ -1593,9 +1695,8 @@ def test_compute_regular_polygon_vertices_input_error_handling( expected_error: type[Exception], error_message: str, ) -> None: - with pytest.raises(expected_error) as e: + with pytest.raises(expected_error, match=error_message): ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type] - assert str(e.value) == error_message def test_continuous_horizontal_edges_polygon() -> None: @@ -1620,13 +1721,16 @@ def test_continuous_horizontal_edges_polygon() -> None: def test_discontiguous_corners_polygon() -> None: img, draw = create_base_image_draw((84, 68)) draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK) + draw.polygon( + ((82, 29), (82, 26), (82, 24), (67, 22), (52, 29), (52, 15), (67, 22)), 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) + assert_image_equal_tofile(img, expected) def test_polygon2() -> None: @@ -1653,8 +1757,3 @@ def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None: draw.rectangle(xy) with pytest.raises(ValueError): draw.rounded_rectangle(xy) - - -def test_getdraw() -> None: - with pytest.warns(DeprecationWarning): - ImageDraw.getdraw(None, []) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index c80aa739cdf..e0d368228a0 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -65,6 +65,36 @@ def test_mode() -> None: ImageDraw2.Draw("L") +@pytest.mark.parametrize("bbox", BBOX) +@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) +def test_arc(bbox: Coords, start: float, end: float) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("white", width=1) + + # Act + draw.arc(bbox, pen, start, end) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1) + + +@pytest.mark.parametrize("bbox", BBOX) +def test_chord(bbox: Coords) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("yellow") + brush = ImageDraw2.Brush("red") + + # Act + draw.chord(bbox, pen, 0, 180, brush) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_RGB.png", 1) + + @pytest.mark.parametrize("bbox", BBOX) def test_ellipse(bbox: Coords) -> None: # Arrange @@ -123,6 +153,22 @@ def test_line_pen_as_brush(points: Coords) -> None: assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") +@pytest.mark.parametrize("bbox", BBOX) +@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) +def test_pieslice(bbox: Coords, start: float, end: float) -> None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue") + brush = ImageDraw2.Brush("white") + + # Act + draw.pieslice(bbox, pen, start, end, brush) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1) + + @pytest.mark.parametrize("points", POINTS) def test_polygon(points: Coords) -> None: # Arrange diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 68b28ef07e2..5177059055a 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -31,7 +31,7 @@ class TestImageFile: - def test_parser(self) -> None: + def test_parser(self, monkeypatch: pytest.MonkeyPatch) -> None: def roundtrip(format: str) -> tuple[Image.Image, Image.Image]: im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): @@ -55,12 +55,9 @@ def roundtrip(format: str) -> tuple[Image.Image, Image.Image]: assert_image_equal(*roundtrip("IM")) assert_image_equal(*roundtrip("MSP")) if features.check("zlib"): - try: - # force multiple blocks in PNG driver - ImageFile.MAXBLOCK = 8192 - assert_image_equal(*roundtrip("PNG")) - finally: - ImageFile.MAXBLOCK = MAXBLOCK + # force multiple blocks in PNG driver + monkeypatch.setattr(ImageFile, "MAXBLOCK", 8192) + assert_image_equal(*roundtrip("PNG")) assert_image_equal(*roundtrip("PPM")) assert_image_equal(*roundtrip("TIFF")) assert_image_equal(*roundtrip("XBM")) @@ -90,10 +87,23 @@ def test_ico(self) -> None: data = f.read() with ImageFile.Parser() as p: p.feed(data) + assert p.image is not None assert (48, 48) == p.image.size + @pytest.mark.filterwarnings("ignore:Corrupt EXIF data") + def test_incremental_tiff(self) -> None: + with ImageFile.Parser() as p: + with open("Tests/images/hopper.tif", "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 p.image is not None + assert (128, 128) == p.image.size + @skip_unless_feature("webp") - @skip_unless_feature("webp_anim") def test_incremental_webp(self) -> None: with ImageFile.Parser() as p: with open("Tests/images/hopper.webp", "rb") as f: @@ -103,29 +113,42 @@ def test_incremental_webp(self) -> None: assert not p.image p.feed(f.read()) + assert p.image is not None assert (128, 128) == p.image.size @skip_unless_feature("zlib") - def test_safeblock(self) -> None: + def test_safeblock(self, monkeypatch: pytest.MonkeyPatch) -> None: im1 = hopper() - try: - ImageFile.SAFEBLOCK = 1 - im2 = fromstring(tostring(im1, "PNG")) - finally: - ImageFile.SAFEBLOCK = SAFEBLOCK + monkeypatch.setattr(ImageFile, "SAFEBLOCK", 1) + im2 = fromstring(tostring(im1, "PNG")) assert_image_equal(im1, im2) - def test_raise_oserror(self) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(OSError): - ImageFile.raise_oserror(1) + def test_tile_size(self) -> None: + with open("Tests/images/hopper.tif", "rb") as im_fp: + data = im_fp.read() + + reads = [] + + class FP(BytesIO): + def read(self, size: int | None = None) -> bytes: + reads.append(size) + return super().read(size) + + fp = FP(data) + with Image.open(fp) as im: + assert len(im.tile) == 7 + + im.load() + + # Despite multiple tiles, assert only one tile caused a read of maxblock size + assert reads.count(im.decodermaxblock) == 1 def test_raise_typeerror(self) -> None: with pytest.raises(TypeError): parser = ImageFile.Parser() - parser.feed(1) + parser.feed(1) # type: ignore[arg-type] def test_negative_stride(self) -> None: with open("Tests/images/raw_negative_stride.bin", "rb") as f: @@ -135,6 +158,39 @@ def test_negative_stride(self) -> None: with pytest.raises(OSError): p.close() + def test_negative_offset(self) -> None: + with Image.open("Tests/images/raw_negative_stride.bin") as im: + with pytest.raises(ValueError, match="Tile offset cannot be negative"): + im.load() + + @pytest.mark.parametrize("xy", ((-1, 0), (0, -1))) + def test_negative_tile_extents(self, xy: tuple[int, int]) -> None: + im = Image.new("1", (1, 1)) + fp = BytesIO() + with pytest.raises(SystemError, match="tile cannot extend outside image"): + ImageFile._save(im, fp, [ImageFile._Tile("raw", xy + (1, 1), 0, "1")]) + + def test_extents_none(self) -> None: + with Image.open("Tests/images/hopper.jpg") as im: + im.tile = [im.tile[0]._replace(extents=None)] + im.load() + + for extents in ("invalid", (0,), ("0", "0", "0", "0")): + with Image.open("Tests/images/hopper.jpg") as im: + im.tile = [im.tile[0]._replace(extents=extents)] # type: ignore[arg-type] + with pytest.raises(ValueError, match="invalid extents"): + im.load() + + im2 = Image.new("L", (1, 1)) + fp = BytesIO() + tile = ImageFile._Tile("jpeg", None, 0, "L") + ImageFile._save(im2, fp, [tile]) + + for extents in ("invalid", (0,), ("0", "0", "0", "0")): + tile = tile._replace(extents=extents) # type: ignore[arg-type] + with pytest.raises(ValueError, match="invalid extents"): + ImageFile._save(im2, fp, [tile]) + def test_no_format(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -162,9 +218,8 @@ def test_truncated(self) -> None: b"0" * ImageFile.SAFEBLOCK ) # only SAFEBLOCK bytes, so that the header is truncated ) - with pytest.raises(OSError) as e: + with pytest.raises(OSError, match="Truncated File Read"): BmpImagePlugin.BmpImageFile(b) - assert str(e.value) == "Truncated File Read" @skip_unless_feature("zlib") def test_truncated_with_errors(self) -> None: @@ -177,13 +232,10 @@ def test_truncated_with_errors(self) -> None: im.load() @skip_unless_feature("zlib") - def test_truncated_without_errors(self) -> None: + def test_truncated_without_errors(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/truncated_image.png") as im: - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - im.load() - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + im.load() @skip_unless_feature("zlib") def test_broken_datastream_with_errors(self) -> None: @@ -192,13 +244,12 @@ def test_broken_datastream_with_errors(self) -> None: im.load() @skip_unless_feature("zlib") - def test_broken_datastream_without_errors(self) -> None: + def test_broken_datastream_without_errors( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: 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 + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + im.load() class MockPyDecoder(ImageFile.PyDecoder): @@ -209,7 +260,7 @@ def __init__(self, mode: str, *args: Any) -> None: super().__init__(mode, *args) - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: # eof return -1, 0 @@ -237,7 +288,9 @@ def _open(self) -> None: self.rawmode = "RGBA" self._mode = "RGBA" self._size = (200, 200) - self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] + self.tile = [ + ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None) + ] class CodecsTest: @@ -263,49 +316,43 @@ def test_setimage(self) -> None: with pytest.raises(ValueError): MockPyDecoder.last.set_as_raw(b"\x00") - def test_extents_none(self) -> None: + @pytest.mark.parametrize( + "extents", + ( + (-10, yoff, xoff + xsize, yoff + ysize), + (xoff, -10, xoff + xsize, yoff + ysize), + (xoff, yoff, -10, yoff + ysize), + (xoff, yoff, xoff + xsize, -10), + (xoff, yoff, xoff + xsize + 100, yoff + ysize), + (xoff, yoff, xoff + xsize, yoff + ysize + 100), + ), + ) + def test_extents(self, extents: tuple[int, int, int, int]) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - im.tile = [("MOCK", None, 32, None)] - - im.load() - - assert MockPyDecoder.last.state.xoff == 0 - assert MockPyDecoder.last.state.yoff == 0 - assert MockPyDecoder.last.state.xsize == 200 - assert MockPyDecoder.last.state.ysize == 200 + im.tile = [ImageFile._Tile("MOCK", extents, 32, None)] - def test_negsize(self) -> None: - buf = BytesIO(b"\x00" * 255) - - im = MockImageFile(buf) - im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)] - - with pytest.raises(ValueError): - im.load() - - im.tile = [("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)] with pytest.raises(ValueError): im.load() - def test_oversize(self) -> None: + def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)] + im.tile = [ImageFile._Tile("MOCK", None, 32, None)] - with pytest.raises(ValueError): - im.load() + im.load() - im.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None)] - with pytest.raises(ValueError): - im.load() + assert MockPyDecoder.last.state.xoff == 0 + assert MockPyDecoder.last.state.yoff == 0 + assert MockPyDecoder.last.state.xsize == 200 + assert MockPyDecoder.last.state.ysize == 200 def test_decode(self) -> None: - decoder = ImageFile.PyDecoder(None) + decoder = ImageFile.PyDecoder("") with pytest.raises(NotImplementedError): - decoder.decode(None) + decoder.decode(b"") class TestPyEncoder(CodecsTest): @@ -316,7 +363,13 @@ def test_setimage(self) -> None: fp = BytesIO() ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] + im, + fp, + [ + ImageFile._Tile( + "MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB" + ) + ], ) assert MockPyEncoder.last @@ -325,22 +378,18 @@ def test_setimage(self) -> None: assert MockPyEncoder.last.state.xsize == xsize assert MockPyEncoder.last.state.ysize == ysize - def test_extents_none(self) -> None: - buf = BytesIO(b"\x00" * 255) - - im = MockImageFile(buf) - im.tile = [("MOCK", None, 32, None)] - - fp = BytesIO() - ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) - - assert MockPyEncoder.last - assert MockPyEncoder.last.state.xoff == 0 - assert MockPyEncoder.last.state.yoff == 0 - assert MockPyEncoder.last.state.xsize == 200 - assert MockPyEncoder.last.state.ysize == 200 - - def test_negsize(self) -> None: + @pytest.mark.parametrize( + "extents", + ( + (-10, yoff, xoff + xsize, yoff + ysize), + (xoff, -10, xoff + xsize, yoff + ysize), + (xoff, yoff, -10, yoff + ysize), + (xoff, yoff, xoff + xsize, -10), + (xoff, yoff, xoff + xsize + 100, yoff + ysize), + (xoff, yoff, xoff + xsize, yoff + ysize + 100), + ), + ) + def test_extents(self, extents: tuple[int, int, int, int]) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -348,40 +397,31 @@ def test_negsize(self) -> None: fp = BytesIO() MockPyEncoder.last = None with pytest.raises(ValueError): - ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] - ) + ImageFile._save(im, fp, [ImageFile._Tile("MOCK", extents, 0, "RGB")]) last: MockPyEncoder | None = MockPyEncoder.last assert last assert last.cleanup_called with pytest.raises(ValueError): - ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] - ) + ImageFile._save(im, fp, [ImageFile._Tile("MOCK", extents, 0, "RGB")]) - def test_oversize(self) -> None: + def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) + im.tile = [ImageFile._Tile("MOCK", None, 32, None)] fp = BytesIO() - with pytest.raises(ValueError): - ImageFile._save( - im, - fp, - [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")], - ) + ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")]) - with pytest.raises(ValueError): - ImageFile._save( - im, - fp, - [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], - ) + assert MockPyEncoder.last + assert MockPyEncoder.last.state.xoff == 0 + assert MockPyEncoder.last.state.yoff == 0 + assert MockPyEncoder.last.state.xsize == 200 + assert MockPyEncoder.last.state.ysize == 200 def test_encode(self) -> None: - encoder = ImageFile.PyEncoder(None) + encoder = ImageFile.PyEncoder("") with pytest.raises(NotImplementedError): encoder.encode(0) @@ -394,7 +434,7 @@ def test_encode(self) -> None: encoder.encode_to_pyfd() with pytest.raises(NotImplementedError): - encoder.encode_to_file(None, None) + encoder.encode_to_file(0, 0) def test_zero_height(self) -> None: with pytest.raises(UnidentifiedImageError): diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 9cb42037106..d0b458d6b77 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -5,12 +5,12 @@ import re import shutil import sys +import tempfile from io import BytesIO from pathlib import Path from typing import Any, BinaryIO import pytest -from packaging.version import parse as parse_version from PIL import Image, ImageDraw, ImageFont, features from PIL._typing import StrOrBytesPath @@ -19,6 +19,7 @@ assert_image_equal, assert_image_equal_tofile, assert_image_similar_tofile, + has_feature_version, is_win32, skip_unless_feature, skip_unless_feature_version, @@ -123,7 +124,7 @@ def test_render_equal(layout_engine: ImageFont.Layout) -> None: def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None: - tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) + tempfile = tmp_path / ("temp_" + chr(128) + ".ttf") try: shutil.copy(FONT_PATH, tempfile) except UnicodeEncodeError: @@ -253,7 +254,8 @@ def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None: @pytest.mark.parametrize( - "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) + "align, ext", + (("left", ""), ("center", "_center"), ("right", "_right"), ("justify", "_justify")), ) def test_render_multiline_text_align( font: ImageFont.FreeTypeFont, align: str, ext: str @@ -265,6 +267,23 @@ def test_render_multiline_text_align( assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) +def test_render_multiline_text_justify_anchor( + font: ImageFont.FreeTypeFont, +) -> None: + im = Image.new("RGB", (280, 240)) + draw = ImageDraw.Draw(im) + for xy, anchor in (((0, 0), "la"), ((140, 80), "ma"), ((280, 160), "ra")): + draw.multiline_text( + xy, + "hey you you are awesome\nthis looks awkward\nthis\nlooks awkward", + font=font, + anchor=anchor, + align="justify", + ) + + assert_image_equal_tofile(im, "Tests/images/multiline_text_justify_anchor.png") + + def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -460,17 +479,62 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: assert mask.size == (108, 13) +def test_stroke_mask() -> None: + # Arrange + text = "i" + + # Act + font = ImageFont.truetype(FONT_PATH, 128) + mask = font.getmask(text, stroke_width=2) + + # Assert + assert mask.getpixel((34, 5)) == 255 + assert mask.getpixel((38, 5)) == 0 + assert mask.getpixel((42, 5)) == 255 + + +def test_load_invalid_file() -> None: + with pytest.raises(SyntaxError, match="Not a PILfont file"): + ImageFont.load("Tests/images/1_trns.png") + + +def test_load_when_image_not_found() -> None: + with tempfile.NamedTemporaryFile(delete=False) as tmp: + pass + with pytest.raises(OSError) as e: + ImageFont.load(tmp.name) + + os.unlink(tmp.name) + + root = os.path.splitext(tmp.name)[0] + assert str(e.value) == f"cannot find glyph data file {root}.{{gif|pbm|png}}" + + def test_load_path_not_found() -> None: # Arrange filename = "somefilenamethatdoesntexist.ttf" # Act/Assert - with pytest.raises(OSError): + with pytest.raises(OSError) as e: ImageFont.load_path(filename) + + # The file doesn't exist, so don't suggest `load` + assert filename in str(e.value) + assert "did you mean" not in str(e.value) with pytest.raises(OSError): ImageFont.truetype(filename) +def test_load_path_existing_path() -> None: + with tempfile.NamedTemporaryFile() as tmp: + with pytest.raises(OSError) as e: + ImageFont.load_path(tmp.name) + + # The file exists, so the error message suggests to use `load` instead + assert tmp.name in str(e.value) + assert " did you mean" in str(e.value) + + def test_load_non_font_bytes() -> None: with open("Tests/images/hopper.jpg", "rb") as f: with pytest.raises(OSError): @@ -491,7 +555,7 @@ def test_default_font() -> None: draw.text((10, 60), txt, font=larger_default_font) # Assert - assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png") + assert_image_similar_tofile(im, "Tests/images/default_font_freetype.png", 0.13) @pytest.mark.parametrize("mode", ("", "1", "RGBA")) @@ -516,7 +580,7 @@ def test_render_empty(font: ImageFont.FreeTypeFont) -> None: def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: # issue #3777 - text = "A\u278A\U0001F12B" + text = "A\u278a\U0001f12b" target = "Tests/images/unicode_extended.png" ttf = ImageFont.truetype( @@ -632,23 +696,13 @@ def test_complex_font_settings() -> None: def test_variation_get(font: ImageFont.FreeTypeFont) -> None: - version = features.version_module("freetype2") - assert version is not None - freetype = parse_version(version) - 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(), [ + assert font.get_variation_names() == [ b"ExtraLight", b"Light", b"Regular", @@ -688,6 +742,21 @@ def test_variation_get(font: ImageFont.FreeTypeFont) -> None: ] +def test_variation_duplicates() -> None: + font = ImageFont.truetype("Tests/fonts/AdobeVFPrototypeDuplicates.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", + ] + + def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None: im = Image.new("RGB", (100, 75), "white") d = ImageDraw.Draw(im) @@ -704,41 +773,25 @@ def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: - version = features.version_module("freetype2") - assert version is not None - freetype = parse_version(version) - 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"]: + 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"]: + 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: ImageFont.FreeTypeFont) -> None: - version = features.version_module("freetype2") - assert version is not None - freetype = parse_version(version) - 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]) @@ -985,7 +1038,7 @@ def test_sbix(layout_engine: ImageFont.Layout) -> None: im = Image.new("RGB", (400, 400), "white") d = ImageDraw.Draw(im) - d.text((50, 50), "\uE901", font=font, embedded_color=True) + 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 @@ -1002,7 +1055,7 @@ def test_sbix_mask(layout_engine: ImageFont.Layout) -> None: im = Image.new("RGB", (400, 400), "white") d = ImageDraw.Draw(im) - d.text((50, 50), "\uE901", (100, 0, 0), font=font) + 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 @@ -1023,7 +1076,10 @@ def test_colr(layout_engine: ImageFont.Layout) -> None: d.text((15, 5), "Bungee", font=font, embedded_color=True) - assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 21) + if has_feature_version("freetype2", "2.14.0"): + assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 6.1) + else: + assert_image_similar_tofile(im, "Tests/images/colr_bungee_older.png", 21) @skip_unless_feature_version("freetype2", "2.10.0") @@ -1039,7 +1095,7 @@ def test_colr_mask(layout_engine: ImageFont.Layout) -> None: d.text((15, 5), "Bungee", "black", font=font) - assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) + assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 14.1) def test_woff2(layout_engine: ImageFont.Layout) -> None: @@ -1113,6 +1169,9 @@ def test_bytes(font: ImageFont.FreeTypeFont) -> None: ) assert font.getmask2(b"test")[1] == font.getmask2("test")[1] + with pytest.raises(TypeError): + font.getlength((0, 0)) # type: ignore[arg-type] + @pytest.mark.parametrize( "test_file", @@ -1130,15 +1189,15 @@ def test_oom(test_file: str) -> None: def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) - with pytest.warns(UserWarning) as record: + with pytest.warns( + UserWarning, + match="Raqm layout was requested, but Raqm is not available. " + "Falling back to basic layout.", + ): 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." - ) @pytest.mark.parametrize("size", [-1, 0]) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 24c7b871a68..633f6756b0c 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -4,7 +4,12 @@ from PIL import Image, ImageDraw, ImageFont -from .helper import assert_image_similar_tofile, skip_unless_feature +from .helper import ( + assert_image_equal_tofile, + assert_image_similar_tofile, + has_feature_version, + skip_unless_feature, +) FONT_SIZE = 20 FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" @@ -100,11 +105,9 @@ def test_text_direction_ttb() -> None: 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") + if not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb") target = "Tests/images/test_direction_ttb.png" assert_image_similar_tofile(im, target, 2.8) @@ -115,19 +118,17 @@ def test_text_direction_ttb_stroke() -> None: 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") + if not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + draw.text( + (27, 27), + "あい", + font=ttf, + fill=500, + direction="ttb", + stroke_width=2, + stroke_fill="#0f0", + ) target = "Tests/images/test_direction_ttb_stroke.png" assert_image_similar_tofile(im, target, 19.4) @@ -182,7 +183,7 @@ def test_x_max_and_y_offset() -> None: 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) + assert_image_similar_tofile(im, target, 3.8) def test_language() -> None: @@ -215,21 +216,16 @@ def test_getlength( 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") + if direction == "ttb" and not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + assert d.textlength(text, ttf, direction) == expected @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"), + ("i" + ("\u030c" * 15) + "i", "i" + "\u032c" * 15 + "i", "\u035cii", "i\u0305i"), ids=("caron-above", "caron-below", "double-breve", "overline"), ) def test_getlength_combine(mode: str, direction: str, text: str) -> None: @@ -238,17 +234,12 @@ def test_getlength_combine(mode: str, direction: str, text: str) -> None: ttf = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) - try: - target = ttf.getlength("ii", mode, direction) - actual = ttf.getlength(text, mode, direction) + if direction == "ttb" and not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + 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") + assert actual == target @pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) @@ -261,38 +252,36 @@ def test_anchor_ttb(anchor: str) -> None: 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") + if not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f) 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), + ("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), + ("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), + ("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), @@ -306,10 +295,12 @@ def test_anchor_ttb(anchor: str) -> None: # 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] + "name, text, anchor, direction, epsilon", + combine_tests, + ids=[r[0] for r in combine_tests], ) def test_combine( - name: str, text: str, dir: str | None, anchor: str | None, epsilon: float + name: str, text: str, direction: str | None, anchor: str | None, epsilon: float ) -> None: path = f"Tests/images/test_combine_{name}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) @@ -318,11 +309,9 @@ def test_combine( 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") + if direction == "ttb" and not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + d.text((200, 200), text, fill="black", anchor=anchor, direction=direction, font=f) assert_image_similar_tofile(im, path, epsilon) @@ -346,7 +335,7 @@ def test_combine_multiline(anchor: str, align: str) -> None: 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 + 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) @@ -354,11 +343,27 @@ def test_combine_multiline(anchor: str, align: str) -> None: 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) + d.multiline_text((200, 200), text, "black", anchor=anchor, font=f, align=align) assert_image_similar_tofile(im, path, 0.015) +def test_combine_multiline_ttb() -> None: + path = "Tests/images/test_combine_multiline_ttb.png" + f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) + text = "te\nxt" + + 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, f, direction="ttb") + d.rectangle(bbox, outline="red") + d.multiline_text((200, 200), text, "black", f, direction="ttb") + + assert_image_equal_tofile(im, path) + + def test_anchor_invalid_ttb() -> None: font = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new("RGB", (100, 100), "white") @@ -378,8 +383,3 @@ def test_anchor_invalid_ttb() -> None: d.multiline_text((0, 0), "foo\nbar", anchor=anchor, direction="ttb") with pytest.raises(ValueError): d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor, direction="ttb") - # ttb multiline text does not support anchors at all - with pytest.raises(ValueError): - d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb") - with pytest.raises(ValueError): - d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb") diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 695aecbded2..883df051d1e 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -7,7 +7,7 @@ from PIL import Image, ImageDraw, ImageFont, _util, features -from .helper import assert_image_equal_tofile +from .helper import assert_image_equal_tofile, timeout_unless_slower_valgrind fonts = [ImageFont.load_default_imagefont()] if not features.check_module("freetype2"): @@ -30,20 +30,26 @@ def test_default_font(font: ImageFont.ImageFont) -> None: assert_image_equal_tofile(im, "Tests/images/default_font.png") -def test_without_freetype() -> None: - original_core = ImageFont.core +def test_invalid_mode() -> None: + font = ImageFont.ImageFont() + fp = BytesIO() + with Image.open("Tests/images/hopper.png") as im: + with pytest.raises(TypeError, match="invalid font image mode"): + font._load_pilfont_data(fp, im) + + +def test_without_freetype(monkeypatch: pytest.MonkeyPatch) -> None: if features.check_module("freetype2"): - ImageFont.core = _util.DeferredError(ImportError("Disabled for testing")) - try: - with pytest.raises(ImportError): - ImageFont.truetype("Tests/fonts/FreeMono.ttf") + monkeypatch.setattr( + ImageFont, "core", _util.DeferredError(ImportError("Disabled for testing")) + ) + with pytest.raises(ImportError): + ImageFont.truetype("Tests/fonts/FreeMono.ttf") - assert isinstance(ImageFont.load_default(), ImageFont.ImageFont) + assert isinstance(ImageFont.load_default(), ImageFont.ImageFont) - with pytest.raises(ImportError): - ImageFont.load_default(size=14) - finally: - ImageFont.core = original_core + with pytest.raises(ImportError): + ImageFont.load_default(size=14) @pytest.mark.parametrize("font", fonts) @@ -72,7 +78,7 @@ def test_decompression_bomb() -> None: font.getmask("A" * 1_000_000) -@pytest.mark.timeout(4) +@timeout_unless_slower_valgrind(4) def test_oom() -> None: glyph = struct.pack( ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 5dfa51697c2..07cb69719d9 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -9,7 +9,7 @@ from PIL import Image, ImageGrab -from .helper import assert_image_equal_tofile, skip_unless_feature +from .helper import assert_image_equal_tofile, on_ci, skip_unless_feature class TestImageGrab: @@ -40,8 +40,11 @@ def test_grab_x11(self) -> None: @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") def test_grab_no_xcb(self) -> None: - if sys.platform not in ("win32", "darwin") and not shutil.which( - "gnome-screenshot" + if ( + sys.platform not in ("win32", "darwin") + and not shutil.which("gnome-screenshot") + and not shutil.which("grim") + and not shutil.which("spectacle") ): with pytest.raises(OSError) as e: ImageGrab.grab() @@ -57,9 +60,50 @@ def test_grab_invalid_xdisplay(self) -> None: ImageGrab.grab(xdisplay="error.test:0.0") assert str(e.value).startswith("X connection failed") + @pytest.mark.skipif( + sys.platform != "darwin" or not on_ci(), reason="Only runs on macOS CI" + ) + def test_grab_handle(self) -> None: + p = subprocess.Popen( + [ + "osascript", + "-e", + 'tell application "Finder"\n' + 'open ("/" as POSIX file)\n' + "get id of front window\n" + "end tell", + ], + stdout=subprocess.PIPE, + ) + stdout = p.stdout + assert stdout is not None + window = int(stdout.read()) + + ImageGrab.grab(window=window) + + im = ImageGrab.grab((0, 0, 10, 10), window=window) + assert im.size == (10, 10) + + @pytest.mark.skipif( + sys.platform not in ("darwin", "win32"), reason="macOS and Windows only" + ) + def test_grab_invalid_handle(self) -> None: + if sys.platform == "darwin": + with pytest.raises(subprocess.CalledProcessError): + ImageGrab.grab(window=-1) + else: + with pytest.raises( + OSError, match="unable to get device context for handle" + ): + ImageGrab.grab(window=-1) + with pytest.raises(OSError, match="screen grab failed"): + ImageGrab.grab(window=0) + def test_grabclipboard(self) -> None: if sys.platform == "darwin": subprocess.call(["screencapture", "-cx"]) + + ImageGrab.grabclipboard() elif sys.platform == "win32": p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) p.stdin.write( @@ -69,6 +113,8 @@ def test_grabclipboard(self) -> None: [Windows.Forms.Clipboard]::SetImage($bmp)""" ) p.communicate() + + ImageGrab.grabclipboard() else: if not shutil.which("wl-paste") and not shutil.which("xclip"): with pytest.raises( @@ -77,9 +123,6 @@ def test_grabclipboard(self) -> None: r" ImageGrab.grabclipboard\(\) on Linux", ): ImageGrab.grabclipboard() - return - - ImageGrab.grabclipboard() @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") def test_grabclipboard_file(self) -> None: diff --git a/Tests/test_imagemath_lambda_eval.py b/Tests/test_imagemath_lambda_eval.py index 5769c903e49..ce2a32ae80e 100644 --- a/Tests/test_imagemath_lambda_eval.py +++ b/Tests/test_imagemath_lambda_eval.py @@ -1,6 +1,10 @@ from __future__ import annotations -from PIL import Image, ImageMath +from typing import Any + +import pytest + +from PIL import Image, ImageMath, _imagingmath def pixel(im: Image.Image | int) -> str | int: @@ -19,7 +23,7 @@ def pixel(im: Image.Image | int) -> str | int: A2 = A.resize((2, 2)) B2 = B.resize((2, 2)) -images = {"A": A, "B": B, "F": F, "I": I} +images: dict[str, Any] = {"A": A, "B": B, "F": F, "I": I} def test_sanity() -> None: @@ -30,13 +34,13 @@ def test_sanity() -> None: == "I 3" ) assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images)) + pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], **images)) == "I 3" ) assert ( pixel( ImageMath.lambda_eval( - lambda args: args["float"](args["A"]) + args["B"], images + lambda args: args["float"](args["A"]) + args["B"], **images ) ) == "F 3.0" @@ -44,7 +48,7 @@ def test_sanity() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["int"](args["float"](args["A"]) + args["B"]), images + lambda args: args["int"](args["float"](args["A"]) + args["B"]), **images ) ) == "I 3" @@ -52,34 +56,34 @@ def test_sanity() -> None: def test_ops() -> None: - assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, images)) == "I -1" + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, **images)) == "I -1" assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images)) + pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], **images)) == "I 3" ) assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] - args["B"], images)) + pixel(ImageMath.lambda_eval(lambda args: args["A"] - args["B"], **images)) == "I -1" ) assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] * args["B"], images)) + pixel(ImageMath.lambda_eval(lambda args: args["A"] * args["B"], **images)) == "I 2" ) assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] / args["B"], images)) + pixel(ImageMath.lambda_eval(lambda args: args["A"] / args["B"], **images)) == "I 0" ) - assert pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 2, images)) == "I 4" + assert pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 2, **images)) == "I 4" assert ( - pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 33, images)) + pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 33, **images)) == "I 2147483647" ) assert ( pixel( ImageMath.lambda_eval( - lambda args: args["float"](args["A"]) + args["B"], images + lambda args: args["float"](args["A"]) + args["B"], **images ) ) == "F 3.0" @@ -87,7 +91,7 @@ def test_ops() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["float"](args["A"]) - args["B"], images + lambda args: args["float"](args["A"]) - args["B"], **images ) ) == "F -1.0" @@ -95,7 +99,7 @@ def test_ops() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["float"](args["A"]) * args["B"], images + lambda args: args["float"](args["A"]) * args["B"], **images ) ) == "F 2.0" @@ -103,31 +107,33 @@ def test_ops() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["float"](args["A"]) / args["B"], images + lambda args: args["float"](args["A"]) / args["B"], **images ) ) == "F 0.5" ) assert ( - pixel(ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 2, images)) + pixel( + ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 2, **images) + ) == "F 4.0" ) assert ( pixel( - ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 33, images) + ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 33, **images) ) == "F 8589934592.0" ) def test_logical() -> None: - assert pixel(ImageMath.lambda_eval(lambda args: not args["A"], images)) == 0 + assert pixel(ImageMath.lambda_eval(lambda args: not args["A"], **images)) == 0 assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] and args["B"], images)) + pixel(ImageMath.lambda_eval(lambda args: args["A"] and args["B"], **images)) == "L 2" ) assert ( - pixel(ImageMath.lambda_eval(lambda args: args["A"] or args["B"], images)) + pixel(ImageMath.lambda_eval(lambda args: args["A"] or args["B"], **images)) == "L 1" ) @@ -136,7 +142,7 @@ def test_convert() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["convert"](args["A"] + args["B"], "L"), images + lambda args: args["convert"](args["A"] + args["B"], "L"), **images ) ) == "L 3" @@ -144,7 +150,7 @@ def test_convert() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["convert"](args["A"] + args["B"], "1"), images + lambda args: args["convert"](args["A"] + args["B"], "1"), **images ) ) == "1 0" @@ -152,7 +158,7 @@ def test_convert() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["convert"](args["A"] + args["B"], "RGB"), images + lambda args: args["convert"](args["A"] + args["B"], "RGB"), **images ) ) == "RGB (3, 3, 3)" @@ -163,7 +169,7 @@ def test_compare() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["min"](args["A"], args["B"]), images + lambda args: args["min"](args["A"], args["B"]), **images ) ) == "I 1" @@ -171,13 +177,13 @@ def test_compare() -> None: assert ( pixel( ImageMath.lambda_eval( - lambda args: args["max"](args["A"], args["B"]), images + lambda args: args["max"](args["A"], args["B"]), **images ) ) == "I 2" ) - assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 1, images)) == "I 1" - assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 2, images)) == "I 0" + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 1, **images)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 2, **images)) == "I 0" def test_one_image_larger() -> None: @@ -494,3 +500,31 @@ def test_logical_not_equal() -> None: ) == "I 1" ) + + +def test_reflected_operands() -> None: + assert pixel(ImageMath.lambda_eval(lambda args: 1 + args["A"], **images)) == "I 2" + assert pixel(ImageMath.lambda_eval(lambda args: 1 - args["A"], **images)) == "I 0" + assert pixel(ImageMath.lambda_eval(lambda args: 1 * args["A"], **images)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: 1 / args["A"], **images)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: 1 % args["A"], **images)) == "I 0" + assert pixel(ImageMath.lambda_eval(lambda args: 1 ** args["A"], **images)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: 1 & args["A"], **images)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: 1 | args["A"], **images)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: 1 ^ args["A"], **images)) == "I 0" + + +def test_unsupported_mode() -> None: + im = Image.new("RGB", (1, 1)) + with pytest.raises(ValueError, match="unsupported mode: RGB"): + ImageMath.lambda_eval(lambda args: args["im"] + 1, im=im) + + +def test_bad_operand_type(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delattr(_imagingmath, "abs_I") + with pytest.raises(TypeError, match="bad operand type for 'abs'"): + ImageMath.lambda_eval(lambda args: abs(args["I"]), I=I) + + monkeypatch.delattr(_imagingmath, "max_F") + with pytest.raises(TypeError, match="bad operand type for 'max'"): + ImageMath.lambda_eval(lambda args: args["max"](args["I"], args["F"]), I=I, F=F) diff --git a/Tests/test_imagemath_unsafe_eval.py b/Tests/test_imagemath_unsafe_eval.py index 7b8a562d739..5e141a55b4f 100644 --- a/Tests/test_imagemath_unsafe_eval.py +++ b/Tests/test_imagemath_unsafe_eval.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + import pytest from PIL import Image, ImageMath @@ -21,40 +23,35 @@ def pixel(im: Image.Image | int) -> str | int: A2 = A.resize((2, 2)) B2 = B.resize((2, 2)) -images = {"A": A, "B": B, "F": F, "I": I} +images: dict[str, Any] = {"A": A, "B": B, "F": F, "I": I} def test_sanity() -> None: assert ImageMath.unsafe_eval("1") == 1 assert ImageMath.unsafe_eval("1+A", A=2) == 3 assert pixel(ImageMath.unsafe_eval("A+B", A=A, B=B)) == "I 3" - assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3" - assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0" - assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", images)) == "I 3" - - -def test_eval_deprecated() -> None: - with pytest.warns(DeprecationWarning): - assert ImageMath.eval("1") == 1 + assert pixel(ImageMath.unsafe_eval("A+B", **images)) == "I 3" + assert pixel(ImageMath.unsafe_eval("float(A)+B", **images)) == "F 3.0" + assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", **images)) == "I 3" def test_ops() -> None: - assert pixel(ImageMath.unsafe_eval("-A", images)) == "I -1" - assert pixel(ImageMath.unsafe_eval("+B", images)) == "L 2" + assert pixel(ImageMath.unsafe_eval("-A", **images)) == "I -1" + assert pixel(ImageMath.unsafe_eval("+B", **images)) == "L 2" - assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3" - assert pixel(ImageMath.unsafe_eval("A-B", images)) == "I -1" - assert pixel(ImageMath.unsafe_eval("A*B", images)) == "I 2" - assert pixel(ImageMath.unsafe_eval("A/B", images)) == "I 0" - assert pixel(ImageMath.unsafe_eval("B**2", images)) == "I 4" - assert pixel(ImageMath.unsafe_eval("B**33", images)) == "I 2147483647" + assert pixel(ImageMath.unsafe_eval("A+B", **images)) == "I 3" + assert pixel(ImageMath.unsafe_eval("A-B", **images)) == "I -1" + assert pixel(ImageMath.unsafe_eval("A*B", **images)) == "I 2" + assert pixel(ImageMath.unsafe_eval("A/B", **images)) == "I 0" + assert pixel(ImageMath.unsafe_eval("B**2", **images)) == "I 4" + assert pixel(ImageMath.unsafe_eval("B**33", **images)) == "I 2147483647" - assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0" - assert pixel(ImageMath.unsafe_eval("float(A)-B", images)) == "F -1.0" - assert pixel(ImageMath.unsafe_eval("float(A)*B", images)) == "F 2.0" - assert pixel(ImageMath.unsafe_eval("float(A)/B", images)) == "F 0.5" - assert pixel(ImageMath.unsafe_eval("float(B)**2", images)) == "F 4.0" - assert pixel(ImageMath.unsafe_eval("float(B)**33", images)) == "F 8589934592.0" + assert pixel(ImageMath.unsafe_eval("float(A)+B", **images)) == "F 3.0" + assert pixel(ImageMath.unsafe_eval("float(A)-B", **images)) == "F -1.0" + assert pixel(ImageMath.unsafe_eval("float(A)*B", **images)) == "F 2.0" + assert pixel(ImageMath.unsafe_eval("float(A)/B", **images)) == "F 0.5" + assert pixel(ImageMath.unsafe_eval("float(B)**2", **images)) == "F 4.0" + assert pixel(ImageMath.unsafe_eval("float(B)**33", **images)) == "F 8589934592.0" @pytest.mark.parametrize( @@ -72,33 +69,33 @@ def test_prevent_exec(expression: str) -> None: def test_prevent_double_underscores() -> None: with pytest.raises(ValueError): - ImageMath.unsafe_eval("1", {"__": None}) + ImageMath.unsafe_eval("1", __=None) def test_prevent_builtins() -> None: with pytest.raises(ValueError): - ImageMath.unsafe_eval("(lambda: exec('exit()'))()", {"exec": None}) + ImageMath.unsafe_eval("(lambda: exec('exit()'))()", exec=None) def test_logical() -> None: - assert pixel(ImageMath.unsafe_eval("not A", images)) == 0 - assert pixel(ImageMath.unsafe_eval("A and B", images)) == "L 2" - assert pixel(ImageMath.unsafe_eval("A or B", images)) == "L 1" + assert pixel(ImageMath.unsafe_eval("not A", **images)) == 0 + assert pixel(ImageMath.unsafe_eval("A and B", **images)) == "L 2" + assert pixel(ImageMath.unsafe_eval("A or B", **images)) == "L 1" def test_convert() -> None: - assert pixel(ImageMath.unsafe_eval("convert(A+B, 'L')", images)) == "L 3" - assert pixel(ImageMath.unsafe_eval("convert(A+B, '1')", images)) == "1 0" + assert pixel(ImageMath.unsafe_eval("convert(A+B, 'L')", **images)) == "L 3" + assert pixel(ImageMath.unsafe_eval("convert(A+B, '1')", **images)) == "1 0" assert ( - pixel(ImageMath.unsafe_eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)" + pixel(ImageMath.unsafe_eval("convert(A+B, 'RGB')", **images)) == "RGB (3, 3, 3)" ) def test_compare() -> None: - assert pixel(ImageMath.unsafe_eval("min(A, B)", images)) == "I 1" - assert pixel(ImageMath.unsafe_eval("max(A, B)", images)) == "I 2" - assert pixel(ImageMath.unsafe_eval("A == 1", images)) == "I 1" - assert pixel(ImageMath.unsafe_eval("A == 2", images)) == "I 0" + assert pixel(ImageMath.unsafe_eval("min(A, B)", **images)) == "I 1" + assert pixel(ImageMath.unsafe_eval("max(A, B)", **images)) == "I 2" + assert pixel(ImageMath.unsafe_eval("A == 1", **images)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A == 2", **images)) == "I 0" def test_one_image_larger() -> None: diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 32615cf0edb..1d2fae1a6fa 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -7,7 +7,7 @@ from PIL import Image, ImageMorph, _imagingmorph -from .helper import assert_image_equal_tofile, hopper +from .helper import assert_image_equal_tofile, hopper, timeout_unless_slower_valgrind def string_to_img(image_string: str) -> Image.Image: @@ -15,18 +15,14 @@ def string_to_img(image_string: str) -> Image.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) - + im = Image.new("1", (width, height)) + for x in range(width): + for y in range(height): + im.putpixel((x, y), rows[y][x] in "X1") return im -A = string_to_img( - """ +A = string_to_img(""" ....... ....... ..111.. @@ -34,18 +30,22 @@ def string_to_img(image_string: str) -> Image.Image: ..111.. ....... ....... - """ -) + """) def img_to_string(im: Image.Image) -> str: """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) - ) + result = [] + for y in range(im.height): + line = "" + for x in range(im.width): + value = im.getpixel((x, y)) + assert not isinstance(value, tuple) + assert value is not None + line += chars[value > 0] + result.append(line) + return "\n".join(result) def img_string_normalize(im: str) -> str: @@ -75,15 +75,12 @@ def test_lut(op: str) -> None: def test_no_operator_loaded() -> None: im = Image.new("L", (1, 1)) mop = ImageMorph.MorphOp() - with pytest.raises(Exception) as e: + with pytest.raises(Exception, match="No operator loaded"): mop.apply(im) - assert str(e.value) == "No operator loaded" - with pytest.raises(Exception) as e: + with pytest.raises(Exception, match="No operator loaded"): mop.match(im) - assert str(e.value) == "No operator loaded" - with pytest.raises(Exception) as e: + with pytest.raises(Exception, match="No operator loaded"): mop.save_lut("") - assert str(e.value) == "No operator loaded" # Test the named patterns @@ -163,10 +160,12 @@ def test_edge() -> None: ) -def test_corner() -> None: +@pytest.mark.parametrize("mode", ("1", "L")) +def test_corner(mode: str) -> None: # Create a corner detector pattern mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) - count, Aout = mop.apply(A) + image = A.convert(mode) if mode == "L" else A + count, Aout = mop.apply(image) assert count == 5 assert_img_equal_img_string( Aout, @@ -182,7 +181,7 @@ def test_corner() -> None: ) # Test the coordinate counting with the same operator - coords = mop.match(A) + coords = mop.match(image) assert len(coords) == 4 assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) @@ -230,18 +229,15 @@ def test_negate() -> None: def test_incorrect_mode() -> None: - 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" + with hopper() as im: + with pytest.raises(ValueError, match="Image mode must be 1 or L"): + mop.apply(im) + with pytest.raises(ValueError, match="Image mode must be 1 or L"): + mop.match(im) + with pytest.raises(ValueError, match="Image mode must be 1 or L"): + mop.get_on_pixels(im) def test_add_patterns() -> None: @@ -267,16 +263,24 @@ def test_unknown_pattern() -> None: ImageMorph.LutBuilder(op_name="unknown") -def test_pattern_syntax_error() -> None: +@pytest.mark.parametrize( + "pattern", ("a pattern with a syntax error", "4:(" + "X" * 30000) +) +@timeout_unless_slower_valgrind(1) +def test_pattern_syntax_error(pattern: str) -> None: # Arrange lb = ImageMorph.LutBuilder(op_name="corner") - new_patterns = ["a pattern with a syntax error"] + new_patterns = [pattern] lb.add_patterns(new_patterns) # Act / Assert - with pytest.raises(Exception) as e: + with pytest.raises(Exception, match='Syntax error in pattern "'): lb.build_lut() - assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"' + + +def test_build_default_lut() -> None: + lb = ImageMorph.LutBuilder(op_name="corner") + assert lb.build_default_lut() == lb.lut def test_load_invalid_mrl() -> None: @@ -285,9 +289,8 @@ def test_load_invalid_mrl() -> None: mop = ImageMorph.MorphOp() # Act / Assert - with pytest.raises(Exception) as e: + with pytest.raises(Exception, match="Wrong size operator file!"): mop.load_lut(invalid_mrl) - assert str(e.value) == "Wrong size operator file!" def test_roundtrip_mrl(tmp_path: Path) -> None: @@ -319,17 +322,17 @@ def test_set_lut() -> None: def test_wrong_mode() -> None: lut = ImageMorph.LutBuilder(op_name="corner").build_lut() - imrgb = Image.new("RGB", (10, 10)) - iml = Image.new("L", (10, 10)) + imrgb_ptr = Image.new("RGB", (10, 10)).getim() + iml_ptr = Image.new("L", (10, 10)).getim() with pytest.raises(RuntimeError): - _imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) + _imagingmorph.apply(bytes(lut), imrgb_ptr, iml_ptr) with pytest.raises(RuntimeError): - _imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id) + _imagingmorph.apply(bytes(lut), iml_ptr, imrgb_ptr) with pytest.raises(RuntimeError): - _imagingmorph.match(bytes(lut), imrgb.im.id) + _imagingmorph.match(bytes(lut), imrgb_ptr) # Should not raise - _imagingmorph.match(bytes(lut), iml.im.id) + _imagingmorph.match(bytes(lut), iml_ptr) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 64ef929c48d..35fe3bb8a57 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -165,10 +165,10 @@ def test_pad() -> None: def test_pad_round() -> None: im = Image.new("1", (1, 1), 1) new_im = ImageOps.pad(im, (4, 1)) - assert new_im.load()[2, 0] == 1 + assert new_im.getpixel((2, 0)) == 1 new_im = ImageOps.pad(im, (1, 4)) - assert new_im.load()[0, 2] == 1 + assert new_im.getpixel((0, 2)) == 1 @pytest.mark.parametrize("mode", ("P", "PA")) @@ -186,6 +186,21 @@ def test_palette(mode: str) -> None: ) +def test_rgba_palette() -> None: + im = Image.new("P", (1, 1)) + + red = (255, 0, 0, 255) + translucent_black = (0, 0, 0, 127) + im.putpalette(red + translucent_black, "RGBA") + + expanded_im = ImageOps.expand(im, 1, 1) + + palette = expanded_im.palette + assert palette is not None + assert palette.mode == "RGBA" + assert expanded_im.convert("RGBA").getpixel((0, 0)) == translucent_black + + def test_pil163() -> None: # Division by zero in equalize if < 255 pixels in image (@PIL163) @@ -223,6 +238,7 @@ def test_expand_palette(border: int | tuple[int, int, int, int]) -> None: else: left, top, right, bottom = border px = im_expanded.convert("RGB").load() + assert px is not None for x in range(im_expanded.width): for b in range(top): assert px[x, b] == (255, 0, 0) @@ -245,29 +261,35 @@ def test_colorize_2color() -> None: # Open test image (256px by 10px, black to white) with Image.open("Tests/images/bw_gradient.png") as im: - im = im.convert("L") + im_l = im.convert("L") # Create image with original 2-color functionality - im_test = ImageOps.colorize(im, "red", "green") + im_test = ImageOps.colorize(im_l, "red", "green") # Test output image (2-color) left = (0, 1) middle = (127, 1) right = (255, 1) + value = im_test.getpixel(left) + assert isinstance(value, tuple) assert_tuple_approx_equal( - im_test.getpixel(left), + value, (255, 0, 0), threshold=1, msg="black test pixel incorrect", ) + value = im_test.getpixel(middle) + assert isinstance(value, tuple) assert_tuple_approx_equal( - im_test.getpixel(middle), + value, (127, 63, 0), threshold=1, msg="mid test pixel incorrect", ) + value = im_test.getpixel(right) + assert isinstance(value, tuple) assert_tuple_approx_equal( - im_test.getpixel(right), + value, (0, 127, 0), threshold=1, msg="white test pixel incorrect", @@ -279,31 +301,37 @@ def test_colorize_2color_offset() -> None: # Open test image (256px by 10px, black to white) with Image.open("Tests/images/bw_gradient.png") as im: - im = im.convert("L") + im_l = 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 + im_l, black="red", white="green", blackpoint=50, whitepoint=100 ) # Test output image (2-color) with offsets left = (25, 1) middle = (75, 1) right = (125, 1) + value = im_test.getpixel(left) + assert isinstance(value, tuple) assert_tuple_approx_equal( - im_test.getpixel(left), + value, (255, 0, 0), threshold=1, msg="black test pixel incorrect", ) + value = im_test.getpixel(middle) + assert isinstance(value, tuple) assert_tuple_approx_equal( - im_test.getpixel(middle), + value, (127, 63, 0), threshold=1, msg="mid test pixel incorrect", ) + value = im_test.getpixel(right) + assert isinstance(value, tuple) assert_tuple_approx_equal( - im_test.getpixel(right), + value, (0, 127, 0), threshold=1, msg="white test pixel incorrect", @@ -315,11 +343,11 @@ def test_colorize_3color_offset() -> None: # Open test image (256px by 10px, black to white) with Image.open("Tests/images/bw_gradient.png") as im: - im = im.convert("L") + im_l = im.convert("L") # Create image with new three color functionality with offsets im_test = ImageOps.colorize( - im, + im_l, black="red", white="green", mid="blue", @@ -334,29 +362,37 @@ def test_colorize_3color_offset() -> None: middle = (100, 1) right_middle = (150, 1) right = (225, 1) + value = im_test.getpixel(left) + assert isinstance(value, tuple) assert_tuple_approx_equal( - im_test.getpixel(left), + value, (255, 0, 0), threshold=1, msg="black test pixel incorrect", ) + value = im_test.getpixel(left_middle) + assert isinstance(value, tuple) assert_tuple_approx_equal( - im_test.getpixel(left_middle), + value, (127, 0, 127), threshold=1, msg="low-mid test pixel incorrect", ) + value = im_test.getpixel(middle) + assert isinstance(value, tuple) + assert_tuple_approx_equal(value, (0, 0, 255), threshold=1, msg="mid incorrect") + value = im_test.getpixel(right_middle) + assert isinstance(value, tuple) 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), + value, (0, 63, 127), threshold=1, msg="high-mid test pixel incorrect", ) + value = im_test.getpixel(right) + assert isinstance(value, tuple) assert_tuple_approx_equal( - im_test.getpixel(right), + value, (0, 127, 0), threshold=1, msg="white test pixel incorrect", @@ -365,7 +401,7 @@ def test_colorize_3color_offset() -> None: def test_exif_transpose() -> None: exts = [".jpg"] - if features.check("webp") and features.check("webp_anim"): + if features.check("webp"): exts.append(".webp") for ext in exts: with Image.open("Tests/images/hopper" + ext) as base_im: @@ -380,7 +416,6 @@ def check(orientation_im: Image.Image) -> None: else: original_exif = im.info["exif"] transposed_im = ImageOps.exif_transpose(im) - assert transposed_im is not None assert_image_similar(base_im, transposed_im, 17) if orientation_im is base_im: assert "exif" not in im.info @@ -392,7 +427,6 @@ def check(orientation_im: Image.Image) -> None: # Repeat the operation to test that it does not keep transposing transposed_im2 = ImageOps.exif_transpose(transposed_im) - assert transposed_im2 is not None assert_image_equal(transposed_im2, transposed_im) check(base_im) @@ -408,7 +442,6 @@ def check(orientation_im: Image.Image) -> None: assert im.getexif()[0x0112] == 3 transposed_im = ImageOps.exif_transpose(im) - assert transposed_im is not None assert 0x0112 not in transposed_im.getexif() transposed_im._reload_exif() @@ -421,17 +454,24 @@ def check(orientation_im: Image.Image) -> None: assert im.getexif()[0x0112] == 3 transposed_im = ImageOps.exif_transpose(im) - assert transposed_im is not None 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 transposed_im is not None + im1 = hopper() + im1.getexif()[0x0112] = 3 + transposed_im = ImageOps.exif_transpose(im1) assert 0x0112 not in transposed_im.getexif() +def test_exif_transpose_with_xmp_tuple() -> None: + with Image.open("Tests/images/xmp_tags_orientation.png") as im: + assert im.getexif()[0x0112] == 3 + + im.info["xmp"] = (b"test",) + transposed_im = ImageOps.exif_transpose(im) + assert 0x0112 not in transposed_im.getexif() + + def test_exif_transpose_xml_without_xmp() -> None: with Image.open("Tests/images/xmp_tags_orientation.png") as im: assert im.getexif()[0x0112] == 3 diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index c8e2c0467de..920012d8639 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Generator +from collections.abc import Generator import pytest diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 6cf0079dda7..526beb656e8 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,10 +1,11 @@ from __future__ import annotations +import io from pathlib import Path import pytest -from PIL import Image, ImagePalette +from PIL import Image, ImagePalette, PaletteFile from .helper import assert_image_equal, assert_image_equal_tofile @@ -17,10 +18,18 @@ def test_sanity() -> None: def test_reload() -> None: with Image.open("Tests/images/hopper.gif") as im: original = im.copy() + assert im.palette is not None im.palette.dirty = 1 assert_image_equal(im.convert("RGB"), original.convert("RGB")) +def test_save_fp() -> None: + palette = ImagePalette.ImagePalette() + with io.StringIO() as fp: + palette.save(fp) + assert not fp.closed + + def test_getcolor() -> None: palette = ImagePalette.ImagePalette() assert len(palette.palette) == 0 @@ -48,6 +57,12 @@ def test_getcolor() -> None: palette.getcolor("unknown") # type: ignore[arg-type] +def test_getcolor_rgba() -> None: + palette = ImagePalette.ImagePalette("RGBA", (1, 2, 3, 4)) + palette.getcolor((5, 6, 7, 8)) + assert palette.palette == b"\x01\x02\x03\x04\x05\x06\x07\x08" + + def test_getcolor_rgba_color_rgb_palette() -> None: palette = ImagePalette.ImagePalette("RGB") @@ -111,7 +126,7 @@ def test_make_linear_lut() -> None: assert isinstance(lut, list) assert len(lut) == 256 # Check values - for i in range(0, len(lut)): + for i in range(len(lut)): assert lut[i] == i @@ -189,12 +204,25 @@ def test_2bit_palette(tmp_path: Path) -> None: 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.putpalette(b"\xff\x00\x00\x00\xff\x00\x00\x00\xff") # RGB img.save(outfile, format="PNG") assert_image_equal_tofile(img, outfile) +def test_getpalette() -> None: + b = io.BytesIO(b"0 1\n1 2 3 4") + p = PaletteFile.PaletteFile(b) + + palette, rawmode = p.getpalette() + assert palette[:6] == b"\x01\x01\x01\x02\x03\x04" + assert rawmode == "RGB" + + def test_invalid_palette() -> None: with pytest.raises(OSError): ImagePalette.load("Tests/images/hopper.jpg") + + b = io.BytesIO(b"1" * 101) + with pytest.raises(SyntaxError, match="bad palette file"): + PaletteFile.PaletteFile(b) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 9487560af88..8d230eb564c 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -3,7 +3,7 @@ import array import math import struct -from typing import Sequence +from collections.abc import Sequence import pytest @@ -51,6 +51,7 @@ def test_path() -> None: [0.0, 1.0], ((0, 1),), [(0, 1)], + [[0, 1]], ((0.0, 1.0),), [(0.0, 1.0)], array.array("f", [0, 1]), @@ -69,24 +70,37 @@ def test_path_constructors( @pytest.mark.parametrize( - "coords", + "coords, expected", ( - ("a", "b"), - ([0, 1],), - [[0, 1]], - ([0.0, 1.0],), - [[0.0, 1.0]], + ([[0, 1], [2, 3]], [(0.0, 1.0), (2.0, 3.0)]), + ([[0.0, 1.0], [2.0, 3.0]], [(0.0, 1.0), (2.0, 3.0)]), ), ) -def test_invalid_path_constructors( - coords: tuple[str, str] | Sequence[Sequence[int]] +def test_path_list_of_lists( + coords: list[list[float]], expected: list[tuple[float, float]] ) -> None: - # Act - with pytest.raises(ValueError) as e: + p = ImagePath.Path(coords) + assert list(p) == expected + + +@pytest.mark.parametrize( + "coords, message", + ( + ([[1, 2, 3]], "coordinate list must contain exactly 2 coordinates"), + ([[1]], "coordinate list must contain exactly 2 coordinates"), + ([[[1, 2], [3, 4]]], "coordinate list must contain numbers"), + ([["a", "b"]], "coordinate list must contain numbers"), + ), +) +def test_invalid_list_coords(coords: list[list[object]], message: str) -> None: + with pytest.raises(ValueError, match=message): ImagePath.Path(coords) - # Assert - assert str(e.value) == "incorrect coordinate type" + +def test_invalid_path_constructors() -> None: + # Arrange / Act + with pytest.raises(ValueError, match="incorrect coordinate type"): + ImagePath.Path(("a", "b")) @pytest.mark.parametrize( @@ -99,13 +113,9 @@ def test_invalid_path_constructors( ), ) def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None: - # Act - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="wrong number of coordinates"): ImagePath.Path(coords) - # Assert - assert str(e.value) == "wrong number of coordinates" - @pytest.mark.parametrize( "coords, expected", @@ -204,6 +214,17 @@ def test_overflow_segfault() -> None: x[i] = b"0" * 16 +def test_compact_within_map() -> None: + p = ImagePath.Path([0, 1]) + + def map_func(x: float, y: float) -> tuple[float, float]: + p.compact() + return 0, 0 + + with pytest.raises(ValueError): + p.map(map_func) + + class Evil: def __init__(self) -> None: self.corrupt = Image.core.path(0x4000000000000000) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 22cd674ce28..2d7ca0ae0fd 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -52,4 +52,6 @@ def test_image(mode: str) -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + ImageQt.ImageQt("Tests/images/hopper.gif") diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 9b37435eb61..32da22e043f 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -4,13 +4,13 @@ import pytest -from PIL import Image, ImageSequence, TiffImagePlugin +from PIL import Image, ImageSequence, PsdImagePlugin, TiffImagePlugin from .helper import assert_image_equal, hopper, skip_unless_feature def test_sanity(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.im") + test_file = tmp_path / "temp.im" im = hopper("RGB") im.save(test_file) @@ -31,8 +31,9 @@ def test_sanity(tmp_path: Path) -> None: def test_iterator() -> None: with Image.open("Tests/images/multipage.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) i = ImageSequence.Iterator(im) - for index in range(0, im.n_frames): + for index in range(im.n_frames): assert i[index] == next(i) with pytest.raises(IndexError): i[index + 1] @@ -42,6 +43,7 @@ def test_iterator() -> None: def test_iterator_min_frame() -> None: with Image.open("Tests/images/hopper.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) i = ImageSequence.Iterator(im) for index in range(1, im.n_frames): assert i[index] == next(i) @@ -74,9 +76,14 @@ def test_consecutive() -> None: def test_palette_mmap() -> None: # Using mmap in ImageFile can require to reload the palette. with Image.open("Tests/images/multipage-mmap.tiff") as im: - color1 = im.getpalette()[:3] + palette = im.getpalette() + assert palette is not None + color1 = palette[:3] im.seek(0) - color2 = im.getpalette()[:3] + + palette = im.getpalette() + assert palette is not None + color2 = palette[:3] assert color1 == color2 diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 0bff4389607..8d6731acc91 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -16,8 +16,11 @@ def test_sanity() -> None: def test_register() -> None: - # Test registering a viewer that is not a class - ImageShow.register("not a class") + # Test registering a viewer that is an instance + class TestViewer(ImageShow.Viewer): + pass + + ImageShow.register(TestViewer()) # Restore original state ImageShow._viewers.pop() @@ -56,15 +59,12 @@ def test_show(mode: str) -> None: assert ImageShow.show(im) -def test_show_without_viewers() -> None: - viewers = ImageShow._viewers - ImageShow._viewers = [] +def test_show_without_viewers(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(ImageShow, "_viewers", []) with hopper() as im: assert not ImageShow.show(im) - ImageShow._viewers = viewers - @pytest.mark.parametrize( "viewer", @@ -112,7 +112,7 @@ def test_ipythonviewer() -> None: test_viewer = viewer break else: - pytest.fail() + pytest.fail("IPythonViewer not found") - im = hopper() - assert test_viewer.show(im) == 1 + with hopper() as im: + assert test_viewer.show(im) == 1 diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 0dfbc5a2abb..0baab7ce21e 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -57,3 +57,13 @@ def test_constant() -> None: assert st.rms[0] == 128 assert st.var[0] == 0 assert st.stddev[0] == 0 + + +def test_zero_count() -> None: + im = Image.new("L", (0, 0)) + + st = ImageStat.Stat(im) + + assert st.mean == [0] + assert st.rms == [0] + assert st.var == [0] diff --git a/Tests/test_imagetext.py b/Tests/test_imagetext.py new file mode 100644 index 00000000000..507d8240918 --- /dev/null +++ b/Tests/test_imagetext.py @@ -0,0 +1,230 @@ +from __future__ import annotations + +import pytest + +from PIL import Image, ImageDraw, ImageFont, ImageText, features + +from .helper import assert_image_similar_tofile, skip_unless_feature + +FONT_PATH = "Tests/fonts/FreeMono.ttf" + + +@pytest.fixture( + scope="module", + params=[ + pytest.param(ImageFont.Layout.BASIC), + pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), + ], +) +def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout: + return request.param + + +@pytest.fixture( + scope="module", + params=[ + None, + pytest.param(ImageFont.Layout.BASIC, marks=skip_unless_feature("freetype2")), + pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), + ], +) +def font( + request: pytest.FixtureRequest, +) -> ImageFont.ImageFont | ImageFont.FreeTypeFont: + layout_engine = request.param + if layout_engine is None: + return ImageFont.load_default_imagefont() + else: + return ImageFont.truetype(FONT_PATH, 20, layout_engine=layout_engine) + + +def test_get_length(font: ImageFont.ImageFont | ImageFont.FreeTypeFont) -> None: + factor = 1 if isinstance(font, ImageFont.ImageFont) else 2 + assert ImageText.Text("A", font).get_length() == 6 * factor + assert ImageText.Text("AB", font).get_length() == 12 * factor + assert ImageText.Text("M", font).get_length() == 6 * factor + assert ImageText.Text("y", font).get_length() == 6 * factor + assert ImageText.Text("a", font).get_length() == 6 * factor + + text = ImageText.Text("\n", font) + with pytest.raises(ValueError, match="can't measure length of multiline text"): + text.get_length() + + +@pytest.mark.parametrize( + "text, expected", + ( + ("A", (0, 4, 12, 16)), + ("AB", (0, 4, 24, 16)), + ("M", (0, 4, 12, 16)), + ("y", (0, 7, 12, 20)), + ("a", (0, 7, 12, 16)), + ), +) +def test_get_bbox( + font: ImageFont.ImageFont | ImageFont.FreeTypeFont, + text: str, + expected: tuple[int, int, int, int], +) -> None: + if isinstance(font, ImageFont.ImageFont): + expected = (0, 0, expected[2] // 2, 11) + assert ImageText.Text(text, font).get_bbox() == expected + + +def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None: + if features.check_module("freetype2"): + font = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) + text = ImageText.Text("Hello World!", font) + text.embed_color() + assert text.get_length() == 288 + + im = Image.new("RGB", (300, 64), "white") + draw = ImageDraw.Draw(im) + draw.text((10, 10), text, "#fa6") + + assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1) + + text = ImageText.Text("", mode="1") + with pytest.raises( + ValueError, match="Embedded color supported only in RGB and RGBA modes" + ): + text.embed_color() + + +@skip_unless_feature("freetype2") +def test_stroke() -> None: + for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items(): + # Arrange + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype(FONT_PATH, 120) + text = ImageText.Text("A", font) + text.stroke(2, stroke_fill) + + # Act + draw.text((12, 12), text, "#f00") + + # Assert + assert_image_similar_tofile( + im, "Tests/images/imagedraw_stroke_" + suffix + ".png", 3.1 + ) + + +@pytest.mark.parametrize( + "data, width, expected", + ( + ("Hello World!", 100, "Hello World!"), # No wrap required + ("Hello World!", 50, "Hello\nWorld!"), # Wrap word to a new line + # Keep multiple spaces within a line + ("Keep multiple spaces", 90, "Keep multiple\nspaces"), + (" Keep\n leading space", 100, " Keep\n leading space"), + ), +) +@pytest.mark.parametrize("string", (True, False)) +def test_wrap(data: str, width: int, expected: str, string: bool) -> None: + if string: + text = ImageText.Text(data) + assert text.wrap(width) is None + assert text.text == expected + else: + text_bytes = ImageText.Text(data.encode()) + assert text_bytes.wrap(width) is None + assert text_bytes.text == expected.encode() + + +def test_wrap_long_word() -> None: + text = ImageText.Text("Hello World!") + with pytest.raises(ValueError, match="Word does not fit within line"): + text.wrap(25) + + +def test_wrap_unsupported(font: ImageFont.FreeTypeFont) -> None: + transposed_font = ImageFont.TransposedFont(font) + text = ImageText.Text("Hello World!", transposed_font) + with pytest.raises(ValueError, match="TransposedFont not supported"): + text.wrap(50) + + text = ImageText.Text("Hello World!", direction="ttb") + with pytest.raises(ValueError, match="Only ltr direction supported"): + text.wrap(50) + + +def test_wrap_height() -> None: + width = 50 if features.check_module("freetype2") else 60 + text = ImageText.Text("Text does not fit within height") + wrapped = text.wrap(width, 25 if features.check_module("freetype2") else 40) + assert wrapped is not None + assert wrapped.text == " within height" + assert text.text == "Text does\nnot fit" + + text = ImageText.Text("Text does not fit\nwithin height") + wrapped = text.wrap(width, 20) + assert wrapped is not None + assert wrapped.text == " not fit\nwithin height" + assert text.text == "Text does" + + text = ImageText.Text("Text does not fit\n\nwithin height") + wrapped = text.wrap(width, 25 if features.check_module("freetype2") else 40) + assert wrapped is not None + assert wrapped.text == "\nwithin height" + assert text.text == "Text does\nnot fit" + + +def test_wrap_scaling_unsupported() -> None: + font = ImageFont.load_default_imagefont() + text = ImageText.Text("Hello World!", font) + with pytest.raises(ValueError, match="'scaling' only supports FreeTypeFont"): + text.wrap(50, scaling="shrink") + + if features.check_module("freetype2"): + text = ImageText.Text("Hello World!") + with pytest.raises(ValueError, match="'scaling' requires 'height'"): + text.wrap(50, scaling="shrink") + + +@skip_unless_feature("freetype2") +def test_wrap_shrink() -> None: + # No scaling required + text = ImageText.Text("Hello World!") + assert isinstance(text.font, ImageFont.FreeTypeFont) + assert text.font.size == 10 + assert text.wrap(50, 50, "shrink") is None + assert isinstance(text.font, ImageFont.FreeTypeFont) + assert text.font.size == 10 + + with pytest.raises(ValueError, match="Text could not be scaled"): + text.wrap(50, 15, ("shrink", 9)) + + assert text.wrap(50, 15, "shrink") is None + assert text.font.size == 8 + + text = ImageText.Text("Hello World!") + assert text.wrap(50, 15, ("shrink", 7)) is None + assert isinstance(text.font, ImageFont.FreeTypeFont) + assert text.font.size == 8 + + +@skip_unless_feature("freetype2") +def test_wrap_grow() -> None: + # No scaling required + text = ImageText.Text("Hello World!") + assert isinstance(text.font, ImageFont.FreeTypeFont) + assert text.font.size == 10 + assert text.wrap(58, 10, "grow") is None + assert isinstance(text.font, ImageFont.FreeTypeFont) + assert text.font.size == 10 + + with pytest.raises(ValueError, match="Text could not be scaled"): + text.wrap(50, 50, ("grow", 12)) + + assert text.wrap(50, 50, "grow") is None + assert text.font.size == 16 + + text = ImageText.Text("A\nB") + with pytest.raises(ValueError, match="Text could not be scaled"): + text.wrap(50, 10, "grow") + + text = ImageText.Text("Hello World!") + assert text.wrap(50, 50, ("grow", 18)) is None + assert isinstance(text.font, ImageFont.FreeTypeFont) + assert text.font.size == 16 diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index b607b8c43aa..4484dca1064 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -45,10 +45,12 @@ def test_kw() -> None: # Test "file" im = ImageTk._get_image_from_kw(kw) + assert im is not None assert_image_equal(im, im1) # Test "data" im = ImageTk._get_image_from_kw(kw) + assert im is not None assert_image_equal(im, im2) # Test no relevant entry @@ -70,6 +72,11 @@ def test_photoimage(mode: str) -> None: reloaded = ImageTk.getimage(im_tk) assert_image_equal(reloaded, im.convert("RGBA")) + with pytest.raises(ValueError): + ImageTk.PhotoImage() + with pytest.raises(ValueError): + ImageTk.PhotoImage(mode) + def test_photoimage_apply_transparency() -> None: with Image.open("Tests/images/pil123p.png") as im: @@ -102,3 +109,6 @@ def test_bitmapimage() -> None: # reloaded = ImageTk.getimage(im_tk) # assert_image_equal(reloaded, im) + + with pytest.raises(ValueError): + ImageTk.BitmapImage() diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index b43c31b521f..e7af160ddd2 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -57,6 +57,21 @@ def test_dib_mode_string(self) -> None: # Assert assert dib.size == (128, 128) + with pytest.raises(ValueError): + ImageWin.Dib(mode) + + def test_dib_hwnd(self) -> None: + mode = "RGBA" + size = (128, 128) + wnd = 0 + + dib = ImageWin.Dib(mode, size) + hwnd = ImageWin.HWND(wnd) + + dib.expose(hwnd) + dib.draw(hwnd, (0, 0) + size) + assert isinstance(dib.query_palette(hwnd), int) + def test_dib_paste(self) -> None: # Arrange im = hopper() diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index c23a5c690a2..b7421051384 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -87,8 +87,8 @@ def serialize_dib(bi: BITMAPINFOHEADER, pixels: ctypes.c_void_p) -> bytearray: def test_pointer(tmp_path: Path) -> None: im = hopper() - (width, height) = im.size - opath = str(tmp_path / "temp.png") + width, height = im.size + opath = tmp_path / "temp.png" imdib = ImageWin.Dib(im) hdr = BITMAPINFOHEADER() diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index b4a300d0c59..da6157d4e56 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -361,16 +361,6 @@ def test_RGB(self) -> None: "RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233) ) - def test_BGR(self) -> None: - with pytest.warns(DeprecationWarning): - self.assert_unpack( - "BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8) - ) - self.assert_unpack( - "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0) - ) - self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - def test_RGBA(self) -> None: self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) self.assert_unpack( diff --git a/Tests/test_main.py b/Tests/test_main.py index 2582dbee3db..65e7a44d8d0 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -7,6 +7,7 @@ import pytest +@pytest.mark.skipif(sys.platform == "ios", reason="Processes not supported on iOS") @pytest.mark.parametrize( "args, report", ((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)), diff --git a/Tests/test_map.py b/Tests/test_map.py index 93140f6e5a5..1278ba3a6d6 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -7,36 +7,30 @@ from PIL import Image -def test_overflow() -> None: +def test_overflow(monkeypatch: pytest.MonkeyPatch) -> None: # 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 + monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None) # 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 - -def test_tobytes() -> None: +def test_tobytes(monkeypatch: pytest.MonkeyPatch) -> None: # Note that this image triggers the decompression bomb warning: - max_pixels = Image.MAX_IMAGE_PIXELS - Image.MAX_IMAGE_PIXELS = None + monkeypatch.setattr(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() -> None: diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index 1b01f95ce5b..b78b7984fe2 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -16,6 +16,8 @@ def verify(im1: Image.Image) -> None: assert im1.size == im2.size pix1 = im1.load() pix2 = im2.load() + assert pix1 is not None + assert pix2 is not None for y in range(im1.size[1]): for x in range(im1.size[0]): xy = x, y @@ -42,7 +44,7 @@ def test_basic(tmp_path: Path, mode: str) -> None: im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h)) verify(im_out) # transform - filename = str(tmp_path / "temp.im") + filename = tmp_path / "temp.im" im_in.save(filename) with Image.open(filename) as im_out: diff --git a/Tests/test_nanoarrow.py b/Tests/test_nanoarrow.py new file mode 100644 index 00000000000..047be16c5f5 --- /dev/null +++ b/Tests/test_nanoarrow.py @@ -0,0 +1,302 @@ +from __future__ import annotations + +import json +from typing import Any, NamedTuple + +import pytest + +from PIL import Image + +from .helper import ( + assert_deep_equal, + assert_image_equal, + hopper, + is_big_endian, +) + +TYPE_CHECKING = False +if TYPE_CHECKING: + import nanoarrow # type: ignore [import-not-found] +else: + nanoarrow = pytest.importorskip("nanoarrow", reason="Nanoarrow not installed") + +TEST_IMAGE_SIZE = (10, 10) + + +def _test_img_equals_pyarray( + img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1 +) -> None: + assert img.height * img.width * elts_per_pixel == len(arr) + px = img.load() + assert px is not None + if elts_per_pixel > 1 and mask is None: + # have to do element-wise comparison when we're comparing + # flattened r,g,b,a to a pixel. + mask = list(range(elts_per_pixel)) + 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)): + if mask: + pixel = px[x, y] + assert isinstance(pixel, tuple) + for ix, elt in enumerate(mask): + if elts_per_pixel == 1: + assert pixel[ix] == arr[y * img.width + x].as_py()[elt] + else: + assert ( + pixel[ix] + == arr[(y * img.width + x) * elts_per_pixel + elt].as_py() + ) + else: + assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) + + +def _test_img_equals_int32_pyarray( + img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1 +) -> None: + assert img.height * img.width * elts_per_pixel == len(arr) + px = img.load() + assert px is not None + if mask is None: + # have to do element-wise comparison when we're comparing + # flattened rgba in an uint32 to a pixel. + mask = list(range(elts_per_pixel)) + 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)): + pixel = px[x, y] + assert isinstance(pixel, tuple) + arr_pixel_int = arr[y * img.width + x].as_py() + arr_pixel_tuple = ( + arr_pixel_int % 256, + (arr_pixel_int // 256) % 256, + (arr_pixel_int // 256**2) % 256, + (arr_pixel_int // 256**3), + ) + if is_big_endian(): + arr_pixel_tuple = arr_pixel_tuple[::-1] + + for ix, elt in enumerate(mask): + assert pixel[ix] == arr_pixel_tuple[elt] + + +fl_uint8_4_type = nanoarrow.fixed_size_list( + value_type=nanoarrow.uint8(nullable=False), list_size=4, nullable=False +) + + +@pytest.mark.parametrize( + "mode, dtype, mask", + ( + ("L", nanoarrow.uint8(nullable=False), None), + ("I", nanoarrow.int32(nullable=False), None), + ("F", nanoarrow.float32(nullable=False), None), + ("LA", fl_uint8_4_type, [0, 3]), + ("RGB", fl_uint8_4_type, [0, 1, 2]), + ("RGBA", fl_uint8_4_type, None), + ("RGBX", fl_uint8_4_type, None), + ("CMYK", fl_uint8_4_type, None), + ("YCbCr", fl_uint8_4_type, [0, 1, 2]), + ("HSV", fl_uint8_4_type, [0, 1, 2]), + ), +) +def test_to_array(mode: str, dtype: nanoarrow, mask: list[int] | None) -> None: + img = hopper(mode) + + # Resize to non-square + img = img.crop((3, 0, 124, 127)) + assert img.size == (121, 127) + + arr = nanoarrow.Array(img) + _test_img_equals_pyarray(img, arr, mask) + assert arr.schema.type == dtype.type + assert arr.schema.nullable == dtype.nullable + + reloaded = Image.fromarrow(arr, mode, img.size) + assert_image_equal(img, reloaded) + + +def test_lifetime() -> None: + # valgrind shouldn't error out here. + # arrays should be accessible after the image is deleted. + + img = hopper("L") + + arr_1 = nanoarrow.Array(img) + arr_2 = nanoarrow.Array(img) + + del img + + assert sum(arr_1.iter_py()) > 0 + del arr_1 + + assert sum(arr_2.iter_py()) > 0 + del arr_2 + + +def test_lifetime2() -> None: + # valgrind shouldn't error out here. + # img should remain after the arrays are collected. + + img = hopper("L") + + arr_1 = nanoarrow.Array(img) + arr_2 = nanoarrow.Array(img) + + assert sum(arr_1.iter_py()) > 0 + del arr_1 + + assert sum(arr_2.iter_py()) > 0 + del arr_2 + + img2 = img.copy() + px = img2.load() + assert px # make mypy happy + assert isinstance(px[0, 0], int) + + +class DataShape(NamedTuple): + dtype: nanoarrow + # Strictly speaking, elt should be a pixel or pixel component, so + # list[uint8][4], float, int, uint32, uint8, etc. But more + # correctly, it should be exactly the dtype from the line above. + elt: Any + elts_per_pixel: int + + +UINT_ARR = DataShape( + dtype=fl_uint8_4_type, + elt=[1, 2, 3, 4], # array of 4 uint8 per pixel + elts_per_pixel=1, # only one array per pixel +) + +UINT = DataShape( + dtype=nanoarrow.uint8(), + elt=3, # one uint8, + elts_per_pixel=4, # but repeated 4x per pixel +) + +UINT32 = DataShape( + dtype=nanoarrow.uint32(), + elt=0xABCDEF45, # one packed int, doesn't fit in a int32 > 0x80000000 + elts_per_pixel=1, # one per pixel +) + +INT32 = DataShape( + dtype=nanoarrow.uint32(), + elt=0x12CDEF45, # one packed int + elts_per_pixel=1, # one per pixel +) + + +@pytest.mark.parametrize( + "mode, data_tp, mask", + ( + ("L", DataShape(nanoarrow.uint8(), 3, 1), None), + ("I", DataShape(nanoarrow.int32(), 1 << 24, 1), None), + ("F", DataShape(nanoarrow.float32(), 3.14159, 1), None), + ("LA", UINT_ARR, [0, 3]), + ("LA", UINT, [0, 3]), + ("RGB", UINT_ARR, [0, 1, 2]), + ("RGBA", UINT_ARR, None), + ("CMYK", UINT_ARR, None), + ("YCbCr", UINT_ARR, [0, 1, 2]), + ("HSV", UINT_ARR, [0, 1, 2]), + ("RGB", UINT, [0, 1, 2]), + ("RGBA", UINT, None), + ("CMYK", UINT, None), + ("YCbCr", UINT, [0, 1, 2]), + ("HSV", UINT, [0, 1, 2]), + ), +) +def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: + dtype, elt, elts_per_pixel = data_tp + + ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] + if dtype == fl_uint8_4_type: + tmp_arr = nanoarrow.Array( + elt * (ct_pixels * elts_per_pixel), schema=nanoarrow.uint8() + ) + c_array = nanoarrow.c_array_from_buffers( + dtype, ct_pixels, buffers=[], children=[tmp_arr] + ) + arr = nanoarrow.Array(c_array) + else: + arr = nanoarrow.Array( + nanoarrow.c_array([elt] * (ct_pixels * elts_per_pixel), schema=dtype) + ) + img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) + + _test_img_equals_pyarray(img, arr, mask, elts_per_pixel) + + +@pytest.mark.parametrize( + "mode, mask", + ( + ("LA", [0, 3]), + ("RGB", [0, 1, 2]), + ("RGBA", None), + ("CMYK", None), + ("YCbCr", [0, 1, 2]), + ("HSV", [0, 1, 2]), + ), +) +@pytest.mark.parametrize("data_tp", (UINT32, INT32)) +def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) -> None: + dtype, elt, elts_per_pixel = data_tp + + ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] + arr = nanoarrow.Array( + nanoarrow.c_array([elt] * (ct_pixels * elts_per_pixel), schema=dtype) + ) + img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) + + _test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel) + + +@pytest.mark.parametrize( + "mode, metadata", + ( + ("LA", ["L", "X", "X", "A"]), + ("RGB", ["R", "G", "B", "X"]), + ("RGBX", ["R", "G", "B", "X"]), + ("RGBA", ["R", "G", "B", "A"]), + ("CMYK", ["C", "M", "Y", "K"]), + ("YCbCr", ["Y", "Cb", "Cr", "X"]), + ("HSV", ["H", "S", "V", "X"]), + ), +) +def test_image_nested_metadata(mode: str, metadata: list[str]) -> None: + img = hopper(mode) + + arr = nanoarrow.Array(img) + + assert arr.schema.value_type.metadata + assert arr.schema.value_type.metadata[b"image"] + + parsed_metadata = json.loads( + arr.schema.value_type.metadata[b"image"].decode("utf8") + ) + + assert "bands" in parsed_metadata + assert parsed_metadata["bands"] == metadata + + +@pytest.mark.parametrize( + "mode, metadata", + ( + ("L", ["L"]), + ("I", ["I"]), + ("F", ["F"]), + ), +) +def test_image_flat_metadata(mode: str, metadata: list[str]) -> None: + img = hopper(mode) + + arr = nanoarrow.Array(img) + + assert arr.schema.metadata + assert arr.schema.metadata[b"image"] + + parsed_metadata = json.loads(arr.schema.metadata[b"image"].decode("utf8")) + + assert "bands" in parsed_metadata + assert parsed_metadata["bands"] == metadata diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 32d2cf9853a..113d3075532 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,7 +1,6 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING import pytest @@ -9,6 +8,7 @@ from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature +TYPE_CHECKING = False if TYPE_CHECKING: import numpy import numpy.typing as npt @@ -20,23 +20,19 @@ def test_numpy_to_image() -> None: def to_image(dtype: npt.DTypeLike, bands: int = 1, boolean: int = 0) -> Image.Image: + data = tuple(range(100)) if bands == 1: if boolean: - data = [0, 255] * 50 - else: - data = list(range(100)) + data = (0, 255) * 50 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) + assert i.get_flattened_data() == data 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) + assert i.get_flattened_data(0) == tuple(range(100)) return i # Check supported 1-bit integer formats @@ -109,6 +105,7 @@ def _test_img_equals_nparray(img: Image.Image, np_img: _typing.NumpyArray) -> No np_size = np_img.shape[1], np_img.shape[0] assert img.size == np_size px = img.load() + assert px is not None 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_img[y, x]) @@ -140,8 +137,7 @@ def test_save_tiff_uint16() -> None: a.shape = TEST_IMAGE_SIZE img = Image.fromarray(a) - img_px = img.load() - assert img_px[0, 0] == pixel_value + assert img.getpixel((0, 0)) == pixel_value @pytest.mark.parametrize( @@ -193,7 +189,16 @@ def test_putdata() -> None: arr = numpy.zeros((15000,), numpy.float32) im.putdata(arr) - assert len(im.getdata()) == len(arr) + assert len(im.get_flattened_data()) == len(arr) + + +def test_resize() -> None: + im = hopper() + size = (64, 64) + + im_resized = im.resize(numpy.array(size)) + + assert im_resized.size == size @pytest.mark.parametrize( @@ -227,8 +232,10 @@ def test_zero_size() -> None: @skip_unless_feature("libtiff") -def test_load_first() -> None: +def test_transposed() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: + assert im.size == (590, 88) + a = numpy.array(im) assert a.shape == (88, 590) @@ -239,7 +246,7 @@ def test_bool() -> None: a[0][0] = True im2 = Image.fromarray(a) - assert im2.getdata()[0] == 255 + assert im2.getpixel((0, 0)) == 255 def test_no_resource_warning_for_numpy_array() -> None: @@ -251,4 +258,6 @@ def test_no_resource_warning_for_numpy_array() -> None: with Image.open(test_file) as im: # Act/Assert with warnings.catch_warnings(): + warnings.simplefilter("error") + array(im) diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index f6b12cb2047..cba6cd05390 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -20,10 +20,10 @@ def test_text_encode_decode() -> None: - assert encode_text("abc") == b"\xFE\xFF\x00a\x00b\x00c" - assert decode_text(b"\xFE\xFF\x00a\x00b\x00c") == "abc" + assert encode_text("abc") == b"\xfe\xff\x00a\x00b\x00c" + assert decode_text(b"\xfe\xff\x00a\x00b\x00c") == "abc" assert decode_text(b"abc") == "abc" - assert decode_text(b"\x1B a \x1C") == "\u02D9 a \u02DD" + assert decode_text(b"\x1b a \x1c") == "\u02d9 a \u02dd" def test_indirect_refs() -> None: @@ -45,8 +45,8 @@ def test_parsing() -> None: assert PdfParser.get_value(b"false%", 0) == (False, 5) assert PdfParser.get_value(b"null<", 0) == (None, 4) assert PdfParser.get_value(b"%cmt\n %cmt\n 123\n", 0) == (123, 15) - assert PdfParser.get_value(b"<901FA3>", 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"<901FA3>", 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) @@ -56,9 +56,9 @@ def test_parsing() -> None: 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"(\\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 @@ -118,10 +118,15 @@ def test_pdf_repr() -> None: 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>" + assert pdf_repr(PdfBinary(b"\x90\x1f\xa0")) == b"<901FA0>" def test_duplicate_xref_entry() -> None: pdf = PdfParser("Tests/images/duplicate_xref_entry.pdf") assert pdf.xref_table.existing_entries[6][0] == 1197 pdf.close() + + +def test_trailer_loop() -> None: + with pytest.raises(PdfFormatError, match="trailer loop found"): + PdfParser("Tests/images/trailer_loop.pdf") diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index ed415953f06..2447ae67ad7 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -18,35 +18,33 @@ def helper_pickle_file( ) -> None: # Arrange with Image.open(test_file) as im: - filename = str(tmp_path / "temp.pkl") - if mode: - im = im.convert(mode) + filename = tmp_path / "temp.pkl" + converted_im = im.convert(mode) if mode else im # Act with open(filename, "wb") as f: - pickle.dump(im, f, protocol) + pickle.dump(converted_im, f, protocol) with open(filename, "rb") as f: loaded_im = pickle.load(f) # Assert - assert im == loaded_im + assert converted_im == loaded_im def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> None: with Image.open(test_file) as im: - if mode: - im = im.convert(mode) + converted_im = im.convert(mode) if mode else im # Act - dumped_string = pickle.dumps(im, protocol) + dumped_string = pickle.dumps(converted_im, protocol) loaded_im = pickle.loads(dumped_string) # Assert - assert im == loaded_im + assert converted_im == loaded_im @pytest.mark.parametrize( - ("test_file", "test_mode"), + "test_file, test_mode", [ ("Tests/images/hopper.jpg", None), ("Tests/images/hopper.jpg", "L"), @@ -56,16 +54,16 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non ), ("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/eps/zero_bb.png", None), + ("Tests/images/eps/zero_bb_scale2.png", None), + ("Tests/images/eps/non_zero_bb.png", None), + ("Tests/images/eps/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)) +@pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1)) def test_pickle_image( tmp_path: Path, test_file: str, test_mode: str | None, protocol: int ) -> None: @@ -74,22 +72,34 @@ def test_pickle_image( helper_pickle_file(tmp_path, protocol, test_file, test_mode) +def test_pickle_jpeg() -> None: + # Arrange + with Image.open("Tests/images/hopper.jpg") as image: + # Act: roundtrip + unpickled_image = pickle.loads(pickle.dumps(image)) + + # Assert + assert unpickled_image.filename == "Tests/images/hopper.jpg" + assert len(unpickled_image.layer) == 3 + assert unpickled_image.layers == 3 + + def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: # Arrange - filename = str(tmp_path / "temp.pkl") + filename = tmp_path / "temp.pkl" with Image.open("Tests/images/hopper.jpg") as im: - im = im.convert("PA") + im_pa = im.convert("PA") # Act / Assert - for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): - im._mode = "LA" + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + im_pa._mode = "LA" with open(filename, "wb") as f: - pickle.dump(im, f, protocol) + pickle.dump(im_pa, f, protocol) with open(filename, "rb") as f: loaded_im = pickle.load(f) - im._mode = "PA" - assert im == loaded_im + im_pa._mode = "PA" + assert im_pa == loaded_im @skip_unless_feature("webp") @@ -122,7 +132,7 @@ def helper_assert_pickled_font_images( @skip_unless_feature("freetype2") -@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) +@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1))) def test_pickle_font_string(protocol: int) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -136,11 +146,11 @@ def test_pickle_font_string(protocol: int) -> None: @skip_unless_feature("freetype2") -@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) +@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1))) def test_pickle_font_file(tmp_path: Path, protocol: int) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) - filename = str(tmp_path / "temp.pkl") + filename = tmp_path / "temp.pkl" # Act: roundtrip with open(filename, "wb") as f: @@ -150,3 +160,13 @@ def test_pickle_font_file(tmp_path: Path, protocol: int) -> None: # Assert helper_assert_pickled_font_images(font, unpickled_font) + + +def test_load_earlier_data() -> None: + im = pickle.loads( + b"\x80\x04\x95@\x00\x00\x00\x00\x00\x00\x00\x8c\x12PIL.PngImagePlugin" + b"\x94\x8c\x0cPngImageFile\x94\x93\x94)\x81\x94]\x94(}\x94\x8c\x01L\x94K\x01" + b"K\x01\x86\x94NC\x01\x00\x94eb." + ) + assert im.mode == "L" + assert im.size == (1, 1) diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 130ffa863a5..e5c6f7d85cc 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -37,7 +37,7 @@ def test_draw_postscript(tmp_path: Path) -> None: # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript # Arrange - tempfile = str(tmp_path / "temp.ps") + tempfile = tmp_path / "temp.ps" with open(tempfile, "wb") as fp: # Act ps = PSDraw.PSDraw(fp) @@ -49,24 +49,16 @@ def test_draw_postscript(tmp_path: Path) -> None: assert os.path.getsize(tempfile) > 0 -@pytest.mark.parametrize("buffer", (True, False)) -def test_stdout(buffer: bool) -> None: +def test_stdout(monkeypatch: pytest.MonkeyPatch) -> None: # Temporarily redirect stdout - old_stdout = sys.stdout - class MyStdOut: buffer = BytesIO() - mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() + mystdout = MyStdOut() - sys.stdout = mystdout # type: ignore[assignment] + monkeypatch.setattr(sys, "stdout", mystdout) ps = PSDraw.PSDraw() _create_document(ps) - # Reset stdout - sys.stdout = old_stdout - - if isinstance(mystdout, MyStdOut): - mystdout = mystdout.buffer - assert mystdout.getvalue() != b"" + assert mystdout.buffer.getvalue() != b"" diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py new file mode 100644 index 00000000000..f282f2c0059 --- /dev/null +++ b/Tests/test_pyarrow.py @@ -0,0 +1,271 @@ +from __future__ import annotations + +import json +from typing import Any, NamedTuple + +import pytest + +from PIL import Image + +from .helper import ( + assert_deep_equal, + assert_image_equal, + hopper, + is_big_endian, +) + +TYPE_CHECKING = False +if TYPE_CHECKING: + import pyarrow +else: + pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed") + +TEST_IMAGE_SIZE = (10, 10) + + +def _test_img_equals_pyarray( + img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1 +) -> None: + assert img.height * img.width * elts_per_pixel == len(arr) + px = img.load() + assert px is not None + if elts_per_pixel > 1 and mask is None: + # have to do element-wise comparison when we're comparing + # flattened r,g,b,a to a pixel. + mask = list(range(elts_per_pixel)) + 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)): + if mask: + pixel = px[x, y] + assert isinstance(pixel, tuple) + for ix, elt in enumerate(mask): + if elts_per_pixel == 1: + assert pixel[ix] == arr[y * img.width + x].as_py()[elt] + else: + assert ( + pixel[ix] + == arr[(y * img.width + x) * elts_per_pixel + elt].as_py() + ) + else: + assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) + + +def _test_img_equals_int32_pyarray( + img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1 +) -> None: + assert img.height * img.width * elts_per_pixel == len(arr) + px = img.load() + assert px is not None + if mask is None: + # have to do element-wise comparison when we're comparing + # flattened rgba in an uint32 to a pixel. + mask = list(range(elts_per_pixel)) + 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)): + pixel = px[x, y] + assert isinstance(pixel, tuple) + arr_pixel_int = arr[y * img.width + x].as_py() + arr_pixel_tuple = ( + arr_pixel_int % 256, + (arr_pixel_int // 256) % 256, + (arr_pixel_int // 256**2) % 256, + (arr_pixel_int // 256**3), + ) + if is_big_endian(): + arr_pixel_tuple = arr_pixel_tuple[::-1] + + for ix, elt in enumerate(mask): + assert pixel[ix] == arr_pixel_tuple[elt] + + +# really hard to get a non-nullable list type +fl_uint8_4_type = pyarrow.field( + "_", pyarrow.list_(pyarrow.field("_", pyarrow.uint8()).with_nullable(False), 4) +).type + + +@pytest.mark.parametrize( + "mode, dtype, mask", + ( + ("L", pyarrow.uint8(), None), + ("I", pyarrow.int32(), None), + ("F", pyarrow.float32(), None), + ("LA", fl_uint8_4_type, [0, 3]), + ("RGB", fl_uint8_4_type, [0, 1, 2]), + ("RGBA", fl_uint8_4_type, None), + ("RGBX", fl_uint8_4_type, None), + ("CMYK", fl_uint8_4_type, None), + ("YCbCr", fl_uint8_4_type, [0, 1, 2]), + ("HSV", fl_uint8_4_type, [0, 1, 2]), + ), +) +def test_to_array(mode: str, dtype: pyarrow.DataType, mask: list[int] | None) -> None: + img = hopper(mode) + + # Resize to non-square + img = img.crop((3, 0, 124, 127)) + assert img.size == (121, 127) + + arr = pyarrow.array(img) # type: ignore[call-overload] + _test_img_equals_pyarray(img, arr, mask) + assert arr.type == dtype + + reloaded = Image.fromarrow(arr, mode, img.size) + + assert_image_equal(img, reloaded) + + +def test_lifetime() -> None: + # valgrind shouldn't error out here. + # arrays should be accessible after the image is deleted. + + img = hopper("L") + + arr_1 = pyarrow.array(img) # type: ignore[call-overload] + arr_2 = pyarrow.array(img) # type: ignore[call-overload] + + del img + + assert arr_1.sum().as_py() > 0 + del arr_1 + + assert arr_2.sum().as_py() > 0 + del arr_2 + + +def test_lifetime2() -> None: + # valgrind shouldn't error out here. + # img should remain after the arrays are collected. + + img = hopper("L") + + arr_1 = pyarrow.array(img) # type: ignore[call-overload] + arr_2 = pyarrow.array(img) # type: ignore[call-overload] + + assert arr_1.sum().as_py() > 0 + del arr_1 + + assert arr_2.sum().as_py() > 0 + del arr_2 + + img2 = img.copy() + px = img2.load() + assert px # make mypy happy + assert isinstance(px[0, 0], int) + + +class DataShape(NamedTuple): + dtype: pyarrow.DataType + # Strictly speaking, elt should be a pixel or pixel component, so + # list[uint8][4], float, int, uint32, uint8, etc. But more + # correctly, it should be exactly the dtype from the line above. + elt: Any + elts_per_pixel: int + + +UINT_ARR = DataShape( + dtype=fl_uint8_4_type, + elt=[1, 2, 3, 4], # array of 4 uint8 per pixel + elts_per_pixel=1, # only one array per pixel +) + +UINT = DataShape( + dtype=pyarrow.uint8(), + elt=3, # one uint8, + elts_per_pixel=4, # but repeated 4x per pixel +) + +UINT32 = DataShape( + dtype=pyarrow.uint32(), + elt=0xABCDEF45, # one packed int, doesn't fit in a int32 > 0x80000000 + elts_per_pixel=1, # one per pixel +) + +INT32 = DataShape( + dtype=pyarrow.uint32(), + elt=0x12CDEF45, # one packed int + elts_per_pixel=1, # one per pixel +) + + +@pytest.mark.parametrize( + "mode, data_tp, mask", + ( + ("L", DataShape(pyarrow.uint8(), 3, 1), None), + ("I", DataShape(pyarrow.int32(), 1 << 24, 1), None), + ("F", DataShape(pyarrow.float32(), 3.14159, 1), None), + ("LA", UINT_ARR, [0, 3]), + ("LA", UINT, [0, 3]), + ("RGB", UINT_ARR, [0, 1, 2]), + ("RGBA", UINT_ARR, None), + ("CMYK", UINT_ARR, None), + ("YCbCr", UINT_ARR, [0, 1, 2]), + ("HSV", UINT_ARR, [0, 1, 2]), + ("RGB", UINT, [0, 1, 2]), + ("RGBA", UINT, None), + ("CMYK", UINT, None), + ("YCbCr", UINT, [0, 1, 2]), + ("HSV", UINT, [0, 1, 2]), + ), +) +def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: + dtype, elt, elts_per_pixel = data_tp + + ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] + arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype) + img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) + + _test_img_equals_pyarray(img, arr, mask, elts_per_pixel) + + +@pytest.mark.parametrize( + "mode, data_tp, mask", + ( + ("LA", UINT32, [0, 3]), + ("RGB", UINT32, [0, 1, 2]), + ("RGBA", UINT32, None), + ("CMYK", UINT32, None), + ("YCbCr", UINT32, [0, 1, 2]), + ("HSV", UINT32, [0, 1, 2]), + ("LA", INT32, [0, 3]), + ("RGB", INT32, [0, 1, 2]), + ("RGBA", INT32, None), + ("CMYK", INT32, None), + ("YCbCr", INT32, [0, 1, 2]), + ("HSV", INT32, [0, 1, 2]), + ), +) +def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: + dtype, elt, elts_per_pixel = data_tp + + ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] + arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype) + img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) + + _test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel) + + +@pytest.mark.parametrize( + "mode, metadata", + ( + ("LA", ["L", "X", "X", "A"]), + ("RGB", ["R", "G", "B", "X"]), + ("RGBX", ["R", "G", "B", "X"]), + ("RGBA", ["R", "G", "B", "A"]), + ("CMYK", ["C", "M", "Y", "K"]), + ("YCbCr", ["Y", "Cb", "Cr", "X"]), + ("HSV", ["H", "S", "V", "X"]), + ), +) +def test_image_metadata(mode: str, metadata: list[str]) -> None: + img = hopper(mode) + + arr = pyarrow.array(img) # type: ignore[call-overload] + + assert arr.type.field(0).metadata + assert arr.type.field(0).metadata[b"image"] + + parsed_metadata = json.loads(arr.type.field(0).metadata[b"image"].decode("utf8")) + + assert "bands" in parsed_metadata + assert parsed_metadata["bands"] == metadata diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index c2f7fe22ecb..915dbe7b685 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,15 +1,43 @@ from __future__ import annotations +from importlib.metadata import metadata + import pytest from PIL import __version__ +TYPE_CHECKING = False + +if TYPE_CHECKING: + from importlib.metadata import PackageMetadata + pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") +def map_metadata_keys(md: PackageMetadata) -> dict[str, str | list[str] | None]: + # Convert installed wheel metadata into canonical Core Metadata 2.4 format. + # This was a utility method in pyroma 4.3.3; it was removed in 5.0. + # This implementation is constructed from the relevant logic from + # Pyroma 5.0's `build_metadata()` implementation. This has been submitted + # upstream to Pyroma as https://github.com/regebro/pyroma/pull/116, + # so it may be possible to simplify this test in future. + data = {} + for key in set(md): + value = md.get_all(key) + key = pyroma.projectdata.normalize(key) + + if value is not None and len(value) == 1: + first_value = value[0] + if first_value.strip() != "UNKNOWN": + data[key] = first_value + else: + data[key] = value + return data + + def test_pyroma() -> None: # Arrange - data = pyroma.projectdata.get_data(".") + data = map_metadata_keys(metadata("Pillow")) # Act rating = pyroma.ratings.rate(data) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 28f66891c0d..b31e2a4ef25 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,13 +1,16 @@ from __future__ import annotations -from pathlib import Path - import pytest from PIL import Image, ImageQt from .helper import assert_image_equal_tofile, assert_image_similar, hopper +TYPE_CHECKING = False +if TYPE_CHECKING: + from pathlib import Path + + if ImageQt.qt_is_installed: from PIL.ImageQt import QPixmap @@ -16,9 +19,14 @@ 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 + from PySide6.QtCore import QPoint # type: ignore[assignment] + from PySide6.QtGui import QImage, QPainter, QRegion # type: ignore[assignment] + from PySide6.QtWidgets import ( # type: ignore[assignment] + QApplication, + QHBoxLayout, + QLabel, + QWidget, + ) class Example(QWidget): def __init__(self) -> None: @@ -28,9 +36,10 @@ def __init__(self) -> None: qimage = ImageQt.ImageQt(img) - pixmap1 = ImageQt.QPixmap.fromImage(qimage) + pixmap1 = getattr(ImageQt.QPixmap, "fromImage")(qimage) - QHBoxLayout(self) # hbox + # hbox + QHBoxLayout(self) lbl = QLabel(self) # Segfault in the problem @@ -56,7 +65,7 @@ def test_sanity(tmp_path: Path) -> None: im = hopper(mode) data = ImageQt.toqpixmap(im) - assert isinstance(data, QPixmap) + assert data.__class__.__name__ == "QPixmap" assert not data.isNull() # Test saving the file @@ -64,10 +73,10 @@ def test_sanity(tmp_path: Path) -> None: 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) + imageqt = ImageQt.ImageQt(im) + data = getattr(QPixmap, "fromImage")(imageqt) + qt_format = getattr(QImage, "Format") if ImageQt.qt_version == "6" else QImage + qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32")) painter = QPainter(qimage) image_label = QLabel() image_label.setPixmap(data) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 6110be707f5..0004b552153 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,13 +1,15 @@ from __future__ import annotations -from pathlib import Path - import pytest from PIL import ImageQt from .helper import assert_image_equal, assert_image_equal_tofile, hopper +TYPE_CHECKING = False +if TYPE_CHECKING: + from pathlib import Path + pytestmark = pytest.mark.skipif( not ImageQt.qt_is_installed, reason="Qt bindings are not installed" ) diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index dd4fc46c374..a7e95ed83c8 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -2,14 +2,18 @@ import shutil from io import BytesIO -from pathlib import Path -from typing import IO, Callable import pytest from PIL import GifImagePlugin, Image, JpegImagePlugin -from .helper import cjpeg_available, djpeg_available, is_win32, netpbm_available +from .helper import djpeg_available, is_win32, netpbm_available + +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from pathlib import Path + from typing import IO TEST_JPG = "Tests/images/hopper.jpg" TEST_GIF = "Tests/images/hopper.gif" @@ -35,25 +39,23 @@ def assert_save_filename_check( @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") def test_load_djpeg_filename(self, tmp_path: Path) -> None: for filename in test_filenames: - src_file = str(tmp_path / filename) + src_file = tmp_path / filename shutil.copy(TEST_JPG, src_file) with Image.open(src_file) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) im.load_djpeg() - @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") - def test_save_cjpeg_filename(self, tmp_path: Path) -> None: - 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: Path) -> None: with Image.open(TEST_GIF) as im: - im = im.convert("RGB") - self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) + im_rgb = im.convert("RGB") + self.assert_save_filename_check( + tmp_path, im_rgb, GifImagePlugin._save_netpbm + ) @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") def test_save_netpbm_filename_l_mode(self, tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: - im = im.convert("L") - self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) + im_l = im.convert("L") + self.assert_save_filename_check(tmp_path, im_l, GifImagePlugin._save_netpbm) diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py index 073e5415c41..976f6238465 100644 --- a/Tests/test_tiff_crashes.py +++ b/Tests/test_tiff_crashes.py @@ -52,3 +52,17 @@ def test_tiff_crashes(test_file: str) -> None: pytest.skip("test image not found") except OSError: pass + + +def test_tiff_mmap() -> None: + try: + with Image.open("Tests/images/crash_mmap.tif") as im: + im.seek(1) + im.load() + + im.seek(0) + im.load() + except FileNotFoundError: + if on_ci(): + raise + pytest.skip("test image not found") diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index ae80b98b84d..42d06b89696 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -11,7 +11,11 @@ from .helper import hopper, skip_unless_feature -def _test_equal(num, denom, target) -> None: +def _test_equal( + num: float | Fraction | IFDRational, + denom: int, + target: float | Fraction | IFDRational, +) -> None: t = IFDRational(num, denom) assert target == t @@ -50,8 +54,8 @@ def test_nonetype() -> None: assert xres.denominator is not None assert yres._val is not None - assert xres and 1 - assert xres and yres + assert xres + assert yres @pytest.mark.parametrize( @@ -61,11 +65,12 @@ def test_ifd_rational_save( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool ) -> None: im = hopper() - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" res = IFDRational(301, 1) monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff) im.save(out, dpi=(res, res), compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) diff --git a/Tests/test_uploader.py b/Tests/test_uploader.py index d55ceb4be17..0491a22a11c 100644 --- a/Tests/test_uploader.py +++ b/Tests/test_uploader.py @@ -1,6 +1,6 @@ from __future__ import annotations -from .helper import assert_image_equal, assert_image_similar, hopper +from .helper import assert_image_equal, hopper def check_upload_equal() -> None: @@ -12,4 +12,4 @@ def check_upload_equal() -> None: def check_upload_similar() -> None: result = hopper("P").convert("RGB") target = hopper("RGB") - assert_image_similar(result, target, 0) + assert_image_equal(result, target) diff --git a/Tests/test_util.py b/Tests/test_util.py index 197ef79eef5..9eaabf18a73 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -30,28 +30,6 @@ def test_is_not_path(tmp_path: Path) -> None: assert not it_is_not -def test_is_directory() -> None: - # Arrange - directory = "Tests" - - # Act - it_is = _util.is_directory(directory) - - # Assert - assert it_is - - -def test_is_not_directory() -> None: - # Arrange - text = "abc" - - # Act - it_is_not = _util.is_directory(text) - - # Assert - assert not it_is_not - - def test_deferred_error() -> None: # Arrange diff --git a/Tests/32bit_segfault_check.py b/checks/32bit_segfault_check.py old mode 100755 new mode 100644 similarity index 85% rename from Tests/32bit_segfault_check.py rename to checks/32bit_segfault_check.py index 06ed2ed2f60..e277bc10af9 --- a/Tests/32bit_segfault_check.py +++ b/checks/32bit_segfault_check.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 from __future__ import annotations import sys diff --git a/Tests/check_fli_oob.py b/checks/check_fli_oob.py similarity index 100% rename from Tests/check_fli_oob.py rename to checks/check_fli_oob.py diff --git a/Tests/check_fli_overflow.py b/checks/check_fli_overflow.py similarity index 100% rename from Tests/check_fli_overflow.py rename to checks/check_fli_overflow.py diff --git a/Tests/check_icns_dos.py b/checks/check_icns_dos.py similarity index 100% rename from Tests/check_icns_dos.py rename to checks/check_icns_dos.py diff --git a/Tests/check_imaging_leaks.py b/checks/check_imaging_leaks.py old mode 100755 new mode 100644 similarity index 86% rename from Tests/check_imaging_leaks.py rename to checks/check_imaging_leaks.py index 231789ca0a0..65090b6b6ae --- a/Tests/check_imaging_leaks.py +++ b/checks/check_imaging_leaks.py @@ -1,18 +1,19 @@ -#!/usr/bin/env python3 from __future__ import annotations -from typing import Any, Callable +import sys +from collections.abc import Callable +from typing import Any import pytest 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") +pytestmark = pytest.mark.skipif( + sys.platform.startswith("win32"), reason="requires Unix or macOS" +) def _get_mem_usage() -> float: diff --git a/Tests/check_j2k_dos.py b/checks/check_j2k_dos.py similarity index 100% rename from Tests/check_j2k_dos.py rename to checks/check_j2k_dos.py diff --git a/Tests/check_j2k_leaks.py b/checks/check_j2k_leaks.py similarity index 80% rename from Tests/check_j2k_leaks.py rename to checks/check_j2k_leaks.py index bbe35b591fc..7103d502e40 100644 --- a/Tests/check_j2k_leaks.py +++ b/checks/check_j2k_leaks.py @@ -1,12 +1,11 @@ from __future__ import annotations +import sys from io import BytesIO import pytest -from PIL import Image - -from .helper import is_win32, skip_unless_feature +from PIL import Image, features # Limits for testing the leak mem_limit = 1024 * 1048576 @@ -15,8 +14,10 @@ test_file = "Tests/images/rgb_trns_ycbc.jp2" pytestmark = [ - pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"), - skip_unless_feature("jpg_2000"), + pytest.mark.skipif( + sys.platform.startswith("win32"), reason="requires Unix or macOS" + ), + pytest.mark.skipif(not features.check("jpg_2000"), reason="jpg_2000 not available"), ] diff --git a/Tests/check_j2k_overflow.py b/checks/check_j2k_overflow.py similarity index 86% rename from Tests/check_j2k_overflow.py rename to checks/check_j2k_overflow.py index dbdd5a4f557..58566c4b219 100644 --- a/Tests/check_j2k_overflow.py +++ b/checks/check_j2k_overflow.py @@ -9,6 +9,6 @@ def test_j2k_overflow(tmp_path: Path) -> None: im = Image.new("RGBA", (1024, 131584)) - target = str(tmp_path / "temp.jpc") + target = tmp_path / "temp.jpc" with pytest.raises(OSError): im.save(target) diff --git a/Tests/check_jp2_overflow.py b/checks/check_jp2_overflow.py similarity index 100% rename from Tests/check_jp2_overflow.py rename to checks/check_jp2_overflow.py diff --git a/Tests/check_jpeg_leaks.py b/checks/check_jpeg_leaks.py similarity index 93% rename from Tests/check_jpeg_leaks.py rename to checks/check_jpeg_leaks.py index 5f290c6cd37..2c27ce1d5af 100644 --- a/Tests/check_jpeg_leaks.py +++ b/checks/check_jpeg_leaks.py @@ -1,10 +1,11 @@ from __future__ import annotations +import sys from io import BytesIO import pytest -from .helper import hopper, is_win32 +from PIL import Image iterations = 5000 @@ -13,12 +14,14 @@ When run on a system without the jpeg leak fixes, the valgrind runs look like this. -valgrind --tool=massif python test-installed.py -s -v Tests/check_jpeg_leaks.py +valgrind --tool=massif python test-installed.py -s -v checks/check_jpeg_leaks.py """ -pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") +pytestmark = pytest.mark.skipif( + sys.platform.startswith("win32"), reason="requires Unix or macOS" +) """ pre patch: @@ -112,10 +115,10 @@ ), ) def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None: - im = hopper("RGB") - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG", qtables=qtables) + with Image.open("Tests/images/hopper.ppm") as im: + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", qtables=qtables) def test_exif_leak() -> None: @@ -173,12 +176,12 @@ def test_exif_leak() -> None: 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) + with Image.open("Tests/images/hopper.ppm") as im: + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", exif=exif) def test_base_save() -> None: @@ -207,8 +210,7 @@ def test_base_save() -> None: | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: 0 +----------------------------------------------------------------------->Gi 0 7.882""" - im = hopper("RGB") - - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG") + with Image.open("Tests/images/hopper.ppm") as im: + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG") diff --git a/Tests/check_large_memory.py b/checks/check_large_memory.py similarity index 97% rename from Tests/check_large_memory.py rename to checks/check_large_memory.py index a9ce79e57e6..c9feda3b1b0 100644 --- a/Tests/check_large_memory.py +++ b/checks/check_large_memory.py @@ -32,7 +32,7 @@ def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im = Image.new("L", (xdim, ydim), 0) im.save(f) diff --git a/Tests/check_large_memory_numpy.py b/checks/check_large_memory_numpy.py similarity index 96% rename from Tests/check_large_memory_numpy.py rename to checks/check_large_memory_numpy.py index f4ca8d0aa68..458b0ab72d2 100644 --- a/Tests/check_large_memory_numpy.py +++ b/checks/check_large_memory_numpy.py @@ -28,7 +28,7 @@ def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: dtype = np.uint8 a = np.zeros((xdim, ydim), dtype=dtype) - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im = Image.fromarray(a, "L") im.save(f) diff --git a/Tests/check_libtiff_segfault.py b/checks/check_libtiff_segfault.py similarity index 100% rename from Tests/check_libtiff_segfault.py rename to checks/check_libtiff_segfault.py diff --git a/Tests/check_png_dos.py b/checks/check_png_dos.py similarity index 73% rename from Tests/check_png_dos.py rename to checks/check_png_dos.py index 63d6657bc45..1bfb94ab7f7 100644 --- a/Tests/check_png_dos.py +++ b/checks/check_png_dos.py @@ -3,26 +3,25 @@ import zlib from io import BytesIO +import pytest + from PIL import Image, ImageFile, PngImagePlugin TEST_FILE = "Tests/images/png_decompression_dos.png" -def test_ignore_dos_text() -> None: - ImageFile.LOAD_TRUNCATED_IMAGES = True +def test_ignore_dos_text(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) - try: - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: im.load() - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False - assert isinstance(im, PngImagePlugin.PngImageFile) - for s in im.text.values(): - assert len(s) < 1024 * 1024, "Text chunk larger than 1M" + assert isinstance(im, PngImagePlugin.PngImageFile) + for s in im.text.values(): + assert 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(): + assert len(s) < 1024 * 1024, "Text chunk larger than 1M" def test_dos_text() -> None: diff --git a/Tests/check_release_notes.py b/checks/check_release_notes.py similarity index 100% rename from Tests/check_release_notes.py rename to checks/check_release_notes.py diff --git a/checks/check_wheel.py b/checks/check_wheel.py new file mode 100644 index 00000000000..f716c8498bb --- /dev/null +++ b/checks/check_wheel.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import platform +import sys + +from PIL import features + + +def test_wheel_modules() -> None: + expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"} + + if sys.platform == "win32": + # tkinter is not available in cibuildwheel installed CPython on Windows + try: + import tkinter + + assert tkinter + except ImportError: + expected_modules.remove("tkinter") + + # libavif is not available on Windows for ARM64 architectures + if platform.machine() == "ARM64": + expected_modules.remove("avif") + + elif sys.platform == "ios": + # tkinter is not available on iOS + expected_modules.remove("tkinter") + + assert set(features.get_supported_modules()) == expected_modules + + +def test_wheel_codecs() -> None: + expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"} + + assert set(features.get_supported_codecs()) == expected_codecs + + +def test_wheel_features() -> None: + expected_features = { + "raqm", + "fribidi", + "harfbuzz", + "libjpeg_turbo", + "zlib_ng", + "xcb", + } + + if sys.platform == "win32": + expected_features.remove("xcb") + elif sys.platform == "ios": + # Can't distribute raqm due to licensing, and there's no system version; + # fribidi and harfbuzz won't be available if raqm isn't available. + expected_features -= {"raqm", "fribidi", "harfbuzz"} + + assert set(features.get_supported_features()) == expected_features diff --git a/codecov.yml b/codecov.yml index 1ea7974ebbe..c29b4bc9015 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,7 +1,7 @@ # Documentation: https://docs.codecov.com/docs/codecov-yaml codecov: - # Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" + # Avoid "Missing base report" due to committing with "[CI skip]" # https://github.com/codecov/support/issues/363 # https://docs.codecov.com/docs/comparing-commits allow_coverage_offsets: true @@ -16,7 +16,5 @@ coverage: # Matches 'omit:' in .coveragerc ignore: - - "Tests/32bit_segfault_check.py" - - "Tests/bench_cffi_access.py" - - "Tests/check_*.py" + - "checks/*.py" - "Tests/createfontdatachunk.py" diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh new file mode 100755 index 00000000000..f0d1d851dd1 --- /dev/null +++ b/depends/docker-test-valgrind-memory.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +## Run this as the test script in the Docker valgrind image. +## Note -- can be included directly into the Docker image, +## but requires the current python.supp. + +source /vpy3/bin/activate +cd /Pillow +make clean +make install +make valgrind-leak diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh index 04bfbc7556b..52010475376 100755 --- a/depends/download-and-extract.sh +++ b/depends/download-and-extract.sh @@ -5,7 +5,10 @@ archive=$1 url=$2 if [ ! -f $archive.tar.gz ]; then - wget --no-verbose -O $archive.tar.gz $url + wget -O $archive.tar.gz $url \ + --no-verbose \ + --retry-connrefused \ + --retry-on-http-error=429,503,504 fi rmdir $archive diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 9dd7742ed34..de63abdecc7 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -2,7 +2,7 @@ # install libimagequant archive_name=libimagequant -archive_version=4.3.1 +archive_version=4.4.1 archive=$archive_name-$archive_version @@ -23,14 +23,14 @@ else cargo cinstall --prefix=/usr --destdir=. # Copy into place - sudo cp usr/lib/libimagequant.so* /usr/lib/ + sudo find usr -name libimagequant.so* -exec cp {} /usr/lib/ \; sudo cp usr/include/libimagequant.h /usr/include/ if [ -n "$GITHUB_ACTIONS" ]; then # Copy to cache rm -rf ~/cache-$archive_name mkdir ~/cache-$archive_name - cp usr/lib/libimagequant.so* ~/cache-$archive_name/ + find usr -name libimagequant.so* -exec cp {} ~/cache-$archive_name/ \; cp usr/include/libimagequant.h ~/cache-$archive_name/ fi diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh new file mode 100755 index 00000000000..2c5687391a9 --- /dev/null +++ b/depends/install_libavif.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +set -eo pipefail + +version=1.4.1 + +if [[ "$GHA_LIBAVIF_CACHE_HIT" == "true" ]]; then + + LIBDIR=/usr/lib/x86_64-linux-gnu + + # Copy cached files into place + sudo cp ~/cache-libavif/lib/* $LIBDIR/ + sudo cp -r ~/cache-libavif/include/avif /usr/include/ + +else + + ./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz + + pushd libavif-$version + + if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then + PREFIX=$(brew --prefix) + else + PREFIX=/usr + fi + + PKGCONFIG=${PKGCONFIG:-pkg-config} + + LIBAVIF_CMAKE_FLAGS=() + HAS_DECODER=0 + HAS_ENCODER=0 + + if $PKGCONFIG --exists aom; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM) + HAS_ENCODER=1 + HAS_DECODER=1 + fi + + if $PKGCONFIG --exists dav1d; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM) + HAS_DECODER=1 + fi + + if $PKGCONFIG --exists libgav1; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM) + HAS_DECODER=1 + fi + + if $PKGCONFIG --exists rav1e; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM) + HAS_ENCODER=1 + fi + + if $PKGCONFIG --exists SvtAv1Enc; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM) + HAS_ENCODER=1 + fi + + if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL) + fi + + cmake \ + -DCMAKE_INSTALL_PREFIX=$PREFIX \ + -DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_MACOSX_RPATH=OFF \ + -DAVIF_LIBSHARPYUV=LOCAL \ + -DAVIF_LIBYUV=LOCAL \ + "${LIBAVIF_CMAKE_FLAGS[@]}" \ + . + + sudo make install + + if [ -n "$GITHUB_ACTIONS" ] && [ "$(uname)" != "Darwin" ]; then + # Copy to cache + LIBDIR=/usr/lib/x86_64-linux-gnu + rm -rf ~/cache-libavif + mkdir -p ~/cache-libavif/lib + mkdir -p ~/cache-libavif/include + cp $LIBDIR/libavif.so* ~/cache-libavif/lib/ + cp -r /usr/include/avif ~/cache-libavif/include/ + fi + + popd + +fi diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index 8c2967bc21b..bc7c7c634ed 100755 --- a/depends/install_openjpeg.sh +++ b/depends/install_openjpeg.sh @@ -1,7 +1,7 @@ #!/bin/bash # install openjpeg -archive=openjpeg-2.5.2 +archive=openjpeg-2.5.4 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index 070ba23a178..33bb2d0a77f 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -2,12 +2,12 @@ # install raqm -archive=libraqm-0.10.1 +archive=libraqm-0.10.3 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz pushd $archive -meson build --prefix=/usr && sudo ninja -C build install +meson build --prefix=/usr && ninja -C build install popd diff --git a/depends/install_webp.sh b/depends/install_webp.sh index c47fb35f125..c328fe2c86a 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,12 +1,32 @@ #!/bin/bash # install webp -archive=libwebp-1.4.0 +archive=libwebp-1.6.0 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz +if [[ "$GHA_LIBWEBP_CACHE_HIT" == "true" ]]; then -pushd $archive + # Copy cached files into place + sudo cp ~/cache-libwebp/lib/* /usr/lib/ + sudo cp -r ~/cache-libwebp/include/webp /usr/include/ -./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install +else -popd + ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz + + pushd $archive + + ./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install + + if [ -n "$GITHUB_ACTIONS" ]; then + # Copy to cache + rm -rf ~/cache-libwebp + mkdir -p ~/cache-libwebp/lib + mkdir -p ~/cache-libwebp/include + cp /usr/lib/libwebp*.so* /usr/lib/libwebp*.a ~/cache-libwebp/lib/ + cp /usr/lib/libsharpyuv.so* /usr/lib/libsharpyuv.a ~/cache-libwebp/lib/ + cp -r /usr/include/webp ~/cache-libwebp/include/ + fi + + popd + +fi diff --git a/docs/COPYING b/docs/COPYING index d5ee19f81a6..1852f9e4786 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-2024 by Jeffrey A. Clark and contributors + Copyright © 2010 by Jeffrey 'Alex' Clark and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/docs/Guardfile b/docs/Guardfile deleted file mode 100755 index 16a891a730d..00000000000 --- a/docs/Guardfile +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -from livereload.compiler import shell -from livereload.task import Task - -Task.add("*.rst", shell("make html")) -Task.add("*/*.rst", shell("make html")) -Task.add("Makefile", shell("make html")) -Task.add("conf.py", shell("make html")) diff --git a/docs/Makefile b/docs/Makefile index 8f13f1aea1c..8c10192940d 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,207 +3,67 @@ # You can set these variables from the command line. PYTHON = python3 -SPHINXOPTS = SPHINXBUILD = $(PYTHON) -m sphinx.cmd.build -PAPER = +SPHINXOPTS = --fail-on-warning BUILDDIR = _build +BUILDER = html +JOBS = auto +PAPER = # Internal variables. PAPEROPT_a4 = --define latex_paper_size=a4 PAPEROPT_letter = --define latex_paper_size=letter -ALLSPHINXOPTS = --doctree-dir $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +ALLSPHINXOPTS = --builder $(BUILDER) \ + --doctree-dir $(BUILDDIR)/doctrees \ + --jobs $(JOBS) \ + $(PAPEROPT_$(PAPER)) \ + $(SPHINXOPTS) \ + . $(BUILDDIR)/$(BUILDER) .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " htmlview to open the index page built by the html target in your browser" + @echo " htmllive to rebuild and reload HTML files in your browser" @echo " serve to start a local server for viewing docs" - @echo " livehtml to start a local server for viewing docs and auto-reload on change" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" .PHONY: clean clean: -rm -rf $(BUILDDIR)/* install-sphinx: - $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinxext-opengraph + $(PYTHON) -m pip install -e ..[docs] .PHONY: html html: $(MAKE) install-sphinx - $(SPHINXBUILD) --builder html --fail-on-warning --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + $(SPHINXBUILD) $(ALLSPHINXOPTS) .PHONY: dirhtml -dirhtml: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." +dirhtml: BUILDER = dirhtml +dirhtml: html .PHONY: singlehtml -singlehtml: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -.PHONY: pickle -pickle: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -.PHONY: json -json: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -.PHONY: htmlhelp -htmlhelp: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -.PHONY: qthelp -qthelp: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PillowPILfork.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc" - -.PHONY: devhelp -devhelp: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/PillowPILfork" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PillowPILfork" - @echo "# devhelp" - -.PHONY: epub -epub: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -.PHONY: latex -latex: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -.PHONY: latexpdf -latexpdf: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder 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." - -.PHONY: text -text: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -.PHONY: man -man: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -.PHONY: texinfo -texinfo: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -.PHONY: info -info: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder 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." - -.PHONY: gettext -gettext: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -.PHONY: changes -changes: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." +singlehtml: BUILDER = singlehtml +singlehtml: html .PHONY: linkcheck -linkcheck: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder 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." - -.PHONY: doctest -doctest: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." +linkcheck: BUILDER = linkcheck +linkcheck: html .PHONY: htmlview htmlview: html $(PYTHON) -c "import os, webbrowser; webbrowser.open('file://' + os.path.realpath('$(BUILDDIR)/html/index.html'))" -.PHONY: livehtml -livehtml: html - livereload $(BUILDDIR)/html -p 33233 +.PHONY: htmllive +htmllive: SPHINXBUILD = $(PYTHON) -m sphinx_autobuild +htmllive: SPHINXOPTS = --open-browser --delay 0 +htmllive: html .PHONY: serve serve: diff --git a/docs/PIL.rst b/docs/PIL.rst index bdbf1373d88..5225e9644cd 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -1,10 +1,10 @@ -PIL Package (autodoc of remaining modules) +PIL package (autodoc of remaining modules) ========================================== Reference for modules whose documentation has not yet been ported or written can be found here. -:mod:`PIL` Module +:mod:`PIL` module ----------------- .. py:module:: PIL @@ -12,7 +12,7 @@ can be found here. .. autoexception:: UnidentifiedImageError :show-inheritance: -:mod:`~PIL.BdfFontFile` Module +:mod:`~PIL.BdfFontFile` module ------------------------------ .. automodule:: PIL.BdfFontFile @@ -20,7 +20,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.ContainerIO` Module +:mod:`~PIL.ContainerIO` module ------------------------------ .. automodule:: PIL.ContainerIO @@ -28,7 +28,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.FontFile` Module +:mod:`~PIL.FontFile` module --------------------------- .. automodule:: PIL.FontFile @@ -36,7 +36,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.GdImageFile` Module +:mod:`~PIL.GdImageFile` module ------------------------------ .. automodule:: PIL.GdImageFile @@ -44,7 +44,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.GimpGradientFile` Module +:mod:`~PIL.GimpGradientFile` module ----------------------------------- .. automodule:: PIL.GimpGradientFile @@ -52,7 +52,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.GimpPaletteFile` Module +:mod:`~PIL.GimpPaletteFile` module ---------------------------------- .. automodule:: PIL.GimpPaletteFile @@ -60,7 +60,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.ImageDraw2` Module +:mod:`~PIL.ImageDraw2` module ----------------------------- .. automodule:: PIL.ImageDraw2 @@ -69,7 +69,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.ImageMode` Module +:mod:`~PIL.ImageMode` module ---------------------------- .. automodule:: PIL.ImageMode @@ -77,7 +77,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.PaletteFile` Module +:mod:`~PIL.PaletteFile` module ------------------------------ .. automodule:: PIL.PaletteFile @@ -85,7 +85,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.PcfFontFile` Module +:mod:`~PIL.PcfFontFile` module ------------------------------ .. automodule:: PIL.PcfFontFile @@ -93,7 +93,7 @@ can be found here. :undoc-members: :show-inheritance: -:class:`.PngImagePlugin.iTXt` Class +:class:`.PngImagePlugin.iTXt` class ----------------------------------- .. autoclass:: PIL.PngImagePlugin.iTXt @@ -107,7 +107,7 @@ 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 @@ -116,7 +116,7 @@ can be found here. :show-inheritance: -:mod:`~PIL.TarIO` Module +:mod:`~PIL.TarIO` module ------------------------ .. automodule:: PIL.TarIO @@ -124,7 +124,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.WalImageFile` Module +:mod:`~PIL.WalImageFile` module ------------------------------- .. automodule:: PIL.WalImageFile diff --git a/docs/about.rst b/docs/about.rst index 98cdd8e5ab0..7df895b8ffc 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -6,19 +6,18 @@ Goals The fork author's goal is to foster and support active development of PIL through: -- Continuous integration testing via `GitHub Actions`_ and `AppVeyor`_ +- Continuous integration testing via `GitHub Actions`_ - Publicized development activity on `GitHub`_ - Regular releases to the `Python Package Index`_ .. _GitHub Actions: https://github.com/python-pillow/Pillow/actions -.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow .. _GitHub: https://github.com/python-pillow/Pillow .. _Python Package Index: https://pypi.org/project/pillow/ License ------- -Like PIL, Pillow is `licensed under the open source HPND License `_ +Like PIL, Pillow is `licensed under the open source MIT-CMU License `_ Why a fork? ----------- diff --git a/docs/conf.py b/docs/conf.py index f12b30e65fb..189758944d9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = "7.3" +needs_sphinx = "8.2" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -55,9 +55,9 @@ project = "Pillow (PIL Fork)" copyright = ( "1995-2011 Fredrik Lundh and contributors, " - "2010-2024 Jeffrey A. Clark and contributors." + "2010 Jeffrey 'Alex' Clark and contributors." ) -author = "Fredrik Lundh (PIL), Jeffrey A. Clark (Pillow)" +author = "Fredrik Lundh (PIL), Jeffrey 'Alex' Clark (Pillow)" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -121,7 +121,7 @@ # 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 = [] +nitpick_ignore = [("py:class", "_CmsProfileCompatible")] # -- Options for HTML output ---------------------------------------------- @@ -229,97 +229,6 @@ # implements a search results scorer. If empty, the default will be used. # html_search_scorer = 'scorer.js' -# Output file base name for HTML help builder. -htmlhelp_basename = "PillowPILForkdoc" - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements: dict[str, str] = { - # 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", - "Pillow (PIL Fork) Documentation", - "Jeffrey A. Clark", - "manual", - ) -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# If true, show page references after internal links. -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, "pillowpilfork", "Pillow (PIL Fork) Documentation", [author], 1) -] - -# If true, show URL addresses after external links. -# man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - master_doc, - "PillowPILFork", - "Pillow (PIL Fork) Documentation", - author, - "PillowPILFork", - "Pillow is the friendly PIL fork by Jeffrey A. Clark and contributors.", - "Miscellaneous", - ) -] - -# Documents to append as an appendix to all manuals. -# texinfo_appendices = [] - -# If false, no module index is generated. -# texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -# texinfo_no_detailmenu = False - linkcheck_allowed_redirects = { r"https://www.bestpractices.dev/projects/6331": r"https://www.bestpractices.dev/en/.*", @@ -338,8 +247,6 @@ # https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html _repo = "https://github.com/python-pillow/Pillow/" extlinks = { - "cve": ("https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-%s", "CVE-%s"), - "cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"), "issue": (_repo + "issues/%s", "#%s"), "pr": (_repo + "pull/%s", "#%s"), "pypi": ("https://pypi.org/project/%s/", "%s"), diff --git a/docs/dater.py b/docs/dater.py index f9fb0c1da6e..87dacbd5a12 100644 --- a/docs/dater.py +++ b/docs/dater.py @@ -8,10 +8,10 @@ import re import subprocess -from typing import TYPE_CHECKING +TYPE_CHECKING = False if TYPE_CHECKING: - from sphinx.application import Sphinx + from typing import Any DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+") VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n") @@ -28,7 +28,7 @@ def get_date_for(git_version: str) -> str | None: return out.split()[0] -def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None: +def add_date(app: Any, doc_name: str, source: list[str]) -> None: if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])): old_title = m.group(1) @@ -43,6 +43,6 @@ def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None: source[0] = result -def setup(app: Sphinx) -> dict[str, bool]: +def setup(app: Any) -> dict[str, bool]: app.connect("source-read", add_date) return {"parallel_read_safe": True} diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 0f996a94bec..b6a7af0a824 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,57 +12,113 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a :py:exc:`DeprecationWarning` is issued. -PSFile -~~~~~~ +ExifTags.IFD.Makernote +^^^^^^^^^^^^^^^^^^^^^^ -.. deprecated:: 9.5.0 +.. deprecated:: 11.1.0 -The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will -be removed in Pillow 11 (2024-10-15). This class was only made as a helper to -be used internally, so there is no replacement. If you need this functionality -though, it is a very short class that can easily be recreated in your own code. +``ExifTags.IFD.Makernote`` has been deprecated. Instead, use +``ExifTags.IFD.MakerNote``. -PyAccess and Image.USE_CFFI_ACCESS -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Image.Image.get_child_images() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. deprecated:: 10.0.0 +.. deprecated:: 11.2.1 + +``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow +13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The +method uses an image's file pointer, and so child images could only be retrieved from +an :py:class:`PIL.ImageFile.ImageFile` instance. + +Image.fromarray mode parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.3.0 + +Using the ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` was deprecated in +Pillow 11.3.0. In Pillow 12.0.0, this was partially reverted, and it is now only +deprecated when changing data types. Since pixel values do not contain information +about palettes or color spaces, the parameter can still be used to place grayscale L +mode data within a P mode image, or read RGB data as YCbCr for example. If omitted, the +mode will be automatically determined from the object's shape and type. + +Saving I mode images as PNG +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.3.0 + +In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain +at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly +changing the data, this is now deprecated. Instead, the image can be converted to +another mode before saving:: + + from PIL import Image + im = Image.new("I", (1, 1)) + im.convert("I;16").save("out.png") + +ImageCms.ImageCmsProfile.product_name and .product_info +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 12.0.0 + +``ImageCms.ImageCmsProfile.product_name`` and the corresponding +``.product_info`` attributes have been deprecated, and will be removed in +Pillow 13 (2026-10-15). They have been set to ``None`` since Pillow 2.3.0. + +Image._show +~~~~~~~~~~~ -Since Pillow's C API is now faster than PyAccess on PyPy, -:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow -11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. +.. deprecated:: 12.0.0 -``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is -similarly deprecated. +``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15). +Use :py:meth:`~PIL.ImageShow.show` instead. + +Image getdata() +~~~~~~~~~~~~~~~ + +.. deprecated:: 12.1.0 + +:py:meth:`~PIL.Image.Image.getdata` has been deprecated. +:py:meth:`~PIL.Image.Image.get_flattened_data` can be used instead. This new method is +identical, except that it returns a tuple of pixel values, instead of an internal +Pillow data type. + +Removed features +---------------- + +Deprecated features are only removed in major releases after an appropriate +period of deprecation has passed. ImageFile.raise_oserror -~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 10.2.0 +.. versionremoved:: 12.0.0 -``ImageFile.raise_oserror()`` has been deprecated and will be removed in Pillow -12.0.0 (2025-10-15). The function is undocumented and is only useful for translating -error codes returned by a codec's ``decode()`` method, which ImageFile already does -automatically. +``ImageFile.raise_oserror()`` has been removed. The function was undocumented and was +only useful for translating error codes returned by a codec's ``decode()`` method, +which ImageFile already did automatically. IptcImageFile helper functions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 10.2.0 +.. versionremoved:: 12.0.0 The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant -``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow -12.0.0 (2025-10-15). These are undocumented helper functions intended -for internal use, so there is no replacement. They can each be replaced -by a single line of code using builtin functions in Python. +``IptcImageFile.PAD`` have been removed. These were undocumented helper functions +intended for internal use, so there is no replacement. They can each be replaced by a +single line of code using builtin functions in Python. ImageCms constants and versions() function -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 10.3.0 +.. versionremoved:: 12.0.0 -A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. -This includes a table of flags based on LittleCMS version 1 which has been -replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. +A number of constants and a function in :py:mod:`.ImageCms` have been removed. This +includes a table of flags based on LittleCMS version 1 which has been replaced with a +new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. ============================================ ==================================================== Deprecated Use instead @@ -96,46 +152,146 @@ ImageMath eval() ^^^^^^^^^^^^^^^^ .. deprecated:: 10.3.0 +.. versionremoved:: 12.0.0 -``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or +``ImageMath.eval()`` has been removed. Use :py:meth:`~PIL.ImageMath.lambda_eval` or :py:meth:`~PIL.ImageMath.unsafe_eval` instead. BGR;15, BGR 16 and BGR;24 ^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 -The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated. +The experimental BGR;15, BGR;16 and BGR;24 modes have been removed. Non-image modes in ImageCms ^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 The use in :py:mod:`.ImageCms` of input modes and output modes that are not Pillow -image modes has been deprecated. Defaulting to "L" or "1" if the mode cannot be mapped -is also deprecated. +image modes has been removed. Defaulting to "L" or "1" if the mode cannot be mapped has +also been removed. Support for LibTIFF earlier than 4 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 -Support for LibTIFF earlier than version 4 has been deprecated. +Support for LibTIFF earlier than version 4 has been removed. Upgrade to a newer version of LibTIFF instead. ImageDraw.getdraw hints parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 -The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. +The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been removed. -Removed features ----------------- +FreeType 2.9.0 +^^^^^^^^^^^^^^ -Deprecated features are only removed in major releases after an appropriate -period of deprecation has passed. +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +Support for FreeType 2.9.0 has been removed. FreeType 2.9.1 is the minimum version +supported. + +We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ + +ICNS (width, height, scale) sizes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +Setting an ICNS image size to ``(width, height, scale)`` before loading has been +removed. Instead, ``load(scale)`` can be used. + +Image isImageType() +^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +``Image.isImageType(im)`` has been removed. Use ``isinstance(im, Image.Image)`` +instead. + +ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and +:py:meth:`~PIL.ImageMath.unsafe_eval()` has been removed. One or more keyword +arguments can be used instead. + +JpegImageFile.huffman_ac and JpegImageFile.huffman_dc +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They +have been removed. + +Specific WebP feature checks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +``features.check("transp_webp")``, ``features.check("webp_mux")`` and +``features.check("webp_anim")`` have been removed. + +Get internal pointers to objects +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been +removed. They were used for obtaining raw pointers to ``ImagingCore`` internals. To +interact with C code, you can use ``Image.Image.getim()``, which returns a ``Capsule`` +object. + +TiffImagePlugin IFD_LEGACY_API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionremoved:: 11.0.0 + +``TiffImagePlugin.IFD_LEGACY_API`` has been removed, as it was an unused setting. + +PSFile +~~~~~~ + +.. deprecated:: 9.5.0 +.. versionremoved:: 11.0.0 + +The :py:class:`!PSFile` class was removed in Pillow 11 (2024-10-15). +This class was only made as a helper to be used internally, +so there is no replacement. If you need this functionality though, +it is a very short class that can easily be recreated in your own code. + +PyAccess and Image.USE_CFFI_ACCESS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 10.0.0 +.. versionremoved:: 11.0.0 + +Since Pillow's C API is now faster than PyAccess on PyPy, ``PyAccess`` has been +removed. Pillow's C API will now be used on PyPy instead. + +``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, was +similarly removed. Tk/Tcl 8.4 ~~~~~~~~~~ diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 2a2a0ba2961..e0557976c28 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -14,6 +14,7 @@ import struct from io import BytesIO +from typing import IO from PIL import Image, ImageFile @@ -94,26 +95,26 @@ DXT5_FOURCC = 0x35545844 -def _decode565(bits): +def _decode565(bits: int) -> tuple[int, int, int]: a = ((bits >> 11) & 0x1F) << 3 b = ((bits >> 5) & 0x3F) << 2 c = (bits & 0x1F) << 3 return a, b, c -def _c2a(a, b): +def _c2a(a: int, b: int) -> int: return (2 * a + b) // 3 -def _c2b(a, b): +def _c2b(a: int, b: int) -> int: return (a + b) // 2 -def _c3(a, b): +def _c3(a: int, b: int) -> int: return (2 * b + a) // 3 -def _dxt1(data, width, height): +def _dxt1(data: IO[bytes], width: int, height: int) -> bytes: # TODO implement this function as pixel format in decode.c ret = bytearray(4 * width * height) @@ -151,7 +152,7 @@ def _dxt1(data, width, height): return bytes(ret) -def _dxtc_alpha(a0, a1, ac0, ac1, ai): +def _dxtc_alpha(a0: int, a1: int, ac0: int, ac1: int, ai: int) -> int: if ai <= 12: ac = (ac0 >> ai) & 7 elif ai == 15: @@ -175,7 +176,7 @@ def _dxtc_alpha(a0, a1, ac0, ac1, ai): return alpha -def _dxt5(data, width, height): +def _dxt5(data: IO[bytes], width: int, height: int) -> bytes: # TODO implement this function as pixel format in decode.c ret = bytearray(4 * width * height) @@ -211,7 +212,8 @@ class DdsImageFile(ImageFile.ImageFile): format = "DDS" format_description = "DirectDraw Surface" - def _open(self): + def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(4)): msg = "not a DDS file" raise SyntaxError(msg) @@ -242,19 +244,22 @@ def _open(self): elif fourcc == b"DXT5": self.decoder = "DXT5" else: - msg = f"Unimplemented pixel format {fourcc}" + msg = f"Unimplemented pixel format {repr(fourcc)}" raise NotImplementedError(msg) - self.tile = [(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] + self.tile = [ + ImageFile._Tile(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1)) + ] - def load_seek(self, pos): + def load_seek(self, pos: int) -> None: pass class DXT1Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: + assert self.fd is not None try: self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize)) except struct.error as e: @@ -266,7 +271,8 @@ def decode(self, buffer): class DXT5Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: + assert self.fd is not None try: self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize)) except struct.error as e: @@ -279,8 +285,8 @@ def decode(self, buffer): Image.register_decoder("DXT5", DXT5Decoder) -def _accept(prefix): - return prefix[:4] == b"DDS " +def _accept(prefix: bytes) -> bool: + return prefix.startswith(b"DDS ") Image.register_open(DdsImageFile.format, DdsImageFile, _accept) diff --git a/docs/handbook/animated_hopper.gif b/docs/handbook/animated_hopper.gif new file mode 100644 index 00000000000..e6eeaffcc4e Binary files /dev/null and b/docs/handbook/animated_hopper.gif differ diff --git a/docs/handbook/appendices.rst b/docs/handbook/appendices.rst index 347a8848b37..c20d8bc8bb2 100644 --- a/docs/handbook/appendices.rst +++ b/docs/handbook/appendices.rst @@ -8,4 +8,5 @@ Appendices image-file-formats text-anchors + third-party-plugins writing-your-own-image-plugin diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 7da1078c14e..46f612be3b2 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -30,35 +30,35 @@ image. Each pixel uses the full range of the bit depth. So a 1-bit pixel has a r INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current release supports the following standard modes: - * ``1`` (1-bit pixels, black and white, stored with one pixel per byte) - * ``L`` (8-bit pixels, grayscale) - * ``P`` (8-bit pixels, mapped to any other mode using a color palette) - * ``RGB`` (3x8-bit pixels, true color) - * ``RGBA`` (4x8-bit pixels, true color with transparency mask) - * ``CMYK`` (4x8-bit pixels, color separation) - * ``YCbCr`` (3x8-bit pixels, color video format) +* ``1`` (1-bit pixels, black and white, stored with one pixel per byte) +* ``L`` (8-bit pixels, grayscale) +* ``P`` (8-bit pixels, mapped to any other mode using a color palette) +* ``RGB`` (3x8-bit pixels, true color) +* ``RGBA`` (4x8-bit pixels, true color with transparency mask) +* ``CMYK`` (4x8-bit pixels, color separation) +* ``YCbCr`` (3x8-bit pixels, color video format) - * Note that this refers to the JPEG, and not the ITU-R BT.2020, standard + * Note that this refers to the JPEG, and not the ITU-R BT.2020, standard - * ``LAB`` (3x8-bit pixels, the L*a*b color space) - * ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space) +* ``LAB`` (3x8-bit pixels, the L*a*b color space) +* ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space) - * Hue's range of 0-255 is a scaled version of 0 degrees <= Hue < 360 degrees + * Hue's range of 0-255 is a scaled version of 0 degrees <= Hue < 360 degrees - * ``I`` (32-bit signed integer pixels) - * ``F`` (32-bit floating point pixels) +* ``I`` (32-bit signed integer pixels) +* ``F`` (32-bit floating point pixels) Pillow also provides limited support for a few additional modes, including: - * ``LA`` (L with alpha) - * ``PA`` (P with alpha) - * ``RGBX`` (true color with padding) - * ``RGBa`` (true color with premultiplied alpha) - * ``La`` (L with premultiplied alpha) - * ``I;16`` (16-bit unsigned integer pixels) - * ``I;16L`` (16-bit little endian unsigned integer pixels) - * ``I;16B`` (16-bit big endian unsigned integer pixels) - * ``I;16N`` (16-bit native endian unsigned integer pixels) +* ``LA`` (L with alpha) +* ``PA`` (P with alpha) +* ``RGBX`` (true color with padding) +* ``RGBa`` (true color with premultiplied alpha) +* ``La`` (L with premultiplied alpha) +* ``I;16`` (16-bit unsigned integer pixels) +* ``I;16L`` (16-bit little endian unsigned integer pixels) +* ``I;16B`` (16-bit big endian unsigned integer pixels) +* ``I;16N`` (16-bit native endian unsigned integer pixels) Premultiplied alpha is where the values for each other channel have been multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)`` @@ -84,7 +84,7 @@ pixels. .. _coordinate-system: -Coordinate System +Coordinate system ----------------- The Python Imaging Library uses a Cartesian pixel coordinate system, with (0,0) @@ -101,6 +101,28 @@ Palette The palette mode (``P``) uses a color palette to define the actual color for each pixel. +.. _colors: + +Colors +------ + +To specify colors, you can use tuples with a value for each channel in the image, e.g. +``Image.new("RGB", (1, 1), (255, 0, 0))``. + +If an image has a single channel, you can use a single number instead, e.g. +``Image.new("L", (1, 1), 255)``. For "F" mode images, floating point values are also +accepted. In the case of "P" mode images, these will be indexes for the color palette. + +If a single value is used for an image with more than one channel, it will still be +parsed:: + + >>> from PIL import Image + >>> im = Image.new("RGBA", (1, 1), 0x04030201) + >>> im.getpixel((0, 0)) + (1, 2, 3, 4) + +Some methods accept other forms, such as color names. See :ref:`color-names`. + Info ---- diff --git a/docs/handbook/contrasted_hopper.jpg b/docs/handbook/contrasted_hopper.jpg new file mode 100644 index 00000000000..b2d7cfacbb6 Binary files /dev/null and b/docs/handbook/contrasted_hopper.jpg differ diff --git a/docs/handbook/cropped_hopper.webp b/docs/handbook/cropped_hopper.webp new file mode 100644 index 00000000000..8d0ae4f9757 Binary files /dev/null and b/docs/handbook/cropped_hopper.webp differ diff --git a/docs/handbook/enhanced_hopper.webp b/docs/handbook/enhanced_hopper.webp new file mode 100644 index 00000000000..a582ac0c216 Binary files /dev/null and b/docs/handbook/enhanced_hopper.webp differ diff --git a/docs/handbook/flip_left_right_hopper.webp b/docs/handbook/flip_left_right_hopper.webp new file mode 100644 index 00000000000..59452268dfe Binary files /dev/null and b/docs/handbook/flip_left_right_hopper.webp differ diff --git a/docs/handbook/flip_top_bottom_hopper.webp b/docs/handbook/flip_top_bottom_hopper.webp new file mode 100644 index 00000000000..28af4bf3659 Binary files /dev/null and b/docs/handbook/flip_top_bottom_hopper.webp differ diff --git a/docs/handbook/hopper_ps.webp b/docs/handbook/hopper_ps.webp new file mode 100644 index 00000000000..3dd2943d682 Binary files /dev/null and b/docs/handbook/hopper_ps.webp differ diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 1ec97214900..a9fd764e613 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -24,6 +24,83 @@ present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``. Fully supported formats ----------------------- +AVIF +^^^^ + +Pillow reads and writes AVIF files, including AVIF sequence images. +It is only possible to save 8-bit AVIF images, and all AVIF images are decoded +as 8-bit RGB(A). + +The :py:meth:`~PIL.Image.Image.save` method supports the following options: + +**quality** + Integer, 0-100, defaults to 75. 0 gives the smallest size and poorest + quality, 100 the largest size and best quality. + +**subsampling** + If present, sets the subsampling for the encoder. Defaults to ``4:2:0``. + Options include: + + * ``4:0:0`` + * ``4:2:0`` + * ``4:2:2`` + * ``4:4:4`` + +**speed** + Quality/speed trade-off (0=slower/better, 10=fastest). Defaults to 6. + +**max_threads** + Limit the number of active threads used. By default, there is no limit. If the aom + codec is used, there is a maximum of 64. + +**range** + YUV range, either "full" or "limited". Defaults to "full". + +**codec** + AV1 codec to use for encoding. Specific values are "aom", "rav1e", and + "svt", presuming the chosen codec is available. Defaults to "auto", which + will choose the first available codec in the order of the preceding list. + +**tile_rows** / **tile_cols** + For tile encoding, the (log 2) number of tile rows and columns to use. + Valid values are 0-6, default 0. Ignored if "autotiling" is set to true. + +**autotiling** + Split the image up to allow parallelization. Enabled automatically if "tile_rows" + and "tile_cols" both have their default values of zero. + +**alpha_premultiplied** + Encode the image with premultiplied alpha. Defaults to ``False``. + +**advanced** + Codec specific options. + +**icc_profile** + The ICC Profile to include in the saved file. + +**exif** + The exif data to include in the saved file. + +**xmp** + The XMP data to include in the saved file. + +Saving sequences +~~~~~~~~~~~~~~~~ + +When calling :py:meth:`~PIL.Image.Image.save` to write an AVIF 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 + images in the list can be single or multiframe images. + +**duration** + The display duration of each frame, in milliseconds. Pass a single + integer for a constant duration, or a list or tuple to set the + duration for each frame separately. + BLP ^^^ @@ -68,7 +145,7 @@ by DirectX. DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode. .. versionadded:: 3.4.0 - DXT3 images can be read in ``RGB`` mode and DX10 images can be read in + DXT3 images can be read in ``RGBA`` mode and DX10 images can be read in ``RGB`` and ``RGBA`` mode. .. versionadded:: 6.0.0 @@ -93,6 +170,12 @@ DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode. in ``P`` mode. +.. versionadded:: 11.2.1 + DXT1, DXT3, DXT5, BC2, BC3 and BC5 pixel formats can be saved:: + + im.save(out, pixel_format="DXT1") + + DIB ^^^ @@ -229,13 +312,14 @@ following options are available:: im.save(out, save_all=True, append_images=[im1, im2, ...]) **save_all** - If present and true, all frames of the image will be saved. If - not, then only the first frame of a multiframe image will be saved. + If present and true, or if ``append_images`` is not empty, all frames of + the image will be saved. Otherwise, only the first frame of a multiframe + image will be saved. **append_images** A list of images to append as additional frames. Each of the images in the list can be single or multiframe images. - This is currently supported for GIF, PDF, PNG, TIFF, and WebP. + This is supported for AVIF, GIF, PDF, PNG, TIFF and WebP. It is also supported for ICO and ICNS. If images are passed in of relevant sizes, they will be used instead of scaling down the main image. @@ -324,12 +408,19 @@ sets the following :py:attr:`~PIL.Image.Image.info` property: **sizes** A list of supported sizes found in this icon file; these are a 3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina - icon and 1 for a standard icon. You *are* permitted to use this 3-tuple - format for the :py:attr:`~PIL.Image.Image.size` property if you set it - before calling :py:meth:`~PIL.Image.Image.load`; after loading, the size - will be reset to a 2-tuple containing pixel dimensions (so, e.g. if you - ask for ``(512, 512, 2)``, the final value of - :py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``). + icon and 1 for a standard icon. + +.. _icns-loading: + +Loading +~~~~~~~ + +You can call the :py:meth:`~PIL.Image.Image.load` method with the following parameter. + +**scale** + Affects the scale of the resultant image. If the size is set to ``(512, 512)``, + after loading at scale 2, the final value of :py:attr:`~PIL.Image.Image.size` will + be ``(1024, 1024)``. .. _icns-saving: @@ -447,7 +538,8 @@ The :py:meth:`~PIL.Image.open` method may set the following Raw EXIF data from the image. **comment** - A comment about the image. + A comment about the image, from the COM marker. This is separate from the + UserComment tag that may be stored in the EXIF data. .. versionadded:: 7.1.0 @@ -465,6 +557,8 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: hardly any gain in image quality. The value ``keep`` is only valid for JPEG files and will retain the original image quality level, subsampling, and qtables. + For more information on how qtables are modified based on the quality parameter, + see the qtables section. **optimize** If present and true, indicates that the encoder should make an extra pass @@ -530,6 +624,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: range(len(keys))) of lists of 64 integers. There must be between 2 and 4 tables. + If a quality parameter is provided, the qtables will be adjusted accordingly. + By default, the qtables are based on a standard JPEG table with a quality of 50. + The qtable values will be reduced if the quality is higher than 50 and increased + if the quality is lower than 50. + .. versionadded:: 2.5.0 **streamtype** @@ -565,10 +664,19 @@ JPEG 2000 Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``, ``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to ``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel. -Beginning with version 8.3.0, Pillow can read (but not write) ``RGB``, -``RGBA``, and ``YCbCr`` images with subsampled components. Pillow supports -JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 files -(``.jp2`` or ``.jpx`` files). + +.. versionadded:: 8.3.0 + Pillow can read (but not write) ``RGB``, ``RGBA``, and ``YCbCr`` images with + subsampled components. + +.. versionadded:: 10.4.0 + Pillow can read ``CMYK`` images with OpenJPEG 2.5.1 and later. + +.. versionadded:: 11.1.0 + Pillow can write ``CMYK`` images with OpenJPEG 2.5.3 and later. + +Pillow supports JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed +JPEG 2000 files (``.jp2`` or ``.jpx`` files). When loading, if you set the ``mode`` on the image prior to the :py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to @@ -685,6 +793,30 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: you fail to do this, you will get errors about not being able to load the ``_imaging`` DLL). +MPO +^^^ + +Pillow reads and writes Multi Picture Object (MPO) files. When first opened, it loads +the primary image. 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, or if ``append_images`` is not empty, all frames +will be saved. + +**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 + MSP ^^^ @@ -696,16 +828,6 @@ PCX Pillow reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` data. -PFM -^^^ - -.. versionadded:: 10.3.0 - -Pillow reads and writes grayscale (Pf format) Portable FloatMap (PFM) files -containing ``F`` data. - -Color (PF format) PFM files are not supported. - Opening ~~~~~~~ @@ -867,7 +989,7 @@ where applicable: The number of times to loop this APNG, 0 indicates infinite looping. **duration** - The time to display this APNG frame (in milliseconds). + The time to display this APNG frame (in milliseconds), given as a float. .. note:: @@ -887,7 +1009,8 @@ 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: +parameter should be set to ``True`` or ``append_images`` should not be empty. The +following parameters can also be set: **default_image** Boolean value, specifying whether or not the base image is a default image. @@ -908,9 +1031,8 @@ parameter must be set to ``True``. The following parameters can also be set: Defaults to 0. **duration** - Integer (or list or tuple of integers) length of time to display this APNG frame - (in milliseconds). - Defaults to 0. + The length of time (or list or tuple of lengths 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 @@ -949,12 +1071,39 @@ parameter must be set to ``True``. The following parameters can also be set: PPM ^^^ -Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or -``RGB`` data. +Pillow reads and writes PBM, PGM, PPM, PNM and PFM files containing ``1``, ``L``, ``I``, +``RGB`` or ``F`` data. "Raw" (P4 to P6) formats can be read, and are used when writing. -Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well. +.. versionadded:: 9.2.0 + "Plain" (P1 to P3) formats can be read. + +.. versionadded:: 10.3.0 + Grayscale (Pf format) Portable FloatMap (PFM) files containing + ``F`` data can be read and used when writing. + +Color (PF format) PFM files are not supported. + +QOI +^^^ + +.. versionadded:: 9.5.0 + +Pillow reads and writes images in Quite OK Image format using a Python codec. If you +wish to write code specifically for this format, :pypi:`qoi` is an alternative library +that uses C to decode the image and interfaces with NumPy. + +.. _qoi-saving: + +Saving +~~~~~~ + +The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: + +**colorspace** + If set to "sRGB", the colorspace will be written as sRGB with linear alpha, instead + of all channels being linear. SGI ^^^ @@ -1096,7 +1245,7 @@ numbers are returned as a tuple of ``(numerator, denominator)``. .. deprecated:: 3.0.0 -Reading Multi-frame TIFF Images +Reading multi-frame TIFF images ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The TIFF loader supports the :py:meth:`~PIL.Image.Image.seek` and @@ -1116,15 +1265,14 @@ Saving The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: **save_all** - If true, Pillow will save all frames of the image to a multiframe tiff document. + If true, or if ``append_images`` is not empty, Pillow will save all frames of the + image to a multiframe tiff document. .. 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. + images in the list can be single or multiframe images. .. versionadded:: 4.2.0 @@ -1168,6 +1316,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 8.4.0 +**big_tiff** + If true, the image will be saved as a BigTIFF. + + .. versionadded:: 11.1.0 + **compression** A string containing the desired compression method for the file. (valid only with libtiff installed) Valid compression @@ -1220,8 +1373,7 @@ using the general tags available through tiffinfo. WebP ^^^^ -Pillow reads and writes WebP files. The specifics of Pillow's capabilities with -this format are currently undocumented. +Pillow reads and writes WebP files. Requires libwebp v0.5.0 or later. .. _webp-saving: @@ -1249,33 +1401,23 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **exact** If true, preserve the transparent RGB values. Otherwise, discard invisible RGB values for better compression. Defaults to false. - Requires libwebp 0.5.0 or later. **icc_profile** - The ICC Profile to include in the saved file. Only supported if - the system WebP library was built with webpmux support. + The ICC Profile to include in the saved file. **exif** - The exif data to include in the saved file. Only supported if - the system WebP library was built with webpmux support. + The exif data to include in the saved file. **xmp** - The XMP data to include in the saved file. Only supported if - the system WebP library was built with webpmux support. + The XMP data to include in the saved file. 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")``. - 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. +argument is present and true, or if ``append_images`` is not empty, 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 @@ -1439,30 +1581,6 @@ Note that there may be an embedded gamma of 2.2 in MIC files. To enable MIC support, you must install :pypi:`olefile`. -MPO -^^^ - -Pillow identifies and reads Multi Picture Object (MPO) files, loading the primary -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 ^^^ @@ -1483,15 +1601,6 @@ PSD Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0. -QOI -^^^ - -.. versionadded:: 9.5.0 - -Pillow reads images in Quite OK Image format using a Python decoder. If you wish to -write code specifically for this format, :pypi:`qoi` is an alternative library that -uses C to decode the image and interfaces with NumPy. - SUN ^^^ @@ -1528,19 +1637,21 @@ To add other read or write support, use :py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF handler. :: - from PIL import Image + from typing import IO + + from PIL import Image, ImageFile from PIL import WmfImagePlugin - class WmfHandler: - def open(self, im): + class WmfHandler(ImageFile.StubHandler): + def open(self, im: ImageFile.StubImageFile) -> None: ... - def load(self, im): + def load(self, im: ImageFile.StubImageFile) -> Image.Image: ... return image - def save(self, im, fp, filename): + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: ... @@ -1553,7 +1664,8 @@ handler. :: XPM ^^^ -Pillow reads X pixmap files (mode ``P``) with 256 colors or less. +Pillow reads X pixmap files as P mode images if there are 256 colors or less, and as +RGB images otherwise. .. _xpm-opening: @@ -1567,6 +1679,11 @@ The :py:meth:`~PIL.Image.open` method sets the following Transparency color index. This key is omitted if the image is not transparent. +XV thumbnails +^^^^^^^^^^^^^ + +Pillow can read XV thumbnail files. + Write-only formats ------------------ @@ -1599,15 +1716,14 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum **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``. + parameter should be present and set to ``True`` or ``append_images`` should not be + empty. .. 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``. + of the images in the list can be single or multiframe images. .. versionadded:: 4.2.0 @@ -1673,11 +1789,6 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 5.3.0 -XV Thumbnails -^^^^^^^^^^^^^ - -Pillow can read XV thumbnail files. - Identify-only formats --------------------- diff --git a/docs/handbook/masked_hopper.webp b/docs/handbook/masked_hopper.webp new file mode 100644 index 00000000000..ef892cb94f7 Binary files /dev/null and b/docs/handbook/masked_hopper.webp differ diff --git a/docs/handbook/merged_hopper.webp b/docs/handbook/merged_hopper.webp new file mode 100644 index 00000000000..324628358e8 Binary files /dev/null and b/docs/handbook/merged_hopper.webp differ diff --git a/docs/handbook/overview.rst b/docs/handbook/overview.rst index 17964d1c5f3..ab22b9807a1 100644 --- a/docs/handbook/overview.rst +++ b/docs/handbook/overview.rst @@ -13,7 +13,7 @@ processing tool. Let’s look at a few possible uses of this library. -Image Archives +Image archives -------------- The Python Imaging Library is ideal for image archival and batch processing @@ -24,7 +24,7 @@ The current version identifies and reads a large number of formats. Write support is intentionally restricted to the most commonly used interchange and presentation formats. -Image Display +Image display ------------- The current release includes Tk :py:class:`~PIL.ImageTk.PhotoImage` and @@ -36,7 +36,7 @@ support. 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 +Image processing ---------------- The library contains basic image processing functionality, including point operations, filtering with a set of built-in convolution kernels, and colour space conversions. diff --git a/docs/handbook/pasted_hopper.webp b/docs/handbook/pasted_hopper.webp new file mode 100644 index 00000000000..90dc3199405 Binary files /dev/null and b/docs/handbook/pasted_hopper.webp differ diff --git a/docs/handbook/rebanded_hopper.webp b/docs/handbook/rebanded_hopper.webp new file mode 100644 index 00000000000..7a9069c9f3d Binary files /dev/null and b/docs/handbook/rebanded_hopper.webp differ diff --git a/docs/handbook/rolled_hopper.webp b/docs/handbook/rolled_hopper.webp new file mode 100644 index 00000000000..7fe80246363 Binary files /dev/null and b/docs/handbook/rolled_hopper.webp differ diff --git a/docs/handbook/rotated_hopper_180.webp b/docs/handbook/rotated_hopper_180.webp new file mode 100644 index 00000000000..08e14f0e3a1 Binary files /dev/null and b/docs/handbook/rotated_hopper_180.webp differ diff --git a/docs/handbook/rotated_hopper_270.webp b/docs/handbook/rotated_hopper_270.webp new file mode 100644 index 00000000000..ead2e102e23 Binary files /dev/null and b/docs/handbook/rotated_hopper_270.webp differ diff --git a/docs/handbook/rotated_hopper_90.webp b/docs/handbook/rotated_hopper_90.webp new file mode 100644 index 00000000000..9a5f70b2030 Binary files /dev/null and b/docs/handbook/rotated_hopper_90.webp differ diff --git a/docs/handbook/show_hopper.webp b/docs/handbook/show_hopper.webp new file mode 100644 index 00000000000..5cb73325b35 Binary files /dev/null and b/docs/handbook/show_hopper.webp differ diff --git a/docs/handbook/third-party-plugins.rst b/docs/handbook/third-party-plugins.rst new file mode 100644 index 00000000000..51181a59663 --- /dev/null +++ b/docs/handbook/third-party-plugins.rst @@ -0,0 +1,22 @@ +Third-party plugins +=================== + +Pillow uses a plugin model which allows users to add their own +decoders and encoders to the library, without any changes to the library +itself. + +Here is a list of PyPI projects that offer additional plugins: + +* :pypi:`amigainfo`: Adds support for Amiga Workbench .info icon files. +* :pypi:`amos-abk`: AMOS BASIC sprite and image banks. +* :pypi:`DjvuRleImagePlugin`: Plugin for the DjVu RLE image format as defined in the DjVuLibre docs. +* :pypi:`heif-image-plugin`: Simple HEIF/HEIC images plugin, based on the pyheif library. +* :pypi:`jxlpy`: Introduces reading and writing support for JPEG XL. +* :pypi:`pillow-degas`: Adds reading Atari ST Degas image files. +* :pypi:`pillow-heif`: Python bindings to libheif for working with HEIF images. +* :pypi:`pillow-jpls`: Plugin for the JPEG-LS codec, based on the Charls JPEG-LS implementation. Python bindings implemented using pybind11. +* :pypi:`pillow-jxl-plugin`: Plugin for JPEG-XL, using Rust for bindings. +* :pypi:`pillow-mbm`: Adds support for KSP's proprietary MBM texture format. +* :pypi:`pillow-netpbm`: Adds .pam support, and loads images using `Netpbm `__'s converter collection. +* :pypi:`pillow-svg`: Implements basic SVG read support. Supports basic paths, shapes, and text. +* :pypi:`raw-pillow-opener`: Simple camera raw opener, based on the rawpy library. diff --git a/docs/handbook/thumbnail_hopper.jpg b/docs/handbook/thumbnail_hopper.jpg new file mode 100644 index 00000000000..8c10589c2cf Binary files /dev/null and b/docs/handbook/thumbnail_hopper.jpg differ diff --git a/docs/handbook/transformed_hopper.webp b/docs/handbook/transformed_hopper.webp new file mode 100644 index 00000000000..be5e90f9677 Binary files /dev/null and b/docs/handbook/transformed_hopper.webp differ diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 6cb1e26392c..28c0abe4437 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -37,6 +37,9 @@ example, let’s display the image we just loaded:: >>> im.show() +.. image:: show_hopper.webp + :align: center + .. note:: The standard version of :py:meth:`~PIL.Image.Image.show` is not very @@ -79,6 +82,9 @@ Convert files to JPEG except OSError: print("cannot convert", infile) +.. image:: ../../Tests/images/hopper.jpg + :align: center + A second argument can be supplied to the :py:meth:`~PIL.Image.Image.save` method which explicitly specifies a file format. If you use a non-standard extension, you must always specify the format this way: @@ -103,6 +109,9 @@ Create JPEG thumbnails except OSError: print("cannot create thumbnail for", infile) +.. image:: thumbnail_hopper.jpg + :align: center + It is important to note that the library doesn’t decode or load the raster data unless it really has to. When you open a file, the file header is read to determine the file format and extract things like mode, size, and other @@ -113,7 +122,7 @@ This means that opening an image file is a fast operation, which is independent of the file size and compression type. Here’s a simple script to quickly identify a set of image files: -Identify Image Files +Identify image files ^^^^^^^^^^^^^^^^^^^^ :: @@ -140,16 +149,19 @@ Copying a subrectangle from an image :: - box = (100, 100, 400, 400) + box = (0, 0, 64, 64) region = im.crop(box) The region is defined by a 4-tuple, where coordinates are (left, upper, right, lower). The Python Imaging Library uses a coordinate system with (0, 0) in the upper left corner. Also note that coordinates refer to positions between the -pixels, so the region in the above example is exactly 300x300 pixels. +pixels, so the region in the above example is exactly 64x64 pixels. The region could now be processed in a certain manner and pasted back. +.. image:: cropped_hopper.webp + :align: center + Processing a subrectangle, and pasting it back ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -164,6 +176,9 @@ modes of the original image and the region do not need to match. If they don’t the region is automatically converted before being pasted (see the section on :ref:`color-transforms` below for details). +.. image:: pasted_hopper.webp + :align: center + Here’s an additional example: Rolling an image @@ -171,7 +186,7 @@ Rolling an image :: - def roll(im, delta): + def roll(im: Image.Image, delta: int) -> Image.Image: """Roll an image sideways.""" xsize, ysize = im.size @@ -186,6 +201,9 @@ Rolling an image return im +.. image:: rolled_hopper.webp + :align: center + Or if you would like to merge two images into a wider image: Merging images @@ -193,7 +211,7 @@ Merging images :: - def merge(im1, im2): + def merge(im1: Image.Image, im2: Image.Image) -> Image.Image: w = im1.size[0] + im2.size[0] h = max(im1.size[1], im2.size[1]) im = Image.new("RGBA", (w, h)) @@ -203,6 +221,9 @@ Merging images return im +.. image:: merged_hopper.webp + :align: center + 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 image is opaque in that position (that is, the pasted image should be used as @@ -229,6 +250,9 @@ Note that for a single-band image, :py:meth:`~PIL.Image.Image.split` returns the image itself. To work with individual color bands, you may want to convert the image to “RGB” first. +.. image:: rebanded_hopper.webp + :align: center + Geometrical transforms ---------------------- @@ -245,6 +269,9 @@ Simple geometry transforms out = im.resize((128, 128)) out = im.rotate(45) # degrees counter-clockwise +.. image:: rotated_hopper_90.webp + :align: center + To rotate the image in 90 degree steps, you can either use the :py:meth:`~PIL.Image.Image.rotate` method or the :py:meth:`~PIL.Image.Image.transpose` method. The latter can also be used to @@ -256,11 +283,38 @@ Transposing an image :: out = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + +.. image:: flip_left_right_hopper.webp + :align: center + +:: + out = im.transpose(Image.Transpose.FLIP_TOP_BOTTOM) + +.. image:: flip_top_bottom_hopper.webp + :align: center + +:: + out = im.transpose(Image.Transpose.ROTATE_90) + +.. image:: rotated_hopper_90.webp + :align: center + +:: + out = im.transpose(Image.Transpose.ROTATE_180) + +.. image:: rotated_hopper_180.webp + :align: center + +:: + out = im.transpose(Image.Transpose.ROTATE_270) +.. image:: rotated_hopper_270.webp + :align: center + ``transpose(ROTATE)`` operations can also be performed identically with :py:meth:`~PIL.Image.Image.rotate` operations, provided the ``expand`` flag is true, to provide for the same changes to the image's size. @@ -278,7 +332,7 @@ choose to resize relative to a given size. from PIL import Image, ImageOps size = (100, 150) - with Image.open("Tests/images/hopper.webp") as im: + with Image.open("hopper.webp") as im: ImageOps.contain(im, size).save("imageops_contain.webp") ImageOps.cover(im, size).save("imageops_cover.webp") ImageOps.fit(im, size).save("imageops_fit.webp") @@ -342,7 +396,10 @@ Applying filters from PIL import ImageFilter out = im.filter(ImageFilter.DETAIL) -Point Operations +.. image:: enhanced_hopper.webp + :align: center + +Point operations ^^^^^^^^^^^^^^^^ The :py:meth:`~PIL.Image.Image.point` method can be used to translate the pixel @@ -355,8 +412,11 @@ Applying point transforms :: - # multiply each pixel by 1.2 - out = im.point(lambda i: i * 1.2) + # multiply each pixel by 20 + out = im.point(lambda i: i * 20) + +.. image:: transformed_hopper.webp + :align: center Using the above technique, you can quickly apply any simple expression to an image. You can also combine the :py:meth:`~PIL.Image.Image.point` and @@ -388,6 +448,9 @@ Note the syntax used to create the mask:: imout = im.point(lambda i: expression and 255) +.. image:: masked_hopper.webp + :align: center + Python only evaluates the portion of a logical expression as is necessary to determine the outcome, and returns the last value examined as the result of the expression. So if the expression above is false (0), Python does not look at @@ -412,6 +475,10 @@ Enhancing images enh = ImageEnhance.Contrast(im) enh.enhance(1.3).show("30% more contrast") + +.. image:: contrasted_hopper.jpg + :align: center + Image sequences --------------- @@ -444,10 +511,42 @@ Reading sequences As seen in this example, you’ll get an :py:exc:`EOFError` exception when the sequence ends. +Writing sequences +^^^^^^^^^^^^^^^^^ + +You can create animated GIFs with Pillow, e.g. + +:: + + from PIL import Image + + # List of image filenames + image_filenames = [ + "hopper.jpg", + "rotated_hopper_270.jpg", + "rotated_hopper_180.jpg", + "rotated_hopper_90.jpg", + ] + + # Open images and create a list + images = [Image.open(filename) for filename in image_filenames] + + # Save the images as an animated GIF + images[0].save( + "animated_hopper.gif", + append_images=images[1:], + duration=500, # duration of each frame in milliseconds + loop=0, # loop forever + ) + + +.. image:: animated_hopper.gif + :align: center + The following class lets you use the for-statement to loop over the sequence: -Using the ImageSequence Iterator class -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Using the :py:class:`~PIL.ImageSequence.Iterator` class +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :: @@ -467,25 +566,61 @@ Drawing PostScript :: - from PIL import Image - from PIL import PSDraw + from PIL import Image, PSDraw + import os - with Image.open("hopper.ppm") as im: - title = "hopper" - box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points + # Define the PostScript file + ps_file = open("hopper.ps", "wb") + + # Create a PSDraw object + ps = PSDraw.PSDraw(ps_file) + + # Start the document + ps.begin_document() + + # Set the text to be drawn + text = "Hopper" + + # Define the PostScript font + font_name = "Helvetica-Narrow-Bold" + font_size = 36 + + # Calculate text size (approximation as PSDraw doesn't provide direct method) + # Assuming average character width as 0.6 of the font size + text_width = len(text) * font_size * 0.6 + text_height = font_size - ps = PSDraw.PSDraw() # default is sys.stdout or sys.stdout.buffer - ps.begin_document(title) + # Set the position (top-center) + page_width, page_height = 595, 842 # A4 size in points + text_x = (page_width - text_width) // 2 + text_y = page_height - text_height - 50 # Distance from the top of the page - # draw the image (75 dpi) - ps.image(box, im, 75) - ps.rectangle(box) + # Load the image + image_path = "hopper.ppm" # Update this with your image path + with Image.open(image_path) as im: + # Resize the image if it's too large + im.thumbnail((page_width - 100, page_height // 2)) - # draw title - ps.setfont("HelveticaNarrow-Bold", 36) - ps.text((3 * 72, 4 * 72), title) + # Define the box where the image will be placed + img_x = (page_width - im.width) // 2 + img_y = text_y + text_height - 200 # 200 points below the text - ps.end_document() + # Draw the image (75 dpi) + ps.image((img_x, img_y, img_x + im.width, img_y + im.height), im, 75) + + # Draw the text + ps.setfont(font_name, font_size) + ps.text((text_x, text_y), text) + + # End the document + ps.end_document() + ps_file.close() + +.. image:: hopper_ps.webp + +.. note:: + + PostScript converted to PDF for display purposes More on reading images ---------------------- @@ -542,7 +677,7 @@ Reading from URL from PIL import Image from urllib.request import urlopen - url = "https://python-pillow.org/assets/images/pillow-logo.png" + url = "https://python-pillow.github.io/assets/images/pillow-logo.png" img = Image.open(urlopen(url)) @@ -553,7 +688,7 @@ Reading from a tar archive from PIL import Image, TarIO - fp = TarIO.TarIO("Tests/images/hopper.tar", "hopper.jpg") + fp = TarIO.TarIO("hopper.tar", "hopper.jpg") im = Image.open(fp) @@ -568,8 +703,7 @@ 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): + def compress_image(source_path: str, dest_path: str) -> None: with Image.open(source_path) as img: if img.mode != "RGB": img = img.convert("RGB") diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 956d63aa771..21a9124d781 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -1,6 +1,6 @@ .. _image-plugins: -Writing Your Own Image Plugin +Writing your own image plugin ============================= Pillow uses a plugin model which allows you to add your own @@ -53,8 +53,8 @@ true color. from PIL import Image, ImageFile - def _accept(prefix): - return prefix[:4] == b"SPAM" + def _accept(prefix: bytes) -> bool: + return prefix.startswith(b"SPAM") class SpamImageFile(ImageFile.ImageFile): @@ -62,7 +62,7 @@ true color. format = "SPAM" format_description = "Spam raster image" - def _open(self): + def _open(self) -> None: header = self.fp.read(128).split() @@ -82,7 +82,7 @@ true color. raise SyntaxError(msg) # data descriptor - self.tile = [("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] Image.register_open(SpamImageFile.format, SpamImageFile, _accept) @@ -329,7 +329,7 @@ The fields are used as follows: .. _file-codecs: -Writing Your Own File Codec in C +Writing your own file codec in C ================================ There are 3 stages in a file codec's lifetime: @@ -414,7 +414,7 @@ memory and release any resources from external libraries. .. _file-codecs-py: -Writing Your Own File Codec in Python +Writing your own file codec in Python ===================================== Python file decoders and encoders should derive from diff --git a/docs/index.rst b/docs/index.rst index 3a12953f08b..8612f77a55d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,7 @@ Pillow ====== -Pillow is the friendly PIL fork by `Jeffrey A. Clark and contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and contributors. +Pillow is the friendly PIL fork by `Jeffrey 'Alex' Clark and contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and contributors. Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_. @@ -29,14 +29,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_. After you install Homebrew, run:: - brew install libjpeg libtiff little-cms2 openjpeg webp + brew install libavif libjpeg libraqm libtiff little-cms2 openjpeg webp - To install libraqm on macOS use Homebrew to install its dependencies:: + If you would like to use libavif with more codecs than just aom, then + instead of installing libavif through Homebrew directly, you can use + Homebrew to install libavif's build dependencies:: - brew install freetype harfbuzz fribidi + brew install aom dav1d rav1e svt-av1 - Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + Then see ``depends/install_libavif.sh`` to install libavif. .. tab:: Windows @@ -182,9 +194,9 @@ Many of Pillow's features require external libraries: pacman -S \ mingw-w64-x86_64-gcc \ - mingw-w64-x86_64-python3 \ - mingw-w64-x86_64-python3-pip \ - mingw-w64-x86_64-python3-setuptools + mingw-w64-x86_64-python \ + mingw-w64-x86_64-python-pip \ + mingw-w64-x86_64-python-setuptools Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: @@ -197,12 +209,8 @@ Many of Pillow's features require external libraries: mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-openjpeg2 \ mingw-w64-x86_64-libimagequant \ - mingw-w64-x86_64-libraqm - - https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with - MSYS2. To workaround this, before installing Pillow you must run:: - - export SETUPTOOLS_USE_DISTUTILS=stdlib + mingw-w64-x86_64-libraqm \ + mingw-w64-x86_64-libavif .. tab:: FreeBSD @@ -214,7 +222,7 @@ Many of Pillow's features require external libraries: Prerequisites are installed on **FreeBSD 10 or 11** with:: - sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb + sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb libavif Then see ``depends/install_raqm_cmake.sh`` to install libraqm. @@ -263,30 +271,31 @@ After navigating to the Pillow directory, run:: .. _compressed archive from PyPI: https://pypi.org/project/pillow/#files -Build Options +Build options ^^^^^^^^^^^^^ * Config setting: ``-C parallel=n``. Can also be given with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use - multiprocessing to build the extension. Setting ``-C parallel=n`` + multiprocessing to build the extensions. Setting ``-C parallel=n`` sets the number of CPUs to use to ``n``, 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. + using a setting of 1. By default, it uses as many CPUs as are present. * Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, - ``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``, - ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``. + ``-C lcms=disable``, ``-C webp=disable``, + ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``, + ``-C avif=disable``. Disable building the corresponding feature even if the development libraries are present on the building machine. * Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, - ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``, - ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``. + ``-C lcms=enable``, ``-C webp=enable``, + ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``, + ``-C avif=enable``. 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. + an exception if the libraries are not found. Tcl and Tk must be used + together. * Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``. These flags are used to compile a modified version of libraqm and @@ -309,7 +318,7 @@ Sample usage:: .. _old-versions: -Old Versions +Old versions ============ You can download old distributions from the `release history at PyPI diff --git a/docs/installation/newer-versions.csv b/docs/installation/newer-versions.csv index e21caf52071..e948dd5400e 100644 --- a/docs/installation/newer-versions.csv +++ b/docs/installation/newer-versions.csv @@ -1,8 +1,10 @@ -Python,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5 -Pillow >= 10.1,Yes,Yes,Yes,Yes,Yes,,, -Pillow 10.0,,Yes,Yes,Yes,Yes,,, -Pillow 9.3 - 9.5,,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 +Python,3.14,3.13,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5 +Pillow 12,Yes,Yes,Yes,Yes,Yes,,,,, +Pillow 11,,Yes,Yes,Yes,Yes,Yes,,,, +Pillow 10.1 - 10.4,,,Yes,Yes,Yes,Yes,Yes,,, +Pillow 10.0,,,,Yes,Yes,Yes,Yes,,, +Pillow 9.3 - 9.5,,,,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/installation/platform-support.rst b/docs/installation/platform-support.rst index ed25d33a6f3..7e6ad1e7790 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -1,6 +1,6 @@ .. _platform-support: -Platform Support +Platform support ================ Current platform support for Pillow. Binary distributions are @@ -9,7 +9,7 @@ should compile and run everywhere platform support is listed. In general, we aim to support all current versions of Linux, macOS, and Windows. -Continuous Integration Targets +Continuous integration targets ------------------------------ These platforms are built and tested for every change. @@ -17,55 +17,51 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Operating system | Tested Python versions | Tested architecture | +==================================+============================+=====================+ -| Alpine | 3.9 | x86-64 | +| Alpine | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Amazon Linux 2 | 3.9 | x86-64 | +| Amazon Linux 2 | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Amazon Linux 2023 | 3.9 | x86-64 | +| Amazon Linux 2023 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Arch | 3.9 | x86-64 | +| Arch | 3.14 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| CentOS Stream 9 | 3.9 | x86-64 | +| CentOS Stream 9 | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Debian 11 Bullseye | 3.9 | x86-64 | +| CentOS Stream 10 | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Debian 12 Bookworm | 3.11 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ -| Fedora 39 | 3.12 | x86-64 | +| Debian 13 Trixie | 3.13 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ -| Fedora 40 | 3.12 | x86-64 | +| Fedora 42 | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Gentoo | 3.9 | x86-64 | +| Fedora 43 | 3.14 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 13 Ventura | 3.8, 3.9 | x86-64 | +| Gentoo | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 | -| | PyPy3 | | +| macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14, | arm64 | +| | 3.15, PyPy3 | | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | +| macOS 26 Tahoe | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | 3.12, 3.13, PyPy3 | | -| +----------------------------+---------------------+ -| | 3.10 | arm64v8 | +| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, | +| Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, 3.12, 3.13, | x86-64 | +| | 3.14, 3.15, PyPy3 | | +| +----------------------------+---------------------+ +| | 3.12 | arm64v8, ppc64le, | | | | s390x | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2016 | 3.8 | x86-64 | +| Windows Server 2022 | 3.10 | x86 | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | 3.12, 3.13, PyPy3 | | -| +----------------------------+---------------------+ -| | 3.12 | x86 | -| +----------------------------+---------------------+ -| | 3.9 (MinGW) | x86-64 | +| Windows Server 2025 | 3.11, 3.12, 3.13, 3.14, | x86-64 | +| | 3.15, PyPy3 | | | +----------------------------+---------------------+ -| | 3.8, 3.9 (Cygwin) | x86-64 | +| | 3.14 (MinGW) | x86-64 | +----------------------------------+----------------------------+---------------------+ -Other Platforms +Other platforms --------------- These platforms have been reported to work at the versions mentioned. @@ -75,94 +71,102 @@ 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 | | Latest tested | | Tested | -| | | versions | | Pillow version | | processors | -+==================================+============================+==================+==============+ -| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.3.0 |arm | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | -| +----------------------------+------------------+ | -| | 3.7 | 9.5.0 | | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | -| +----------------------------+------------------+--------------+ -| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.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 11 | 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 11 Pro | 3.11, 3.12 | 10.2.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 | -+----------------------------------+----------------------------+------------------+--------------+ ++----------------------------------+-----------------------------+------------------+--------------+ +| Operating system | | Tested Python | | Latest tested | | Tested | +| | | versions | | Pillow version | | processors | ++==================================+=============================+==================+==============+ +| macOS 26 Tahoe | 3.10, 3.11, 3.12, 3.13, 3.14| 12.1.1 |arm | +| +-----------------------------+------------------+ | +| | 3.9 | 11.3.0 | | ++----------------------------------+-----------------------------+------------------+--------------+ +| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13 | 11.3.0 |arm | +| +-----------------------------+------------------+ | +| | 3.8 | 10.4.0 | | ++----------------------------------+-----------------------------+------------------+--------------+ +| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm | ++----------------------------------+-----------------------------+------------------+--------------+ +| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | +| +-----------------------------+------------------+ | +| | 3.7 | 9.5.0 | | ++----------------------------------+-----------------------------+------------------+--------------+ +| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | ++----------------------------------+-----------------------------+------------------+--------------+ +| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | +| +-----------------------------+------------------+--------------+ +| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.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 11 23H2 | 3.9, 3.10, 3.11, 3.12, 3.13 | 11.0.0 |arm64 | ++----------------------------------+-----------------------------+------------------+--------------+ +| Windows 11 Pro | 3.11, 3.12 | 10.2.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 | ++----------------------------------+-----------------------------+------------------+--------------+ diff --git a/docs/installation/python-support.rst b/docs/installation/python-support.rst index dd5765b6b70..7daee8afc78 100644 --- a/docs/installation/python-support.rst +++ b/docs/installation/python-support.rst @@ -1,6 +1,6 @@ .. _python-support: -Python Support +Python support ============== Pillow supports these Python versions. diff --git a/docs/make.bat b/docs/make.bat index 0ed5ee1a57e..9d15537fb9e 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -7,10 +7,8 @@ if "%SPHINXBUILD%" == "" ( ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help @@ -22,20 +20,7 @@ if "%1" == "help" ( echo. htmlview to open the index page built by the html target in your browser echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled goto end ) @@ -80,107 +65,6 @@ if "%1" == "singlehtml" ( goto end ) -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PillowPILfork.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PillowPILfork.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 @@ -190,13 +74,4 @@ or in %BUILDDIR%/linkcheck/output.txt. goto end ) -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - :end diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 06965ead3f0..e6bcd9d59c2 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ExifTags .. py:currentmodule:: PIL.ExifTags -:py:mod:`~PIL.ExifTags` Module +:py:mod:`~PIL.ExifTags` module ============================== The :py:mod:`~PIL.ExifTags` module exposes several :py:class:`enum.IntEnum` diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 66c5e5422da..adee49228d2 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.Image .. py:currentmodule:: PIL.Image -:py:mod:`~PIL.Image` Module +:py:mod:`~PIL.Image` module =========================== The :py:mod:`~PIL.Image` module provides a class with the same name which is @@ -79,6 +79,7 @@ Constructing images .. autofunction:: new .. autofunction:: fromarray +.. autofunction:: fromarrow .. autofunction:: frombytes .. autofunction:: frombuffer @@ -112,7 +113,7 @@ Registering plugins .. autofunction:: register_decoder .. autofunction:: register_encoder -The Image Class +The Image class --------------- .. autoclass:: PIL.Image.Image @@ -190,6 +191,7 @@ This helps to get the bounding box coordinates of the input image:: .. automethod:: PIL.Image.Image.getchannel .. automethod:: PIL.Image.Image.getcolors .. automethod:: PIL.Image.Image.getdata +.. automethod:: PIL.Image.Image.get_flattened_data .. automethod:: PIL.Image.Image.getexif .. automethod:: PIL.Image.Image.getextrema .. automethod:: PIL.Image.Image.getpalette @@ -260,7 +262,7 @@ method. :: .. automethod:: PIL.Image.Image.load .. automethod:: PIL.Image.Image.close -Image Attributes +Image attributes ---------------- Instances of the :py:class:`Image` class have the following attributes: @@ -362,6 +364,7 @@ Classes :undoc-members: :show-inheritance: .. autoclass:: PIL.Image.ImagePointHandler +.. autoclass:: PIL.Image.ImagePointTransform .. autoclass:: PIL.Image.ImageTransformHandler Protocols @@ -369,6 +372,8 @@ Protocols .. autoclass:: SupportsArrayInterface :show-inheritance: +.. autoclass:: SupportsArrowArrayInterface + :show-inheritance: .. autoclass:: SupportsGetData :show-inheritance: diff --git a/docs/reference/ImageChops.rst b/docs/reference/ImageChops.rst index 9519361a7e6..505181db6b5 100644 --- a/docs/reference/ImageChops.rst +++ b/docs/reference/ImageChops.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageChops .. py:currentmodule:: PIL.ImageChops -:py:mod:`~PIL.ImageChops` ("Channel Operations") Module +:py:mod:`~PIL.ImageChops` ("channel operations") module ======================================================= The :py:mod:`~PIL.ImageChops` module contains a number of arithmetical image diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 96bd14dd32c..4a21236775f 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageCms .. py:currentmodule:: PIL.ImageCms -:py:mod:`~PIL.ImageCms` Module +:py:mod:`~PIL.ImageCms` module ============================== The :py:mod:`~PIL.ImageCms` module provides color profile management @@ -56,7 +56,6 @@ Functions .. autofunction:: get_display_profile .. autofunction:: isIntentSupported .. autofunction:: profileToProfile -.. autofunction:: versions CmsProfile ---------- @@ -286,6 +285,14 @@ can be easily displayed in a chromaticity diagram, for example). The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. + .. py:attribute:: media_white_point + :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None + + This tag specifies the media white point and is used for + generating absolute colorimetry. + + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. + .. py:attribute:: media_white_point_temperature :type: float | None diff --git a/docs/reference/ImageColor.rst b/docs/reference/ImageColor.rst index 31faeac788b..68e228dba0f 100644 --- a/docs/reference/ImageColor.rst +++ b/docs/reference/ImageColor.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageColor .. py:currentmodule:: PIL.ImageColor -:py:mod:`~PIL.ImageColor` Module +:py:mod:`~PIL.ImageColor` module ================================ The :py:mod:`~PIL.ImageColor` module contains color tables and converters from @@ -11,7 +11,7 @@ others. .. _color-names: -Color Names +Color names ----------- The ImageColor module supports the following string formats: diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 3e9aa73f8c3..4c956759334 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageDraw .. py:currentmodule:: PIL.ImageDraw -:py:mod:`~PIL.ImageDraw` Module +:py:mod:`~PIL.ImageDraw` module =============================== The :py:mod:`~PIL.ImageDraw` module provides simple 2D graphics for @@ -45,20 +45,55 @@ Colors ^^^^^^ To specify colors, you can use numbers or tuples just as you would use with -: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. +:py:meth:`PIL.Image.new`. See :ref:`colors` for more information. For palette images (mode “P”), use integers as color indexes. In 1.1.4 and later, you can also use RGB 3-tuples or color names (see below). The drawing layer will automatically assign color indexes, as long as you don’t draw with more than 256 colors. -Color Names +Color names ^^^^^^^^^^^ See :ref:`color-names` for the color names supported by Pillow. +Alpha channel +^^^^^^^^^^^^^ + +By default, when drawing onto an existing image, the image's pixel values are simply +replaced by the new color:: + + im = Image.new("RGBA", (1, 1), (255, 0, 0)) + d = ImageDraw.Draw(im) + d.rectangle((0, 0, 1, 1), (0, 255, 0, 127)) + assert im.getpixel((0, 0)) == (0, 255, 0, 127) + + # Alpha channel values have no effect when drawing with RGB mode + im = Image.new("RGB", (1, 1), (255, 0, 0)) + d = ImageDraw.Draw(im) + d.rectangle((0, 0, 1, 1), (0, 255, 0, 127)) + assert im.getpixel((0, 0)) == (0, 255, 0) + +If you would like to combine translucent color with an RGB image, then initialize the +ImageDraw instance with the RGBA mode:: + + from PIL import Image, ImageDraw + im = Image.new("RGB", (1, 1), (255, 0, 0)) + d = ImageDraw.Draw(im, "RGBA") + d.rectangle((0, 0, 1, 1), (0, 255, 0, 127)) + assert im.getpixel((0, 0)) == (128, 127, 0) + +If you would like to combine translucent color with an RGBA image underneath, you will +need to combine multiple images:: + + from PIL import Image, ImageDraw + im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) + im2 = Image.new("RGBA", (1, 1)) + d = ImageDraw.Draw(im2) + d.rectangle((0, 0, 1, 1), (0, 255, 0, 127)) + im.paste(im2.convert("RGB"), mask=im2) + assert im.getpixel((0, 0)) == (128, 127, 0, 255) + Fonts ^^^^^ @@ -75,7 +110,7 @@ To load a OpenType/TrueType font, use the truetype function in the :py:mod:`~PIL.ImageFont` module. Note that this function depends on third-party libraries, and may not available in all PIL builds. -Example: Draw Partial Opacity Text +Example: Draw partial opacity text ---------------------------------- :: @@ -102,7 +137,7 @@ Example: Draw Partial Opacity Text out.show() -Example: Draw Multiline Text +Example: Draw multiline text ---------------------------- :: @@ -387,8 +422,11 @@ Methods the number of pixels between lines. :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``. + ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines + the relative alignment of lines. Use the ``anchor`` parameter to + specify the alignment to ``xy``. + + .. versionadded:: 11.2.1 ``"justify"`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -455,8 +493,11 @@ Methods of Pillow, but implemented only in version 8.0.0. :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 align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines + the relative alignment of lines. Use the ``anchor`` parameter to + specify the alignment to ``xy``. + + .. versionadded:: 11.2.1 ``"justify"`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -541,6 +582,8 @@ Methods hello_world = hello + world # kerning is disabled, no need to adjust assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"]) # True + .. seealso:: :py:meth:`PIL.ImageText.Text.get_length` + .. versionadded:: 8.0.0 :param text: Text to be measured. May not contain any newline characters. @@ -599,8 +642,11 @@ Methods 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``. + ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines + the relative alignment of lines. Use the ``anchor`` parameter to + specify the alignment to ``xy``. + + .. versionadded:: 11.2.1 ``"justify"`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -639,6 +685,8 @@ Methods 1/64 pixel precision. The bounding box includes extra margins for some fonts, e.g. italics or accents. + .. seealso:: :py:meth:`PIL.ImageText.Text.get_bbox` + .. versionadded:: 8.0.0 :param xy: The anchor coordinates of the text. @@ -650,8 +698,11 @@ Methods vertical text. See :ref:`text-anchors` for details. 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 align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines + the relative alignment of lines. Use the ``anchor`` parameter to + specify the alignment to ``xy``. + + .. versionadded:: 11.2.1 ``"justify"`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index 529acad4a91..334d1d4b228 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageEnhance .. py:currentmodule:: PIL.ImageEnhance -:py:mod:`~PIL.ImageEnhance` Module +:py:mod:`~PIL.ImageEnhance` module ================================== The :py:mod:`~PIL.ImageEnhance` module contains a number of classes that can be used diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index e59c7311a99..4c34ff8128b 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageFile .. py:currentmodule:: PIL.ImageFile -:py:mod:`~PIL.ImageFile` Module +:py:mod:`~PIL.ImageFile` module =============================== The :py:mod:`~PIL.ImageFile` module provides support functions for the image open @@ -19,7 +19,7 @@ Example: Parse an image from PIL import ImageFile - fp = open("hopper.pgm", "rb") + fp = open("hopper.ppm", "rb") p = ImageFile.Parser() @@ -37,6 +37,11 @@ Example: Parse an image Classes ------- +.. autoclass:: PIL.ImageFile._Tile() + :member-order: bysource + :members: + :show-inheritance: + .. autoclass:: PIL.ImageFile.Parser() :members: @@ -69,5 +74,6 @@ Constants --------- .. autodata:: PIL.ImageFile.LOAD_TRUNCATED_IMAGES +.. autodata:: PIL.ImageFile.MAXBLOCK .. autodata:: PIL.ImageFile.ERRORS :annotation: diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index 5f2b6af7c10..1c201cacca4 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageFilter .. py:currentmodule:: PIL.ImageFilter -:py:mod:`~PIL.ImageFilter` Module +:py:mod:`~PIL.ImageFilter` module ================================= The :py:mod:`~PIL.ImageFilter` module contains definitions for a pre-defined set of diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index edbdd9a32a6..920a05e65e9 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -1,23 +1,30 @@ .. py:module:: PIL.ImageFont .. py:currentmodule:: PIL.ImageFont -:py:mod:`~PIL.ImageFont` Module +:py:mod:`~PIL.ImageFont` module =============================== 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.ImageDraw.text` method. -PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You can use -`pilfont.py `_ -from :pypi:`pillow-scripts` to convert BDF and -PCF font descriptors (X window font formats) to this format. +Pillow uses its own font file format to store bitmap fonts, limited to 256 characters. You +can use :py:meth:`~PIL.FontFile.FontFile.to_imagefont` to convert BDF and PCF font +descriptors (X Window font formats) to this format:: + + from PIL import PcfFontFile + with open("Tests/fonts/10x20-ISO8859-1.pcf", "rb") as fp: + font = PcfFontFile.PcfFontFile(fp) + imagefont = font.to_imagefont() 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. +When measuring text sizes, this module will not break at newline characters. For +multiline text, see the :py:mod:`~PIL.ImageDraw` module. + .. warning:: To protect against potential DOS attacks when using arbitrary strings as text input, Pillow will raise a :py:exc:`ValueError` if the number of characters @@ -91,3 +98,11 @@ Constants Set to 1,000,000, to protect against potential DOS attacks. Pillow will raise a :py:exc:`ValueError` if the number of characters is over this limit. The check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``. + +Dictionaries +------------ + +.. autoclass:: Axis + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index db2987eb081..4138667850b 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageGrab .. py:currentmodule:: PIL.ImageGrab -:py:mod:`~PIL.ImageGrab` Module +:py:mod:`~PIL.ImageGrab` module =============================== The :py:mod:`~PIL.ImageGrab` module can be used to copy the contents of the screen @@ -9,17 +9,20 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 -.. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None) +.. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None, window=None) Take a snapshot of the screen. The pixels inside the bounding box are returned as an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted, the entire screen is copied, and on macOS, it will be at 2x if on a Retina screen. On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return - a snapshot of the screen, ``gnome-screenshot`` will be used as fallback if it is - installed. To disable this behaviour, pass ``xdisplay=""`` instead. + a snapshot of the screen, ``gnome-screenshot``, ``grim`` or ``spectacle`` will be + used as a fallback if they are installed. To disable this behaviour, pass + ``xdisplay=""`` instead. - .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) + .. versionadded:: 1.1.3 Windows support + .. versionadded:: 3.0.0 macOS support + .. versionadded:: 7.1.0 Linux support :param bbox: What region to copy. Default is the entire screen. On macOS, this is not increased to 2x for Retina screens, so the full @@ -39,6 +42,13 @@ or the clipboard to a PIL image memory. You can check X11 support using :py:func:`PIL.features.check_feature` with ``feature="xcb"``. .. versionadded:: 7.1.0 + + :param window: + Capture a single window. On Windows, this is a HWND. On macOS, this is a + CGWindowID. + + .. versionadded:: 11.2.1 Windows support + .. versionadded:: 12.1.0 macOS support :return: An image .. py:function:: grabclipboard() @@ -47,7 +57,9 @@ or the clipboard to a PIL image memory. On Linux, ``wl-paste`` or ``xclip`` is required. - .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS), 9.4.0 (Linux) + .. versionadded:: 1.1.4 Windows support + .. versionadded:: 3.3.0 macOS support + .. versionadded:: 9.4.0 Linux support :return: On Windows, an image, a list of filenames, or None if the clipboard does not contain image data or filenames. diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index 2535db7113c..0ee49b15008 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageMath .. py:currentmodule:: PIL.ImageMath -:py:mod:`~PIL.ImageMath` Module +:py:mod:`~PIL.ImageMath` module =============================== The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressions”, that @@ -31,20 +31,21 @@ Example: Using the :py:mod:`~PIL.ImageMath` module b=im2 ) -.. py:function:: lambda_eval(expression, options) +.. py:function:: lambda_eval(expression, options, **kw) Returns the result of an image function. :param expression: A function that receives a dictionary. - :param options: Values to add to the function's dictionary, mapping image - names to Image instances. You can use one or more keyword - arguments instead of a dictionary, as shown in the above - example. Note that the names must be valid Python - identifiers. + :param options: Values to add to the function's dictionary. Note that the names + must be valid Python identifiers. Deprecated. + You can instead use one or more keyword arguments, as + shown in the above example. + :param \**kw: Values to add to the function's dictionary, mapping image names to + Image instances. :return: An image, an integer value, a floating point value, or a pixel tuple, depending on the expression. -.. py:function:: unsafe_eval(expression, options) +.. py:function:: unsafe_eval(expression, options, **kw) Evaluates an image expression. @@ -61,11 +62,12 @@ Example: Using the :py:mod:`~PIL.ImageMath` module :param expression: A string which uses the standard Python expression syntax. In addition to the standard operators, you can also use the functions described below. - :param options: Values to add to the function's dictionary, mapping image - names to Image instances. You can use one or more keyword - arguments instead of a dictionary, as shown in the above - example. Note that the names must be valid Python - identifiers. + :param options: Values to add to the evaluation context. Note that the names must + be valid Python identifiers. Deprecated. + You can instead use one or more keyword arguments, as + shown in the above example. + :param \**kw: Values to add to the evaluation context, mapping image names to Image + instances. :return: An image, an integer value, a floating point value, or a pixel tuple, depending on the expression. @@ -84,7 +86,7 @@ Expression syntax It is not recommended to process expressions without considering this. :py:meth:`lambda_eval` is a more secure alternative. -Standard Operators +Standard operators ^^^^^^^^^^^^^^^^^^ You can use standard arithmetical operators for addition (+), subtraction (-), @@ -100,7 +102,7 @@ an 8-bit image, the result will be a 32-bit floating point image. You can force conversion using the ``convert()``, ``float()``, and ``int()`` functions described below. -Bitwise Operators +Bitwise operators ^^^^^^^^^^^^^^^^^ The module also provides operations that operate on individual bits. This @@ -114,7 +116,7 @@ mask off unwanted bits. Bitwise operators don’t work on floating point images. -Logical Operators +Logical operators ^^^^^^^^^^^^^^^^^ Logical operators like ``and``, ``or``, and ``not`` work @@ -126,7 +128,7 @@ treated as true. Note that ``and`` and ``or`` return the last evaluated operand, while not always returns a boolean value. -Built-in Functions +Built-in functions ^^^^^^^^^^^^^^^^^^ These functions are applied to each individual pixel. diff --git a/docs/reference/ImageMorph.rst b/docs/reference/ImageMorph.rst index d4522a06ae3..f7a3027131e 100644 --- a/docs/reference/ImageMorph.rst +++ b/docs/reference/ImageMorph.rst @@ -1,13 +1,53 @@ .. py:module:: PIL.ImageMorph .. py:currentmodule:: PIL.ImageMorph -:py:mod:`~PIL.ImageMorph` Module +:py:mod:`~PIL.ImageMorph` module ================================ -The :py:mod:`~PIL.ImageMorph` module provides morphology operations on images. +The :py:mod:`~PIL.ImageMorph` module allows `morphology`_ operators ("MorphOp") to be +applied to 1 or L mode images:: -.. automodule:: PIL.ImageMorph + from PIL import Image, ImageMorph + img = Image.open("Tests/images/hopper.bw") + mop = ImageMorph.MorphOp(op_name="erosion4") + count, imgOut = mop.apply(img) + imgOut.show() + +.. _morphology: https://en.wikipedia.org/wiki/Mathematical_morphology + +In addition to applying operators, you can also analyse images. + +You can inspect an image in isolation to determine which pixels are non-empty:: + + print(mop.get_on_pixels(img)) # [(0, 0), (1, 0), (2, 0), ...] + +Or you can retrieve a list of pixels that match the operator. This is the number of +pixels that will be non-empty after the operator is applied:: + + coords = mop.match(img) + print(coords) # [(17, 1), (18, 1), (34, 1), ...] + print(len(coords)) # 550 + + imgOut = mop.apply(img)[1] + print(len(mop.get_on_pixels(imgOut))) # 550 + +If you would like more customized operators, you can pass patterns to the MorphOp +class:: + + mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) + +Or you can pass lookup table ("LUT") data directly. This LUT data can be constructed +with the :py:class:`~PIL.ImageMorph.LutBuilder`:: + + builder = ImageMorph.LutBuilder() + mop = ImageMorph.MorphOp(lut=builder.build_lut()) + +.. autoclass:: LutBuilder + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: MorphOp :members: :undoc-members: :show-inheritance: - :noindex: diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst index fcaa3c8f675..1ecff09f000 100644 --- a/docs/reference/ImageOps.rst +++ b/docs/reference/ImageOps.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageOps .. py:currentmodule:: PIL.ImageOps -:py:mod:`~PIL.ImageOps` Module +:py:mod:`~PIL.ImageOps` module ============================== The :py:mod:`~PIL.ImageOps` module contains a number of ‘ready-made’ image diff --git a/docs/reference/ImagePalette.rst b/docs/reference/ImagePalette.rst index 72ccfac7d83..42ce5cb134c 100644 --- a/docs/reference/ImagePalette.rst +++ b/docs/reference/ImagePalette.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImagePalette .. py:currentmodule:: PIL.ImagePalette -:py:mod:`~PIL.ImagePalette` Module +:py:mod:`~PIL.ImagePalette` module ================================== The :py:mod:`~PIL.ImagePalette` module contains a class of the same name to diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index 23544b613a0..5f560634998 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImagePath .. py:currentmodule:: PIL.ImagePath -:py:mod:`~PIL.ImagePath` Module +:py:mod:`~PIL.ImagePath` module =============================== The :py:mod:`~PIL.ImagePath` module is used to store and manipulate 2-dimensional diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index 7e67a44d364..88d7b8a2095 100644 --- a/docs/reference/ImageQt.rst +++ b/docs/reference/ImageQt.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageQt .. py:currentmodule:: PIL.ImageQt -:py:mod:`~PIL.ImageQt` Module +:py:mod:`~PIL.ImageQt` module ============================= The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6 or PySide6 diff --git a/docs/reference/ImageSequence.rst b/docs/reference/ImageSequence.rst index a27b2fb4efc..0d6f394dd6f 100644 --- a/docs/reference/ImageSequence.rst +++ b/docs/reference/ImageSequence.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageSequence .. py:currentmodule:: PIL.ImageSequence -:py:mod:`~PIL.ImageSequence` Module +:py:mod:`~PIL.ImageSequence` module =================================== The :py:mod:`~PIL.ImageSequence` module contains a wrapper class that lets you diff --git a/docs/reference/ImageShow.rst b/docs/reference/ImageShow.rst index 5cedede69e6..12c8741cee2 100644 --- a/docs/reference/ImageShow.rst +++ b/docs/reference/ImageShow.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageShow .. py:currentmodule:: PIL.ImageShow -:py:mod:`~PIL.ImageShow` Module +:py:mod:`~PIL.ImageShow` module =============================== -The :py:mod:`~PIL.ImageShow` Module is used to display images. +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 diff --git a/docs/reference/ImageStat.rst b/docs/reference/ImageStat.rst index f694663828a..ede1199209c 100644 --- a/docs/reference/ImageStat.rst +++ b/docs/reference/ImageStat.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageStat .. py:currentmodule:: PIL.ImageStat -:py:mod:`~PIL.ImageStat` Module +:py:mod:`~PIL.ImageStat` module =============================== The :py:mod:`~PIL.ImageStat` module calculates global statistics for an image, or diff --git a/docs/reference/ImageText.rst b/docs/reference/ImageText.rst new file mode 100644 index 00000000000..8744ad36825 --- /dev/null +++ b/docs/reference/ImageText.rst @@ -0,0 +1,61 @@ +.. py:module:: PIL.ImageText +.. py:currentmodule:: PIL.ImageText + +:py:mod:`~PIL.ImageText` module +=============================== + +The :py:mod:`~PIL.ImageText` module defines a :py:class:`~PIL.ImageText.Text` class. +Instances of this class provide a way to use fonts with text strings or bytes. The +result is a simple API to apply styling to pieces of text and measure or draw them. + +Example +------- + +:: + + from PIL import Image, ImageDraw, ImageFont, ImageText + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 24) + + text = ImageText.Text("Hello world", font) + text.embed_color() + text.stroke(2, "#0f0") + + print(text.get_length()) # 154.0 + print(text.get_bbox()) # (-2, 3, 156, 22) + + im = Image.new("RGB", text.get_bbox()[2:]) + d = ImageDraw.Draw(im) + d.text((0, 0), text, "#f00") + +Comparison +---------- + +Without ``ImageText.Text``:: + + from PIL import Image, ImageDraw + im = Image.new(mode, size) + d = ImageDraw.Draw(im) + + d.textlength(text, font, direction, features, language, embedded_color) + d.multiline_textbbox(xy, text, font, anchor, spacing, align, direction, features, language, stroke_width, embedded_color) + d.text(xy, text, fill, font, anchor, spacing, align, direction, features, language, stroke_width, stroke_fill, embedded_color) + +With ``ImageText.Text``:: + + from PIL import ImageText + text = ImageText.Text(text, font, mode, spacing, direction, features, language) + text.embed_color() + text.stroke(stroke_width, stroke_fill) + + text.get_length() + text.get_bbox(xy, anchor, align) + + im = Image.new(mode, size) + d = ImageDraw.Draw(im) + d.text(xy, text, fill, anchor=anchor, align=align) + +Methods +------- + +.. autoclass:: PIL.ImageText.Text + :members: diff --git a/docs/reference/ImageTk.rst b/docs/reference/ImageTk.rst index 134ef565188..3ab72b83dbf 100644 --- a/docs/reference/ImageTk.rst +++ b/docs/reference/ImageTk.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageTk .. py:currentmodule:: PIL.ImageTk -:py:mod:`~PIL.ImageTk` Module +:py:mod:`~PIL.ImageTk` module ============================= The :py:mod:`~PIL.ImageTk` module contains support to create and modify Tkinter diff --git a/docs/reference/ImageTransform.rst b/docs/reference/ImageTransform.rst index 5b0a5ce49dc..5302799349a 100644 --- a/docs/reference/ImageTransform.rst +++ b/docs/reference/ImageTransform.rst @@ -2,7 +2,7 @@ .. py:module:: PIL.ImageTransform .. py:currentmodule:: PIL.ImageTransform -:py:mod:`~PIL.ImageTransform` Module +:py:mod:`~PIL.ImageTransform` module ==================================== The :py:mod:`~PIL.ImageTransform` module contains implementations of diff --git a/docs/reference/ImageWin.rst b/docs/reference/ImageWin.rst index 4151be4a746..c0b9bd2ba95 100644 --- a/docs/reference/ImageWin.rst +++ b/docs/reference/ImageWin.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageWin .. py:currentmodule:: PIL.ImageWin -:py:mod:`~PIL.ImageWin` Module (Windows-only) +:py:mod:`~PIL.ImageWin` module (Windows-only) ============================================= The :py:mod:`~PIL.ImageWin` module contains support to create and display images on diff --git a/docs/reference/JpegPresets.rst b/docs/reference/JpegPresets.rst index aafae44cf4a..b0a3ba8b532 100644 --- a/docs/reference/JpegPresets.rst +++ b/docs/reference/JpegPresets.rst @@ -1,6 +1,6 @@ .. py:currentmodule:: PIL.JpegPresets -:py:mod:`~PIL.JpegPresets` Module +:py:mod:`~PIL.JpegPresets` module ================================= .. automodule:: PIL.JpegPresets diff --git a/docs/reference/PSDraw.rst b/docs/reference/PSDraw.rst index 3e8512e7aa8..9eed775fc09 100644 --- a/docs/reference/PSDraw.rst +++ b/docs/reference/PSDraw.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.PSDraw .. py:currentmodule:: PIL.PSDraw -:py:mod:`~PIL.PSDraw` Module +:py:mod:`~PIL.PSDraw` module ============================ The :py:mod:`~PIL.PSDraw` module provides simple print support for PostScript diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index 1ac3d034b49..e4af94b9f18 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -1,6 +1,6 @@ .. _PixelAccess: -:py:class:`PixelAccess` Class +:py:class:`PixelAccess` class ============================= The PixelAccess class provides read and write access to @@ -40,7 +40,7 @@ Access using negative indexes is also possible. :: -:py:class:`PixelAccess` Class +:py:class:`PixelAccess` class ----------------------------- .. class:: PixelAccess @@ -59,7 +59,7 @@ Access using negative indexes is also possible. :: 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. See :ref:`colors` for more information. :param xy: The pixel coordinate, given as (x, y). :param color: The pixel value according to its mode, diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst deleted file mode 100644 index 04b2a47eed3..00000000000 --- a/docs/reference/PyAccess.rst +++ /dev/null @@ -1,47 +0,0 @@ -.. py:module:: PIL.PyAccess -.. py:currentmodule:: PIL.PyAccess - -:py:mod:`~PIL.PyAccess` Module -============================== - -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. - - :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 -------- - -The following script loads an image, accesses one pixel from it, then changes it. :: - - from PIL import Image - - 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. :: - - px[-1, -1] = (0, 0, 0) - print(px[-1, -1]) - - - -:py:class:`PyAccess` Class --------------------------- - -.. autoclass:: PIL.PyAccess.PyAccess() - :members: - :special-members: __getitem__, __setitem__ diff --git a/docs/reference/TiffTags.rst b/docs/reference/TiffTags.rst index 7cb7d16ae47..d75a4847897 100644 --- a/docs/reference/TiffTags.rst +++ b/docs/reference/TiffTags.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.TiffTags .. py:currentmodule:: PIL.TiffTags -:py:mod:`~PIL.TiffTags` Module +:py:mod:`~PIL.TiffTags` module ============================== The :py:mod:`~PIL.TiffTags` module exposes many of the standard TIFF diff --git a/docs/reference/arrow_support.rst b/docs/reference/arrow_support.rst new file mode 100644 index 00000000000..8e8b86c8e4d --- /dev/null +++ b/docs/reference/arrow_support.rst @@ -0,0 +1,86 @@ +.. _arrow-support: + +============= +Arrow support +============= + +`Arrow `__ +is an in-memory data exchange format that is the spiritual +successor to the NumPy array interface. It provides for zero-copy +access to columnar data, which in our case is ``Image`` data. + +The goal with Arrow is to provide native zero-copy interoperability +with any Arrow provider or consumer in the Python ecosystem. + +.. warning:: Zero-copy does not mean zero allocation -- the internal + memory layout of Pillow images contains an allocation for row + pointers, so there is a non-zero, but significantly smaller than a + full-copy memory cost to reading an Arrow image. + + +Data formats +============ + +Pillow currently supports exporting Arrow images in all modes. + +For single-band images, the exported array is width*height elements, +with each pixel corresponding to the appropriate Arrow type. + +For multiband images, the exported array is width*height fixed-length +four-element arrays of uint8. This is memory compatible with the raw +image storage of four bytes per pixel. + +Mode ``1`` images are exported as one uint8 byte/pixel, as this is +consistent with the internal storage. + +Pillow will accept, but not produce, one other format. For any +multichannel image with 32-bit storage per pixel, Pillow will accept +an array of width*height int32 elements, which will then be +interpreted using the mode-specific interpretation of the bytes. + +The image mode must match the Arrow band format when reading single +channel images. + +Memory allocator +================ + +Pillow's default memory allocator, the :ref:`block_allocator`, +allocates up to a 16 MB block for images by default. Larger images +overflow into additional blocks. Arrow requires a single continuous +memory allocation, so images allocated in multiple blocks cannot be +exported in the Arrow format. + +To enable the single block allocator:: + + from PIL import Image + Image.core.set_use_block_allocator(1) + +Note that this is a global setting, not a per-image setting. + +Unsupported features +==================== + +* Table/dataframe protocol. We support a single array. +* Null markers, producing or consuming. Null values are inferred from + the mode, e.g. RGB images are stored in the first three bytes of + each 32-bit pixel, and the last byte is an implied null. +* Schema negotiation. There is an optional schema for the requested + datatype in the Arrow source interface. We ignore that + parameter. +* Array metadata. + +Internal details +================ + +Python Arrow C interface: +https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html + +The memory that is exported from the Arrow interface is shared -- not +copied, so the lifetime of the memory allocation is no longer strictly +tied to the life of the Python object. + +The core imaging struct now has a refcount associated with it, and the +lifetime of the core image struct is now divorced from the Python +image object. Creating an arrow reference to the image increments the +refcount, and the imaging struct is only released when the refcount +reaches zero. diff --git a/docs/reference/block_allocator.rst b/docs/reference/block_allocator.rst index 1abe5280fbf..5ad9d9fd123 100644 --- a/docs/reference/block_allocator.rst +++ b/docs/reference/block_allocator.rst @@ -1,7 +1,10 @@ -Block Allocator + +.. _block_allocator: + +Block allocator =============== -Previous Design +Previous design --------------- Historically there have been two image allocators in Pillow: @@ -13,7 +16,7 @@ large images and makes one allocation for each scan line of size between one allocation and potentially thousands of small allocations, leading to unpredictable performance penalties around the transition. -New Design +New design ---------- ``ImagingAllocateArray`` now allocates space for images as a chain of @@ -25,7 +28,7 @@ line. This is now the default for all internal allocations. specifically requesting a single segment of memory for sharing with other code. -Memory Pools +Memory pools ------------ There is now a memory pool to contain a supply of recently freed @@ -34,14 +37,14 @@ fresh allocation. This caching of free blocks is currently disabled by default, but can be enabled and tweaked using three environment variables: - * ``PILLOW_ALIGNMENT``, in bytes. Specifies the alignment of memory - allocations. Valid values are powers of 2 between 1 and - 128, inclusive. Defaults to 1. +* ``PILLOW_ALIGNMENT``, in bytes. Specifies the alignment of memory + allocations. Valid values are powers of 2 between 1 and + 128, inclusive. Defaults to 1. - * ``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. +* ``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. - * ``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. +* ``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. diff --git a/docs/reference/c_extension_debugging.rst b/docs/reference/c_extension_debugging.rst index 5e85869058c..12dca6cf2c9 100644 --- a/docs/reference/c_extension_debugging.rst +++ b/docs/reference/c_extension_debugging.rst @@ -1,5 +1,5 @@ -C Extension debugging on Linux, with gbd/valgrind. -================================================== +C extension debugging on Linux, with GBD/Valgrind +================================================= Install the tools ----------------- @@ -17,7 +17,7 @@ Then ``sudo apt-get install libtiff5-dbgsym`` - There's a bug with the ``python3-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: + running Python: :: @@ -49,7 +49,7 @@ Then ``sudo apt-get install libtiff5-dbgsym`` source ~/vpy38-dbg/bin/activate cd ~/Pillow && make install -Test Case +Test case --------- Take your test image, and make a really simple harness. diff --git a/docs/reference/features.rst b/docs/reference/features.rst index c6619306186..45067ba3587 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.features .. py:currentmodule:: PIL.features -:py:mod:`~PIL.features` Module +:py:mod:`~PIL.features` module ============================== The :py:mod:`PIL.features` module can be used to detect which Pillow features are available on your system. @@ -21,6 +21,7 @@ Support for the following modules can be checked: * ``freetype2``: FreeType font support via :py:func:`PIL.ImageFont.truetype`. * ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`. * ``webp``: WebP image support. +* ``avif``: AVIF image support. .. autofunction:: PIL.features.check_module .. autofunction:: PIL.features.version_module @@ -54,9 +55,8 @@ 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. +* ``mozjpeg``: (compile time) Whether Pillow was compiled against the MozJPEG version of libjpeg. Compile-time version number is available. +* ``zlib_ng``: (compile time) Whether Pillow was compiled against the zlib-ng version of zlib. Compile-time version number is available. * ``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. diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 82c75e373ad..1ce26c909e8 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -24,6 +24,7 @@ Reference ImageSequence ImageShow ImageStat + ImageText ImageTk ImageTransform ImageWin @@ -32,7 +33,6 @@ Reference JpegPresets PSDraw PixelAccess - PyAccess features ../PIL plugins diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst index 99a18e9ea99..6bba673b9d6 100644 --- a/docs/reference/internal_design.rst +++ b/docs/reference/internal_design.rst @@ -1,4 +1,4 @@ -Internal Reference +Internal reference ================== .. toctree:: @@ -9,3 +9,4 @@ Internal Reference block_allocator internal_modules c_extension_debugging + arrow_support diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index e4cb17c4d8f..41a8837b30a 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -1,7 +1,7 @@ -Internal Modules +Internal modules ================ -:mod:`~PIL._binary` Module +:mod:`~PIL._binary` module -------------------------- .. automodule:: PIL._binary @@ -9,7 +9,7 @@ Internal Modules :undoc-members: :show-inheritance: -:mod:`~PIL._deprecate` Module +:mod:`~PIL._deprecate` module ----------------------------- .. automodule:: PIL._deprecate @@ -17,7 +17,7 @@ Internal Modules :undoc-members: :show-inheritance: -:mod:`~PIL._tkinter_finder` Module +:mod:`~PIL._tkinter_finder` module ---------------------------------- .. automodule:: PIL._tkinter_finder @@ -25,7 +25,7 @@ Internal Modules :undoc-members: :show-inheritance: -:mod:`~PIL._typing` Module +:mod:`~PIL._typing` module -------------------------- .. module:: PIL._typing @@ -33,6 +33,14 @@ Internal Modules Provides a convenient way to import type hints that are not available on some Python versions. +.. py:class:: Buffer + + Typing alias. + +.. py:class:: IntegralLike + + Typing alias. + .. py:class:: NumpyArray Typing alias. @@ -45,12 +53,7 @@ on some Python versions. An object that supports the read method. -.. py:data:: TypeGuard - :value: typing.TypeGuard - - See :py:obj:`typing.TypeGuard`. - -:mod:`~PIL._util` Module +:mod:`~PIL._util` module ------------------------ .. automodule:: PIL._util @@ -58,7 +61,7 @@ on some Python versions. :undoc-members: :show-inheritance: -:mod:`~PIL._version` Module +:mod:`~PIL._version` module --------------------------- .. module:: PIL._version @@ -70,7 +73,7 @@ on some Python versions. This is the master version number for Pillow, all other uses reference this module. -:mod:`PIL.Image.core` Module +:mod:`PIL.Image.core` module ---------------------------- .. module:: PIL._imaging @@ -78,3 +81,7 @@ on some Python versions. An internal interface module previously known as :mod:`~PIL._imaging`, implemented in :file:`_imaging.c`. + +.. py:class:: ImagingCore + + A representation of the image data. diff --git a/docs/reference/limits.rst b/docs/reference/limits.rst index a71b514b5aa..d2f8f7d1fab 100644 --- a/docs/reference/limits.rst +++ b/docs/reference/limits.rst @@ -4,7 +4,7 @@ Limits This page is documentation to the various fundamental size limits in the Pillow implementation. -Internal Limits +Internal limits =============== * Image sizes cannot be negative. These are checked both in @@ -25,10 +25,10 @@ Internal Limits is smaller than 2GB, as calculated by ``y*stride`` (so 2Gpx for 'L' images, and .5Gpx for 'RGB' -Format Size Limits +Format size limits ================== * ICO: Max size is 256x256 -* Webp: 16383x16383 (underlying library size limit: +* WebP: 16383x16383 (underlying library size limit: https://developers.google.com/speed/webp/docs/api) diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index 730c8da5b80..0d43cbc730e 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -1,6 +1,6 @@ .. _file-handling: -File Handling in Pillow +File handling in Pillow ======================= When opening a file as an image, Pillow requires a filename, ``os.PathLike`` @@ -36,7 +36,7 @@ have multiple frames. 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 lifecycle --------------- * ``Image.open()`` Filenames and ``Path`` objects are opened as a file. @@ -97,7 +97,7 @@ Complications im6.load() # FAILS, closed file -Proposed File Handling +Proposed file handling ---------------------- * ``Image.Image.load()`` should close the image file, unless there are diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index 18cd99cf3a2..243d4f353f5 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -1,7 +1,15 @@ Plugin reference ================ -:mod:`~PIL.BmpImagePlugin` Module +:mod:`~PIL.AvifImagePlugin` module +---------------------------------- + +.. automodule:: PIL.AvifImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.BmpImagePlugin` module --------------------------------- .. automodule:: PIL.BmpImagePlugin @@ -9,7 +17,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.BufrStubImagePlugin` Module +:mod:`~PIL.BufrStubImagePlugin` module -------------------------------------- .. automodule:: PIL.BufrStubImagePlugin @@ -17,7 +25,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.CurImagePlugin` Module +:mod:`~PIL.CurImagePlugin` module --------------------------------- .. automodule:: PIL.CurImagePlugin @@ -25,7 +33,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.DcxImagePlugin` Module +:mod:`~PIL.DcxImagePlugin` module --------------------------------- .. automodule:: PIL.DcxImagePlugin @@ -33,7 +41,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.DdsImagePlugin` Module +:mod:`~PIL.DdsImagePlugin` module --------------------------------- .. automodule:: PIL.DdsImagePlugin @@ -41,7 +49,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.EpsImagePlugin` Module +:mod:`~PIL.EpsImagePlugin` module --------------------------------- .. automodule:: PIL.EpsImagePlugin @@ -49,15 +57,15 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.FitsImagePlugin` Module --------------------------------------- +:mod:`~PIL.FitsImagePlugin` module +---------------------------------- .. automodule:: PIL.FitsImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`~PIL.FliImagePlugin` Module +:mod:`~PIL.FliImagePlugin` module --------------------------------- .. automodule:: PIL.FliImagePlugin @@ -65,7 +73,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.FpxImagePlugin` Module +:mod:`~PIL.FpxImagePlugin` module --------------------------------- .. automodule:: PIL.FpxImagePlugin @@ -73,7 +81,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.GbrImagePlugin` Module +:mod:`~PIL.GbrImagePlugin` module --------------------------------- .. automodule:: PIL.GbrImagePlugin @@ -81,7 +89,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.GifImagePlugin` Module +:mod:`~PIL.GifImagePlugin` module --------------------------------- .. automodule:: PIL.GifImagePlugin @@ -89,7 +97,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.GribStubImagePlugin` Module +:mod:`~PIL.GribStubImagePlugin` module -------------------------------------- .. automodule:: PIL.GribStubImagePlugin @@ -97,7 +105,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.Hdf5StubImagePlugin` Module +:mod:`~PIL.Hdf5StubImagePlugin` module -------------------------------------- .. automodule:: PIL.Hdf5StubImagePlugin @@ -105,7 +113,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.IcnsImagePlugin` Module +:mod:`~PIL.IcnsImagePlugin` module ---------------------------------- .. automodule:: PIL.IcnsImagePlugin @@ -113,7 +121,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.IcoImagePlugin` Module +:mod:`~PIL.IcoImagePlugin` module --------------------------------- .. automodule:: PIL.IcoImagePlugin @@ -121,7 +129,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.ImImagePlugin` Module +:mod:`~PIL.ImImagePlugin` module -------------------------------- .. automodule:: PIL.ImImagePlugin @@ -129,7 +137,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.ImtImagePlugin` Module +:mod:`~PIL.ImtImagePlugin` module --------------------------------- .. automodule:: PIL.ImtImagePlugin @@ -137,7 +145,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.IptcImagePlugin` Module +:mod:`~PIL.IptcImagePlugin` module ---------------------------------- .. automodule:: PIL.IptcImagePlugin @@ -145,7 +153,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.JpegImagePlugin` Module +:mod:`~PIL.JpegImagePlugin` module ---------------------------------- .. automodule:: PIL.JpegImagePlugin @@ -153,7 +161,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.Jpeg2KImagePlugin` Module +:mod:`~PIL.Jpeg2KImagePlugin` module ------------------------------------ .. automodule:: PIL.Jpeg2KImagePlugin @@ -161,7 +169,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.McIdasImagePlugin` Module +:mod:`~PIL.McIdasImagePlugin` module ------------------------------------ .. automodule:: PIL.McIdasImagePlugin @@ -169,7 +177,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.MicImagePlugin` Module +:mod:`~PIL.MicImagePlugin` module --------------------------------- .. automodule:: PIL.MicImagePlugin @@ -177,7 +185,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.MpegImagePlugin` Module +:mod:`~PIL.MpegImagePlugin` module ---------------------------------- .. automodule:: PIL.MpegImagePlugin @@ -185,7 +193,15 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.MspImagePlugin` Module +:mod:`~PIL.MpoImagePlugin` module +--------------------------------- + +.. automodule:: PIL.MpoImagePlugin + :members: + :undoc-members: + :show-inheritance: + +:mod:`~PIL.MspImagePlugin` module --------------------------------- .. automodule:: PIL.MspImagePlugin @@ -193,7 +209,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PalmImagePlugin` Module +:mod:`~PIL.PalmImagePlugin` module ---------------------------------- .. automodule:: PIL.PalmImagePlugin @@ -201,7 +217,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PcdImagePlugin` Module +:mod:`~PIL.PcdImagePlugin` module --------------------------------- .. automodule:: PIL.PcdImagePlugin @@ -209,7 +225,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PcxImagePlugin` Module +:mod:`~PIL.PcxImagePlugin` module --------------------------------- .. automodule:: PIL.PcxImagePlugin @@ -217,7 +233,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PdfImagePlugin` Module +:mod:`~PIL.PdfImagePlugin` module --------------------------------- .. automodule:: PIL.PdfImagePlugin @@ -225,7 +241,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PixarImagePlugin` Module +:mod:`~PIL.PixarImagePlugin` module ----------------------------------- .. automodule:: PIL.PixarImagePlugin @@ -233,7 +249,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PngImagePlugin` Module +:mod:`~PIL.PngImagePlugin` module --------------------------------- .. automodule:: PIL.PngImagePlugin @@ -244,7 +260,7 @@ Plugin reference :member-order: groupwise -:mod:`~PIL.PpmImagePlugin` Module +:mod:`~PIL.PpmImagePlugin` module --------------------------------- .. automodule:: PIL.PpmImagePlugin @@ -252,7 +268,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PsdImagePlugin` Module +:mod:`~PIL.PsdImagePlugin` module --------------------------------- .. automodule:: PIL.PsdImagePlugin @@ -260,7 +276,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.SgiImagePlugin` Module +:mod:`~PIL.SgiImagePlugin` module --------------------------------- .. automodule:: PIL.SgiImagePlugin @@ -268,7 +284,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.SpiderImagePlugin` Module +:mod:`~PIL.SpiderImagePlugin` module ------------------------------------ .. automodule:: PIL.SpiderImagePlugin @@ -276,7 +292,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.SunImagePlugin` Module +:mod:`~PIL.SunImagePlugin` module --------------------------------- .. automodule:: PIL.SunImagePlugin @@ -284,7 +300,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.TgaImagePlugin` Module +:mod:`~PIL.TgaImagePlugin` module --------------------------------- .. automodule:: PIL.TgaImagePlugin @@ -292,7 +308,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.TiffImagePlugin` Module +:mod:`~PIL.TiffImagePlugin` module ---------------------------------- .. automodule:: PIL.TiffImagePlugin @@ -300,7 +316,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.WebPImagePlugin` Module +:mod:`~PIL.WebPImagePlugin` module ---------------------------------- .. automodule:: PIL.WebPImagePlugin @@ -308,7 +324,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.WmfImagePlugin` Module +:mod:`~PIL.WmfImagePlugin` module --------------------------------- .. automodule:: PIL.WmfImagePlugin @@ -316,7 +332,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.XVThumbImagePlugin` Module +:mod:`~PIL.XVThumbImagePlugin` module ------------------------------------- .. automodule:: PIL.XVThumbImagePlugin @@ -324,7 +340,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.XbmImagePlugin` Module +:mod:`~PIL.XbmImagePlugin` module --------------------------------- .. automodule:: PIL.XbmImagePlugin @@ -332,7 +348,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.XpmImagePlugin` Module +:mod:`~PIL.XpmImagePlugin` module --------------------------------- .. automodule:: PIL.XpmImagePlugin diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index adada6e0151..3e2aa84b1ce 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -28,7 +28,7 @@ This threshold can be changed by setting :py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``. -Backwards Incompatible Changes +Backwards incompatible changes ============================== Categories @@ -158,13 +158,13 @@ PyAccess and Image.USE_CFFI_ACCESS ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Since Pillow's C API is now faster than PyAccess on PyPy, -:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow +:py:mod:`!PyAccess` has been deprecated and will be removed in Pillow 11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. ``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is similarly deprecated. -API Changes +API changes =========== Added line width parameter to ImageDraw regular_polygon @@ -173,7 +173,7 @@ Added line width parameter to ImageDraw regular_polygon An optional line ``width`` parameter has been added to ``ImageDraw.Draw.regular_polygon``. -API Additions +API additions ============= Added ``alpha_only`` argument to ``getbbox()`` @@ -184,7 +184,7 @@ Added ``alpha_only`` argument to ``getbbox()`` and the image has an alpha channel, trim transparent pixels. Otherwise, trim pixels when all channels are zero. -Other Changes +Other changes ============= 32-bit wheels diff --git a/docs/releasenotes/10.0.1.rst b/docs/releasenotes/10.0.1.rst index 02189d51405..aa17a62e02b 100644 --- a/docs/releasenotes/10.0.1.rst +++ b/docs/releasenotes/10.0.1.rst @@ -11,7 +11,7 @@ This release provides an updated install script and updated wheels to include libwebp 1.3.2, preventing a potential heap buffer overflow in WebP. -Other Changes +Other changes ============= Updated tests to pass with latest zlib version diff --git a/docs/releasenotes/10.1.0.rst b/docs/releasenotes/10.1.0.rst index fd556bdf17f..996e6a3ed3f 100644 --- a/docs/releasenotes/10.1.0.rst +++ b/docs/releasenotes/10.1.0.rst @@ -1,7 +1,7 @@ 10.1.0 ------ -API Changes +API changes =========== Setting image mode @@ -35,7 +35,7 @@ to be specified, rather than a single number for both dimensions. :: ImageFilter.BoxBlur((2, 5)) ImageFilter.GaussianBlur((2, 5)) -API Additions +API additions ============= EpsImagePlugin.gs_binary @@ -71,7 +71,7 @@ size and font_size arguments when using default font Pillow has had a "better than nothing" default font, which can only be drawn at one font size. Now, if FreeType support is available, a version of -`Aileron Regular `_ is loaded, which can be +`Aileron Regular `_ is loaded, which can be drawn at chosen font sizes. The following ``size`` and ``font_size`` arguments can now be used to specify a @@ -84,7 +84,7 @@ font size for this new builtin font:: draw.multiline_text((0, 0), "test", font_size=24) draw.multiline_textbbox((0, 0), "test", font_size=24) -Other Changes +Other changes ============= Python 3.12 diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 1c6b78b0841..3377487857e 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -53,7 +53,7 @@ The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant for internal use, so there is no replacement. They can each be replaced by a single line of code using builtin functions in Python. -API Changes +API changes =========== Zero or negative font size error @@ -63,7 +63,7 @@ When creating a :py:class:`~PIL.ImageFont.FreeTypeFont` instance, either directl through :py:func:`~PIL.ImageFont.truetype`, if the font size is zero or less, a :py:exc:`ValueError` will now be raised. -API Additions +API additions ============= Added DdsImagePlugin enums @@ -95,7 +95,7 @@ JPEG tables-only streamtype When saving JPEG files, ``streamtype`` can now be set to 1, for tables-only. This will output only the quantization and Huffman tables for the image. -Other Changes +Other changes ============= Added DDS BC4U and DX10 BC1 and BC4 reading diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index 2f0437d94f9..6c7d8ea0a11 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -65,7 +65,7 @@ ImageMath.eval() :py:meth:`~PIL.ImageMath.unsafe_eval` instead. See earlier security notes for more information. -API Changes +API changes =========== Added alpha_quality argument when saving WebP images @@ -87,7 +87,7 @@ Negative P1-P3 PPM value error If a P1-P3 PPM image contains a negative value, a :py:exc:`ValueError` will now be raised. -API Additions +API additions ============= Added PerspectiveTransform @@ -97,7 +97,7 @@ Added PerspectiveTransform that all of the :py:data:`~PIL.Image.Transform` values now have a corresponding subclass of :py:class:`~PIL.ImageTransform.Transform`. -Other Changes +Other changes ============= Portable FloatMap (PFM) images diff --git a/docs/releasenotes/10.4.0.rst b/docs/releasenotes/10.4.0.rst index 8d3706be617..84a6091c96d 100644 --- a/docs/releasenotes/10.4.0.rst +++ b/docs/releasenotes/10.4.0.rst @@ -41,7 +41,7 @@ ImageDraw.getdraw hints parameter The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. -API Additions +API additions ============= ImageDraw.circle @@ -51,7 +51,7 @@ Added :py:meth:`~PIL.ImageDraw.ImageDraw.circle`. It provides the same functiona :py:meth:`~PIL.ImageDraw.ImageDraw.ellipse`, but instead of taking a bounding box, it takes a center point and radius. -Other Changes +Other changes ============= Python 3.13 beta diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst new file mode 100644 index 00000000000..020fbf7df7d --- /dev/null +++ b/docs/releasenotes/11.0.0.rst @@ -0,0 +1,162 @@ +11.0.0 +------ + +Backwards incompatible changes +============================== + +Python 3.8 +^^^^^^^^^^ + +Pillow has dropped support for Python 3.8, +which reached end-of-life in October 2024. + +Python 3.12 on macOS <= 10.12 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The latest version of Python 3.12 only supports macOS versions 10.13 and later, +and so Pillow has also updated the deployment target for its prebuilt Python 3.12 +wheels. + +PSFile +^^^^^^ + +The :py:class:`!PSFile` class was removed in Pillow 11 (2024-10-15). +This class was only made as a helper to be used internally, +so there is no replacement. If you need this functionality though, +it is a very short class that can easily be recreated in your own code. + +PyAccess and Image.USE_CFFI_ACCESS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Since Pillow's C API is now faster than PyAccess on PyPy, ``PyAccess`` has been +removed. Pillow's C API will now be used on PyPy instead. + +``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, was +similarly removed. + +TiffImagePlugin IFD_LEGACY_API +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An unused setting, ``TiffImagePlugin.IFD_LEGACY_API``, has been removed. + +WebP 0.4 +^^^^^^^^ + +Support for WebP 0.4 and earlier has been removed; WebP 0.5 is the minimum supported. + +Deprecations +============ + +FreeType 2.9.0 +^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0 +(2025-10-15), when FreeType 2.9.1 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:`2020-15999`). + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ + +Get internal pointers to objects +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been +deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obtaining +raw pointers to ``ImagingCore`` internals. To interact with C code, you can use +``Image.Image.getim()``, which returns a ``Capsule`` object. + +ICNS (width, height, scale) sizes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +Setting an ICNS image size to ``(width, height, scale)`` before loading has been +deprecated. Instead, ``load(scale)`` can be used. + +Image isImageType() +^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +``Image.isImageType(im)`` has been deprecated. Use ``isinstance(im, Image.Image)`` +instead. + +ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and +:py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more +keyword arguments can be used instead. + +JpegImageFile.huffman_ac and JpegImageFile.huffman_dc +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They +have been deprecated, and will be removed in Pillow 12 (2025-10-15). + +Specific WebP feature checks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +``features.check("transp_webp")``, ``features.check("webp_mux")`` and +``features.check("webp_anim")`` are now deprecated. They will always return +``True`` if the WebP module is installed, until they are removed in Pillow +12.0.0 (2025-10-15). + +API changes +=========== + +Default resampling filter for I;16* image modes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The default resampling filter for I;16, I;16L, I;16B and I;16N has been changed from +``Image.NEAREST`` to ``Image.BICUBIC``, to match the majority of modes. + +API additions +============= + +Writing XMP bytes to JPEG and MPO +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +XMP data can now be saved to JPEG files using an ``xmp`` argument:: + + im.save("out.jpg", xmp=b"test") + +The data can also be set through :py:attr:`~PIL.Image.Image.info`, for use when saving +either JPEG or MPO images:: + + im.info["xmp"] = b"test" + im.save("out.jpg") + +Other changes +============= + +Python 3.13 +^^^^^^^^^^^ + +Pillow 10.4.0 had wheels built against Python 3.13 beta, available as a preview to help +others prepare for 3.13, and to ensure Pillow could be used immediately at the release +of 3.13.0 final (2024-10-07, :pep:`719`). + +Pillow 11.0.0 now officially supports Python 3.13. + +Support has also been added for the experimental free-threaded mode of :pep:`703`. + +Python 3.13 only supports macOS versions 10.13 and later. + +C-level flags +^^^^^^^^^^^^^ + +Some compiling flags like ``WITH_THREADING``, ``WITH_IMAGECHOPS``, and other +``WITH_*`` were removed. These flags were not available through the build system, +but they could be edited in the C source. diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst new file mode 100644 index 00000000000..4888ddf56d3 --- /dev/null +++ b/docs/releasenotes/11.1.0.rst @@ -0,0 +1,80 @@ +11.1.0 +------ + +Deprecations +============ + +ExifTags.IFD.Makernote +^^^^^^^^^^^^^^^^^^^^^^ + +``ExifTags.IFD.Makernote`` has been deprecated. Instead, use +``ExifTags.IFD.MakerNote``. + +API changes +=========== + +Writing XMP bytes to JPEG and MPO +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 11.0.0 added writing XMP data to JPEG and MPO images:: + + im.info["xmp"] = b"test" + im.save("out.jpg") + +However, this meant that XMP data was automatically kept from an opened image, +which is inconsistent with the rest of Pillow's behaviour. This functionality +has been removed. To write XMP data, the ``xmp`` argument can still be used for +JPEG files:: + + im.save("out.jpg", xmp=b"test") + +To save XMP data to the second frame of an MPO image, ``encoderinfo`` can now +be used:: + + second_im.encoderinfo = {"xmp": b"test"} + im.save("out.mpo", save_all=True, append_images=[second_im]) + +API additions +============= + +Check for zlib-ng +^^^^^^^^^^^^^^^^^ + +You can check if Pillow has been built against the zlib-ng version of the +zlib library, and what version of zlib-ng is being used:: + + from PIL import features + features.check_feature("zlib_ng") # True or False + features.version_feature("zlib_ng") # "2.2.2" for example, or None + +Saving TIFF as BigTIFF +^^^^^^^^^^^^^^^^^^^^^^ + +TIFF images can now be saved as BigTIFF using a ``big_tiff`` argument:: + + im.save("out.tiff", big_tiff=True) + +Other changes +============= + +Reading JPEG 2000 comments +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When opening a JPEG 2000 image, the comment may now be read into +:py:attr:`~PIL.Image.Image.info` for J2K images, not just JP2 images. + +Saving JPEG 2000 CMYK images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +With OpenJPEG 2.5.3 or later, Pillow can now save CMYK images as JPEG 2000 files. + +Minimum C version +^^^^^^^^^^^^^^^^^ + +C99 is now the minimum version of C required to compile Pillow from source. + +zlib-ng in wheels +^^^^^^^^^^^^^^^^^ + +Wheels are now built against zlib-ng for improved speed. In tests, saving a PNG +was found to be more than twice as fast at higher compression levels. diff --git a/docs/releasenotes/11.2.1.rst b/docs/releasenotes/11.2.1.rst new file mode 100644 index 00000000000..f55b0d7d776 --- /dev/null +++ b/docs/releasenotes/11.2.1.rst @@ -0,0 +1,118 @@ +11.2.1 +------ + +.. warning:: + + The release of Pillow *11.2.0* was halted prematurely, due to hitting PyPI's + project size limit and concern over the size of Pillow wheels containing libavif. + The PyPI limit has now been increased and Pillow *11.2.1* has been released + instead, without libavif included in the wheels. + To avoid confusion, the incomplete 11.2.0 release has been removed from PyPI. + +Security +======== + +Undefined shift when loading compressed DDS images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When loading some compressed DDS formats, an integer was bitshifted by 24 places to +generate the 32 bits of the lookup table. This was undefined behaviour, and has been +present since Pillow 3.4.0. + +Deprecations +============ + +Image.Image.get_child_images() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.2.1 + +``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow +13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The +method uses an image's file pointer, and so child images could only be retrieved from +an :py:class:`PIL.ImageFile.ImageFile` instance. + +API changes +=========== + +``append_images`` no longer requires ``save_all`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, ``save_all`` was required to in order to use ``append_images``. Now, +``save_all`` will default to ``True`` if ``append_images`` is not empty and the format +supports saving multiple frames:: + + im.save("out.gif", append_images=ims) + +API additions +============= + +``"justify"`` multiline text alignment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be +aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`:: + + from PIL import Image, ImageDraw + im = Image.new("RGB", (50, 25)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify") + draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify") + +Specify window in ImageGrab on Windows +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When using :py:meth:`~PIL.ImageGrab.grab`, a specific window can be selected using the +HWND:: + + from PIL import ImageGrab + ImageGrab.grab(window=hwnd) + +Check for MozJPEG +^^^^^^^^^^^^^^^^^ + +You can check if Pillow has been built against the MozJPEG version of the +libjpeg library, and what version of MozJPEG is being used:: + + from PIL import features + features.check_feature("mozjpeg") # True or False + features.version_feature("mozjpeg") # "4.1.1" for example, or None + +Saving compressed DDS images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1, DXT3, +DXT5, BC2, BC3 and BC5 are supported:: + + im.save("out.dds", pixel_format="DXT1") + +Other changes +============= + +Arrow support +^^^^^^^^^^^^^ + +`Arrow `__ is an in-memory data exchange format that is the +spiritual successor to the NumPy array interface. It provides for zero-copy access to +columnar data, which in our case is ``Image`` data. + +To create an image with zero-copy shared memory from an object exporting the +arrow_c_array interface protocol:: + + from PIL import Image + import pyarrow as pa + arr = pa.array([0]*(5*5*4), type=pa.uint8()) + im = Image.fromarrow(arr, 'RGBA', (5, 5)) + +Pillow images can also be converted to Arrow objects:: + + from PIL import Image + import pyarrow as pa + im = Image.open('hopper.jpg') + arr = pa.array(im) + +Reading and writing AVIF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow can now read and write AVIF images when built from source with libavif 1.0.0 +or later. diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst new file mode 100644 index 00000000000..5c04a037316 --- /dev/null +++ b/docs/releasenotes/11.3.0.rst @@ -0,0 +1,94 @@ +11.3.0 +------ + +Security +======== + +:cve:`2025-48379`: Write buffer overflow on BCn encoding +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +There is a heap buffer overflow when writing a sufficiently large (>64k encoded with +default settings) image in the DDS format due to writing into a buffer without checking +for available space. + +This only affects users who save untrusted data as a compressed DDS image. + +* Unclear how large the potential write could be. It is likely limited by process + segfault, so it's not necessarily deterministic. It may be practically unbounded. +* Unclear if there's a restriction on the bytes that could be emitted. It's likely that + the only restriction is that the bytes would be emitted in chunks of 8 or 16. + +This was introduced in Pillow 11.2.0 when the feature was added. + +Deprecations +============ + +Image.fromarray mode parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` has been deprecated. The +mode can be automatically determined from the object's shape and type instead. + +.. note:: + + Since pixel values do not contain information about palettes or color spaces, part + of this functionality was restored in Pillow 12.0.0. The parameter can be used to + place grayscale L mode data within a P mode image, or read RGB data as YCbCr for + example. + +Saving I mode images as PNG +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain +at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly +changing the data, this is now deprecated. Instead, the image can be converted to +another mode before saving:: + + from PIL import Image + im = Image.new("I", (1, 1)) + im.convert("I;16").save("out.png") + +Other changes +============= + +Added QOI saving +^^^^^^^^^^^^^^^^ + +Support has been added for saving QOI images. ``colorspace`` can be used to specify the +colorspace as sRGB with linear alpha, e.g. ``im.save("out.qoi", colorspace="sRGB")``. +By default, all channels will be linear. + +Support using more screenshot utilities with ImageGrab on Linux +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.ImageGrab.grab` is now able to use GNOME Screenshot, grim or Spectacle +on Linux in order to take a snapshot of the screen. + +Do not build against libavif < 1 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow only supports libavif 1.0.0 or later. In order to prevent errors when building +from source, if a user happens to have an earlier libavif on their system, Pillow will +now ignore it. + +AVIF support in wheels +^^^^^^^^^^^^^^^^^^^^^^ + +Support for reading and writing AVIF images is now included in Pillow's wheels, except +for Windows ARM64 and iOS. libaom is available as an encoder and dav1d as a decoder. +(Thank you Frankie Dintino and Andrew Murray!) + +iOS +^^^ + +Pillow now provides wheels that can be used on iOS ARM64 devices, and on the iOS +simulator on ARM64 and x86_64. Currently, only Python 3.13 wheels are available. +(Thank you Russell Keith-Magee and Andrew Murray!) + +Python 3.14 beta +^^^^^^^^^^^^^^^^ + +To help other projects prepare for Python 3.14, wheels are now built for the +3.14 beta as a preview. This is not official support for Python 3.14, but rather +an opportunity for you to test how Pillow works with the beta and report any +problems. diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst new file mode 100644 index 00000000000..4c00d8c4cc1 --- /dev/null +++ b/docs/releasenotes/12.0.0.rst @@ -0,0 +1,186 @@ +12.0.0 +------ + +Backwards incompatible changes +============================== + +Python 3.9 +^^^^^^^^^^ + +Pillow has dropped support for Python 3.9, +which reached end-of-life in October 2025. + +ImageFile.raise_oserror +^^^^^^^^^^^^^^^^^^^^^^^ + +``ImageFile.raise_oserror()`` has been removed. The function was undocumented and was +only useful for translating error codes returned by a codec's ``decode()`` method, +which ImageFile already did automatically. + +IptcImageFile helper functions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant +``IptcImageFile.PAD`` have been removed. These were undocumented helper functions +intended for internal use, so there is no replacement. They can each be replaced by a +single line of code using builtin functions in Python. + +ImageCms constants and versions() function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A number of constants and a function in :py:mod:`.ImageCms` have been removed. This +includes a table of flags based on LittleCMS version 1 which has been replaced with a +new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. + +============================================ ==================================================== +Deprecated Use instead +============================================ ==================================================== +``ImageCms.DESCRIPTION`` No replacement +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +============================================ ==================================================== + +ImageMath eval() +^^^^^^^^^^^^^^^^ + +``ImageMath.eval()`` has been removed. Use :py:meth:`~PIL.ImageMath.lambda_eval` or +:py:meth:`~PIL.ImageMath.unsafe_eval` instead. + +BGR;15, BGR 16 and BGR;24 +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The experimental BGR;15, BGR;16 and BGR;24 modes have been removed. + +Non-image modes in ImageCms +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The use in :py:mod:`.ImageCms` of input modes and output modes that are not Pillow +image modes has been removed. Defaulting to "L" or "1" if the mode cannot be mapped has +also been removed. + +Support for LibTIFF earlier than 4 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support for LibTIFF earlier than version 4 has been removed. +Upgrade to a newer version of LibTIFF instead. + +ImageDraw.getdraw hints parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been removed. + +FreeType 2.9.0 +^^^^^^^^^^^^^^ + +Support for FreeType 2.9.0 has been removed. FreeType 2.9.1 is the minimum version +supported. + +We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ + +Deprecations +============ + +Image._show +^^^^^^^^^^^ + +``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15). +Use :py:meth:`~PIL.ImageShow.show` instead. + +ImageCms.ImageCmsProfile.product_name and .product_info +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``ImageCms.ImageCmsProfile.product_name`` and the corresponding +``.product_info`` attributes have been deprecated, and will be removed in +Pillow 13 (2026-10-15). They have been set to ``None`` since Pillow 2.3.0. + +API changes +=========== + +Image.alpha_composite: LA images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.alpha_composite` can now use LA images as well as RGBA. + +API additions +============= + +Added ImageText.Text +^^^^^^^^^^^^^^^^^^^^ + +:py:class:`PIL.ImageText.Text` has been added, as a simpler way to use fonts with text +strings or bytes. + +Without ``ImageText.Text``:: + + from PIL import Image, ImageDraw + im = Image.new(mode, size) + d = ImageDraw.Draw(im) + + d.textlength(text, font, direction, features, language, embedded_color) + d.multiline_textbbox(xy, text, font, anchor, spacing, align, direction, features, language, stroke_width, embedded_color) + d.text(xy, text, fill, font, anchor, spacing, align, direction, features, language, stroke_width, stroke_fill, embedded_color) + +With ``ImageText.Text``:: + + from PIL import ImageText + text = ImageText.Text(text, font, mode, spacing, direction, features, language) + text.embed_color() + text.stroke(stroke_width, stroke_fill) + + text.get_length() + text.get_bbox(xy, anchor, align) + + im = Image.new(mode, size) + d = ImageDraw.Draw(im) + d.text(xy, text, fill, anchor=anchor, align=align) + +Other changes +============= + +Python 3.14 +^^^^^^^^^^^ + +Pillow 11.3.0 had wheels built against Python 3.14 beta, available as a preview to help +others prepare for 3.14, and to ensure Pillow could be used immediately at the release +of 3.14.0 final (2025-10-07, :pep:`745`). + +Pillow 12.0.0 now officially supports Python 3.14. + +Image.fromarray mode parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In Pillow 11.3.0, the ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` was +deprecated. Part of this functionality has been restored in Pillow 12.0.0. Since pixel +values do not contain information about palettes or color spaces, the parameter can be +used to place grayscale L mode data within a P mode image, or read RGB data as YCbCr +for example. + +ImageMorph operations must have length 1 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Valid ImageMorph operations are 4, N, 1 and M. By limiting the length to 1 character +within Pillow, long execution times can be avoided if a user provided long pattern +strings. Reported by `Jang Choi `__. diff --git a/docs/releasenotes/12.1.0.rst b/docs/releasenotes/12.1.0.rst new file mode 100644 index 00000000000..9740b700842 --- /dev/null +++ b/docs/releasenotes/12.1.0.rst @@ -0,0 +1,49 @@ +12.1.0 +------ + +Deprecations +============ + +Image getdata() +^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.Image.getdata` has been deprecated. +:py:meth:`~PIL.Image.Image.get_flattened_data` can be used instead. This new method is +identical, except that it returns a tuple of pixel values, instead of an internal +Pillow data type. + +API changes +=========== + +ImageMorph build_default_lut() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To match the behaviour of :py:meth:`~PIL.ImageMorph.LutBuilder.build_lut`, +:py:meth:`~PIL.ImageMorph.LutBuilder.build_default_lut()` now returns the new LUT. + +API additions +============= + +Image get_flattened_data() +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.Image.get_flattened_data` is identical to the deprecated +:py:meth:`~PIL.Image.Image.getdata`, except that the new method returns a tuple of +pixel values, instead of an internal Pillow data type. + +Specify window in ImageGrab on macOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When using :py:meth:`~PIL.ImageGrab.grab`, a specific window can now be selected on +macOS in addition to Windows. On macOS, this is a CGWindowID:: + + from PIL import ImageGrab + ImageGrab.grab(window=cgwindowid) + +Other changes +============= + +Added MorphOp support for 1 mode images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:class:`~PIL.ImageMorph.MorphOp` now supports both 1 mode and L mode images. diff --git a/docs/releasenotes/12.1.1.rst b/docs/releasenotes/12.1.1.rst new file mode 100644 index 00000000000..86083b4ad08 --- /dev/null +++ b/docs/releasenotes/12.1.1.rst @@ -0,0 +1,24 @@ +12.1.1 +------ + +Security +======== + +:cve:`2026-25990`: Fix OOB write with invalid tile extents +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Check that tile extents do not use negative x or y offsets when decoding or encoding, +and raise an error if they do, rather than allowing an OOB write. + +An out-of-bounds write may be triggered when opening a specially crafted PSD image. +This only affects Pillow >= 10.3.0. Reported by +`Yarden Porat `__. + +Other changes +============= + +Patch libavif for svt-av1 4.0 compatibility +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A patch has been added to ``depends/install_libavif.sh``, to allow libavif 1.3.0 to be +compatible with the recently released svt-av1 4.0.0. diff --git a/docs/releasenotes/12.2.0.rst b/docs/releasenotes/12.2.0.rst new file mode 100644 index 00000000000..b03afb6651f --- /dev/null +++ b/docs/releasenotes/12.2.0.rst @@ -0,0 +1,124 @@ +12.2.0 +------ + +Security +======== + +Prevent FITS decompression bomb +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When decompressing GZIP data from a FITS image, Pillow did not limit the amount of data +being read, meaning that it was vulnerable to GZIP decompression bombs. This was +introduced in Pillow 10.3.0. + +The data being read is now limited to only the necessary amount. + +Fix OOB write with invalid tile extents +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 12.1.1 added improved checks for tile extents to prevent an OOB write from +specially crafted PSD images in Pillow >= 10.3.0. However, these checks did not +consider integer overflow. This has been corrected. + +Prevent PDF parsing trailer infinite loop +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When parsing a PDF, if a trailer refers to itself, or a more complex cyclic loop +exists, then an infinite loop occurs. Pillow now keeps a record of which trailers it +has already processed. PdfParser was added in Pillow 4.2.0. + +Integer overflow when processing fonts +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If a font advances for each glyph by an exceeding large amount, when Pillow keeps track +of the current position, it may lead to an integer overflow. This has been fixed. + +Heap buffer overflow with nested list coordinates +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Passing nested lists as coordinates to APIs that accept coordinates such as +``ImagePath.Path``, :py:meth:`~PIL.ImageDraw.ImageDraw.polygon` +and :py:meth:`~PIL.ImageDraw.ImageDraw.line` could cause a heap buffer overflow, +as nested lists were recursively unpacked beyond the allocated buffer. +Coordinate lists are now validated to contain exactly two numeric coordinates. +This was introduced in Pillow 11.2.1. + +API changes +=========== + +Error when encoding an empty image +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Attempting to encode an image with zero width or height would previously raise +a :py:exc:`SystemError`. That has now been changed to a :py:exc:`ValueError`. + +This does not add any new errors. SGI, ICNS and ICO formats are still able to +save (0, 0) images. + +API additions +============= + +FontFile.to_imagefont() +^^^^^^^^^^^^^^^^^^^^^^^ + +:py:class:`~PIL.FontFile.FontFile` instances can now be directly converted to +:py:class:`~PIL.ImageFont.ImageFont` instances:: + + >>> from PIL import PcfFontFile + >>> with open("Tests/fonts/10x20-ISO8859-1.pcf", "rb") as fp: + ... pcffont = PcfFontFile.PcfFontFile(fp) + ... pcffont.to_imagefont() + ... + + +ImageText.Text.wrap +^^^^^^^^^^^^^^^^^^^ + +:py:meth:`.ImageText.Text.wrap` has been added, to wrap text to fit within a given +width:: + + from PIL import ImageText + text = ImageText.Text("Hello World!") + text.wrap(50) + print(text.text) # "Hello\nWorld!" + +or within a certain width and height, returning a new :py:class:`.ImageText.Text` +instance if the text does not fit:: + + text = ImageText.Text("Text does not fit within height") + print(text.wrap(50, 25).text == " within height") + print(text.text) # "Text does\nnot fit" + +or scaling, optionally with a font size limit:: + + text.wrap(50, 15, "shrink") + text.wrap(50, 15, ("shrink", 7)) + text.wrap(58, 10, "grow") + text.wrap(50, 50, ("grow", 12)) + +EXIF tag FrameRate +^^^^^^^^^^^^^^^^^^ + +The EXIF tag ``FrameRate`` has been added. + +Other changes +============= + +Support reading JPEG2000 images with CMYK palettes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +JPEG2000 images with CMYK palettes can now be read. This is the first integration of +CMYK palettes into Pillow. + +Lazy plugin loading +^^^^^^^^^^^^^^^^^^^ + +When opening or saving an image, Pillow now lazily loads only the required plugin +based on the file extension, instead of importing all plugins upfront. This makes +``open`` 2.3-15.6x faster and ``save`` 2.2-9x faster for common formats. + +Thread safety for free-threaded Python +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Critical sections are now used to protect FreeType font objects, improving thread +safety when using fonts in the free-threaded build of Python. diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst index e9b0995bb9f..a1ddd117873 100644 --- a/docs/releasenotes/2.7.0.rst +++ b/docs/releasenotes/2.7.0.rst @@ -1,13 +1,13 @@ 2.7.0 ----- -Sane Plugin +Sane plugin ^^^^^^^^^^^ The Sane plugin has now been split into its own repo: https://github.com/python-pillow/Sane . -Png text chunk size limits +PNG text chunk size limits ^^^^^^^^^^^^^^^^^^^^^^^^^^ To prevent potential denial of service attacks using compressed text @@ -155,7 +155,7 @@ so the quality was worse compared to other Gaussian blur software. The new implementation does not have this drawback. -TIFF Parameter Changes +TIFF parameter changes ^^^^^^^^^^^^^^^^^^^^^^ Several kwarg parameters for saving TIFF images were previously diff --git a/docs/releasenotes/3.0.0.rst b/docs/releasenotes/3.0.0.rst index 8bc477f7020..dcd8031f588 100644 --- a/docs/releasenotes/3.0.0.rst +++ b/docs/releasenotes/3.0.0.rst @@ -1,7 +1,7 @@ 3.0.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Several methods that have been marked as deprecated for many releases @@ -18,10 +18,10 @@ have been removed in this release: * ``ImageWin.fromstring()`` * ``ImageWin.tostring()`` -Other Changes +Other changes ============= -Saving Multipage Images +Saving multipage images ^^^^^^^^^^^^^^^^^^^^^^^ There is now support for saving multipage images in the ``GIF`` and @@ -30,10 +30,10 @@ as a keyword argument to the save:: im.save('test.pdf', save_all=True) -Tiff ImageFileDirectory Rewrite +TIFF ImageFileDirectory rewrite ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The Tiff ImageFileDirectory metadata code has been rewritten. Where +The TIFF ImageFileDirectory metadata code has been rewritten. Where previously it returned a somewhat arbitrary set of values and tuples, it now returns bare values where appropriate and tuples when the metadata item is a sequence or collection. @@ -41,7 +41,7 @@ metadata item is a sequence or collection. The original metadata is still available in the TiffImage.tags, the new values are available in the TiffImage.tags_v2 member. The old structures will be deprecated at some point in the future. When -saving Tiff metadata, new code should use the +saving TIFF metadata, new code should use the TiffImagePlugin.ImageFileDirectory_v2 class. LibJpeg and Zlib are required by default diff --git a/docs/releasenotes/3.1.0.rst b/docs/releasenotes/3.1.0.rst index 951819f1956..90f77ff6150 100644 --- a/docs/releasenotes/3.1.0.rst +++ b/docs/releasenotes/3.1.0.rst @@ -22,7 +22,7 @@ not the absolute height of each line. There is also now a default spacing of 4px between lines. -Exif, Jpeg and Tiff Metadata +EXIF, JPEG and TIFF metadata ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ There were major changes in the TIFF ImageFileDirectory support in @@ -63,7 +63,7 @@ single item tuples have been unwrapped and return a bare element. The format returned by Pillow 3.0 has been abandoned. A more fully featured interface for EXIF is anticipated in a future release. -Out of Spec Metadata +Out of spec metadata ++++++++++++++++++++ In Pillow 3.0 and 3.1, images that contain metadata that is internally diff --git a/docs/releasenotes/3.2.0.rst b/docs/releasenotes/3.2.0.rst index 3ed8fae574b..20d7d073eee 100644 --- a/docs/releasenotes/3.2.0.rst +++ b/docs/releasenotes/3.2.0.rst @@ -1,7 +1,7 @@ 3.2.0 ----- -New DDS and FTEX Image Plugins +New DDS and FTEX image plugins ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``DdsImagePlugin`` reading DXT1 and DXT5 encoded ``.dds`` images was @@ -18,7 +18,7 @@ Updates to the GbrImagePlugin The ``GbrImagePlugin`` (GIMP brush format) has been updated to fix support for version 1 files and add support for version 2 files. -Passthrough Parameters for ImageDraw.text +Passthrough parameters for ImageDraw.text ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``ImageDraw.multiline_text`` and ``ImageDraw.multiline_size`` take extra diff --git a/docs/releasenotes/3.3.0.rst b/docs/releasenotes/3.3.0.rst index cd6f7e2f93c..9447245c495 100644 --- a/docs/releasenotes/3.3.0.rst +++ b/docs/releasenotes/3.3.0.rst @@ -11,7 +11,7 @@ libimagequant. We cannot distribute binaries due to licensing differences. -New Setup.py options +New setup.py options ^^^^^^^^^^^^^^^^^^^^ There are two new options to control the ``build_ext`` task in ``setup.py``: @@ -43,7 +43,7 @@ This greatly improves both quality and performance in this case. Also, the bug with wrong image size calculation when rotating by 90 degrees was fixed. -Image Metadata +Image metadata ^^^^^^^^^^^^^^ The return type for binary data in version 2 Exif and Tiff metadata diff --git a/docs/releasenotes/3.3.2.rst b/docs/releasenotes/3.3.2.rst index 73156a65dbb..60ffbdcbadb 100644 --- a/docs/releasenotes/3.3.2.rst +++ b/docs/releasenotes/3.3.2.rst @@ -4,7 +4,7 @@ Security ======== -Integer overflow in Map.c +Integer overflow in map.c ^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow prior to 3.3.2 may experience integer overflow errors in map.c @@ -27,7 +27,7 @@ memory without duplicating the image first. This issue was found by Cris Neckar at Divergent Security. -Sign Extension in Storage.c +Sign extension in Storage.c ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow prior to 3.3.2 and PIL 1.1.7 (at least) do not check for diff --git a/docs/releasenotes/3.4.0.rst b/docs/releasenotes/3.4.0.rst index 8a5a7efe350..01ec77a58cc 100644 --- a/docs/releasenotes/3.4.0.rst +++ b/docs/releasenotes/3.4.0.rst @@ -1,7 +1,7 @@ 3.4.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Image.core.open_ppm removed @@ -14,7 +14,7 @@ been removed. If you were using this function, please use Deprecations ============ -Deprecation Warning when Saving JPEGs +Deprecation warning when saving JPEGs ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ JPEG images cannot contain an alpha channel. Pillow prior to 3.4.0 @@ -22,7 +22,7 @@ silently drops the alpha channel. With this release Pillow will now issue a :py:exc:`DeprecationWarning` when attempting to save a ``RGBA`` mode image as a JPEG. This will become an error in Pillow 4.2. -API Additions +API additions ============= New resizing filters @@ -37,7 +37,7 @@ two times shorter window than ``BILINEAR``. It can be used for image reduction providing the image downscaling quality comparable to ``BICUBIC``. Both new filters don't show good quality for the image upscaling. -New DDS Decoders +New DDS decoders ^^^^^^^^^^^^^^^^ Pillow can now decode DXT3 images, as well as the previously supported diff --git a/docs/releasenotes/4.0.0.rst b/docs/releasenotes/4.0.0.rst index 625f237e841..dd97463f6e3 100644 --- a/docs/releasenotes/4.0.0.rst +++ b/docs/releasenotes/4.0.0.rst @@ -1,7 +1,7 @@ 4.0.0 ----- -Python 2.6 and 3.2 Dropped +Python 2.6 and 3.2 dropped ^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow 4.0 no longer supports Python 2.6 and 3.2. We will not be diff --git a/docs/releasenotes/4.1.0.rst b/docs/releasenotes/4.1.0.rst index 80ad9b9fb63..1f809ad18cb 100644 --- a/docs/releasenotes/4.1.0.rst +++ b/docs/releasenotes/4.1.0.rst @@ -15,10 +15,10 @@ Several deprecated items have been removed. ``PIL.ImageDraw.ImageDraw.setfont`` have been removed. -Other Changes +Other changes ============= -Closing Files When Opening Images +Closing files when opening images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The file handling when opening images has been overhauled. Previously, @@ -41,7 +41,7 @@ is specified: the underlying file until we are done with the image. The mapping will be closed in the ``close`` or ``__del__`` method. -Changes to GIF Handling When Saving +Changes to GIF handling when saving ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The :py:class:`PIL.GifImagePlugin` code has been refactored to fix the flow when @@ -57,14 +57,14 @@ saving images. There are two external changes that arise from this: This refactor fixed some bugs with palette handling when saving multiple frame GIFs. -New Method: Image.remap_palette +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. -Added Decoder Registry and Support for Python Based Decoders +Added decoder registry and support for Python-based decoders ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ There is now a decoder registry similar to the image plugin diff --git a/docs/releasenotes/4.1.1.rst b/docs/releasenotes/4.1.1.rst index 8c8055bfad8..1cbd3853b30 100644 --- a/docs/releasenotes/4.1.1.rst +++ b/docs/releasenotes/4.1.1.rst @@ -1,7 +1,7 @@ 4.1.1 ----- -Fix Regression with reading DPI from EXIF data +Fix regression with reading DPI from EXIF data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Some JPEG images don't contain DPI information in the image metadata, diff --git a/docs/releasenotes/4.2.0.rst b/docs/releasenotes/4.2.0.rst index bc2a45f025f..0ea3de39906 100644 --- a/docs/releasenotes/4.2.0.rst +++ b/docs/releasenotes/4.2.0.rst @@ -1,7 +1,7 @@ 4.2.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Several deprecated items have been removed @@ -17,17 +17,17 @@ Several deprecated items have been removed was shown. From Pillow 4.2.0, the deprecation warning is removed and an :py:exc:`IOError` is raised. -Removed Core Image Function +Removed core Image function ^^^^^^^^^^^^^^^^^^^^^^^^^^^ The unused function ``Image.core.new_array`` was removed. This is an internal function that should not have been used by user code, but it was accessible from the python layer. -Other Changes +Other changes ============= -Added Complex Text Rendering +Added complex text rendering ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow now supports complex text rendering for scripts requiring glyph @@ -36,7 +36,7 @@ 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 +New optional parameters ^^^^^^^^^^^^^^^^^^^^^^^ * :py:meth:`PIL.ImageDraw.floodfill` has a new optional parameter: @@ -47,7 +47,7 @@ New Optional Parameters optional parameter for specifying additional images to create multipage outputs. -New DecompressionBomb Warning +New DecompressionBomb warning ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :py:meth:`PIL.Image.Image.crop` now may raise a DecompressionBomb diff --git a/docs/releasenotes/4.2.1.rst b/docs/releasenotes/4.2.1.rst index 2061f646746..617d51e52c2 100644 --- a/docs/releasenotes/4.2.1.rst +++ b/docs/releasenotes/4.2.1.rst @@ -3,7 +3,7 @@ There are no functional changes in this release. -Fixed Windows PyPy Build +Fixed Windows PyPy build ^^^^^^^^^^^^^^^^^^^^^^^^ A change in the 4.2.0 cycle broke the Windows PyPy build. This has diff --git a/docs/releasenotes/4.3.0.rst b/docs/releasenotes/4.3.0.rst index ea81fc45ea0..87a57799f5a 100644 --- a/docs/releasenotes/4.3.0.rst +++ b/docs/releasenotes/4.3.0.rst @@ -1,7 +1,7 @@ 4.3.0 ----- -API Changes +API changes =========== Deprecations @@ -12,7 +12,7 @@ Several undocumented functions in ImageOps have been deprecated: ``box_blur``. Use the equivalent operations in ``ImageFilter`` instead. These functions will be removed in a future release. -TIFF Metadata Changes +TIFF metadata changes ^^^^^^^^^^^^^^^^^^^^^ * TIFF tags with unknown type/quantity now default to being bare @@ -27,7 +27,7 @@ TIFF Metadata Changes items, as there can be multiple items, one for UTF-8, and one for UTF-16. -Core Image API Changes +Core Image API changes ^^^^^^^^^^^^^^^^^^^^^^ These are internal functions that should not have been used by user @@ -44,10 +44,10 @@ The ``PIL.Image.core.getcount`` methods have been removed, use ``PIL.Image.core.get_stats()['new_count']`` property instead. -API Additions +API additions ============= -Get One Channel From Image +Get one channel from image ^^^^^^^^^^^^^^^^^^^^^^^^^^ A new method :py:meth:`PIL.Image.Image.getchannel` has been added to @@ -56,14 +56,14 @@ return a single channel by index or name. For example, ``getchannel`` should work up to 6 times faster than ``image.split()[0]`` in previous Pillow versions. -Box Blur +Box blur ^^^^^^^^ A new filter, :py:class:`PIL.ImageFilter.BoxBlur`, has been added. This is a filter with similar results to a Gaussian blur, but is much faster. -Partial Resampling +Partial resampling ^^^^^^^^^^^^^^^^^^ Added new argument ``box`` for :py:meth:`PIL.Image.Image.resize`. This @@ -71,14 +71,14 @@ argument defines a source rectangle from within the source image to be resized. This is very similar to the ``image.crop(box).resize(size)`` sequence except that ``box`` can be specified with subpixel accuracy. -New Transpose Operation +New transpose operation ^^^^^^^^^^^^^^^^^^^^^^^ The ``Image.TRANSVERSE`` operation has been added to :py:meth:`PIL.Image.Image.transpose`. This is equivalent to a transpose operation about the opposite diagonal. -Multiband Filters +Multiband filters ^^^^^^^^^^^^^^^^^ There is a new :py:class:`PIL.ImageFilter.MultibandFilter` base class @@ -87,10 +87,10 @@ 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. -Other Changes +Other changes ============= -Loading 16-bit TIFF Images +Loading 16-bit TIFF images ^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow now can read 16-bit multichannel TIFF files including files @@ -101,7 +101,7 @@ Pillow now can read 16-bit signed integer single channel TIFF files. The image data is promoted to 32-bit for storage and processing. -SGI Images +SGI images ^^^^^^^^^^ Pillow can now read and write uncompressed 16-bit multichannel SGI @@ -129,7 +129,7 @@ This release contains several performance improvements: falling back to an allocation for each scan line for images larger than the block size. -CMYK Conversion +CMYK conversion ^^^^^^^^^^^^^^^ The basic CMYK->RGB conversion has been tweaked to match the formula diff --git a/docs/releasenotes/5.0.0.rst b/docs/releasenotes/5.0.0.rst index be00a45cd87..2b93e032287 100644 --- a/docs/releasenotes/5.0.0.rst +++ b/docs/releasenotes/5.0.0.rst @@ -1,10 +1,10 @@ 5.0.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== -Python 3.3 Dropped +Python 3.3 dropped ^^^^^^^^^^^^^^^^^^ Python 3.3 is EOL and no longer supported due to moving testing from nose, @@ -12,7 +12,7 @@ which is deprecated, to pytest, which doesn't support Python 3.3. We will not be creating binaries, testing, or retaining compatibility with this version. The final version of Pillow for Python 3.3 is 4.3.0. -Decompression Bombs now raise Exceptions +Decompression bombs now raise exceptions ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow has previously emitted warnings for images that are @@ -31,7 +31,7 @@ separate package, pillow-scripts, living at https://github.com/python-pillow/pillow-scripts. -API Changes +API changes =========== OleFileIO.py @@ -54,7 +54,7 @@ Several image plugins supported a named ``check`` parameter on their nominally private ``_save`` method to preflight if the image could be saved in that format. That parameter has been removed. -API Additions +API additions ============= Image.transform @@ -65,16 +65,16 @@ A new named parameter, ``fillcolor``, has been added to the area outside the transformed area in the output image. This parameter takes the same color specifications as used in ``Image.new``. -GIF Disposal +GIF disposal ^^^^^^^^^^^^ Multiframe GIF images now take an optional disposal parameter to specify the disposal option for changed pixels. -Other Changes +Other changes ============= -Compressed TIFF Images +Compressed TIFF images ^^^^^^^^^^^^^^^^^^^^^^ Previously, there were some compression modes (JPEG, Packbits, and @@ -82,7 +82,7 @@ LZW) that were supported with Pillow's internal TIFF decoder. All compressed TIFFs are now read using the ``libtiff`` decoder, as it implements the compression schemes more correctly. -Libraqm is now Dynamically Linked +Libraqm is now dynamically linked ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The libraqm dependency for complex text scripts is now linked @@ -90,14 +90,14 @@ 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. -Source Layout Changes +Source layout changes ^^^^^^^^^^^^^^^^^^^^^ The Pillow source is now stored within the ``src`` directory of the distribution. This prevents accidental imports of the PIL directory when running Python from the project directory. -Setup.py Changes +Setup.py changes ^^^^^^^^^^^^^^^^ Multiarch support on Linux should be more robust, especially on Debian diff --git a/docs/releasenotes/5.1.0.rst b/docs/releasenotes/5.1.0.rst index 4e3d10ac596..4b80e8521cf 100644 --- a/docs/releasenotes/5.1.0.rst +++ b/docs/releasenotes/5.1.0.rst @@ -1,7 +1,7 @@ 5.1.0 ----- -API Changes +API changes =========== Optional channels for TIFF files @@ -12,22 +12,22 @@ 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. -API Additions +API additions ============= -Append to PDF Files +Append to PDF files ^^^^^^^^^^^^^^^^^^^ Images can now be appended to PDF files in place by passing in ``append=True`` when saving the image. -New BLP File Format +New BLP file format ^^^^^^^^^^^^^^^^^^^ Pillow now supports reading the BLP "Blizzard Mipmap" file format used for tiles in Blizzard's engine. -Other Changes +Other changes ============= WebP memory leak diff --git a/docs/releasenotes/5.2.0.rst b/docs/releasenotes/5.2.0.rst index d9b8f0fb7c8..d183378201b 100644 --- a/docs/releasenotes/5.2.0.rst +++ b/docs/releasenotes/5.2.0.rst @@ -1,7 +1,7 @@ 5.2.0 ----- -API Changes +API changes =========== Deprecations @@ -17,7 +17,7 @@ Pillow 6.0.0, and ``PILLOW_VERSION`` will be removed after that. Use ``PIL.__version__`` instead. -API Additions +API additions ============= 3D color lookup tables @@ -75,7 +75,7 @@ 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 +Other changes ============= Support added for Python 3.7 diff --git a/docs/releasenotes/5.3.0.rst b/docs/releasenotes/5.3.0.rst index 8f276da2407..6adce95b210 100644 --- a/docs/releasenotes/5.3.0.rst +++ b/docs/releasenotes/5.3.0.rst @@ -1,7 +1,7 @@ 5.3.0 ----- -API Changes +API changes =========== Image size @@ -20,7 +20,7 @@ The exceptions to this are: as direct image size setting was previously necessary to work around an issue with tile extents. -API Additions +API additions ============= Added line width parameter to rectangle and ellipse-based shapes @@ -59,7 +59,7 @@ 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 +Other changes ============= Added support for reading tiled TIFF images through LibTIFF. Compressed TIFF diff --git a/docs/releasenotes/5.4.0.rst b/docs/releasenotes/5.4.0.rst index 6d7277c70ea..13b540d600b 100644 --- a/docs/releasenotes/5.4.0.rst +++ b/docs/releasenotes/5.4.0.rst @@ -1,7 +1,7 @@ 5.4.0 ----- -API Changes +API changes =========== APNG extension to PNG plugin @@ -55,7 +55,7 @@ TIFF images can now be saved with custom integer, float and string TIFF tags:: print(im2.tag_v2[37002]) # "custom tag value" print(im2.tag_v2[37004]) # b"custom tag value" -Other Changes +Other changes ============= ImageOps.fit diff --git a/docs/releasenotes/6.0.0.rst b/docs/releasenotes/6.0.0.rst index 5e69f0b6b5a..b788b2eeb71 100644 --- a/docs/releasenotes/6.0.0.rst +++ b/docs/releasenotes/6.0.0.rst @@ -1,7 +1,7 @@ 6.0.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Python 3.4 dropped @@ -32,7 +32,7 @@ Removed deprecated VERSION ``VERSION`` (the old PIL version, always 1.1.7) has been removed. Use ``__version__`` instead. -API Changes +API changes =========== Deprecations @@ -137,7 +137,7 @@ loaded, ``Image.MIME["PPM"]`` will now return the generic "image/x-portable-anym The TGA, PCX and ICO formats also now have MIME types: "image/x-tga", "image/x-pcx" and "image/x-icon" respectively. -API Additions +API additions ============= DIB file format @@ -186,7 +186,7 @@ EXIF data can now be read from and saved to PNG images. However, unlike other im 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 +Other changes ============= Reading new DDS image format diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst index ce3edc5fa9b..761f435f391 100644 --- a/docs/releasenotes/6.1.0.rst +++ b/docs/releasenotes/6.1.0.rst @@ -23,7 +23,7 @@ Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") -API Additions +API additions ============= Image.entropy @@ -61,7 +61,7 @@ file. ``ImageFont.FreeTypeFont`` has four new methods, instead. An :py:exc:`IOError` will be raised if the font is not a variation font. FreeType 2.9.1 or greater is required. -Other Changes +Other changes ============= ImageTk.getimage diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index b851c56fc0e..b37cd7160e7 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -29,7 +29,7 @@ perform operations on it. The CVE is regarding DOS problems, such as consuming large amounts of memory, or taking a large amount of time to process an image. -API Changes +API changes =========== Image.getexif @@ -48,7 +48,7 @@ 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. -API Additions +API additions ============= Text stroking @@ -93,7 +93,7 @@ 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. -Other Changes +Other changes ============= Removed bdist_wininst .exe installers diff --git a/docs/releasenotes/6.2.1.rst b/docs/releasenotes/6.2.1.rst index 372298fbc2a..0ede05917a4 100644 --- a/docs/releasenotes/6.2.1.rst +++ b/docs/releasenotes/6.2.1.rst @@ -1,7 +1,7 @@ 6.2.1 ----- -API Changes +API changes =========== Deprecations @@ -15,7 +15,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 +Other changes ============= Support added for Python 3.8 diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index ed6026593e6..9504c974a11 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -1,7 +1,7 @@ 7.0.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Python 2.7 @@ -78,7 +78,7 @@ bounds of resulting image. This may be useful in a subsequent .. _chain methods: https://en.wikipedia.org/wiki/Method_chaining -API Additions +API additions ============= Custom unidentified image error @@ -124,7 +124,7 @@ now also be loaded at another resolution:: with Image.open("drawing.wmf") as im: im.load(dpi=144) -Other Changes +Other changes ============= Image.__del__ diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst index 0dd8669a5b8..c2aeb0f749b 100644 --- a/docs/releasenotes/7.1.0.rst +++ b/docs/releasenotes/7.1.0.rst @@ -35,7 +35,7 @@ out-of-bounds reads via a crafted JP2 file. In ``libImaging/SgiRleDecode.c`` in Pillow through 7.0.0, a number of out-of-bounds reads exist in the parsing of SGI image files, a different issue than :cve:`2020-5311`. -API Changes +API changes =========== Allow saving of zero quality JPEG images @@ -50,7 +50,7 @@ been resolved. :: im = Image.open("hopper.jpg") im.save("out.jpg", quality=0) -API Additions +API additions ============= New channel operations @@ -101,7 +101,7 @@ 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. -Other Changes +Other changes ============= If present, only use alpha channel for bounding box diff --git a/docs/releasenotes/7.2.0.rst b/docs/releasenotes/7.2.0.rst index 91e54da1999..12bafa8ce90 100644 --- a/docs/releasenotes/7.2.0.rst +++ b/docs/releasenotes/7.2.0.rst @@ -1,7 +1,7 @@ 7.2.0 ----- -API Changes +API changes =========== Replaced TiffImagePlugin DEBUG with logging diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst index 1fc245c9a3c..d0dde756fc3 100644 --- a/docs/releasenotes/8.0.0.rst +++ b/docs/releasenotes/8.0.0.rst @@ -1,7 +1,7 @@ 8.0.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Python 3.5 @@ -44,7 +44,7 @@ Removed Use instead ``product_model`` Unicode :py:attr:`~.CmsProfile.model` ======================== =================================================== -API Changes +API changes =========== ImageDraw.text: stroke_width @@ -67,7 +67,7 @@ Add MIME type to PsdImagePlugin "image/vnd.adobe.photoshop" is now registered as the :py:class:`.PsdImagePlugin.PsdImageFile` MIME type. -API Additions +API additions ============= Image.open: add formats parameter @@ -135,7 +135,7 @@ 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 +Other changes ============= Improved ellipse-drawing algorithm diff --git a/docs/releasenotes/8.1.0.rst b/docs/releasenotes/8.1.0.rst index 5c399331846..06e6d997484 100644 --- a/docs/releasenotes/8.1.0.rst +++ b/docs/releasenotes/8.1.0.rst @@ -26,7 +26,7 @@ leading to an out-of-bounds write in ``TiffDecode.c``. This potentially affects versions from 6.0.0 to 8.0.1, depending on the version of LibTIFF. This was reported through `Tidelift`_. -:cve:`2020-35655`: SGI Decode buffer overrun +:cve:`2020-35655`: SGI decode buffer overrun ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 byte read overflow in ``SgiRleDecode.c``, where the code was not correctly @@ -64,7 +64,7 @@ Makefile The ``install-venv`` target has been deprecated. -API Additions +API additions ============= Append images to ICO @@ -77,7 +77,7 @@ With this release, a list of images can be provided to the ``append_images`` par when saving, to replace the scaled down versions. This is the same functionality that already exists for the ICNS format. -Other Changes +Other changes ============= Makefile diff --git a/docs/releasenotes/8.1.1.rst b/docs/releasenotes/8.1.1.rst index 690421c2a56..b8ad5a898a0 100644 --- a/docs/releasenotes/8.1.1.rst +++ b/docs/releasenotes/8.1.1.rst @@ -32,7 +32,7 @@ DOS attack. There is an out-of-bounds read in ``SgiRleDecode.c`` since Pillow 4.3.0. -Other Changes +Other changes ============= A crash with the feature flags for libimagequant, libjpeg-turbo, WebP and XCB on diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index 50fe9aa1988..a59560695eb 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -74,7 +74,7 @@ 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 +API changes =========== Image.alpha_composite: dest @@ -107,7 +107,7 @@ removed. Instead, ``Image.getmodebase()``, ``Image.getmodetype()``, ``Image.getmodebandnames()``, ``Image.getmodebands()`` or ``ImageMode.getmode()`` can be used. -API Additions +API additions ============= getxmp() for JPEG images @@ -177,7 +177,7 @@ be specified through a keyword argument:: im.save("out.tif", icc_profile=...) -Other Changes +Other changes ============= GIF writer uses LZW encoding diff --git a/docs/releasenotes/8.3.0.rst b/docs/releasenotes/8.3.0.rst index 4ef914f6471..c4624085400 100644 --- a/docs/releasenotes/8.3.0.rst +++ b/docs/releasenotes/8.3.0.rst @@ -33,7 +33,7 @@ dictionary. The ``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 +API changes =========== Changed WebP default "method" value when saving @@ -73,7 +73,7 @@ through :py:meth:`~PIL.Image.Image.getexif`. This also provides access to the GP EXIF IFDs, through ``im.getexif().get_ifd(0x8825)`` and ``im.getexif().get_ifd(0x8769)`` respectively. -API Additions +API additions ============= ImageOps.contain @@ -100,7 +100,7 @@ format, through the new ``bitmap_format`` argument:: im.save("out.ico", bitmap_format="bmp") -Other Changes +Other changes ============= Added DDS BC5 reading and uncompressed saving diff --git a/docs/releasenotes/8.3.2.rst b/docs/releasenotes/8.3.2.rst index 34ba703f70a..e26a6ceda4c 100644 --- a/docs/releasenotes/8.3.2.rst +++ b/docs/releasenotes/8.3.2.rst @@ -20,7 +20,7 @@ bytes off the end of the allocated buffer from the heap. Present since Pillow 7. This bug was found by Google's `OSS-Fuzz`_ `CIFuzz`_ runs. -Other Changes +Other changes ============= Python 3.10 wheels diff --git a/docs/releasenotes/8.4.0.rst b/docs/releasenotes/8.4.0.rst index bdc8e802082..3bdf77d564d 100644 --- a/docs/releasenotes/8.4.0.rst +++ b/docs/releasenotes/8.4.0.rst @@ -13,7 +13,7 @@ Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular length 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 +API additions ============= Added "transparency" argument for loading EPS images @@ -33,7 +33,7 @@ Added WalImageFile class :py:class:`PIL.Image.Image` instance. It now returns a dedicated :py:class:`PIL.WalImageFile.WalImageFile` class. -Other Changes +Other changes ============= Speed improvement when rotating square images diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst index fee66b6d0b5..660e5514cf8 100644 --- a/docs/releasenotes/9.0.0.rst +++ b/docs/releasenotes/9.0.0.rst @@ -59,7 +59,7 @@ initializing ``ImagePath.Path``. .. _OSS-Fuzz: https://github.com/google/oss-fuzz -Backwards Incompatible Changes +Backwards incompatible changes ============================== Python 3.6 @@ -102,7 +102,7 @@ ImageFile.raise_ioerror has been removed. Use ``ImageFile.raise_oserror`` instead. -API Changes +API changes =========== Added line width parameter to ImageDraw polygon @@ -111,7 +111,7 @@ Added line width parameter to ImageDraw polygon An optional line ``width`` parameter has been added to ``ImageDraw.Draw.polygon``. -API Additions +API additions ============= ImageShow.XDGViewer @@ -132,7 +132,7 @@ Support has been added for the "title" argument in argument will also now be supported, e.g. ``im.show(title="My Image")`` and ``ImageShow.show(im, title="My Image")``. -Other Changes +Other changes ============= Convert subsequent GIF frames to RGB or RGBA diff --git a/docs/releasenotes/9.0.1.rst b/docs/releasenotes/9.0.1.rst index f65e3bcc2ec..5326afe782c 100644 --- a/docs/releasenotes/9.0.1.rst +++ b/docs/releasenotes/9.0.1.rst @@ -21,7 +21,7 @@ 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 +Other changes ============= Pillow 9.0 added support for ``xdg-open`` as an image viewer, but there have been diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index 5b83d1e9c56..72749ce8c54 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -94,7 +94,7 @@ The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be re Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead. -API Changes +API changes =========== Raise an error when performing a negative crop @@ -137,7 +137,7 @@ On macOS, the last argument may need to be wrapped in quotes, e.g. Therefore ``requirements.txt`` has been removed along with the ``make install-req`` command for installing its contents. -API Additions +API additions ============= Added get_photoshop_blocks() to parse Photoshop TIFF tag @@ -193,7 +193,7 @@ palette. :: from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY -Other Changes +Other changes ============= musllinux wheels diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 6e064734391..a3c9800b683 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -126,7 +126,7 @@ Use instead:: draw = ImageDraw.Draw(im) draw.text((100 / 2, 100 / 2), "Hello world", font=font, anchor="mm") -API Additions +API additions ============= Image.apply_transparency @@ -137,7 +137,7 @@ with "transparency" in ``im.info``, and apply the transparency to the palette in The image's palette mode will become "RGBA", and "transparency" will be removed from ``im.info``. -Other Changes +Other changes ============= Using gnome-screenshot on Linux diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index e5987ce086c..bb1e731fd75 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -28,7 +28,7 @@ This was introduced in Pillow 9.2.0, found with `OSS-Fuzz`_ and fixed by limitin ``SAMPLESPERPIXEL`` to the number of planes that we can decode. -API Additions +API additions ============= Allow default ImageDraw font to be set @@ -65,7 +65,7 @@ The data from :py:data:`~PIL.ExifTags.TAGS` and :py:data:`~PIL.ExifTags.GPS`. -Other Changes +Other changes ============= Python 3.11 wheels diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 37f26a22c05..3b202157dc0 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -20,7 +20,7 @@ Pillow attempted to dereference a null pointer in ``ImageFont``, leading to a crash. An error is now raised instead. This has been present since Pillow 8.0.0. -API Additions +API additions ============= Added start position for getmask and getmask2 @@ -88,7 +88,7 @@ When saving a JPEG image, a comment can now be written from im.save(out, comment="Test comment") -Other Changes +Other changes ============= Added support for DDS L and LA images diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index 08e9ec2a473..6bf2079c812 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -32,12 +32,12 @@ Deprecations PSFile ^^^^^^ -The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will +The :py:class:`!PSFile` class has been deprecated and will be removed in Pillow 11 (2024-10-15). This class was only made as a helper to be used internally, so there is no replacement. If you need this functionality though, it is a very short class that can easily be recreated in your own code. -API Additions +API additions ============= QOI file format @@ -71,7 +71,7 @@ If OpenJPEG 2.4.0 or later is available and the ``plt`` keyword argument is present and true when saving JPEG2000 images, tell the encoder to generate PLT markers. -Other Changes +Other changes ============= Added support for saving PDFs in RGBA mode diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 6ee5fb6c8f4..07687297933 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -1,4 +1,4 @@ -Release Notes +Release notes ============= Pillow is released quarterly on January 2nd, April 1st, July 1st and October 15th. @@ -14,6 +14,15 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + versioning + 12.2.0 + 12.1.1 + 12.1.0 + 12.0.0 + 11.3.0 + 11.2.1 + 11.1.0 + 11.0.0 10.4.0 10.3.0 10.2.0 @@ -74,4 +83,3 @@ expected to be backported to earlier versions. 2.5.2 2.3.2 2.3.1 - versioning diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst index cfc7221a3cc..b603a9938c5 100644 --- a/docs/releasenotes/template.rst +++ b/docs/releasenotes/template.rst @@ -14,12 +14,14 @@ TODO TODO -Backwards Incompatible Changes +Backwards incompatible changes ============================== TODO ^^^^ +TODO + Deprecations ============ @@ -28,7 +30,7 @@ TODO TODO -API Changes +API changes =========== TODO @@ -36,7 +38,7 @@ TODO TODO -API Additions +API additions ============= TODO @@ -44,7 +46,7 @@ TODO TODO -Other Changes +Other changes ============= TODO diff --git a/docs/releasenotes/versioning.rst b/docs/releasenotes/versioning.rst index 2a0af9e59ec..884102d16f7 100644 --- a/docs/releasenotes/versioning.rst +++ b/docs/releasenotes/versioning.rst @@ -17,8 +17,8 @@ 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 +these occur every October, 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 `_" diff --git a/pyproject.toml b/pyproject.toml index 20e87ad327d..65e3b76591e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,8 @@ [build-system] build-backend = "backend" requires = [ - "setuptools>=67.8", + "pybind11", + "setuptools>=77", ] backend-path = [ "_custom_build", @@ -9,23 +10,25 @@ backend-path = [ [project] name = "pillow" -description = "Python Imaging Library (Fork)" +description = "Python Imaging Library (fork)" readme = "README.md" keywords = [ "Imaging", ] -license = {text = "HPND"} -authors = [{name = "Jeffrey A. Clark", email = "aclark@aclark.net"}] -requires-python = ">=3.8" +license = "MIT-CMU" +license-files = [ "LICENSE" ] +authors = [ + { name = "Jeffrey 'Alex' Clark", email = "aclark@aclark.net" }, +] +requires-python = ">=3.10" classifiers = [ "Development Status :: 6 - Mature", - "License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Multimedia :: Graphics", @@ -38,55 +41,59 @@ classifiers = [ dynamic = [ "version", ] -[project.optional-dependencies] -docs = [ +optional-dependencies.docs = [ "furo", "olefile", - "sphinx>=7.3", + "sphinx>=8.2", + "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph", ] -fpx = [ +optional-dependencies.fpx = [ "olefile", ] -mic = [ +optional-dependencies.mic = [ "olefile", ] -tests = [ +optional-dependencies.test-arrow = [ + "arro3-compute", + "arro3-core", + "nanoarrow", + "pyarrow", +] +optional-dependencies.tests = [ "check-manifest", - "coverage", + "coverage>=7.4.2", "defusedxml", "markdown2", "olefile", "packaging", - "pyroma", + "pyroma>=5", "pytest", "pytest-cov", "pytest-timeout", + "pytest-xdist", + "trove-classifiers>=2024.10.12", ] -typing = [ - 'typing-extensions; python_version < "3.10"', -] -xmp = [ +optional-dependencies.xmp = [ "defusedxml", ] -[project.urls] -Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst" -Documentation = "https://pillow.readthedocs.io" -Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi" -Homepage = "https://python-pillow.org" -Mastodon = "https://fosstodon.org/@pillow" -"Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" -Source = "https://github.com/python-pillow/Pillow" +urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" +urls.Changelog = "https://github.com/python-pillow/Pillow/releases" +urls.Documentation = "https://pillow.readthedocs.io" +urls.Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi" +urls.Homepage = "https://python-pillow.github.io" +urls.Mastodon = "https://fosstodon.org/@pillow" +urls.Source = "https://github.com/python-pillow/Pillow" [tool.setuptools] -packages = ["PIL"] +packages = [ + "PIL", +] include-package-data = true -package-dir = {"" = "src"} - -[tool.setuptools.dynamic] -version = {attr = "PIL.__version__"} +package-dir = { "" = "src" } +dynamic.version = { attr = "PIL.__version__" } [tool.cibuildwheel] before-all = ".github/workflows/wheels-dependencies.sh" @@ -94,51 +101,102 @@ build-verbosity = 1 config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" +test-requires = [ + "numpy", +] +xbuild-tools = [] +# Disable platform guessing on iOS, and disable raqm (since there won't be a +# vendor version, and we can't distribute it due to licensing) +ios.config-settings = "raqm=disable imagequant=disable platform-guessing=disable" +# iOS needs to be given a specific pytest invocation and list of test sources. +ios.test-sources = [ + "checks", + "Tests", + "selftest.py", +] +ios.test-command = [ + "python -m selftest", + "python -m pytest -vv -x -W always checks/check_wheel.py Tests", +] +# There's no numpy wheel for iOS (yet...) +ios.test-requires = [] +# Disable platform guessing on macOS to avoid picking up Homebrew etc. +macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" +# Isolate macOS build environment from Homebrew etc. +macos.environment.PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +overrides = [ + # iOS environment is isolated by cibuildwheel, but needs the dependencies + { select = "*_iphoneos", environment.PATH = "$(pwd)/build/deps/iphoneos/bin:$PATH" }, + # iOS simulator environment is isolated by cibuildwheel, but needs the dependencies + { select = "*_iphonesimulator", environment.PATH = "$(pwd)/build/deps/iphonesimulator/bin:$PATH" }, + { select = "*-win32", test-requires = [] }, +] + +[tool.black] +exclude = "wheels/multibuild" [tool.ruff] +exclude = [ "wheels/multibuild" ] fix = true - -[tool.ruff.lint] -select = [ - "C4", # flake8-comprehensions - "E", # pycodestyle errors - "EM", # flake8-errmsg - "F", # pyflakes errors - "I", # isort - "ISC", # flake8-implicit-str-concat - "LOG", # flake8-logging - "PGH", # pygrep-hooks - "PYI", # flake8-pyi +lint.select = [ + "C4", # flake8-comprehensions + "E", # pycodestyle errors + "EM", # flake8-errmsg + "F", # pyflakes errors + "I", # isort + "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging + "PERF", # perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PT", # flake8-pytest-style + "PYI", # flake8-pyi "RUF100", # unused noqa (yesqa) - "UP", # pyupgrade - "W", # pycodestyle warnings - "YTT", # flake8-2020 -] -ignore = [ - "E203", # Whitespace before ':' - "E221", # Multiple spaces before operator - "E226", # Missing whitespace around arithmetic operator - "E241", # Multiple spaces after ',' - "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 + "UP", # pyupgrade + "W", # pycodestyle warnings + "YTT", # flake8-2020 +] +lint.ignore = [ + "E203", # Whitespace before ':' + "E221", # Multiple spaces before operator + "E226", # Missing whitespace around arithmetic operator + "E241", # Multiple spaces after ',' + "PIE790", # flake8-pie: unnecessary-placeholder + "PT001", # pytest-fixture-incorrect-parentheses-style + "PT007", # pytest-parametrize-values-wrong-type + "PT011", # pytest-raises-too-broad + "PT012", # pytest-raises-with-multiple-statements + "PT017", # pytest-assert-in-except "PYI034", # flake8-pyi: typing.Self added in Python 3.11 ] +lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [ + "I002", +] +lint.per-file-ignores."Tests/oss-fuzz/fuzz_pillow.py" = [ + "I002", +] +lint.flake8-pytest-style.parametrize-names-type = "csv" +lint.isort.known-first-party = [ + "PIL", +] +lint.isort.required-imports = [ + "from __future__ import annotations", +] -[tool.ruff.lint.per-file-ignores] -"Tests/oss-fuzz/fuzz_font.py" = ["I002"] -"Tests/oss-fuzz/fuzz_pillow.py" = ["I002"] - -[tool.ruff.lint.isort] -known-first-party = ["PIL"] -required-imports = ["from __future__ import annotations"] +[tool.pyproject-fmt] +max_supported_python = "3.14" -[tool.pytest.ini_options] -addopts = "-ra --color=yes" -testpaths = ["Tests"] +[tool.pytest] +addopts = [ "-ra", "--color=auto" ] +testpaths = [ + "Tests", +] [tool.mypy] -python_version = "3.8" +python_version = "3.10" pretty = true disallow_any_generics = true +disallow_untyped_defs = true enable_error_code = "ignore-without-code" extra_checks = true follow_imports = "silent" diff --git a/selftest.py b/selftest.py index 9e049367edc..64898fc920d 100755 --- a/selftest.py +++ b/selftest.py @@ -40,19 +40,21 @@ def testimage() -> None: >>> with Image.open("Tests/images/hopper.gif") as im: ... _info(im) ('GIF', 'P', (128, 128)) - >>> _info(Image.open("Tests/images/hopper.ppm")) + >>> with Image.open("Tests/images/hopper.ppm") as im: + ... _info(im) ('PPM', 'RGB', (128, 128)) >>> try: - ... _info(Image.open("Tests/images/hopper.jpg")) + ... with Image.open("Tests/images/hopper.jpg") as im: + ... _info(im) ... except OSError as v: - ... print(v) + ... print(v) ('JPEG', 'RGB', (128, 128)) PIL doesn't actually load the image data until it's needed, or you call the "load" method: >>> im = Image.open("Tests/images/hopper.ppm") - >>> print(im.im) # internal image attribute + >>> print(im._im) # internal image attribute None >>> a = im.load() >>> type(im.im) # doctest: +ELLIPSIS @@ -74,7 +76,7 @@ def testimage() -> None: ('R', 'G', 'B') >>> im.getbbox() (0, 0, 128, 128) - >>> len(im.getdata()) + >>> len(im.get_flattened_data()) 16384 >>> im.getextrema() ((0, 255), (0, 255), (0, 255)) diff --git a/setup.py b/setup.py index 0abfaaddc5b..496c8cb1f0e 100644 --- a/setup.py +++ b/setup.py @@ -15,21 +15,35 @@ import subprocess import sys import warnings +from collections.abc import Iterator +from pybind11.setup_helpers import ParallelCompile from setuptools import Extension, setup from setuptools.command.build_ext import build_ext +TYPE_CHECKING = False +if TYPE_CHECKING: + from setuptools import _BuildInfo -def get_version(): +configuration: dict[str, list[str]] = {} + +# parse configuration from _custom_build/backend.py +while sys.argv[-1].startswith("--pillow-configuration="): + _, key, value = sys.argv.pop().split("=", 2) + configuration.setdefault(key, []).append(value) + +default = int(configuration.get("parallel", ["0"])[-1]) +ParallelCompile("MAX_CONCURRENCY", default).install() + + +def get_version() -> str: version_file = "src/PIL/_version.py" with open(version_file, encoding="utf-8") as f: return f.read().split('"')[1] -configuration = {} - - PILLOW_VERSION = get_version() +AVIF_ROOT = None FREETYPE_ROOT = None HARFBUZZ_ROOT = None FRIBIDI_ROOT = None @@ -43,7 +57,7 @@ def get_version(): ZLIB_ROOT = None FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ -if sys.platform == "win32" and sys.version_info >= (3, 13): +if sys.platform == "win32" and sys.version_info >= (3, 15): import atexit atexit.register( @@ -62,10 +76,12 @@ def get_version(): _LIB_IMAGING = ( "Access", "AlphaComposite", + "Arrow", "Resample", "Reduce", "Bands", "BcnDecode", + "BcnEncode", "BitDecode", "Blend", "Chops", @@ -143,7 +159,7 @@ class RequiredDependencyException(Exception): PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version -def _dbg(s, tp=None): +def _dbg(s: str, tp: str | tuple[str, ...] | None = None) -> None: if DEBUG: if tp: print(s % tp) @@ -151,11 +167,14 @@ def _dbg(s, tp=None): print(s) -def _find_library_dirs_ldconfig(): +def _find_library_dirs_ldconfig() -> list[str]: # Based on ctypes.util from Python 2 ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig" - if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): + args: list[str] + env: dict[str, str] + expr: str + if sys.platform.startswith(("linux", "gnu")): if struct.calcsize("l") == 4: machine = os.uname()[4] + "-32" else: @@ -184,13 +203,11 @@ def _find_library_dirs_ldconfig(): try: p = subprocess.Popen( - args, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, env=env + args, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, env=env, text=True ) except OSError: # E.g. command not found return [] - [data, _] = p.communicate() - if isinstance(data, bytes): - data = data.decode("latin1") + data = p.communicate()[0] dirs = [] for dll in re.findall(expr, data): @@ -200,7 +217,9 @@ def _find_library_dirs_ldconfig(): return dirs -def _add_directory(path, subdir, where=None): +def _add_directory( + path: list[str], subdir: str | None, where: int | None = None +) -> None: if subdir is None: return subdir = os.path.realpath(subdir) @@ -216,16 +235,17 @@ def _add_directory(path, subdir, where=None): path.insert(where, subdir) -def _find_include_file(self, include): +def _find_include_file(self: pil_build_ext, include: str) -> str | None: 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)): + path = os.path.join(directory, include) + if os.path.isfile(path): _dbg("Found %s", include) - return 1 - return 0 + return path + return None -def _find_library_file(self, library): +def _find_library_file(self: pil_build_ext, library: str) -> str | None: ret = self.compiler.find_library_file(self.compiler.library_dirs, library) if ret: _dbg("Found library %s at %s", (library, ret)) @@ -234,7 +254,7 @@ def _find_library_file(self, library): return ret -def _find_include_dir(self, dirname, include): +def _find_include_dir(self: pil_build_ext, dirname: str, include: str) -> bool | str: 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)): @@ -245,6 +265,7 @@ def _find_include_dir(self, dirname, include): if os.path.isfile(os.path.join(subdir, include)): _dbg("Found %s in %s", (include, subdir)) return subdir + return False def _cmd_exists(cmd: str) -> bool: @@ -256,7 +277,7 @@ def _cmd_exists(cmd: str) -> bool: ) -def _pkg_config(name): +def _pkg_config(name: str) -> tuple[list[str], list[str]] | None: command = os.environ.get("PKG_CONFIG", "pkg-config") for keep_system in (True, False): try: @@ -281,12 +302,13 @@ def _pkg_config(name): subprocess.check_output(command_cflags).decode("utf8").strip(), )[::2][1:] return libs, cflags - except Exception: + except Exception: # noqa: PERF203 pass + return None class pil_build_ext(build_ext): - class feature: + class ext_feature: features = [ "zlib", "jpeg", @@ -295,32 +317,39 @@ class feature: "raqm", "lcms", "webp", - "webpmux", "jpeg2000", "imagequant", "xcb", + "avif", ] required = {"jpeg", "zlib"} - vendor = set() + vendor: set[str] = set() - def __init__(self): + def __init__(self) -> None: + self._settings: dict[str, str | bool | None] = {} for f in self.features: - setattr(self, f, None) + self.set(f, None) - def require(self, feat): + def require(self, feat: str) -> bool: return feat in self.required - def want(self, feat): - return getattr(self, feat) is None + def get(self, feat: str) -> str | bool | None: + return self._settings[feat] + + def set(self, feat: str, value: str | bool | None) -> None: + self._settings[feat] = value + + def want(self, feat: str) -> bool: + return self._settings[feat] is None - def want_vendor(self, feat): + def want_vendor(self, feat: str) -> bool: return feat in self.vendor - def __iter__(self): + def __iter__(self) -> Iterator[str]: yield from self.features - feature = feature() + feature = ext_feature() user_options = ( build_ext.user_options @@ -331,21 +360,19 @@ def __iter__(self): for x in ("raqm", "fribidi") ] + [ - ("disable-platform-guessing", None, "Disable platform guessing on Linux"), + ("disable-platform-guessing", None, "Disable platform guessing"), ("debug", None, "Debug logging"), ] - + [("add-imaging-libs=", None, "Add libs to _imaging build")] ) @staticmethod - def check_configuration(option, value): + def check_configuration(option: str, value: str) -> bool | None: return True if value in configuration.get(option, []) else None - def initialize_options(self): + def initialize_options(self) -> None: self.disable_platform_guessing = self.check_configuration( "platform-guessing", "disable" ) - self.add_imaging_libs = "" build_ext.initialize_options(self) for x in self.feature: setattr(self, f"disable_{x}", self.check_configuration(x, "disable")) @@ -356,7 +383,7 @@ def initialize_options(self): self.debug = True self.parallel = configuration.get("parallel", [None])[-1] - def finalize_options(self): + def finalize_options(self) -> None: build_ext.finalize_options(self) if self.debug: global DEBUG @@ -364,25 +391,28 @@ def finalize_options(self): 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 + self.parallel = None + + cpu_count = os.cpu_count() + if cpu_count is not None: + try: + self.parallel = int(os.environ.get("MAX_CONCURRENCY", cpu_count)) + except TypeError: + pass for x in self.feature: if getattr(self, f"disable_{x}"): - setattr(self.feature, x, False) + self.feature.set(x, False) self.feature.required.discard(x) _dbg("Disabling %s", x) if getattr(self, f"enable_{x}"): - msg = f"Conflicting options: --enable-{x} and --disable-{x}" + msg = f"Conflicting options: '-C {x}=enable' and '-C {x}=disable'" raise ValueError(msg) if x == "freetype": - _dbg("--disable-freetype implies --disable-raqm") + _dbg("'-C freetype=disable' implies '-C raqm=disable'") if getattr(self, "enable_raqm"): msg = ( - "Conflicting options: --enable-raqm and --disable-freetype" + "Conflicting options: " + "'-C raqm=enable' and '-C freetype=disable'" ) raise ValueError(msg) setattr(self, "disable_raqm", True) @@ -390,20 +420,28 @@ def finalize_options(self): _dbg("Requiring %s", x) self.feature.required.add(x) if x == "raqm": - _dbg("--enable-raqm implies --enable-freetype") + _dbg("'-C raqm=enable' implies '-C freetype=enable'") self.feature.required.add("freetype") for x in ("raqm", "fribidi"): if getattr(self, f"vendor_{x}"): if getattr(self, "disable_raqm"): - msg = f"Conflicting options: --vendor-{x} and --disable-raqm" + msg = f"Conflicting options: '-C {x}=vendor' and '-C raqm=disable'" raise ValueError(msg) if x == "fribidi" and not getattr(self, "vendor_raqm"): - msg = f"Conflicting options: --vendor-{x} and not --vendor-raqm" + msg = ( + f"Conflicting options: '-C {x}=vendor' and not '-C raqm=vendor'" + ) raise ValueError(msg) _dbg("Using vendored version of %s", x) self.feature.vendor.add(x) - def _update_extension(self, name, libraries, define_macros=None, sources=None): + def _update_extension( + self, + name: str, + libraries: list[str] | list[str | bool | None], + define_macros: list[tuple[str, str | None]] | None = None, + sources: list[str] | None = None, + ) -> None: for extension in self.extensions: if extension.name == name: extension.libraries += libraries @@ -416,16 +454,16 @@ def _update_extension(self, name, libraries, define_macros=None, sources=None): extension.extra_link_args = ["--stdlib=libc++"] break - def _remove_extension(self, name): + def _remove_extension(self, name: str) -> None: for extension in self.extensions: if extension.name == name: self.extensions.remove(extension) break - def get_macos_sdk_path(self): + def get_macos_sdk_path(self) -> str | None: try: sdk_path = ( - subprocess.check_output(["xcrun", "--show-sdk-path"]) + subprocess.check_output(["xcrun", "--show-sdk-path", "--sdk", "macosx"]) .strip() .decode("latin1") ) @@ -443,9 +481,22 @@ def get_macos_sdk_path(self): sdk_path = commandlinetools_sdk_path return sdk_path - def build_extensions(self): - library_dirs = [] - include_dirs = [] + def get_ios_sdk_path(self) -> str: + try: + sdk = sys.implementation._multiarch.split("-")[-1] + _dbg("Using %s SDK", sdk) + return ( + subprocess.check_output(["xcrun", "--show-sdk-path", "--sdk", sdk]) + .strip() + .decode("latin1") + ) + except Exception: + msg = "Unable to identify location of iOS SDK." + raise ValueError(msg) + + def build_extensions(self) -> None: + library_dirs: list[str] = [] + include_dirs: list[str] = [] pkg_config = None if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")): @@ -454,6 +505,7 @@ def build_extensions(self): # # add configured kits for root_name, lib_name in { + "AVIF_ROOT": "avif", "JPEG_ROOT": "libjpeg", "JPEG2K_ROOT": "libopenjp2", "TIFF_ROOT": ("libtiff-5", "libtiff-4"), @@ -469,19 +521,22 @@ def build_extensions(self): 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")) + root_prefix = os.environ[root_name] + root = ( + os.path.join(root_prefix, "lib"), + os.path.join(root_prefix, "include"), + ) if root is None and pkg_config: - if isinstance(lib_name, tuple): + if isinstance(lib_name, str): + _dbg("Looking for `%s` using pkg-config.", lib_name) + root = pkg_config(lib_name) + else: for lib_name2 in lib_name: - _dbg(f"Looking for `{lib_name2}` using pkg-config.") + _dbg("Looking for `%s` using pkg-config.", lib_name2) root = pkg_config(lib_name2) if root: break - else: - _dbg(f"Looking for `{lib_name}` using pkg-config.") - root = pkg_config(lib_name) if isinstance(root, tuple): lib_root, include_root = root @@ -580,6 +635,7 @@ def build_extensions(self): _add_directory(library_dirs, "/usr/X11/lib") _add_directory(include_dirs, "/usr/X11/include") + # Add the macOS SDK path. sdk_path = self.get_macos_sdk_path() if sdk_path: _add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib")) @@ -587,11 +643,19 @@ def build_extensions(self): for extension in self.extensions: extension.extra_compile_args = ["-Wno-nullability-completeness"] - elif ( - sys.platform.startswith("linux") - or sys.platform.startswith("gnu") - or sys.platform.startswith("freebsd") - ): + + elif sys.platform == "ios": + # Add the iOS SDK path. + sdk_path = self.get_ios_sdk_path() + + # Add the iOS SDK path. + _add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib")) + _add_directory(include_dirs, os.path.join(sdk_path, "usr", "include")) + + for extension in self.extensions: + extension.extra_compile_args = ["-Wno-nullability-completeness"] + + elif sys.platform.startswith(("linux", "gnu", "freebsd")): for dirname in _find_library_dirs_ldconfig(): _add_directory(library_dirs, dirname) if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"): @@ -661,22 +725,24 @@ def build_extensions(self): _dbg("Looking for zlib") if _find_include_file(self, "zlib.h"): if _find_library_file(self, "z"): - feature.zlib = "z" + feature.set("zlib", "z") elif sys.platform == "win32" and _find_library_file(self, "zlib"): - feature.zlib = "zlib" # alternative name + feature.set("zlib", "zlib") # alternative name + elif sys.platform == "win32" and _find_library_file(self, "zdll"): + feature.set("zlib", "zdll") # dll import library if feature.want("jpeg"): _dbg("Looking for jpeg") if _find_include_file(self, "jpeglib.h"): if _find_library_file(self, "jpeg"): - feature.jpeg = "jpeg" + feature.set("jpeg", "jpeg") elif sys.platform == "win32" and _find_library_file(self, "libjpeg"): - feature.jpeg = "libjpeg" # alternative name + feature.set("jpeg", "libjpeg") # alternative name - feature.openjpeg_version = None + feature.set("openjpeg_version", None) if feature.want("jpeg2000"): _dbg("Looking for jpeg2000") - best_version = None + best_version: tuple[int, ...] | None = None best_path = None # Find the best version @@ -698,7 +764,7 @@ def build_extensions(self): best_path = os.path.join(directory, name) _dbg( "Best openjpeg version %s so far in %s", - (best_version, best_path), + (str(best_version), best_path), ) if best_version and _find_library_file(self, "openjp2"): @@ -706,26 +772,26 @@ def build_extensions(self): # rather than having to cope with the versioned # include path _add_directory(self.compiler.include_dirs, best_path, 0) - feature.jpeg2000 = "openjp2" - feature.openjpeg_version = ".".join(str(x) for x in best_version) + feature.set("jpeg2000", "openjp2") + feature.set("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 _find_library_file(self, "imagequant"): - feature.imagequant = "imagequant" + feature.set("imagequant", "imagequant") elif _find_library_file(self, "libimagequant"): - feature.imagequant = "libimagequant" + feature.set("imagequant", "libimagequant") 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 in ["win32", "darwin"] and _find_library_file( self, "libtiff" ): - feature.tiff = "libtiff" + feature.set("tiff", "libtiff") + elif _find_library_file(self, "tiff"): + feature.set("tiff", "tiff") if feature.want("freetype"): _dbg("Looking for freetype") @@ -746,31 +812,31 @@ def build_extensions(self): freetype_version = 21 break if freetype_version: - feature.freetype = "freetype" + feature.set("freetype", "freetype") if subdir: _add_directory(self.compiler.include_dirs, subdir, 0) - if feature.freetype and feature.want("raqm"): + if feature.get("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" + feature.set("raqm", "raqm") elif _find_library_file(self, "libraqm"): - feature.raqm = "libraqm" + feature.set("raqm", "libraqm") else: # want to build Raqm from src/thirdparty _dbg("Looking for HarfBuzz") - feature.harfbuzz = None + feature.set("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: + feature.set("harfbuzz", "harfbuzz") + if feature.get("harfbuzz"): if not feature.want_vendor("fribidi"): # want system FriBiDi _dbg("Looking for FriBiDi") - feature.fribidi = None + feature.set("fribidi", None) fribidi_dir = _find_include_dir(self, "fribidi", "fribidi.h") if fribidi_dir: if isinstance(fribidi_dir, str): @@ -778,53 +844,53 @@ def build_extensions(self): self.compiler.include_dirs, fribidi_dir, 0 ) if _find_library_file(self, "fribidi"): - feature.fribidi = "fribidi" - feature.raqm = True + feature.set("fribidi", "fribidi") + feature.set("raqm", True) else: # want to build FriBiDi shim from src/thirdparty - feature.raqm = True + feature.set("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" + feature.set("lcms", "lcms2") elif _find_library_file(self, "lcms2_static"): # alternate Windows name. - feature.lcms = "lcms2_static" + feature.set("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" - ): - # 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 all( + _find_include_file(self, "webp/" + include) + for include in ("encode.h", "decode.h", "mux.h", "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" - ): - feature.webpmux = "libwebpmux" + # In Google's precompiled zip it is called "libwebp" + for prefix in ("", "lib"): + if all( + _find_library_file(self, prefix + library) + for library in ("webp", "webpmux", "webpdemux") + ): + feature.set("webp", prefix + "webp") + break 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" + feature.set("xcb", "xcb") + + if feature.want("avif"): + _dbg("Looking for avif") + if avif_h := _find_include_file(self, "avif/avif.h"): + with open(avif_h, "rb") as fp: + major_version = int( + fp.read().split(b"#define AVIF_VERSION_MAJOR ")[1].split()[0] + ) + if major_version >= 1 and _find_library_file(self, "avif"): + feature.set("avif", "avif") for f in feature: - if not getattr(feature, f) and feature.require(f): + if not feature.get(f) and feature.require(f): if f in ("jpeg", "zlib"): raise RequiredDependencyException(f) raise DependencyException(f) @@ -832,10 +898,10 @@ def build_extensions(self): # # core library - libs = self.add_imaging_libs.split() - defs = [] - if feature.tiff: - libs.append(feature.tiff) + libs: list[str | bool | None] = [] + defs: list[tuple[str, str | None]] = [] + if feature.get("tiff"): + libs.append(feature.get("tiff")) defs.append(("HAVE_LIBTIFF", None)) if sys.platform == "win32": # This define needs to be defined if-and-only-if it was defined @@ -843,22 +909,28 @@ def build_extensions(self): # 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.jpeg: - libs.append(feature.jpeg) + elif sys.platform == "ios": + # Ensure transitive dependencies are linked. + libs.append("lzma") + if feature.get("jpeg"): + libs.append(feature.get("jpeg")) defs.append(("HAVE_LIBJPEG", None)) - if feature.jpeg2000: - libs.append(feature.jpeg2000) + if feature.get("jpeg2000"): + libs.append(feature.get("jpeg2000")) defs.append(("HAVE_OPENJPEG", None)) if sys.platform == "win32" and not PLATFORM_MINGW: defs.append(("OPJ_STATIC", None)) - if feature.zlib: - libs.append(feature.zlib) + if feature.get("zlib"): + libs.append(feature.get("zlib")) defs.append(("HAVE_LIBZ", None)) - if feature.imagequant: - libs.append(feature.imagequant) + if feature.get("imagequant"): + libs.append(feature.get("imagequant")) defs.append(("HAVE_LIBIMAGEQUANT", None)) - if feature.xcb: - libs.append(feature.xcb) + if feature.get("xcb"): + libs.append(feature.get("xcb")) + if sys.platform == "ios": + # Ensure transitive dependencies are linked. + libs.append("Xau") defs.append(("HAVE_XCB", None)) if sys.platform == "win32": libs.extend(["kernel32", "user32", "gdi32"]) @@ -872,50 +944,60 @@ def build_extensions(self): # # additional libraries - if feature.freetype: + if feature.get("freetype"): srcs = [] libs = ["freetype"] defs = [] - if feature.raqm: + if feature.get("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) + libs.append(feature.get("raqm")) else: # building Raqm from src/thirdparty defs.append(("HAVE_RAQM", None)) srcs.append("src/thirdparty/raqm/raqm.c") - libs.append(feature.harfbuzz) + libs.append(feature.get("harfbuzz")) if not feature.want_vendor("fribidi"): # using system FriBiDi defs.append(("HAVE_FRIBIDI_SYSTEM", None)) - libs.append(feature.fribidi) + libs.append(feature.get("fribidi")) else: # building FriBiDi shim from src/thirdparty srcs.append("src/thirdparty/fribidi-shim/fribidi.c") + + if sys.platform == "ios": + # Ensure transitive dependencies are linked. + libs.extend(["z", "bz2", "brotlicommon", "brotlidec", "png"]) + self._update_extension("PIL._imagingft", libs, defs, srcs) else: self._remove_extension("PIL._imagingft") - if feature.lcms: - extra = [] + if feature.get("lcms"): + libs = [feature.get("lcms")] if sys.platform == "win32": - extra.extend(["user32", "gdi32"]) - self._update_extension("PIL._imagingcms", [feature.lcms] + extra) + libs.extend(["user32", "gdi32"]) + self._update_extension("PIL._imagingcms", libs) else: self._remove_extension("PIL._imagingcms") - if feature.webp: - libs = [feature.webp] - defs = [] - - if feature.webpmux: - defs.append(("HAVE_WEBPMUX", None)) - libs.append(feature.webpmux) - libs.append(feature.webpmux.replace("pmux", "pdemux")) - - self._update_extension("PIL._webp", libs, defs) + webp = feature.get("webp") + if isinstance(webp, str): + libs = [webp, webp + "mux", webp + "demux"] + if sys.platform == "ios": + # Ensure transitive dependencies are linked. + libs.append("sharpyuv") + self._update_extension("PIL._webp", libs) else: self._remove_extension("PIL._webp") + if feature.get("avif"): + libs = [feature.get("avif")] + if sys.platform == "win32": + libs.extend(["ntdll", "userenv", "ws2_32", "bcrypt"]) + self._update_extension("PIL._avif", libs) + else: + self._remove_extension("PIL._avif") + tk_libs = ["psapi"] if sys.platform in ("win32", "cygwin") else [] self._update_extension("PIL._imagingtk", tk_libs) @@ -926,14 +1008,14 @@ def build_extensions(self): self.summary_report(feature) - def summary_report(self, feature): + def summary_report(self, feature: ext_feature) -> None: print("-" * 68) print("PIL SETUP SUMMARY") print("-" * 68) print(f"version Pillow {PILLOW_VERSION}") - v = sys.version.split("[") - print(f"platform {sys.platform} {v[0].strip()}") - for v in v[1:]: + version = sys.version.split("[") + print(f"platform {sys.platform} {version[0].strip()}") + for v in version[1:]: print(f" [{v.strip()}") print("-" * 68) @@ -944,17 +1026,21 @@ def summary_report(self, feature): raqm_extra_info += ", FriBiDi shim" options = [ - (feature.jpeg, "JPEG"), - (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)"), + (feature.get("jpeg"), "JPEG"), + ( + feature.get("jpeg2000"), + "OPENJPEG (JPEG2000)", + feature.get("openjpeg_version"), + ), + (feature.get("zlib"), "ZLIB (PNG/ZIP)"), + (feature.get("imagequant"), "LIBIMAGEQUANT"), + (feature.get("tiff"), "LIBTIFF"), + (feature.get("freetype"), "FREETYPE2"), + (feature.get("raqm"), "RAQM (Text shaping)", raqm_extra_info), + (feature.get("lcms"), "LITTLECMS2"), + (feature.get("webp"), "WEBP"), + (feature.get("xcb"), "XCB (X protocol)"), + (feature.get("avif"), "LIBAVIF"), ] all = 1 @@ -983,35 +1069,40 @@ def summary_report(self, feature): print("") -def debug_build(): +def debug_build() -> bool: 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")) +libraries: list[tuple[str, _BuildInfo]] = [ + ("pil_imaging_mode", {"sources": ["src/libImaging/Mode.c"]}), +] + +files: list[str | os.PathLike[str]] = ["src/_imaging.c"] +files.extend("src/" + src_file + ".c" for src_file in _IMAGING) +files.extend( + os.path.join("src/libImaging", src_file + ".c") for src_file in _LIB_IMAGING +) 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._avif", ["src/_avif.c"]), Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]), - Extension("PIL._imagingmath", ["src/_imagingmath.c"]), + Extension( + "PIL._imagingmath", + ["src/_imagingmath.c"], + libraries=None if sys.platform == "win32" else ["m"], + ), Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), ] -# parse configuration from _custom_build/backend.py -while sys.argv[-1].startswith("--pillow-configuration="): - _, key, value = sys.argv.pop().split("=", 2) - configuration.setdefault(key, []).append(value) - try: setup( cmdclass={"build_ext": pil_build_ext}, ext_modules=ext_modules, + libraries=libraries, zip_safe=not (debug_build() or PLATFORM_MINGW), ) except RequiredDependencyException as err: @@ -1030,7 +1121,7 @@ def debug_build(): msg = f""" The headers or library files could not be found for {str(err)}, -which was requested by the option flag --enable-{str(err)} +which was requested by the option flag '-C {str(err)}=enable' """ sys.stderr.write(msg) diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py new file mode 100644 index 00000000000..43c39a9fbe7 --- /dev/null +++ b/src/PIL/AvifImagePlugin.py @@ -0,0 +1,293 @@ +from __future__ import annotations + +import os +from io import BytesIO +from typing import IO + +from . import ExifTags, Image, ImageFile + +try: + from . import _avif + + SUPPORTED = True +except ImportError: + SUPPORTED = False + +# Decoder options as module globals, until there is a way to pass parameters +# to Image.open (see https://github.com/python-pillow/Pillow/issues/569) +DECODE_CODEC_CHOICE = "auto" +DEFAULT_MAX_THREADS = 0 + + +def get_codec_version(codec_name: str) -> str | None: + versions = _avif.codec_versions() + for version in versions.split(", "): + if version.split(" [")[0] == codec_name: + return version.split(":")[-1].split(" ")[0] + return None + + +def _accept(prefix: bytes) -> bool | str: + if prefix[4:8] != b"ftyp": + return False + major_brand = prefix[8:12] + if major_brand in ( + # coding brands + b"avif", + b"avis", + # We accept files with AVIF container brands; we can't yet know if + # the ftyp box has the correct compatible brands, but if it doesn't + # then the plugin will raise a SyntaxError which Pillow will catch + # before moving on to the next plugin that accepts the file. + # + # Also, because this file might not actually be an AVIF file, we + # don't raise an error if AVIF support isn't properly compiled. + b"mif1", + b"msf1", + ): + if not SUPPORTED: + return ( + "image file could not be identified because AVIF support not installed" + ) + return True + return False + + +def _get_default_max_threads() -> int: + if DEFAULT_MAX_THREADS: + return DEFAULT_MAX_THREADS + if hasattr(os, "sched_getaffinity"): + return len(os.sched_getaffinity(0)) + else: + return os.cpu_count() or 1 + + +class AvifImageFile(ImageFile.ImageFile): + format = "AVIF" + format_description = "AVIF image" + __frame = -1 + + def _open(self) -> None: + if not SUPPORTED: + msg = "image file could not be opened because AVIF support not installed" + raise SyntaxError(msg) + + if DECODE_CODEC_CHOICE != "auto" and not _avif.decoder_codec_available( + DECODE_CODEC_CHOICE + ): + msg = "Invalid opening codec" + raise ValueError(msg) + + assert self.fp is not None + self._decoder = _avif.AvifDecoder( + self.fp.read(), + DECODE_CODEC_CHOICE, + _get_default_max_threads(), + ) + + # Get info from decoder + self._size, self.n_frames, self._mode, icc, exif, exif_orientation, xmp = ( + self._decoder.get_info() + ) + self.is_animated = self.n_frames > 1 + + if icc: + self.info["icc_profile"] = icc + if xmp: + self.info["xmp"] = xmp + + if exif_orientation != 1 or exif: + exif_data = Image.Exif() + if exif: + exif_data.load(exif) + original_orientation = exif_data.get(ExifTags.Base.Orientation, 1) + else: + original_orientation = 1 + if exif_orientation != original_orientation: + exif_data[ExifTags.Base.Orientation] = exif_orientation + exif = exif_data.tobytes() + if exif: + self.info["exif"] = exif + self.seek(0) + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + + # Set tile + self.__frame = frame + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)] + + def load(self) -> Image.core.PixelAccess | None: + if self.tile: + # We need to load the image data for this frame + data, timescale, pts_in_timescales, duration_in_timescales = ( + self._decoder.get_frame(self.__frame) + ) + self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale)) + self.info["duration"] = round(1000 * (duration_in_timescales / timescale)) + + if self.fp and self._exclusive_fp: + self.fp.close() + self.fp = BytesIO(data) + + return super().load() + + def load_seek(self, pos: int) -> None: + pass + + def tell(self) -> int: + return self.__frame + + +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + _save(im, fp, filename, save_all=True) + + +def _save( + im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False +) -> None: + info = im.encoderinfo.copy() + if save_all: + append_images = list(info.get("append_images", [])) + else: + append_images = [] + + total = 0 + for ims in [im] + append_images: + total += getattr(ims, "n_frames", 1) + + quality = info.get("quality", 75) + if not isinstance(quality, int) or quality < 0 or quality > 100: + msg = "Invalid quality setting" + raise ValueError(msg) + + duration = info.get("duration", 0) + subsampling = info.get("subsampling", "4:2:0") + speed = info.get("speed", 6) + max_threads = info.get("max_threads", _get_default_max_threads()) + codec = info.get("codec", "auto") + if codec != "auto" and not _avif.encoder_codec_available(codec): + msg = "Invalid saving codec" + raise ValueError(msg) + range_ = info.get("range", "full") + tile_rows_log2 = info.get("tile_rows", 0) + tile_cols_log2 = info.get("tile_cols", 0) + alpha_premultiplied = bool(info.get("alpha_premultiplied", False)) + autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0)) + + icc_profile = info.get("icc_profile", im.info.get("icc_profile")) + exif_orientation = 1 + if exif := info.get("exif"): + if isinstance(exif, Image.Exif): + exif_data = exif + else: + exif_data = Image.Exif() + exif_data.load(exif) + if ExifTags.Base.Orientation in exif_data: + exif_orientation = exif_data.pop(ExifTags.Base.Orientation) + exif = exif_data.tobytes() if exif_data else b"" + elif isinstance(exif, Image.Exif): + exif = exif_data.tobytes() + + xmp = info.get("xmp") + + if isinstance(xmp, str): + xmp = xmp.encode("utf-8") + + advanced = info.get("advanced") + if advanced is not None: + if isinstance(advanced, dict): + advanced = advanced.items() + try: + advanced = tuple(advanced) + except TypeError: + invalid = True + else: + invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced) + if invalid: + msg = ( + "advanced codec options must be a dict of key-value string " + "pairs or a series of key-value two-tuples" + ) + raise ValueError(msg) + + # Setup the AVIF encoder + enc = _avif.AvifEncoder( + im.size, + subsampling, + quality, + speed, + max_threads, + codec, + range_, + tile_rows_log2, + tile_cols_log2, + alpha_premultiplied, + autotiling, + icc_profile or b"", + exif or b"", + exif_orientation, + xmp or b"", + advanced, + ) + + # Add each frame + frame_idx = 0 + frame_duration = 0 + cur_idx = im.tell() + is_single_frame = total == 1 + try: + for ims in [im] + append_images: + # Get number of frames in this image + nfr = getattr(ims, "n_frames", 1) + + for idx in range(nfr): + ims.seek(idx) + + # Make sure image mode is supported + frame = ims + rawmode = ims.mode + if ims.mode not in {"RGB", "RGBA"}: + rawmode = "RGBA" if ims.has_transparency_data else "RGB" + frame = ims.convert(rawmode) + + # Update frame duration + if isinstance(duration, (list, tuple)): + frame_duration = duration[frame_idx] + else: + frame_duration = duration + + # Append the frame to the animation encoder + enc.add( + frame.tobytes("raw", rawmode), + frame_duration, + frame.size, + rawmode, + is_single_frame, + ) + + # Update frame index + frame_idx += 1 + + if not save_all: + break + + finally: + im.seek(cur_idx) + + # Get the final output from the encoder + data = enc.finish() + if data is None: + msg = "cannot write file as AVIF (encoder returned None)" + raise OSError(msg) + + fp.write(data) + + +Image.register_open(AvifImageFile.format, AvifImageFile, _accept) +if SUPPORTED: + Image.register_save(AvifImageFile.format, _save) + Image.register_save_all(AvifImageFile.format, _save_all) + Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"]) + Image.register_mime(AvifImageFile.format, "image/avif") diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index bc1416c74c6..1c8c28ff038 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -20,23 +20,13 @@ """ Parse X Bitmap Distribution Format (BDF) """ + from __future__ import annotations from typing import BinaryIO from . import FontFile, Image -bdf_slant = { - "R": "Roman", - "I": "Italic", - "O": "Oblique", - "RI": "Reverse Italic", - "RO": "Reverse Oblique", - "OT": "Other", -} - -bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"} - def bdf_char( f: BinaryIO, @@ -54,7 +44,7 @@ def bdf_char( s = f.readline() if not s: return None - if s[:9] == b"STARTCHAR": + if s.startswith(b"STARTCHAR"): break id = s[9:].strip().decode("ascii") @@ -62,7 +52,7 @@ def bdf_char( props = {} while True: s = f.readline() - if not s or s[:6] == b"BITMAP": + if not s or s.startswith(b"BITMAP"): break i = s.find(b" ") props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") @@ -71,7 +61,7 @@ def bdf_char( bitmap = bytearray() while True: s = f.readline() - if not s or s[:7] == b"ENDCHAR": + if not s or s.startswith(b"ENDCHAR"): break bitmap += s[:-1] @@ -107,7 +97,7 @@ def __init__(self, fp: BinaryIO) -> None: super().__init__() s = fp.readline() - if s[:13] != b"STARTFONT 2.1": + if not s.startswith(b"STARTFONT 2.1"): msg = "not a valid BDF file" raise SyntaxError(msg) @@ -116,7 +106,7 @@ def __init__(self, fp: BinaryIO) -> None: while True: s = fp.readline() - if not s or s[:13] == b"ENDPROPERTIES": + if not s or s.startswith(b"ENDPROPERTIES"): break i = s.find(b" ") props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index b9cefafdd07..6bb92edf891 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -246,7 +246,7 @@ class BLPFormatError(NotImplementedError): def _accept(prefix: bytes) -> bool: - return prefix[:4] in (b"BLP1", b"BLP2") + return prefix.startswith((b"BLP1", b"BLP2")) class BlpImageFile(ImageFile.ImageFile): @@ -258,30 +258,46 @@ class BlpImageFile(ImageFile.ImageFile): format_description = "Blizzard Mipmap Format" def _open(self) -> None: + assert self.fp is not None self.magic = self.fp.read(4) + if not _accept(self.magic): + msg = f"Bad BLP magic {repr(self.magic)}" + raise BLPFormatError(msg) - self.fp.seek(5, os.SEEK_CUR) - (self._blp_alpha_depth,) = struct.unpack(" tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: try: - self._read_blp_header() + self._read_header() self._load() except struct.error as e: msg = "Truncated BLP file" @@ -292,27 +308,12 @@ def decode(self, buffer: bytes) -> tuple[int, int]: def _load(self) -> None: pass - def _read_blp_header(self) -> None: - assert self.fd is not None - self.fd.seek(4) - (self._blp_compression,) = struct.unpack(" None: + self._offsets = struct.unpack("<16I", self._safe_read(16 * 4)) + self._lengths = struct.unpack("<16I", self._safe_read(16 * 4)) def _safe_read(self, length: int) -> bytes: + assert self.fd is not None return ImageFile._safe_read(self.fd, length) def _read_palette(self) -> list[tuple[int, int, int, int]]: @@ -325,9 +326,11 @@ def _read_palette(self) -> list[tuple[int, int, int, int]]: ret.append((b, g, r, a)) return ret - def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray: + def _read_bgra( + self, palette: list[tuple[int, int, int, int]], alpha: bool + ) -> bytearray: data = bytearray() - _data = BytesIO(self._safe_read(self._blp_lengths[0])) + _data = BytesIO(self._safe_read(self._lengths[0])) while True: try: (offset,) = struct.unpack(" bytearray: break b, g, r, a = palette[offset] d: tuple[int, ...] = (r, g, b) - if self._blp_alpha_depth: + if alpha: d += (a,) data.extend(d) return data @@ -343,19 +346,21 @@ def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray: class BLP1Decoder(_BLPBaseDecoder): def _load(self) -> None: - if self._blp_compression == Format.JPEG: + self._compression, self._encoding, alpha = self.args + + if self._compression == Format.JPEG: self._decode_jpeg_stream() - elif self._blp_compression == 1: - if self._blp_encoding in (4, 5): + elif self._compression == 1: + if self._encoding in (4, 5): palette = self._read_palette() - data = self._read_bgra(palette) + data = self._read_bgra(palette, alpha) self.set_as_raw(data) else: - msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}" + msg = f"Unsupported BLP encoding {repr(self._encoding)}" raise BLPFormatError(msg) else: - msg = f"Unsupported BLP compression {repr(self._blp_encoding)}" + msg = f"Unsupported BLP compression {repr(self._encoding)}" raise BLPFormatError(msg) def _decode_jpeg_stream(self) -> None: @@ -364,62 +369,61 @@ def _decode_jpeg_stream(self) -> None: (jpeg_header_size,) = struct.unpack(" None: + self._compression, self._encoding, alpha, self._alpha_encoding = self.args + palette = self._read_palette() assert self.fd is not None - self.fd.seek(self._blp_offsets[0]) + self.fd.seek(self._offsets[0]) - if self._blp_compression == 1: + if self._compression == 1: # Uncompressed or DirectX compression - if self._blp_encoding == Encoding.UNCOMPRESSED: - data = self._read_bgra(palette) + if self._encoding == Encoding.UNCOMPRESSED: + data = self._read_bgra(palette, alpha) - elif self._blp_encoding == Encoding.DXT: + elif self._encoding == Encoding.DXT: data = bytearray() - if self._blp_alpha_encoding == AlphaEncoding.DXT1: - linesize = (self.size[0] + 3) // 4 * 8 - for yb in range((self.size[1] + 3) // 4): - for d in decode_dxt1( - self._safe_read(linesize), alpha=bool(self._blp_alpha_depth) - ): + if self._alpha_encoding == AlphaEncoding.DXT1: + linesize = (self.state.xsize + 3) // 4 * 8 + for yb in range((self.state.ysize + 3) // 4): + for d in decode_dxt1(self._safe_read(linesize), alpha): data += d - elif self._blp_alpha_encoding == AlphaEncoding.DXT3: - linesize = (self.size[0] + 3) // 4 * 16 - for yb in range((self.size[1] + 3) // 4): + elif self._alpha_encoding == AlphaEncoding.DXT3: + linesize = (self.state.xsize + 3) // 4 * 16 + for yb in range((self.state.ysize + 3) // 4): for d in decode_dxt3(self._safe_read(linesize)): data += d - elif self._blp_alpha_encoding == AlphaEncoding.DXT5: - linesize = (self.size[0] + 3) // 4 * 16 - for yb in range((self.size[1] + 3) // 4): + elif self._alpha_encoding == AlphaEncoding.DXT5: + linesize = (self.state.xsize + 3) // 4 * 16 + for yb in range((self.state.ysize + 3) // 4): for d in decode_dxt5(self._safe_read(linesize)): data += d else: - msg = f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}" + msg = f"Unsupported alpha encoding {repr(self._alpha_encoding)}" raise BLPFormatError(msg) else: - msg = f"Unknown BLP encoding {repr(self._blp_encoding)}" + msg = f"Unknown BLP encoding {repr(self._encoding)}" raise BLPFormatError(msg) else: - msg = f"Unknown BLP compression {repr(self._blp_compression)}" + msg = f"Unknown BLP compression {repr(self._compression)}" raise BLPFormatError(msg) self.set_as_raw(data) @@ -466,17 +470,23 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2" fp.write(magic) + assert im.palette is not None fp.write(struct.pack(" bool: - return prefix[:2] == b"BM" + return prefix.startswith(b"BM") def _dib_accept(prefix: bytes) -> bool: @@ -72,16 +74,21 @@ class BmpImageFile(ImageFile.ImageFile): for k, v in COMPRESSIONS.items(): vars()[k] = v - def _bitmap(self, header=0, offset=0): + def _bitmap(self, header: int = 0, offset: int = 0) -> None: """Read relevant info about the BMP""" + assert self.fp is not None read, seek = self.fp.read, self.fp.seek if header: seek(header) # read bmp header size @offset 14 (this is part of the header size) - file_info = {"header_size": i32(read(4)), "direction": -1} + file_info: dict[str, bool | int | tuple[int, ...]] = { + "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 + assert isinstance(file_info["header_size"], int) header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4) # ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1 @@ -92,7 +99,7 @@ def _bitmap(self, header=0, offset=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["compression"] = self.COMPRESSIONS["RAW"] file_info["palette_padding"] = 3 # --------------------------------------------- Windows Bitmap v3 to v5 @@ -122,8 +129,9 @@ def _bitmap(self, header=0, offset=0): ) file_info["colors"] = i32(header_data, 28) file_info["palette_padding"] = 4 + assert isinstance(file_info["pixels_per_meter"], tuple) self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"]) - if file_info["compression"] == self.BITFIELDS: + if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]: masks = ["r_mask", "g_mask", "b_mask"] if len(header_data) >= 48: if len(header_data) >= 52: @@ -144,6 +152,10 @@ def _bitmap(self, header=0, offset=0): file_info["a_mask"] = 0x0 for mask in masks: file_info[mask] = i32(read(4)) + assert isinstance(file_info["r_mask"], int) + assert isinstance(file_info["g_mask"], int) + assert isinstance(file_info["b_mask"], int) + assert isinstance(file_info["a_mask"], int) file_info["rgb_mask"] = ( file_info["r_mask"], file_info["g_mask"], @@ -161,27 +173,29 @@ def _bitmap(self, header=0, offset=0): # ------------------ Special case : header is reported 40, which # ---------------------- is shorter than real size for bpp >= 16 + assert isinstance(file_info["width"], int) + assert isinstance(file_info["height"], int) 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"]) - ) + assert isinstance(file_info["bits"], int) + if not file_info.get("colors", 0): + file_info["colors"] = 1 << file_info["bits"] + assert isinstance(file_info["palette_padding"], int) + assert isinstance(file_info["colors"], int) if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8: - offset += 4 * file_info["colors"] + offset += file_info["palette_padding"] * 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: + self._mode, raw_mode = BIT2MODE.get(file_info["bits"], ("", "")) + if not self.mode: msg = f"Unsupported BMP pixel depth ({file_info['bits']})" raise OSError(msg) # ---------------- Process BMP with Bitfields compression (not palette) decoder_name = "raw" - if file_info["compression"] == self.BITFIELDS: - SUPPORTED = { + if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]: + SUPPORTED: dict[int, list[tuple[int, ...]]] = { 32: [ (0xFF0000, 0xFF00, 0xFF, 0x0), (0xFF000000, 0xFF0000, 0xFF00, 0x0), @@ -213,12 +227,14 @@ def _bitmap(self, header=0, offset=0): file_info["bits"] == 32 and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]] ): + assert isinstance(file_info["rgba_mask"], tuple) 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"]] ): + assert isinstance(file_info["rgb_mask"], tuple) raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])] else: msg = "Unsupported BMP bitfields layout" @@ -226,10 +242,15 @@ def _bitmap(self, header=0, offset=0): else: msg = "Unsupported BMP bitfields layout" raise OSError(msg) - elif file_info["compression"] == self.RAW: - if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset + elif file_info["compression"] == self.COMPRESSIONS["RAW"]: + if file_info["bits"] == 32 and ( + header == 22 or USE_RAW_ALPHA # 32-bit .cur offset + ): raw_mode, self._mode = "BGRA", "RGBA" - elif file_info["compression"] in (self.RLE8, self.RLE4): + elif file_info["compression"] in ( + self.COMPRESSIONS["RLE8"], + self.COMPRESSIONS["RLE4"], + ): decoder_name = "bmp_rle" else: msg = f"Unsupported BMP compression ({file_info['compression']})" @@ -269,14 +290,15 @@ def _bitmap(self, header=0, offset=0): # ---------------------------- Finally set the tile data for the plugin self.info["compression"] = file_info["compression"] - args = [raw_mode] + args: list[Any] = [raw_mode] if decoder_name == "bmp_rle": - args.append(file_info["compression"] == self.RLE4) + args.append(file_info["compression"] == self.COMPRESSIONS["RLE4"]) else: + assert isinstance(file_info["width"], int) args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3)) args.append(file_info["direction"]) self.tile = [ - ( + ImageFile._Tile( decoder_name, (0, 0, file_info["width"], file_info["height"]), offset or self.fp.tell(), @@ -287,6 +309,7 @@ def _bitmap(self, header=0, offset=0): def _open(self) -> None: """Open file, check magic number and read header""" # read 14 bytes: magic number, filesize, reserved, header final offset + assert self.fp is not None head_data = self.fp.read(14) # choke if the file does not have the required magic bytes if not _accept(head_data): @@ -301,7 +324,7 @@ def _open(self) -> None: class BmpRleDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None rle4 = self.args[1] data = bytearray() @@ -343,7 +366,7 @@ def decode(self, buffer: bytes) -> tuple[int, int]: bytes_read = self.fd.read(2) if len(bytes_read) < 2: break - right, up = self.fd.read(2) + right, up = bytes_read data += b"\x00" * (right + up * self.state.xsize) x = len(data) % self.state.xsize else: @@ -367,7 +390,7 @@ def decode(self, buffer: bytes) -> tuple[int, int]: 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])) + self.set_as_raw(bytes(data), rawmode, (0, self.args[-1])) return -1, 0 @@ -421,9 +444,9 @@ def _save( image = stride * im.size[1] if im.mode == "1": - palette = b"".join(o8(i) * 4 for i in (0, 255)) + palette = b"".join(o8(i) * 3 + b"\x00" for i in (0, 255)) elif im.mode == "L": - palette = b"".join(o8(i) * 4 for i in range(256)) + palette = b"".join(o8(i) * 3 + b"\x00" for i in range(256)) elif im.mode == "P": palette = im.im.getpalette("RGB", "BGRX") colors = len(palette) // 4 @@ -464,7 +487,9 @@ def _save( if palette: fp.write(palette) - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))] + ) # diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 0ee2f653b2c..d82c4c746c3 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -10,6 +10,7 @@ # from __future__ import annotations +import os from typing import IO from . import Image, ImageFile @@ -32,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC" + return prefix.startswith((b"BUFR", b"ZCZC")) class BufrStubImageFile(ImageFile.StubImageFile): @@ -40,22 +41,17 @@ class BufrStubImageFile(ImageFile.StubImageFile): format_description = "BUFR" def _open(self) -> None: - offset = self.fp.tell() - + assert self.fp is not None if not _accept(self.fp.read(4)): msg = "Not a BUFR file" raise SyntaxError(msg) - self.fp.seek(offset) + self.fp.seek(-4, os.SEEK_CUR) # make something up self._mode = "F" self._size = 1, 1 - loader = self._load() - if loader: - loader.open(self) - def _load(self) -> ImageFile.StubHandler | None: return _handler diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py index 0035296a45c..ec9e66c714f 100644 --- a/src/PIL/ContainerIO.py +++ b/src/PIL/ContainerIO.py @@ -16,10 +16,11 @@ from __future__ import annotations import io -from typing import IO, AnyStr, Generic, Literal +from collections.abc import Iterable +from typing import IO, AnyStr, NoReturn -class ContainerIO(Generic[AnyStr]): +class ContainerIO(IO[AnyStr]): """ A file object that provides read access to a part of an existing file (for example a TAR file). @@ -45,7 +46,10 @@ def __init__(self, file: IO[AnyStr], offset: int, length: int) -> None: def isatty(self) -> bool: return False - def seek(self, offset: int, mode: Literal[0, 1, 2] = io.SEEK_SET) -> None: + def seekable(self) -> bool: + return True + + def seek(self, offset: int, mode: int = io.SEEK_SET) -> int: """ Move file pointer. @@ -53,6 +57,7 @@ def seek(self, offset: int, mode: Literal[0, 1, 2] = io.SEEK_SET) -> None: :param mode: Starting position. Use 0 for beginning of region, 1 for current offset, and 2 for end of region. You cannot move the pointer outside the defined region. + :returns: Offset from start of region, in bytes. """ if mode == 1: self.pos = self.pos + offset @@ -63,6 +68,7 @@ def seek(self, offset: int, mode: Literal[0, 1, 2] = io.SEEK_SET) -> None: # clamp self.pos = max(0, min(self.pos, self.length)) self.fh.seek(self.offset + self.pos) + return self.pos def tell(self) -> int: """ @@ -72,27 +78,32 @@ def tell(self) -> int: """ return self.pos - def read(self, n: int = 0) -> AnyStr: + def readable(self) -> bool: + return True + + def read(self, n: int = -1) -> AnyStr: """ Read data. - :param n: Number of bytes to read. If omitted or zero, + :param n: Number of bytes to read. If omitted, zero or negative, read until end of region. :returns: An 8-bit string. """ - if n: + if n > 0: n = min(n, self.length - self.pos) else: n = self.length - self.pos - if not n: # EOF + if n <= 0: # EOF return b"" if "b" in self.fh.mode else "" # type: ignore[return-value] self.pos = self.pos + n return self.fh.read(n) - def readline(self) -> AnyStr: + def readline(self, n: int = -1) -> AnyStr: """ Read a line of text. + :param n: Number of bytes to read. If omitted, zero or negative, + read until end of line. :returns: An 8-bit string. """ s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment] @@ -102,14 +113,16 @@ def readline(self) -> AnyStr: if not c: break s = s + c - if c == newline_character: + if c == newline_character or len(s) == n: break return s - def readlines(self) -> list[AnyStr]: + def readlines(self, n: int | None = -1) -> list[AnyStr]: """ Read multiple lines of text. + :param n: Number of lines to read. If omitted, zero, negative or None, + read until end of region. :returns: A list of 8-bit strings. """ lines = [] @@ -118,4 +131,43 @@ def readlines(self) -> list[AnyStr]: if not s: break lines.append(s) + if len(lines) == n: + break return lines + + def writable(self) -> bool: + return False + + def write(self, b: AnyStr) -> NoReturn: + raise NotImplementedError() + + def writelines(self, lines: Iterable[AnyStr]) -> NoReturn: + raise NotImplementedError() + + def truncate(self, size: int | None = None) -> int: + raise NotImplementedError() + + def __enter__(self) -> ContainerIO[AnyStr]: + return self + + def __exit__(self, *args: object) -> None: + self.close() + + def __iter__(self) -> ContainerIO[AnyStr]: + return self + + def __next__(self) -> AnyStr: + line = self.readline() + if not line: + msg = "end of region" + raise StopIteration(msg) + return line + + def fileno(self) -> int: + return self.fh.fileno() + + def flush(self) -> None: + self.fh.flush() + + def close(self) -> None: + self.fh.close() diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index 85e2145e766..9c188e08446 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -26,7 +26,7 @@ def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"\0\0\2\0" + return prefix.startswith(b"\0\0\2\0") ## @@ -38,6 +38,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): format_description = "Windows Cursor" def _open(self) -> None: + assert self.fp is not None offset = self.fp.tell() # check magic @@ -63,8 +64,7 @@ def _open(self) -> None: # patch up the bitmap height 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 = [self.tile[0]._replace(extents=(0, 0) + self.size)] # diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index f67f27d73bb..d3f456ddcc4 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -24,6 +24,7 @@ from . import Image from ._binary import i32le as i32 +from ._util import DeferredError from .PcxImagePlugin import PcxImageFile MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? @@ -44,6 +45,7 @@ class DcxImageFile(PcxImageFile): def _open(self) -> None: # Header + assert self.fp is not None s = self.fp.read(4) if not _accept(s): msg = "not a DCX file" @@ -66,6 +68,8 @@ def _open(self) -> None: def seek(self, frame: int) -> None: if not self._seek_check(frame): return + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.frame = frame self.fp = self._fp self.fp.seek(self._offset[frame]) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index a57e4aea2d5..312f602a6b1 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -1,5 +1,5 @@ """ -A Pillow loader for .dds files (S3TC-compressed aka DXTC) +A Pillow plugin for .dds files (S3TC-compressed aka DXTC) Jerome Leclanche Documentation: @@ -12,7 +12,6 @@ from __future__ import annotations -import io import struct import sys from enum import IntEnum, IntFlag @@ -333,6 +332,7 @@ class DdsImageFile(ImageFile.ImageFile): format_description = "DirectDraw Surface" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(4)): msg = "not a DDS file" raise SyntaxError(msg) @@ -340,21 +340,20 @@ def _open(self) -> None: if header_size != 124: msg = f"Unsupported header size {repr(header_size)}" raise OSError(msg) - header_bytes = self.fp.read(header_size - 4) - if len(header_bytes) != 120: - msg = f"Incomplete header: {len(header_bytes)} bytes" + header = self.fp.read(header_size - 4) + if len(header) != 120: + msg = f"Incomplete header: {len(header)} bytes" raise OSError(msg) - header = io.BytesIO(header_bytes) - flags, height, width = struct.unpack("<3I", header.read(12)) + flags, height, width = struct.unpack("<3I", header[:12]) self._size = (width, height) extents = (0, 0) + self.size - pitch, depth, mipmaps = struct.unpack("<3I", header.read(12)) - struct.unpack("<11I", header.read(44)) # reserved + pitch, depth, mipmaps = struct.unpack("<3I", header[12:24]) + struct.unpack("<11I", header[24:68]) # reserved # pixel format - pfsize, pfflags, fourcc, bitcount = struct.unpack("<4I", header.read(16)) + pfsize, pfflags, fourcc, bitcount = struct.unpack("<4I", header[68:84]) n = 0 rawmode = None if pfflags & DDPF.RGB: @@ -366,8 +365,8 @@ def _open(self) -> None: self._mode = "RGB" mask_count = 3 - masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4)) - self.tile = [("dds_rgb", extents, 0, (bitcount, masks))] + masks = struct.unpack(f"<{mask_count}I", header[84 : 84 + mask_count * 4]) + self.tile = [ImageFile._Tile("dds_rgb", extents, 0, (bitcount, masks))] return elif pfflags & DDPF.LUMINANCE: if bitcount == 8: @@ -419,6 +418,14 @@ def _open(self) -> None: self._mode = "RGBA" self.pixel_format = "BC1" n = 1 + elif dxgi_format in (DXGI_FORMAT.BC2_TYPELESS, DXGI_FORMAT.BC2_UNORM): + self._mode = "RGBA" + self.pixel_format = "BC2" + n = 2 + elif dxgi_format in (DXGI_FORMAT.BC3_TYPELESS, DXGI_FORMAT.BC3_UNORM): + self._mode = "RGBA" + self.pixel_format = "BC3" + n = 3 elif dxgi_format in (DXGI_FORMAT.BC4_TYPELESS, DXGI_FORMAT.BC4_UNORM): self._mode = "L" self.pixel_format = "BC4" @@ -481,7 +488,7 @@ def load_seek(self, pos: int) -> None: class DdsRgbDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None bitcount, masks = self.args @@ -508,6 +515,8 @@ def decode(self, buffer: bytes) -> tuple[int, int]: # Remove the zero padding, and scale it to 8 bits data += o8( int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255) + if mask_totals[i] + else 0 ) self.set_as_raw(data) return -1, 0 @@ -518,30 +527,68 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: msg = f"cannot write mode {im.mode} as DDS" raise OSError(msg) - alpha = im.mode[-1] == "A" - if im.mode[0] == "L": - pixel_flags = DDPF.LUMINANCE - rawmode = im.mode - if alpha: - rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF] + flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT + bitcount = len(im.getbands()) * 8 + pixel_format = im.encoderinfo.get("pixel_format") + args: tuple[int] | str + if pixel_format: + codec_name = "bcn" + flags |= DDSD.LINEARSIZE + pitch = (im.width + 3) * 4 + rgba_mask = [0, 0, 0, 0] + pixel_flags = DDPF.FOURCC + if pixel_format == "DXT1": + fourcc = D3DFMT.DXT1 + args = (1,) + elif pixel_format == "DXT3": + fourcc = D3DFMT.DXT3 + args = (2,) + elif pixel_format == "DXT5": + fourcc = D3DFMT.DXT5 + args = (3,) else: - rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000] + fourcc = D3DFMT.DX10 + if pixel_format == "BC2": + args = (2,) + dxgi_format = DXGI_FORMAT.BC2_TYPELESS + elif pixel_format == "BC3": + args = (3,) + dxgi_format = DXGI_FORMAT.BC3_TYPELESS + elif pixel_format == "BC5": + args = (5,) + dxgi_format = DXGI_FORMAT.BC5_TYPELESS + if im.mode != "RGB": + msg = "only RGB mode can be written as BC5" + raise OSError(msg) + else: + msg = f"cannot write pixel format {pixel_format}" + raise OSError(msg) else: - pixel_flags = DDPF.RGB - rawmode = im.mode[::-1] - rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF] + codec_name = "raw" + flags |= DDSD.PITCH + pitch = (im.width * bitcount + 7) // 8 + + alpha = im.mode[-1] == "A" + if im.mode[0] == "L": + pixel_flags = DDPF.LUMINANCE + args = im.mode + if alpha: + rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF] + else: + rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000] + else: + pixel_flags = DDPF.RGB + args = im.mode[::-1] + rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF] + if alpha: + r, g, b, a = im.split() + im = Image.merge("RGBA", (a, r, g, b)) if alpha: - r, g, b, a = im.split() - im = Image.merge("RGBA", (a, r, g, b)) - if alpha: - pixel_flags |= DDPF.ALPHAPIXELS - rgba_mask.append(0xFF000000 if alpha else 0) - - flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PITCH | DDSD.PIXELFORMAT - bitcount = len(im.getbands()) * 8 - pitch = (im.width * bitcount + 7) // 8 + pixel_flags |= DDPF.ALPHAPIXELS + rgba_mask.append(0xFF000000 if alpha else 0) + fourcc = D3DFMT.UNKNOWN fp.write( o32(DDS_MAGIC) + struct.pack( @@ -556,17 +603,20 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: ) + struct.pack("11I", *((0,) * 11)) # reserved # pfsize, pfflags, fourcc, bitcount - + struct.pack("<4I", 32, pixel_flags, 0, bitcount) + + struct.pack("<4I", 32, pixel_flags, fourcc, bitcount) + struct.pack("<4I", *rgba_mask) # dwRGBABitMask + struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0) ) - ImageFile._save( - im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))] - ) + if fourcc == D3DFMT.DX10: + fp.write( + # dxgi_format, 2D resource, misc, array size, straight alpha + struct.pack("<5I", dxgi_format, 3, 0, 0, 1) + ) + ImageFile._save(im, fp, [ImageFile._Tile(codec_name, (0, 0) + im.size, 0, args)]) def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"DDS " + return prefix.startswith(b"DDS ") Image.register_open(DdsImageFile.format, DdsImageFile, _accept) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index f31b1c1a2d0..aeb7b0c93b3 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -31,7 +31,6 @@ from . import Image, ImageFile from ._binary import i32le as i32 -from ._deprecate import deprecate # -------------------------------------------------------------------- @@ -66,16 +65,24 @@ def has_ghostscript() -> bool: return gs_binary is not False -def Ghostscript(tile, size, fp, scale=1, transparency=False): +def Ghostscript( + tile: list[ImageFile._Tile], + size: tuple[int, int], + fp: IO[bytes], + scale: int = 1, + transparency: bool = False, +) -> Image.core.ImagingCore: """Render an image using Ghostscript""" global gs_binary if not has_ghostscript(): msg = "Unable to locate Ghostscript on paths" raise OSError(msg) + assert isinstance(gs_binary, str) # Unpack decoder tile - decoder, tile, offset, data = tile[0] - length, bbox = data + args = tile[0].args + assert isinstance(args, tuple) + length, bbox = args # Hack to support hi-res rendering scale = int(scale) or 1 @@ -114,7 +121,13 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): lengthfile -= len(s) f.write(s) - device = "pngalpha" if transparency else "ppmraw" + if transparency: + # "RGBA" + device = "pngalpha" + else: + # "pnmraw" automatically chooses between + # PBM ("1"), PGM ("L"), and PPM ("RGB"). + device = "pnmraw" # Build Ghostscript command command = [ @@ -144,8 +157,9 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW subprocess.check_call(command, startupinfo=startupinfo) - out_im = Image.open(outfile) - out_im.load() + with Image.open(outfile) as out_im: + out_im.load() + return out_im.im.copy() finally: try: os.unlink(outfile) @@ -154,50 +168,11 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): except OSError: pass - im = out_im.im.copy() - out_im.close() - return im - - -class PSFile: - """ - Wrapper for bytesio object that treats either CR or LF as end of line. - This class is no longer used internally, but kept for backwards compatibility. - """ - - def __init__(self, fp): - deprecate( - "PSFile", - 11, - action="If you need the functionality of this class " - "you will need to implement it yourself.", - ) - self.fp = fp - self.char = None - - def seek(self, offset, whence=io.SEEK_SET): - self.char = None - self.fp.seek(offset, whence) - - def readline(self) -> str: - s = [self.char or b""] - self.char = None - - c = self.fp.read(1) - while (c not in b"\r\n") and len(c): - s.append(c) - c = self.fp.read(1) - - self.char = self.fp.read(1) - # line endings can be 1 or 2 of \r \n, in either order - if self.char in b"\r\n": - self.char = None - - return b"".join(s).decode("latin-1") - def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) + return prefix.startswith(b"%!PS") or ( + len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5 + ) ## @@ -214,13 +189,18 @@ class EpsImageFile(ImageFile.ImageFile): mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"} def _open(self) -> None: - (length, offset) = self._find_offset(self.fp) + assert self.fp is not None + length, offset = self._find_offset(self.fp) # go to offset - start of "%!PS" self.fp.seek(offset) self._mode = "RGB" - self._size = None + + # When reading header comments, the first comment is used. + # When reading trailer comments, the last comment is used. + bounding_box: list[int] | None = None + imagedata_size: tuple[int, int] | None = None byte_arr = bytearray(255) bytes_mv = memoryview(byte_arr) @@ -242,8 +222,8 @@ def check_required_header_comments() -> None: msg = 'EPS header missing "%%BoundingBox" comment' raise SyntaxError(msg) - def _read_comment(s: str) -> bool: - nonlocal reading_trailer_comments + def read_comment(s: str) -> bool: + nonlocal bounding_box, reading_trailer_comments try: m = split.match(s) except re.error as e: @@ -258,14 +238,12 @@ def _read_comment(s: str) -> bool: if k == "BoundingBox": if v == "(atend)": reading_trailer_comments = True - elif not self._size or (trailer_reached and reading_trailer_comments): + elif not bounding_box or (trailer_reached and reading_trailer_comments): try: # Note: The DSC spec says that BoundingBox # 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))] + bounding_box = [int(float(i)) for i in v.split()] except Exception: pass return True @@ -316,11 +294,11 @@ def _read_comment(s: str) -> bool: continue s = str(bytes_mv[:bytes_read], "latin-1") - if not _read_comment(s): + if not read_comment(s): m = field.match(s) if m: k = m.group(1) - if k[:8] == "PS-Adobe": + if k.startswith("PS-Adobe"): self.info["PS-Adobe"] = k[9:] else: self.info[k] = "" @@ -335,6 +313,12 @@ def _read_comment(s: str) -> bool: # Check for an "ImageData" descriptor # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096 + # If we've already read an "ImageData" descriptor, + # don't read another one. + if imagedata_size: + bytes_read = 0 + continue + # Values: # columns # rows @@ -360,23 +344,39 @@ def _read_comment(s: str) -> bool: else: break - self._size = columns, rows - return + # Parse the columns and rows after checking the bit depth and mode + # in case the bit depth and/or mode are invalid. + imagedata_size = columns, rows elif bytes_mv[:5] == b"%%EOF": break elif trailer_reached and reading_trailer_comments: # Load EPS trailer s = str(bytes_mv[:bytes_read], "latin-1") - _read_comment(s) + read_comment(s) elif bytes_mv[:9] == b"%%Trailer": trailer_reached = True + elif bytes_mv[:14] == b"%%BeginBinary:": + bytecount = int(byte_arr[14:bytes_read]) + self.fp.seek(bytecount, os.SEEK_CUR) bytes_read = 0 - if not self._size: + # A "BoundingBox" is always required, + # even if an "ImageData" descriptor size exists. + if not bounding_box: msg = "cannot determine EPS bounding box" raise OSError(msg) - def _find_offset(self, fp): + # An "ImageData" size takes precedence over the "BoundingBox". + self._size = imagedata_size or ( + bounding_box[2] - bounding_box[0], + bounding_box[3] - bounding_box[1], + ) + + self.tile = [ + ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box)) + ] + + def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]: s = fp.read(4) if s == b"%!PS": @@ -399,9 +399,12 @@ def _find_offset(self, fp): return length, offset - def load(self, scale=1, transparency=False): + def load( + self, scale: int = 1, transparency: bool = False + ) -> Image.core.PixelAccess | None: # Load EPS via Ghostscript if self.tile: + assert self.fp is not None self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency) self._mode = self.im.mode self._size = self.im.size @@ -458,7 +461,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) - if hasattr(fp, "flush"): fp.flush() - ImageFile._save(im, fp, [("eps", (0, 0) + im.size, 0, None)]) + ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)]) fp.write(b"\n%%%%EndBinary\n") fp.write(b"grestore end\n") diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 39b4aa55262..a9522e761b0 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -13,6 +13,7 @@ This module provides constants and clear-text names for various well-known EXIF tags. """ + from __future__ import annotations from enum import IntEnum @@ -288,6 +289,7 @@ class Base(IntEnum): OpcodeList2 = 0xC741 OpcodeList3 = 0xC74E NoiseProfile = 0xC761 + FrameRate = 0xC764 """Maps EXIF tags to tag names.""" @@ -303,38 +305,38 @@ class Base(IntEnum): 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 + GPSVersionID = 0x00 + GPSLatitudeRef = 0x01 + GPSLatitude = 0x02 + GPSLongitudeRef = 0x03 + GPSLongitude = 0x04 + GPSAltitudeRef = 0x05 + GPSAltitude = 0x06 + GPSTimeStamp = 0x07 + GPSSatellites = 0x08 + GPSStatus = 0x09 + GPSMeasureMode = 0x0A + GPSDOP = 0x0B + GPSSpeedRef = 0x0C + GPSSpeed = 0x0D + GPSTrackRef = 0x0E + GPSTrack = 0x0F + GPSImgDirectionRef = 0x10 + GPSImgDirection = 0x11 + GPSMapDatum = 0x12 + GPSDestLatitudeRef = 0x13 + GPSDestLatitude = 0x14 + GPSDestLongitudeRef = 0x15 + GPSDestLongitude = 0x16 + GPSDestBearingRef = 0x17 + GPSDestBearing = 0x18 + GPSDestDistanceRef = 0x19 + GPSDestDistance = 0x1A + GPSProcessingMethod = 0x1B + GPSAreaInformation = 0x1C + GPSDateStamp = 0x1D + GPSDifferential = 0x1E + GPSHPositioningError = 0x1F """Maps EXIF GPS tags to tag names.""" @@ -342,40 +344,41 @@ class GPS(IntEnum): class Interop(IntEnum): - InteropIndex = 1 - InteropVersion = 2 - RelatedImageFileFormat = 4096 - RelatedImageWidth = 4097 - RelatedImageHeight = 4098 + InteropIndex = 0x0001 + InteropVersion = 0x0002 + RelatedImageFileFormat = 0x1000 + RelatedImageWidth = 0x1001 + RelatedImageHeight = 0x1002 class IFD(IntEnum): - Exif = 34665 - GPSInfo = 34853 - Makernote = 37500 - Interop = 40965 + Exif = 0x8769 + GPSInfo = 0x8825 + MakerNote = 0x927C + Makernote = 0x927C # Deprecated + Interop = 0xA005 IFD1 = -1 class LightSource(IntEnum): - Unknown = 0 - Daylight = 1 - Fluorescent = 2 - Tungsten = 3 - Flash = 4 - Fine = 9 - Cloudy = 10 - Shade = 11 - DaylightFluorescent = 12 - DayWhiteFluorescent = 13 - CoolWhiteFluorescent = 14 - WhiteFluorescent = 15 - StandardLightA = 17 - StandardLightB = 18 - StandardLightC = 19 - D55 = 20 - D65 = 21 - D75 = 22 - D50 = 23 - ISO = 24 - Other = 255 + Unknown = 0x00 + Daylight = 0x01 + Fluorescent = 0x02 + Tungsten = 0x03 + Flash = 0x04 + Fine = 0x09 + Cloudy = 0x0A + Shade = 0x0B + DaylightFluorescent = 0x0C + DayWhiteFluorescent = 0x0D + CoolWhiteFluorescent = 0x0E + WhiteFluorescent = 0x0F + StandardLightA = 0x11 + StandardLightB = 0x12 + StandardLightC = 0x13 + D55 = 0x14 + D65 = 0x15 + D75 = 0x16 + D50 = 0x17 + ISO = 0x18 + Other = 0xFF diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 4846054b1e4..e918407784d 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -17,7 +17,7 @@ def _accept(prefix: bytes) -> bool: - return prefix[:6] == b"SIMPLE" + return prefix.startswith(b"SIMPLE") class FitsImageFile(ImageFile.ImageFile): @@ -67,7 +67,7 @@ def _open(self) -> None: raise ValueError(msg) offset += self.fp.tell() - 80 - self.tile = [(decoder_name, (0, 0) + self.size, offset, args)] + self.tile = [ImageFile._Tile(decoder_name, (0, 0) + self.size, offset, args)] def _get_size( self, headers: dict[bytes, bytes], prefix: bytes @@ -126,19 +126,20 @@ def _parse_headers( class FitsGzipDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None - value = gzip.decompress(self.fd.read()) - - rows = [] - offset = 0 - number_of_bits = min(self.args[0] // 8, 4) - for y in range(self.state.ysize): - row = bytearray() - for x in range(self.state.xsize): - row += value[offset + (4 - number_of_bits) : offset + 4] - offset += 4 - rows.append(row) + with gzip.open(self.fd) as fp: + value = fp.read(self.state.xsize * self.state.ysize * 4) + + rows = [] + offset = 0 + number_of_bits = min(self.args[0] // 8, 4) + for y in range(self.state.ysize): + row = bytearray() + for x in range(self.state.xsize): + row += value[offset + (4 - number_of_bits) : offset + 4] + offset += 4 + rows.append(row) self.set_as_raw(bytes([pixel for row in rows[::-1] for pixel in row])) return -1, 0 diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index dceb839279a..da1e8e95cf3 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -22,6 +22,7 @@ from ._binary import i16le as i16 from ._binary import i32le as i32 from ._binary import o8 +from ._util import DeferredError # # decoder @@ -29,7 +30,7 @@ def _accept(prefix: bytes) -> bool: return ( - len(prefix) >= 6 + len(prefix) >= 16 and i16(prefix, 4) in [0xAF11, 0xAF12] and i16(prefix, 14) in [0, 3] # flags ) @@ -45,10 +46,16 @@ class FliImageFile(ImageFile.ImageFile): format_description = "Autodesk FLI/FLC Animation" _close_exclusive_fp_after_loading = False - def _open(self): + def _open(self) -> None: # HEAD + assert self.fp is not None s = self.fp.read(128) - if not (_accept(s) and s[20:22] == b"\x00\x00"): + if not ( + _accept(s) + and s[20:22] == b"\x00" * 2 + and s[42:80] == b"\x00" * 38 + and s[88:] == b"\x00" * 40 + ): msg = "not an FLI/FLC file" raise SyntaxError(msg) @@ -76,14 +83,13 @@ def _open(self): if i16(s, 4) == 0xF100: # prefix chunk; ignore it - self.__offset = self.__offset + i32(s) - self.fp.seek(self.__offset) + self.fp.seek(self.__offset + i32(s)) s = self.fp.read(16) if i16(s, 4) == 0xF1FA: # look for palette chunk number_of_subchunks = i16(s, 6) - chunk_size = None + chunk_size: int | None = None for _ in range(number_of_subchunks): if chunk_size is not None: self.fp.seek(chunk_size - 6, os.SEEK_CUR) @@ -96,8 +102,9 @@ def _open(self): 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)) + self.palette = ImagePalette.raw( + "RGB", b"".join(o8(r) + o8(g) + o8(b) for (r, g, b) in palette) + ) # set things up to decode first frame self.__frame = -1 @@ -105,10 +112,11 @@ def _open(self): self.__rewind = self.fp.tell() self.seek(0) - def _palette(self, palette, shift): + def _palette(self, palette: list[tuple[int, int, int]], shift: int) -> None: # load palette i = 0 + assert self.fp is not None for e in range(i16(self.fp.read(2))): s = self.fp.read(2) i = i + s[0] @@ -133,6 +141,8 @@ def seek(self, frame: int) -> None: self._seek(f) def _seek(self, frame: int) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex if frame == 0: self.__frame = -1 self._fp.seek(self.__rewind) @@ -158,7 +168,7 @@ def _seek(self, frame: int) -> None: framesize = i32(s) self.decodermaxblock = framesize - self.tile = [("fli", (0, 0) + self.size, self.__offset, None)] + self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset)] self.__offset += framesize diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 1e0c1c166b5..341431d3f45 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -18,7 +18,7 @@ import os from typing import BinaryIO -from . import Image, _binary +from . import Image, ImageFont, _binary WIDTH = 800 @@ -110,6 +110,22 @@ def compile(self) -> None: self.bitmap.paste(im.crop(src), s) self.metrics[i] = d, dst, s + def _encode_metrics(self) -> bytes: + values: list[int] = [] + for i in range(256): + m = self.metrics[i] + if m: + values.extend(m[0] + m[1] + m[2]) + else: + values.extend((0,) * 10) + + data = bytearray() + for v in values: + if v < 0: + v += 65536 + data += _binary.o16be(v) + return bytes(data) + def save(self, filename: str) -> None: """Save font""" @@ -126,9 +142,18 @@ def save(self, filename: str) -> None: fp.write(b"PILfont\n") fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!! fp.write(b"DATA\n") - for id in range(256): - m = self.metrics[id] - if not m: - puti16(fp, (0,) * 10) - else: - puti16(fp, m[0] + m[1] + m[2]) + fp.write(self._encode_metrics()) + + def to_imagefont(self) -> ImageFont.ImageFont: + """Convert to ImageFont""" + + self.compile() + + # font data + if not self.bitmap: + msg = "No bitmap created" + raise ValueError(msg) + + imagefont = ImageFont.ImageFont() + imagefont._load(self.bitmap, self._encode_metrics()) + return imagefont diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index c1927bd26aa..0b06aac965f 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -42,7 +42,7 @@ def _accept(prefix: bytes) -> bool: - return prefix[:8] == olefile.MAGIC + return prefix.startswith(olefile.MAGIC) ## @@ -53,18 +53,20 @@ class FpxImageFile(ImageFile.ImageFile): format = "FPX" format_description = "FlashPix" - def _open(self): + def _open(self) -> None: # # read the OLE directory and see if this is a likely # to be a FlashPix file + assert self.fp is not None try: self.ole = olefile.OleFileIO(self.fp) except OSError as e: msg = "not an FPX file; invalid OLE file" raise SyntaxError(msg) from e - if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B": + root = self.ole.root + if not root or root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B": msg = "not an FPX file; bad root CLSID" raise SyntaxError(msg) @@ -80,6 +82,8 @@ def _open_index(self, index: int = 1) -> None: # size (highest resolution) + assert isinstance(prop[0x1000002], int) + assert isinstance(prop[0x1000003], int) self._size = prop[0x1000002], prop[0x1000003] size = max(self.size) @@ -99,8 +103,7 @@ def _open_index(self, index: int = 1) -> None: s = prop[0x2000002 | id] - bands = i32(s, 4) - if bands > 4: + if not isinstance(s, bytes) or (bands := i32(s, 4)) > 4: msg = "Invalid number of bands" raise OSError(msg) @@ -138,7 +141,7 @@ def _open_subimage(self, index: int = 1, subimage: int = 0) -> None: size = i32(s, 4), i32(s, 8) # tilecount = i32(s, 12) - tilesize = i32(s, 16), i32(s, 20) + xtile, ytile = i32(s, 16), i32(s, 20) # channels = i32(s, 24) offset = i32(s, 28) length = i32(s, 32) @@ -153,7 +156,6 @@ def _open_subimage(self, index: int = 1, subimage: int = 0) -> None: x = y = 0 xsize, ysize = size - xtile, ytile = tilesize self.tile = [] for i in range(0, len(s), length): @@ -164,18 +166,18 @@ def _open_subimage(self, index: int = 1, subimage: int = 0) -> None: if compression == 0: self.tile.append( - ( + ImageFile._Tile( "raw", (x, y, x1, y1), i32(s, i) + 28, - (self.rawmode,), + self.rawmode, ) ) elif compression == 1: # FIXME: the fill decoder is not implemented self.tile.append( - ( + ImageFile._Tile( "fill", (x, y, x1, y1), i32(s, i) + 28, @@ -203,7 +205,7 @@ def _open_subimage(self, index: int = 1, subimage: int = 0) -> None: jpegmode = rawmode self.tile.append( - ( + ImageFile._Tile( "jpeg", (x, y, x1, y1), i32(s, i) + 28, @@ -221,17 +223,18 @@ def _open_subimage(self, index: int = 1, subimage: int = 0) -> None: msg = "unknown/invalid compression" raise OSError(msg) - x = x + xtile + x += xtile if x >= xsize: x, y = 0, y + ytile if y >= ysize: break # isn't really required + assert self.fp is not None self.stream = stream self._fp = self.fp self.fp = None - def load(self): + def load(self) -> Image.core.PixelAccess | None: if not self.fp: self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"]) diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index 5acbb4912f7..e4d836cbdb2 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -72,6 +72,7 @@ class FtexImageFile(ImageFile.ImageFile): format_description = "Texture File Format (IW2:EOC)" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(4)): msg = "not an FTEX file" raise SyntaxError(msg) @@ -79,8 +80,6 @@ def _open(self) -> None: self._size = struct.unpack("<2i", self.fp.read(8)) mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8)) - self._mode = "RGB" - # Only support single-format files. # I don't know of any multi-format file. assert format_count == 1 @@ -93,9 +92,10 @@ def _open(self) -> None: if format == Format.DXT1: self._mode = "RGBA" - self.tile = [("bcn", (0, 0) + self.size, 0, 1)] + self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))] elif format == Format.UNCOMPRESSED: - self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))] + self._mode = "RGB" + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, "RGB")] else: msg = f"Invalid texture compression format: {repr(format)}" raise ValueError(msg) @@ -108,7 +108,7 @@ def load_seek(self, pos: int) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:4] == MAGIC + return prefix.startswith(MAGIC) Image.register_open(FtexImageFile.format, FtexImageFile, _accept) diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index 93e89b1e655..ec666c81c2c 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -42,6 +42,7 @@ class GbrImageFile(ImageFile.ImageFile): format_description = "GIMP brush file" def _open(self) -> None: + assert self.fp is not None header_size = i32(self.fp.read(4)) if header_size < 20: msg = "not a GIMP brush" @@ -54,7 +55,7 @@ def _open(self) -> None: width = i32(self.fp.read(4)) height = i32(self.fp.read(4)) color_depth = i32(self.fp.read(4)) - if width <= 0 or height <= 0: + if width == 0 or height == 0: msg = "not a GIMP brush" raise SyntaxError(msg) if color_depth not in (1, 4): @@ -71,7 +72,7 @@ def _open(self) -> None: raise SyntaxError(msg) self.info["spacing"] = i32(self.fp.read(4)) - comment = self.fp.read(comment_length)[:-1] + self.info["comment"] = self.fp.read(comment_length)[:-1] if color_depth == 1: self._mode = "L" @@ -80,16 +81,15 @@ def _open(self) -> None: self._size = width, height - self.info["comment"] = comment - # Image might not be small Image._decompression_bomb_check(self.size) # Data is an uncompressed block of w * h * bytes/pixel self._data_size = width * height * color_depth - def load(self): - if not self.im: + def load(self) -> Image.core.PixelAccess | None: + if self._im is None: + assert self.fp is not None self.im = Image.core.new(self.mode, self.size) self.frombytes(self.fp.read(self._data_size)) return Image.Image.load(self) diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 88b87a22cd6..d73bc1982f8 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -25,6 +25,7 @@ class is not registered for use with :py:func:`PIL.Image.open()`. To open a implementation is provided for convenience and demonstrational purposes only. """ + from __future__ import annotations from typing import IO @@ -56,7 +57,7 @@ def _open(self) -> None: msg = "Not a valid GD 2.x .gd file" raise SyntaxError(msg) - self._mode = "L" # FIXME: "P" + self._mode = "P" self._size = i16(s, 2), i16(s, 4) true_color = s[6] @@ -68,15 +69,15 @@ def _open(self) -> None: self.info["transparency"] = tindex self.palette = ImagePalette.raw( - "XBGR", s[7 + true_color_offset + 4 : 7 + true_color_offset + 4 + 256 * 4] + "RGBX", s[7 + true_color_offset + 6 : 7 + true_color_offset + 6 + 256 * 4] ) self.tile = [ - ( + ImageFile._Tile( "raw", (0, 0) + self.size, - 7 + true_color_offset + 4 + 256 * 4, - ("L", 0, 1), + 7 + true_color_offset + 6 + 256 * 4, + "L", ) ] diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 284128c773f..b8db5d83284 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -29,10 +29,9 @@ import math import os import subprocess -import sys from enum import IntEnum from functools import cached_property -from typing import IO, TYPE_CHECKING, Any, List, Literal, NamedTuple, Union +from typing import Any, NamedTuple, cast from . import ( Image, @@ -46,9 +45,14 @@ from ._binary import i16le as i16 from ._binary import o8 from ._binary import o16le as o16 +from ._util import DeferredError +TYPE_CHECKING = False if TYPE_CHECKING: + from typing import IO, Literal + from . import _imaging + from ._typing import Buffer class LoadingStrategy(IntEnum): @@ -67,7 +71,7 @@ class LoadingStrategy(IntEnum): def _accept(prefix: bytes) -> bool: - return prefix[:6] in [b"GIF87a", b"GIF89a"] + return prefix.startswith((b"GIF87a", b"GIF89a")) ## @@ -83,6 +87,7 @@ class GifImageFile(ImageFile.ImageFile): global_palette = None def data(self) -> bytes | None: + assert self.fp is not None s = self.fp.read(1) if s and s[0]: return self.fp.read(s[0]) @@ -96,6 +101,7 @@ def _is_palette_needed(self, p: bytes) -> bool: def _open(self) -> None: # Screen + assert self.fp is not None s = self.fp.read(13) if not _accept(s): msg = "not a GIF file" @@ -103,7 +109,6 @@ def _open(self) -> None: self.info["version"] = s[:6] self._size = i16(s, 6), i16(s, 8) - self.tile = [] flags = s[10] bits = (flags & 7) + 1 @@ -113,8 +118,8 @@ def _open(self) -> None: # check if palette contains colour indices p = self.fp.read(3 << bits) if self._is_palette_needed(p): - p = ImagePalette.raw("RGB", p) - self.global_palette = self.palette = p + palette = ImagePalette.raw("RGB", p) + self.global_palette = self.palette = palette self._fp = self.fp # FIXME: hack self.__rewind = self.fp.tell() @@ -155,19 +160,21 @@ def seek(self, frame: int) -> None: if not self._seek_check(frame): return if frame < self.__frame: - self.im = None + self._im = None self._seek(0) last_frame = self.__frame - for f in range(self.__frame + 1, frame + 1): - try: + try: + for f in range(self.__frame + 1, frame + 1): self._seek(f) - except EOFError as e: - self.seek(last_frame) - msg = "no more images in GIF file" - raise EOFError(msg) from e + except EOFError as e: + self.seek(last_frame) + msg = "no more images in GIF file" + raise EOFError(msg) from e def _seek(self, frame: int, update_image: bool = True) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex if frame == 0: # rewind self.__offset = 0 @@ -251,14 +258,14 @@ def _seek(self, frame: int, update_image: bool = True) -> None: info["comment"] += b"\n" + comment else: info["comment"] = comment - s = None + s = b"" continue elif s[0] == 255 and frame == 0 and block is not None: # # application extension # info["extension"] = block, self.fp.tell() - if block[:11] == b"NETSCAPE2.0": + if block.startswith(b"NETSCAPE2.0"): block = self.data() if block and len(block) >= 3 and block[0] == 1: self.info["loop"] = i16(block, 1) @@ -294,7 +301,7 @@ def _seek(self, frame: int, update_image: bool = True) -> None: bits = self.fp.read(1)[0] self.__offset = self.fp.tell() break - s = None + s = b"" if interlace is None: msg = "image not found in GIF frame" @@ -320,18 +327,20 @@ def _seek(self, frame: int, update_image: bool = True) -> None: else: self._mode = "L" - if not palette and self.global_palette: + if palette: + self.palette = palette + elif self.global_palette: from copy import copy - palette = copy(self.global_palette) - self.palette = palette + self.palette = copy(self.global_palette) + else: + self.palette = None 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) @@ -345,12 +354,15 @@ def _rgb(color: int) -> tuple[int, int, int]: if self._frame_palette: if color * 3 + 3 > len(self._frame_palette.palette): color = 0 - return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]) + return cast( + tuple[int, int, int], + tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]), + ) else: return (color, color, color) self.dispose = None - self.dispose_extent = frame_dispose_extent + self.dispose_extent: tuple[int, int, int, int] | None = frame_dispose_extent if self.dispose_extent and self.disposal_method >= 2: try: if self.disposal_method == 2: @@ -377,7 +389,7 @@ def _rgb(color: int) -> tuple[int, int, int]: self.dispose = Image.core.fill(dispose_mode, dispose_size, color) else: # replace with previous contents - if self.im is not None: + 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: @@ -405,7 +417,7 @@ def _rgb(color: int) -> tuple[int, int, int]: elif self.mode not in ("RGB", "RGBA"): transparency = frame_transparency self.tile = [ - ( + ImageFile._Tile( "gif", (x0, y0, x1, y1), self.__offset, @@ -435,7 +447,14 @@ def load_prepare(self) -> None: self.im = Image.core.fill("P", self.size, self._frame_transparency or 0) self.im.putpalette("RGB", *self._frame_palette.getdata()) else: - self.im = None + self._im = None + if not self._prev_im and self._im is not None and self.size != self.im.size: + expanded_im = Image.core.fill(self.im.mode, self.size) + if self._frame_palette: + expanded_im.putpalette("RGB", *self._frame_palette.getdata()) + expanded_im.paste(self.im, (0, 0) + self.im.size) + + self.im = expanded_im self._mode = temp_mode self._frame_palette = None @@ -453,9 +472,23 @@ def load_end(self) -> None: return if not self._prev_im: return + if self.size != self._prev_im.size: + if self._frame_transparency is not None: + expanded_im = Image.core.fill("RGBA", self.size) + else: + expanded_im = Image.core.fill("P", self.size) + expanded_im.putpalette("RGB", "RGB", self.im.getpalette()) + expanded_im = expanded_im.convert("RGB") + expanded_im.paste(self._prev_im, (0, 0) + self._prev_im.size) + + self._prev_im = expanded_im + assert self._prev_im is not None if self._frame_transparency is not None: - self.im.putpalettealpha(self._frame_transparency, 0) - frame_im = self.im.convert("RGBA") + if self.mode == "L": + frame_im = self.im.convert_transparent("LA", self._frame_transparency) + else: + self.im.putpalettealpha(self._frame_transparency, 0) + frame_im = self.im.convert("RGBA") else: frame_im = self.im.convert("RGB") @@ -464,7 +497,7 @@ def load_end(self) -> None: self.im = self._prev_im self._mode = self.im.mode - if frame_im.mode == "RGBA": + if frame_im.mode in ("LA", "RGBA"): self.im.paste(frame_im, self.dispose_extent, frame_im) else: self.im.paste(frame_im, self.dispose_extent) @@ -496,6 +529,7 @@ def _normalize_mode(im: Image.Image) -> Image.Image: return im if Image.getmodebase(im.mode) == "RGB": im = im.convert("P", palette=Image.Palette.ADAPTIVE) + assert im.palette is not None if im.palette.mode == "RGBA": for rgba in im.palette.colors: if rgba[3] == 0: @@ -505,7 +539,7 @@ def _normalize_mode(im: Image.Image) -> Image.Image: return im.convert("L") -_Palette = Union[bytes, bytearray, List[int], ImagePalette.ImagePalette] +_Palette = bytes | bytearray | list[int] | ImagePalette.ImagePalette def _normalize_palette( @@ -532,16 +566,18 @@ def _normalize_palette( if im.mode == "P": if not source_palette: - source_palette = im.im.getpalette("RGB")[:768] + im_palette = im.getpalette(None) + assert im_palette is not None + source_palette = bytearray(im_palette) 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) + assert source_palette is not None - used_palette_colors: list[int] | None if palette: - used_palette_colors = [] - assert source_palette is not None + used_palette_colors: list[int | None] = [] + assert im.palette is not None for i in range(0, len(source_palette), 3): source_color = tuple(source_palette[i : i + 3]) index = im.palette.colors.get(source_color) @@ -554,20 +590,25 @@ def _normalize_palette( if j not in used_palette_colors: used_palette_colors[i] = j break - im = im.remap_palette(used_palette_colors) + dest_map: list[int] = [] + for index in used_palette_colors: + assert index is not None + dest_map.append(index) + im = im.remap_palette(dest_map) else: - used_palette_colors = _get_optimize(im, info) - if used_palette_colors is not None: - im = im.remap_palette(used_palette_colors, source_palette) + optimized_palette_colors = _get_optimize(im, info) + if optimized_palette_colors is not None: + im = im.remap_palette(optimized_palette_colors, source_palette) if "transparency" in info: try: - info["transparency"] = used_palette_colors.index( + info["transparency"] = optimized_palette_colors.index( info["transparency"] ) except ValueError: del info["transparency"] return im + assert im.palette is not None im.palette.palette = source_palette return im @@ -579,7 +620,8 @@ def _write_single_frame( ) -> None: im_out = _normalize_mode(im) for k, v in im_out.info.items(): - im.encoderinfo.setdefault(k, v) + if isinstance(k, str): + im.encoderinfo.setdefault(k, v) im_out = _normalize_palette(im_out, palette, im.encoderinfo) for s in _get_global_header(im_out, im.encoderinfo): @@ -592,7 +634,9 @@ def _write_single_frame( _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, [ImageFile._Tile("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])] + ) fp.write(b"\0") # end of image data @@ -600,7 +644,10 @@ def _write_single_frame( def _getbbox( base_im: Image.Image, im_frame: Image.Image ) -> tuple[Image.Image, tuple[int, int, int, int] | None]: - if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im): + palette_bytes = [ + bytes(im.palette.palette) if im.palette else b"" for im in (base_im, im_frame) + ] + if palette_bytes[0] != palette_bytes[1]: im_frame = im_frame.convert("RGBA") base_im = base_im.convert("RGBA") delta = ImageChops.subtract_modulo(im_frame, base_im) @@ -631,7 +678,8 @@ def _write_multiple_frames( for k, v in im_frame.info.items(): if k == "transparency": continue - im.encoderinfo.setdefault(k, v) + if isinstance(k, str): + im.encoderinfo.setdefault(k, v) encoderinfo = im.encoderinfo.copy() if "transparency" in im_frame.info: @@ -655,16 +703,24 @@ def _write_multiple_frames( im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"] continue if im_frames[-1].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) - bbox = _getbbox(background_im, im_frame)[1] + # To appear correctly in viewers using a convention, + # only consider transparency, and not background color + color = im.encoderinfo.get( + "transparency", im.info.get("transparency") + ) + if color is not None: + if background_im is None: + background = _get_background(im_frame, color) + background_im = Image.new("P", im_frame.size, background) + first_palette = im_frames[0].im.palette + assert first_palette is not None + background_im.putpalette(first_palette, first_palette.mode) + bbox = _getbbox(background_im, im_frame)[1] + else: + bbox = (0, 0) + im_frame.size elif encoderinfo.get("optimize") and im_frame.mode != "1": if "transparency" not in encoderinfo: + assert im_frame.palette is not None try: encoderinfo["transparency"] = ( im_frame.palette._new_color_index(im_frame) @@ -697,7 +753,7 @@ def _write_multiple_frames( if delta.mode == "P": # Convert to L without considering palette delta_l = Image.new("L", delta.size) - delta_l.putdata(delta.getdata()) + delta_l.putdata(delta.get_flattened_data()) delta = delta_l mask = ImageMath.lambda_eval( lambda args: args["convert"](args["im"] * 255, "1"), @@ -727,7 +783,8 @@ def _write_multiple_frames( if not palette: frame_data.encoderinfo["include_color_table"] = True - im_frame = im_frame.crop(frame_data.bbox) + if frame_data.bbox != (0, 0) + im_frame.size: + 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 @@ -880,7 +937,13 @@ def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None: :param info: encoderinfo :returns: list of indexes of palette entries in use, or None """ - if im.mode in ("P", "L") and info and info.get("optimize"): + if ( + im.mode in ("P", "L") + and info + and info.get("optimize") + and im.width != 0 + and im.height != 0 + ): # Potentially expensive operation. # The palette saves 3 bytes per color not used, but palette @@ -902,6 +965,7 @@ def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None: if optimise or max(used_palette_colors) >= len(used_palette_colors): return used_palette_colors + assert im.palette is not None num_palette_colors = len(im.palette.palette) // Image.getmodebands( im.palette.mode ) @@ -951,7 +1015,13 @@ def _get_palette_bytes(im: Image.Image) -> bytes: :param im: Image object :returns: Bytes, len<=768 suitable for inclusion in gif header """ - return im.palette.palette if im.palette else b"" + if not im.palette: + return b"" + + palette = bytes(im.palette.palette) + if im.palette.mode == "RGBA": + palette = b"".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3)) + return palette def _get_background( @@ -964,6 +1034,7 @@ def _get_background( # 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 + assert im.palette is not None try: background = im.palette.getcolor(info_background, im) except ValueError as e: @@ -1055,7 +1126,9 @@ def _write_frame_data( _write_local_header(fp, im_frame, offset, 0) ImageFile._save( - im_frame, fp, [("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])] + im_frame, + fp, + [ImageFile._Tile("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])], ) fp.write(b"\0") # end of image data @@ -1121,18 +1194,9 @@ def getdata( class Collector(BytesIO): data = [] - if sys.version_info >= (3, 12): - from collections.abc import Buffer - - def write(self, data: Buffer) -> int: - self.data.append(data) - return len(data) - - else: - - def write(self, data: Any) -> int: - self.data.append(data) - return len(data) + def write(self, data: Buffer) -> int: + self.data.append(data) + return len(data) im.load() # make sure raster data is available diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index 220eac57e38..fb958721882 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -18,13 +18,18 @@ the corresponding code in GIMP, written by Federico Mena Quintero. See the GIMP distribution for more information.) """ + from __future__ import annotations from math import log, pi, sin, sqrt -from typing import IO, Callable from ._binary import o8 +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from typing import IO + EPSILON = 1e-10 """""" # Enable auto-doc for data member @@ -116,7 +121,7 @@ class GimpGradientFile(GradientFile): """File handler for GIMP's gradient format.""" def __init__(self, fp: IO[bytes]) -> None: - if fp.readline()[:13] != b"GIMP Gradient": + if not fp.readline().startswith(b"GIMP Gradient"): msg = "not a GIMP gradient file" raise SyntaxError(msg) diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 4cad0ebeea1..016257d3dd2 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -16,9 +16,11 @@ from __future__ import annotations import re -from typing import IO +from io import BytesIO -from ._binary import o8 +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import IO class GimpPaletteFile: @@ -26,14 +28,18 @@ class GimpPaletteFile: rawmode = "RGB" - def __init__(self, fp: IO[bytes]) -> None: - palette = [o8(i) * 3 for i in range(256)] - - if fp.readline()[:12] != b"GIMP Palette": + def _read(self, fp: IO[bytes], limit: bool = True) -> None: + if not fp.readline().startswith(b"GIMP Palette"): msg = "not a GIMP palette file" raise SyntaxError(msg) - for i in range(256): + palette: list[int] = [] + i = 0 + while True: + if limit and i == 256 + 3: + break + + i += 1 s = fp.readline() if not s: break @@ -41,18 +47,29 @@ def __init__(self, fp: IO[bytes]) -> None: # skip fields and comment lines if re.match(rb"\w+:|#", s): continue - if len(s) > 100: + if limit and len(s) > 100: msg = "bad palette file" raise SyntaxError(msg) - v = tuple(map(int, s.split()[:3])) - if len(v) != 3: + v = s.split(maxsplit=3) + if len(v) < 3: msg = "bad palette entry" raise ValueError(msg) - palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) + palette += (int(v[i]) for i in range(3)) + if limit and len(palette) == 768: + break + + self.palette = bytes(palette) + + def __init__(self, fp: IO[bytes]) -> None: + self._read(fp) - self.palette = b"".join(palette) + @classmethod + def frombytes(cls, data: bytes) -> GimpPaletteFile: + self = cls.__new__(cls) + self._read(BytesIO(data), False) + return self def getpalette(self) -> tuple[bytes, str]: return self.palette, self.rawmode diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index e9aa084b281..3784ef2f134 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -10,6 +10,7 @@ # from __future__ import annotations +import os from typing import IO from . import Image, ImageFile @@ -32,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"GRIB" and prefix[7] == 1 + return len(prefix) >= 8 and prefix.startswith(b"GRIB") and prefix[7] == 1 class GribStubImageFile(ImageFile.StubImageFile): @@ -40,22 +41,17 @@ class GribStubImageFile(ImageFile.StubImageFile): format_description = "GRIB" def _open(self) -> None: - offset = self.fp.tell() - + assert self.fp is not None if not _accept(self.fp.read(8)): msg = "Not a GRIB file" raise SyntaxError(msg) - self.fp.seek(offset) + self.fp.seek(-8, os.SEEK_CUR) # make something up self._mode = "F" self._size = 1, 1 - loader = self._load() - if loader: - loader.open(self) - def _load(self) -> ImageFile.StubHandler | None: return _handler diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index cc9e73deb80..1a56660f7bd 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -10,6 +10,7 @@ # from __future__ import annotations +import os from typing import IO from . import Image, ImageFile @@ -32,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:8] == b"\x89HDF\r\n\x1a\n" + return prefix.startswith(b"\x89HDF\r\n\x1a\n") class HDF5StubImageFile(ImageFile.StubImageFile): @@ -40,22 +41,17 @@ class HDF5StubImageFile(ImageFile.StubImageFile): format_description = "HDF5" def _open(self) -> None: - offset = self.fp.tell() - + assert self.fp is not None if not _accept(self.fp.read(8)): msg = "Not an HDF file" raise SyntaxError(msg) - self.fp.seek(offset) + self.fp.seek(-8, os.SEEK_CUR) # make something up self._mode = "F" self._size = 1, 1 - loader = self._load() - if loader: - loader.open(self) - def _load(self) -> ImageFile.StubHandler | None: return _handler diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 2a89d498cbf..cb7a74c2e3b 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -34,13 +34,15 @@ HEADERSIZE = 8 -def nextheader(fobj): +def nextheader(fobj: IO[bytes]) -> tuple[bytes, int]: return struct.unpack(">4sI", fobj.read(HEADERSIZE)) -def read_32t(fobj, start_length, size): +def read_32t( + fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] +) -> dict[str, Image.Image]: # The 128x128 icon seems to have an extra header for some reason. - (start, length) = start_length + start, length = start_length fobj.seek(start) sig = fobj.read(4) if sig != b"\x00\x00\x00\x00": @@ -49,12 +51,14 @@ def read_32t(fobj, start_length, size): return read_32(fobj, (start + 4, length - 4), size) -def read_32(fobj, start_length, size): +def read_32( + fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] +) -> dict[str, Image.Image]: """ Read a 32bit RGB icon resource. Seems to be either uncompressed or an RLE packbits-like scheme. """ - (start, length) = start_length + start, length = start_length fobj.seek(start) pixel_size = (size[0] * size[2], size[1] * size[2]) sizesq = pixel_size[0] * pixel_size[1] @@ -72,14 +76,13 @@ def read_32(fobj, start_length, size): byte = fobj.read(1) if not byte: break - byte = byte[0] - if byte & 0x80: - blocksize = byte - 125 + byte_int = byte[0] + if byte_int & 0x80: + blocksize = byte_int - 125 byte = fobj.read(1) - for i in range(blocksize): - data.append(byte) + data.extend([byte] * blocksize) else: - blocksize = byte + 1 + blocksize = byte_int + 1 data.append(fobj.read(blocksize)) bytesleft -= blocksize if bytesleft <= 0: @@ -92,7 +95,9 @@ def read_32(fobj, start_length, size): return {"RGB": im} -def read_mk(fobj, start_length, size): +def read_mk( + fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] +) -> dict[str, Image.Image]: # Alpha masks seem to be uncompressed start = start_length[0] fobj.seek(start) @@ -102,18 +107,21 @@ def read_mk(fobj, start_length, size): return {"A": band} -def read_png_or_jpeg2000(fobj, start_length, size): - (start, length) = start_length +def read_png_or_jpeg2000( + fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] +) -> dict[str, Image.Image]: + start, length = start_length fobj.seek(start) sig = fobj.read(12) - if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a": + + im: Image.Image + if sig.startswith(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" + sig.startswith((b"\xff\x4f\xff\x51", b"\x0d\x0a\x87\x0a")) or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" ): if not enable_jpeg2k: @@ -164,12 +172,12 @@ class IcnsFile: ], } - def __init__(self, fobj): + def __init__(self, fobj: IO[bytes]) -> None: """ fobj is a file-like object as an icns resource """ # signature : (start, length) - self.dct = dct = {} + self.dct = {} self.fobj = fobj sig, filesize = nextheader(fobj) if not _accept(sig): @@ -183,11 +191,11 @@ def __init__(self, fobj): raise SyntaxError(msg) i += HEADERSIZE blocksize -= HEADERSIZE - dct[sig] = (i, blocksize) + self.dct[sig] = (i, blocksize) fobj.seek(blocksize, io.SEEK_CUR) i += blocksize - def itersizes(self): + def itersizes(self) -> list[tuple[int, int, int]]: sizes = [] for size, fmts in self.SIZES.items(): for fmt, reader in fmts: @@ -196,14 +204,14 @@ def itersizes(self): break return sizes - def bestsize(self): + def bestsize(self) -> tuple[int, int, int]: sizes = self.itersizes() if not sizes: msg = "No 32bit icon resources found" raise SyntaxError(msg) return max(sizes) - def dataforsize(self, size): + def dataforsize(self, size: tuple[int, int, int]) -> dict[str, Image.Image]: """ Get an icon resource as {channel: array}. Note that the arrays are bottom-up like windows bitmaps and will likely @@ -216,18 +224,20 @@ def dataforsize(self, size): dct.update(reader(self.fobj, desc, size)) return dct - def getimage(self, size=None): + def getimage( + self, size: tuple[int, int] | tuple[int, int, int] | None = None + ) -> Image.Image: if size is None: size = self.bestsize() - if len(size) == 2: + elif len(size) == 2: size = (size[0], size[1], 1) channels = self.dataforsize(size) - im = channels.get("RGBA", None) + im = channels.get("RGBA") if im: return im - im = channels.get("RGB").copy() + im = channels["RGB"].copy() try: im.putalpha(channels["A"]) except KeyError: @@ -254,6 +264,7 @@ class IcnsImageFile(ImageFile.ImageFile): format_description = "Mac OS icns resource" def _open(self) -> None: + assert self.fp is not None self.icns = IcnsFile(self.fp) self._mode = "RGBA" self.info["sizes"] = self.icns.itersizes() @@ -264,39 +275,30 @@ def _open(self) -> None: ) @property - def size(self): + def size(self) -> tuple[int, int]: 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"]: - msg = "This is not one of the allowed sizes of this image" - raise ValueError(msg) - 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], - ) + def size(self, value: tuple[int, int]) -> None: + # Check that a matching size exists, + # or that there is a scale that would create a size that matches + for size in self.info["sizes"]: + simple_size = size[0] * size[2], size[1] * size[2] + scale = simple_size[0] // value[0] + if simple_size[1] / value[1] == scale: + self._size = value + return + msg = "This is not one of the allowed sizes of this image" + raise ValueError(msg) + + def load(self, scale: int | None = None) -> Image.core.PixelAccess | None: + if scale is not None: + width, height = self.size[:2] + self.size = width * scale, height * scale + self.best_size = width, height, scale px = Image.Image.load(self) - if self.im is not None and self.im.size == self.size: + if self._im is not None and self.im.size == self.size: # Already loaded return px self.load_prepare() @@ -374,7 +376,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:4] == MAGIC + return prefix.startswith(MAGIC) Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept) diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 227fcf35cbb..8dd57ff858a 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -17,6 +17,20 @@ # . # https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki # +# Copyright 2008 Bryan Davis +# +# 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 +# +# https://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. + # Icon format references: # * https://en.wikipedia.org/wiki/ICO_(file_format) # * https://msdn.microsoft.com/en-us/library/ms997538.aspx @@ -25,7 +39,7 @@ import warnings from io import BytesIO from math import ceil, log -from typing import IO +from typing import IO, NamedTuple from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin from ._binary import i16le as i16 @@ -97,7 +111,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if bits != 32: and_mask = Image.new("1", size) ImageFile._save( - and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))] + and_mask, + image_io, + [ImageFile._Tile("raw", (0, 0) + size, 0, ("1", 0, -1))], ) else: frame.save(image_io, "png") @@ -116,11 +132,25 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:4] == _MAGIC + return prefix.startswith(_MAGIC) + + +class IconHeader(NamedTuple): + width: int + height: int + nb_color: int + reserved: int + planes: int + bpp: int + size: int + offset: int + dim: tuple[int, int] + square: int + color_depth: int class IcoFile: - def __init__(self, buf): + def __init__(self, buf: IO[bytes]) -> None: """ Parse image from file-like object containing ico file data """ @@ -141,55 +171,48 @@ def __init__(self, buf): for i in range(self.nb_items): s = buf.read(16) - icon_header = { - "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"): - 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 + width = s[0] or 256 + height = s[1] or 256 + + # No. of colors in image (0 if >=8bpp) + nb_color = s[2] + bpp = i16(s, 6) + icon_header = IconHeader( + width=width, + height=height, + nb_color=nb_color, + reserved=s[3], + planes=i16(s, 4), + bpp=i16(s, 6), + size=i32(s, 8), + offset=i32(s, 12), + dim=(width, height), + square=width * height, + # See Wikipedia notes about color depth. + # We need this just to differ images with equal sizes + color_depth=bpp or (nb_color != 0 and ceil(log(nb_color, 2))) or 256, ) - 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["square"], reverse=True) + self.entry = sorted(self.entry, key=lambda x: x.square, reverse=True) - def sizes(self): + def sizes(self) -> set[tuple[int, int]]: """ - Get a list of all available icon sizes and color depths. + Get a set 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): + def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int: for i, h in enumerate(self.entry): - if size == h["dim"] and (bpp is False or bpp == h["color_depth"]): + if size == h.dim and (bpp is False or bpp == h.color_depth): return i return 0 - def getimage(self, size, bpp=False): + def getimage(self, size: tuple[int, int], bpp: int | bool = False) -> Image.Image: """ Get an image from the icon """ @@ -202,9 +225,9 @@ def frame(self, idx: int) -> Image.Image: 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) im: Image.Image if data[:8] == PngImagePlugin._MAGIC: @@ -219,11 +242,10 @@ def frame(self, idx: int) -> Image.Image: # change tile dimension to only encompass XOR image 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 + im.tile[0] = ImageFile._Tile(d, (0, 0) + im.size, o, a) # figure out where AND mask image starts - bpp = header["bpp"] - if 32 == bpp: + if header.bpp == 32: # 32-bit color depth icon image allows semitransparent areas # PIL's DIB format ignores transparency bits, recover them. # The DIB is packed in BGRX byte order where X is the alpha @@ -235,13 +257,19 @@ def frame(self, idx: int) -> Image.Image: alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4] # 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 - ) + try: + mask = Image.frombuffer( + "L", # 8bpp + im.size, # (w, h) + alpha_bytes, # source chars + "raw", # raw decoder + ("L", 0, -1), # 8bpp inverted, unpadded, reversed + ) + except ValueError: + if ImageFile.LOAD_TRUNCATED_IMAGES: + mask = None + else: + raise else: # get AND image from end of bitmap w = im.size[0] @@ -253,25 +281,32 @@ def frame(self, idx: int) -> Image.Image: # padded row size * height / bits per char total_bytes = int((w * im.size[1]) / 8) - and_mask_offset = header["offset"] + header["size"] - total_bytes + 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 - ) + try: + 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 + ) + except ValueError: + if ImageFile.LOAD_TRUNCATED_IMAGES: + mask = None + else: + raise # 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.putalpha(mask) + if mask: + im = im.convert("RGBA") + im.putalpha(mask) return im @@ -305,31 +340,31 @@ class IcoImageFile(ImageFile.ImageFile): format_description = "Windows Icon" def _open(self) -> None: + assert self.fp is not None self.ico = IcoFile(self.fp) self.info["sizes"] = self.ico.sizes() - self.size = self.ico.entry[0]["dim"] + self.size = self.ico.entry[0].dim self.load() @property - def size(self): + def size(self) -> tuple[int, int]: return self._size @size.setter - def size(self, value): + def size(self, value: tuple[int, int]) -> None: if value not in self.info["sizes"]: msg = "This is not one of the allowed sizes of this image" raise ValueError(msg) self._size = value - def load(self): - if self.im is not None and self.im.size == self.size: + def load(self) -> Image.core.PixelAccess | None: + 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.pyaccess = None self._mode = im.mode if im.palette: self.palette = im.palette @@ -342,6 +377,7 @@ def load(self): self.info["sizes"] = set(sizes) self.size = im.size + return Image.Image.load(self) def load_seek(self, pos: int) -> None: # Flag the ImageFile.Parser so that it diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 2fb7ecd52a0..ef54f16e97e 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -31,6 +31,7 @@ from typing import IO, Any from . import Image, ImageFile, ImagePalette +from ._util import DeferredError # -------------------------------------------------------------------- # Standard tags @@ -124,6 +125,7 @@ def _open(self) -> None: # Quick rejection: if there's not an LF among the first # 100 bytes, this is (probably) not a text header. + assert self.fp is not None if b"\n" not in self.fp.read(100): msg = "not an IM file" raise SyntaxError(msg) @@ -145,7 +147,7 @@ def _open(self) -> None: 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 @@ -155,9 +157,9 @@ def _open(self) -> None: msg = "not an IM file" raise SyntaxError(msg) - if s[-2:] == b"\r\n": + if s.endswith(b"\r\n"): s = s[:-2] - elif s[-1:] == b"\n": + elif s.endswith(b"\n"): s = s[:-1] try: @@ -209,7 +211,7 @@ def _open(self) -> None: self._mode = self.info[MODE] # Skip forward to start of image data - while s and s[:1] != b"\x1A": + while s and not s.startswith(b"\x1a"): s = self.fp.read(1) if not s: msg = "File truncated" @@ -247,13 +249,17 @@ def _open(self) -> None: self._fp = self.fp # FIXME: hack - if self.rawmode[:2] == "F;": + if self.rawmode.startswith("F;"): # ifunc95 formats try: # 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 = [ + ImageFile._Tile( + "bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1) + ) + ] return except ValueError: pass @@ -263,13 +269,17 @@ def _open(self) -> None: # 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)), + ImageFile._Tile("raw", (0, 0) + self.size, offs, ("G", 0, -1)), + ImageFile._Tile("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)), + ImageFile._Tile( + "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 = [ + ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1)) + ] @property def n_frames(self) -> int: @@ -282,6 +292,8 @@ def is_animated(self) -> bool: def seek(self, frame: int) -> None: if not self._seek_check(frame): return + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.frame = frame @@ -295,7 +307,9 @@ def seek(self, frame: int) -> None: self.fp = self._fp - self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] + self.tile = [ + ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1)) + ] def tell(self) -> int: return self.frame @@ -347,7 +361,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 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"Image size (x*y): {im.size[0]}*{im.size[1]}\r\n".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") @@ -360,7 +374,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 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))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))] + ) # diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d41c06523f4..574980771f9 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -38,19 +38,9 @@ import sys import tempfile import warnings -from collections.abc import Callable, MutableMapping +from collections.abc import MutableMapping from enum import IntEnum -from types import ModuleType -from typing import ( - IO, - TYPE_CHECKING, - Any, - Literal, - Protocol, - Sequence, - Tuple, - cast, -) +from typing import IO, Protocol, cast # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -65,7 +55,6 @@ ) from ._binary import i32le, o32be, o32le from ._deprecate import deprecate -from ._typing import StrOrBytesPath, TypeGuard from ._util import DeferredError, is_path ElementTree: ModuleType | None @@ -74,6 +63,12 @@ except ImportError: ElementTree = None +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable, Iterator, Sequence + from types import ModuleType + from typing import Any, Literal + logger = logging.getLogger(__name__) @@ -108,7 +103,6 @@ class DecompressionBombError(Exception): raise ImportError(msg) except ImportError as v: - core = DeferredError.new(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 @@ -125,28 +119,6 @@ class DecompressionBombError(Exception): raise -USE_CFFI_ACCESS = False -cffi: ModuleType | None -try: - import cffi -except ImportError: - cffi = None - - -def isImageType(t: Any) -> TypeGuard[Image]: - """ - Checks if an object is an image object. - - .. warning:: - - This function is for internal use only. - - :param t: object to check if it's an image - :returns: True if the object is an image - """ - return hasattr(t, "im") - - # # Constants @@ -228,8 +200,15 @@ class Quantize(IntEnum): # -------------------------------------------------------------------- # Registries +TYPE_CHECKING = False if TYPE_CHECKING: - from . import ImageFile, PyAccess + import mmap + from xml.etree.ElementTree import Element + + from IPython.lib.pretty import PrettyPrinter + + from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin + from ._typing import CapsuleType, NumpyArray, StrOrBytesPath ID: list[str] = [] OPEN: dict[ str, @@ -251,9 +230,9 @@ class Quantize(IntEnum): _ENDIAN = "<" if sys.byteorder == "little" else ">" -def _conv_type_shape(im): +def _conv_type_shape(im: Image) -> tuple[tuple[int, ...], str]: m = ImageMode.getmode(im.mode) - shape = (im.height, im.width) + shape: tuple[int, ...] = (im.height, im.width) extra = len(m.bands) if extra != 1: shape += (extra,) @@ -344,10 +323,112 @@ def getmodebands(mode: str) -> int: _initialized = 0 +# Mapping from file extension to plugin module name for lazy importing +_EXTENSION_PLUGIN: dict[str, str] = { + # Common formats (preinit) + ".bmp": "BmpImagePlugin", + ".dib": "BmpImagePlugin", + ".gif": "GifImagePlugin", + ".jfif": "JpegImagePlugin", + ".jpe": "JpegImagePlugin", + ".jpg": "JpegImagePlugin", + ".jpeg": "JpegImagePlugin", + ".pbm": "PpmImagePlugin", + ".pgm": "PpmImagePlugin", + ".pnm": "PpmImagePlugin", + ".ppm": "PpmImagePlugin", + ".pfm": "PpmImagePlugin", + ".png": "PngImagePlugin", + ".apng": "PngImagePlugin", + # Less common formats (init) + ".avif": "AvifImagePlugin", + ".avifs": "AvifImagePlugin", + ".blp": "BlpImagePlugin", + ".bufr": "BufrStubImagePlugin", + ".cur": "CurImagePlugin", + ".dcx": "DcxImagePlugin", + ".dds": "DdsImagePlugin", + ".ps": "EpsImagePlugin", + ".eps": "EpsImagePlugin", + ".fit": "FitsImagePlugin", + ".fits": "FitsImagePlugin", + ".fli": "FliImagePlugin", + ".flc": "FliImagePlugin", + ".fpx": "FpxImagePlugin", + ".ftc": "FtexImagePlugin", + ".ftu": "FtexImagePlugin", + ".gbr": "GbrImagePlugin", + ".grib": "GribStubImagePlugin", + ".h5": "Hdf5StubImagePlugin", + ".hdf": "Hdf5StubImagePlugin", + ".icns": "IcnsImagePlugin", + ".ico": "IcoImagePlugin", + ".im": "ImImagePlugin", + ".iim": "IptcImagePlugin", + ".jp2": "Jpeg2KImagePlugin", + ".j2k": "Jpeg2KImagePlugin", + ".jpc": "Jpeg2KImagePlugin", + ".jpf": "Jpeg2KImagePlugin", + ".jpx": "Jpeg2KImagePlugin", + ".j2c": "Jpeg2KImagePlugin", + ".mic": "MicImagePlugin", + ".mpg": "MpegImagePlugin", + ".mpeg": "MpegImagePlugin", + ".mpo": "MpoImagePlugin", + ".msp": "MspImagePlugin", + ".palm": "PalmImagePlugin", + ".pcd": "PcdImagePlugin", + ".pcx": "PcxImagePlugin", + ".pdf": "PdfImagePlugin", + ".pxr": "PixarImagePlugin", + ".psd": "PsdImagePlugin", + ".qoi": "QoiImagePlugin", + ".bw": "SgiImagePlugin", + ".rgb": "SgiImagePlugin", + ".rgba": "SgiImagePlugin", + ".sgi": "SgiImagePlugin", + ".ras": "SunImagePlugin", + ".tga": "TgaImagePlugin", + ".icb": "TgaImagePlugin", + ".vda": "TgaImagePlugin", + ".vst": "TgaImagePlugin", + ".tif": "TiffImagePlugin", + ".tiff": "TiffImagePlugin", + ".webp": "WebPImagePlugin", + ".wmf": "WmfImagePlugin", + ".emf": "WmfImagePlugin", + ".xbm": "XbmImagePlugin", + ".xpm": "XpmImagePlugin", +} + + +def _import_plugin_for_extension(ext: str | bytes) -> bool: + """Import only the plugin needed for a specific file extension.""" + if not ext: + return False + + if isinstance(ext, bytes): + ext = ext.decode() + ext = ext.lower() + if ext in EXTENSION: + return True + + plugin = _EXTENSION_PLUGIN.get(ext) + if plugin is None: + return False + + try: + logger.debug("Importing %s", plugin) + __import__(f"{__spec__.parent}.{plugin}", globals(), locals(), []) + return True + except ImportError as e: + logger.debug("Image: failed to import %s: %s", plugin, e) + return False + def preinit() -> None: """ - Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers. + Explicitly loads BMP, GIF, JPEG, PPM and PNG file format drivers. It is called when opening or saving images. """ @@ -403,12 +484,11 @@ def init() -> bool: if _initialized >= 2: return False - parent_name = __name__.rpartition(".")[0] for plugin in _plugins: try: logger.debug("Importing %s", plugin) - __import__(f"{parent_name}.{plugin}", globals(), locals(), []) - except ImportError as e: + __import__(f"{__spec__.parent}.{plugin}", globals(), locals(), []) + except ImportError as e: # noqa: PERF203 logger.debug("Image: failed to import %s: %s", plugin, e) if OPEN or SAVE: @@ -475,43 +555,53 @@ def _getencoder( # Simple expression analyzer -class _E: - def __init__(self, scale, offset) -> None: +class ImagePointTransform: + """ + Used with :py:meth:`~PIL.Image.Image.point` for single band images with more than + 8 bits, this represents an affine transformation, where the value is multiplied by + ``scale`` and ``offset`` is added. + """ + + def __init__(self, scale: float, offset: float) -> None: self.scale = scale self.offset = offset - def __neg__(self): - return _E(-self.scale, -self.offset) + def __neg__(self) -> ImagePointTransform: + return ImagePointTransform(-self.scale, -self.offset) - def __add__(self, other): - if isinstance(other, _E): - return _E(self.scale + other.scale, self.offset + other.offset) - return _E(self.scale, self.offset + other) + def __add__(self, other: ImagePointTransform | float) -> ImagePointTransform: + if isinstance(other, ImagePointTransform): + return ImagePointTransform( + self.scale + other.scale, self.offset + other.offset + ) + return ImagePointTransform(self.scale, self.offset + other) __radd__ = __add__ - def __sub__(self, other): + def __sub__(self, other: ImagePointTransform | float) -> ImagePointTransform: return self + -other - def __rsub__(self, other): + def __rsub__(self, other: ImagePointTransform | float) -> ImagePointTransform: return other + -self - def __mul__(self, other): - if isinstance(other, _E): + def __mul__(self, other: ImagePointTransform | float) -> ImagePointTransform: + if isinstance(other, ImagePointTransform): return NotImplemented - return _E(self.scale * other, self.offset * other) + return ImagePointTransform(self.scale * other, self.offset * other) __rmul__ = __mul__ - def __truediv__(self, other): - if isinstance(other, _E): + def __truediv__(self, other: ImagePointTransform | float) -> ImagePointTransform: + if isinstance(other, ImagePointTransform): return NotImplemented - return _E(self.scale / other, self.offset / other) + return ImagePointTransform(self.scale / other, self.offset / other) -def _getscaleoffset(expr): - a = expr(_E(1, 0)) - return (a.scale, a.offset) if isinstance(a, _E) else (0, a) +def _getscaleoffset( + expr: Callable[[ImagePointTransform], ImagePointTransform | float], +) -> tuple[float, float]: + a = expr(ImagePointTransform(1, 0)) + return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a) # -------------------------------------------------------------------- @@ -540,17 +630,26 @@ class Image: format_description: str | None = None _close_exclusive_fp_after_loading = True - def __init__(self): + def __init__(self) -> None: # FIXME: take "new" parameters / other image? - # FIXME: turn mode and size into delegating properties? - self.im = None + self._im: core.ImagingCore | DeferredError | None = None self._mode = "" self._size = (0, 0) - self.palette = None - self.info = {} + self.palette: ImagePalette.ImagePalette | None = None + self.info: dict[str | tuple[int, int], Any] = {} self.readonly = 0 - self.pyaccess = None - self._exif = None + self._exif: Exif | None = None + + @property + def im(self) -> core.ImagingCore: + if isinstance(self._im, DeferredError): + raise self._im.ex + assert self._im is not None + return self._im + + @im.setter + def im(self, im: core.ImagingCore) -> None: + self._im = im @property def width(self) -> int: @@ -568,6 +667,14 @@ def size(self) -> tuple[int, int]: def mode(self) -> str: return self._mode + @property + def readonly(self) -> int: + return (self._im and self._im.readonly) or self._readonly + + @readonly.setter + def readonly(self, readonly: int) -> None: + self._readonly = readonly + def _new(self, im: core.ImagingCore) -> Image: new = Image() new.im = im @@ -584,27 +691,14 @@ def _new(self, im: core.ImagingCore) -> Image: return new # Context manager support - def __enter__(self): + def __enter__(self) -> Image: return self - def _close_fp(self): - 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() - - def __exit__(self, *args): - if hasattr(self, "fp"): - if getattr(self, "_exclusive_fp", False): - self._close_fp() - self.fp = None + def __exit__(self, *args: object) -> None: + pass def close(self) -> None: """ - Closes the file pointer, if possible. - This operation will destroy the image core and release its memory. The image data will be unusable afterward. @@ -613,25 +707,19 @@ def close(self) -> None: :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for more information. """ - if hasattr(self, "fp"): - try: - self._close_fp() - self.fp = None - except Exception as msg: - logger.debug("Error closing: %s", msg) - if getattr(self, "map", None): - self.map = None + if sys.platform == "win32" and hasattr(sys, "pypy_version_info"): + self.map.close() + self.map: mmap.mmap | None = 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 = DeferredError(ValueError("Operation on closed image")) + self._im = DeferredError(ValueError("Operation on closed image")) def _copy(self) -> None: self.load() self.im = self.im.copy() - self.pyaccess = None self.readonly = 0 def _ensure_mutable(self) -> None: @@ -677,29 +765,20 @@ def __eq__(self, other: object) -> bool: ) def __repr__(self) -> str: - 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), + return ( + f"<{self.__class__.__module__}.{self.__class__.__name__} " + f"image mode={self.mode} size={self.size[0]}x{self.size[1]} " + f"at 0x{id(self):X}>" ) - def _repr_pretty_(self, p, cycle) -> None: + def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: """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], - ) + f"<{self.__class__.__module__}.{self.__class__.__name__} " + f"image mode={self.mode} size={self.size[0]}x{self.size[1]}>" ) def _repr_image(self, image_format: str, **kwargs: Any) -> bytes | None: @@ -730,37 +809,35 @@ def _repr_jpeg_(self) -> bytes | None: return self._repr_image("JPEG") @property - def __array_interface__(self): + def __array_interface__(self) -> dict[str, str | bytes | int | tuple[int, ...]]: # numpy array interface support - 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(str(e)) - raise + new: dict[str, str | bytes | int | tuple[int, ...]] = {"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"], new["typestr"] = _conv_type_shape(self) return new - def __getstate__(self): + def __arrow_c_schema__(self) -> object: + self.load() + return self.im.__arrow_c_schema__() + + def __arrow_c_array__( + self, requested_schema: object | None = None + ) -> tuple[object, object]: + self.load() + return (self.im.__arrow_c_schema__(), self.im.__arrow_c_array__()) + + def __getstate__(self) -> list[Any]: im_data = self.tobytes() # load image first return [self.info, self.mode, self.size, self.getpalette(), im_data] - def __setstate__(self, state) -> None: + def __setstate__(self, state: list[Any]) -> None: Image.__init__(self) - info, mode, size, palette, data = state + info, mode, size, palette, data = state[:5] self.info = info self._mode = mode self._size = size @@ -775,18 +852,20 @@ def tobytes(self, encoder_name: str = "raw", *args: Any) -> bytes: .. warning:: - This method returns the raw image data from the internal - storage. For compressed image data (e.g. PNG, JPEG) use - :meth:`~.save`, with a BytesIO parameter for in-memory - data. + This method returns raw image data derived from Pillow's internal + storage. For compressed image data (e.g. PNG, JPEG) use + :meth:`~.save`, with a BytesIO parameter for in-memory data. + + :param encoder_name: What encoder to use. - :param encoder_name: What encoder to use. The default is to - use the standard "raw" encoder. + The default is to use the standard "raw" encoder. + To see how this packs pixel data into the returned + bytes, see :file:`libImaging/Pack.c`. - 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. + 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. :returns: A :py:class:`bytes` object. """ @@ -806,9 +885,11 @@ def tobytes(self, encoder_name: str = "raw", *args: Any) -> bytes: # unpack data e = _getencoder(self.mode, encoder_name, encoder_args) - e.setimage(self.im) + e.setimage(self.im, (0, 0) + self.size) - bufsize = max(65536, self.size[0] * 4) # see RawEncode.c + from . import ImageFile + + bufsize = max(ImageFile.MAXBLOCK, self.size[0] * 4) # see RawEncode.c output = [] while True: @@ -849,7 +930,10 @@ def tobitmap(self, name: str = "image") -> bytes: ) def frombytes( - self, data: bytes | bytearray, decoder_name: str = "raw", *args: Any + self, + data: bytes | bytearray | SupportsArrayInterface, + decoder_name: str = "raw", + *args: Any, ) -> None: """ Loads this image with pixel data from a bytes object. @@ -872,7 +956,7 @@ def frombytes( # unpack data d = _getdecoder(self.mode, decoder_name, decoder_args) - d.setimage(self.im) + d.setimage(self.im, (0, 0) + self.size) s = d.decode(data) if s[0] >= 0: @@ -882,7 +966,7 @@ def frombytes( msg = "cannot decode image data" raise ValueError(msg) - def load(self) -> core.PixelAccess | PyAccess.PyAccess | None: + def load(self) -> core.PixelAccess | None: """ Allocates storage for the image and loads the pixel data. In normal cases, you don't need to call this method, since the @@ -895,9 +979,9 @@ def load(self) -> core.PixelAccess | PyAccess.PyAccess | None: operations. See :ref:`file-handling` for more information. :returns: An image access object. - :rtype: :py:class:`.PixelAccess` or :py:class:`.PyAccess` + :rtype: :py:class:`.PixelAccess` """ - if self.im is not None and self.palette and self.palette.dirty: + if self._im is not None and self.palette and self.palette.dirty: # realize palette mode, arr = self.palette.getdata() self.im.putpalette(self.palette.mode, mode, arr) @@ -909,20 +993,14 @@ def load(self) -> core.PixelAccess | PyAccess.PyAccess | None: else: self.im.putpalettealphas(self.info["transparency"]) self.palette.mode = "RGBA" - else: + elif self.palette.mode != mode: + # If the palette rawmode is different to the mode, + # then update the Python palette data self.palette.palette = self.im.getpalette( self.palette.mode, self.palette.mode ) - 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 + if self._im is not None: return self.im.pixel_access(self.readonly) return None @@ -989,9 +1067,6 @@ def convert( :returns: An :py:class:`~PIL.Image.Image` object. """ - if mode in ("BGR;15", "BGR;16", "BGR;24"): - deprecate(mode, 12) - self.load() has_transparency = "transparency" in self.info @@ -1027,13 +1102,19 @@ def convert_transparency( elif len(mode) == 3: transparency = tuple( convert_transparency(matrix[i * 4 : i * 4 + 4], transparency) - for i in range(0, len(transparency)) + for i in range(len(transparency)) ) new_im.info["transparency"] = transparency return new_im - if mode == "P" and self.mode == "RGBA": - return self.quantize(colors) + if self.mode == "RGBA": + if mode == "P": + return self.quantize(colors) + elif mode == "PA": + r, g, b, a = self.split() + rgb = merge("RGB", (r, g, b)) + p = rgb.quantize(colors) + return merge("PA", (p, a)) trns = None delete_trns = False @@ -1063,9 +1144,11 @@ def convert_transparency( # use existing conversions trns_im = new(self.mode, (1, 1)) if self.mode == "P": - trns_im.putpalette(self.palette) + assert self.palette is not None + trns_im.putpalette(self.palette, self.palette.mode) if isinstance(t, tuple): err = "Couldn't allocate a palette color for transparency" + assert trns_im.palette is not None try: t = trns_im.palette.getcolor(t, self) except ValueError as e: @@ -1115,7 +1198,7 @@ def convert_transparency( if trns is not None: try: new_im.info["transparency"] = new_im.palette.getcolor( - cast(Tuple[int, ...], trns), # trns was converted to RGB + cast(tuple[int, ...], trns), # trns was converted to RGB new_im, ) except Exception: @@ -1126,17 +1209,23 @@ def convert_transparency( return new_im if "LAB" in (self.mode, mode): - other_mode = mode if self.mode == "LAB" else self.mode + im = self + if mode == "LAB": + if im.mode not in ("RGB", "RGBA", "RGBX"): + im = im.convert("RGBA") + other_mode = im.mode + else: + other_mode = 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] + profiles = [lab, srgb] if im.mode == "LAB" else [srgb, lab] transform = ImageCms.buildTransform( - profiles[0], profiles[1], self.mode, mode + profiles[0], profiles[1], im.mode, mode ) - return transform.apply(self) + return transform.apply(im) # colorspace conversion if dither is None: @@ -1157,7 +1246,7 @@ def convert_transparency( raise ValueError(msg) from e new_im = self._new(im) - if mode == "P" and palette != Palette.ADAPTIVE: + if mode in ("P", "PA") and palette != Palette.ADAPTIVE: from . import ImagePalette new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB")) @@ -1167,7 +1256,9 @@ def convert_transparency( if trns is not None: if new_im.mode == "P" and new_im.palette: try: - new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im) + new_im.info["transparency"] = new_im.palette.getcolor( + cast(tuple[int, ...], trns), new_im # trns was converted to RGB + ) except ValueError as e: del new_im.info["transparency"] if str(e) != "cannot allocate more than 256 colors": @@ -1185,7 +1276,7 @@ def quantize( colors: int = 256, method: int | None = None, kmeans: int = 0, - palette=None, + palette: Image | None = None, dither: Dither = Dither.FLOYDSTEINBERG, ) -> Image: """ @@ -1245,6 +1336,7 @@ def quantize( raise ValueError(msg) im = self.im.convert("P", dither, palette.im) new_im = self._new(im) + assert palette.palette is not None new_im.palette = palette.palette.copy() return new_im @@ -1257,8 +1349,8 @@ def quantize( from . import ImagePalette mode = im.im.getpalettemode() - palette = im.im.getpalette(mode, mode)[: colors * len(mode)] - im.palette = ImagePalette.ImagePalette(mode, palette) + palette_data = im.im.getpalette(mode, mode)[: colors * len(mode)] + im.palette = ImagePalette.ImagePalette(mode, palette_data) return im @@ -1348,15 +1440,6 @@ def draft( """ pass - def _expand(self, xmargin: int, ymargin: int | None = None) -> Image: - if ymargin is None: - ymargin = xmargin - self.load() - return self._new(self.im.expand(xmargin, ymargin)) - - if TYPE_CHECKING: - from . import ImageFilter - def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image: """ Filters this image using the given filter. For a list of @@ -1413,7 +1496,9 @@ def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int] | Non self.load() return self.im.getbbox(alpha_only) - def getcolors(self, maxcolors: int = 256): + def getcolors( + self, maxcolors: int = 256 + ) -> list[tuple[int, tuple[int, ...]]] | list[tuple[int, float]] | None: """ Returns a list of colors used in this image. @@ -1430,13 +1515,13 @@ def getcolors(self, maxcolors: int = 256): self.load() if self.mode in ("1", "L", "P"): h = self.im.histogram() - out = [(h[i], i) for i in range(256) if h[i]] + out: list[tuple[int, float]] = [(h[i], i) for i in range(256) if h[i]] if len(out) > maxcolors: return None return out return self.im.getcolors(maxcolors) - def getdata(self, band: int | None = None): + def getdata(self, band: int | None = None) -> core.ImagingCore: """ Returns the contents of this image as a sequence object containing pixel values. The sequence object is flattened, so @@ -1453,12 +1538,31 @@ def getdata(self, band: int | None = None): value (e.g. 0 to get the "R" band from an "RGB" image). :returns: A sequence-like object. """ + deprecate("Image.Image.getdata", 14, "get_flattened_data") self.load() if band is not None: return self.im.getband(band) return self.im # could be abused + def get_flattened_data( + self, band: int | None = None + ) -> tuple[tuple[int, ...], ...] | tuple[float, ...]: + """ + Returns the contents of this image as a tuple containing pixel values. + The sequence object is flattened, so that values for line one follow + directly after the values of line zero, and so on. + + :param band: What band to return. The default is to return + all bands. To return a single band, pass in the index + value (e.g. 0 to get the "R" band from an "RGB" image). + :returns: A tuple containing pixel values. + """ + self.load() + if band is not None: + return tuple(self.im.getband(band)) + return tuple(self.im) + def getextrema(self) -> tuple[float, float] | tuple[tuple[int, int], ...]: """ Gets the minimum and maximum pixel values for each band in @@ -1474,7 +1578,7 @@ def getextrema(self) -> tuple[float, float] | tuple[tuple[int, int], ...]: return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands)) return self.im.getextrema() - def getxmp(self): + def getxmp(self) -> dict[str, Any]: """ Returns a dictionary containing the XMP tags. Requires defusedxml to be installed. @@ -1485,8 +1589,8 @@ def getxmp(self): def get_name(tag: str) -> str: return re.sub("^{[^}]+}", "", tag) - def get_value(element): - value = {get_name(k): v for k, v in element.attrib.items()} + def get_value(element: Element) -> str | dict[str, Any] | None: + value: dict[str, Any] = {get_name(k): v for k, v in element.attrib.items()} children = list(element) if children: for child in children: @@ -1510,7 +1614,7 @@ def get_value(element): return {} if "xmp" not in self.info: return {} - root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00")) + root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00 ")) return {get_name(root.tag): get_value(root)} def getexif(self) -> Exif: @@ -1532,8 +1636,13 @@ def getexif(self) -> Exif: "".join(self.info["Raw profile type exif"].split("\n")[3:]) ) elif hasattr(self, "tag_v2"): + from . import TiffImagePlugin + + assert isinstance(self, TiffImagePlugin.TiffImageFile) self._exif.bigtiff = self.tag_v2._bigtiff self._exif.endian = self.tag_v2._endian + + assert self.fp is not None self._exif.load_from_fp(self.fp, self.tag_v2._offset) if exif_info is not None: self._exif.load(exif_info) @@ -1541,8 +1650,11 @@ def getexif(self) -> Exif: # XMP tags if ExifTags.Base.Orientation not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") + pattern: str | bytes = r'tiff:Orientation(="|>)([0-9])' + if not xmp_tags and (xmp_tags := self.info.get("xmp")): + pattern = rb'tiff:Orientation(="|>)([0-9])' if xmp_tags: - match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) + match = re.search(pattern, xmp_tags) if match: self._exif[ExifTags.Base.Orientation] = int(match[2]) @@ -1555,50 +1667,12 @@ def _reload_exif(self) -> None: self.getexif() def get_child_images(self) -> list[ImageFile.ImageFile]: - child_images = [] - exif = self.getexif() - ifds = [] - if ExifTags.Base.SubIFDs in exif: - subifd_offsets = exif[ExifTags.Base.SubIFDs] - if subifd_offsets: - if not isinstance(subifd_offsets, tuple): - subifd_offsets = (subifd_offsets,) - for subifd_offset in subifd_offsets: - ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) - ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) - if ifd1 and ifd1.get(513): - ifds.append((ifd1, exif._info.next)) - - offset = None - for ifd, ifd_offset in ifds: - current_offset = self.fp.tell() - if offset is None: - offset = current_offset - - fp = self.fp - thumbnail_offset = ifd.get(513) - if thumbnail_offset is not None: - thumbnail_offset += getattr(self, "_exif_offset", 0) - self.fp.seek(thumbnail_offset) - data = self.fp.read(ifd.get(514)) - fp = io.BytesIO(data) - - with open(fp) as im: - from . import TiffImagePlugin + from . import ImageFile - if thumbnail_offset is None and isinstance( - im, TiffImagePlugin.TiffImageFile - ): - im._frame_pos = [ifd_offset] - im._seek(0) - im.load() - child_images.append(im) - - if offset is not None: - self.fp.seek(offset) - return child_images + deprecate("Image.Image.get_child_images", 13) + return ImageFile.ImageFile.get_child_images(self) # type: ignore[arg-type] - def getim(self): + def getim(self) -> CapsuleType: """ Returns a capsule that points to the internal image memory. @@ -1642,11 +1716,15 @@ def has_transparency_data(self) -> bool: :returns: A boolean. """ - return ( + if ( self.mode in ("LA", "La", "PA", "RGBA", "RGBa") - or (self.mode == "P" and self.palette.mode.endswith("A")) or "transparency" in self.info - ) + ): + return True + if self.mode == "P": + assert self.palette is not None + return self.palette.mode.endswith("A") + return False def apply_transparency(self) -> None: """ @@ -1685,8 +1763,6 @@ def getpixel( """ self.load() - if self.pyaccess: - return self.pyaccess.getpixel(xy) return self.im.getpixel(tuple(xy)) def getprojection(self) -> tuple[list[int], list[int]]: @@ -1701,7 +1777,9 @@ def getprojection(self) -> tuple[list[int], list[int]]: x, y = self.im.getprojection() return list(x), list(y) - def histogram(self, mask: Image | None = None, extrema=None) -> list[int]: + def histogram( + self, mask: Image | None = None, extrema: tuple[float, float] | None = None + ) -> list[int]: """ Returns a histogram for the image. The histogram is returned as a list of pixel counts, one for each pixel value in the source @@ -1727,12 +1805,14 @@ def histogram(self, mask: Image | None = None, extrema=None) -> list[int]: mask.load() return self.im.histogram((0, 0), mask.im) if self.mode in ("I", "F"): - if extrema is None: - extrema = self.getextrema() - return self.im.histogram(extrema) + return self.im.histogram( + extrema if extrema is not None else self.getextrema() + ) return self.im.histogram() - def entropy(self, mask=None, extrema=None): + def entropy( + self, mask: Image | None = None, extrema: tuple[float, float] | None = None + ) -> float: """ Calculates and returns the entropy for the image. @@ -1753,9 +1833,9 @@ def entropy(self, mask=None, extrema=None): 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( + extrema if extrema is not None else self.getextrema() + ) return self.im.entropy() def paste( @@ -1776,9 +1856,10 @@ def paste( details). Instead of an image, the source can be a integer or tuple - containing pixel values. The method then fills the region - with the given color. When creating RGB images, you can - also use color strings as supported by the ImageColor module. + containing pixel values. The method then fills the region + with the given color. When creating RGB images, you can + also use color strings as supported by the ImageColor module. See + :ref:`colors` for more information. If a mask is given, this method updates only the regions indicated by the mask. You can use either "1", "L", "LA", "RGBA" @@ -1803,23 +1884,22 @@ def paste( :param mask: An optional mask image. """ - if isImageType(box): + if isinstance(box, Image): if mask is not None: msg = "If using second argument as mask, third argument must be None" raise ValueError(msg) # abbreviated paste(im, mask) syntax mask = box box = None - assert not isinstance(box, Image) if box is None: box = (0, 0) if len(box) == 2: # upper left corner given; get size from image or mask - if isImageType(im): + if isinstance(im, Image): size = im.size - elif isImageType(mask): + elif isinstance(mask, Image): size = mask.size else: # FIXME: use self.size here? @@ -1827,26 +1907,28 @@ def paste( raise ValueError(msg) box += (box[0] + size[0], box[1] + size[1]) + source: core.ImagingCore | str | float | tuple[float, ...] if isinstance(im, str): from . import ImageColor - im = ImageColor.getcolor(im, self.mode) - - elif isImageType(im): + source = ImageColor.getcolor(im, self.mode) + elif isinstance(im, Image): im.load() if self.mode != im.mode: 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 + source = im.im + else: + source = im self._ensure_mutable() if mask: mask.load() - self.im.paste(im, box, mask.im) + self.im.paste(source, box, mask.im) else: - self.im.paste(im, box) + self.im.paste(source, box) def alpha_composite( self, im: Image, dest: Sequence[int] = (0, 0), source: Sequence[int] = (0, 0) @@ -1906,7 +1988,13 @@ def alpha_composite( def point( self, - lut: Sequence[float] | Callable[[int], float] | ImagePointHandler, + lut: ( + Sequence[float] + | NumpyArray + | Callable[[int], float] + | Callable[[ImagePointTransform], ImagePointTransform | float] + | ImagePointHandler + ), mode: str | None = None, ) -> Image: """ @@ -1923,7 +2011,7 @@ def point( object:: class Example(Image.ImagePointHandler): - def point(self, data): + def point(self, im: Image) -> Image: # Return result :param mode: Output mode (default is same as input). This can only be used if the source image has mode "L" or "P", and the output has mode "1" or the @@ -1942,10 +2030,10 @@ def point(self, data): # check if the function can be used with point_transform # UNDONE wiredfool -- I think this prevents us from ever doing # a gamma function point transform on > 8bit images. - scale, offset = _getscaleoffset(lut) + scale, offset = _getscaleoffset(lut) # type: ignore[arg-type] return self._new(self.im.point_transform(scale, offset)) # for other modes, convert the function to a table - flatLut = [lut(i) for i in range(256)] * self.im.bands + flatLut = [lut(i) for i in range(256)] * self.im.bands # type: ignore[arg-type] else: flatLut = lut @@ -1983,7 +2071,6 @@ def putalpha(self, alpha: Image | int) -> None: msg = "alpha channel could not be added" raise ValueError(msg) from e # sanity check self.im = im - self.pyaccess = None self._mode = self.im.mode except KeyError as e: msg = "illegal image mode" @@ -1994,7 +2081,7 @@ def putalpha(self, alpha: Image | int) -> None: else: band = 3 - if isImageType(alpha): + if isinstance(alpha, Image): # alpha layer if alpha.mode not in ("1", "L"): msg = "illegal image mode" @@ -2004,7 +2091,6 @@ def putalpha(self, alpha: Image | int) -> None: alpha = alpha.convert("L") else: # constant alpha - alpha = cast(int, alpha) # see python/typing#1013 try: self.im.fillband(band, alpha) except (AttributeError, ValueError): @@ -2017,7 +2103,7 @@ def putalpha(self, alpha: Image | int) -> None: def putdata( self, - data: Sequence[float] | Sequence[Sequence[int]], + data: Sequence[float] | Sequence[Sequence[int]] | core.ImagingCore | NumpyArray, scale: float = 1.0, offset: float = 0.0, ) -> None: @@ -2029,7 +2115,8 @@ def putdata( sequence ends. The scale and offset values are used to adjust the sequence values: **pixel = value*scale + offset**. - :param data: A flattened sequence object. + :param data: A flattened sequence object. See :ref:`colors` for more + information about values. :param scale: An optional scale value. The default is 1.0. :param offset: An optional offset value. The default is 0.0. """ @@ -2038,7 +2125,11 @@ def putdata( self.im.putdata(data, scale, offset) - def putpalette(self, data, rawmode="RGB") -> None: + def putpalette( + self, + data: ImagePalette.ImagePalette | bytes | Sequence[int], + rawmode: str = "RGB", + ) -> None: """ Attaches a palette to this image. The image must be a "P", "PA", "L" or "LA" image. @@ -2054,8 +2145,8 @@ def putpalette(self, data, rawmode="RGB") -> None: 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. Either "RGB", "RGBA", or a mode - that can be transformed to "RGB" or "RGBA" (e.g. "R", "BGR;15", "RGBA;L"). + :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", "CMYK", or a + mode that can be transformed to one of those modes (e.g. "R", "RGBA;L"). """ from . import ImagePalette @@ -2063,14 +2154,23 @@ def putpalette(self, data, rawmode="RGB") -> None: msg = "illegal image mode" raise ValueError(msg) if isinstance(data, ImagePalette.ImagePalette): - palette = ImagePalette.raw(data.rawmode, data.palette) + if data.rawmode is not None: + palette = ImagePalette.raw(data.rawmode, data.palette) + else: + palette = ImagePalette.ImagePalette(palette=data.palette) + palette.dirty = 1 else: if not isinstance(data, bytes): data = bytes(data) palette = ImagePalette.raw(rawmode, data) self._mode = "PA" if "A" in self.mode else "P" self.palette = palette - self.palette.mode = "RGBA" if "A" in rawmode else "RGB" + if rawmode.startswith("CMYK"): + self.palette.mode = "CMYK" + elif "A" in rawmode: + self.palette.mode = "RGBA" + else: + self.palette.mode = "RGB" self.load() # install new palette def putpixel( @@ -2080,7 +2180,7 @@ def putpixel( 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. In addition to this, RGB and RGBA tuples are - accepted for P and PA images. + accepted for P and PA images. See :ref:`colors` for more information. Note that this method is relatively slow. For more extensive changes, use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw` @@ -2097,12 +2197,7 @@ def putpixel( :param value: The pixel value. """ - if self.readonly: - self._copy() - self.load() - - if self.pyaccess: - return self.pyaccess.putpixel(xy, value) + self._ensure_mutable() if ( self.mode in ("P", "PA") @@ -2113,11 +2208,14 @@ def putpixel( if self.mode == "PA": alpha = value[3] if len(value) == 4 else 255 value = value[:3] - palette_index = self.palette.getcolor(value, self) + assert self.palette is not None + palette_index = self.palette.getcolor(tuple(value), self) value = (palette_index, alpha) if self.mode == "PA" else palette_index return self.im.putpixel(xy, value) - def remap_palette(self, dest_map, source_palette=None): + def remap_palette( + self, dest_map: list[int], source_palette: bytes | bytearray | None = None + ) -> Image: """ Rewrites the image to reorder the palette. @@ -2145,6 +2243,9 @@ def remap_palette(self, dest_map, source_palette=None): source_palette = self.im.getpalette(palette_mode, palette_mode) else: # L-mode source_palette = bytearray(i // 3 for i in range(768)) + elif len(source_palette) > 768: + bands = 4 + palette_mode = "RGBA" palette_bytes = b"" new_positions = [0] * 256 @@ -2202,7 +2303,12 @@ def remap_palette(self, dest_map, source_palette=None): return m_im - def _get_safe_box(self, size, resample, box): + def _get_safe_box( + self, + size: tuple[int, int], + resample: Resampling, + box: tuple[float, float, float, float], + ) -> tuple[int, int, int, int]: """Expands the box so it includes adjacent pixels that may be used by resampling with the given resampling filter. """ @@ -2221,7 +2327,7 @@ def _get_safe_box(self, size, resample, box): def resize( self, - size: tuple[int, int], + size: tuple[int, int] | list[int] | NumpyArray, resample: int | None = None, box: tuple[float, float, float, float] | None = None, reducing_gap: float | None = None, @@ -2229,15 +2335,13 @@ def resize( """ Returns a resized copy of this image. - :param size: The requested size in pixels, as a 2-tuple: + :param size: The requested size in pixels, as a tuple or array: (width, height). :param resample: An optional resampling filter. This can be 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 @@ -2260,8 +2364,7 @@ def resize( """ if resample is None: - type_special = ";" in self.mode - resample = Resampling.NEAREST if type_special else Resampling.BICUBIC + resample = Resampling.BICUBIC elif resample not in ( Resampling.NEAREST, Resampling.BILINEAR, @@ -2290,10 +2393,10 @@ def resize( msg = "reducing_gap must be 1.0 or greater" raise ValueError(msg) - self.load() if box is None: box = (0, 0) + self.size + size = tuple(size) if self.size == size and box == (0, 0) + self.size: return self.copy() @@ -2311,7 +2414,7 @@ def resize( 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) + reduce_box = self._get_safe_box(size, cast(Resampling, resample), box) factor = (factor_x, factor_y) self = ( self.reduce(factor, box=reduce_box) @@ -2325,7 +2428,14 @@ def resize( (box[3] - reduce_box[1]) / factor_y, ) - return self._new(self.im.resize(size, resample, box)) + if self.size[1] > self.size[0] * 100 and size[1] < self.size[1]: + im = self.im.resize( + (self.size[0], size[1]), resample, (0, box[1], self.size[0], box[3]) + ) + im = im.resize(size, resample, (box[0], 0, box[2], size[1])) + else: + im = self.im.resize(size, resample, box) + return self._new(im) def reduce( self, @@ -2447,8 +2557,8 @@ def rotate( 0.0, ] - def transform(x, y, matrix): - (a, b, c, d, e, f) = matrix + def transform(x: float, y: float, matrix: list[float]) -> tuple[float, float]: + a, b, c, d, e, f = matrix return a * x + b * y + c, d * x + e * y + f matrix[2], matrix[5] = transform( @@ -2462,9 +2572,9 @@ def transform(x, y, matrix): xx = [] yy = [] for x, y in ((0, 0), (w, 0), (w, h), (0, h)): - x, y = transform(x, y, matrix) - xx.append(x) - yy.append(y) + transformed_x, transformed_y = transform(x, y, matrix) + xx.append(transformed_x) + yy.append(transformed_y) nw = math.ceil(max(xx)) - math.floor(min(xx)) nh = math.ceil(max(yy)) - math.floor(min(yy)) @@ -2502,7 +2612,21 @@ def save( 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 params: Extra parameters to the image writer. + :param params: Extra parameters to the image writer. These can also be + set on the image itself through ``encoderinfo``. This is useful when + saving multiple images:: + + # Saving XMP data to a single image + from PIL import Image + red = Image.new("RGB", (1, 1), "#f00") + red.save("out.mpo", xmp=b"test") + + # Saving XMP data to the second frame of an image + from PIL import Image + black = Image.new("RGB", (1, 1)) + red = Image.new("RGB", (1, 1), "#f00") + red.encoderinfo = {"xmp": b"test"} + black.save("out.mpo", save_all=True, append_images=[red]) :returns: None :exception ValueError: If the output format could not be determined from the file name. Use the format option to solve this. @@ -2513,7 +2637,7 @@ def save( filename: str | bytes = "" open_fp = False if is_path(fp): - filename = os.path.realpath(os.fspath(fp)) + filename = os.fspath(fp) open_fp = True elif fp == sys.stdout: try: @@ -2522,21 +2646,22 @@ def save( pass if not filename and hasattr(fp, "name") and is_path(fp.name): # only set the name for metadata purposes - filename = os.path.realpath(os.fspath(fp.name)) - - # may mutate self! - self._ensure_mutable() - - save_all = params.pop("save_all", False) - self.encoderinfo = params - self.encoderconfig: tuple[Any, ...] = () + filename = os.fspath(fp.name) - preinit() + if format: + preinit() + else: + filename_ext = os.path.splitext(filename)[1].lower() + ext = ( + filename_ext.decode() + if isinstance(filename_ext, bytes) + else filename_ext + ) - filename_ext = os.path.splitext(filename)[1].lower() - ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext + # Try importing only the plugin for this extension first + if not _import_plugin_for_extension(ext): + preinit() - if not format: if ext not in EXTENSION: init() try: @@ -2545,9 +2670,29 @@ def save( msg = f"unknown file extension: {ext}" raise ValueError(msg) from e + from . import ImageFile + + # may mutate self! + if isinstance(self, ImageFile.ImageFile) and os.path.abspath( + filename + ) == os.path.abspath(self.filename): + self._ensure_mutable() + else: + self.load() + + save_all = params.pop("save_all", None) + self._default_encoderinfo = params + encoderinfo = getattr(self, "encoderinfo", {}) + self._attach_default_encoderinfo(self) + self.encoderconfig: tuple[Any, ...] = () + if format.upper() not in SAVE: init() - if save_all: + if save_all or ( + save_all is None + and params.get("append_images") + and format.upper() in SAVE_ALL + ): save_handler = SAVE_ALL[format.upper()] else: save_handler = SAVE[format.upper()] @@ -2575,9 +2720,16 @@ def save( except PermissionError: pass raise + finally: + self.encoderinfo = encoderinfo if open_fp: fp.close() + def _attach_default_encoderinfo(self, im: Image) -> dict[str, Any]: + encoderinfo = getattr(self, "encoderinfo", {}) + self.encoderinfo = {**im._default_encoderinfo, **encoderinfo} + return encoderinfo + def seek(self, frame: int) -> None: """ Seeks to the given frame in this sequence file. If you seek @@ -2620,7 +2772,9 @@ def show(self, title: str | None = None) -> None: :param title: Optional title to use for the image window, where possible. """ - _show(self, title=title) + from . import ImageShow + + ImageShow.show(self, title) def split(self) -> tuple[Image, ...]: """ @@ -2722,7 +2876,7 @@ def thumbnail( provided_size = tuple(map(math.floor, size)) def preserve_aspect_ratio() -> tuple[int, int] | None: - def round_aspect(number, key): + def round_aspect(number: float, key: Callable[[int], float]) -> int: return max(min(math.floor(number), math.ceil(number), key=key), 1) x, y = provided_size @@ -2738,27 +2892,18 @@ def round_aspect(number, key): ) return x, y + preserved_size = preserve_aspect_ratio() + if preserved_size is None: + return + final_size = preserved_size + box = None - final_size: tuple[int, int] if reducing_gap is not None: - preserved_size = preserve_aspect_ratio() - if preserved_size is None: - return - final_size = preserved_size - res = self.draft( None, (int(size[0] * reducing_gap), int(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 - preserved_size = preserve_aspect_ratio() - if preserved_size is None: - return - final_size = preserved_size if self.size != final_size: im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap) @@ -2768,7 +2913,6 @@ def round_aspect(number, key): 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. @@ -2867,8 +3011,14 @@ def getdata(self): return im def __transformer( - self, box, image, method, data, resample=Resampling.NEAREST, fill=1 - ): + self, + box: tuple[int, int, int, int], + image: Image, + method: Transform, + data: Sequence[float], + resample: int = Resampling.NEAREST, + fill: bool = True, + ) -> None: w = box[2] - box[0] h = box[3] - box[1] @@ -2917,11 +3067,12 @@ def __transformer( Resampling.BICUBIC, ): if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS): - msg = { + unusable: dict[int, str] = { Resampling.BOX: "Image.Resampling.BOX", Resampling.HAMMING: "Image.Resampling.HAMMING", Resampling.LANCZOS: "Image.Resampling.LANCZOS", - }[resample] + f" ({resample}) cannot be used." + } + msg = unusable[resample] + f" ({resample}) cannot be used." else: msg = f"Unknown resampling filter ({resample})." @@ -2968,7 +3119,7 @@ def effect_spread(self, distance: int) -> Image: self.load() return self._new(self.im.effect_spread(distance)) - def toqimage(self): + def toqimage(self) -> ImageQt.ImageQt: """Returns a QImage copy of this image""" from . import ImageQt @@ -2977,7 +3128,7 @@ def toqimage(self): raise ImportError(msg) return ImageQt.toqimage(self) - def toqpixmap(self): + def toqpixmap(self) -> ImageQt.QPixmap: """Returns a QPixmap copy of this image""" from . import ImageQt @@ -2991,7 +3142,7 @@ def toqpixmap(self): # Abstract handlers. -class ImagePointHandler: +class ImagePointHandler(abc.ABC): """ Used as a mixin by point transforms (for use with :py:meth:`~PIL.Image.Image.point`) @@ -3002,7 +3153,7 @@ def point(self, im: Image) -> Image: pass -class ImageTransformHandler: +class ImageTransformHandler(abc.ABC): """ Used as a mixin by geometry transforms (for use with :py:meth:`~PIL.Image.Image.transform`) @@ -3021,15 +3172,6 @@ def transform( # -------------------------------------------------------------------- # Factories -# -# Debugging - - -def _wedge() -> Image: - """Create grayscale wedge (for debugging only)""" - - return Image()._new(core.wedge("L")) - def _check_size(size: Any) -> None: """ @@ -3061,18 +3203,15 @@ def new( :param mode: The mode to use for the new image. See: :ref:`concept-modes`. :param size: A 2-tuple, containing (width, height) in pixels. - :param color: What color to use for the image. Default is black. - If given, this should be a single integer or floating point value - for single-band modes, and a tuple for multi-band modes (one value - per band). When creating RGB or HSV images, you can also use color - strings as supported by the ImageColor module. If the color is - None, the image is not initialised. + :param color: What color to use for the image. Default is black. If given, + this should be a single integer or floating point value for single-band + modes, and a tuple for multi-band modes (one value per band). When + creating RGB or HSV images, you can also use color strings as supported + by the ImageColor module. See :ref:`colors` for more information. If the + color is None, the image is not initialised. :returns: An :py:class:`~PIL.Image.Image` object. """ - if mode in ("BGR;15", "BGR;16", "BGR;24"): - deprecate(mode, 12) - _check_size(size) if color is None: @@ -3092,7 +3231,7 @@ def new( and isinstance(color, (list, tuple)) and all(isinstance(i, int) for i in color) ): - color_ints: tuple[int, ...] = cast(Tuple[int, ...], tuple(color)) + color_ints: tuple[int, ...] = cast(tuple[int, ...], tuple(color)) if len(color_ints) == 3 or len(color_ints) == 4: # RGB or RGBA value for a P image from . import ImagePalette @@ -3105,7 +3244,7 @@ def new( def frombytes( mode: str, size: tuple[int, int], - data: bytes | bytearray, + data: bytes | bytearray | SupportsArrayInterface, decoder_name: str = "raw", *args: Any, ) -> Image: @@ -3149,7 +3288,11 @@ def frombytes( def frombuffer( - mode: str, size: tuple[int, int], data, decoder_name: str = "raw", *args: Any + mode: str, + size: tuple[int, int], + data: bytes | SupportsArrayInterface, + decoder_name: str = "raw", + *args: Any, ) -> Image: """ Creates an image memory referencing pixel data in a byte buffer. @@ -3216,6 +3359,18 @@ def __array_interface__(self) -> dict[str, Any]: raise NotImplementedError() +class SupportsArrowArrayInterface(Protocol): + """ + An object that has an ``__arrow_c_array__`` method corresponding to the arrow c + data interface. + """ + + def __arrow_c_array__( + self, requested_schema: "PyCapsule" = None # type: ignore[name-defined] # noqa: F821, UP037 + ) -> tuple["PyCapsule", "PyCapsule"]: # type: ignore[name-defined] # noqa: F821, UP037 + raise NotImplementedError() + + def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: """ Creates an image memory from an object exporting the array interface @@ -3244,19 +3399,10 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: transferred. This means that P and PA mode images will lose their palette. :param obj: Object with array interface - :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) + :param mode: Optional mode to use when reading ``obj``. Since pixel values do not + contain information about palettes or color spaces, this can be used to place + grayscale L mode data within a P mode image, or read RGB data as YCbCr for + example. See: :ref:`concept-modes` for general information about modes. :returns: An image object. @@ -3267,20 +3413,28 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: shape = arr["shape"] ndim = len(shape) strides = arr.get("strides", None) - if mode is None: - try: - typekey = (1, 1) + shape[2:], arr["typestr"] - except KeyError as e: + try: + typekey = (1, 1) + shape[2:], arr["typestr"] + except KeyError as e: + if mode is not None: + typekey = None + color_modes: list[str] = [] + else: msg = "Cannot handle this data type" raise TypeError(msg) from e + if typekey is not None: try: - mode, rawmode = _fromarray_typemap[typekey] + typemode, rawmode, color_modes = _fromarray_typemap[typekey] except KeyError as e: typekey_shape, typestr = typekey msg = f"Cannot handle this data type: {typekey_shape}, {typestr}" raise TypeError(msg) from e - else: + if mode is not None: + if mode != typemode and mode not in color_modes: + deprecate("'mode' parameter for changing data types", 13) rawmode = mode + else: + mode = typemode if mode in ["1", "L", "I", "P", "F"]: ndmax = 2 elif mode == "RGB": @@ -3304,7 +3458,59 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) -def fromqimage(im): +def fromarrow( + obj: SupportsArrowArrayInterface, mode: str, size: tuple[int, int] +) -> Image: + """Creates an image with zero-copy shared memory from an object exporting + the arrow_c_array interface protocol:: + + from PIL import Image + import pyarrow as pa + arr = pa.array([0]*(5*5*4), type=pa.uint8()) + im = Image.fromarrow(arr, 'RGBA', (5, 5)) + + If the data representation of the ``obj`` is not compatible with + Pillow internal storage, a ValueError is raised. + + Pillow images can also be converted to Arrow objects:: + + from PIL import Image + import pyarrow as pa + im = Image.open('hopper.jpg') + arr = pa.array(im) + + As with array support, when converting Pillow images to arrays, + only pixel values are transferred. This means that P and PA mode + images will lose their palette. + + :param obj: Object with an arrow_c_array interface + :param mode: Image mode. + :param size: Image size. This must match the storage of the arrow object. + :returns: An Image object + + Note that according to the Arrow spec, both the producer and the + consumer should consider the exported array to be immutable, as + unsynchronized updates will potentially cause inconsistent data. + + See: :ref:`arrow-support` for more detailed information + + .. versionadded:: 11.2.1 + + """ + if not hasattr(obj, "__arrow_c_array__"): + msg = "arrow_c_array interface not found" + raise ValueError(msg) + + schema_capsule, array_capsule = obj.__arrow_c_array__() + _im = core.new_arrow(mode, size, schema_capsule, array_capsule) + if _im: + return Image()._new(_im) + + msg = "new_arrow returned None without an exception" + raise ValueError(msg) + + +def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile: """Creates an image instance from a QImage image""" from . import ImageQt @@ -3314,7 +3520,7 @@ def fromqimage(im): return ImageQt.fromqimage(im) -def fromqpixmap(im): +def fromqpixmap(im: ImageQt.QPixmap) -> ImageFile.ImageFile: """Creates an image instance from a QPixmap image""" from . import ImageQt @@ -3325,29 +3531,29 @@ def fromqpixmap(im): _fromarray_typemap = { - # (shape, typestr) => mode, rawmode + # (shape, typestr) => mode, rawmode, color modes # first two members of shape are set to one - ((1, 1), "|b1"): ("1", "1;8"), - ((1, 1), "|u1"): ("L", "L"), - ((1, 1), "|i1"): ("I", "I;8"), - ((1, 1), "u2"): ("I", "I;16B"), - ((1, 1), "i2"): ("I", "I;16BS"), - ((1, 1), "u4"): ("I", "I;32B"), - ((1, 1), "i4"): ("I", "I;32BS"), - ((1, 1), "f4"): ("F", "F;32BF"), - ((1, 1), "f8"): ("F", "F;64BF"), - ((1, 1, 2), "|u1"): ("LA", "LA"), - ((1, 1, 3), "|u1"): ("RGB", "RGB"), - ((1, 1, 4), "|u1"): ("RGBA", "RGBA"), + ((1, 1), "|b1"): ("1", "1;8", []), + ((1, 1), "|u1"): ("L", "L", ["P"]), + ((1, 1), "|i1"): ("I", "I;8", []), + ((1, 1), "u2"): ("I", "I;16B", []), + ((1, 1), "i2"): ("I", "I;16BS", []), + ((1, 1), "u4"): ("I", "I;32B", []), + ((1, 1), "i4"): ("I", "I;32BS", []), + ((1, 1), "f4"): ("F", "F;32BF", []), + ((1, 1), "f8"): ("F", "F;64BF", []), + ((1, 1, 2), "|u1"): ("LA", "LA", ["La", "PA"]), + ((1, 1, 3), "|u1"): ("RGB", "RGB", ["YCbCr", "LAB", "HSV"]), + ((1, 1, 4), "|u1"): ("RGBA", "RGBA", ["RGBa", "RGBX", "CMYK"]), # shortcuts: - ((1, 1), f"{_ENDIAN}i4"): ("I", "I"), - ((1, 1), f"{_ENDIAN}f4"): ("F", "F"), + ((1, 1), f"{_ENDIAN}i4"): ("I", "I", []), + ((1, 1), f"{_ENDIAN}f4"): ("F", "F", []), } @@ -3425,9 +3631,7 @@ def open( exclusive_fp = False filename: str | bytes = "" if is_path(fp): - filename = os.path.realpath(os.fspath(fp)) - - if filename: + filename = os.fspath(fp) fp = builtins.open(filename, "rb") exclusive_fp = True else: @@ -3441,7 +3645,11 @@ def open( prefix = fp.read(16) - preinit() + # Try to import just the plugin needed for this file extension + # before falling back to preinit() which imports common plugins + ext = os.path.splitext(filename)[1] if filename else "" + if not _import_plugin_for_extension(ext): + preinit() warning_messages: list[str] = [] @@ -3477,14 +3685,19 @@ def _open_core( im = _open_core(fp, filename, prefix, formats) if im is None and formats is ID: - checked_formats = ID.copy() - if init(): - im = _open_core( - fp, - filename, - prefix, - tuple(format for format in formats if format not in checked_formats), - ) + # Try preinit (few common plugins) then init (all plugins) + for loader in (preinit, init): + checked_formats = ID.copy() + loader() + if formats != checked_formats: + im = _open_core( + fp, + filename, + prefix, + tuple(f for f in formats if f not in checked_formats), + ) + if im is not None: + break if im: im._exclusive_fp = exclusive_fp @@ -3506,9 +3719,8 @@ def alpha_composite(im1: Image, im2: Image) -> Image: """ Alpha composite im2 over im1. - :param im1: The first image. Must have mode RGBA. - :param im2: The second image. Must have mode RGBA, and the same size as - the first image. + :param im1: The first image. Must have mode RGBA or LA. + :param im2: The second image. Must have the same mode and size as the first image. :returns: An :py:class:`~PIL.Image.Image` object. """ @@ -3557,7 +3769,7 @@ def composite(image1: Image, image2: Image, mask: Image) -> Image: return image -def eval(image, *args): +def eval(image: Image, *args: Callable[[int], float]) -> Image: """ Applies the function (which should take one argument) to each pixel in the given image. If the image has more than one band, the same @@ -3606,7 +3818,10 @@ def merge(mode: str, bands: Sequence[Image]) -> Image: def register_open( id: str, - factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], + factory: ( + Callable[[IO[bytes], str | bytes], ImageFile.ImageFile] + | type[ImageFile.ImageFile] + ), accept: Callable[[bytes], bool | str] | None = None, ) -> None: """ @@ -3731,6 +3946,7 @@ def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None: def _show(image: Image, **options: Any) -> None: from . import ImageShow + deprecate("Image._show", 13, "ImageShow.show") ImageShow.show(image, **options) @@ -3852,7 +4068,7 @@ class Exif(_ExifBase): gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) print(gps_ifd) - Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.Makernote``, + Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.MakerNote``, ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``. :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: @@ -3861,18 +4077,18 @@ class Exif(_ExifBase): print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99 """ - endian = None + endian: str | None = None bigtiff = False _loaded = False - def __init__(self): - self._data = {} - self._hidden_data = {} - self._ifds = {} - self._info = None - self._loaded_exif = None + def __init__(self) -> None: + self._data: dict[int, Any] = {} + self._hidden_data: dict[int, Any] = {} + self._ifds: dict[int, dict[int, Any]] = {} + self._info: TiffImagePlugin.ImageFileDirectory_v2 | None = None + self._loaded_exif: bytes | None = None - def _fixup(self, value): + def _fixup(self, value: Any) -> Any: try: if len(value) == 1 and isinstance(value, tuple): return value[0] @@ -3880,27 +4096,29 @@ def _fixup(self, value): pass return value - def _fixup_dict(self, src_dict): + def _fixup_dict(self, src_dict: dict[int, Any]) -> dict[int, Any]: # 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, group=None): + def _get_ifd_dict( + self, offset: int, group: int | None = None + ) -> dict[int, Any] | None: 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 + return None else: from . import TiffImagePlugin info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group) info.load(self.fp) - return self._fixup_dict(info) + return self._fixup_dict(dict(info)) - def _get_head(self): - version = b"\x2B" if self.bigtiff else b"\x2A" + def _get_head(self) -> bytes: + version = b"\x2b" if self.bigtiff else b"\x2a" if self.endian == "<": head = b"II" + version + b"\x00" + o32le(8) else: @@ -3910,7 +4128,7 @@ def _get_head(self): head += b"\x00\x00\x00\x00" return head - def load(self, data): + def load(self, data: bytes) -> None: # Extract EXIF information. This is highly experimental, # and is likely to be replaced with something better in a future # version. @@ -3923,13 +4141,13 @@ def load(self, data): self._data.clear() self._hidden_data.clear() self._ifds.clear() - if data and data.startswith(b"Exif\x00\x00"): + while data and data.startswith(b"Exif\x00\x00"): data = data[6:] if not data: self._info = None return - self.fp = io.BytesIO(data) + self.fp: IO[bytes] = io.BytesIO(data) self.head = self.fp.read(8) # process dictionary from . import TiffImagePlugin @@ -3939,7 +4157,7 @@ def load(self, data): self.fp.seek(self._info.next) self._info.load(self.fp) - def load_from_fp(self, fp, offset=None): + def load_from_fp(self, fp: IO[bytes], offset: int | None = None) -> None: self._loaded_exif = None self._data.clear() self._hidden_data.clear() @@ -3962,7 +4180,7 @@ def load_from_fp(self, fp, offset=None): self.fp.seek(offset) self._info.load(self.fp) - def _get_merged_dict(self): + def _get_merged_dict(self) -> dict[int, Any]: merged_dict = dict(self) # get EXIF extension @@ -3984,6 +4202,9 @@ def tobytes(self, offset: int = 8) -> bytes: head = self._get_head() ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) + for tag, ifd_dict in self._ifds.items(): + if tag not in self: + ifd[tag] = ifd_dict for tag, value in self.items(): if tag in [ ExifTags.IFD.Exif, @@ -4000,103 +4221,114 @@ def tobytes(self, offset: int = 8) -> bytes: ifd[tag] = value return b"Exif\x00\x00" + head + ifd.tobytes(offset) - def get_ifd(self, tag): + def get_ifd(self, tag: int) -> dict[int, Any]: if tag not in self._ifds: if tag == ExifTags.IFD.IFD1: if self._info is not None and self._info.next != 0: - self._ifds[tag] = self._get_ifd_dict(self._info.next) + ifd = self._get_ifd_dict(self._info.next) + if ifd is not None: + self._ifds[tag] = ifd elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: offset = self._hidden_data.get(tag, self.get(tag)) if offset is not None: - self._ifds[tag] = self._get_ifd_dict(offset, tag) - elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: + ifd = self._get_ifd_dict(offset, tag) + if ifd is not None: + self._ifds[tag] = ifd + elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.MakerNote]: if ExifTags.IFD.Exif not in self._ifds: self.get_ifd(ExifTags.IFD.Exif) tag_data = self._ifds[ExifTags.IFD.Exif][tag] - if tag == ExifTags.IFD.Makernote: + if tag == ExifTags.IFD.MakerNote: from .TiffImagePlugin import ImageFileDirectory_v2 - if tag_data[:8] == b"FUJIFILM": - ifd_offset = i32le(tag_data, 8) - ifd_data = tag_data[ifd_offset:] + try: + if tag_data.startswith(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 + makernote = {} + for i in range(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: dict[str, int | bytes] = { + "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 + )[0] + + self.fp.read(4) + camerainfo["Category"] = self.fp.read(2) + + makernote = {0x1101: camerainfo} + self._ifds[tag] = makernote + except struct.error: + pass else: # Interop - self._ifds[tag] = self._get_ifd_dict(tag_data, tag) - ifd = self._ifds.get(tag, {}) + ifd = self._get_ifd_dict(tag_data, tag) + if ifd is not None: + self._ifds[tag] = ifd + ifd = self._ifds.setdefault(tag, {}) if tag == ExifTags.IFD.Exif and self._hidden_data: ifd = { k: v for (k, v) in ifd.items() - if k not in (ExifTags.IFD.Interop, ExifTags.IFD.Makernote) + if k not in (ExifTags.IFD.Interop, ExifTags.IFD.MakerNote) } return ifd @@ -4120,16 +4352,16 @@ def __len__(self) -> int: keys.update(self._info) return len(keys) - def __getitem__(self, tag): + def __getitem__(self, tag: int) -> Any: 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) -> bool: + def __contains__(self, tag: object) -> bool: return tag in self._data or (self._info is not None and tag in self._info) - def __setitem__(self, tag, value) -> None: + def __setitem__(self, tag: int, value: Any) -> None: if self._info is not None and tag in self._info: del self._info[tag] self._data[tag] = value @@ -4139,8 +4371,10 @@ def __delitem__(self, tag: int) -> None: del self._info[tag] else: del self._data[tag] + if tag in self._ifds: + del self._ifds[tag] - def __iter__(self): + def __iter__(self) -> Iterator[int]: keys = set(self._data) if self._info is not None: keys.update(self._info) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index ec10230f12b..513e28acf33 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -25,12 +25,16 @@ from functools import reduce from typing import Any, Literal, SupportsFloat, SupportsInt, Union -from . import Image, __version__ +from . import Image from ._deprecate import deprecate from ._typing import SupportsRead try: from . import _imagingcms as core + + _CmsProfileCompatible = Union[ + str, SupportsRead[bytes], core.CmsProfile, "ImageCmsProfile" + ] except ImportError as ex: # Allow error import for doc purposes, but error out when accessing # anything in core. @@ -104,20 +108,6 @@ _VERSION = "1.0.0 pil" -def __getattr__(name: str) -> Any: - if name == "DESCRIPTION": - deprecate("PIL.ImageCms.DESCRIPTION", 12) - return _DESCRIPTION - elif name == "VERSION": - deprecate("PIL.ImageCms.VERSION", 12) - return _VERSION - elif name == "FLAGS": - deprecate("PIL.ImageCms.FLAGS", 12, "PIL.ImageCms.Flags") - return _FLAGS - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - # --------------------------------------------------------------------. @@ -244,6 +234,7 @@ def __init__(self, profile: str | SupportsRead[bytes] | core.CmsProfile) -> None low-level profile object """ + self.filename: str | None = None if isinstance(profile, str): if sys.platform == "win32": @@ -252,22 +243,24 @@ def __init__(self, profile: str | SupportsRead[bytes] | core.CmsProfile) -> None profile_bytes_path.decode("ascii") except UnicodeDecodeError: with open(profile, "rb") as f: - self._set(core.profile_frombytes(f.read())) + self.profile = core.profile_frombytes(f.read()) return - self._set(core.profile_open(profile), profile) + self.filename = profile + self.profile = core.profile_open(profile) elif hasattr(profile, "read"): - self._set(core.profile_frombytes(profile.read())) + self.profile = core.profile_frombytes(profile.read()) elif isinstance(profile, core.CmsProfile): - self._set(profile) + self.profile = profile else: msg = "Invalid type for Profile" # type: ignore[unreachable] raise TypeError(msg) - def _set(self, profile: core.CmsProfile, filename: str | None = None) -> None: - self.profile = profile - self.filename = filename - self.product_name = None # profile.product_name - self.product_info = None # profile.product_info + def __getattr__(self, name: str) -> Any: + if name in ("product_name", "product_info"): + deprecate(f"ImageCms.ImageCmsProfile.{name}", 13) + return None + msg = f"'{self.__class__.__name__}' object has no attribute '{name}'" + raise AttributeError(msg) def tobytes(self) -> bytes: """ @@ -299,31 +292,6 @@ def __init__( proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC, flags: Flags = Flags.NONE, ): - supported_modes = ( - "RGB", - "RGBA", - "RGBX", - "CMYK", - "I;16", - "I;16L", - "I;16B", - "YCbCr", - "LAB", - "L", - "1", - ) - for mode in (input_mode, output_mode): - if mode not in supported_modes: - deprecate( - mode, - 12, - { - "L;16": "I;16 or I;16L", - "L:16B": "I;16B", - "YCCA": "YCbCr", - "YCC": "YCbCr", - }.get(mode), - ) if proof is None: self.transform = core.buildTransform( input.profile, output.profile, input_mode, output_mode, intent, flags @@ -349,19 +317,17 @@ def point(self, im: Image.Image) -> Image.Image: return self.apply(im) def apply(self, im: Image.Image, imOut: Image.Image | None = None) -> Image.Image: - im.load() if imOut is None: imOut = Image.new(self.output_mode, im.size, None) - self.transform.apply(im.im.id, imOut.im.id) + self.transform.apply(im.getim(), imOut.getim()) imOut.info["icc_profile"] = self.output_profile.tobytes() return imOut def apply_in_place(self, im: Image.Image) -> Image.Image: - im.load() if im.mode != self.output_mode: msg = "mode mismatch" raise ValueError(msg) # wrong output mode - self.transform.apply(im.im.id, im.im.id) + self.transform.apply(im.getim(), im.getim()) im.info["icc_profile"] = self.output_profile.tobytes() return im @@ -391,10 +357,6 @@ def get_display_profile(handle: SupportsInt | None = None) -> ImageCmsProfile | # pyCMS compatible layer # --------------------------------------------------------------------. -_CmsProfileCompatible = Union[ - str, SupportsRead[bytes], core.CmsProfile, ImageCmsProfile -] - class PyCMSError(Exception): """(pyCMS) Exception class. @@ -1112,16 +1074,3 @@ def isIntentSupported( return -1 except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) from v - - -def versions() -> tuple[str, str | None, str, str]: - """ - (pyCMS) Fetches versions. - """ - - deprecate( - "PIL.ImageCms.versions()", - 12, - '(PIL.features.version("littlecms2"), sys.version, PIL.__version__)', - ) - return _VERSION, core.littlecms_version, sys.version.split()[0], __version__ diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 244d3d5f022..9b0864d1a89 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -32,26 +32,23 @@ from __future__ import annotations import math -import numbers import struct -from types import ModuleType -from typing import TYPE_CHECKING, AnyStr, Callable, List, Sequence, Tuple, Union, cast +from collections.abc import Sequence +from typing import cast -from . import Image, ImageColor -from ._deprecate import deprecate -from ._typing import Coords - -# experimental access to the outline API -Outline: Callable[[], Image.core._Outline] | None -try: - Outline = Image.core.outline -except AttributeError: - Outline = None +from . import Image, ImageColor, ImageText +TYPE_CHECKING = False if TYPE_CHECKING: + from collections.abc import Callable + from types import ModuleType + from typing import Any, AnyStr + from . import ImageDraw2, ImageFont + from ._typing import Coords, _Ink -_Ink = Union[float, Tuple[int, ...], str] +# experimental access to the outline API +Outline: Callable[[], Image.core._Outline] = Image.core.outline """ A simple 2D drawing interface for PIL images. @@ -77,9 +74,7 @@ def __init__(self, im: Image.Image, mode: str | None = None) -> None: must be the same as the image mode. If omitted, the mode defaults to the mode of the image. """ - im.load() - if im.readonly: - im._copy() # make it writeable + im._ensure_mutable() blend = 0 if mode is None: mode = im.mode @@ -159,13 +154,13 @@ def _getink( if ink is not None: if isinstance(ink, str): ink = ImageColor.getcolor(ink, self.mode) - if self.palette and not isinstance(ink, numbers.Number): + if self.palette and isinstance(ink, tuple): ink = self.palette.getcolor(ink, self._image) result_ink = self.draw.draw_ink(ink) if fill is not None: if isinstance(fill, str): fill = ImageColor.getcolor(fill, self.mode) - if self.palette and not isinstance(fill, numbers.Number): + if self.palette and isinstance(fill, tuple): fill = self.palette.getcolor(fill, self._image) result_fill = self.draw.draw_ink(fill) return result_ink, result_fill @@ -368,22 +363,10 @@ def polygon( # 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(mask) draw.draw.draw_polygon(xy, mask_ink, 1) - ink_im = mask.copy() - draw = Draw(ink_im) - width = width * 2 - 1 - draw.draw.draw_polygon(xy, mask_ink, 0, width) - - mask.paste(ink_im, mask=fill_im) - - im = Image.new(self.mode, self.im.size) - draw = Draw(im) - draw.draw.draw_polygon(xy, ink, 0, width) - self.im.paste(im.im, (0, 0) + im.size, mask.im) + self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im) def regular_polygon( self, @@ -504,7 +487,7 @@ def draw_corners(pieslice: bool) -> None: if full_x: self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1) - else: + elif x1 - r - 1 >= x0 + r + 1: self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1) if not full_x and not full_y: left = [x0, y0, x0 + r, y1] @@ -552,68 +535,42 @@ def draw_corners(pieslice: bool) -> None: right[3] -= r + 1 self.draw.draw_rectangle(right, ink, 1) - def _multiline_check(self, text: AnyStr) -> bool: - split_character = "\n" if isinstance(text, str) else b"\n" - - return split_character in text - - def _multiline_split(self, text: AnyStr) -> list[AnyStr]: - return text.split("\n" if isinstance(text, str) else b"\n") - - def _multiline_spacing(self, font, spacing, stroke_width): - return ( - self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] - + stroke_width - + spacing - ) - def text( self, xy: tuple[float, float], - text: str, - fill=None, + text: AnyStr | ImageText.Text[AnyStr], + fill: _Ink | None = None, font: ( ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None ) = None, - anchor=None, - spacing=4, - align="left", - direction=None, - features=None, - language=None, - stroke_width=0, - stroke_fill=None, - embedded_color=False, - *args, - **kwargs, + anchor: str | None = None, + spacing: float = 4, + align: str = "left", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + stroke_fill: _Ink | None = None, + embedded_color: bool = False, + *args: Any, + **kwargs: Any, ) -> None: """Draw text.""" - if embedded_color and self.mode not in ("RGB", "RGBA"): - msg = "Embedded color supported only in RGB and RGBA modes" - raise ValueError(msg) - - if font is None: - font = self._getfont(kwargs.get("font_size")) - - if self._multiline_check(text): - return self.multiline_text( - xy, - text, - fill, - font, - anchor, - spacing, - align, - direction, - features, - language, - stroke_width, - stroke_fill, - embedded_color, + if isinstance(text, ImageText.Text): + image_text = text + else: + if font is None: + font = self._getfont(kwargs.get("font_size")) + image_text = ImageText.Text( + text, font, self.mode, spacing, direction, features, language ) + if embedded_color: + image_text.embed_color() + if stroke_width: + image_text.stroke(stroke_width, stroke_fill) def getink(fill: _Ink | None) -> int: ink, fill_ink = self._getink(fill) @@ -622,75 +579,82 @@ def getink(fill: _Ink | None) -> int: return fill_ink return ink - def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: - mode = self.fontmode - if stroke_width == 0 and embedded_color: - mode = "RGBA" - coord = [] - start = [] - for i in range(2): - coord.append(int(xy[i])) - start.append(math.modf(xy[i])[0]) - try: - mask, offset = font.getmask2( # type: ignore[union-attr,misc] - text, - mode, - direction=direction, - features=features, - language=language, - stroke_width=stroke_width, - anchor=anchor, - ink=ink, - start=start, - *args, - **kwargs, - ) - coord = [coord[0] + offset[0], coord[1] + offset[1]] - except AttributeError: + ink = getink(fill) + if ink is None: + return + + stroke_ink = None + if image_text.stroke_width: + stroke_ink = ( + getink(image_text.stroke_fill) + if image_text.stroke_fill is not None + else ink + ) + + for line in image_text._split(xy, anchor, align): + + def draw_text(ink: int, stroke_width: float = 0) -> None: + mode = self.fontmode + if stroke_width == 0 and embedded_color: + mode = "RGBA" + x = int(line.x) + y = int(line.y) + start = (math.modf(line.x)[0], math.modf(line.y)[0]) try: - mask = font.getmask( # type: ignore[misc] - text, + mask, offset = image_text.font.getmask2( # type: ignore[union-attr,misc] + line.text, mode, - direction, - features, - language, - stroke_width, - anchor, - ink, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + stroke_filled=True, + anchor=line.anchor, + ink=ink, start=start, *args, **kwargs, ) - except TypeError: - mask = font.getmask(text) - 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) - ink_alpha = struct.pack("i", ink)[3] - color.fillband(3, ink_alpha) - x, y = coord - if self.im is not None: - 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 + x += offset[0] + y += offset[1] + except AttributeError: + try: + mask = image_text.font.getmask( # type: ignore[misc] + line.text, + mode, + direction, + features, + language, + stroke_width, + line.anchor, + ink, + start=start, + *args, + **kwargs, + ) + except TypeError: + mask = image_text.font.getmask(line.text) + if mode == "RGBA": + # image_text.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) + ink_alpha = struct.pack("i", ink)[3] + color.fillband(3, ink_alpha) + if self.im is not None: + self.im.paste( + color, (x, y, x + mask.size[0], y + mask.size[1]), mask + ) + else: + self.draw.draw_bitmap((x, y), mask, ink) if stroke_ink is not None: # Draw stroked text - draw_text(stroke_ink, stroke_width) + draw_text(stroke_ink, image_text.stroke_width) # Draw normal text - draw_text(ink, 0) + if ink != stroke_ink: + draw_text(ink) else: # Only draw normal text draw_text(ink) @@ -698,272 +662,142 @@ def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: def multiline_text( self, xy: tuple[float, float], - text: str, - fill=None, + text: AnyStr, + fill: _Ink | None = None, font: ( ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None ) = None, - anchor=None, - spacing=4, - align="left", - direction=None, - features=None, - language=None, - stroke_width=0, - stroke_fill=None, - embedded_color=False, + anchor: str | None = None, + spacing: float = 4, + align: str = "left", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + stroke_fill: _Ink | None = None, + embedded_color: bool = False, *, - font_size=None, + font_size: float | None = None, ) -> None: - if direction == "ttb": - msg = "ttb direction is unsupported for multiline text" - raise ValueError(msg) - - if anchor is None: - anchor = "la" - elif len(anchor) != 2: - msg = "anchor must be a 2 character string" - raise ValueError(msg) - elif anchor[1] in "tb": - msg = "anchor not supported for multiline text" - raise ValueError(msg) - - if font is None: - font = self._getfont(font_size) - - widths = [] - max_width: float = 0 - lines = self._multiline_split(text) - line_spacing = self._multiline_spacing(font, spacing, stroke_width) - for line in lines: - line_width = self.textlength( - line, font, direction=direction, features=features, language=language - ) - widths.append(line_width) - max_width = max(max_width, line_width) - - 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 - elif align == "center": - left += width_difference / 2.0 - elif align == "right": - left += width_difference - else: - msg = 'align must be "left", "center" or "right"' - raise ValueError(msg) - - 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 + return self.text( + xy, + text, + fill, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + stroke_fill, + embedded_color, + font_size=font_size, + ) def textlength( self, - text: str, + text: AnyStr, font: ( ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None ) = None, - direction=None, - features=None, - language=None, - embedded_color=False, + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + embedded_color: bool = False, *, - font_size=None, + font_size: float | None = None, ) -> float: """Get the length of a given string, in pixels with 1/64 precision.""" - if self._multiline_check(text): - msg = "can't measure length of multiline text" - raise ValueError(msg) - if embedded_color and self.mode not in ("RGB", "RGBA"): - msg = "Embedded color supported only in RGB and RGBA modes" - raise ValueError(msg) - if font is None: font = self._getfont(font_size) - mode = "RGBA" if embedded_color else self.fontmode - return font.getlength(text, mode, direction, features, language) + image_text = ImageText.Text( + text, + font, + self.mode, + direction=direction, + features=features, + language=language, + ) + if embedded_color: + image_text.embed_color() + return image_text.get_length() 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, + xy: tuple[float, float], + text: AnyStr, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, + anchor: str | None = None, + spacing: float = 4, + align: str = "left", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + embedded_color: bool = False, *, - font_size=None, - ) -> tuple[int, int, int, int]: + font_size: float | None = None, + ) -> tuple[float, float, float, float]: """Get the bounding box of a given string, in pixels.""" - if embedded_color and self.mode not in ("RGB", "RGBA"): - msg = "Embedded color supported only in RGB and RGBA modes" - raise ValueError(msg) - if font is None: font = self._getfont(font_size) - - if self._multiline_check(text): - return self.multiline_textbbox( - xy, - text, - font, - anchor, - spacing, - align, - direction, - features, - language, - stroke_width, - embedded_color, - ) - - mode = "RGBA" if embedded_color else self.fontmode - bbox = font.getbbox( - text, mode, direction, features, language, stroke_width, anchor + image_text = ImageText.Text( + text, font, self.mode, spacing, direction, features, language ) - return bbox[0] + xy[0], bbox[1] + xy[1], bbox[2] + xy[0], bbox[3] + xy[1] + if embedded_color: + image_text.embed_color() + if stroke_width: + image_text.stroke(stroke_width) + return image_text.get_bbox(xy, anchor, align) 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, + xy: tuple[float, float], + text: AnyStr, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ) = None, + anchor: str | None = None, + spacing: float = 4, + align: str = "left", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + embedded_color: bool = False, *, - font_size=None, - ) -> tuple[int, int, int, int]: - if direction == "ttb": - msg = "ttb direction is unsupported for multiline text" - raise ValueError(msg) - - if anchor is None: - anchor = "la" - elif len(anchor) != 2: - msg = "anchor must be a 2 character string" - raise ValueError(msg) - elif anchor[1] in "tb": - msg = "anchor not supported for multiline text" - raise ValueError(msg) - - if font is None: - font = self._getfont(font_size) - - widths = [] - max_width: float = 0 - lines = self._multiline_split(text) - line_spacing = self._multiline_spacing(font, spacing, stroke_width) - for line in lines: - 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) - - 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: tuple[int, int, int, int] | None = 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: - msg = 'align must be "left", "center" or "right"' - raise ValueError(msg) - - 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 + font_size: float | None = None, + ) -> tuple[float, float, float, float]: + return self.textbbox( + xy, + text, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + embedded_color, + font_size=font_size, + ) def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw: @@ -983,16 +817,11 @@ def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw: return ImageDraw(im, mode) -def getdraw( - im: Image.Image | None = None, hints: list[str] | None = None -) -> tuple[ImageDraw2.Draw | None, ModuleType]: +def getdraw(im: Image.Image | None = None) -> tuple[ImageDraw2.Draw | None, ModuleType]: """ :param im: The image to draw in. - :param hints: An optional list of hints. Deprecated. :returns: A (drawing context, drawing resource factory) tuple. """ - if hints is not None: - deprecate("'hints' parameter", 12) from . import ImageDraw2 draw = ImageDraw2.Draw(im) if im is not None else None @@ -1124,7 +953,7 @@ def _compute_regular_polygon_vertices( msg = "bounding_circle should only contain numeric data" raise ValueError(msg) - *centroid, polygon_radius = cast(List[float], list(bounding_circle)) + *centroid, polygon_radius = cast(list[float], list(bounding_circle)) elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)): if not all( isinstance(i, (int, float)) for i in bounding_circle[0] @@ -1136,7 +965,7 @@ def _compute_regular_polygon_vertices( msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" raise ValueError(msg) - centroid = cast(List[float], list(bounding_circle[0])) + centroid = cast(list[float], list(bounding_circle[0])) polygon_radius = cast(float, bounding_circle[1]) else: msg = ( @@ -1180,7 +1009,7 @@ def _get_angles(n_sides: int, rotation: float) -> list[float]: degrees = 360 / n_sides # Start with the bottom left polygon vertex current_angle = (270 - 0.5 * degrees) + rotation - for _ in range(0, n_sides): + for _ in range(n_sides): angles.append(current_angle) current_angle += degrees if current_angle > 360: @@ -1203,4 +1032,4 @@ def _color_diff( first = color1 if isinstance(color1, tuple) else (color1,) second = color2 if isinstance(color2, tuple) else (color2,) - return sum(abs(first[i] - second[i]) for i in range(0, len(second))) + return sum(abs(first[i] - second[i]) for i in range(len(second))) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index e89a78be4b5..2c9e39b2c41 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -22,12 +22,13 @@ .. seealso:: :py:mod:`PIL.ImageDraw` """ + from __future__ import annotations -from typing import BinaryIO +from typing import Any, AnyStr, BinaryIO from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath -from ._typing import StrOrBytesPath +from ._typing import Coords, StrOrBytesPath class Pen: @@ -74,12 +75,19 @@ def __init__( image = Image.new(image, size, color) self.draw = ImageDraw.Draw(image) self.image = image - self.transform = None + self.transform: tuple[float, float, float, float, float, float] | None = None def flush(self) -> Image.Image: return self.image - def render(self, op, xy, pen, brush=None): + def render( + self, + op: str, + xy: Coords, + pen: Pen | Brush | None, + brush: Brush | Pen | None = None, + **kwargs: Any, + ) -> None: # handle color arguments outline = fill = None width = 1 @@ -95,63 +103,89 @@ def render(self, op, xy, pen, brush=None): fill = pen.color # handle transformation if self.transform: - xy = ImagePath.Path(xy) - xy.transform(self.transform) + path = ImagePath.Path(xy) + path.transform(self.transform) + xy = path # render the item - if op == "line": - self.draw.line(xy, fill=outline, width=width) + if op in ("arc", "line"): + kwargs.setdefault("fill", outline) else: - getattr(self.draw, op)(xy, fill=fill, outline=outline) + kwargs.setdefault("fill", fill) + kwargs.setdefault("outline", outline) + if op == "line": + kwargs.setdefault("width", width) + getattr(self.draw, op)(xy, **kwargs) - def settransform(self, offset): + def settransform(self, offset: tuple[float, float]) -> None: """Sets a transformation offset.""" - (xoffset, yoffset) = offset + xoffset, yoffset = offset self.transform = (1, 0, xoffset, 0, 1, yoffset) - def arc(self, xy, start, end, *options): + def arc( + self, + xy: Coords, + pen: Pen | Brush | None, + start: float, + end: float, + *options: Any, + ) -> None: """ 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) + self.render("arc", xy, pen, *options, start=start, end=end) - def chord(self, xy, start, end, *options): + def chord( + self, + xy: Coords, + pen: Pen | Brush | None, + start: float, + end: float, + *options: Any, + ) -> None: """ 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) + self.render("chord", xy, pen, *options, start=start, end=end) - def ellipse(self, xy, *options): + def ellipse(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: """ Draws an ellipse inside the given bounding box. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse` """ - self.render("ellipse", xy, *options) + self.render("ellipse", xy, pen, *options) - def line(self, xy, *options): + def line(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: """ Draws a line between the coordinates in the ``xy`` list. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line` """ - self.render("line", xy, *options) + self.render("line", xy, pen, *options) - def pieslice(self, xy, start, end, *options): + def pieslice( + self, + xy: Coords, + pen: Pen | Brush | None, + start: float, + end: float, + *options: Any, + ) -> None: """ 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) + self.render("pieslice", xy, pen, *options, start=start, end=end) - def polygon(self, xy, *options): + def polygon(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: """ Draws a polygon. @@ -162,28 +196,31 @@ def polygon(self, xy, *options): .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon` """ - self.render("polygon", xy, *options) + self.render("polygon", xy, pen, *options) - def rectangle(self, xy, *options): + def rectangle(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: """ Draws a rectangle. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle` """ - self.render("rectangle", xy, *options) + self.render("rectangle", xy, pen, *options) - def text(self, xy, text, font): + def text(self, xy: tuple[float, float], text: AnyStr, font: Font) -> None: """ 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) + path = ImagePath.Path(xy) + path.transform(self.transform) + xy = path self.draw.text(xy, text, font=font.font, fill=font.color) - def textbbox(self, xy, text, font): + def textbbox( + self, xy: tuple[float, float], text: AnyStr, font: Font + ) -> tuple[float, float, float, float]: """ Returns bounding box (in pixels) of given text. @@ -192,11 +229,12 @@ def textbbox(self, xy, text, font): .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textbbox` """ if self.transform: - xy = ImagePath.Path(xy) - xy.transform(self.transform) + path = ImagePath.Path(xy) + path.transform(self.transform) + xy = path return self.draw.textbbox(xy, text, font=font.font) - def textlength(self, text, font): + def textlength(self, text: AnyStr, font: Font) -> float: """ Returns length (in pixels) of given text. This is the amount by which following text should be offset. diff --git a/src/PIL/ImageEnhance.py b/src/PIL/ImageEnhance.py index d7e99a96815..0e7e6dd8ae6 100644 --- a/src/PIL/ImageEnhance.py +++ b/src/PIL/ImageEnhance.py @@ -55,7 +55,9 @@ def __init__(self, image: Image.Image) -> None: if "A" in image.getbands(): self.intermediate_mode = "LA" - self.degenerate = image.convert(self.intermediate_mode).convert(image.mode) + if self.intermediate_mode != image.mode: + image = image.convert(self.intermediate_mode).convert(image.mode) + self.degenerate = image class Contrast(_Enhance): @@ -68,11 +70,15 @@ class Contrast(_Enhance): def __init__(self, image: Image.Image) -> None: 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 image.mode != "L": + image = image.convert("L") + mean = int(ImageStat.Stat(image).mean[0] + 0.5) + self.degenerate = Image.new("L", image.size, mean) + if self.degenerate.mode != self.image.mode: + self.degenerate = self.degenerate.convert(self.image.mode) + + if "A" in self.image.getbands(): + self.degenerate.putalpha(self.image.getchannel("A")) class Brightness(_Enhance): diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 69e7ee54811..c70d93f3c97 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -31,15 +31,33 @@ import abc import io import itertools +import logging +import os import struct -import sys -from typing import IO, Any, NamedTuple +from typing import IO, Any, NamedTuple, cast -from . import Image -from ._deprecate import deprecate -from ._util import is_path +from . import ExifTags, Image +from ._util import DeferredError, is_path + +TYPE_CHECKING = False +if TYPE_CHECKING: + from ._typing import StrOrBytesPath + +logger = logging.getLogger(__name__) MAXBLOCK = 65536 +""" +By default, Pillow processes image data in blocks. This helps to prevent excessive use +of resources. Codecs may disable this behaviour with ``_pulls_fd`` or ``_pushes_fd``. + +When reading an image, this is the number of bytes to read at once. + +When writing an image, this is the number of bytes to write at once. +If the image width times 4 is greater, then that will be used instead. +Plugins may also set a greater number. + +User code may set this to another number. +""" SAFEBLOCK = 1024 * 1024 @@ -76,26 +94,16 @@ def _get_oserror(error: int, *, encoder: bool) -> OSError: return OSError(msg) -def raise_oserror(error: int) -> OSError: - deprecate( - "raise_oserror", - 12, - action="It is only useful for translating error codes returned by a codec's " - "decode() method, which ImageFile already does automatically.", - ) - raise _get_oserror(error, encoder=False) - - -def _tilesort(t): +def _tilesort(t: _Tile) -> int: # sort on offset return t[2] class _Tile(NamedTuple): codec_name: str - extents: tuple[int, int, int, int] - offset: int - args: tuple[Any, ...] | str | None + extents: tuple[int, int, int, int] | None + offset: int = 0 + args: tuple[Any, ...] | str | None = None # @@ -106,36 +114,44 @@ class _Tile(NamedTuple): class ImageFile(Image.Image): """Base class for image file format handlers.""" - def __init__(self, fp=None, filename=None): + def __init__( + self, fp: StrOrBytesPath | IO[bytes], filename: str | bytes | None = None + ) -> None: super().__init__() self._min_frame = 0 - self.custom_mimetype = None + self.custom_mimetype: str | None = None - self.tile = None - """ A list of tile descriptors, or ``None`` """ + self.tile: list[_Tile] = [] + """ A list of tile descriptors """ self.readonly = 1 # until we know better - self.decoderconfig = () + self.decoderconfig: tuple[Any, ...] = () self.decodermaxblock = MAXBLOCK + self.fp: IO[bytes] | None + self._fp: IO[bytes] | DeferredError if is_path(fp): # filename self.fp = open(fp, "rb") - self.filename = fp + self.filename = os.fspath(fp) self._exclusive_fp = True else: # stream - self.fp = fp - self.filename = filename + self.fp = cast(IO[bytes], fp) + self.filename = filename if filename is not None else "" # can be overridden - self._exclusive_fp = None + self._exclusive_fp = False try: try: self._open() + + if isinstance(self, StubImageFile): + if loader := self._load(): + loader.open(self) except ( IndexError, # end of data TypeError, # end of data (ord) @@ -154,6 +170,99 @@ def __init__(self, fp=None, filename=None): self.fp.close() raise + def _open(self) -> None: + pass + + # Context manager support + def __enter__(self) -> ImageFile: + return self + + def _close_fp(self) -> None: + if getattr(self, "_fp", False) and not isinstance(self._fp, DeferredError): + if self._fp != self.fp: + self._fp.close() + self._fp = DeferredError(ValueError("Operation on closed image")) + if self.fp: + self.fp.close() + + def __exit__(self, *args: object) -> None: + if getattr(self, "_exclusive_fp", False): + self._close_fp() + self.fp = None + + def close(self) -> None: + """ + Closes the file pointer, if possible. + + This operation will destroy the image core and release its memory. + The image data will be unusable afterward. + + 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._close_fp() + self.fp = None + except Exception as msg: + logger.debug("Error closing: %s", msg) + + super().close() + + def get_child_images(self) -> list[ImageFile]: + child_images = [] + exif = self.getexif() + ifds = [] + if ExifTags.Base.SubIFDs in exif: + subifd_offsets = exif[ExifTags.Base.SubIFDs] + if subifd_offsets: + if not isinstance(subifd_offsets, tuple): + subifd_offsets = (subifd_offsets,) + ifds = [ + (exif._get_ifd_dict(subifd_offset), subifd_offset) + for subifd_offset in subifd_offsets + ] + ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) + if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset): + assert exif._info is not None + ifds.append((ifd1, exif._info.next)) + + offset = None + for ifd, ifd_offset in ifds: + assert self.fp is not None + current_offset = self.fp.tell() + if offset is None: + offset = current_offset + + fp = self.fp + if ifd is not None: + thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset) + if thumbnail_offset is not None: + thumbnail_offset += getattr(self, "_exif_offset", 0) + self.fp.seek(thumbnail_offset) + + length = ifd.get(ExifTags.Base.JpegIFByteCount) + assert isinstance(length, int) + data = self.fp.read(length) + fp = io.BytesIO(data) + + with Image.open(fp) as im: + from . import TiffImagePlugin + + if thumbnail_offset is None and isinstance( + im, TiffImagePlugin.TiffImageFile + ): + im._frame_pos = [ifd_offset] + im._seek(0) + im.load() + child_images.append(im) + + if offset is not None: + assert self.fp is not None + self.fp.seek(offset) + return child_images + def get_format_mimetype(self) -> str | None: if self.custom_mimetype: return self.custom_mimetype @@ -161,8 +270,13 @@ def get_format_mimetype(self) -> str | None: return Image.MIME.get(self.format.upper()) return None - def __setstate__(self, state): + def __getstate__(self) -> list[Any]: + return super().__getstate__() + [self.filename] + + def __setstate__(self, state: list[Any]) -> None: self.tile = [] + if len(state) > 5: + self.filename = state[5] super().__setstate__(state) def verify(self) -> None: @@ -170,14 +284,14 @@ def verify(self) -> None: # raise exception if something's wrong. must be called # directly after open, and closes file when finished. - if self._exclusive_fp: + if self._exclusive_fp and self.fp: self.fp.close() self.fp = None - def load(self): + def load(self) -> Image.core.PixelAccess | None: """Load image data based on tile list""" - if self.tile is None: + if not self.tile and self._im is None: msg = "cannot load this image" raise OSError(msg) @@ -185,25 +299,24 @@ def load(self): if not self.tile: return pixel - self.map = None + self.map: mmap.mmap | None = 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") + assert self.fp is not None readonly = 0 # look for read/seek overrides - try: + if hasattr(self, "load_read"): read = self.load_read # don't use mmap if there are custom read/seek functions use_mmap = False - except AttributeError: + else: read = self.fp.read - try: + if hasattr(self, "load_seek"): seek = self.load_seek use_mmap = False - except AttributeError: + else: seek = self.fp.seek if use_mmap: @@ -213,10 +326,14 @@ def load(self): args = (args, 0, 1) if ( decoder_name == "raw" + and isinstance(args, tuple) and len(args) >= 3 and args[0] == self.mode and args[0] in Image._MAPMODES ): + if offset < 0: + msg = "Tile offset cannot be negative" + raise ValueError(msg) try: # use mmap, if possible import mmap @@ -243,11 +360,8 @@ def load(self): # sort tiles in file order self.tile.sort(key=_tilesort) - try: - # FIXME: This is a hack to handle TIFF's JpegTables tag. - prefix = self.tile_prefix - except AttributeError: - prefix = b"" + # FIXME: This is a hack to handle TIFF's JpegTables tag. + prefix = getattr(self, "tile_prefix", b"") # Remove consecutive duplicates that only differ by their offset self.tile = [ @@ -256,7 +370,7 @@ def load(self): self.tile, lambda tile: (tile[0], tile[1], tile[3]) ) ] - for decoder_name, extents, offset, args in self.tile: + for i, (decoder_name, extents, offset, args) in enumerate(self.tile): seek(offset) decoder = Image._getdecoder( self.mode, decoder_name, args, self.decoderconfig @@ -269,8 +383,13 @@ def load(self): else: b = prefix while True: + read_bytes = self.decodermaxblock + if i + 1 < len(self.tile): + next_offset = self.tile[i + 1].offset + if next_offset > offset: + read_bytes = next_offset - offset try: - s = read(self.decodermaxblock) + s = read(read_bytes) except (IndexError, struct.error) as e: # truncated png/gif if LOAD_TRUNCATED_IMAGES: @@ -315,7 +434,7 @@ def load(self): def load_prepare(self) -> None: # create image memory if necessary - if not self.im or self.im.mode != self.mode or self.im.size != self.size: + if self._im is None: self.im = Image.core.new(self.mode, self.size) # create palette (optional) if self.mode == "P": @@ -333,14 +452,14 @@ def load_end(self) -> None: # def load_read(self, read_bytes: int) -> bytes: # pass - def _seek_check(self, frame): + def _seek_check(self, frame: int) -> bool: if ( frame < self._min_frame # Only check upper limit on frames if additional seek operations # are not required to do so or ( not (hasattr(self, "_n_frames") and self._n_frames is None) - and frame >= self.n_frames + self._min_frame + and frame >= getattr(self, "n_frames") + self._min_frame ) ): msg = "attempt to seek outside sequence" @@ -349,7 +468,7 @@ def _seek_check(self, frame): return self.tell() != frame -class StubHandler: +class StubHandler(abc.ABC): def open(self, im: StubImageFile) -> None: pass @@ -358,7 +477,7 @@ def load(self, im: StubImageFile) -> Image.Image: pass -class StubImageFile(ImageFile): +class StubImageFile(ImageFile, metaclass=abc.ABCMeta): """ Base class for stub image loaders. @@ -366,11 +485,11 @@ class StubImageFile(ImageFile): certain format, but relies on external code to load the file. """ + @abc.abstractmethod def _open(self) -> None: - msg = "StubImageFile subclass must implement _open" - raise NotImplementedError(msg) + pass - def load(self): + def load(self) -> Image.core.PixelAccess | None: loader = self._load() if loader is None: msg = f"cannot find loader for this {self.format} file" @@ -378,14 +497,14 @@ def load(self): image = loader.load(self) assert image is not None # become the other object (!) - self.__class__ = image.__class__ + self.__class__ = image.__class__ # type: ignore[assignment] self.__dict__ = image.__dict__ return image.load() + @abc.abstractmethod def _load(self) -> StubHandler | None: """(Hook) Find actual image loader.""" - msg = "StubImageFile subclass must implement _load" - raise NotImplementedError(msg) + pass class Parser: @@ -396,8 +515,8 @@ class Parser: incremental = None image: Image.Image | None = None - data = None - decoder = None + data: bytes | None = None + decoder: Image.core.ImagingDecoder | PyDecoder | None = None offset = 0 finished = 0 @@ -409,7 +528,7 @@ def reset(self) -> None: """ assert self.data is None, "cannot reuse parsers" - def feed(self, data): + def feed(self, data: bytes) -> None: """ (Consumer) Feed data to the parser. @@ -466,10 +585,7 @@ def feed(self, data): pass # not enough data else: flag = hasattr(im, "load_seek") or hasattr(im, "load_read") - if flag or len(im.tile) != 1: - # custom load code, or multiple tiles - self.decode = None - else: + if not flag and len(im.tile) == 1: # initialize decoder im.load_prepare() d, e, o, a = im.tile[0] @@ -485,13 +601,13 @@ def feed(self, data): self.image = im - def __enter__(self): + def __enter__(self) -> Parser: return self def __exit__(self, *args: object) -> None: self.close() - def close(self): + def close(self) -> Image.Image: """ (Consumer) Close the stream. @@ -525,7 +641,7 @@ def close(self): # -------------------------------------------------------------------- -def _save(im, fp, tile, bufsize=0) -> None: +def _save(im: Image.Image, fp: IO[bytes], tile: list[_Tile], bufsize: int = 0) -> None: """Helper to save image based on tile list :param im: Image object. @@ -553,7 +669,14 @@ def _save(im, fp, tile, bufsize=0) -> None: fp.flush() -def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None): +def _encode_tile( + im: Image.Image, + fp: IO[bytes], + tile: list[_Tile], + bufsize: int, + fh: int | None, + exc: BaseException | None = None, +) -> None: for encoder_name, extents, offset, args in tile: if offset > 0: fp.seek(offset) @@ -573,6 +696,7 @@ def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None): break else: # slight speedup: compress to real file object + assert fh is not None errcode = encoder.encode_to_file(fh, bufsize) if errcode < 0: raise _get_oserror(errcode, encoder=True) from exc @@ -580,7 +704,7 @@ def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None): encoder.cleanup() -def _safe_read(fp, size): +def _safe_read(fp: IO[bytes], size: int) -> bytes: """ Reads large blocks in a safe way. Unlike fp.read(n), this function doesn't trust the user. If the requested size is larger than @@ -601,18 +725,18 @@ def _safe_read(fp, size): msg = "Truncated File Read" raise OSError(msg) return data - data = [] + blocks: list[bytes] = [] remaining_size = size while remaining_size > 0: block = fp.read(min(remaining_size, SAFEBLOCK)) if not block: break - data.append(block) + blocks.append(block) remaining_size -= len(block) - if sum(len(d) for d in data) < size: + if sum(len(block) for block in blocks) < size: msg = "Truncated File Read" raise OSError(msg) - return b"".join(data) + return b"".join(blocks) class PyCodecState: @@ -629,18 +753,18 @@ def extents(self) -> tuple[int, int, int, int]: class PyCodec: fd: IO[bytes] | None - def __init__(self, mode, *args): - self.im = None + def __init__(self, mode: str, *args: Any) -> None: + self.im: Image.core.ImagingCore | None = None self.state = PyCodecState() self.fd = None self.mode = mode self.init(args) - def init(self, args): + def init(self, args: tuple[Any, ...]) -> None: """ Override to perform codec specific initialization - :param args: Array of args items from the tile entry + :param args: Tuple of arg items from the tile entry :returns: None """ self.args = args @@ -653,7 +777,7 @@ def cleanup(self) -> None: """ pass - def setfd(self, fd): + def setfd(self, fd: IO[bytes]) -> None: """ Called from ImageFile to set the Python file-like object @@ -662,7 +786,11 @@ def setfd(self, fd): """ self.fd = fd - def setimage(self, im, extents: tuple[int, int, int, int] | None = None) -> None: + def setimage( + self, + im: Image.core.ImagingCore, + extents: tuple[int, int, int, int] | None = None, + ) -> None: """ Called from ImageFile to set the core output image for the codec @@ -676,27 +804,21 @@ def setimage(self, im, extents: tuple[int, int, int, int] | None = None) -> None self.im = im if extents: - (x0, y0, x1, y1) = extents - else: - (x0, y0, x1, y1) = (0, 0, 0, 0) + x0, y0, x1, y1 = extents + + if x0 < 0 or y0 < 0 or x1 > self.im.size[0] or y1 > self.im.size[1]: + msg = "Tile cannot extend outside image" + raise ValueError(msg) - if x0 == 0 and x1 == 0: - self.state.xsize, self.state.ysize = self.im.size - else: self.state.xoff = x0 self.state.yoff = y0 self.state.xsize = x1 - x0 self.state.ysize = y1 - y0 + else: + self.state.xsize, self.state.ysize = self.im.size if self.state.xsize <= 0 or self.state.ysize <= 0: - msg = "Size cannot be negative" - raise ValueError(msg) - - if ( - self.state.xsize + self.state.xoff > self.im.size[0] - or self.state.ysize + self.state.yoff > self.im.size[1] - ): - msg = "Tile cannot extend outside image" + msg = "Size must be positive" raise ValueError(msg) @@ -714,7 +836,7 @@ class PyDecoder(PyCodec): def pulls_fd(self) -> bool: return self._pulls_fd - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: """ Override to perform the decoding process. @@ -726,19 +848,22 @@ def decode(self, buffer: bytes) -> tuple[int, int]: msg = "unavailable in base decoder" raise NotImplementedError(msg) - def set_as_raw(self, data: bytes, rawmode=None) -> None: + def set_as_raw( + self, data: bytes, rawmode: str | None = None, extra: tuple[Any, ...] = () + ) -> 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 extra: Extra arguments for the decoder. :returns: None """ if not rawmode: rawmode = self.mode - d = Image._getdecoder(self.mode, "raw", rawmode) + d = Image._getdecoder(self.mode, "raw", rawmode, extra) assert self.im is not None d.setimage(self.im, self.state.extents()) s = d.decode(data) @@ -793,7 +918,7 @@ def encode_to_pyfd(self) -> tuple[int, int]: self.fd.write(data) return bytes_consumed, errcode - def encode_to_file(self, fh, bufsize): + def encode_to_file(self, fh: int, bufsize: int) -> int: """ :param fh: File handle. :param bufsize: Buffer size. @@ -806,5 +931,5 @@ def encode_to_file(self, fh, bufsize): while errcode == 0: status, errcode, buf = self.encode(bufsize) if status > 0: - fh.write(buf[status:]) + os.write(fh, buf[status:]) return errcode diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index e18b4a4466a..9326eeeda9d 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -18,15 +18,20 @@ import abc import functools -from types import ModuleType -from typing import TYPE_CHECKING, Any, Callable, Sequence, cast +from collections.abc import Sequence +from typing import cast +TYPE_CHECKING = False if TYPE_CHECKING: + from collections.abc import Callable + from types import ModuleType + from typing import Any + from . import _imaging from ._typing import NumpyArray -class Filter: +class Filter(abc.ABC): @abc.abstractmethod def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: pass @@ -552,7 +557,7 @@ def transform( ch_out = channels or ch_in size_1d, size_2d, size_3d = self.size - table = [0] * (size_1d * size_2d * size_3d * ch_out) + table: list[float] = [0] * (size_1d * size_2d * size_3d * ch_out) idx_in = 0 idx_out = 0 for b in range(size_3d): @@ -597,8 +602,6 @@ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: self.mode or image.mode, Image.Resampling.BILINEAR, self.channels, - self.size[0], - self.size[1], - self.size[2], + self.size, self.table, ) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index d260eef6946..06ea0359c45 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -34,18 +34,26 @@ from enum import IntEnum from io import BytesIO from types import ModuleType -from typing import IO, TYPE_CHECKING, Any, BinaryIO +from typing import IO, Any, BinaryIO, TypedDict, cast from . import Image from ._typing import StrOrBytesPath from ._util import DeferredError, is_path +TYPE_CHECKING = False if TYPE_CHECKING: from . import ImageFile from ._imaging import ImagingFont from ._imagingft import Font +class Axis(TypedDict): + minimum: int | None + default: int | None + maximum: int | None + name: bytes | None + + class Layout(IntEnum): BASIC = 0 RAQM = 1 @@ -91,21 +99,24 @@ class ImageFont: def _load_pilfont(self, filename: str) -> None: with open(filename, "rb") as fp: image: ImageFile.ImageFile | None = None + root = os.path.splitext(filename)[0] + for ext in (".png", ".gif", ".pbm"): if image: image.close() try: - fullname = os.path.splitext(filename)[0] + ext + fullname = root + ext image = Image.open(fullname) except Exception: pass else: - if image and image.mode in ("1", "L"): + if image.mode in ("1", "L"): break else: if image: image.close() - msg = "cannot find glyph data file" + + msg = f"cannot find glyph data file {root}.{{gif|pbm|png}}" raise OSError(msg) self.file = fullname @@ -114,11 +125,20 @@ def _load_pilfont(self, filename: str) -> None: image.close() def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None: + # check image + if image.mode not in ("1", "L"): + image.close() + + msg = "invalid font image mode" + raise TypeError(msg) + # read PILfont header - if file.readline() != b"PILfont\n": + if file.read(8) != b"PILfont\n": + image.close() + msg = "Not a PILfont file" raise SyntaxError(msg) - file.readline().split(b";") + file.readline() self.info = [] # FIXME: should be a dictionary while True: s = file.readline() @@ -129,16 +149,16 @@ def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None: # read PILfont metrics data = file.read(256 * 20) - # check image - if image.mode not in ("1", "L"): - msg = "invalid font image mode" - raise TypeError(msg) + self._load(image, data) + def _load(self, image: Image.Image, data: bytes) -> None: image.load() self.font = Image.core.font(image.im, data) - def getmask(self, text, mode="", *args, **kwargs): + def getmask( + self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any + ) -> Image.core.ImagingCore: """ Create a bitmap for the text. @@ -203,7 +223,7 @@ class FreeTypeFont: def __init__( self, - font: StrOrBytesPath | BinaryIO | None = None, + font: StrOrBytesPath | BinaryIO, size: float = 10, index: int = 0, encoding: str = "", @@ -215,7 +235,7 @@ def __init__( raise core.ex if size <= 0: - msg = "font size must be greater than 0" + msg = f"font size must be greater than 0, not {size}" raise ValueError(msg) self.path = font @@ -236,14 +256,14 @@ def __init__( self.layout_engine = layout_engine - def load_from_bytes(f): + def load_from_bytes(f: IO[bytes]) -> None: self.font_bytes = f.read() self.font = core.getfont( "", size, index, encoding, self.font_bytes, layout_engine ) if is_path(font): - font = os.path.realpath(os.fspath(font)) + font = os.fspath(font) if sys.platform == "win32": font_bytes_path = font if isinstance(font, bytes) else font.encode() try: @@ -258,14 +278,14 @@ def load_from_bytes(f): font, size, index, encoding, layout_engine=layout_engine ) else: - load_from_bytes(font) + load_from_bytes(cast(IO[bytes], font)) - def __getstate__(self): + def __getstate__(self) -> list[Any]: return [self.path, self.size, self.index, self.encoding, self.layout_engine] - def __setstate__(self, state): + def __setstate__(self, state: list[Any]) -> None: path, size, index, encoding, layout_engine = state - self.__init__(path, size, index, encoding, layout_engine) + FreeTypeFont.__init__(self, path, size, index, encoding, layout_engine) def getname(self) -> tuple[str | None, str | None]: """ @@ -283,7 +303,12 @@ def getmetrics(self) -> tuple[int, int]: return self.font.ascent, self.font.descent def getlength( - self, text: str | bytes, mode="", direction=None, features=None, language=None + self, + text: str | bytes, + mode: str = "", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, ) -> float: """ Returns length (in pixels with 1/64 precision) of given text when rendered @@ -424,16 +449,16 @@ def getbbox( def getmask( self, - text, - mode="", - direction=None, - features=None, - language=None, - stroke_width=0, - anchor=None, - ink=0, - start=None, - ): + text: str | bytes, + mode: str = "", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + anchor: str | None = None, + ink: int = 0, + start: tuple[float, float] | None = None, + ) -> Image.core.ImagingCore: """ Create a bitmap for the text. @@ -516,17 +541,17 @@ def getmask( def getmask2( self, text: str | bytes, - mode="", - direction=None, - features=None, - language=None, - stroke_width=0, - anchor=None, - ink=0, - start=None, - *args, - **kwargs, - ): + mode: str = "", + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + stroke_width: float = 0, + anchor: str | None = None, + ink: int = 0, + start: tuple[float, float] | None = None, + *args: Any, + **kwargs: Any, + ) -> tuple[Image.core.ImagingCore, tuple[int, int]]: """ Create a bitmap for the text. @@ -599,7 +624,7 @@ def getmask2( if start is None: start = (0, 0) - def fill(width, height): + def fill(width: int, height: int) -> Image.core.ImagingCore: size = (width, height) Image._decompression_bomb_check(size) return Image.core.fill("RGBA" if mode == "RGBA" else "L", size) @@ -612,15 +637,20 @@ def fill(width, height): features, language, stroke_width, + kwargs.get("stroke_filled", False), anchor, ink, - start[0], - start[1], + start, ) def font_variant( - self, font=None, size=None, index=None, encoding=None, layout_engine=None - ): + self, + font: StrOrBytesPath | BinaryIO | None = None, + size: float | None = None, + index: int | None = None, + encoding: str | None = None, + layout_engine: Layout | None = None, + ) -> FreeTypeFont: """ Create a copy of this FreeTypeFont object, using any specified arguments to override the settings. @@ -648,14 +678,14 @@ def get_variation_names(self) -> list[bytes]: :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: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e - return [name.replace(b"\x00", b"") for name in names] - - def set_variation_by_name(self, name): + names = [] + for name in self.font.getvarnames(): + name = name.replace(b"\x00", b"") + if name not in names: + names.append(name) + return names + + def set_variation_by_name(self, name: str | bytes) -> None: """ :param name: The name of the style. :exception OSError: If the font is not a variation font. @@ -674,16 +704,12 @@ def set_variation_by_name(self, name): self.font.setvarname(index) - def get_variation_axes(self): + def get_variation_axes(self) -> list[Axis]: """ :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: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e + axes = self.font.getvaraxes() for axis in axes: if axis["name"]: axis["name"] = axis["name"].replace(b"\x00", b"") @@ -694,17 +720,15 @@ def set_variation_by_axes(self, axes: list[float]) -> None: :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: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e + self.font.setvaraxes(axes) class TransposedFont: """Wrapper for writing rotated or mirrored text""" - def __init__(self, font, orientation=None): + def __init__( + self, font: ImageFont | FreeTypeFont, orientation: Image.Transpose | None = None + ): """ Wrapper that creates a transposed font from any existing font object. @@ -718,13 +742,17 @@ def __init__(self, font, orientation=None): self.font = font self.orientation = orientation # any 'transpose' argument, or None - def getmask(self, text, mode="", *args, **kwargs): + def getmask( + self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any + ) -> Image.core.ImagingCore: im = self.font.getmask(text, mode, *args, **kwargs) if self.orientation is not None: return im.transpose(self.orientation) return im - def getbbox(self, text, *args, **kwargs): + def getbbox( + self, text: str | bytes, *args: Any, **kwargs: Any + ) -> tuple[int, int, float, float]: # 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) @@ -734,7 +762,7 @@ def getbbox(self, text, *args, **kwargs): return 0, 0, height, width return 0, 0, width, height - def getlength(self, text: str | bytes, *args, **kwargs) -> float: + def getlength(self, text: str | bytes, *args: Any, **kwargs: Any) -> float: if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): msg = "text length is undefined for text rotated by 90 or 270 degrees" raise ValueError(msg) @@ -743,8 +771,9 @@ def getlength(self, text: str | bytes, *args, **kwargs) -> float: def load(filename: str) -> ImageFont: """ - Load a font file. This function loads a font object from the given - bitmap font file, and returns the corresponding font object. + Load a font file. This function loads a font object from the given + bitmap font file, and returns the corresponding font object. For loading TrueType + or OpenType fonts instead, see :py:func:`~PIL.ImageFont.truetype`. :param filename: Name of font file. :return: A font object. @@ -756,7 +785,7 @@ def load(filename: str) -> ImageFont: def truetype( - font: StrOrBytesPath | BinaryIO | None = None, + font: StrOrBytesPath | BinaryIO, size: float = 10, index: int = 0, encoding: str = "", @@ -764,9 +793,10 @@ def truetype( ) -> FreeTypeFont: """ 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. + 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. For loading bitmap fonts instead, see :py:func:`~PIL.ImageFont.load` + and :py:func:`~PIL.ImageFont.load_path`. 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 @@ -827,7 +857,7 @@ def truetype( :exception ValueError: If the font size is not greater than zero. """ - def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont: + def freetype(font: StrOrBytesPath | BinaryIO) -> FreeTypeFont: return FreeTypeFont(font, size, index, encoding, layout_engine) try: @@ -900,9 +930,12 @@ def load_path(filename: str | bytes) -> ImageFont: for directory in sys.path: try: return load(os.path.join(directory, filename)) - except OSError: + except OSError: # noqa: PERF203 pass - msg = "cannot find font file" + msg = f'cannot find font file "{filename}" in sys.path' + if os.path.exists(filename): + msg += f', did you mean ImageFont.load("{filename}") instead?' + raise OSError(msg) @@ -910,9 +943,7 @@ def load_default_imagefont() -> ImageFont: f = ImageFont() f._load_pilfont_data( # courB08 - BytesIO( - base64.b64decode( - b""" + BytesIO(base64.b64decode(b""" UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA @@ -1004,13 +1035,8 @@ def load_default_imagefont() -> ImageFont: 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 @@ -1034,17 +1060,14 @@ def load_default_imagefont() -> ImageFont: AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v// Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR w7IkEbzhVQAAAABJRU5ErkJggg== -""" - ) - ) - ), +"""))), ) return f def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: """If FreeType support is available, load a version of Aileron Regular, - https://dotcolon.net/font/aileron, with a more limited character set. + https://dotcolon.net/fonts/aileron, with a more limited character set. Otherwise, load a "better than nothing" font. @@ -1058,9 +1081,7 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: """ if isinstance(core, ModuleType) or size is not None: return truetype( - BytesIO( - base64.b64decode( - b""" + BytesIO(base64.b64decode(b""" AAEAAAAPAIAAAwBwRkZUTYwDlUAAADFoAAAAHEdERUYAqADnAAAo8AAAACRHUE9ThhmITwAAKfgAA AduR1NVQnHxefoAACkUAAAA4k9TLzJovoHLAAABeAAAAGBjbWFw5lFQMQAAA6gAAAGqZ2FzcP//AA MAACjoAAAACGdseWYmRXoPAAAGQAAAHfhoZWFkE18ayQAAAPwAAAA2aGhlYQboArEAAAE0AAAAJGh @@ -1281,9 +1302,7 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: gAWAAAAEQADAAoAFAAMAA0ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAEgAGAAEAHgAkAC YAJwApACoALQAuAC8AMgAzADcAOAA5ADoAPAA9AEUASABOAE8AUgBTAFUAVwBZAFoAWwBcAF0AcwA AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ== -""" - ) - ), +""")), 10 if size is None else size, layout_engine=Layout.BASIC, ) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index e27ca7e5033..66ee6dd33a3 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -25,12 +25,17 @@ from . import Image +TYPE_CHECKING = False +if TYPE_CHECKING: + from . import ImageWin + def grab( bbox: tuple[int, int, int, int] | None = None, include_layered_windows: bool = False, all_screens: bool = False, xdisplay: str | None = None, + window: int | ImageWin.HWND | None = None, ) -> Image.Image: im: Image.Image if xdisplay is None: @@ -38,21 +43,57 @@ def grab( fh, filepath = tempfile.mkstemp(".png") os.close(fh) args = ["screencapture"] - if bbox: + if window is not None: + args += ["-l", str(window)] + elif bbox: left, top, right, bottom = bbox args += ["-R", f"{left},{top},{right-left},{bottom-top}"] - subprocess.call(args + ["-x", filepath]) + args += ["-x", filepath] + retcode = subprocess.call(args) + if retcode: + raise subprocess.CalledProcessError(retcode, args) im = Image.open(filepath) im.load() os.unlink(filepath) if bbox: - im_resized = im.resize((right - left, bottom - top)) - im.close() - return im_resized + if window is not None: + # Determine if the window was in Retina mode or not + # by capturing it without the shadow, + # and checking how different the width is + fh, filepath = tempfile.mkstemp(".png") + os.close(fh) + args = ["screencapture", "-l", str(window), "-o", "-x", filepath] + retcode = subprocess.call(args) + if retcode: + raise subprocess.CalledProcessError(retcode, args) + with Image.open(filepath) as im_no_shadow: + retina = im.width - im_no_shadow.width > 100 + os.unlink(filepath) + + # Since screencapture's -R does not work with -l, + # crop the image manually + if retina: + left, top, right, bottom = bbox + im_cropped = im.resize( + (right - left, bottom - top), + box=tuple(coord * 2 for coord in bbox), + ) + else: + im_cropped = im.crop(bbox) + im.close() + return im_cropped + else: + im_resized = im.resize((right - left, bottom - top)) + im.close() + return im_resized return im elif sys.platform == "win32": + if window is not None: + all_screens = -1 offset, size, data = Image.core.grabscreen_win32( - include_layered_windows, all_screens + include_layered_windows, + all_screens, + int(window) if window is not None else 0, ) im = Image.frombytes( "RGB", @@ -77,14 +118,21 @@ def grab( raise OSError(msg) size, data = Image.core.grabscreen_x11(display_name) except OSError: - if ( - display_name is None - and sys.platform not in ("darwin", "win32") - and shutil.which("gnome-screenshot") - ): + if display_name is None and sys.platform not in ("darwin", "win32"): + if shutil.which("gnome-screenshot"): + args = ["gnome-screenshot", "-f"] + elif shutil.which("grim"): + args = ["grim"] + elif shutil.which("spectacle"): + args = ["spectacle", "-n", "-b", "-f", "-o"] + else: + raise fh, filepath = tempfile.mkstemp(".png") os.close(fh) - subprocess.call(["gnome-screenshot", "-f", filepath]) + args.append(filepath) + retcode = subprocess.call(args) + if retcode: + raise subprocess.CalledProcessError(retcode, args) im = Image.open(filepath) im.load() os.unlink(filepath) @@ -104,38 +152,27 @@ def grab( def grabclipboard() -> Image.Image | list[str] | None: if sys.platform == "darwin": - fh, filepath = tempfile.mkstemp(".png") - os.close(fh) - commands = [ - 'set theFile to (open for access POSIX file "' - + filepath - + '" with write permission)', - "try", - " write (the clipboard as «class PNGf») to theFile", - "end try", - "close access theFile", - ] - script = ["osascript"] - for command in commands: - script += ["-e", command] - subprocess.call(script) - - im = None - if os.stat(filepath).st_size != 0: - im = Image.open(filepath) - im.load() - os.unlink(filepath) - return im + p = subprocess.run( + ["osascript", "-e", "get the clipboard as «class PNGf»"], + capture_output=True, + ) + if p.returncode != 0: + return None + + import binascii + + data = io.BytesIO(binascii.unhexlify(p.stdout[11:-3])) + return Image.open(data) 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: + if data[16] == 0: files = data[o:].decode("mbcs").split("\0") + else: + files = data[o:].decode("utf-16le").split("\0") return files[: files.index("")] if isinstance(data, bytes): data = io.BytesIO(data) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 6664434ea45..dfdc50c0552 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -17,11 +17,14 @@ from __future__ import annotations import builtins -from types import CodeType -from typing import Any, Callable from . import Image, _imagingmath -from ._deprecate import deprecate + +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from types import CodeType + from typing import Any class _Operand: @@ -59,13 +62,12 @@ def apply( if im2 is None: # unary operation out = Image.new(mode or im_1.mode, im_1.size, None) - im_1.load() try: op = getattr(_imagingmath, f"{op}_{im_1.mode}") except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.unop(op, out.im.id, im_1.im.id) + _imagingmath.unop(op, out.getim(), im_1.getim()) else: # binary operation im_2 = self.__fixup(im2) @@ -86,14 +88,12 @@ def apply( if im_2.size != size: im_2 = im_2.crop((0, 0) + size) out = Image.new(mode or im_1.mode, im_1.size, None) - im_1.load() - im_2.load() try: op = getattr(_imagingmath, f"{op}_{im_1.mode}") except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id) + _imagingmath.binop(op, out.getim(), im_1.getim(), im_2.getim()) return _Operand(out) # unary operators @@ -176,10 +176,10 @@ def __rshift__(self, other: _Operand | float) -> _Operand: return self.apply("rshift", self, other) # logical - def __eq__(self, other): + def __eq__(self, other: _Operand | float) -> _Operand: # type: ignore[override] return self.apply("eq", self, other) - def __ne__(self, other): + def __ne__(self, other: _Operand | float) -> _Operand: # type: ignore[override] return self.apply("ne", self, other) def __lt__(self, other: _Operand | float) -> _Operand: @@ -236,11 +236,7 @@ def imagemath_convert(self: _Operand, mode: str) -> _Operand: } -def lambda_eval( - expression: Callable[[dict[str, Any]], Any], - options: dict[str, Any] = {}, - **kw: Any, -) -> Any: +def lambda_eval(expression: Callable[[dict[str, Any]], Any], **kw: Any) -> Any: """ Returns the result of an image function. @@ -249,19 +245,16 @@ def lambda_eval( :py:func:`~PIL.Image.merge` function. :param expression: A function that receives a dictionary. - :param options: Values to add to the function's dictionary. You - can either use a dictionary, or one or more keyword - arguments. + :param **kw: Values to add to the function's dictionary. :return: The expression result. This is usually an image object, but can also be an integer, a floating point value, or a pixel tuple, depending on the expression. """ args: dict[str, Any] = ops.copy() - args.update(options) args.update(kw) for k, v in args.items(): - if hasattr(v, "im"): + if isinstance(v, Image.Image): args[k] = _Operand(v) out = expression(args) @@ -271,11 +264,7 @@ def lambda_eval( return out -def unsafe_eval( - expression: str, - options: dict[str, Any] = {}, - **kw: Any, -) -> Any: +def unsafe_eval(expression: str, **kw: Any) -> Any: """ Evaluates an image expression. This uses Python's ``eval()`` function to process the expression string, and carries the security risks of doing so. It is not @@ -287,9 +276,7 @@ def unsafe_eval( :py:func:`~PIL.Image.merge` function. :param expression: A string containing a Python-style expression. - :param options: Values to add to the evaluation context. You - can either use a dictionary, or one or more keyword - arguments. + :param **kw: Values to add to the evaluation context. :return: The evaluated expression. This is usually an image object, but can also be an integer, a floating point value, or a pixel tuple, depending on the expression. @@ -297,15 +284,14 @@ def unsafe_eval( # build execution namespace args: dict[str, Any] = ops.copy() - for k in list(options.keys()) + list(kw.keys()): + for k in kw: if "__" in k or hasattr(builtins, k): msg = f"'{k}' not allowed" raise ValueError(msg) - args.update(options) args.update(kw) for k, v in args.items(): - if hasattr(v, "im"): + if isinstance(v, Image.Image): args[k] = _Operand(v) compiled_code = compile(expression, "", "eval") @@ -326,32 +312,3 @@ def scan(code: CodeType) -> None: return out.im except AttributeError: return out - - -def eval( - expression: str, - _dict: dict[str, Any] = {}, - **kw: Any, -) -> Any: - """ - Evaluates an image expression. - - Deprecated. Use lambda_eval() or unsafe_eval() instead. - - :param expression: A string containing a Python-style expression. - :param _dict: Values to add to the evaluation context. You - can either use a dictionary, or one or more keyword - arguments. - :return: The evaluated expression. This is usually an image object, but can - also be an integer, a floating point value, or a pixel tuple, - depending on the expression. - - .. deprecated:: 10.3.0 - """ - - deprecate( - "ImageMath.eval", - 12, - "ImageMath.lambda_eval or ImageMath.unsafe_eval", - ) - return unsafe_eval(expression, _dict, **kw) diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 92a08d2cbcb..b7c6c863659 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -18,8 +18,6 @@ from functools import lru_cache from typing import NamedTuple -from ._deprecate import deprecate - class ModeDescriptor(NamedTuple): """Wrapper for mode strings.""" @@ -57,16 +55,11 @@ def getmode(mode: str) -> ModeDescriptor: "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"), "|u1"), - "BGR;16": ("RGB", "L", ("B", "G", "R"), "|u1"), - "BGR;24": ("RGB", "L", ("B", "G", "R"), "|u1"), "LA": ("L", "L", ("L", "A"), "|u1"), "La": ("L", "L", ("L", "a"), "|u1"), "PA": ("RGB", "L", ("P", "A"), "|u1"), } if mode in modes: - if mode in ("BGR;15", "BGR;16", "BGR;24"): - deprecate(mode, 12) base_mode, base_type, bands, type_str = modes[mode] return ModeDescriptor(mode, bands, base_mode, base_type, type_str) diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 6a43983d33d..9fcd8d78db4 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -65,10 +65,12 @@ class LutBuilder: def __init__( self, patterns: list[str] | None = None, op_name: str | None = None ) -> None: - if patterns is not None: - self.patterns = patterns - else: - self.patterns = [] + """ + :param patterns: A list of input patterns, or None. + :param op_name: The name of a known pattern. One of "corner", "dilation4", + "dilation8", "erosion4", "erosion8" or "edge". + :exception Exception: If the op_name is not recognized. + """ self.lut: bytearray | None = None if op_name is not None: known_patterns = { @@ -88,20 +90,38 @@ def __init__( raise Exception(msg) self.patterns = known_patterns[op_name] + elif patterns is not None: + self.patterns = patterns + else: + self.patterns = [] def add_patterns(self, patterns: list[str]) -> None: + """ + Append to list of patterns. + + :param patterns: Additional patterns. + """ self.patterns += patterns - def build_default_lut(self) -> None: + def build_default_lut(self) -> bytearray: + """ + Set the current LUT, and return it. + + This is the default LUT that patterns will be applied against when building. + """ symbols = [0, 1] m = 1 << 4 # pos of current pixel self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE)) + return self.lut def get_lut(self) -> bytearray | None: + """ + Returns the current LUT + """ return self.lut def _string_permute(self, pattern: str, permutation: list[int]) -> str: - """string_permute takes a pattern and a permutation and returns the + """Takes a pattern and a permutation and returns the string permuted according to the permutation list. """ assert len(permutation) == 9 @@ -110,7 +130,7 @@ def _string_permute(self, pattern: str, permutation: list[int]) -> str: def _pattern_permute( self, basic_pattern: str, options: str, basic_result: int ) -> list[tuple[str, int]]: - """pattern_permute takes a basic pattern and its result and clones + """Takes a basic pattern and its result and clones the pattern according to the modifications described in the $options parameter. It returns a list of all cloned patterns.""" patterns = [(basic_pattern, basic_result)] @@ -140,17 +160,16 @@ def _pattern_permute( return patterns def build_lut(self) -> bytearray: - """Compile all patterns into a morphology lut. + """Compile all patterns into a morphology LUT, and return it. - TBD :Build based on (file) morphlut:modify_lut - """ + This is the data to be passed into MorphOp.""" self.build_default_lut() assert self.lut is not None patterns = [] # 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: msg = 'Syntax error in pattern "' + p + '"' raise Exception(msg) @@ -163,15 +182,14 @@ def build_lut(self) -> bytearray: patterns += self._pattern_permute(pattern, options, result) - # compile the patterns into regular expressions for speed + # Compile the patterns into regular expressions for speed compiled_patterns = [] for pattern in patterns: p = pattern[0].replace(".", "X").replace("X", "[01]") compiled_patterns.append((re.compile(p), pattern[1])) # Step through table and find patterns that match. - # Note that all the patterns are searched. The last one - # caught overrides + # Note that all the patterns are searched. The last one found takes priority for i in range(LUT_SIZE): # Build the bit pattern bitpattern = bin(i)[2:] @@ -193,57 +211,82 @@ def __init__( op_name: str | None = None, patterns: list[str] | None = None, ) -> None: - """Create a binary morphological operator""" - self.lut = lut - if op_name is not None: - self.lut = LutBuilder(op_name=op_name).build_lut() - elif patterns is not None: - self.lut = LutBuilder(patterns=patterns).build_lut() + """Create a binary morphological operator. + + If the LUT is not provided, then it is built using LutBuilder from the op_name + or the patterns. + + :param lut: The LUT data. + :param patterns: A list of input patterns, or None. + :param op_name: The name of a known pattern. One of "corner", "dilation4", + "dilation8", "erosion4", "erosion8", "edge". + :exception Exception: If the op_name is not recognized. + """ + if patterns is None and op_name is None: + self.lut = lut + else: + self.lut = LutBuilder(patterns, op_name).build_lut() def apply(self, image: Image.Image) -> tuple[int, Image.Image]: - """Run a single morphological operation on an image + """Run a single morphological operation on an image. Returns a tuple of the number of changed pixels and the - morphed image""" + morphed image. + + :param image: A 1-mode or L-mode image. + :exception Exception: If the current operator is None. + :exception ValueError: If the image is not 1 or L mode.""" if self.lut is None: msg = "No operator loaded" raise Exception(msg) - if image.mode != "L": - msg = "Image mode must be L" + if image.mode not in ("1", "L"): + msg = "Image mode must be 1 or L" raise ValueError(msg) - outimage = Image.new(image.mode, image.size, None) - count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) + outimage = Image.new(image.mode, image.size) + count = _imagingmorph.apply(bytes(self.lut), image.getim(), outimage.getim()) return count, outimage def match(self, image: Image.Image) -> list[tuple[int, int]]: """Get a list of coordinates matching the morphological operation on an image. - Returns a list of tuples of (x,y) coordinates - of all matching pixels. See :ref:`coordinate-system`.""" + Returns a list of tuples of (x,y) coordinates of all matching pixels. See + :ref:`coordinate-system`. + + :param image: A 1-mode or L-mode image. + :exception Exception: If the current operator is None. + :exception ValueError: If the image is not 1 or L mode.""" if self.lut is None: msg = "No operator loaded" raise Exception(msg) - if image.mode != "L": - msg = "Image mode must be L" + if image.mode not in ("1", "L"): + msg = "Image mode must be 1 or L" raise ValueError(msg) - return _imagingmorph.match(bytes(self.lut), image.im.id) + return _imagingmorph.match(bytes(self.lut), image.getim()) def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: - """Get a list of all turned on pixels in a binary image + """Get a list of all turned on pixels in a 1 or L mode image. + + Returns a list of tuples of (x,y) coordinates of all non-empty pixels. See + :ref:`coordinate-system`. - Returns a list of tuples of (x,y) coordinates - of all matching pixels. See :ref:`coordinate-system`.""" + :param image: A 1-mode or L-mode image. + :exception ValueError: If the image is not 1 or L mode.""" - if image.mode != "L": - msg = "Image mode must be L" + if image.mode not in ("1", "L"): + msg = "Image mode must be 1 or L" raise ValueError(msg) - return _imagingmorph.get_on_pixels(image.im.id) + return _imagingmorph.get_on_pixels(image.getim()) def load_lut(self, filename: str) -> None: - """Load an operator from an mrl file""" + """ + Load an operator from an mrl file + + :param filename: The file to read from. + :exception Exception: If the length of the file data is not 512. + """ with open(filename, "rb") as f: self.lut = bytearray(f.read()) @@ -253,7 +296,12 @@ def load_lut(self, filename: str) -> None: raise Exception(msg) def save_lut(self, filename: str) -> None: - """Save an operator to an mrl file""" + """ + Save an operator to an mrl file. + + :param filename: The destination file. + :exception Exception: If the current operator is None. + """ if self.lut is None: msg = "No operator loaded" raise Exception(msg) @@ -261,5 +309,9 @@ def save_lut(self, filename: str) -> None: f.write(self.lut) def set_lut(self, lut: bytearray | None) -> None: - """Set the lut from an external source""" + """ + Set the LUT from an external source + + :param lut: A new LUT. + """ self.lut = lut diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index a84c0834561..42b10bd7bc8 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -21,7 +21,8 @@ import functools import operator import re -from typing import Protocol, Sequence, cast +from collections.abc import Sequence +from typing import Literal, Protocol, cast, overload from . import ExifTags, Image, ImagePalette @@ -212,14 +213,14 @@ def colorize( blue = [] # Create the low-end values - for i in range(0, blackpoint): + for i in range(blackpoint): red.append(rgb_black[0]) green.append(rgb_black[1]) blue.append(rgb_black[2]) # Create the mapping (2-color) if rgb_mid is None: - range_map = range(0, whitepoint - blackpoint) + range_map = range(whitepoint - blackpoint) for i in range_map: red.append( @@ -234,8 +235,8 @@ def colorize( # Create the mapping (3-color) else: - range_map1 = range(0, midpoint - blackpoint) - range_map2 = range(0, whitepoint - midpoint) + range_map1 = range(midpoint - blackpoint) + range_map2 = range(whitepoint - midpoint) for i in range_map1: red.append( @@ -255,7 +256,7 @@ def colorize( blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2)) # Create the high-end values - for i in range(0, 256 - whitepoint): + for i in range(256 - whitepoint): red.append(rgb_white[0]) green.append(rgb_white[1]) blue.append(rgb_white[2]) @@ -361,7 +362,9 @@ def pad( else: out = Image.new(image.mode, size, color) if resized.palette: - out.putpalette(resized.getpalette()) + palette = resized.getpalette() + if palette is not None: + out.putpalette(palette) if resized.width != size[0]: x = round((size[0] - resized.width) * max(0, min(centering[0], 1))) out.paste(resized, (x, 0)) @@ -496,14 +499,15 @@ def expand( height = top + image.size[1] + bottom color = _color(fill, image.mode) if image.palette: - palette = ImagePalette.ImagePalette(palette=image.getpalette()) + mode = image.palette.mode + palette = ImagePalette.ImagePalette(mode, image.getpalette(mode)) if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4): color = palette.getcolor(color) else: palette = None out = Image.new(image.mode, (width, height), color) if palette: - out.putpalette(palette.palette) + out.putpalette(palette.palette, mode) out.paste(image, (left, top)) return out @@ -670,6 +674,16 @@ def solarize(image: Image.Image, threshold: int = 128) -> Image.Image: return _lut(image, lut) +@overload +def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ... + + +@overload +def exif_transpose( + image: Image.Image, *, in_place: Literal[False] = False +) -> Image.Image: ... + + def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None: """ If an image has an EXIF Orientation tag, other than 1, transpose the image @@ -695,11 +709,11 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image 8: Image.Transpose.ROTATE_90, }.get(orientation) if method is not None: - transposed_image = image.transpose(method) if in_place: - image.im = transposed_image.im - image.pyaccess = None - image._size = transposed_image._size + image.im = image.im.transpose(method) + image._size = image.im.size + else: + transposed_image = image.transpose(method) exif_image = image if in_place else transposed_image exif = exif_image.getexif() @@ -716,11 +730,15 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image r"([0-9])", ): value = exif_image.info[key] - exif_image.info[key] = ( - re.sub(pattern, "", value) - if isinstance(value, str) - else re.sub(pattern.encode(), b"", value) - ) + if isinstance(value, str): + value = re.sub(pattern, "", value) + elif isinstance(value, tuple): + value = tuple( + re.sub(pattern.encode(), b"", v) for v in value + ) + else: + value = re.sub(pattern.encode(), b"", value) + exif_image.info[key] = value if not in_place: return transposed_image elif not in_place: diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index ed38285dc28..2abbd46eaf1 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -18,10 +18,12 @@ from __future__ import annotations import array -from typing import IO, TYPE_CHECKING, Sequence +from collections.abc import Sequence +from typing import IO from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile +TYPE_CHECKING = False if TYPE_CHECKING: from . import Image @@ -116,7 +118,7 @@ def _new_color_index( ) -> int: if not isinstance(self.palette, bytearray): self._palette = bytearray(self.palette) - index = len(self.palette) // 3 + index = len(self.palette) // len(self.mode) special_colors: tuple[int | tuple[int, ...] | None, ...] = () if image: special_colors = ( @@ -166,11 +168,12 @@ def getcolor( index = self._new_color_index(image, e) assert isinstance(self._palette, bytearray) self.colors[color] = index - if index * 3 < len(self.palette): + mode_len = len(self.mode) + if index * mode_len < len(self.palette): self._palette = ( - self._palette[: index * 3] + self._palette[: index * mode_len] + bytes(color) - + self._palette[index * 3 + 3 :] + + self._palette[index * mode_len + mode_len :] ) else: self._palette += bytes(color) @@ -188,26 +191,29 @@ def save(self, fp: str | IO[str]) -> None: if self.rawmode: msg = "palette contains raw palette data" raise ValueError(msg) + open_fp = False if isinstance(fp, str): fp = open(fp, "w") - fp.write("# Palette\n") - fp.write(f"# Mode: {self.mode}\n") - for i in range(256): - fp.write(f"{i}") - for j in range(i * len(self.mode), (i + 1) * len(self.mode)): - try: - fp.write(f" {self.palette[j]}") - except IndexError: - fp.write(" 0") - fp.write("\n") - fp.close() + open_fp = True + try: + fp.write("# Palette\n") + fp.write(f"# Mode: {self.mode}\n") + palette_len = len(self.palette) + for i in range(256): + fp.write(f"{i}") + for j in range(i * len(self.mode), (i + 1) * len(self.mode)): + fp.write(f" {self.palette[j] if j < palette_len else 0}") + fp.write("\n") + finally: + if open_fp: + fp.close() # -------------------------------------------------------------------- # Internal -def raw(rawmode, data: Sequence[int] | bytes | bytearray) -> ImagePalette: +def raw(rawmode: str, data: Sequence[int] | bytes | bytearray) -> ImagePalette: palette = ImagePalette() palette.rawmode = rawmode palette.palette = data diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 35a37760c76..af4d0742d6b 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -19,11 +19,19 @@ import sys from io import BytesIO -from typing import Callable from . import Image from ._util import is_path +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any + + from . import ImageFile + + QBuffer: type + qt_version: str | None qt_versions = [ ["6", "PyQt6"], @@ -34,17 +42,17 @@ qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True) for version, qt_module in qt_versions: try: - QBuffer: type - QIODevice: type - QImage: type - QPixmap: type qRgba: Callable[[int, int, int, int], int] if qt_module == "PyQt6": - from PyQt6.QtCore import QBuffer, QIODevice + from PyQt6.QtCore import QBuffer, QByteArray, 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 + from PySide6.QtCore import ( # type: ignore[assignment] + QBuffer, + QByteArray, + QIODevice, + ) + from PySide6.QtGui import QImage, QPixmap, qRgba # type: ignore[assignment] except (ImportError, RuntimeError): continue qt_is_installed = True @@ -55,26 +63,27 @@ qt_version = None -def rgb(r, g, b, a=255): +def rgb(r: int, g: int, b: int, a: int = 255) -> int: """(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 -def fromqimage(im): +def fromqimage(im: QImage | QPixmap) -> ImageFile.ImageFile: """ :param im: QImage or PIL ImageQt object """ buffer = QBuffer() + qt_openmode: object if qt_version == "6": try: - qt_openmode = QIODevice.OpenModeFlag + qt_openmode = getattr(QIODevice, "OpenModeFlag") except AttributeError: - qt_openmode = QIODevice.OpenMode + qt_openmode = getattr(QIODevice, "OpenMode") else: qt_openmode = QIODevice - buffer.open(qt_openmode.ReadWrite) + buffer.open(getattr(qt_openmode, "ReadWrite")) # preserve alpha channel with png # otherwise ppm is more friendly with Image.open if im.hasAlphaChannel(): @@ -90,11 +99,11 @@ def fromqimage(im): return Image.open(b) -def fromqpixmap(im): +def fromqpixmap(im: QPixmap) -> ImageFile.ImageFile: return fromqimage(im) -def align8to32(bytes, width, mode): +def align8to32(bytes: bytes, width: int, mode: str) -> bytes: """ converts each scanline of data from 8 bit to 32 bit aligned """ @@ -120,7 +129,7 @@ def align8to32(bytes, width, mode): return b"".join(new_data) -def _toqclass_helper(im): +def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]: data = None colortable = None exclusive_fp = False @@ -132,30 +141,32 @@ def _toqclass_helper(im): if is_path(im): im = Image.open(im) exclusive_fp = True + assert isinstance(im, Image.Image) - qt_format = QImage.Format if qt_version == "6" else QImage + qt_format = getattr(QImage, "Format") if qt_version == "6" else QImage if im.mode == "1": - format = qt_format.Format_Mono + format = getattr(qt_format, "Format_Mono") elif im.mode == "L": - format = qt_format.Format_Indexed8 + format = getattr(qt_format, "Format_Indexed8") colortable = [rgb(i, i, i) for i in range(256)] elif im.mode == "P": - format = qt_format.Format_Indexed8 + format = getattr(qt_format, "Format_Indexed8") palette = im.getpalette() + assert palette is not None colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)] elif im.mode == "RGB": # Populate the 4th channel with 255 im = im.convert("RGBA") data = im.tobytes("raw", "BGRA") - format = qt_format.Format_RGB32 + format = getattr(qt_format, "Format_RGB32") elif im.mode == "RGBA": data = im.tobytes("raw", "BGRA") - format = qt_format.Format_ARGB32 + format = getattr(qt_format, "Format_ARGB32") elif im.mode == "I;16": im = im.point(lambda i: i * 256) - format = qt_format.Format_Grayscale16 + format = getattr(qt_format, "Format_Grayscale16") else: if exclusive_fp: im.close() @@ -172,7 +183,7 @@ def _toqclass_helper(im): if qt_is_installed: class ImageQt(QImage): - def __init__(self, im): + def __init__(self, im: Image.Image | str | QByteArray) -> None: """ An PIL image wrapper for Qt. This is a subclass of PyQt's QImage class. @@ -196,10 +207,13 @@ def __init__(self, im): self.setColorTable(im_data["colortable"]) -def toqimage(im) -> ImageQt: +def toqimage(im: Image.Image | str | QByteArray) -> ImageQt: return ImageQt(im) -def toqpixmap(im): +def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap: qimage = toqimage(im) - return QPixmap.fromImage(qimage) + pixmap = getattr(QPixmap, "fromImage")(qimage) + if qt_version == "6": + pixmap.detach() + return pixmap diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index 2c185027630..361be48971e 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -16,10 +16,12 @@ ## from __future__ import annotations -from typing import Callable - from . import Image +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + class Iterator: """ @@ -33,7 +35,7 @@ class Iterator: :param im: An image object. """ - def __init__(self, im: Image.Image): + def __init__(self, im: Image.Image) -> None: if not hasattr(im, "seek"): msg = "im must have seek method" raise AttributeError(msg) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 037d6f492ce..7705608e3ec 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -26,7 +26,7 @@ _viewers = [] -def register(viewer, order: int = 1) -> None: +def register(viewer: type[Viewer] | Viewer, order: int = 1) -> None: """ The :py:func:`register` function is used to register additional viewers:: @@ -40,11 +40,8 @@ def register(viewer, order: int = 1) -> None: 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() - except TypeError: - pass # raised if viewer wasn't a class + if isinstance(viewer, type) and issubclass(viewer, Viewer): + viewer = viewer() if order > 0: _viewers.append(viewer) else: @@ -178,7 +175,9 @@ def show_file(self, path: str, **options: Any) -> int: if not os.path.exists(path): raise FileNotFoundError subprocess.call(["open", "-a", "Preview.app", path]) - executable = sys.executable or shutil.which("python3") + + pyinstaller = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS") + executable = (not pyinstaller and sys.executable) or shutil.which("python3") if executable: subprocess.Popen( [ @@ -195,7 +194,7 @@ def show_file(self, path: str, **options: Any) -> int: register(MacViewer) -class UnixViewer(Viewer): +class UnixViewer(abc.ABC, Viewer): format = "PNG" options = {"compress_level": 1, "save_all": True} diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py index 8bc504526f0..3a1044ba449 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -120,7 +120,7 @@ def sum2(self) -> list[float]: @cached_property def mean(self) -> list[float]: """Average (arithmetic mean) pixel level for each band in the image.""" - return [self.sum[i] / self.count[i] for i in self.bands] + return [self.sum[i] / self.count[i] if self.count[i] else 0 for i in self.bands] @cached_property def median(self) -> list[int]: @@ -141,13 +141,20 @@ def median(self) -> list[int]: @cached_property def rms(self) -> list[float]: """RMS (root-mean-square) for each band in the image.""" - return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands] + return [ + math.sqrt(self.sum2[i] / self.count[i]) if self.count[i] else 0 + for i in self.bands + ] @cached_property def var(self) -> list[float]: """Variance for each band in the image.""" return [ - (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i] + ( + (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i] + if self.count[i] + else 0 + ) for i in self.bands ] diff --git a/src/PIL/ImageText.py b/src/PIL/ImageText.py new file mode 100644 index 00000000000..008d20d38e1 --- /dev/null +++ b/src/PIL/ImageText.py @@ -0,0 +1,508 @@ +from __future__ import annotations + +import math +import re +from typing import AnyStr, Generic, NamedTuple + +from . import ImageFont +from ._typing import _Ink + +Font = ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont + + +class _Line(NamedTuple): + x: float + y: float + anchor: str + text: str | bytes + + +class _Wrap(Generic[AnyStr]): + lines: list[AnyStr] = [] + position = 0 + offset = 0 + + def __init__( + self, + text: Text[AnyStr], + width: int, + height: int | None = None, + font: Font | None = None, + ) -> None: + self.text: Text[AnyStr] = text + self.width = width + self.height = height + self.font = font + + input_text = self.text.text + emptystring = "" if isinstance(input_text, str) else b"" + line = emptystring + + for word in re.findall( + r"\s*\S+" if isinstance(input_text, str) else rb"\s*\S+", input_text + ): + newlines = re.findall( + r"[^\S\n]*\n" if isinstance(input_text, str) else rb"[^\S\n]*\n", word + ) + if newlines: + if not self.add_line(line): + break + for i, line in enumerate(newlines): + if i != 0 and not self.add_line(emptystring): + break + self.position += len(line) + word = word[len(line) :] + line = emptystring + + new_line = line + word + if self.text._get_bbox(new_line, self.font)[2] <= width: + # This word fits on the line + line = new_line + continue + + # This word does not fit on the line + if line and not self.add_line(line): + break + + original_length = len(word) + word = word.lstrip() + self.offset = original_length - len(word) + + if self.text._get_bbox(word, self.font)[2] > width: + if font is None: + msg = "Word does not fit within line" + raise ValueError(msg) + break + line = word + else: + if line: + self.add_line(line) + self.remaining_text: AnyStr = input_text[self.position :] + + def add_line(self, line: AnyStr) -> bool: + lines = self.lines + [line] + if self.height is not None: + last_line_y = self.text._split(lines=lines)[-1].y + last_line_height = self.text._get_bbox(line, self.font)[3] + if last_line_y + last_line_height > self.height: + return False + + self.lines = lines + self.position += len(line) + self.offset + self.offset = 0 + return True + + +class Text(Generic[AnyStr]): + def __init__( + self, + text: AnyStr, + font: Font | None = None, + mode: str = "RGB", + spacing: float = 4, + direction: str | None = None, + features: list[str] | None = None, + language: str | None = None, + ) -> None: + """ + :param text: String to be drawn. + :param font: Either an :py:class:`~PIL.ImageFont.ImageFont` instance, + :py:class:`~PIL.ImageFont.FreeTypeFont` instance, + :py:class:`~PIL.ImageFont.TransposedFont` instance or ``None``. If + ``None``, the default font from :py:meth:`.ImageFont.load_default` + will be used. + :param mode: The image mode this will be used with. + :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) 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. + """ + self.text: AnyStr = text + self.font = font or ImageFont.load_default() + + self.mode = mode + self.spacing = spacing + self.direction = direction + self.features = features + self.language = language + + self.embedded_color = False + + self.stroke_width: float = 0 + self.stroke_fill: _Ink | None = None + + def embed_color(self) -> None: + """ + Use embedded color glyphs (COLR, CBDT, SBIX). + """ + if self.mode not in ("RGB", "RGBA"): + msg = "Embedded color supported only in RGB and RGBA modes" + raise ValueError(msg) + self.embedded_color = True + + def stroke(self, width: float = 0, fill: _Ink | None = None) -> None: + """ + :param width: The width of the text stroke. + :param fill: Color to use for the text stroke when drawing. If not given, will + default to the ``fill`` parameter from + :py:meth:`.ImageDraw.ImageDraw.text`. + """ + self.stroke_width = width + self.stroke_fill = fill + + def _get_fontmode(self) -> str: + if self.mode in ("1", "P", "I", "F"): + return "1" + elif self.embedded_color: + return "RGBA" + else: + return "L" + + def wrap( + self, + width: int, + height: int | None = None, + scaling: str | tuple[str, int] | None = None, + ) -> Text[AnyStr] | None: + """ + Wrap text to fit within a given width. + + :param width: The width to fit within. + :param height: An optional height limit. Any text that does not fit within this + will be returned as a new :py:class:`.Text` object. + :param scaling: An optional directive to scale the text, either "grow" as much + as possible within the given dimensions, or "shrink" until it + fits. It can also be a tuple of (direction, limit), with an + integer limit to stop scaling at. + + :returns: An :py:class:`.Text` object, or None. + """ + if isinstance(self.font, ImageFont.TransposedFont): + msg = "TransposedFont not supported" + raise ValueError(msg) + if self.direction not in (None, "ltr"): + msg = "Only ltr direction supported" + raise ValueError(msg) + + if scaling is None: + wrap = _Wrap(self, width, height) + else: + if not isinstance(self.font, ImageFont.FreeTypeFont): + msg = "'scaling' only supports FreeTypeFont" + raise ValueError(msg) + if height is None: + msg = "'scaling' requires 'height'" + raise ValueError(msg) + + if isinstance(scaling, str): + limit = 1 + else: + scaling, limit = scaling + + font = self.font + wrap = _Wrap(self, width, height, font) + if scaling == "shrink": + if not wrap.remaining_text: + return None + + size = math.ceil(font.size) + while wrap.remaining_text: + if size == max(limit, 1): + msg = "Text could not be scaled" + raise ValueError(msg) + size -= 1 + font = self.font.font_variant(size=size) + wrap = _Wrap(self, width, height, font) + self.font = font + else: + if wrap.remaining_text: + msg = "Text could not be scaled" + raise ValueError(msg) + + size = math.floor(font.size) + while not wrap.remaining_text: + if size == limit: + msg = "Text could not be scaled" + raise ValueError(msg) + size += 1 + font = self.font.font_variant(size=size) + last_wrap = wrap + wrap = _Wrap(self, width, height, font) + size -= 1 + if size != self.font.size: + self.font = self.font.font_variant(size=size) + wrap = last_wrap + + if wrap.remaining_text: + text = Text( + text=wrap.remaining_text, + font=self.font, + mode=self.mode, + spacing=self.spacing, + direction=self.direction, + features=self.features, + language=self.language, + ) + text.embedded_color = self.embedded_color + text.stroke_width = self.stroke_width + text.stroke_fill = self.stroke_fill + else: + text = None + + newline = "\n" if isinstance(self.text, str) else b"\n" + self.text = newline.join(wrap.lines) + return text + + def get_length(self) -> float: + """ + Returns length (in pixels with 1/64 precision) of text. + + 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:: + + hello = ImageText.Text("Hello", font).get_length() + world = ImageText.Text("World", font).get_length() + helloworld = ImageText.Text("HelloWorld", font).get_length() + assert hello + world == helloworld + + use:: + + hello = ( + ImageText.Text("HelloW", font).get_length() - + ImageText.Text("W", font).get_length() + ) # adjusted for kerning + world = ImageText.Text("World", font).get_length() + helloworld = ImageText.Text("HelloWorld", font).get_length() + assert hello + world == helloworld + + or disable kerning with (requires libraqm):: + + hello = ImageText.Text("Hello", font, features=["-kern"]).get_length() + world = ImageText.Text("World", font, features=["-kern"]).get_length() + helloworld = ImageText.Text( + "HelloWorld", font, features=["-kern"] + ).get_length() + assert hello + world == helloworld + + :return: Either width for horizontal text, or height for vertical text. + """ + if isinstance(self.text, str): + multiline = "\n" in self.text + else: + multiline = b"\n" in self.text + if multiline: + msg = "can't measure length of multiline text" + raise ValueError(msg) + return self.font.getlength( + self.text, + self._get_fontmode(), + self.direction, + self.features, + self.language, + ) + + def _split( + self, + xy: tuple[float, float] = (0, 0), + anchor: str | None = None, + align: str = "left", + lines: list[str] | list[bytes] | None = None, + ) -> list[_Line]: + if anchor is None: + anchor = "lt" if self.direction == "ttb" else "la" + elif len(anchor) != 2: + msg = "anchor must be a 2 character string" + raise ValueError(msg) + + if lines is None: + lines = ( + self.text.split("\n") + if isinstance(self.text, str) + else self.text.split(b"\n") + ) + if len(lines) == 1: + return [_Line(xy[0], xy[1], anchor, lines[0])] + + if anchor[1] in "tb" and self.direction != "ttb": + msg = "anchor not supported for multiline text" + raise ValueError(msg) + + fontmode = self._get_fontmode() + line_spacing = ( + self.font.getbbox( + "A", + fontmode, + None, + self.features, + self.language, + self.stroke_width, + )[3] + + self.stroke_width + + self.spacing + ) + + top = xy[1] + parts = [] + if self.direction == "ttb": + left = xy[0] + for line in lines: + parts.append(_Line(left, top, anchor, line)) + left += line_spacing + else: + widths = [] + max_width: float = 0 + for line in lines: + line_width = self.font.getlength( + line, fontmode, self.direction, self.features, self.language + ) + widths.append(line_width) + max_width = max(max_width, line_width) + + if anchor[1] == "m": + top -= (len(lines) - 1) * line_spacing / 2.0 + elif anchor[1] == "d": + top -= (len(lines) - 1) * line_spacing + + idx = -1 + for line in lines: + left = xy[0] + idx += 1 + width_difference = max_width - widths[idx] + + # align by align parameter + if align in ("left", "justify"): + pass + elif align == "center": + left += width_difference / 2.0 + elif align == "right": + left += width_difference + else: + msg = 'align must be "left", "center", "right" or "justify"' + raise ValueError(msg) + + if ( + align == "justify" + and width_difference != 0 + and idx != len(lines) - 1 + ): + words = ( + line.split(" ") if isinstance(line, str) else line.split(b" ") + ) + if len(words) > 1: + # align left by anchor + if anchor[0] == "m": + left -= max_width / 2.0 + elif anchor[0] == "r": + left -= max_width + + word_widths = [ + self.font.getlength( + word, + fontmode, + self.direction, + self.features, + self.language, + ) + for word in words + ] + word_anchor = "l" + anchor[1] + width_difference = max_width - sum(word_widths) + i = 0 + for word in words: + parts.append(_Line(left, top, word_anchor, word)) + left += word_widths[i] + width_difference / (len(words) - 1) + i += 1 + top += line_spacing + continue + + # align left by anchor + if anchor[0] == "m": + left -= width_difference / 2.0 + elif anchor[0] == "r": + left -= width_difference + parts.append(_Line(left, top, anchor, line)) + top += line_spacing + + return parts + + def _get_bbox( + self, text: str | bytes, font: Font | None = None, anchor: str | None = None + ) -> tuple[float, float, float, float]: + return (font or self.font).getbbox( + text, + self._get_fontmode(), + self.direction, + self.features, + self.language, + self.stroke_width, + anchor, + ) + + def get_bbox( + self, + xy: tuple[float, float] = (0, 0), + anchor: str | None = None, + align: str = "left", + ) -> tuple[float, float, float, float]: + """ + Returns bounding box (in pixels) of text. + + Use :py:meth:`get_length` 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. + + :param xy: The anchor coordinates of the text. + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left, + specifically ``la`` for horizontal text and ``lt`` for + vertical text. See :ref:`text-anchors` for details. + :param align: For multiline text, ``"left"``, ``"center"``, ``"right"`` or + ``"justify"`` determines the relative alignment of lines. Use the + ``anchor`` parameter to specify the alignment to ``xy``. + + :return: ``(left, top, right, bottom)`` bounding box + """ + bbox: tuple[float, float, float, float] | None = None + for x, y, anchor, text in self._split(xy, anchor, align): + bbox_line = self._get_bbox(text, anchor=anchor) + bbox_line = ( + bbox_line[0] + x, + bbox_line[1] + y, + bbox_line[2] + x, + bbox_line[3] + y, + ) + 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]), + ) + + assert bbox is not None + return bbox diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 90defdbbc23..3a4cb81e9ef 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -28,48 +28,42 @@ import tkinter from io import BytesIO +from typing import Any -from . import Image +from . import Image, ImageFile + +TYPE_CHECKING = False +if TYPE_CHECKING: + from ._typing import CapsuleType # -------------------------------------------------------------------- # Check for Tkinter interface hooks -_pilbitmap_ok = None - - -def _pilbitmap_check() -> int: - global _pilbitmap_ok - if _pilbitmap_ok is None: - try: - im = Image.new("1", (1, 1)) - tkinter.BitmapImage(data=f"PIL:{im.im.id}") - _pilbitmap_ok = 1 - except tkinter.TclError: - _pilbitmap_ok = 0 - return _pilbitmap_ok - -def _get_image_from_kw(kw): +def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None: source = None if "file" in kw: source = kw.pop("file") elif "data" in kw: source = BytesIO(kw.pop("data")) - if source: - return Image.open(source) + if not source: + return None + return Image.open(source) -def _pyimagingtkcall(command, photo, id): +def _pyimagingtkcall( + command: str, photo: PhotoImage | tkinter.PhotoImage, ptr: CapsuleType +) -> None: tk = photo.tk try: - tk.call(command, photo, id) + tk.call(command, photo, repr(ptr)) 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) + tk.call(command, photo, repr(ptr)) # -------------------------------------------------------------------- @@ -96,27 +90,36 @@ class PhotoImage: image file). """ - def __init__(self, image=None, size=None, **kw): + def __init__( + self, + image: Image.Image | str | None = None, + size: tuple[int, int] | None = None, + **kw: Any, + ) -> None: # Tk compatibility: file or data if image is None: image = _get_image_from_kw(kw) - if hasattr(image, "mode") and hasattr(image, "size"): + if image is None: + msg = "Image is required" + raise ValueError(msg) + elif isinstance(image, str): + mode = image + image = None + + if size is None: + msg = "If first argument is mode, size is required" + raise ValueError(msg) + else: # got an image instead of a mode mode = image.mode if mode == "P": # palette mapped data image.apply_transparency() image.load() - try: - mode = image.palette.mode - except AttributeError: - mode = "RGB" # default + mode = image.palette.mode if image.palette else "RGB" size = image.size kw["width"], kw["height"] = size - else: - mode = image - image = None if mode not in ["1", "L", "RGB", "RGBA"]: mode = Image.getmodebase(mode) @@ -129,7 +132,10 @@ def __init__(self, image=None, size=None, **kw): self.paste(image) def __del__(self) -> None: - name = self.__photo.name + try: + name = self.__photo.name + except AttributeError: + return self.__photo.name = None try: self.__photo.tk.call("image", "delete", name) @@ -172,15 +178,14 @@ def paste(self, im: Image.Image) -> None: the bitmap image. """ # convert to blittable - im.load() + ptr = im.getim() image = im.im - if image.isblock() and im.mode == self.__mode: - block = image - else: - block = image.new_block(self.__mode, im.size) + if not image.isblock() or im.mode != self.__mode: + block = Image.core.new_block(self.__mode, im.size) image.convert2(block, image) # convert directly between buffers + ptr = block.ptr - _pyimagingtkcall("PyImagingPhoto", self.__photo, block.id) + _pyimagingtkcall("PyImagingPhoto", self.__photo, ptr) # -------------------------------------------------------------------- @@ -201,26 +206,24 @@ class BitmapImage: :param image: A PIL image. """ - def __init__(self, image=None, **kw): + def __init__(self, image: Image.Image | None = None, **kw: Any) -> None: # Tk compatibility: file or data if image is None: image = _get_image_from_kw(kw) + if image is None: + msg = "Image is required" + raise ValueError(msg) self.__mode = image.mode self.__size = image.size - if _pilbitmap_check(): - # fast way (requires the pilbitmap booster patch) - image.load() - kw["data"] = f"PIL:{image.im.id}" - self.__im = image # must keep a reference - else: - # slow but safe way - kw["data"] = image.tobitmap() - self.__photo = tkinter.BitmapImage(**kw) + self.__photo = tkinter.BitmapImage(data=image.tobitmap(), **kw) def __del__(self) -> None: - name = self.__photo.name + try: + name = self.__photo.name + except AttributeError: + return self.__photo.name = None try: self.__photo.tk.call("image", "delete", name) @@ -257,28 +260,7 @@ def __str__(self) -> str: def getimage(photo: PhotoImage) -> Image.Image: """Copies the contents of a PhotoImage to a PIL image memory.""" im = Image.new("RGBA", (photo.width(), photo.height())) - block = im.im - _pyimagingtkcall("PyImagingPhotoGet", photo, block.id) + _pyimagingtkcall("PyImagingPhotoGet", photo, im.getim()) return im - - -def _show(image, title): - """Helper for the Image.show method.""" - - class UI(tkinter.Label): - def __init__(self, master, im): - if im.mode == "1": - self.image = BitmapImage(im, foreground="white", master=master) - else: - self.image = PhotoImage(im, master=master) - super().__init__(master, image=self.image, bg="black", bd=0) - - if not tkinter._default_root: - msg = "tkinter not initialized" - raise OSError(msg) - top = tkinter.Toplevel() - if title: - top.title(title) - UI(top, image).pack() diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index ffd7916745c..fb144ff38a1 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -14,7 +14,8 @@ # from __future__ import annotations -from typing import Any, Sequence +from collections.abc import Sequence +from typing import Any from . import Image @@ -47,9 +48,9 @@ class AffineTransform(Transform): Define an affine image transform. This function takes a 6-tuple (a, b, c, d, e, f) which contain the first - two rows from an affine transform matrix. For each pixel (x, y) in the - output image, the new value is taken from a position (a x + b y + c, - d x + e y + f) in the input image, rounded to nearest pixel. + two rows from the inverse of an affine transform matrix. For each pixel + (x, y) in the output image, the new value is taken from a position (a x + + b y + c, d x + e y + f) in the input image, rounded to nearest pixel. This function can be used to scale, translate, rotate, and shear the original image. @@ -57,7 +58,7 @@ class AffineTransform(Transform): See :py:meth:`.Image.transform` :param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows - from an affine transform matrix. + from the inverse of an affine transform matrix. """ method = Image.Transform.AFFINE diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py index 978c5a9d176..98c28f29f1d 100644 --- a/src/PIL/ImageWin.py +++ b/src/PIL/ImageWin.py @@ -70,11 +70,14 @@ class Dib: """ def __init__( - self, image: Image.Image | str, size: tuple[int, int] | list[int] | None = None + self, image: Image.Image | str, size: tuple[int, int] | None = None ) -> None: if isinstance(image, str): mode = image image = "" + if size is None: + msg = "If first argument is mode, size is required" + raise ValueError(msg) else: mode = image.mode size = image.size @@ -87,7 +90,7 @@ def __init__( assert not isinstance(image, str) self.paste(image) - def expose(self, handle): + def expose(self, handle: int | HDC | HWND) -> None: """ Copy the bitmap contents to a device context. @@ -95,17 +98,22 @@ def expose(self, handle): HDC or HWND instance. In PythonWin, you can use ``CDC.GetHandleAttrib()`` to get a suitable handle. """ + handle_int = int(handle) if isinstance(handle, HWND): - dc = self.image.getdc(handle) + dc = self.image.getdc(handle_int) try: - result = self.image.expose(dc) + self.image.expose(dc) finally: - self.image.releasedc(handle, dc) + self.image.releasedc(handle_int, dc) else: - result = self.image.expose(handle) - return result + self.image.expose(handle_int) - def draw(self, handle, dst, src=None): + def draw( + self, + handle: int | HDC | HWND, + dst: tuple[int, int, int, int], + src: tuple[int, int, int, int] | None = None, + ) -> None: """ Same as expose, but allows you to specify where to draw the image, and what part of it to draw. @@ -115,19 +123,19 @@ def draw(self, handle, dst, src=None): the destination have different sizes, the image is resized as necessary. """ - if not src: + if src is None: src = (0, 0) + self.size + handle_int = int(handle) if isinstance(handle, HWND): - dc = self.image.getdc(handle) + dc = self.image.getdc(handle_int) try: - result = self.image.draw(dc, dst, src) + self.image.draw(dc, dst, src) finally: - self.image.releasedc(handle, dc) + self.image.releasedc(handle_int, dc) else: - result = self.image.draw(handle, dst, src) - return result + self.image.draw(handle_int, dst, src) - def query_palette(self, handle): + def query_palette(self, handle: int | HDC | HWND) -> int: """ Installs the palette associated with the image in the given device context. @@ -139,17 +147,18 @@ def query_palette(self, handle): :param handle: Device context (HDC), cast to a Python integer, or an HDC or HWND instance. - :return: A true value if one or more entries were changed (this - indicates that the image should be redrawn). + :return: The number of entries that were changed (if one or more entries, + this indicates that the image should be redrawn). """ + handle_int = int(handle) if isinstance(handle, HWND): - handle = self.image.getdc(handle) + handle = self.image.getdc(handle_int) try: result = self.image.query_palette(handle) finally: self.image.releasedc(handle, handle) else: - result = self.image.query_palette(handle) + result = self.image.query_palette(handle_int) return result def paste( @@ -202,22 +211,22 @@ def __init__( title, self.__dispatcher, width or 0, height or 0 ) - def __dispatcher(self, action, *args): - return getattr(self, f"ui_handle_{action}")(*args) + def __dispatcher(self, action: str, *args: int) -> None: + getattr(self, f"ui_handle_{action}")(*args) - def ui_handle_clear(self, dc, x0, y0, x1, y1): + def ui_handle_clear(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None: pass - def ui_handle_damage(self, x0, y0, x1, y1): + def ui_handle_damage(self, x0: int, y0: int, x1: int, y1: int) -> None: pass def ui_handle_destroy(self) -> None: pass - def ui_handle_repair(self, dc, x0, y0, x1, y1): + def ui_handle_repair(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None: pass - def ui_handle_resize(self, width, height): + def ui_handle_resize(self, width: int, height: int) -> None: pass def mainloop(self) -> None: @@ -227,12 +236,12 @@ def mainloop(self) -> None: class ImageWindow(Window): """Create an image window which displays the given image.""" - def __init__(self, image, title="PIL"): + def __init__(self, image: Image.Image | Dib, title: str = "PIL") -> None: if not isinstance(image, Dib): image = Dib(image) self.image = image width, height = image.size super().__init__(title, width=width, height=height) - def ui_handle_repair(self, dc, x0, y0, x1, y1): + def ui_handle_repair(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None: self.image.draw(dc, (x0, y0, x1, y1)) diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index abb3fb762e7..c4eccee3423 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -55,14 +55,14 @@ def _open(self) -> None: if not s: break - if s == b"\x0C": + if s == b"\x0c": # image data begins self.tile = [ - ( + ImageFile._Tile( "raw", (0, 0) + self.size, self.fp.tell() - len(buffer), - (self.mode, 0, 1), + self.mode, ) ] diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 73df83bfb31..9c8be8b4e36 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -17,24 +17,15 @@ from __future__ import annotations from io import BytesIO -from typing import Sequence +from typing import cast from . import Image, ImageFile from ._binary import i16be as i16 from ._binary import i32be as i32 -from ._deprecate import deprecate COMPRESSION = {1: "raw", 5: "jpeg"} -def __getattr__(name: str) -> bytes: - if name == "PAD": - deprecate("IptcImagePlugin.PAD", 12) - return b"\0\0\0\0" - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - # # Helpers @@ -43,24 +34,6 @@ def _i(c: bytes) -> int: return i32((b"\0\0\0\0" + c)[-4:]) -def _i8(c: int | bytes) -> int: - return c if isinstance(c, int) else c[0] - - -def i(c: bytes) -> int: - """.. deprecated:: 10.2.0""" - deprecate("IptcImagePlugin.i", 12) - return _i(c) - - -def dump(c: Sequence[int | bytes]) -> None: - """.. deprecated:: 10.2.0""" - deprecate("IptcImagePlugin.dump", 12) - for i in c: - print(f"{_i8(i):02x}", end=" ") - print() - - ## # Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields # from TIFF and JPEG files, use the getiptcinfo function. @@ -76,6 +49,7 @@ def getint(self, key: tuple[int, int]) -> int: def field(self) -> tuple[tuple[int, int] | None, int]: # # get a IPTC field header + assert self.fp is not None s = self.fp.read(5) if not s.strip(b"\x00"): return None, 0 @@ -103,6 +77,7 @@ def field(self) -> tuple[tuple[int, int] | None, int]: def _open(self) -> None: # load descriptive fields + assert self.fp is not None while True: offset = self.fp.tell() tag, size = self.field() @@ -123,16 +98,18 @@ def _open(self) -> None: # mode layers = self.info[(3, 60)][0] component = self.info[(3, 60)][1] - if (3, 65) in self.info: - id = self.info[(3, 65)][0] - 1 - else: - id = 0 if layers == 1 and not component: self._mode = "L" - elif layers == 3 and component: - self._mode = "RGB"[id] - elif layers == 4 and component: - self._mode = "CMYK"[id] + band = None + else: + if layers == 3 and component: + self._mode = "RGB" + elif layers == 4 and component: + self._mode = "CMYK" + if (3, 65) in self.info: + band = self.info[(3, 65)][0] - 1 + else: + band = 0 # size self._size = self.getint((3, 20)), self.getint((3, 30)) @@ -146,36 +123,47 @@ def _open(self) -> None: # tile if tag == (8, 10): - self.tile = [("iptc", (0, 0) + self.size, offset, compression)] - - def load(self): - if len(self.tile) != 1 or self.tile[0][0] != "iptc": - return ImageFile.ImageFile.load(self) - - offset, compression = self.tile[0][2:] - - self.fp.seek(offset) - - # Copy image data to temporary file - o = BytesIO() - if compression == "raw": - # To simplify access to the extracted file, - # prepend a PPM header - o.write(b"P5\n%d %d\n255\n" % self.size) - while True: - type, size = self.field() - if type != (8, 10): - break - while size > 0: - s = self.fp.read(min(size, 8192)) - if not s: + self.tile = [ + ImageFile._Tile("iptc", (0, 0) + self.size, offset, (compression, band)) + ] + + def load(self) -> Image.core.PixelAccess | None: + if self.tile: + args = self.tile[0].args + assert isinstance(args, tuple) + compression, band = args + + assert self.fp is not None + self.fp.seek(self.tile[0].offset) + + # Copy image data to temporary file + o = BytesIO() + if compression == "raw": + # To simplify access to the extracted file, + # prepend a PPM header + o.write(b"P5\n%d %d\n255\n" % self.size) + while True: + type, size = self.field() + if type != (8, 10): break - o.write(s) - size -= len(s) - - with Image.open(o) as _im: - _im.load() - self.im = _im.im + while size > 0: + s = self.fp.read(min(size, 8192)) + if not s: + break + o.write(s) + size -= len(s) + + with Image.open(o) as _im: + if band is not None: + bands = [Image.new("L", _im.size)] * Image.getmodebands(self.mode) + bands[band] = _im + im = Image.merge(self.mode, bands) + else: + im = _im + im.load() + self.im = im.im + self.tile = [] + return ImageFile.ImageFile.load(self) Image.register_open(IptcImageFile.format, IptcImageFile) @@ -183,7 +171,9 @@ def load(self): Image.register_extension(IptcImageFile.format, ".iim") -def getiptcinfo(im): +def getiptcinfo( + im: ImageFile.ImageFile, +) -> dict[tuple[int, int], bytes | list[bytes]] | None: """ Get IPTC information from TIFF, JPEG, or IPTC file. @@ -197,7 +187,7 @@ def getiptcinfo(im): if isinstance(im, IptcImageFile): # return info dictionary right away - return im.info + return {k: v for k, v in im.info.items() if isinstance(k, tuple)} elif isinstance(im, JpegImagePlugin.JpegImageFile): # extract the IPTC/NAA resource @@ -209,8 +199,8 @@ def getiptcinfo(im): # get raw data from the IPTC/NAA tag (PhotoShop tags the data # as 4-byte integers, so we cannot use the get method...) try: - data = im.tag.tagdata[TiffImagePlugin.IPTC_NAA_CHUNK] - except (AttributeError, KeyError): + data = im.tag_v2._tagdata[TiffImagePlugin.IPTC_NAA_CHUNK] + except KeyError: pass if data is None: @@ -220,16 +210,17 @@ def getiptcinfo(im): class FakeImage: pass - im = FakeImage() - im.__class__ = IptcImageFile + fake_im = FakeImage() + fake_im.__class__ = IptcImageFile # type: ignore[assignment] + iptc_im = cast(IptcImageFile, fake_im) # parse the IPTC information chunk - im.info = {} - im.fp = BytesIO(data) + iptc_im.info = {} + iptc_im.fp = BytesIO(data) try: - im._open() + iptc_im._open() except (IndexError, KeyError): pass # expected failure - return im.info + return {k: v for k, v in iptc_im.info.items() if isinstance(k, tuple)} diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index e50cd7799a3..cb37735300c 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -18,10 +18,15 @@ import io import os import struct -from typing import IO, Tuple, cast +from typing import cast from . import Image, ImageFile, ImagePalette, _binary +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from typing import IO + class BoxReader: """ @@ -29,7 +34,7 @@ class BoxReader: and to easily step into and read sub-boxes. """ - def __init__(self, fp, length=-1): + def __init__(self, fp: IO[bytes], length: int = -1) -> None: self.fp = fp self.has_length = length >= 0 self.length = length @@ -82,7 +87,7 @@ def next_box_type(self) -> bytes: self.remaining_in_box = -1 # Read the length and type of the next box - lbox, tbox = cast(Tuple[int, bytes], self.read_fields(">I4s")) + lbox, tbox = cast(tuple[int, bytes], self.read_fields(">I4s")) if lbox == 1: lbox = cast(int, self.read_fields(">Q")[0]) hlen = 16 @@ -97,7 +102,7 @@ def next_box_type(self) -> bytes: return tbox -def _parse_codestream(fp) -> tuple[tuple[int, int], str]: +def _parse_codestream(fp: IO[bytes]) -> tuple[tuple[int, int], str]: """Parse the JPEG 2000 codestream to extract the size and component count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" @@ -137,7 +142,15 @@ def _res_to_dpi(num: int, denom: int, exp: int) -> float | None: return (254 * num * (10**exp)) / (10000 * denom) -def _parse_jp2_header(fp): +def _parse_jp2_header( + fp: IO[bytes], +) -> tuple[ + tuple[int, int], + str, + str | None, + tuple[float, float] | None, + ImagePalette.ImagePalette | None, +]: """Parse the JP2 header box to extract size, component count, color space information, and optionally DPI information, returning a (size, mode, mimetype, dpi) tuple.""" @@ -155,6 +168,7 @@ def _parse_jp2_header(fp): elif tbox == b"ftyp": if reader.read_fields(">4s")[0] == b"jpx ": mimetype = "image/jpx" + assert header is not None size = None mode = None @@ -162,12 +176,16 @@ def _parse_jp2_header(fp): nc = None dpi = None # 2-tuple of DPI info, or None palette = None + colr = None while header.has_next_box(): tbox = header.next_box_type() if tbox == b"ihdr": height, width, nc, bpc = header.read_fields(">IIHB") + assert isinstance(height, int) + assert isinstance(width, int) + assert isinstance(bpc, int) size = (width, height) if nc == 1 and (bpc & 0x7F) > 8: mode = "I;16" @@ -179,17 +197,38 @@ def _parse_jp2_header(fp): mode = "RGB" elif nc == 4: mode = "RGBA" - elif tbox == b"colr" and nc == 4: + elif tbox == b"colr": meth, _, _, enumcs = header.read_fields(">BBBI") - if meth == 1 and enumcs == 12: - mode = "CMYK" - elif tbox == b"pclr" and mode in ("L", "LA"): + if meth == 1: + if enumcs in (0, 15): + colr = "1" + elif enumcs == 12: + colr = "CMYK" + if nc == 4: + mode = "CMYK" + elif enumcs == 17: + colr = "L" + elif tbox == b"pclr" and mode in ("L", "LA") and colr not in ("1", "L"): ne, npc = header.read_fields(">HB") - bitdepths = header.read_fields(">" + ("B" * npc)) - if max(bitdepths) <= 8: - palette = ImagePalette.ImagePalette() + assert isinstance(ne, int) + assert isinstance(npc, int) + max_bitdepth = 0 + for bitdepth in header.read_fields(">" + ("B" * npc)): + assert isinstance(bitdepth, int) + if bitdepth > max_bitdepth: + max_bitdepth = bitdepth + if max_bitdepth <= 8: + if npc == 4: + palette_mode = "CMYK" if colr == "CMYK" else "RGBA" + else: + palette_mode = "RGB" + palette = ImagePalette.ImagePalette(palette_mode) for i in range(ne): - palette.getcolor(header.read_fields(">" + ("B" * npc))) + color: list[int] = [] + for value in header.read_fields(">" + ("B" * npc)): + assert isinstance(value, int) + color.append(value) + palette.getcolor(tuple(color)) mode = "P" if mode == "L" else "PA" elif tbox == b"res ": res = header.read_boxes() @@ -197,6 +236,12 @@ def _parse_jp2_header(fp): tres = res.next_box_type() if tres == b"resc": vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB") + assert isinstance(vrcn, int) + assert isinstance(vrcd, int) + assert isinstance(hrcn, int) + assert isinstance(hrcd, int) + assert isinstance(vrce, int) + assert isinstance(hrce, int) hres = _res_to_dpi(hrcn, hrcd, hrce) vres = _res_to_dpi(vrcn, vrcd, vrce) if hres is not None and vres is not None: @@ -219,10 +264,12 @@ class Jpeg2KImageFile(ImageFile.ImageFile): format_description = "JPEG 2000 (ISO 15444)" def _open(self) -> None: + assert self.fp is not None sig = self.fp.read(4) if sig == b"\xff\x4f\xff\x51": self.codec = "j2k" self._size, self._mode = _parse_codestream(self.fp) + self._parse_comment() else: sig = sig + self.fp.read(8) @@ -233,6 +280,9 @@ def _open(self) -> None: if dpi is not None: self.info["dpi"] = dpi if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"): + hdr = self.fp.read(2) + length = _binary.i16be(hdr) + self.fp.seek(length - 2, os.SEEK_CUR) self._parse_comment() else: msg = "not a JPEG 2000 file" @@ -258,7 +308,7 @@ def _open(self) -> None: length = -1 self.tile = [ - ( + ImageFile._Tile( "jpeg2k", (0, 0) + self.size, 0, @@ -267,10 +317,7 @@ def _open(self) -> None: ] def _parse_comment(self) -> None: - hdr = self.fp.read(2) - length = _binary.i16be(hdr) - self.fp.seek(length - 2, os.SEEK_CUR) - + assert self.fp is not None while True: marker = self.fp.read(2) if not marker: @@ -288,18 +335,23 @@ def _parse_comment(self) -> None: else: self.fp.seek(length - 2, os.SEEK_CUR) - @property - def reduce(self): + @property # type: ignore[override] + def reduce( + self, + ) -> ( + Callable[[int | tuple[int, int], tuple[int, int, int, int] | None], Image.Image] + | int + ): # 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): + def reduce(self, value: int) -> None: self._reduce = value - def load(self): + def load(self) -> Image.core.PixelAccess | None: if self.tile and self._reduce: power = 1 << self._reduce adjust = power >> 1 @@ -310,16 +362,16 @@ def load(self): # Update the reduce and layers settings t = self.tile[0] + assert isinstance(t[3], tuple) 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)] + self.tile = [ImageFile._Tile(t[0], (0, 0) + self.size, t[2], t3)] return ImageFile.ImageFile.load(self) def _accept(prefix: bytes) -> bool: - return ( - prefix[:4] == b"\xff\x4f\xff\x51" - or prefix[:12] == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" + return prefix.startswith( + (b"\xff\x4f\xff\x51", b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a") ) @@ -391,7 +443,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: plt, ) - ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) + ImageFile._save(im, fp, [ImageFile._Tile("jpeg2k", (0, 0) + im.size, 0, kind)]) # ------------------------------------------------------------ diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index b15bf06d2b1..46320eb3b5b 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -42,7 +42,6 @@ import sys import tempfile import warnings -from typing import IO, Any from . import Image, ImageFile from ._binary import i16be as i16 @@ -51,29 +50,37 @@ from ._binary import o16be as o16 from .JpegPresets import presets +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import IO, Any + + from .MpoImagePlugin import MpoImageFile + # # Parser def Skip(self: JpegImageFile, marker: int) -> None: + assert self.fp is not None n = i16(self.fp.read(2)) - 2 ImageFile._safe_read(self.fp, n) -def APP(self, marker): +def APP(self: JpegImageFile, marker: int) -> None: # # Application marker. Store these in the APP dictionary. # Also look for well-known application markers. + assert self.fp is not None n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) - app = "APP%d" % (marker & 15) + app = f"APP{marker & 15}" self.app[app] = s # compatibility self.applist.append((app, s)) - if marker == 0xFFE0 and s[:4] == b"JFIF": + if marker == 0xFFE0 and s.startswith(b"JFIF"): # extract JFIF information self.info["jfif"] = version = i16(s, 5) # version self.info["jfif_version"] = divmod(version, 256) @@ -86,21 +93,24 @@ def APP(self, marker): else: if jfif_unit == 1: self.info["dpi"] = jfif_density + elif jfif_unit == 2: # cm + # 1 dpcm = 2.54 dpi + self.info["dpi"] = tuple(d * 2.54 for d in jfif_density) self.info["jfif_unit"] = jfif_unit self.info["jfif_density"] = jfif_density - elif marker == 0xFFE1 and s[:6] == b"Exif\0\0": + elif marker == 0xFFE1 and s.startswith(b"Exif\0\0"): # extract EXIF information if "exif" in self.info: self.info["exif"] += s[6:] else: self.info["exif"] = s self._exif_offset = self.fp.tell() - n + 6 - elif marker == 0xFFE1 and s[:29] == b"http://ns.adobe.com/xap/1.0/\x00": + elif marker == 0xFFE1 and s.startswith(b"http://ns.adobe.com/xap/1.0/\x00"): self.info["xmp"] = s.split(b"\x00", 1)[1] - elif marker == 0xFFE2 and s[:5] == b"FPXR\0": + elif marker == 0xFFE2 and s.startswith(b"FPXR\0"): # extract FlashPix information (incomplete) self.info["flashpix"] = s # FIXME: value will change - elif marker == 0xFFE2 and s[:12] == b"ICC_PROFILE\0": + elif marker == 0xFFE2 and s.startswith(b"ICC_PROFILE\0"): # Since an ICC profile can be larger than the maximum size of # a JPEG marker (64K), we need provisions to split it into # multiple markers. The format defined by the ICC specifies @@ -113,12 +123,12 @@ 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": + elif marker == 0xFFED and s.startswith(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: + try: + while s[offset : offset + 4] == b"8BIM": offset += 4 # resource code code = i16(s, offset) @@ -133,19 +143,20 @@ def APP(self, marker): offset += 4 data = s[offset : offset + size] if code == 0x03ED: # ResolutionInfo - data = { + photoshop[code] = { "XResolution": i32(data, 0) / 65536, "DisplayedUnitsX": i16(data, 4), "YResolution": i32(data, 8) / 65536, "DisplayedUnitsY": i16(data, 12), } - photoshop[code] = data + else: + photoshop[code] = data offset += size offset += offset & 1 # align - except struct.error: - break # insufficient data + except struct.error: + pass # insufficient data - elif marker == 0xFFEE and s[:5] == b"Adobe": + elif marker == 0xFFEE and s.startswith(b"Adobe"): self.info["adobe"] = i16(s, 5) # extract Adobe custom properties try: @@ -154,7 +165,7 @@ def APP(self, marker): pass else: self.info["adobe_transform"] = adobe_transform - elif marker == 0xFFE2 and s[:4] == b"MPF\0": + elif marker == 0xFFE2 and s.startswith(b"MPF\0"): # extract MPO information self.info["mp"] = s[4:] # offset is current location minus buffer size @@ -165,6 +176,7 @@ def APP(self, marker): def COM(self: JpegImageFile, marker: int) -> None: # # Comment marker. Store these in the APP dictionary. + assert self.fp is not None n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) @@ -181,9 +193,12 @@ def SOF(self: JpegImageFile, marker: int) -> None: # mode. Note that this could be made a bit brighter, by # looking for JFIF and Adobe APP markers. + assert self.fp is not None n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) self._size = i16(s, 3), i16(s, 1) + if self._im is not None and self.size != self.im.size: + self._im = None self.bits = s[0] if self.bits != 8: @@ -229,6 +244,7 @@ def DQT(self: JpegImageFile, marker: int) -> None: # FIXME: The quantization tables can be used to estimate the # compression quality. + assert self.fp is not None n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) while len(s): @@ -317,7 +333,7 @@ def DQT(self: JpegImageFile, marker: int) -> None: def _accept(prefix: bytes) -> bool: # Magic number was taken from https://en.wikipedia.org/wiki/JPEG - return prefix[:3] == b"\xFF\xD8\xFF" + return prefix.startswith(b"\xff\xd8\xff") ## @@ -328,25 +344,27 @@ class JpegImageFile(ImageFile.ImageFile): format = "JPEG" format_description = "JPEG (ISO 10918)" - def _open(self): + def _open(self) -> None: + assert self.fp is not None s = self.fp.read(3) if not _accept(s): msg = "not a JPEG file" raise SyntaxError(msg) - s = b"\xFF" + s = b"\xff" # Create attributes self.bits = self.layers = 0 + self._exif_offset = 0 # JPEG specifics (internal) - self.layer = [] - self.huffman_dc = {} - self.huffman_ac = {} - self.quantization = {} - self.app = {} # compatibility - self.applist = [] - self.icclist = [] + self.layer: list[tuple[int, int, int, int]] = [] + self._huffman_dc: dict[Any, Any] = {} + self._huffman_ac: dict[Any, Any] = {} + self.quantization: dict[int, list[int]] = {} + self.app: dict[str, bytes] = {} # compatibility + self.applist: list[tuple[str, bytes]] = [] + self.icclist: list[bytes] = [] while True: i = s[0] @@ -366,7 +384,9 @@ def _open(self): rawmode = self.mode if self.mode == "CMYK": rawmode = "CMYK;I" # assume adobe conventions - self.tile = [("jpeg", (0, 0) + self.size, 0, (rawmode, ""))] + self.tile = [ + ImageFile._Tile("jpeg", (0, 0) + self.size, 0, (rawmode, "")) + ] # self.__offset = self.fp.tell() break s = self.fp.read(1) @@ -381,19 +401,27 @@ def _open(self): self._read_dpi_from_exif() + def __getstate__(self) -> list[Any]: + return super().__getstate__() + [self.layers, self.layer] + + def __setstate__(self, state: list[Any]) -> None: + self.layers, self.layer = state[6:] + super().__setstate__(state) + def load_read(self, read_bytes: int) -> bytes: """ internal: read more image data For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker so libjpeg can finish decoding """ + assert self.fp is not None 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 b"\xff\xd9" return s @@ -411,6 +439,7 @@ def draft( scale = 1 original_size = self.size + assert isinstance(a, tuple) if a[0] == "RGB" and mode in ["L", "YCbCr"]: self._mode = mode a = mode, "" @@ -420,6 +449,7 @@ def draft( for s in [8, 4, 2, 1]: if scale >= s: break + assert e is not None e = ( e[0], e[1], @@ -429,7 +459,7 @@ def draft( self._size = ((self.size[0] + s - 1) // s, (self.size[1] + s - 1) // s) scale = s - self.tile = [(d, e, o, a)] + self.tile = [ImageFile._Tile(d, e, o, a)] self.decoderconfig = (scale, 0) box = (0, 0, original_size[0] / scale, original_size[1] / scale) @@ -466,7 +496,7 @@ def load_djpeg(self) -> None: self.tile = [] - def _getexif(self) -> dict[str, Any] | None: + def _getexif(self) -> dict[int, Any] | None: return _getexif(self) def _read_dpi_from_exif(self) -> None: @@ -498,17 +528,17 @@ def _read_dpi_from_exif(self) -> None: ): self.info["dpi"] = 72, 72 - def _getmp(self): + def _getmp(self) -> dict[int, Any] | None: return _getmp(self) -def _getexif(self) -> dict[str, Any] | None: +def _getexif(self: JpegImageFile) -> dict[int, Any] | None: if "exif" not in self.info: return None return self.getexif()._get_merged_dict() -def _getmp(self): +def _getmp(self: JpegImageFile) -> dict[int, Any] | None: # 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 plugin. @@ -521,7 +551,7 @@ 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.startswith(b"\x4d\x4d\x00\x2a") else "<" # process dictionary from . import TiffImagePlugin @@ -543,7 +573,7 @@ def _getmp(self): mpentries = [] try: rawmpentries = mp[0xB002] - for entrynum in range(0, quant): + for entrynum in range(quant): unpackedentry = struct.unpack_from( f"{endianness}LLLHH", rawmpentries, entrynum * 16 ) @@ -616,7 +646,7 @@ def _getmp(self): # fmt: on -def get_sampling(im): +def get_sampling(im: Image.Image) -> int: # There's no subsampling when images have only 1 layer # (grayscale images) or when they are CMYK (4 layers), # so set subsampling to the default value. @@ -624,17 +654,13 @@ def get_sampling(im): # 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 isinstance(im, JpegImageFile) 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: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.width == 0 or im.height == 0: - msg = "cannot write empty image as JPEG" - raise ValueError(msg) - try: rawmode = RAWMODE[im.mode] except KeyError as e: @@ -683,7 +709,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: raise ValueError(msg) subsampling = get_sampling(im) - def validate_qtables(qtables): + def validate_qtables( + qtables: ( + str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None + ), + ) -> list[list[int]] | None: if qtables is None: return qtables if isinstance(qtables, str): @@ -708,17 +738,15 @@ def validate_qtables(qtables): if not (0 < len(qtables) < 5): msg = "None or too many quantization tables" raise ValueError(msg) - for idx, table in enumerate(qtables): - try: + try: + for idx, table in enumerate(qtables): if len(table) != 64: msg = "Invalid quantization table" raise TypeError(msg) - table = array.array("H", table) - except TypeError as e: - msg = "Invalid quantization table" - raise ValueError(msg) from e - else: - qtables[idx] = list(table) + qtables[idx] = list(array.array("H", table)) + except TypeError as e: + msg = "Invalid quantization table" + raise ValueError(msg) from e return qtables if qtables == "keep": @@ -731,19 +759,27 @@ def validate_qtables(qtables): extra = info.get("extra", b"") MAX_BYTES_IN_MARKER = 65533 - icc_profile = info.get("icc_profile") - if icc_profile: - ICC_OVERHEAD_LEN = 14 - MAX_DATA_BYTES_IN_MARKER = MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN + if xmp := info.get("xmp"): + overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" + max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len + if len(xmp) > max_data_bytes_in_marker: + msg = "XMP data is too long" + raise ValueError(msg) + size = o16(2 + overhead_len + len(xmp)) + extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp + + if icc_profile := info.get("icc_profile"): + overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers)) + max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len markers = [] while icc_profile: - markers.append(icc_profile[:MAX_DATA_BYTES_IN_MARKER]) - icc_profile = icc_profile[MAX_DATA_BYTES_IN_MARKER:] + markers.append(icc_profile[:max_data_bytes_in_marker]) + icc_profile = icc_profile[max_data_bytes_in_marker:] i = 1 for marker in markers: - size = o16(2 + ICC_OVERHEAD_LEN + len(marker)) + size = o16(2 + overhead_len + len(marker)) extra += ( - b"\xFF\xE2" + b"\xff\xe2" + size + b"ICC_PROFILE\0" + o8(i) @@ -776,8 +812,7 @@ def validate_qtables(qtables): optimize, info.get("keep_rgb", False), info.get("streamtype", 0), - dpi[0], - dpi[1], + dpi, subsampling, info.get("restart_marker_blocks", 0), info.get("restart_marker_rows", 0), @@ -791,7 +826,6 @@ def validate_qtables(qtables): # 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": @@ -808,28 +842,22 @@ def validate_qtables(qtables): else: # 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(bufsize, len(exif) + 5, len(extra) + 1) + bufsize = max(len(exif) + 5, len(extra) + 1) - ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize) - - -def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - # ALTERNATIVE: handle JPEGs via the IJG command line utilities. - tempfile = im._dump() - subprocess.check_call(["cjpeg", "-outfile", filename, tempfile]) - try: - os.unlink(tempfile) - except OSError: - pass + ImageFile._save( + im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize + ) ## # Factory for making JPEG and MPO instances -def jpeg_factory(fp=None, filename=None): +def jpeg_factory( + fp: IO[bytes], filename: str | bytes | None = None +) -> JpegImageFile | MpoImageFile: im = JpegImageFile(fp, filename) try: mpheader = im._getmp() - if mpheader[45057] > 1: + if mpheader is not None and mpheader[45057] > 1: for segment, content in im.applist: if segment == "APP1" and b' hdrgm:Version="' in content: # Ultra HDR images are not yet supported diff --git a/src/PIL/JpegPresets.py b/src/PIL/JpegPresets.py index 3aefa073ce2..d0e64a35ee1 100644 --- a/src/PIL/JpegPresets.py +++ b/src/PIL/JpegPresets.py @@ -37,7 +37,7 @@ :func:`.JpegImagePlugin.get_sampling` function. In JPEG compressed data a JPEG marker is used instead of an EXIF tag. -(ref.: https://web.archive.org/web/20240227115053/https://exiv2.org/tags.html) +(ref.: https://exiv2.org/tags.html) Quantization tables diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 27972236c0a..9a47933b69c 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -23,7 +23,7 @@ def _accept(prefix: bytes) -> bool: - return prefix[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04" + return prefix.startswith(b"\x00\x00\x00\x00\x00\x00\x00\x04") ## @@ -44,15 +44,13 @@ def _open(self) -> None: raise SyntaxError(msg) self.area_descriptor_raw = s - self.area_descriptor = w = [0] + list(struct.unpack("!64i", s)) + self.area_descriptor = w = [0, *struct.unpack("!64i", s)] # get mode if w[11] == 1: mode = rawmode = "L" elif w[11] == 2: - # FIXME: add memory map support - mode = "I" - rawmode = "I;16B" + mode = rawmode = "I;16B" elif w[11] == 4: # FIXME: add memory map support mode = "I" @@ -67,7 +65,9 @@ def _open(self) -> None: offset = w[34] + w[15] stride = w[15] + w[10] * w[11] * w[14] - self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))] + self.tile = [ + ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride, 1)) + ] # -------------------------------------------------------------------- diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 07239887f9f..99a07bae02c 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -26,7 +26,7 @@ def _accept(prefix: bytes) -> bool: - return prefix[:8] == olefile.MAGIC + return prefix.startswith(olefile.MAGIC) ## @@ -54,7 +54,7 @@ def _open(self) -> None: self.images = [ path for path in self.ole.listdir() - if path[1:] and path[0][-4:] == ".ACI" and path[1] == "Image" + if path[1:] and path[0].endswith(".ACI") and path[1] == "Image" ] # if we didn't find any images, this is probably not @@ -67,18 +67,14 @@ def _open(self) -> None: self._n_frames = len(self.images) self.is_animated = self._n_frames > 1 + assert self.fp is not None self.__fp = self.fp self.seek(0) - def seek(self, frame): + def seek(self, frame: int) -> None: if not self._seek_check(frame): return - try: - filename = self.images[frame] - except IndexError as e: - msg = "no such frame" - raise EOFError(msg) from e - + filename = self.images[frame] self.fp = self.ole.openstream(filename) TiffImagePlugin.TiffImageFile._open(self) diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index ad4d3e93768..47ebe9d62c4 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -33,11 +33,7 @@ def next(self) -> int: def peek(self, bits: int) -> int: while self.bits < bits: - c = self.next() - if c < 0: - self.bits = 0 - continue - self.bitbuffer = (self.bitbuffer << 8) + c + self.bitbuffer = (self.bitbuffer << 8) + self.next() self.bits += 8 return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1 @@ -54,7 +50,7 @@ def read(self, bits: int) -> int: def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"\x00\x00\x01\xb3" + return prefix.startswith(b"\x00\x00\x01\xb3") ## diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index f21570661f8..bee0a56f9ac 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -19,18 +19,19 @@ # from __future__ import annotations -import itertools import os import struct -from typing import IO +from typing import IO, Any, cast from . import ( Image, + ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin, ) from ._binary import o32le +from ._util import DeferredError def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: @@ -45,24 +46,31 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: mpf_offset = 28 offsets: list[int] = [] - for imSequence in itertools.chain([im], append_images): - for im_frame in ImageSequence.Iterator(imSequence): + im_sequences = [im, *append_images] + total = sum(getattr(seq, "n_frames", 1) for seq in im_sequences) + for im_sequence in im_sequences: + for im_frame in ImageSequence.Iterator(im_sequence): if not offsets: # APP2 marker + ifd_length = 66 + 16 * total im_frame.encoderinfo["extra"] = ( - b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 + b"\xff\xe2" + + struct.pack(">H", 6 + ifd_length) + + b"MPF\0" + + b" " * ifd_length ) - exif = im_frame.encoderinfo.get("exif") - if isinstance(exif, Image.Exif): - exif = exif.tobytes() - im_frame.encoderinfo["exif"] = exif - if exif: + if exif := im_frame.encoderinfo.get("exif"): + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + im_frame.encoderinfo["exif"] = exif mpf_offset += 4 + len(exif) JpegImagePlugin._save(im_frame, fp, filename) offsets.append(fp.tell()) else: + encoderinfo = im_frame._attach_default_encoderinfo(im) im_frame.save(fp, "JPEG") + im_frame.encoderinfo = encoderinfo offsets.append(fp.tell() - offsets[-1]) ifd = TiffImagePlugin.ImageFileDirectory_v2() @@ -83,7 +91,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: ifd[0xB002] = mpentries fp.seek(mpf_offset) - fp.write(b"II\x2A\x00" + o32le(8) + ifd.tobytes(8)) + fp.write(b"II\x2a\x00" + o32le(8) + ifd.tobytes(8)) fp.seek(0, os.SEEK_END) @@ -97,12 +105,16 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): _close_exclusive_fp_after_loading = False def _open(self) -> None: + assert self.fp is not None self.fp.seek(0) # prep the fp in order to pass the JPEG test JpegImagePlugin.JpegImageFile._open(self) self._after_jpeg_open() - def _after_jpeg_open(self, mpheader=None): + def _after_jpeg_open(self, mpheader: dict[int, Any] | None = None) -> None: self.mpinfo = mpheader if mpheader is not None else self._getmp() + if self.mpinfo is None: + msg = "Image appears to be a malformed MPO file" + raise ValueError(msg) self.n_frames = self.mpinfo[0xB001] self.__mpoffsets = [ mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002] @@ -113,6 +125,7 @@ def _after_jpeg_open(self, mpheader=None): assert self.n_frames == len(self.__mpoffsets) del self.info["mpoffset"] # no longer needed self.is_animated = self.n_frames > 1 + assert self.fp is not None self._fp = self.fp # FIXME: hack self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame self.__frame = 0 @@ -121,11 +134,15 @@ def _after_jpeg_open(self, mpheader=None): self.readonly = 1 def load_seek(self, pos: int) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex self._fp.seek(pos) def seek(self, frame: int) -> None: if not self._seek_check(frame): return + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.fp = self._fp self.offset = self.__mpoffsets[frame] @@ -142,14 +159,19 @@ def seek(self, frame: int) -> None: if self.info.get("exif") != original_exif: self._reload_exif() - self.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])] + self.tile = [ + ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1]) + ] self.__frame = frame def tell(self) -> int: return self.__frame @staticmethod - def adopt(jpeg_instance, mpheader=None): + def adopt( + jpeg_instance: JpegImagePlugin.JpegImageFile, + mpheader: dict[int, Any] | None = None, + ) -> MpoImageFile: """ Transform the instance of JpegImageFile into an instance of MpoImageFile. @@ -161,8 +183,9 @@ def adopt(jpeg_instance, mpheader=None): double call to _open. """ jpeg_instance.__class__ = MpoImageFile - jpeg_instance._after_jpeg_open(mpheader) - return jpeg_instance + mpo_instance = cast(MpoImageFile, jpeg_instance) + mpo_instance._after_jpeg_open(mpheader) + return mpo_instance # --------------------------------------------------------------------- diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 0a75c868b97..fa0f52fe8db 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -37,7 +37,7 @@ def _accept(prefix: bytes) -> bool: - return prefix[:4] in [b"DanM", b"LinS"] + return prefix.startswith((b"DanM", b"LinS")) ## @@ -69,10 +69,10 @@ def _open(self) -> None: self._mode = "1" self._size = i16(s, 4), i16(s, 6) - if s[:4] == b"DanM": - self.tile = [("raw", (0, 0) + self.size, 32, ("1", 0, 1))] + if s.startswith(b"DanM"): + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, "1")] else: - self.tile = [("MSP", (0, 0) + self.size, 32, None)] + self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)] class MspDecoder(ImageFile.PyDecoder): @@ -112,7 +112,7 @@ class MspDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None img = io.BytesIO() @@ -140,7 +140,7 @@ def decode(self, buffer: bytes) -> tuple[int, int]: runtype = row[idx] idx += 1 if runtype == 0: - (runcount, runval) = struct.unpack_from("Bc", row, idx) + runcount, runval = struct.unpack_from("Bc", row, idx) img.write(runval * runcount) idx += 2 else: @@ -152,7 +152,7 @@ def decode(self, buffer: bytes) -> tuple[int, int]: msg = f"Corrupted MSP file in row {x}" raise OSError(msg) from e - self.set_as_raw(img.getvalue(), ("1", 0, 1)) + self.set_as_raw(img.getvalue(), "1") return -1, 0 @@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(o16(h)) # image body - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, "1")]) # diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index 673eae1d12c..e6b74a91888 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -17,10 +17,13 @@ from __future__ import annotations import sys -from typing import TYPE_CHECKING +from typing import IO from . import EpsImagePlugin +TYPE_CHECKING = False + + ## # Simple PostScript graphics interface. @@ -28,15 +31,12 @@ class PSDraw: """ Sets up printing to the given file. If ``fp`` is omitted, - ``sys.stdout.buffer`` or ``sys.stdout`` is assumed. + ``sys.stdout.buffer`` is assumed. """ - def __init__(self, fp=None): + def __init__(self, fp: IO[bytes] | None = None) -> None: if not fp: - try: - fp = sys.stdout.buffer - except AttributeError: - fp = sys.stdout + fp = sys.stdout.buffer self.fp = fp def begin_document(self, id: str | None = None) -> None: @@ -100,7 +100,8 @@ def text(self, xy: tuple[int, int], text: str) -> None: Draws text at the given position. You must use :py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method. """ - text_bytes = bytes(text, "UTF-8") + # The font is loaded as ISOLatin1Encoding, so use latin-1 here. + text_bytes = bytes(text, "latin-1") text_bytes = b"\\(".join(text_bytes.split(b"(")) text_bytes = b"\\)".join(text_bytes.split(b")")) self.fp.write(b"%d %d M (%s) S\n" % (xy + (text_bytes,))) diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index 81652e5eec2..2a26e5d4e22 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -32,7 +32,7 @@ def __init__(self, fp: IO[bytes]) -> None: if not s: break - if s[:1] == b"#": + if s.startswith(b"#"): continue if len(s) > 100: msg = "bad palette file" diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index 1735070f81b..232adf3d3bb 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -116,9 +116,6 @@ def build_prototype_image() -> Image.Image: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if im.mode == "P": - # we assume this is a color Palm image with the standard colormap, - # unless the "info" dict has a "custom-colormap" field - rawmode = "P" bpp = 8 version = 1 @@ -172,11 +169,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: compression_type = _COMPRESSION_TYPES["none"] flags = 0 - if im.mode == "P" and "custom-colormap" in im.info: - flags = flags & _FLAGS["custom-colormap"] - colormapsize = 4 * 256 + 2 - colormapmode = im.palette.mode - colormap = im.getdata().getpalette() + if im.mode == "P": + flags |= _FLAGS["custom-colormap"] + colormap = im.im.getpalette() + colors = len(colormap) // 3 + colormapsize = 4 * colors + 2 else: colormapsize = 0 @@ -195,25 +192,16 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # now write colormap if necessary - if colormapsize > 0: - fp.write(o16b(256)) - for i in range(256): + if colormapsize: + fp.write(o16b(colors)) + for i in range(colors): fp.write(o8(i)) - if colormapmode == "RGB": - fp.write( - 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]) - ) + fp.write(colormap[3 * i : 3 * i + 3]) # now convert data to raw form - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))] + ) if hasattr(fp, "flush"): fp.flush() @@ -222,8 +210,8 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # # -------------------------------------------------------------------- -Image.register_save("Palm", _save) +Image.register_save("PALM", _save) -Image.register_extension("Palm", ".palm") +Image.register_extension("PALM", ".palm") -Image.register_mime("Palm", "image/palm") +Image.register_mime("PALM", "image/palm") diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 1cd5c4a9dbe..296f3775b0d 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -32,9 +32,9 @@ def _open(self) -> None: assert self.fp is not None self.fp.seek(2048) - s = self.fp.read(2048) + s = self.fp.read(1539) - if s[:4] != b"PCD_": + if not s.startswith(b"PCD_"): msg = "not a PCD file" raise SyntaxError(msg) @@ -43,19 +43,21 @@ def _open(self) -> None: if orientation == 1: self.tile_post_rotate = 90 elif orientation == 3: - self.tile_post_rotate = -90 + self.tile_post_rotate = 270 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 = (512, 768) if orientation in (1, 3) else (768, 512) + self.tile = [ImageFile._Tile("pcd", (0, 0, 768, 512), 96 * 2048)] + + def load_prepare(self) -> None: + if self._im is None and self.tile_post_rotate: + self.im = Image.core.new(self.mode, (768, 512)) + ImageFile.ImageFile.load_prepare(self) def load_end(self) -> None: if self.tile_post_rotate: # Handle rotated PCDs - assert self.im is not None - - self.im = self.im.rotate(self.tile_post_rotate) - self._size = self.im.size + self.im = self.rotate(self.tile_post_rotate, expand=True).im # diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 0d1968b140a..b923293b06a 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -18,7 +18,6 @@ from __future__ import annotations import io -from typing import BinaryIO, Callable from . import FontFile, Image from ._binary import i8 @@ -27,6 +26,11 @@ from ._binary import i32be as b32 from ._binary import i32le as l32 +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from typing import BinaryIO + # -------------------------------------------------------------------- # declarations @@ -247,7 +251,7 @@ def _load_encoding(self) -> list[int | None]: ] if encoding_offset != 0xFFFF: encoding[i] = encoding_offset - except UnicodeDecodeError: + except UnicodeDecodeError: # noqa: PERF203 # character is not supported in selected encoding pass diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index dd42003b5a3..3e34e3c63ba 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -39,7 +39,7 @@ def _accept(prefix: bytes) -> bool: - return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5] + return len(prefix) >= 2 and prefix[0] == 10 and prefix[1] in [0, 2, 3, 5] ## @@ -54,7 +54,7 @@ def _open(self) -> None: # header assert self.fp is not None - s = self.fp.read(128) + s = self.fp.read(68) if not _accept(s): msg = "not a PCX file" raise SyntaxError(msg) @@ -66,6 +66,8 @@ def _open(self) -> None: raise SyntaxError(msg) logger.debug("BBox: %s %s %s %s", *bbox) + offset = self.fp.tell() + 60 + # format version = s[1] bits = s[3] @@ -86,7 +88,7 @@ def _open(self) -> None: elif bits == 1 and planes in (2, 4): mode = "P" - rawmode = "P;%dL" % planes + rawmode = f"P;{planes}L" self.palette = ImagePalette.raw("RGB", s[16:64]) elif version == 5 and bits == 8 and planes == 1: @@ -102,7 +104,6 @@ def _open(self) -> None: break if mode == "P": self.palette = ImagePalette.raw("RGB", s[1:]) - self.fp.seek(128) elif version == 5 and bits == 8 and planes == 3: mode = "RGB" @@ -128,7 +129,7 @@ def _open(self) -> None: bbox = (0, 0) + self.size logger.debug("size: %sx%s", *self.size) - self.tile = [("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))] + self.tile = [ImageFile._Tile("pcx", bbox, offset, (rawmode, planes * stride))] # -------------------------------------------------------------------- @@ -145,6 +146,10 @@ def _open(self) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.width == 0 or im.height == 0: + msg = "Cannot write empty image as PCX" + raise ValueError(msg) + try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: @@ -186,7 +191,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + o16(dpi[0]) + o16(dpi[1]) + b"\0" * 24 - + b"\xFF" * 24 + + b"\xff" * 24 + b"\0" + o8(planes) + o16(stride) @@ -198,12 +203,12 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: assert fp.tell() == 128 - ImageFile._save(im, fp, [("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))]) + ImageFile._save( + im, fp, [ImageFile._Tile("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))] + ) if im.mode == "P": # colour palette - assert im.im is not None - fp.write(o8(12)) palette = im.im.getpalette("RGB", "RGB") palette += b"\x00" * (768 - len(palette)) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index f0da1e04797..5594c7e0f2b 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -25,9 +25,9 @@ import math import os import time -from typing import IO +from typing import IO, Any -from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features +from . import Image, ImageFile, ImageSequence, PdfParser, features # # -------------------------------------------------------------------- @@ -48,7 +48,12 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # (Internal) Image save plugin for the PDF format. -def _write_image(im, filename, existing_pdf, image_refs): +def _write_image( + im: Image.Image, + filename: str | bytes, + existing_pdf: PdfParser.PdfParser, + image_refs: list[PdfParser.IndirectReference], +) -> tuple[PdfParser.IndirectReference, str]: # FIXME: Should replace ASCIIHexDecode with RunLengthDecode # (packbits) or LZWDecode (tiff/lzw compression). Note that # PDF 1.2 also supports Flatedecode (zip compression). @@ -61,10 +66,10 @@ def _write_image(im, filename, existing_pdf, image_refs): width, height = im.size - dict_obj = {"BitsPerComponent": 8} + dict_obj: dict[str, Any] = {"BitsPerComponent": 8} if im.mode == "1": if features.check("libtiff"): - filter = "CCITTFaxDecode" + decode_filter = "CCITTFaxDecode" dict_obj["BitsPerComponent"] = 1 params = PdfParser.PdfArray( [ @@ -79,22 +84,23 @@ def _write_image(im, filename, existing_pdf, image_refs): ] ) else: - filter = "DCTDecode" + decode_filter = "DCTDecode" dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale elif im.mode == "L": - filter = "DCTDecode" + decode_filter = "DCTDecode" # params = f"<< /Predictor 15 /Columns {width-2} >>" dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale elif im.mode == "LA": - filter = "JPXDecode" + decode_filter = "JPXDecode" # params = f"<< /Predictor 15 /Columns {width-2} >>" procset = "ImageB" # grayscale dict_obj["SMaskInData"] = 1 elif im.mode == "P": - filter = "ASCIIHexDecode" + decode_filter = "ASCIIHexDecode" palette = im.getpalette() + assert palette is not None dict_obj["ColorSpace"] = [ PdfParser.PdfName("Indexed"), PdfParser.PdfName("DeviceRGB"), @@ -110,15 +116,15 @@ def _write_image(im, filename, existing_pdf, image_refs): image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0] dict_obj["SMask"] = image_ref elif im.mode == "RGB": - filter = "DCTDecode" + decode_filter = "DCTDecode" dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB") procset = "ImageC" # color images elif im.mode == "RGBA": - filter = "JPXDecode" + decode_filter = "JPXDecode" procset = "ImageC" # color images dict_obj["SMaskInData"] = 1 elif im.mode == "CMYK": - filter = "DCTDecode" + decode_filter = "DCTDecode" dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK") procset = "ImageC" # color images decode = [1, 0, 1, 0, 1, 0, 1, 0] @@ -131,9 +137,9 @@ def _write_image(im, filename, existing_pdf, image_refs): op = io.BytesIO() - if filter == "ASCIIHexDecode": - ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) - elif filter == "CCITTFaxDecode": + if decode_filter == "ASCIIHexDecode": + ImageFile._save(im, op, [ImageFile._Tile("hex", (0, 0) + im.size, 0, im.mode)]) + elif decode_filter == "CCITTFaxDecode": im.save( op, "TIFF", @@ -141,21 +147,22 @@ def _write_image(im, filename, existing_pdf, image_refs): # use a single strip strip_size=math.ceil(width / 8) * height, ) - elif filter == "DCTDecode": + elif decode_filter == "DCTDecode": Image.SAVE["JPEG"](im, op, filename) - elif filter == "JPXDecode": + elif decode_filter == "JPXDecode": del dict_obj["BitsPerComponent"] Image.SAVE["JPEG2000"](im, op, filename) else: - msg = f"unsupported PDF filter ({filter})" + msg = f"unsupported PDF filter ({decode_filter})" raise ValueError(msg) stream = op.getvalue() - if filter == "CCITTFaxDecode": + filter: PdfParser.PdfArray | PdfParser.PdfName + if decode_filter == "CCITTFaxDecode": stream = stream[8:] - filter = PdfParser.PdfArray([PdfParser.PdfName(filter)]) + filter = PdfParser.PdfArray([PdfParser.PdfName(decode_filter)]) else: - filter = PdfParser.PdfName(filter) + filter = PdfParser.PdfName(decode_filter) image_ref = image_refs.pop(0) existing_pdf.write_obj( @@ -174,12 +181,15 @@ def _write_image(im, filename, existing_pdf, image_refs): return image_ref, procset -def _save(im, fp, filename, save_all=False): +def _save( + im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False +) -> None: is_appending = im.encoderinfo.get("append", False) + filename_str = filename.decode() if isinstance(filename, bytes) else filename if is_appending: - existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="r+b") + existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="r+b") else: - existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") + existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="w+b") dpi = im.encoderinfo.get("dpi") if dpi: @@ -211,7 +221,7 @@ def _save(im, fp, filename, save_all=False): existing_pdf.start_writing() existing_pdf.write_header() - existing_pdf.write_comment(f"created by Pillow {__version__} PDF driver") + existing_pdf.write_comment("created by Pillow PDF driver") # # pages @@ -228,12 +238,7 @@ def _save(im, fp, filename, save_all=False): for im in ims: im_number_of_pages = 1 if save_all: - try: - im_number_of_pages = im.n_frames - except AttributeError: - # Image format does not have n_frames. - # It is a single frame image - pass + im_number_of_pages = getattr(im, "n_frames", 1) number_of_pages += im_number_of_pages for i in range(im_number_of_pages): image_refs.append(existing_pdf.next_object_id(0)) @@ -250,7 +255,9 @@ def _save(im, fp, filename, save_all=False): page_number = 0 for im_sequence in ims: - im_pages = ImageSequence.Iterator(im_sequence) if save_all else [im_sequence] + im_pages: ImageSequence.Iterator | list[Image.Image] = ( + ImageSequence.Iterator(im_sequence) if save_all else [im_sequence] + ) for im in im_pages: image_ref, procset = _write_image(im, filename, existing_pdf, image_refs) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 9e22313475e..f7f3a46431d 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -8,7 +8,15 @@ import re import time import zlib -from typing import TYPE_CHECKING, Any, List, NamedTuple, Union +from typing import Any, NamedTuple + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import IO + + _DictBase = collections.UserDict[str | bytes, Any] +else: + _DictBase = collections.UserDict # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set @@ -19,14 +27,14 @@ def encode_text(s: str) -> bytes: PDFDocEncoding = { 0x16: "\u0017", - 0x18: "\u02D8", - 0x19: "\u02C7", - 0x1A: "\u02C6", - 0x1B: "\u02D9", - 0x1C: "\u02DD", - 0x1D: "\u02DB", - 0x1E: "\u02DA", - 0x1F: "\u02DC", + 0x18: "\u02d8", + 0x19: "\u02c7", + 0x1A: "\u02c6", + 0x1B: "\u02d9", + 0x1C: "\u02dd", + 0x1D: "\u02db", + 0x1E: "\u02da", + 0x1F: "\u02dc", 0x80: "\u2022", 0x81: "\u2020", 0x82: "\u2021", @@ -36,33 +44,33 @@ def encode_text(s: str) -> bytes: 0x86: "\u0192", 0x87: "\u2044", 0x88: "\u2039", - 0x89: "\u203A", + 0x89: "\u203a", 0x8A: "\u2212", 0x8B: "\u2030", - 0x8C: "\u201E", - 0x8D: "\u201C", - 0x8E: "\u201D", + 0x8C: "\u201e", + 0x8D: "\u201c", + 0x8E: "\u201d", 0x8F: "\u2018", 0x90: "\u2019", - 0x91: "\u201A", + 0x91: "\u201a", 0x92: "\u2122", - 0x93: "\uFB01", - 0x94: "\uFB02", + 0x93: "\ufb01", + 0x94: "\ufb02", 0x95: "\u0141", 0x96: "\u0152", 0x97: "\u0160", 0x98: "\u0178", - 0x99: "\u017D", + 0x99: "\u017d", 0x9A: "\u0131", 0x9B: "\u0142", 0x9C: "\u0153", 0x9D: "\u0161", - 0x9E: "\u017E", - 0xA0: "\u20AC", + 0x9E: "\u017e", + 0xA0: "\u20ac", } -def decode_text(b): +def decode_text(b: bytes) -> str: if b[: len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE: return b[len(codecs.BOM_UTF16_BE) :].decode("utf_16_be") else: @@ -99,7 +107,7 @@ def __eq__(self, other: object) -> bool: assert isinstance(other, IndirectReference) return other.object_id == self.object_id and other.generation == self.generation - def __ne__(self, other): + def __ne__(self, other: object) -> bool: return not (self == other) def __hash__(self) -> int: @@ -112,13 +120,17 @@ def __str__(self) -> str: class XrefTable: - def __init__(self): - self.existing_entries = {} # object ID => (offset, generation) - self.new_entries = {} # object ID => (offset, generation) + def __init__(self) -> None: + self.existing_entries: dict[int, tuple[int, int]] = ( + {} + ) # object ID => (offset, generation) + self.new_entries: dict[int, tuple[int, int]] = ( + {} + ) # object ID => (offset, generation) self.deleted_entries = {0: 65536} # object ID => generation self.reading_finished = False - def __setitem__(self, key, value): + def __setitem__(self, key: int, value: tuple[int, int]) -> None: if self.reading_finished: self.new_entries[key] = value else: @@ -126,13 +138,13 @@ def __setitem__(self, key, value): if key in self.deleted_entries: del self.deleted_entries[key] - def __getitem__(self, key): + def __getitem__(self, key: int) -> tuple[int, int]: try: return self.new_entries[key] except KeyError: return self.existing_entries[key] - def __delitem__(self, key): + def __delitem__(self, key: int) -> None: if key in self.new_entries: generation = self.new_entries[key][1] + 1 del self.new_entries[key] @@ -146,7 +158,7 @@ def __delitem__(self, key): msg = f"object ID {key} cannot be deleted because it doesn't exist" raise IndexError(msg) - def __contains__(self, key): + def __contains__(self, key: int) -> bool: return key in self.existing_entries or key in self.new_entries def __len__(self) -> int: @@ -156,19 +168,19 @@ def __len__(self) -> int: | set(self.deleted_entries.keys()) ) - def keys(self): + def keys(self) -> set[int]: return ( set(self.existing_entries.keys()) - set(self.deleted_entries.keys()) ) | set(self.new_entries.keys()) - def write(self, f): + def write(self, f: IO[bytes]) -> int: 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 + prev: int | None = None for index, key in enumerate(keys): if prev is None or prev + 1 == key: prev = key @@ -178,7 +190,7 @@ def write(self, f): break else: contiguous_keys = keys - keys = None + keys = [] 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: @@ -202,7 +214,9 @@ def write(self, f): class PdfName: - def __init__(self, name): + name: bytes + + def __init__(self, name: PdfName | bytes | str) -> None: if isinstance(name, PdfName): self.name = name.name elif isinstance(name, bytes): @@ -213,7 +227,7 @@ def __init__(self, name): def name_as_str(self) -> str: return self.name.decode("us-ascii") - def __eq__(self, other): + def __eq__(self, other: object) -> bool: return ( isinstance(other, PdfName) and other.name == self.name ) or other == self.name @@ -225,7 +239,7 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}({repr(self.name)})" @classmethod - def from_pdf_stream(cls, data): + def from_pdf_stream(cls, data: bytes) -> PdfName: return cls(PdfParser.interpret_name(data)) allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"} @@ -240,25 +254,19 @@ def __bytes__(self) -> bytes: return bytes(result) -class PdfArray(List[Any]): +class PdfArray(list[Any]): def __bytes__(self) -> bytes: return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" -if TYPE_CHECKING: - _DictBase = collections.UserDict[Union[str, bytes], Any] -else: - _DictBase = collections.UserDict - - class PdfDict(_DictBase): - def __setattr__(self, key, value): + def __setattr__(self, key: str, value: Any) -> None: if key == "data": collections.UserDict.__setattr__(self, key, value) else: self[key.encode("us-ascii")] = value - def __getattr__(self, key): + def __getattr__(self, key: str) -> str | time.struct_time: try: value = self[key.encode("us-ascii")] except KeyError as e: @@ -300,7 +308,7 @@ def __bytes__(self) -> bytes: class PdfBinary: - def __init__(self, data): + def __init__(self, data: list[int] | bytes) -> None: self.data = data def __bytes__(self) -> bytes: @@ -308,27 +316,27 @@ def __bytes__(self) -> bytes: class PdfStream: - def __init__(self, dictionary, buf): + def __init__(self, dictionary: PdfDict, buf: bytes) -> None: self.dictionary = dictionary self.buf = buf - def decode(self): + def decode(self) -> bytes: try: - filter = self.dictionary.Filter - except AttributeError: + filter = self.dictionary[b"Filter"] + except KeyError: return self.buf if filter == b"FlateDecode": try: - expected_length = self.dictionary.DL - except AttributeError: - expected_length = self.dictionary.Length + expected_length = self.dictionary[b"DL"] + except KeyError: + expected_length = self.dictionary[b"Length"] return zlib.decompress(self.buf, bufsize=int(expected_length)) else: - msg = f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported" + msg = f"stream filter {repr(filter)} unknown/unsupported" raise NotImplementedError(msg) -def pdf_repr(x): +def pdf_repr(x: Any) -> bytes: if x is True: return b"true" elif x is False: @@ -363,12 +371,19 @@ class PdfParser: Supports PDF up to 1.4 """ - def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"): + def __init__( + self, + filename: str | None = None, + f: IO[bytes] | None = None, + buf: bytes | bytearray | None = None, + start_offset: int = 0, + mode: str = "rb", + ) -> None: if buf and f: msg = "specify buf or f or filename, but not both buf and f" raise RuntimeError(msg) self.filename = filename - self.buf = buf + self.buf: bytes | bytearray | mmap.mmap | None = buf self.f = f self.start_offset = start_offset self.should_close_buf = False @@ -377,12 +392,16 @@ def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"): 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.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.cached_objects: dict[IndirectReference, Any] = {} + self.root_ref: IndirectReference | None + self.info_ref: IndirectReference | None + self.pages_ref: IndirectReference | None + self.last_xref_section_offset: int | None + if self.buf: self.read_pdf_info() else: self.file_size_total = self.file_size_this = 0 @@ -390,12 +409,12 @@ def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"): self.root_ref = None self.info = PdfDict() self.info_ref = None - self.page_tree_root = {} - self.pages = [] - self.orig_pages = [] + self.page_tree_root = PdfDict() + self.pages: list[IndirectReference] = [] + self.orig_pages: list[IndirectReference] = [] self.pages_ref = None self.last_xref_section_offset = None - self.trailer_dict = {} + self.trailer_dict: dict[bytes, Any] = {} self.xref_table = XrefTable() self.xref_table.reading_finished = True if f: @@ -412,10 +431,8 @@ def start_writing(self) -> None: self.seek_end() def close_buf(self) -> None: - try: + if isinstance(self.buf, mmap.mmap): self.buf.close() - except AttributeError: - pass self.buf = None def close(self) -> None: @@ -426,15 +443,19 @@ def close(self) -> None: self.f = None def seek_end(self) -> None: + assert self.f is not None self.f.seek(0, os.SEEK_END) def write_header(self) -> None: + assert self.f is not None self.f.write(b"%PDF-1.4\n") - def write_comment(self, s): + def write_comment(self, s: str) -> None: + assert self.f is not None self.f.write(f"% {s}\n".encode()) def write_catalog(self) -> IndirectReference: + assert self.f is not None self.del_root() self.root_ref = self.next_object_id(self.f.tell()) self.pages_ref = self.next_object_id(0) @@ -477,7 +498,10 @@ def rewrite_pages(self) -> None: pages_tree_node_ref = pages_tree_node.get(b"Parent", None) self.orig_pages = [] - def write_xref_and_trailer(self, new_root_ref=None): + def write_xref_and_trailer( + self, new_root_ref: IndirectReference | None = None + ) -> None: + assert self.f is not None if new_root_ref: self.del_root() self.root_ref = new_root_ref @@ -485,7 +509,10 @@ def write_xref_and_trailer(self, new_root_ref=None): 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} + trailer_dict: dict[str | bytes, Any] = { + 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: @@ -497,16 +524,20 @@ def write_xref_and_trailer(self, new_root_ref=None): + b"\nstartxref\n%d\n%%%%EOF" % start_xref ) - def write_page(self, ref, *objs, **dict_obj): - if isinstance(ref, int): - ref = self.pages[ref] + def write_page( + self, ref: int | IndirectReference | None, *objs: Any, **dict_obj: Any + ) -> IndirectReference: + obj_ref = self.pages[ref] if isinstance(ref, int) else 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) + return self.write_obj(obj_ref, *objs, **dict_obj) - def write_obj(self, ref, *objs, **dict_obj): + def write_obj( + self, ref: IndirectReference | None, *objs: Any, **dict_obj: Any + ) -> IndirectReference: + assert self.f is not None f = self.f if ref is None: ref = self.next_object_id(f.tell()) @@ -534,7 +565,7 @@ def del_root(self) -> None: del self.xref_table[self.root[b"Pages"].object_id] @staticmethod - def get_buf_from_file(f): + def get_buf_from_file(f: IO[bytes]) -> bytes | mmap.mmap: if hasattr(f, "getbuffer"): return f.getbuffer() elif hasattr(f, "getvalue"): @@ -546,10 +577,15 @@ def get_buf_from_file(f): return b"" def read_pdf_info(self) -> None: + assert self.buf is not None self.file_size_total = len(self.buf) self.file_size_this = self.file_size_total - self.start_offset self.read_trailer() + check_format_condition( + self.trailer_dict.get(b"Root") is not None, "Root is missing" + ) self.root_ref = self.trailer_dict[b"Root"] + assert self.root_ref is not None 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: @@ -560,12 +596,15 @@ def read_pdf_info(self) -> None: 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( + self.root.get(b"Pages") is not None, "/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"] + assert self.pages_ref is not None 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 @@ -573,7 +612,7 @@ def read_pdf_info(self) -> None: # and we need to rewrite the pages and their list self.orig_pages = self.pages[:] - def next_object_id(self, offset=None): + def next_object_id(self, offset: int | None = None) -> IndirectReference: try: # TODO: support reuse of deleted objects reference = IndirectReference(max(self.xref_table.keys()) + 1, 0) @@ -623,12 +662,13 @@ def next_object_id(self, offset=None): re.DOTALL, ) - def read_trailer(self): + def read_trailer(self) -> None: + assert self.buf is not None 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") + check_format_condition(m is not None, "trailer end not found") # make sure we found the LAST trailer last_match = m while m: @@ -636,6 +676,7 @@ def read_trailer(self): m = self.re_trailer_end.search(self.buf, m.start() + 16) if not m: m = last_match + assert m is not None trailer_data = m.group(1) self.last_xref_section_offset = int(m.group(2)) self.trailer_dict = self.interpret_trailer(trailer_data) @@ -644,12 +685,16 @@ def read_trailer(self): if b"Prev" in self.trailer_dict: self.read_prev_trailer(self.trailer_dict[b"Prev"]) - def read_prev_trailer(self, xref_section_offset): + def read_prev_trailer( + self, xref_section_offset: int, processed_offsets: list[int] = [] + ) -> None: + assert self.buf is not None 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") + check_format_condition(m is not None, "previous trailer not found") + assert m is not None trailer_data = m.group(1) check_format_condition( int(m.group(2)) == xref_section_offset, @@ -657,7 +702,11 @@ def read_prev_trailer(self, xref_section_offset): ) trailer_dict = self.interpret_trailer(trailer_data) if b"Prev" in trailer_dict: - self.read_prev_trailer(trailer_dict[b"Prev"]) + processed_offsets.append(xref_section_offset) + check_format_condition( + trailer_dict[b"Prev"] not in processed_offsets, "trailer loop found" + ) + self.read_prev_trailer(trailer_dict[b"Prev"], processed_offsets) re_whitespace_optional = re.compile(whitespace_optional) re_name = re.compile( @@ -670,7 +719,7 @@ def read_prev_trailer(self, xref_section_offset): re_dict_end = re.compile(whitespace_optional + rb">>" + whitespace_optional) @classmethod - def interpret_trailer(cls, trailer_data): + def interpret_trailer(cls, trailer_data: bytes) -> dict[bytes, Any]: trailer = {} offset = 0 while True: @@ -678,14 +727,18 @@ def interpret_trailer(cls, trailer_data): if not m: m = cls.re_dict_end.match(trailer_data, offset) check_format_condition( - m and m.end() == len(trailer_data), + m is not None 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()) + assert isinstance(key, bytes) + value, value_offset = cls.get_value(trailer_data, m.end()) trailer[key] = value + if value_offset is None: + break + offset = value_offset check_format_condition( b"Size" in trailer and isinstance(trailer[b"Size"], int), "/Size not in trailer or not an integer", @@ -699,7 +752,7 @@ def interpret_trailer(cls, trailer_data): re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?") @classmethod - def interpret_name(cls, raw, as_text=False): + def interpret_name(cls, raw: bytes, as_text: bool = False) -> str | bytes: name = b"" for m in cls.re_hashes_in_name.finditer(raw): if m.group(3): @@ -761,7 +814,13 @@ def interpret_name(cls, raw, as_text=False): ) @classmethod - def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1): + def get_value( + cls, + data: bytes | bytearray | mmap.mmap, + offset: int, + expect_indirect: IndirectReference | None = None, + max_nesting: int = -1, + ) -> tuple[Any, int | None]: if max_nesting == 0: return None, None m = cls.re_comment.match(data, offset) @@ -783,11 +842,16 @@ def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1): == 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: + object, object_offset = cls.get_value( + data, m.end(), max_nesting=max_nesting - 1 + ) + if object_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") + m = cls.re_indirect_def_end.match(data, object_offset) + check_format_condition( + m is not None, "indirect object definition end not found" + ) + assert m is not None return object, m.end() check_format_condition( not expect_indirect, "indirect object definition not found" @@ -806,46 +870,53 @@ def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1): m = cls.re_dict_start.match(data, offset) if m: offset = m.end() - result = {} + result: dict[Any, Any] = {} m = cls.re_dict_end.match(data, offset) + current_offset: int | None = offset while not m: - key, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1) - if offset is None: + assert current_offset is not None + key, current_offset = cls.get_value( + data, current_offset, max_nesting=max_nesting - 1 + ) + if current_offset is None: return result, None - value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1) + value, current_offset = cls.get_value( + data, current_offset, max_nesting=max_nesting - 1 + ) result[key] = value - if offset is None: + if current_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) + m = cls.re_dict_end.match(data, current_offset) + current_offset = m.end() + m = cls.re_stream_start.match(data, current_offset) if m: - try: - stream_len_str = result.get(b"Length") - stream_len = int(stream_len_str) - except (TypeError, ValueError) as e: - msg = f"bad or missing Length in stream dict ({stream_len_str})" - raise PdfFormatError(msg) from e + stream_len = result.get(b"Length") + if stream_len is None or not isinstance(stream_len, int): + msg = f"bad or missing Length in stream dict ({stream_len})" + raise PdfFormatError(msg) 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 + check_format_condition(m is not None, "stream end not found") + assert m is not None + current_offset = m.end() + return PdfStream(PdfDict(result), stream_data), current_offset + return PdfDict(result), current_offset m = cls.re_array_start.match(data, offset) if m: offset = m.end() - result = [] + results = [] m = cls.re_array_end.match(data, offset) + current_offset = 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() + assert current_offset is not None + value, current_offset = cls.get_value( + data, current_offset, max_nesting=max_nesting - 1 + ) + results.append(value) + if current_offset is None: + return results, None + m = cls.re_array_end.match(data, current_offset) + return results, m.end() m = cls.re_null.match(data, offset) if m: return None, m.end() @@ -905,7 +976,9 @@ def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1): } @classmethod - def get_literal_string(cls, data, offset): + def get_literal_string( + cls, data: bytes | bytearray | mmap.mmap, offset: int + ) -> tuple[bytes, int]: nesting_depth = 0 result = bytearray() for m in cls.re_lit_str_token.finditer(data, offset): @@ -941,12 +1014,14 @@ def get_literal_string(cls, data, offset): ) 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): + def read_xref_table(self, xref_section_offset: int) -> int: + assert self.buf is not None 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") + check_format_condition(m is not None, "xref section start not found") + assert m is not None offset = m.end() while True: m = self.re_xref_subsection_start.match(self.buf, offset) @@ -961,7 +1036,8 @@ def read_xref_table(self, xref_section_offset): 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") + check_format_condition(m is not None, "xref entry not found") + assert m is not None offset = m.end() is_free = m.group(3) == b"f" if not is_free: @@ -971,13 +1047,14 @@ def read_xref_table(self, xref_section_offset): self.xref_table[i] = new_entry return offset - def read_indirect(self, ref, max_nesting=-1): + def read_indirect(self, ref: IndirectReference, max_nesting: int = -1) -> Any: 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}", ) + assert self.buf is not None value = self.get_value( self.buf, offset + self.start_offset, @@ -987,14 +1064,15 @@ def read_indirect(self, ref, max_nesting=-1): self.cached_objects[ref] = value return value - def linearize_page_tree(self, node=None): - if node is None: - node = self.page_tree_root + def linearize_page_tree( + self, node: PdfDict | None = None + ) -> list[IndirectReference]: + page_node = node if node is not None else self.page_tree_root check_format_condition( - node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages" + page_node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages" ) pages = [] - for kid in node[b"Kids"]: + for kid in page_node[b"Kids"]: kid_object = self.read_indirect(kid) if kid_object[b"Type"] == b"Page": pages.append(kid) diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 887b6568bf7..d2b6d0a97e4 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -28,7 +28,7 @@ def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"\200\350\000\000" + return prefix.startswith(b"\200\350\000\000") ## @@ -61,7 +61,7 @@ def _open(self) -> None: # FIXME: to be continued... # create tile descriptor (assuming "dumped") - self.tile = [("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1))] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 1024, self.mode)] # diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index d2834929b8f..76a15bd0dc6 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -39,7 +39,8 @@ import warnings import zlib from enum import IntEnum -from typing import IO, TYPE_CHECKING, Any, NoReturn +from fractions import Fraction +from typing import IO, NamedTuple, cast from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from ._binary import i16be as i16 @@ -47,8 +48,14 @@ from ._binary import o8 from ._binary import o16be as o16 from ._binary import o32be as o32 +from ._deprecate import deprecate +from ._util import DeferredError +TYPE_CHECKING = False if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, NoReturn + from . import _imaging logger = logging.getLogger(__name__) @@ -135,16 +142,16 @@ class Blend(IntEnum): """ -def _safe_zlib_decompress(s): +def _safe_zlib_decompress(s: bytes) -> bytes: dobj = zlib.decompressobj() plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) if dobj.unconsumed_tail: - msg = "Decompressed Data Too Large" + msg = "Decompressed data too large for PngImagePlugin.MAX_TEXT_CHUNK" raise ValueError(msg) return plaintext -def _crc32(data, seed=0): +def _crc32(data: bytes, seed: int = 0) -> int: return zlib.crc32(data, seed) & 0xFFFFFFFF @@ -191,7 +198,7 @@ def push(self, cid: bytes, pos: int, length: int) -> None: assert self.queue is not None self.queue.append((cid, pos, length)) - def call(self, cid, pos, length): + def call(self, cid: bytes, pos: int, length: int) -> bytes: """Call the appropriate chunk handler""" logger.debug("STREAM %r %s %s", cid, pos, length) @@ -230,6 +237,7 @@ def verify(self, endchunk: bytes = b"IEND") -> list[bytes]: cids = [] + assert self.fp is not None while True: try: cid, pos, length = self.read() @@ -256,7 +264,9 @@ class iTXt(str): tkey: str | bytes | None @staticmethod - def __new__(cls, text, lang=None, tkey=None): + def __new__( + cls, text: str, lang: str | None = None, tkey: str | None = None + ) -> iTXt: """ :param cls: the class to use when creating the instance :param text: value for this key @@ -366,21 +376,27 @@ def add_text( # PNG image stream (IHDR/IEND) +class _RewindState(NamedTuple): + info: dict[str | tuple[int, int], Any] + tile: list[ImageFile._Tile] + seq_num: int | None + + class PngStream(ChunkStream): - def __init__(self, fp): + def __init__(self, fp: IO[bytes]) -> None: super().__init__(fp) # local copies of Image attributes - self.im_info = {} - self.im_text = {} + self.im_info: dict[str | tuple[int, int], Any] = {} + self.im_text: dict[str, str | iTXt] = {} self.im_size = (0, 0) - 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.im_mode = "" + self.im_tile: list[ImageFile._Tile] = [] + self.im_palette: tuple[str, bytes] | None = None + self.im_custom_mimetype: str | None = None + self.im_n_frames: int | None = None + self._seq_num: int | None = None + self.rewind_state = _RewindState({}, [], None) self.text_memory = 0 @@ -394,19 +410,20 @@ def check_text_memory(self, chunklen: int) -> None: raise ValueError(msg) def save_rewind(self) -> None: - self.rewind_state = { - "info": self.im_info.copy(), - "tile": self.im_tile, - "seq_num": self._seq_num, - } + self.rewind_state = _RewindState( + self.im_info.copy(), + self.im_tile, + self._seq_num, + ) def rewind(self) -> None: - self.im_info = self.rewind_state["info"].copy() - self.im_tile = self.rewind_state["tile"] - self._seq_num = self.rewind_state["seq_num"] + self.im_info = self.rewind_state.info.copy() + self.im_tile = self.rewind_state.tile + self._seq_num = self.rewind_state.seq_num def chunk_iCCP(self, pos: int, length: int) -> bytes: # ICC profile + assert self.fp is not None s = ImageFile._safe_read(self.fp, length) # according to PNG spec, the iCCP chunk contains: # Profile name 1-79 bytes (character string) @@ -434,6 +451,7 @@ def chunk_iCCP(self, pos: int, length: int) -> bytes: def chunk_IHDR(self, pos: int, length: int) -> bytes: # image header + assert self.fp is not None s = ImageFile._safe_read(self.fp, length) if length < 13: if ImageFile.LOAD_TRUNCATED_IMAGES: @@ -455,11 +473,11 @@ def chunk_IHDR(self, pos: int, length: int) -> bytes: def chunk_IDAT(self, pos: int, length: int) -> NoReturn: # image data if "bbox" in self.im_info: - tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)] + tile = [ImageFile._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)] + tile = [ImageFile._Tile("zip", (0, 0) + self.im_size, pos, self.im_rawmode)] self.im_tile = tile self.im_idat = length msg = "image data found" @@ -471,6 +489,7 @@ def chunk_IEND(self, pos: int, length: int) -> NoReturn: def chunk_PLTE(self, pos: int, length: int) -> bytes: # palette + assert self.fp is not None s = ImageFile._safe_read(self.fp, length) if self.im_mode == "P": self.im_palette = "RGB", s @@ -478,6 +497,7 @@ def chunk_PLTE(self, pos: int, length: int) -> bytes: def chunk_tRNS(self, pos: int, length: int) -> bytes: # transparency + assert self.fp is not None s = ImageFile._safe_read(self.fp, length) if self.im_mode == "P": if _simple_palette.match(s): @@ -490,7 +510,9 @@ def chunk_tRNS(self, pos: int, length: int) -> bytes: # otherwise, we have a byte string with one alpha value # for each palette entry self.im_info["transparency"] = s - elif self.im_mode in ("1", "L", "I;16"): + elif self.im_mode == "1": + self.im_info["transparency"] = 255 if i16(s) else 0 + elif self.im_mode in ("L", "I;16"): self.im_info["transparency"] = i16(s) elif self.im_mode == "RGB": self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4) @@ -498,6 +520,7 @@ def chunk_tRNS(self, pos: int, length: int) -> bytes: def chunk_gAMA(self, pos: int, length: int) -> bytes: # gamma setting + assert self.fp is not None s = ImageFile._safe_read(self.fp, length) self.im_info["gamma"] = i32(s) / 100000.0 return s @@ -506,8 +529,9 @@ def chunk_cHRM(self, pos: int, length: int) -> bytes: # chromaticity, 8 unsigned ints, actual value is scaled by 100,000 # WP x,y, Red x,y, Green x,y Blue x,y + assert self.fp is not None s = ImageFile._safe_read(self.fp, length) - raw_vals = struct.unpack(">%dI" % (len(s) // 4), s) + raw_vals = struct.unpack(f">{len(s) // 4}I", s) self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals) return s @@ -518,6 +542,7 @@ def chunk_sRGB(self, pos: int, length: int) -> bytes: # 2 saturation # 3 absolute colorimetric + assert self.fp is not None s = ImageFile._safe_read(self.fp, length) if length < 1: if ImageFile.LOAD_TRUNCATED_IMAGES: @@ -529,6 +554,7 @@ def chunk_sRGB(self, pos: int, length: int) -> bytes: def chunk_pHYs(self, pos: int, length: int) -> bytes: # pixels per unit + assert self.fp is not None s = ImageFile._safe_read(self.fp, length) if length < 9: if ImageFile.LOAD_TRUNCATED_IMAGES: @@ -546,6 +572,7 @@ def chunk_pHYs(self, pos: int, length: int) -> bytes: def chunk_tEXt(self, pos: int, length: int) -> bytes: # text + assert self.fp is not None s = ImageFile._safe_read(self.fp, length) try: k, v = s.split(b"\0", 1) @@ -554,17 +581,18 @@ def chunk_tEXt(self, pos: int, length: int) -> bytes: k = s v = b"" if k: - k = k.decode("latin-1", "strict") + k_str = k.decode("latin-1", "strict") v_str = v.decode("latin-1", "replace") - self.im_info[k] = v if k == "exif" else v_str - self.im_text[k] = v_str + self.im_info[k_str] = v if k == b"exif" else v_str + self.im_text[k_str] = v_str self.check_text_memory(len(v_str)) return s def chunk_zTXt(self, pos: int, length: int) -> bytes: # compressed text + assert self.fp is not None s = ImageFile._safe_read(self.fp, length) try: k, v = s.split(b"\0", 1) @@ -589,16 +617,17 @@ def chunk_zTXt(self, pos: int, length: int) -> bytes: v = b"" if k: - k = k.decode("latin-1", "strict") - v = v.decode("latin-1", "replace") + k_str = 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_str] = self.im_text[k_str] = v_str + self.check_text_memory(len(v_str)) return s def chunk_iTXt(self, pos: int, length: int) -> bytes: # international text + assert self.fp is not None r = s = ImageFile._safe_read(self.fp, length) try: k, r = r.split(b"\0", 1) @@ -627,25 +656,27 @@ def chunk_iTXt(self, pos: int, length: int) -> bytes: if k == b"XML:com.adobe.xmp": self.im_info["xmp"] = v 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") + k_str = k.decode("latin-1", "strict") + lang_str = lang.decode("utf-8", "strict") + tk_str = tk.decode("utf-8", "strict") + v_str = 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)) + self.im_info[k_str] = self.im_text[k_str] = iTXt(v_str, lang_str, tk_str) + self.check_text_memory(len(v_str)) return s def chunk_eXIf(self, pos: int, length: int) -> bytes: + assert self.fp is not None 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: int, length: int) -> bytes: + assert self.fp is not None s = ImageFile._safe_read(self.fp, length) if length < 8: if ImageFile.LOAD_TRUNCATED_IMAGES: @@ -666,6 +697,7 @@ def chunk_acTL(self, pos: int, length: int) -> bytes: return s def chunk_fcTL(self, pos: int, length: int) -> bytes: + assert self.fp is not None s = ImageFile._safe_read(self.fp, length) if length < 26: if ImageFile.LOAD_TRUNCATED_IMAGES: @@ -695,6 +727,7 @@ def chunk_fcTL(self, pos: int, length: int) -> bytes: return s def chunk_fdAT(self, pos: int, length: int) -> bytes: + assert self.fp is not None if length < 4: if ImageFile.LOAD_TRUNCATED_IMAGES: s = ImageFile._safe_read(self.fp, length) @@ -715,7 +748,7 @@ def chunk_fdAT(self, pos: int, length: int) -> bytes: def _accept(prefix: bytes) -> bool: - return prefix[:8] == _MAGIC + return prefix.startswith(_MAGIC) ## @@ -727,6 +760,7 @@ class PngImageFile(ImageFile.ImageFile): format_description = "Portable network graphics" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(8)): msg = "not a PNG file" raise SyntaxError(msg) @@ -767,7 +801,7 @@ def _open(self) -> None: self._mode = self.png.im_mode self._size = self.png.im_size self.info = self.png.im_info - self._text = None + self._text: dict[str, str | iTXt] | None = 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 @@ -794,7 +828,7 @@ def _open(self) -> None: self.is_animated = self.n_frames > 1 @property - def text(self): + def text(self) -> dict[str, str | iTXt]: # experimental if self._text is None: # iTxt, tEXt and zTXt chunks may appear at the end of the file @@ -806,6 +840,7 @@ def text(self): self.load() if self.is_animated: self.seek(frame) + assert self._text is not None return self._text def verify(self) -> None: @@ -822,9 +857,7 @@ def verify(self) -> None: self.png.verify() self.png.close() - if self._exclusive_fp: - self.fp.close() - self.fp = None + super().verify() def seek(self, frame: int) -> None: if not self._seek_check(frame): @@ -833,26 +866,27 @@ def seek(self, frame: int) -> None: self._seek(0, True) last_frame = self.__frame - for f in range(self.__frame + 1, frame + 1): - try: + try: + for f in range(self.__frame + 1, frame + 1): self._seek(f) - except EOFError as e: - self.seek(last_frame) - msg = "no more images in APNG file" - raise EOFError(msg) from e + except EOFError as e: + self.seek(last_frame) + msg = "no more images in APNG file" + raise EOFError(msg) from e def _seek(self, frame: int, rewind: bool = False) -> None: assert self.png is not None + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.dispose: _imaging.ImagingCore | None + dispose_extent = None 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._im = None self.info = self.png.im_info self.tile = self.png.im_tile self.fp = self._fp @@ -861,7 +895,7 @@ def _seek(self, frame: int, rewind: bool = False) -> 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") + dispose_extent = self.info.get("bbox") self.__frame = 0 else: if frame != self.__frame + 1: @@ -919,11 +953,13 @@ def _seek(self, frame: int, rewind: bool = False) -> None: 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") + dispose_extent = self.info.get("bbox") if not self.tile: msg = "image not found in APNG frame" raise EOFError(msg) + if dispose_extent: + self.dispose_extent: tuple[float, float, float, float] = dispose_extent # 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: @@ -954,6 +990,7 @@ def load_read(self, read_bytes: int) -> bytes: """internal: read more image data""" assert self.png is not None + assert self.fp is not None while self.__idat == 0: # end of chunk, skip forward to next one @@ -987,6 +1024,7 @@ def load_read(self, read_bytes: int) -> bytes: def load_end(self) -> None: """internal: finished reading image data""" assert self.png is not None + assert self.fp is not None if self.__idat != 0: self.fp.read(self.__idat) while True: @@ -1036,13 +1074,17 @@ def load_end(self) -> None: "RGBA", self.info["transparency"] ) else: + if self.im.mode == "P" and "transparency" in self.info: + t = self.info["transparency"] + if isinstance(t, bytes): + updated.putpalettealphas(t) + elif isinstance(t, int): + updated.putpalettealpha(t) mask = updated.convert("RGBA") self._prev_im.paste(updated, self.dispose_extent, mask) self.im = self._prev_im - if self.pyaccess: - self.pyaccess = None - def _getexif(self) -> dict[str, Any] | None: + def _getexif(self) -> dict[int, Any] | None: if "exif" not in self.info: self.load() if "exif" not in self.info and "Raw profile type exif" not in self.info: @@ -1079,21 +1121,21 @@ def getexif(self) -> Image.Exif: } -def putchunk(fp, cid, *data): +def putchunk(fp: IO[bytes], cid: bytes, *data: bytes) -> None: """Write a PNG chunk (including CRC field)""" - data = b"".join(data) + byte_data = b"".join(data) - fp.write(o32(len(data)) + cid) - fp.write(data) - crc = _crc32(data, _crc32(cid)) + fp.write(o32(len(byte_data)) + cid) + fp.write(byte_data) + crc = _crc32(byte_data, _crc32(cid)) fp.write(o32(crc)) class _idat: # wrap output from the encoder in IDAT chunks - def __init__(self, fp, chunk): + def __init__(self, fp: IO[bytes], chunk: Callable[..., None]) -> None: self.fp = fp self.chunk = chunk @@ -1104,7 +1146,7 @@ def write(self, data: bytes) -> None: class _fdat: # wrap encoder output in fdAT chunks - def __init__(self, fp, chunk, seq_num): + def __init__(self, fp: IO[bytes], chunk: Callable[..., None], seq_num: int) -> None: self.fp = fp self.chunk = chunk self.seq_num = seq_num @@ -1114,7 +1156,30 @@ def write(self, data: bytes) -> None: self.seq_num += 1 -def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_images): +def _apply_encoderinfo(im: Image.Image, encoderinfo: dict[str, Any]) -> None: + im.encoderconfig = ( + encoderinfo.get("optimize", False), + encoderinfo.get("compress_level", -1), + encoderinfo.get("compress_type", -1), + encoderinfo.get("dictionary", b""), + ) + + +class _Frame(NamedTuple): + im: Image.Image + bbox: tuple[int, int, int, int] | None + encoderinfo: dict[str, Any] + + +def _write_multiple_frames( + im: Image.Image, + fp: IO[bytes], + chunk: Callable[..., None], + mode: str, + rawmode: str, + default_image: Image.Image | None, + append_images: list[Image.Image], +) -> Image.Image | None: duration = im.encoderinfo.get("duration") loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE)) @@ -1125,7 +1190,7 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i else: chain = itertools.chain([im], append_images) - im_frames = [] + im_frames: list[_Frame] = [] frame_count = 0 for im_seq in chain: for im_frame in ImageSequence.Iterator(im_seq): @@ -1146,24 +1211,24 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i if im_frames: previous = im_frames[-1] - prev_disposal = previous["encoderinfo"].get("disposal") - prev_blend = previous["encoderinfo"].get("blend") + 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() + base_im = previous.im.copy() dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0)) - bbox = previous["bbox"] + 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"] + base_im = im_frames[-2].im else: - base_im = previous["im"] + base_im = previous.im delta = ImageChops.subtract_modulo( im_frame.convert("RGBA"), base_im.convert("RGBA") ) @@ -1174,14 +1239,14 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i and prev_blend == encoderinfo.get("blend") and "duration" in encoderinfo ): - 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(_Frame(im_frame, bbox, encoderinfo)) if len(im_frames) == 1 and not default_image: - return im_frames[0]["im"] + return im_frames[0].im # animation control chunk( @@ -1193,21 +1258,29 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i # default image IDAT (if it exists) if default_image: - if im.mode != mode: - im = im.convert(mode) - ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) + default_im = im if im.mode == mode else im.convert(mode) + _apply_encoderinfo(default_im, im.encoderinfo) + ImageFile._save( + default_im, + cast(IO[bytes], _idat(fp, chunk)), + [ImageFile._Tile("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"]: + im_frame = frame_data.im + if not frame_data.bbox: bbox = (0, 0) + im_frame.size else: - bbox = frame_data["bbox"] + 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", 0))) + encoderinfo = frame_data.encoderinfo + frame_duration = encoderinfo.get("duration", 0) + delay = Fraction(frame_duration / 1000).limit_denominator(65535) + if delay.numerator > 65535: + msg = "cannot write duration" + raise ValueError(msg) frame_disposal = encoderinfo.get("disposal", disposal) frame_blend = encoderinfo.get("blend", blend) # frame control @@ -1219,35 +1292,43 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i o32(size[1]), # height o32(bbox[0]), # x_offset o32(bbox[1]), # y_offset - o16(frame_duration), # delay_numerator - o16(1000), # delay_denominator + o16(delay.numerator), # delay_numerator + o16(delay.denominator), # delay_denominator o8(frame_disposal), # dispose_op o8(frame_blend), # blend_op ) seq_num += 1 # frame data + _apply_encoderinfo(im_frame, im.encoderinfo) 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)], + cast(IO[bytes], _idat(fp, chunk)), + [ImageFile._Tile("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)], + cast(IO[bytes], fdat_chunks), + [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)], ) seq_num = fdat_chunks.seq_num + return None def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: _save(im, fp, filename, save_all=True) -def _save(im, fp, filename, chunk=putchunk, save_all=False): +def _save( + im: Image.Image, + fp: IO[bytes], + filename: str | bytes, + chunk: Callable[..., None] = putchunk, + save_all: bool = False, +) -> None: # save an image to disk (called by the save method) if save_all: @@ -1272,6 +1353,9 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): mode = im.mode outmode = mode + palette = [] + if im.palette: + palette = im.getpalette() or [] if mode == "P": # # attempt to minimize storage requirements for palette images @@ -1281,7 +1365,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): else: # check palette contents if im.palette: - colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1) + colors = max(min(len(palette) // 3, 256), 1) else: colors = 256 @@ -1294,20 +1378,14 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): bits = 4 outmode += f";{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""), - ) - # get the corresponding PNG mode try: rawmode, bit_depth, color_type = _OUTMODES[outmode] except KeyError as e: msg = f"cannot write mode {mode} as PNG" raise OSError(msg) from e + if outmode == "I": + deprecate("Saving I mode images as PNG", 13, stacklevel=4) # # write minimal PNG file @@ -1326,10 +1404,9 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): b"\0", # 12: interlace flag ) - chunks = [b"cHRM", b"gAMA", b"sBIT", b"sRGB", b"tIME"] + chunks = [b"cHRM", b"cICP", b"gAMA", b"sBIT", b"sRGB", b"tIME"] - icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) - if icc: + if icc := im.encoderinfo.get("icc_profile", im.info.get("icc_profile")): # ICC profile # according to PNG spec, the iCCP chunk contains: # Profile name 1-79 bytes (character string) @@ -1344,8 +1421,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): # Disallow sRGB chunks when an iCCP-chunk has been emitted. chunks.remove(b"sRGB") - info = im.encoderinfo.get("pnginfo") - if info: + if info := im.encoderinfo.get("pnginfo"): chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"] for info_chunk in info.chunks: cid, data = info_chunk[:2] @@ -1362,7 +1438,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): if im.mode == "P": palette_byte_number = colors * 3 - palette_bytes = im.im.getpalette("RGB")[:palette_byte_number] + palette_bytes = bytes(palette[:palette_byte_number]) while len(palette_bytes) < palette_byte_number: palette_bytes += b"\0" chunk(fp, b"PLTE", palette_bytes) @@ -1377,7 +1453,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): 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 in ("1", "L", "I", "I;16"): transparency = max(0, min(65535, transparency)) @@ -1397,8 +1473,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): alpha_bytes = colors chunk(fp, b"tRNS", alpha[:alpha_bytes]) - dpi = im.encoderinfo.get("dpi") - if dpi: + if dpi := im.encoderinfo.get("dpi"): chunk( fp, b"pHYs", @@ -1415,20 +1490,25 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): chunks.remove(cid) chunk(fp, cid, data) - exif = im.encoderinfo.get("exif") - if exif: + if exif := im.encoderinfo.get("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) + single_im: Image.Image | None = im if save_all: - im = _write_multiple_frames( + single_im = _write_multiple_frames( im, fp, chunk, mode, rawmode, default_image, append_images ) - if im: - ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) + if single_im: + _apply_encoderinfo(single_im, im.encoderinfo) + ImageFile._save( + single_im, + cast(IO[bytes], _idat(fp, chunk)), + [ImageFile._Tile("zip", (0, 0) + single_im.size, 0, rawmode)], + ) if info: for info_chunk in info.chunks: @@ -1449,32 +1529,26 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): # PNG chunk converter -def getchunks(im, **params): +def getchunks(im: Image.Image, **params: Any) -> list[tuple[bytes, bytes, bytes]]: """Return a list of PNG chunks representing this image.""" + from io import BytesIO - class collector: - data = [] - - def write(self, data: bytes) -> None: - pass - - def append(self, chunk: bytes) -> None: - self.data.append(chunk) + chunks = [] - def append(fp, cid, *data): - data = b"".join(data) - crc = o32(_crc32(data, _crc32(cid))) - fp.append((cid, data, crc)) + def append(fp: IO[bytes], cid: bytes, *data: bytes) -> None: + byte_data = b"".join(data) + crc = o32(_crc32(byte_data, _crc32(cid))) + chunks.append((cid, byte_data, crc)) - fp = collector() + fp = BytesIO() try: im.encoderinfo = params - _save(im, fp, None, append) + _save(im, fp, "", append) finally: del im.encoderinfo - return fp.data + return chunks # -------------------------------------------------------------------- diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 16c9ccbba72..307bc97ff65 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -47,7 +47,7 @@ def _accept(prefix: bytes) -> bool: - return prefix[0:1] == b"P" and prefix[1] in b"0123456fy" + return len(prefix) >= 2 and prefix.startswith(b"P") and prefix[1] in b"0123456fy" ## @@ -94,8 +94,8 @@ def _read_token(self) -> bytes: msg = "Reached EOF while reading header" raise ValueError(msg) elif len(token) > 10: - msg = f"Token too long in file header: {token.decode()}" - raise ValueError(msg) + msg_too_long = b"Token too long in file header: %s" % token + raise ValueError(msg_too_long) return token def _open(self) -> None: @@ -151,7 +151,9 @@ def _open(self) -> None: decoder_name = "ppm" args = rawmode if decoder_name == "raw" else (rawmode, maxval) - self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)] + self.tile = [ + ImageFile._Tile(decoder_name, (0, 0) + self.size, self.fp.tell(), args) + ] # @@ -228,7 +230,7 @@ def _decode_bitonal(self) -> bytearray: msg = b"Invalid token for this mode: %s" % bytes([token]) raise ValueError(msg) data = (data + tokens)[:total_bytes] - invert = bytes.maketrans(b"01", b"\xFF\x00") + invert = bytes.maketrans(b"01", b"\xff\x00") return data.translate(invert) def _decode_blocks(self, maxval: int) -> bytearray: @@ -282,7 +284,7 @@ def _decode_blocks(self, maxval: int) -> bytearray: break return data - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: self._comment_spans = False if self.mode == "1": data = self._decode_bitonal() @@ -298,7 +300,7 @@ def decode(self, buffer: bytes) -> tuple[int, int]: class PpmDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None data = bytearray() @@ -333,7 +335,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: rawmode, head = "1;I", b"P4" elif im.mode == "L": rawmode, head = "L", b"P5" - elif im.mode == "I": + elif im.mode in ("I", "I;16"): rawmode, head = "I;16B", b"P5" elif im.mode in ("RGB", "RGBA"): rawmode, head = "RGB", b"P6" @@ -353,7 +355,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: elif head == b"Pf": fp.write(b"-1.0\n") row_order = -1 if im.mode == "F" else 1 - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))] + ) # diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index edf698bf02d..dd3d5ab95fd 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -19,6 +19,7 @@ import io from functools import cached_property +from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i8 @@ -26,6 +27,7 @@ from ._binary import i32be as i32 from ._binary import si16be as si16 from ._binary import si32be as si32 +from ._util import DeferredError MODES = { # (photoshop mode, bits) -> (pil mode, required channels) @@ -46,7 +48,7 @@ def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"8BPS" + return prefix.startswith(b"8BPS") ## @@ -59,6 +61,7 @@ class PsdImageFile(ImageFile.ImageFile): _close_exclusive_fp_after_loading = False def _open(self) -> None: + assert self.fp is not None read = self.fp.read # @@ -142,9 +145,13 @@ def _open(self) -> None: self._min_frame = 1 @cached_property - def layers(self): + def layers( + self, + ) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]: layers = [] if self._layers_position is not None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex self._fp.seek(self._layers_position) _layer_data = io.BytesIO(ImageFile._safe_read(self._fp, self._layers_size)) layers = _layerinfo(_layer_data, self._layers_size) @@ -164,28 +171,31 @@ def is_animated(self) -> bool: def seek(self, layer: int) -> None: if not self._seek_check(layer): return + if isinstance(self._fp, DeferredError): + raise self._fp.ex # seek to given layer (1..max) - try: - _, mode, _, tile = self.layers[layer - 1] - self._mode = mode - self.tile = tile - self.frame = layer - self.fp = self._fp - except IndexError as e: - msg = "no such layer" - raise EOFError(msg) from e + if layer > len(self.layers): + msg = "no more images in PSD file" + raise EOFError(msg) + _, mode, _, tile = self.layers[layer - 1] + self._mode = mode + self.tile = tile + self.frame = layer + self.fp = self._fp def tell(self) -> int: # return layer number (0=image, 1..max=layers) return self.frame -def _layerinfo(fp, ct_bytes): +def _layerinfo( + fp: IO[bytes], ct_bytes: int +) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]: # read layerinfo block layers = [] - def read(size): + def read(size: int) -> bytes: return ImageFile._safe_read(fp, size) ct = si16(read(2)) @@ -203,7 +213,7 @@ def read(size): x1 = si32(read(4)) # image info - mode = [] + bands = [] ct_types = i16(read(2)) if ct_types > 4: fp.seek(ct_types * 6 + 12, io.SEEK_CUR) @@ -215,23 +225,23 @@ def read(size): type = i16(read(2)) if type == 65535: - m = "A" + b = "A" else: - m = "RGBA"[type] + b = "RGBA"[type] - mode.append(m) + bands.append(b) read(4) # size # figure out the image mode - mode.sort() - if mode == ["R"]: + bands.sort() + if bands == ["R"]: mode = "L" - elif mode == ["B", "G", "R"]: + elif bands == ["B", "G", "R"]: mode = "RGB" - elif mode == ["A", "B", "G", "R"]: + elif bands == ["A", "B", "G", "R"]: mode = "RGBA" else: - mode = None # unknown + mode = "" # unknown # skip over blend flags and extra information read(12) # filler @@ -258,19 +268,22 @@ def read(size): layers.append((name, mode, (x0, y0, x1, y1))) # get tiles + layerinfo = [] for i, (name, mode, bbox) in enumerate(layers): tile = [] for m in mode: t = _maketile(fp, m, bbox, 1) if t: tile.extend(t) - layers[i] = name, mode, bbox, tile + layerinfo.append((name, mode, bbox, tile)) - return layers + return layerinfo -def _maketile(file, mode, bbox, channels): - tile = None +def _maketile( + file: IO[bytes], mode: str, bbox: tuple[int, int, int, int], channels: int +) -> list[ImageFile._Tile]: + tiles = [] read = file.read compression = i16(read(2)) @@ -283,26 +296,24 @@ def _maketile(file, mode, bbox, channels): if compression == 0: # # raw compression - tile = [] for channel in range(channels): layer = mode[channel] if mode == "CMYK": layer += ";I" - tile.append(("raw", bbox, offset, layer)) + tiles.append(ImageFile._Tile("raw", bbox, offset, layer)) offset = offset + xsize * ysize elif compression == 1: # # packbits compression i = 0 - tile = [] bytecount = read(channels * ysize * 2) offset = file.tell() for channel in range(channels): layer = mode[channel] if mode == "CMYK": layer += ";I" - tile.append(("packbits", bbox, offset, layer)) + tiles.append(ImageFile._Tile("packbits", bbox, offset, layer)) for y in range(ysize): offset = offset + i16(bytecount, i) i += 2 @@ -312,7 +323,7 @@ def _maketile(file, mode, bbox, channels): if offset & 1: read(1) # padding - return tile + return tiles # -------------------------------------------------------------------- diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py deleted file mode 100644 index 3be1ccace0c..00000000000 --- a/src/PIL/PyAccess.py +++ /dev/null @@ -1,381 +0,0 @@ -# -# The Python Imaging Library -# Pillow fork -# -# Python implementation of the PixelAccess Object -# -# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved. -# Copyright (c) 1995-2009 by Fredrik Lundh. -# Copyright (c) 2013 Eric Soroos -# -# See the README file for information on usage and redistribution -# - -# Notes: -# -# * Implements the pixel access object following Access.c -# * Taking only the tuple form, which is used from python. -# * Fill.c uses the integer form, but it's still going to use the old -# Access.c implementation. -# -from __future__ import annotations - -import logging -import sys -from typing import TYPE_CHECKING - -from ._deprecate import deprecate - -FFI: type -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.new(ex) - -logger = logging.getLogger(__name__) - -if TYPE_CHECKING: - from . import Image - - -class PyAccess: - def __init__(self, img: Image.Image, readonly: bool = False) -> None: - deprecate("PyAccess", 11) - 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.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 - # logger.debug("%s", vals) - self._post_init() - - def _post_init(self) -> None: - pass - - def __setitem__( - self, - xy: tuple[int, int] | list[int], - color: float | tuple[int, ...] | list[int], - ) -> None: - """ - 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. In addition to this, RGB and RGBA tuples - are accepted for P and PA images. - - :param xy: The pixel coordinate, given as (x, y). See - :ref:`coordinate-system`. - :param color: The pixel value. - """ - if self.readonly: - msg = "Attempt to putpixel a read only image" - raise ValueError(msg) - (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] - palette_index = self._palette.getcolor(color, self._img) - color = (palette_index, alpha) if self._im.mode == "PA" else palette_index - - return self.set_pixel(x, y, color) - - def __getitem__(self, xy: tuple[int, int] | list[int]) -> float | tuple[int, ...]: - """ - Returns the pixel at x,y. The pixel is returned as a single - value for single band images or a tuple for multiple band - images - - :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) = 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__ - getpixel = __getitem__ - - def check_xy(self, xy: tuple[int, int]) -> tuple[int, int]: - (x, y) = xy - if not (0 <= x < self.xsize and 0 <= y < self.ysize): - msg = "pixel location out of range" - raise ValueError(msg) - return xy - - def get_pixel(self, x: int, y: int) -> float | tuple[int, ...]: - raise NotImplementedError() - - def set_pixel( - self, x: int, y: int, color: float | tuple[int, ...] | list[int] - ) -> None: - raise NotImplementedError() - - -class _PyAccess32_2(PyAccess): - """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: int, y: int) -> tuple[int, int]: - pixel = self.pixels[y][x] - return pixel.r, pixel.a - - def set_pixel(self, x, y, color): - pixel = self.pixels[y][x] - # tuple - pixel.r = min(color[0], 255) - pixel.a = min(color[1], 255) - - -class _PyAccess32_3(PyAccess): - """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: int, y: int) -> tuple[int, int, int]: - pixel = self.pixels[y][x] - return pixel.r, pixel.g, pixel.b - - def set_pixel(self, x, y, color): - pixel = self.pixels[y][x] - # tuple - pixel.r = min(color[0], 255) - pixel.g = min(color[1], 255) - pixel.b = min(color[2], 255) - pixel.a = 255 - - -class _PyAccess32_4(PyAccess): - """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: int, y: int) -> tuple[int, int, int, int]: - pixel = self.pixels[y][x] - return pixel.r, pixel.g, pixel.b, pixel.a - - def set_pixel(self, x, y, color): - pixel = self.pixels[y][x] - # tuple - pixel.r = min(color[0], 255) - pixel.g = min(color[1], 255) - pixel.b = min(color[2], 255) - pixel.a = min(color[3], 255) - - -class _PyAccess8(PyAccess): - """1, L, P, 8 bit images stored as uint8""" - - def _post_init(self, *args, **kwargs): - self.pixels = self.image8 - - def get_pixel(self, x: int, y: int) -> int: - return self.pixels[y][x] - - def set_pixel(self, x, y, color): - try: - # integer - self.pixels[y][x] = min(color, 255) - except TypeError: - # tuple - self.pixels[y][x] = min(color[0], 255) - - -class _PyAccessI16_N(PyAccess): - """I;16 access, native bitendian without conversion""" - - def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast("unsigned short **", self.image) - - def get_pixel(self, x: int, y: int) -> int: - return self.pixels[y][x] - - def set_pixel(self, x, y, color): - try: - # integer - self.pixels[y][x] = min(color, 65535) - except TypeError: - # tuple - self.pixels[y][x] = min(color[0], 65535) - - -class _PyAccessI16_L(PyAccess): - """I;16L access, with conversion""" - - def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast("struct Pixel_I16 **", self.image) - - def get_pixel(self, x: int, y: int) -> int: - pixel = self.pixels[y][x] - return pixel.l + pixel.r * 256 - - def set_pixel(self, x, y, color): - pixel = self.pixels[y][x] - try: - color = min(color, 65535) - except TypeError: - color = min(color[0], 65535) - - pixel.l = color & 0xFF - pixel.r = color >> 8 - - -class _PyAccessI16_B(PyAccess): - """I;16B access, with conversion""" - - def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast("struct Pixel_I16 **", self.image) - - def get_pixel(self, x: int, y: int) -> int: - pixel = self.pixels[y][x] - return pixel.l * 256 + pixel.r - - def set_pixel(self, x, y, color): - pixel = self.pixels[y][x] - try: - color = min(color, 65535) - except Exception: - color = min(color[0], 65535) - - pixel.l = color >> 8 - pixel.r = color & 0xFF - - -class _PyAccessI32_N(PyAccess): - """Signed Int32 access, native endian""" - - def _post_init(self, *args, **kwargs): - self.pixels = self.image32 - - def get_pixel(self, x: int, y: int) -> int: - return self.pixels[y][x] - - def set_pixel(self, x, y, color): - self.pixels[y][x] = color - - -class _PyAccessI32_Swap(PyAccess): - """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] - - def get_pixel(self, x: int, y: int) -> int: - return self.reverse(self.pixels[y][x]) - - def set_pixel(self, x, y, color): - self.pixels[y][x] = self.reverse(color) - - -class _PyAccessF(PyAccess): - """32 bit float access""" - - def _post_init(self, *args, **kwargs): - self.pixels = ffi.cast("float **", self.image32) - - def get_pixel(self, x: int, y: int) -> float: - return self.pixels[y][x] - - def set_pixel(self, x, y, color): - try: - # not a tuple - self.pixels[y][x] = color - except TypeError: - # tuple - self.pixels[y][x] = color[0] - - -mode_map = { - "1": _PyAccess8, - "L": _PyAccess8, - "P": _PyAccess8, - "I;16N": _PyAccessI16_N, - "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;32L"] = _PyAccessI32_Swap - mode_map["I;32B"] = _PyAccessI32_N - - -def new(img: Image.Image, readonly: bool = False) -> PyAccess | None: - access_type = mode_map.get(img.mode, None) - if not access_type: - logger.debug("PyAccess Not Implemented: %s", img.mode) - return None - return access_type(img, readonly) diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index 202ef52d09b..d0709b1198a 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -8,13 +8,16 @@ from __future__ import annotations import os +from typing import IO from . import Image, ImageFile from ._binary import i32be as i32 +from ._binary import o8 +from ._binary import o32be as o32 def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"qoif" + return prefix.startswith(b"qoif") class QoiImageFile(ImageFile.ImageFile): @@ -22,17 +25,18 @@ class QoiImageFile(ImageFile.ImageFile): format_description = "Quite OK Image" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(4)): msg = "not a QOI file" raise SyntaxError(msg) - self._size = tuple(i32(self.fp.read(4)) for i in range(2)) + self._size = i32(self.fp.read(4)), i32(self.fp.read(4)) channels = self.fp.read(1)[0] self._mode = "RGB" if channels == 3 else "RGBA" self.fp.seek(1, os.SEEK_CUR) # colorspace - self.tile = [("qoi", (0, 0) + self._size, self.fp.tell(), None)] + self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell())] class QoiDecoder(ImageFile.PyDecoder): @@ -47,11 +51,11 @@ def _add_to_previous_pixels(self, value: bytes | bytearray) -> None: hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 self._previously_seen_pixels[hash_value] = value - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None self._previously_seen_pixels = {} - self._add_to_previous_pixels(bytearray((0, 0, 0, 255))) + self._previous_pixel = bytearray((0, 0, 0, 255)) data = bytearray() bands = Image.getmodebands(self.mode) @@ -110,6 +114,122 @@ def decode(self, buffer: bytes) -> tuple[int, int]: return -1, 0 +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.mode == "RGB": + channels = 3 + elif im.mode == "RGBA": + channels = 4 + else: + msg = "Unsupported QOI image mode" + raise ValueError(msg) + + colorspace = 0 if im.encoderinfo.get("colorspace") == "sRGB" else 1 + + fp.write(b"qoif") + fp.write(o32(im.size[0])) + fp.write(o32(im.size[1])) + fp.write(o8(channels)) + fp.write(o8(colorspace)) + + ImageFile._save(im, fp, [ImageFile._Tile("qoi", (0, 0) + im.size)]) + + +class QoiEncoder(ImageFile.PyEncoder): + _pushes_fd = True + _previous_pixel: tuple[int, int, int, int] | None = None + _previously_seen_pixels: dict[int, tuple[int, int, int, int]] = {} + _run = 0 + + def _write_run(self) -> bytes: + data = o8(0b11000000 | (self._run - 1)) # QOI_OP_RUN + self._run = 0 + return data + + def _delta(self, left: int, right: int) -> int: + result = (left - right) & 255 + if result >= 128: + result -= 256 + return result + + def encode(self, bufsize: int) -> tuple[int, int, bytes]: + assert self.im is not None + + self._previously_seen_pixels = {0: (0, 0, 0, 0)} + self._previous_pixel = (0, 0, 0, 255) + + data = bytearray() + w, h = self.im.size + bands = Image.getmodebands(self.mode) + + for y in range(h): + for x in range(w): + pixel = self.im.getpixel((x, y)) + if bands == 3: + pixel = (*pixel, 255) + + if pixel == self._previous_pixel: + self._run += 1 + if self._run == 62: + data += self._write_run() + else: + if self._run: + data += self._write_run() + + r, g, b, a = pixel + hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 + if self._previously_seen_pixels.get(hash_value) == pixel: + data += o8(hash_value) # QOI_OP_INDEX + elif self._previous_pixel: + self._previously_seen_pixels[hash_value] = pixel + + prev_r, prev_g, prev_b, prev_a = self._previous_pixel + if prev_a == a: + delta_r = self._delta(r, prev_r) + delta_g = self._delta(g, prev_g) + delta_b = self._delta(b, prev_b) + + if ( + -2 <= delta_r < 2 + and -2 <= delta_g < 2 + and -2 <= delta_b < 2 + ): + data += o8( + 0b01000000 + | (delta_r + 2) << 4 + | (delta_g + 2) << 2 + | (delta_b + 2) + ) # QOI_OP_DIFF + else: + delta_gr = self._delta(delta_r, delta_g) + delta_gb = self._delta(delta_b, delta_g) + if ( + -8 <= delta_gr < 8 + and -32 <= delta_g < 32 + and -8 <= delta_gb < 8 + ): + data += o8( + 0b10000000 | (delta_g + 32) + ) # QOI_OP_LUMA + data += o8((delta_gr + 8) << 4 | (delta_gb + 8)) + else: + data += o8(0b11111110) # QOI_OP_RGB + data += bytes(pixel[:3]) + else: + data += o8(0b11111111) # QOI_OP_RGBA + data += bytes(pixel) + + self._previous_pixel = pixel + + if self._run: + data += self._write_run() + data += bytes((0, 0, 0, 0, 0, 0, 0, 1)) # padding + + return len(data), 0, data + + Image.register_open(QoiImageFile.format, QoiImageFile, _accept) Image.register_decoder("qoi", QoiDecoder) Image.register_extension(QoiImageFile.format, ".qoi") + +Image.register_save(QoiImageFile.format, _save) +Image.register_encoder("qoi", QoiEncoder) diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index 50d97910932..853022150ae 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -82,17 +82,10 @@ def _open(self) -> None: # zsize : channels count zsize = i16(s, 10) - # layout - layout = bpc, dimension, zsize - # determine mode from bits/zsize - rawmode = "" try: - rawmode = MODES[layout] + rawmode = MODES[(bpc, dimension, zsize)] except KeyError: - pass - - if rawmode == "": msg = "Unsupported SGI image mode" raise ValueError(msg) @@ -109,19 +102,28 @@ def _open(self) -> None: pagesize = xsize * ysize * bpc if bpc == 2: self.tile = [ - ("SGI16", (0, 0) + self.size, headlen, (self.mode, 0, orientation)) + ImageFile._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)) + ImageFile._Tile( + "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)) + ImageFile._Tile( + "sgi_rle", (0, 0) + self.size, headlen, (rawmode, orientation, bpc) + ) ] @@ -147,24 +149,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Run-Length Encoding Compression - Unsupported at this time rle = 0 - # Number of dimensions (x,y,z) - dim = 3 # X Dimension = width / Y Dimension = height x, y = im.size - if im.mode == "L" and y == 1: - dim = 1 - elif im.mode == "L": - dim = 2 # Z Dimension: Number of channels z = len(im.mode) - - if dim in {1, 2}: - z = 1 - - # assert we've got the right number of bands. - if len(im.getbands()) != z: - msg = f"incorrect number of bands in SGI write: {z} vs {len(im.getbands())}" - raise ValueError(msg) + # Number of dimensions (x,y,z) + if im.mode == "L": + dimension = 1 if y == 1 else 2 + else: + dimension = 3 # Minimum Byte value pinmin = 0 @@ -179,7 +172,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 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", dimension)) fp.write(struct.pack(">H", x)) fp.write(struct.pack(">H", y)) fp.write(struct.pack(">H", z)) @@ -205,7 +198,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: class SGI16Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer: bytes) -> tuple[int, int]: + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None assert self.im is not None diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index f5a09c3ef61..11d90699d10 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -37,9 +37,12 @@ import os import struct import sys -from typing import IO, TYPE_CHECKING, Any, Tuple, cast +from typing import IO, Any, cast from . import Image, ImageFile +from ._util import DeferredError + +TYPE_CHECKING = False def isInt(f: Any) -> int: @@ -101,6 +104,7 @@ class SpiderImageFile(ImageFile.ImageFile): def _open(self) -> None: # check header n = 27 * 4 # read 27 float values + assert self.fp is not None f = self.fp.read(n) try: @@ -154,7 +158,7 @@ def _open(self) -> None: self.rawmode = "F;32F" self._mode = "F" - self.tile = [("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, offset, self.rawmode)] self._fp = self.fp # FIXME: hack @property @@ -178,6 +182,8 @@ def seek(self, frame: int) -> None: raise EOFError(msg) if not self._seek_check(frame): return + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) self.fp = self._fp self.fp.seek(self.stkoffset) @@ -187,7 +193,7 @@ def seek(self, frame: int) -> None: def convert2byte(self, depth: int = 255) -> Image.Image: extrema = self.getextrema() assert isinstance(extrema[0], float) - minimum, maximum = cast(Tuple[float, float], extrema) + minimum, maximum = cast(tuple[float, float], extrema) m: float = 1 if maximum != minimum: m = depth / (maximum - minimum) @@ -209,26 +215,27 @@ def tkPhotoImage(self) -> ImageTk.PhotoImage: # given a list of filenames, return a list of images -def loadImageSeries(filelist: list[str] | None = None) -> list[SpiderImageFile] | None: +def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | None: """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" if filelist is None or len(filelist) < 1: return None - imglist = [] + byte_imgs = [] for img in filelist: if not os.path.exists(img): print(f"unable to find {img}") continue try: with Image.open(img) as im: - im = im.convert2byte() + assert isinstance(im, SpiderImageFile) + byte_im = im.convert2byte() except Exception: if not isSpiderImage(img): print(f"{img} is not a Spider image file") continue - im.info["filename"] = img - imglist.append(im) - return imglist + byte_im.info["filename"] = img + byte_imgs.append(byte_im) + return byte_imgs # -------------------------------------------------------------------- @@ -237,7 +244,7 @@ def loadImageSeries(filelist: list[str] | None = None) -> list[SpiderImageFile] def makeSpiderHeader(im: Image.Image) -> list[bytes]: nsam, nrow = im.size - lenbyt = nsam * 4 # There are labrec records in the header + lenbyt = max(1, nsam) * 4 # There are labrec records in the header labrec = int(1024 / lenbyt) if 1024 % lenbyt != 0: labrec += 1 @@ -266,7 +273,7 @@ def makeSpiderHeader(im: Image.Image) -> list[bytes]: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode[0] != "F": + if im.mode != "F": im = im.convert("F") hdr = makeSpiderHeader(im) @@ -278,14 +285,14 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 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, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)]) def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # get the filename extension and register it with Image - filename_ext = os.path.splitext(filename)[1] - ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext - Image.register_extension(SpiderImageFile.format, ext) + if filename_ext := os.path.splitext(filename)[1]: + ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext + Image.register_extension(SpiderImageFile.format, ext) _save(im, fp, filename) @@ -317,9 +324,9 @@ def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: outfile = sys.argv[2] # perform some image operation - im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + transposed_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) + transposed_im.save(outfile, SpiderImageFile.format) diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index 4e098474ab9..8912379ea3e 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -124,9 +124,13 @@ def _open(self) -> None: # (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 = [ + ImageFile._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 = [ + ImageFile._Tile("sun_rle", (0, 0) + self.size, offset, rawmode) + ] else: msg = "Unsupported Sun Raster file type" raise SyntaxError(msg) diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index cba26d4b059..86490a496f3 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -35,12 +35,16 @@ def __init__(self, tarfile: str, file: str) -> None: while True: s = self.fh.read(512) if len(s) != 512: + self.fh.close() + msg = "unexpected end of tar file" raise OSError(msg) name = s[:100].decode("utf-8") i = name.find("\0") if i == 0: + self.fh.close() + msg = "cannot find subfile" raise OSError(msg) if i > 0: @@ -55,13 +59,3 @@ def __init__(self, tarfile: str, file: str) -> None: # Open region super().__init__(self.fh, self.fh.tell(), size) - - # Context manager support - def __enter__(self) -> TarIO: - return self - - def __exit__(self, *args: object) -> None: - self.close() - - def close(self) -> None: - self.fh.close() diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 39104aeced9..b2989a4b764 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -17,11 +17,13 @@ # from __future__ import annotations +import os import warnings from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 +from ._binary import i32le as i32 from ._binary import o8 from ._binary import o16le as o16 @@ -137,7 +139,7 @@ def _open(self) -> None: if imagetype & 8: # compressed self.tile = [ - ( + ImageFile._Tile( "tga_rle", (0, 0) + self.size, self.fp.tell(), @@ -146,7 +148,7 @@ def _open(self) -> None: ] else: self.tile = [ - ( + ImageFile._Tile( "raw", (0, 0) + self.size, self.fp.tell(), @@ -157,8 +159,21 @@ def _open(self) -> None: pass # cannot decode def load_end(self) -> None: + if self.mode == "RGBA": + assert self.fp is not None + self.fp.seek(-26, os.SEEK_END) + footer = self.fp.read(26) + if footer.endswith(b"TRUEVISION-XFILE.\x00"): + # version 2 + extension_offset = i32(footer) + if extension_offset: + self.fp.seek(extension_offset + 494) + attributes_type = self.fp.read(1) + if attributes_type == b"\x00": + # No alpha + self.im.fillband(3, 255) + if self._flip_horizontally: - assert self.im is not None self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) @@ -200,7 +215,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: warnings.warn("id_section has been trimmed to 255 characters") if colormaptype: - assert im.im is not None palette = im.im.getpalette("RGB", "BGR") colormaplength, colormapentry = len(palette) // 3, 24 else: @@ -238,11 +252,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if rle: ImageFile._save( - im, fp, [("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))] + im, + fp, + [ImageFile._Tile("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))], ) else: ImageFile._save( - im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))] + im, + fp, + [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))], ) # write targa version 2 footer diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ac5b63c1bc4..5094faa1325 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -47,24 +47,30 @@ import os import struct import warnings -from collections.abc import MutableMapping +from collections.abc import Callable, MutableMapping from fractions import Fraction from numbers import Number, Rational -from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn +from typing import IO, Any, cast from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 -from ._deprecate import deprecate +from ._util import DeferredError, is_path from .TiffTags import TYPES +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Iterator + from typing import NoReturn + + from ._typing import Buffer, IntegralLike, StrOrBytesPath + 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) @@ -246,6 +252,7 @@ (II, 3, (1,), 1, (8,), ()): ("P", "P"), (MM, 3, (1,), 1, (8,), ()): ("P", "P"), (II, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"), + (MM, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"), (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), @@ -257,6 +264,7 @@ (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"), + (MM, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16B"), (II, 6, (1,), 1, (8,), ()): ("L", "L"), (MM, 6, (1,), 1, (8,), ()): ("L", "L"), # JPEG compressed images handled by LibTiff and auto-converted to RGBX @@ -270,38 +278,39 @@ MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO) 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 + 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 ] -if not getattr(Image.core, "libtiff_support_custom_tags", True): - deprecate("Support for LibTIFF earlier than version 4", 12) - def _accept(prefix: bytes) -> bool: - return prefix[:4] in PREFIXES + return prefix.startswith(tuple(PREFIXES)) -def _limit_rational(val, max_val): +def _limit_rational( + val: float | Fraction | IFDRational, max_val: int +) -> tuple[IntegralLike, IntegralLike]: inv = abs(val) > 1 n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) return n_d[::-1] if inv else n_d -def _limit_signed_rational(val, max_val, min_val): +def _limit_signed_rational( + val: IFDRational, max_val: int, min_val: int +) -> tuple[IntegralLike, IntegralLike]: frac = Fraction(val) - n_d = frac.numerator, frac.denominator + n_d: tuple[IntegralLike, IntegralLike] = frac.numerator, frac.denominator - if min(n_d) < min_val: + if min(float(i) for i in 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) + n_d_float = tuple(float(i) for i in n_d) + if max(n_d_float) > max_val: + n_d = _limit_rational(n_d_float[0] / n_d_float[1], max_val) return n_d @@ -313,8 +322,10 @@ def _limit_signed_rational(val, max_val, min_val): _write_dispatch = {} -def _delegate(op): - def delegate(self, *args): +def _delegate(op: str) -> Any: + def delegate( + self: IFDRational, *args: tuple[float, ...] + ) -> bool | float | Fraction: return getattr(self._val, op)(*args) return delegate @@ -334,12 +345,15 @@ class IFDRational(Rational): __slots__ = ("_numerator", "_denominator", "_val") - def __init__(self, value, denominator=1): + def __init__( + self, value: float | Fraction | IFDRational, denominator: int = 1 + ) -> None: """ :param value: either an integer numerator, a float/rational/other number, or an IFDRational :param denominator: Optional integer denominator """ + self._val: Fraction | float if isinstance(value, IFDRational): self._numerator = value.numerator self._denominator = value.denominator @@ -350,25 +364,30 @@ def __init__(self, value, denominator=1): self._numerator = value.numerator self._denominator = value.denominator else: - self._numerator = value + if TYPE_CHECKING: + self._numerator = cast(IntegralLike, value) + else: + self._numerator = value self._denominator = denominator if denominator == 0: self._val = float("nan") elif denominator == 1: self._val = Fraction(value) + elif int(value) == value: + self._val = Fraction(int(value), denominator) else: - self._val = Fraction(value, denominator) + self._val = Fraction(value / denominator) @property - def numerator(self): + def numerator(self) -> IntegralLike: return self._numerator @property - def denominator(self): + def denominator(self) -> int: return self._denominator - def limit_rational(self, max_denominator): + def limit_rational(self, max_denominator: int) -> tuple[IntegralLike, int]: """ :param max_denominator: Integer, the maximum denominator value @@ -378,13 +397,14 @@ def limit_rational(self, max_denominator): if self.denominator == 0: return self.numerator, self.denominator + assert isinstance(self._val, Fraction) f = self._val.limit_denominator(max_denominator) return f.numerator, f.denominator def __repr__(self) -> str: return str(float(self._val)) - def __hash__(self) -> int: + def __hash__(self) -> int: # type: ignore[override] return self._val.__hash__() def __eq__(self, other: object) -> bool: @@ -395,14 +415,19 @@ def __eq__(self, other: object) -> bool: val = float(val) return val == other - def __getstate__(self): + def __getstate__(self) -> list[float | Fraction | IntegralLike]: return [self._val, self._numerator, self._denominator] - def __setstate__(self, state): + def __setstate__(self, state: list[float | Fraction | IntegralLike]) -> None: IFDRational.__init__(self, 0) _val, _numerator, _denominator = state + assert isinstance(_val, (float, Fraction)) self._val = _val - self._numerator = _numerator + if TYPE_CHECKING: + self._numerator = cast(IntegralLike, _numerator) + else: + self._numerator = _numerator + assert isinstance(_denominator, int) self._denominator = _denominator """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', @@ -444,8 +469,11 @@ def __setstate__(self, state): __int__ = _delegate("__int__") -def _register_loader(idx, size): - def decorator(func): +_LoaderFunc = Callable[["ImageFileDirectory_v2", bytes, bool], Any] + + +def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc]: + def decorator(func: _LoaderFunc) -> _LoaderFunc: from .TiffTags import TYPES if func.__name__.startswith("load_"): @@ -456,26 +484,27 @@ def decorator(func): return decorator -def _register_writer(idx): - def decorator(func): +def _register_writer(idx: int) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: _write_dispatch[idx] = func # noqa: F821 return func return decorator -def _register_basic(idx_fmt_name): +def _register_basic(idx_fmt_name: tuple[int, str, str]) -> None: from .TiffTags import TYPES idx, fmt, name = idx_fmt_name TYPES[idx] = name size = struct.calcsize(f"={fmt}") - _load_dispatch[idx] = ( # noqa: F821 - size, - lambda self, data, legacy_api=True: ( - self._unpack(f"{len(data) // size}{fmt}", data) - ), - ) + + def basic_handler( + self: ImageFileDirectory_v2, data: bytes, legacy_api: bool = True + ) -> tuple[Any, ...]: + return self._unpack(f"{len(data) // size}{fmt}", data) + + _load_dispatch[idx] = size, basic_handler # noqa: F821 _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 b"".join(self._pack(fmt, value) for value in values) ) @@ -548,12 +577,12 @@ class ImageFileDirectory_v2(_IFDv2Base): """ - _load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} + _load_dispatch: dict[int, tuple[int, _LoaderFunc]] = {} _write_dispatch: dict[int, Callable[..., Any]] = {} def __init__( self, - ifh: bytes = b"II\052\0\0\0\0\0", + ifh: bytes = b"II\x2a\x00\x00\x00\x00\x00", prefix: bytes | None = None, group: int | None = None, ) -> None: @@ -583,8 +612,10 @@ def __init__( self.tagtype: dict[int, int] = {} """ Dictionary of tag types """ self.reset() - (self.next,) = ( - self._unpack("Q", ifh[8:]) if self._bigtiff else self._unpack("L", ifh[4:]) + self.next = ( + self._unpack("Q", ifh[8:])[0] + if self._bigtiff + else self._unpack("L", ifh[4:])[0] ) self._legacy_api = False @@ -606,12 +637,12 @@ def reset(self) -> None: self._tagdata: dict[int, bytes] = {} self.tagtype = {} # added 2008-06-05 by Florian Hoech self._next = None - self._offset = None + self._offset: int | None = None def __str__(self) -> str: return str(dict(self)) - def named(self): + def named(self) -> dict[str, Any]: """ :returns: dict of name|key: value @@ -625,7 +656,7 @@ def named(self): def __len__(self) -> int: return len(set(self._tagdata) | set(self._tags_v2)) - def __getitem__(self, tag): + def __getitem__(self, tag: int) -> Any: if tag not in self._tags_v2: # unpack on the fly data = self._tagdata[tag] typ = self.tagtype[tag] @@ -636,13 +667,13 @@ def __getitem__(self, tag): val = (val,) return val - def __contains__(self, tag): + def __contains__(self, tag: object) -> bool: return tag in self._tags_v2 or tag in self._tagdata - def __setitem__(self, tag, value): + def __setitem__(self, tag: int, value: Any) -> None: self._setitem(tag, value, self.legacy_api) - def _setitem(self, tag, value, legacy_api): + def _setitem(self, tag: int, value: Any, legacy_api: bool) -> None: basetypes = (Number, bytes, str) info = TiffTags.lookup(tag, self.group) @@ -654,22 +685,33 @@ def _setitem(self, tag, value, legacy_api): else: self.tagtype[tag] = TiffTags.UNDEFINED if all(isinstance(v, IFDRational) for v in values): - self.tagtype[tag] = ( - TiffTags.RATIONAL - if all(v >= 0 for v in values) - else TiffTags.SIGNED_RATIONAL - ) + for v in values: + assert isinstance(v, IFDRational) + if v < 0: + self.tagtype[tag] = TiffTags.SIGNED_RATIONAL + break + else: + self.tagtype[tag] = TiffTags.RATIONAL elif all(isinstance(v, int) for v in values): - if all(0 <= v < 2**16 for v in values): + short = True + signed_short = True + long = True + for v in values: + assert isinstance(v, int) + if short and not (0 <= v < 2**16): + short = False + if signed_short and not (-(2**15) < v < 2**15): + signed_short = False + if long and v < 0: + long = False + if short: self.tagtype[tag] = TiffTags.SHORT - elif all(-(2**15) < v < 2**15 for v in values): + elif signed_short: self.tagtype[tag] = TiffTags.SIGNED_SHORT + elif long: + self.tagtype[tag] = TiffTags.LONG else: - self.tagtype[tag] = ( - TiffTags.LONG - if all(v >= 0 for v in values) - else TiffTags.SIGNED_LONG - ) + self.tagtype[tag] = TiffTags.SIGNED_LONG elif all(isinstance(v, float) for v in values): self.tagtype[tag] = TiffTags.DOUBLE elif all(isinstance(v, str) for v in values): @@ -687,7 +729,10 @@ def _setitem(self, tag, value, legacy_api): 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) + values = tuple( + info.cvt_enum(value) if isinstance(value, str) else value + for value in values + ) dest = self._tags_v1 if legacy_api else self._tags_v2 @@ -727,13 +772,13 @@ def __delitem__(self, tag: int) -> None: self._tags_v1.pop(tag, None) self._tagdata.pop(tag, None) - def __iter__(self): + def __iter__(self) -> Iterator[int]: return iter(set(self._tagdata) | set(self._tags_v2)) - def _unpack(self, fmt, data): + def _unpack(self, fmt: str, data: bytes) -> tuple[Any, ...]: return struct.unpack(self._endian + fmt, data) - def _pack(self, fmt, *values): + def _pack(self, fmt: str, *values: Any) -> bytes: return struct.pack(self._endian + fmt, *values) list( @@ -754,11 +799,11 @@ def _pack(self, fmt, *values): ) @_register_loader(1, 1) # Basic type, except for the legacy API. - def load_byte(self, data, legacy_api=True): + def load_byte(self, data: bytes, legacy_api: bool = True) -> bytes: return data @_register_writer(1) # Basic type, except for the legacy API. - def write_byte(self, data): + def write_byte(self, data: bytes | int | IFDRational) -> bytes: if isinstance(data, IFDRational): data = int(data) if isinstance(data, int): @@ -766,13 +811,13 @@ def write_byte(self, data): return data @_register_loader(2, 1) - def load_string(self, data, legacy_api=True): + def load_string(self, data: bytes, legacy_api: bool = True) -> str: if data.endswith(b"\0"): data = data[:-1] return data.decode("latin-1", "replace") @_register_writer(2) - def write_string(self, value): + def write_string(self, value: str | bytes | int) -> bytes: # remerge of https://github.com/python-pillow/Pillow/pull/1416 if isinstance(value, int): value = str(value) @@ -781,26 +826,28 @@ def write_string(self, value): return value + b"\0" @_register_loader(5, 8) - def load_rational(self, data, legacy_api=True): + def load_rational( + self, data: bytes, legacy_api: bool = True + ) -> tuple[tuple[int, int] | IFDRational, ...]: vals = self._unpack(f"{len(data) // 4}L", data) - def combine(a, b): + def combine(a: int, b: int) -> tuple[int, int] | IFDRational: 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): + def write_rational(self, *values: IFDRational) -> bytes: 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): + def load_undefined(self, data: bytes, legacy_api: bool = True) -> bytes: return data @_register_writer(7) - def write_undefined(self, value): + def write_undefined(self, value: bytes | int | IFDRational) -> bytes: if isinstance(value, IFDRational): value = int(value) if isinstance(value, int): @@ -808,22 +855,24 @@ def write_undefined(self, value): return value @_register_loader(10, 8) - def load_signed_rational(self, data, legacy_api=True): + def load_signed_rational( + self, data: bytes, legacy_api: bool = True + ) -> tuple[tuple[int, int] | IFDRational, ...]: vals = self._unpack(f"{len(data) // 4}l", data) - def combine(a, b): + def combine(a: int, b: int) -> tuple[int, int] | IFDRational: 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(10) - def write_signed_rational(self, *values): + def write_signed_rational(self, *values: IFDRational) -> bytes: 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): + def _ensure_read(self, fp: IO[bytes], size: int) -> bytes: ret = fp.read(size) if len(ret) != size: msg = ( @@ -833,7 +882,7 @@ def _ensure_read(self, fp, size): raise OSError(msg) return ret - def load(self, fp): + def load(self, fp: IO[bytes]) -> None: self.reset() self._offset = fp.tell() @@ -886,9 +935,9 @@ def load(self, fp): self._tagdata[tag] = data self.tagtype[tag] = typ - msg += " - value: " + ( - "" % size if size > 32 else repr(data) - ) + msg += " - value: " + msg += f"" if size > 32 else repr(data) + logger.debug(msg) (self.next,) = ( @@ -900,12 +949,25 @@ def load(self, fp): warnings.warn(str(msg)) return - def tobytes(self, offset=0): + def _get_ifh(self) -> bytes: + ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42) + if self._bigtiff: + ifh += self._pack("HH", 8, 0) + ifh += self._pack("Q", 16) if self._bigtiff else self._pack("L", 8) + + return ifh + + def tobytes(self, offset: int = 0) -> bytes: # FIXME What about tagdata? - result = self._pack("H", len(self._tags_v2)) + result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2)) - entries = [] - offset = offset + len(result) + len(self._tags_v2) * 12 + 4 + entries: list[tuple[int, int, int, bytes, bytes]] = [] + + fmt = "Q" if self._bigtiff else "L" + fmt_size = 8 if self._bigtiff else 4 + offset += ( + len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + fmt_size + ) stripoffsets = None # pass 1: convert tags to binary format @@ -913,15 +975,11 @@ def tobytes(self, offset=0): for tag, value in sorted(self._tags_v2.items()): if tag == STRIPOFFSETS: stripoffsets = len(entries) - typ = self.tagtype.get(tag) + typ = self.tagtype[tag] logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, 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: - ifh = b"MM\x00\x2A\x00\x00\x00\x08" - ifd = ImageFileDirectory_v2(ifh, group=tag) + ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag) values = self._tags_v2[tag] for ifd_tag, ifd_value in values.items(): ifd[ifd_tag] = ifd_value @@ -932,10 +990,8 @@ def tobytes(self, offset=0): 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) - ) + msg = f"save: {tagname} ({tag}) - type: {typname} ({typ}) - value: " + msg += f"" if len(data) >= 16 else str(values) logger.debug(msg) # count is sum of lengths for string and arbitrary data @@ -946,28 +1002,32 @@ def tobytes(self, offset=0): 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"")) + if len(data) <= fmt_size: + entries.append((tag, typ, count, data.ljust(fmt_size, b"\0"), b"")) else: - entries.append((tag, typ, count, self._pack("L", offset), data)) + entries.append((tag, typ, count, self._pack(fmt, 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: - msg = "multistrip support not yet implemented" - raise NotImplementedError(msg) - value = self._pack("L", self._unpack("L", value)[0] + offset) + size, handler = self._load_dispatch[typ] + values = [val + offset for val in handler(self, data, self.legacy_api)] + data = self._write_dispatch[typ](self, *values) + else: + value = self._pack(fmt, self._unpack(fmt, value)[0] + offset) entries[stripoffsets] = tag, typ, count, value, data # pass 2: write entries to file for tag, typ, count, value, data in entries: logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data)) - result += self._pack("HHL4s", tag, typ, count, value) + result += self._pack( + "HHQ8s" if self._bigtiff else "HHL4s", tag, typ, count, value + ) # -- overwrite here for multi-page -- - result += b"\0\0\0\0" # end of entries + result += self._pack(fmt, 0) # end of entries # pass 3: write auxiliary data to file for tag, typ, count, value, data in entries: @@ -977,10 +1037,9 @@ def tobytes(self, offset=0): return result - def save(self, fp): + def save(self, fp: IO[bytes]) -> int: 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)) + fp.write(self._get_ifh()) offset = fp.tell() result = self.tobytes(offset) @@ -1017,7 +1076,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): .. deprecated:: 3.0.0 """ - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._legacy_api = True @@ -1029,7 +1088,7 @@ def __init__(self, *args, **kwargs): """Dictionary of tag types""" @classmethod - def from_v2(cls, original): + def from_v2(cls, original: ImageFileDirectory_v2) -> ImageFileDirectory_v1: """Returns an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` instance with the same data as is contained in the original @@ -1063,20 +1122,20 @@ def to_v2(self) -> ImageFileDirectory_v2: ifd._tags_v2 = dict(self._tags_v2) return ifd - def __contains__(self, tag): + def __contains__(self, tag: object) -> bool: return tag in self._tags_v1 or tag in self._tagdata def __len__(self) -> int: return len(set(self._tagdata) | set(self._tags_v1)) - def __iter__(self): + def __iter__(self) -> Iterator[int]: return iter(set(self._tagdata) | set(self._tags_v1)) - def __setitem__(self, tag, value): + def __setitem__(self, tag: int, value: Any) -> None: for legacy_api in (False, True): self._setitem(tag, value, legacy_api) - def __getitem__(self, tag): + def __getitem__(self, tag: int) -> Any: if tag not in self._tags_v1: # unpack on the fly data = self._tagdata[tag] typ = self.tagtype[tag] @@ -1089,7 +1148,7 @@ def __getitem__(self, tag): return val -# undone -- switch this pointer when IFD_LEGACY_API == False +# undone -- switch this pointer ImageFileDirectory = ImageFileDirectory_v1 @@ -1102,11 +1161,15 @@ class TiffImageFile(ImageFile.ImageFile): format_description = "Adobe TIFF" _close_exclusive_fp_after_loading = False - def __init__(self, fp=None, filename=None): - self.tag_v2 = None + def __init__( + self, + fp: StrOrBytesPath | IO[bytes], + filename: str | bytes | None = None, + ) -> None: + self.tag_v2: ImageFileDirectory_v2 """ Image file directory (tag dictionary) """ - self.tag = None + self.tag: ImageFileDirectory_v1 """ Legacy tag entries """ super().__init__(fp, filename) @@ -1115,15 +1178,13 @@ def _open(self) -> None: """Open the first image in a TIFF file""" # Header + assert self.fp is not None ifh = self.fp.read(8) if ifh[2] == 43: ifh += self.fp.read(8) self.tag_v2 = ImageFileDirectory_v2(ifh) - # 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 @@ -1139,13 +1200,15 @@ def _open(self) -> None: self._seek(0) @property - def n_frames(self): - if self._n_frames is None: + def n_frames(self) -> int: + current_n_frames = self._n_frames + if current_n_frames is None: current = self.tell() self._seek(len(self._frame_pos)) while self._n_frames is None: self._seek(self.tell() + 1) self.seek(current) + assert self._n_frames is not None return self._n_frames def seek(self, frame: int) -> None: @@ -1153,19 +1216,18 @@ def seek(self, frame: int) -> None: if not self._seek_check(frame): return self._seek(frame) - # Create a new core image object on second and - # subsequent frames in the image. Image may be - # different size/mode. - Image._decompression_bomb_check(self.size) - self.im = Image.core.new(self.mode, self.size) + if self._im is not None and ( + self.im.size != self._tile_size + or self.im.mode != self.mode + or self.readonly + ): + self._im = None def _seek(self, frame: int) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex 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: msg = "no more images in TIFF file" @@ -1198,7 +1260,10 @@ def _seek(self, frame: int) -> None: self.fp.seek(self._frame_pos[frame]) self.tag_v2.load(self.fp) if XMP in self.tag_v2: - self.info["xmp"] = self.tag_v2[XMP] + xmp = self.tag_v2[XMP] + if isinstance(xmp, tuple) and len(xmp) == 1: + xmp = xmp[0] + self.info["xmp"] = xmp elif "xmp" in self.info: del self.info["xmp"] self._reload_exif() @@ -1211,7 +1276,7 @@ def tell(self) -> int: """Return the current frame number""" return self.__frame - def get_photoshop_blocks(self): + def get_photoshop_blocks(self) -> dict[int, dict[str, bytes]]: """ Returns a dictionary of Photoshop "Image Resource Blocks". The keys are the image resource ID. For more information, see @@ -1222,31 +1287,36 @@ def get_photoshop_blocks(self): blocks = {} val = self.tag_v2.get(ExifTags.Base.ImageResources) if val: - while val[:4] == b"8BIM": + while val.startswith(b"8BIM") and len(val) >= 12: id = i16(val[4:6]) n = math.ceil((val[6] + 1) / 2) * 2 - size = i32(val[6 + n : 10 + n]) + try: + size = i32(val[6 + n : 10 + n]) + except struct.error: + break 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): + def load(self) -> Image.core.PixelAccess | None: if self.tile and self.use_load_libtiff: return self._load_libtiff() return super().load() + def load_prepare(self) -> None: + if self._im is None: + Image._decompression_bomb_check(self._tile_size) + self.im = Image.core.new(self.mode, self._tile_size) + ImageFile.ImageFile.load_prepare(self) + def load_end(self) -> None: # allow closing if we're on the first frame, there's no next # This is the ImageFile.load path only, libtiff specific below. if not self.is_animated: self._close_exclusive_fp_after_loading = True - # reset buffered io handle in case fp - # was passed to libtiff, invalidating the buffer - self.fp.tell() - # load IFD data from fp before it is closed exif = self.getexif() for key in TiffTags.TAGS_V2_GROUPS: @@ -1258,7 +1328,7 @@ def load_end(self) -> None: if ExifTags.Base.Orientation in self.tag_v2: del self.tag_v2[ExifTags.Base.Orientation] - def _load_libtiff(self): + def _load_libtiff(self) -> Image.core.PixelAccess | None: """Overload method triggered when we detect a compressed tiff Calls out to libtiff""" @@ -1273,11 +1343,12 @@ def _load_libtiff(self): # (self._compression, (extents tuple), # 0, (rawmode, self._compression, fp)) extents = self.tile[0][1] - args = list(self.tile[0][3]) + args = self.tile[0][3] # To be nice on memory footprint, if there's a # file descriptor, use that instead of reading # into a string in python. + assert self.fp is not None try: fp = hasattr(self.fp, "fileno") and self.fp.fileno() # flush the file descriptor, prevents error on pypy 2.4+ @@ -1291,11 +1362,12 @@ def _load_libtiff(self): fp = False if fp: - args[2] = fp + assert isinstance(args, tuple) + args_list = list(args) + args_list[2] = fp + args = tuple(args_list) - decoder = Image._getdecoder( - self.mode, "libtiff", tuple(args), self.decoderconfig - ) + decoder = Image._getdecoder(self.mode, "libtiff", args, self.decoderconfig) try: decoder.setimage(self.im, extents) except ValueError as e: @@ -1320,8 +1392,17 @@ def _load_libtiff(self): logger.debug("have fileno, calling fileno version of the decoder.") if not close_self_fp: self.fp.seek(0) + # Save and restore the file position, because libtiff will move it + # outside of the Python runtime, and that will confuse + # io.BufferedReader and possible others. + # NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(), + # because the buffer read head already may not equal the actual + # file position, and fp.seek() may just adjust it's internal + # pointer and not actually seek the OS file handle. + pos = os.lseek(fp, 0, os.SEEK_CUR) # 4 bytes, otherwise the trace might error out n, err = decoder.decode(b"fpfp") + os.lseek(fp, pos, os.SEEK_SET) else: # we have something else. logger.debug("don't have fileno or getvalue. just reading") @@ -1339,11 +1420,12 @@ def _load_libtiff(self): self.fp = None # might be shared if err < 0: - raise OSError(err) + msg = f"decoder error {err}" + raise OSError(msg) return Image.Image.load(self) - def _setup(self): + def _setup(self) -> None: """Setup this image object based on current tags""" if 0xBC01 in self.tag_v2: @@ -1369,38 +1451,56 @@ def _setup(self): logger.debug("- photometric_interpretation: %s", photo) logger.debug("- planar_configuration: %s", self._planar_configuration) logger.debug("- fill_order: %s", fillorder) - logger.debug("- YCbCr subsampling: %s", self.tag.get(YCBCRSUBSAMPLING)) + logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) # size - xsize = int(self.tag_v2.get(IMAGEWIDTH)) - ysize = int(self.tag_v2.get(IMAGELENGTH)) - self._size = xsize, ysize + try: + xsize = self.tag_v2[IMAGEWIDTH] + ysize = self.tag_v2[IMAGELENGTH] + except KeyError as e: + msg = "Missing dimensions" + raise TypeError(msg) from e + if not isinstance(xsize, int) or not isinstance(ysize, int): + msg = "Invalid dimensions" + raise ValueError(msg) + self._tile_size = xsize, ysize + orientation = self.tag_v2.get(ExifTags.Base.Orientation) + if orientation in (5, 6, 7, 8): + self._size = ysize, xsize + else: + self._size = xsize, ysize logger.debug("- size: %s", self.size) sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,)) - if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1: + if len(sample_format) > 1 and max(sample_format) == min(sample_format): # 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. - sample_format = (1,) + sample_format = (sample_format[0],) bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,)) extra_tuple = self.tag_v2.get(EXTRASAMPLES, ()) + samples_per_pixel = self.tag_v2.get( + SAMPLESPERPIXEL, + 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1, + ) if photo in (2, 6, 8): # RGB, YCbCr, LAB bps_count = 3 elif photo == 5: # CMYK bps_count = 4 else: bps_count = 1 + if self._planar_configuration == 2 and extra_tuple and max(extra_tuple) == 0: + # If components are stored separately, + # then unspecified extra components at the end can be ignored + bps_tuple = bps_tuple[: -len(extra_tuple)] + samples_per_pixel -= len(extra_tuple) + extra_tuple = () bps_count += len(extra_tuple) 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 @@ -1487,17 +1587,6 @@ def _setup(self): # 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 ( @@ -1506,23 +1595,39 @@ def _setup(self): and self._planar_configuration == 1 ): rawmode = "RGB" + # 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. + elif rawmode == "I;16": + rawmode = "I;16N" + elif rawmode.endswith((";16B", ";16L")): + rawmode = rawmode[:-1] + "N" # 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)) + self.tile.append(ImageFile._Tile("libtiff", (0, 0, xsize, ysize), 0, a)) elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2: # striped image if STRIPOFFSETS in self.tag_v2: offsets = self.tag_v2[STRIPOFFSETS] h = self.tag_v2.get(ROWSPERSTRIP, ysize) - w = self.size[0] + w = xsize else: # tiled image offsets = self.tag_v2[TILEOFFSETS] - w = self.tag_v2.get(TILEWIDTH) + tilewidth = self.tag_v2.get(TILEWIDTH) h = self.tag_v2.get(TILELENGTH) + if not isinstance(tilewidth, int) or not isinstance(h, int): + msg = "Invalid tile dimensions" + raise ValueError(msg) + w = tilewidth + + if w == xsize and h == ysize and self._planar_configuration != 2: + # Every tile covers the image. Only use the last offset + offsets = offsets[-1:] for offset in offsets: if x + w > xsize: @@ -1537,20 +1642,20 @@ def _setup(self): # adjust stride width accordingly stride /= bps_count - a = (tile_rawmode, int(stride), 1) + args = (tile_rawmode, int(stride), 1) self.tile.append( - ( + ImageFile._Tile( self._compression, (x, y, min(x + w, xsize), min(y + h, ysize)), offset, - a, + args, ) ) - x = x + w - if x >= self.size[0]: + x += w + if x >= xsize: x, y = 0, y + h - if y >= self.size[1]: - x = y = 0 + if y >= ysize: + y = 0 layer += 1 else: logger.debug("- unsupported data organization") @@ -1585,7 +1690,7 @@ def _setup(self): "PA": ("PA", II, 3, 1, (8, 8), 2), "I": ("I;32S", II, 1, 2, (32,), None), "I;16": ("I;16", II, 1, 1, (16,), None), - "I;16S": ("I;16S", II, 1, 2, (16,), None), + "I;16L": ("I;16L", II, 1, 1, (16,), None), "F": ("F;32F", II, 1, 3, (32,), None), "RGB": ("RGB", II, 2, 1, (8, 8, 8), None), "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0), @@ -1593,24 +1698,24 @@ 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), - "F;32BF": ("F;32BF", MM, 1, 3, (32,), None), } -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] except KeyError as e: msg = f"cannot write mode {im.mode} as TIFF" raise OSError(msg) from e - ifd = ImageFileDirectory_v2(prefix=prefix) - encoderinfo = im.encoderinfo encoderconfig = im.encoderconfig + + ifd = ImageFileDirectory_v2(prefix=prefix) + if encoderinfo.get("big_tiff"): + ifd._bigtiff = True + try: compression = encoderinfo["compression"] except KeyError: @@ -1663,6 +1768,12 @@ def _save(im, fp, filename): legacy_ifd = im.tag.to_v2() supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})} + if supplied_tags.get(PLANAR_CONFIGURATION) == 2 and EXTRASAMPLES in supplied_tags: + # If the image used separate component planes, + # then EXTRASAMPLES should be ignored when saving contiguously + if SAMPLESPERPIXEL in supplied_tags: + supplied_tags[SAMPLESPERPIXEL] -= len(supplied_tags[EXTRASAMPLES]) + del supplied_tags[EXTRASAMPLES] for tag in ( # IFD offset that may not be correct in the saved image EXIFIFD, @@ -1736,10 +1847,11 @@ def _save(im, fp, filename): 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 + if px is not None: + 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) @@ -1781,11 +1893,11 @@ def _save(im, fp, filename): ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) if im.mode == "YCbCr": - for tag, value in { + for tag, default_value in { YCBCRSUBSAMPLING: (1, 1), REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255), }.items(): - ifd.setdefault(tag, value) + ifd.setdefault(tag, default_value) blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS] if libtiff: @@ -1805,7 +1917,7 @@ def _save(im, fp, filename): if hasattr(fp, "fileno"): try: fp.seek(0) - _fp = os.dup(fp.fileno()) + _fp = fp.fileno() except io.UnsupportedOperation: pass @@ -1828,7 +1940,7 @@ def _save(im, fp, filename): ] # bits per sample is a single short in the tiff directory, not a list. - atts = {BITSPERSAMPLE: bits[0]} + atts: dict[int, Any] = {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. @@ -1838,14 +1950,14 @@ def _save(im, fp, filename): # 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: - if not getattr(Image.core, "libtiff_support_custom_tags", False): - continue - - if tag in ifd.tagtype: + if tag in TiffTags.TAGS_V2_GROUPS: + types[tag] = TiffTags.LONG8 + elif tag in ifd.tagtype: types[tag] = ifd.tagtype[tag] - elif not (isinstance(value, (int, float, str, bytes))): - continue - else: + elif isinstance(value, (int, float, str, bytes)) or ( + isinstance(value, tuple) + and all(isinstance(v, (int, float, IFDRational)) for v in value) + ): type = TiffTags.lookup(tag).type if type: types[tag] = type @@ -1866,7 +1978,7 @@ def _save(im, fp, filename): # 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"): + if im.mode in ("I;16", "I;16B", "I;16L"): rawmode = "I;16N" # Pass tags as sorted list so that the tags are set in a fixed order. @@ -1878,17 +1990,11 @@ def _save(im, fp, filename): encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig) encoder.setimage(im.im, (0, 0) + im.size) while True: - # undone, change to self.decodermaxblock: - errcode, data = encoder.encode(16 * 1024)[1:] + errcode, data = encoder.encode(ImageFile.MAXBLOCK)[1:] if not _fp: fp.write(data) if errcode: break - if _fp: - try: - os.close(_fp) - except OSError: - pass if errcode < 0: msg = f"encoder error {errcode} when writing image file" raise OSError(msg) @@ -1899,16 +2005,18 @@ def _save(im, fp, filename): offset = ifd.save(fp) ImageFile._save( - im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))] + im, + fp, + [ImageFile._Tile("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))], ) # -- helper for multi-page save -- if "_debug_multipage" in encoderinfo: # just to access o32 and o16 (using correct byte order) - im._debug_multipage = ifd + setattr(im, "_debug_multipage", ifd) -class AppendingTiffWriter: +class AppendingTiffWriter(io.BytesIO): fieldSizes = [ 0, # None 1, # byte @@ -1938,17 +2046,18 @@ class AppendingTiffWriter: 521, # JPEGACTables } - def __init__(self, fn, new=False): - if hasattr(fn, "read"): - self.f = fn - self.close_fp = False - else: + def __init__(self, fn: StrOrBytesPath | IO[bytes], new: bool = False) -> None: + self.f: IO[bytes] + if is_path(fn): self.name = fn self.close_fp = True try: self.f = open(fn, "w+b" if new else "r+b") except OSError: self.f = open(fn, "w+b") + else: + self.f = cast(IO[bytes], fn) + self.close_fp = False self.beginning = self.f.tell() self.setup() @@ -1956,24 +2065,25 @@ def setup(self) -> None: # Reset everything. self.f.seek(self.beginning, os.SEEK_SET) - self.whereToWriteNewIFDOffset = None + self.whereToWriteNewIFDOffset: int | None = None self.offsetOfNewPage = 0 self.IIMM = iimm = self.f.read(4) + self._bigtiff = b"\x2b" in iimm if not iimm: # empty file - first page self.isFirst = True return self.isFirst = False - if iimm == b"II\x2a\x00": - self.setEndian("<") - elif iimm == b"MM\x00\x2a": - self.setEndian(">") - else: + if iimm not in PREFIXES: msg = "Invalid TIFF file header" raise RuntimeError(msg) + self.setEndian("<" if iimm.startswith(II) else ">") + + if self._bigtiff: + self.f.seek(4, os.SEEK_CUR) self.skipIFDs() self.goToEnd() @@ -1993,10 +2103,13 @@ def finalize(self) -> None: msg = "IIMM of new page doesn't match IIMM of first page" raise RuntimeError(msg) - ifd_offset = self.readLong() + if self._bigtiff: + self.f.seek(4, os.SEEK_CUR) + ifd_offset = self._read(8 if self._bigtiff else 4) ifd_offset += self.offsetOfNewPage + assert self.whereToWriteNewIFDOffset is not None self.f.seek(self.whereToWriteNewIFDOffset) - self.writeLong(ifd_offset) + self._write(ifd_offset, 8 if self._bigtiff else 4) self.f.seek(ifd_offset) self.fixIFD() @@ -2015,7 +2128,13 @@ def __exit__(self, *args: object) -> None: def tell(self) -> int: return self.f.tell() - self.offsetOfNewPage - def seek(self, offset, whence=io.SEEK_SET): + def seek(self, offset: int, whence: int = io.SEEK_SET) -> int: + """ + :param offset: Distance to seek. + :param whence: Whether the distance is relative to the start, + end or current position. + :returns: The resulting position, relative to the start. + """ if whence == os.SEEK_SET: offset += self.offsetOfNewPage @@ -2036,134 +2155,169 @@ def setEndian(self, endian: str) -> None: self.endian = endian self.longFmt = f"{self.endian}L" self.shortFmt = f"{self.endian}H" - self.tagFormat = f"{self.endian}HHL" + self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L") def skipIFDs(self) -> None: while True: - ifd_offset = self.readLong() + ifd_offset = self._read(8 if self._bigtiff else 4) if ifd_offset == 0: - self.whereToWriteNewIFDOffset = self.f.tell() - 4 + self.whereToWriteNewIFDOffset = self.f.tell() - ( + 8 if self._bigtiff else 4 + ) break self.f.seek(ifd_offset) - num_tags = self.readShort() - self.f.seek(num_tags * 12, os.SEEK_CUR) + num_tags = self._read(8 if self._bigtiff else 2) + self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR) - def write(self, data: bytes) -> int | None: + def write(self, data: Buffer, /) -> int: return self.f.write(data) - def readShort(self) -> int: - (value,) = struct.unpack(self.shortFmt, self.f.read(2)) + def _fmt(self, field_size: int) -> str: + try: + return {2: "H", 4: "L", 8: "Q"}[field_size] + except KeyError: + msg = "offset is not supported" + raise RuntimeError(msg) + + def _read(self, field_size: int) -> int: + (value,) = struct.unpack( + self.endian + self._fmt(field_size), self.f.read(field_size) + ) return value + def readShort(self) -> int: + return self._read(2) + def readLong(self) -> int: - (value,) = struct.unpack(self.longFmt, self.f.read(4)) - return value + return self._read(4) - def rewriteLastShortToLong(self, value: int) -> None: - self.f.seek(-2, os.SEEK_CUR) - bytes_written = self.f.write(struct.pack(self.longFmt, value)) - if bytes_written is not None and bytes_written != 4: - msg = f"wrote only {bytes_written} bytes but wanted 4" + @staticmethod + def _verify_bytes_written(bytes_written: int | None, expected: int) -> None: + if bytes_written is not None and bytes_written != expected: + msg = f"wrote only {bytes_written} bytes but wanted {expected}" raise RuntimeError(msg) + def _rewriteLast( + self, value: int, field_size: int, new_field_size: int = 0 + ) -> None: + self.f.seek(-field_size, os.SEEK_CUR) + if not new_field_size: + new_field_size = field_size + bytes_written = self.f.write( + struct.pack(self.endian + self._fmt(new_field_size), value) + ) + self._verify_bytes_written(bytes_written, new_field_size) + + def rewriteLastShortToLong(self, value: int) -> None: + self._rewriteLast(value, 2, 4) + def rewriteLastShort(self, value: int) -> None: - self.f.seek(-2, os.SEEK_CUR) - bytes_written = self.f.write(struct.pack(self.shortFmt, value)) - if bytes_written is not None and bytes_written != 2: - msg = f"wrote only {bytes_written} bytes but wanted 2" - raise RuntimeError(msg) + return self._rewriteLast(value, 2) def rewriteLastLong(self, value: int) -> None: - self.f.seek(-4, os.SEEK_CUR) - bytes_written = self.f.write(struct.pack(self.longFmt, value)) - if bytes_written is not None and bytes_written != 4: - msg = f"wrote only {bytes_written} bytes but wanted 4" - raise RuntimeError(msg) + return self._rewriteLast(value, 4) + + def _write(self, value: int, field_size: int) -> None: + bytes_written = self.f.write( + struct.pack(self.endian + self._fmt(field_size), value) + ) + self._verify_bytes_written(bytes_written, field_size) def writeShort(self, value: int) -> None: - bytes_written = self.f.write(struct.pack(self.shortFmt, value)) - if bytes_written is not None and bytes_written != 2: - msg = f"wrote only {bytes_written} bytes but wanted 2" - raise RuntimeError(msg) + self._write(value, 2) def writeLong(self, value: int) -> None: - bytes_written = self.f.write(struct.pack(self.longFmt, value)) - if bytes_written is not None and bytes_written != 4: - msg = f"wrote only {bytes_written} bytes but wanted 4" - raise RuntimeError(msg) + self._write(value, 4) def close(self) -> None: self.finalize() - self.f.close() + if self.close_fp: + self.f.close() def fixIFD(self) -> None: - num_tags = self.readShort() + num_tags = self._read(8 if self._bigtiff else 2) for i in range(num_tags): - tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8)) + tag, field_type, count = struct.unpack( + self.tagFormat, self.f.read(12 if self._bigtiff else 8) + ) field_size = self.fieldSizes[field_type] total_size = field_size * count - is_local = total_size <= 4 - offset: int | None + fmt_size = 8 if self._bigtiff else 4 + is_local = total_size <= fmt_size if not is_local: - offset = self.readLong() + self.offsetOfNewPage - self.rewriteLastLong(offset) + offset = self._read(fmt_size) + self.offsetOfNewPage + self._rewriteLast(offset, fmt_size) if tag in self.Tags: cur_pos = self.f.tell() + logger.debug( + "fixIFD: %s (%d) - type: %s (%d) - type size: %d - count: %d", + TiffTags.lookup(tag).name, + tag, + TYPES.get(field_type, "unknown"), + field_type, + field_size, + count, + ) + if is_local: - self.fixOffsets( - count, isShort=(field_size == 2), isLong=(field_size == 4) - ) - self.f.seek(cur_pos + 4) + self._fixOffsets(count, field_size) + self.f.seek(cur_pos + fmt_size) else: self.f.seek(offset) - self.fixOffsets( - count, isShort=(field_size == 2), isLong=(field_size == 4) - ) + self._fixOffsets(count, field_size) self.f.seek(cur_pos) - offset = cur_pos = None - elif is_local: # skip the locally stored value that is not an offset - self.f.seek(4, os.SEEK_CUR) - - def fixOffsets( - self, count: int, isShort: bool = False, isLong: bool = False - ) -> None: - if not isShort and not isLong: - msg = "offset is neither short nor long" - raise RuntimeError(msg) + self.f.seek(fmt_size, os.SEEK_CUR) + def _fixOffsets(self, count: int, field_size: int) -> None: for i in range(count): - offset = self.readShort() if isShort else self.readLong() + offset = self._read(field_size) offset += self.offsetOfNewPage - if isShort and offset >= 65536: - # offset is now too large - we must convert shorts to longs + + new_field_size = 0 + if self._bigtiff and field_size in (2, 4) and offset >= 2**32: + # offset is now too large - we must convert long to long8 + new_field_size = 8 + elif field_size == 2 and offset >= 2**16: + # offset is now too large - we must convert short to long + new_field_size = 4 + if new_field_size: if count != 1: msg = "not implemented" raise RuntimeError(msg) # XXX TODO # simple case - the offset is just one and therefore it is # local (not referenced with another offset) - self.rewriteLastShortToLong(offset) - self.f.seek(-10, os.SEEK_CUR) - self.writeShort(TiffTags.LONG) # rewrite the type to LONG - self.f.seek(8, os.SEEK_CUR) - elif isShort: - self.rewriteLastShort(offset) + self._rewriteLast(offset, field_size, new_field_size) + # Move back past the new offset, past 'count', and before 'field_type' + rewind = -new_field_size - 4 - 2 + self.f.seek(rewind, os.SEEK_CUR) + self.writeShort(new_field_size) # rewrite the type + self.f.seek(2 - rewind, os.SEEK_CUR) else: - self.rewriteLastLong(offset) + self._rewriteLast(offset, field_size) + + def fixOffsets( + self, count: int, isShort: bool = False, isLong: bool = False + ) -> None: + if isShort: + field_size = 2 + elif isLong: + field_size = 4 + else: + field_size = 0 + return self._fixOffsets(count, field_size) def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - encoderinfo = im.encoderinfo.copy() - encoderconfig = im.encoderconfig - append_images = list(encoderinfo.get("append_images", [])) + append_images = list(im.encoderinfo.get("append_images", [])) if not hasattr(im, "n_frames") and not append_images: return _save(im, fp, filename) @@ -2171,18 +2325,17 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: try: with AppendingTiffWriter(fp) as tf: for ims in [im] + append_images: - ims.encoderinfo = encoderinfo - ims.encoderconfig = encoderconfig - if not hasattr(ims, "n_frames"): - nfr = 1 - else: - nfr = ims.n_frames + encoderinfo = ims._attach_default_encoderinfo(im) + if not hasattr(ims, "encoderconfig"): + ims.encoderconfig = () + nfr = getattr(ims, "n_frames", 1) for idx in range(nfr): ims.seek(idx) ims.load() _save(ims, tf, filename) tf.newFrame() + ims.encoderinfo = encoderinfo finally: im.seek(cur_idx) diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index e318c87398c..613a3b7def7 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -32,17 +32,24 @@ class _TagInfo(NamedTuple): class TagInfo(_TagInfo): __slots__: list[str] = [] - def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None): + def __new__( + cls, + value: int | None = None, + name: str = "unknown", + type: int | None = None, + length: int | None = None, + enum: dict[str, int] | None = None, + ) -> TagInfo: return super().__new__(cls, value, name, type, length, enum or {}) - def cvt_enum(self, value): + def cvt_enum(self, value: str) -> int | str: # 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, group=None): +def lookup(tag: int, group: int | None = None) -> TagInfo: """ :param tag: Integer tag number :param group: Which :py:data:`~PIL.TiffTags.TAGS_V2_GROUPS` to look in @@ -89,7 +96,7 @@ def lookup(tag, group=None): IFD = 13 LONG8 = 16 -_tags_v2 = { +_tags_v2: dict[int, tuple[str, int, int] | tuple[str, int, int, dict[str, int]]] = { 254: ("NewSubfileType", LONG, 1), 255: ("SubfileType", SHORT, 1), 256: ("ImageWidth", LONG, 1), @@ -196,6 +203,11 @@ def lookup(tag, group=None): 531: ("YCbCrPositioning", SHORT, 1), 532: ("ReferenceBlackWhite", RATIONAL, 6), 700: ("XMP", BYTE, 0), + # Four private SGI tags + 32995: ("Matteing", SHORT, 1), + 32996: ("DataType", SHORT, 0), + 32997: ("ImageDepth", LONG, 1), + 32998: ("TileDepth", LONG, 1), 33432: ("Copyright", ASCII, 1), 33723: ("IptcNaaInfo", UNDEFINED, 1), 34377: ("PhotoshopInfo", BYTE, 0), @@ -233,7 +245,7 @@ def lookup(tag, group=None): 50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one 50839: ("ImageJMetaData", UNDEFINED, 1), # see Issue #2006 } -TAGS_V2_GROUPS = { +_tags_v2_groups = { # ExifIFD 34665: { 36864: ("ExifVersion", UNDEFINED, 1), @@ -281,7 +293,7 @@ def lookup(tag, group=None): # Legacy Tags structure # these tags aren't included above, but were in the previous versions -TAGS = { +TAGS: dict[int | tuple[int, int], str] = { 347: "JPEGTables", 700: "XMP", # Additional Exif Info @@ -426,9 +438,10 @@ def lookup(tag, group=None): } TAGS_V2: dict[int, TagInfo] = {} +TAGS_V2_GROUPS: dict[int, dict[int, TagInfo]] = {} -def _populate(): +def _populate() -> None: for k, v in _tags_v2.items(): # Populate legacy structure. TAGS[k] = v[0] @@ -438,9 +451,8 @@ def _populate(): TAGS_V2[k] = TagInfo(k, *v) - for tags in TAGS_V2_GROUPS.values(): - for k, v in tags.items(): - tags[k] = TagInfo(k, *v) + for group, tags in _tags_v2_groups.items(): + TAGS_V2_GROUPS[group] = {k: TagInfo(k, *v) for k, v in tags.items()} _populate() @@ -546,7 +558,6 @@ def _populate(): LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes LIBTIFF_CORE.remove(322) # We don't have support for writing tiled images with libtiff LIBTIFF_CORE.remove(323) # Tiled images -LIBTIFF_CORE.remove(333) # Ink Names either # Note to advanced users: There may be combinations of these # parameters and values that when added properly, will work and diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index fbd7be6ed5c..07bbf747155 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -22,10 +22,14 @@ 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 __future__ import annotations +from typing import IO + from . import Image, ImageFile from ._binary import i32le as i32 +from ._typing import StrOrBytesPath class WalImageFile(ImageFile.ImageFile): @@ -36,6 +40,7 @@ def _open(self) -> None: self._mode = "P" # read header fields + assert self.fp is not None header = self.fp.read(32 + 24 + 32 + 12) self._size = i32(header, 32), i32(header, 36) Image._decompression_bomb_check(self.size) @@ -46,19 +51,19 @@ def _open(self) -> None: # strings are null-terminated self.info["name"] = header[:32].split(b"\0", 1)[0] - next_name = header[56 : 56 + 32].split(b"\0", 1)[0] - if next_name: + if next_name := header[56 : 56 + 32].split(b"\0", 1)[0]: self.info["next_name"] = next_name - def load(self): - if not self.im: + def load(self) -> Image.core.PixelAccess | None: + if self._im is None: + assert self.fp is not None 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): +def open(filename: StrOrBytesPath | IO[bytes]) -> WalImageFile: """ Load texture from a Quake2 WAL texture file. diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 59be5bf9d31..e20e40d913f 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -1,7 +1,6 @@ from __future__ import annotations from io import BytesIO -from typing import IO, Any from . import Image, ImageFile @@ -12,10 +11,9 @@ except ImportError: SUPPORTED = False - -_VALID_WEBP_MODES = {"RGBX": True, "RGBA": True, "RGB": True} - -_VALID_WEBP_LEGACY_MODES = {"RGB": True, "RGBA": True} +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import IO, Any _VP8_MODES_BY_IDENTIFIER = { b"VP8 ": "RGB", @@ -25,7 +23,7 @@ def _accept(prefix: bytes) -> bool | str: - is_riff_file_format = prefix[:4] == b"RIFF" + is_riff_file_format = prefix.startswith(b"RIFF") is_webp_file = prefix[8:12] == b"WEBP" is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER @@ -45,58 +43,37 @@ class WebPImageFile(ImageFile.ImageFile): __logical_frame = 0 def _open(self) -> None: - if not _webp.HAVE_WEBPANIM: - # Legacy mode - 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.fp = BytesIO(data) - self.tile = [("raw", (0, 0) + self.size, 0, self.mode)] - self.n_frames = 1 - self.is_animated = False - return - # Use the newer AnimDecoder API to parse the (possibly) animated file, # and access muxed chunks like ICC/EXIF/XMP. + assert self.fp is not None 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 - self.info["loop"] = loop_count - bg_a, bg_r, bg_g, bg_b = ( - (bgcolor >> 24) & 0xFF, - (bgcolor >> 16) & 0xFF, - (bgcolor >> 8) & 0xFF, - bgcolor & 0xFF, + self._size, self.info["loop"], bgcolor, self.n_frames, self.rawmode = ( + self._decoder.get_info() + ) + self.info["background"] = ( + (bgcolor >> 16) & 0xFF, # R + (bgcolor >> 8) & 0xFF, # G + bgcolor & 0xFF, # B + (bgcolor >> 24) & 0xFF, # A ) - self.info["background"] = (bg_r, bg_g, bg_b, bg_a) - 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 = [] + self._mode = "RGB" if self.rawmode == "RGBX" else self.rawmode # Attempt to read ICC / EXIF / XMP chunks from file - icc_profile = self._decoder.get_chunk("ICCP") - exif = self._decoder.get_chunk("EXIF") - xmp = self._decoder.get_chunk("XMP ") - if icc_profile: - self.info["icc_profile"] = icc_profile - if exif: - self.info["exif"] = exif - if xmp: - self.info["xmp"] = xmp + for key, chunk_name in { + "icc_profile": "ICCP", + "exif": "EXIF", + "xmp": "XMP ", + }.items(): + if value := self._decoder.get_chunk(chunk_name): + self.info[key] = value # Initialize seek state self._reset(reset=False) - def _getexif(self) -> dict[str, Any] | None: + def _getexif(self) -> dict[int, Any] | None: if "exif" not in self.info: return None return self.getexif()._get_merged_dict() @@ -115,7 +92,7 @@ def _reset(self, reset: bool = True) -> None: self.__loaded = -1 self.__timestamp = 0 - def _get_next(self): + def _get_next(self) -> tuple[bytes, int, int]: # Get next frame ret = self._decoder.get_next() self.__physical_frame += 1 @@ -144,22 +121,19 @@ def _seek(self, frame: int) -> None: while self.__physical_frame < frame: self._get_next() # Advance to the requested frame - def load(self): - if _webp.HAVE_WEBPANIM: - if self.__loaded != self.__logical_frame: - self._seek(self.__logical_frame) + def load(self) -> Image.core.PixelAccess | None: + if self.__loaded != self.__logical_frame: + self._seek(self.__logical_frame) - # We need to load the image data for this frame - data, timestamp, duration = self._get_next() - self.info["timestamp"] = timestamp - self.info["duration"] = duration - self.__loaded = self.__logical_frame + # We need to load the image data for this frame + data, self.info["timestamp"], self.info["duration"] = self._get_next() + 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.rawmode)] + # Set tile + if self.fp and self._exclusive_fp: + self.fp.close() + self.fp = BytesIO(data) + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.rawmode)] return super().load() @@ -167,12 +141,16 @@ def load_seek(self, pos: int) -> None: pass def tell(self) -> int: - if not _webp.HAVE_WEBPANIM: - return super().tell() - return self.__logical_frame +def _convert_frame(im: Image.Image) -> Image.Image: + # Make sure image mode is supported + if im.mode not in ("RGBX", "RGBA", "RGB"): + im = im.convert("RGBA" if im.has_transparency_data else "RGB") + return im + + def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: encoderinfo = im.encoderinfo.copy() append_images = list(encoderinfo.get("append_images", [])) @@ -241,8 +219,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Setup the WebP animation encoder enc = _webp.WebPAnimEncoder( - im.size[0], - im.size[1], + im.size, background, loop, minimize_size, @@ -258,36 +235,18 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: cur_idx = im.tell() try: for ims in [im] + append_images: - # Get # of frames in this image + # Get number of frames in this image nfr = getattr(ims, "n_frames", 1) for idx in range(nfr): ims.seek(idx) - ims.load() - - # Make sure image mode is supported - frame = ims - rawmode = ims.mode - if ims.mode not in _VALID_WEBP_MODES: - 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" + + frame = _convert_frame(ims) # Append the frame to the animation encoder enc.add( - frame.tobytes("raw", rawmode), + frame.getim(), round(timestamp), - frame.size[0], - frame.size[1], - rawmode, lossless, quality, alpha_quality, @@ -305,7 +264,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: im.seek(cur_idx) # Force encoder to flush frames - enc.add(None, round(timestamp), 0, 0, "", lossless, quality, alpha_quality, 0) + enc.add(None, round(timestamp), lossless, quality, alpha_quality, 0) # Get the final output from the encoder data = enc.assemble(icc_profile, exif, xmp) @@ -330,17 +289,13 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: method = im.encoderinfo.get("method", 4) exact = 1 if im.encoderinfo.get("exact") else 0 - if im.mode not in _VALID_WEBP_LEGACY_MODES: - im = im.convert("RGBA" if im.has_transparency_data else "RGB") + im = _convert_frame(im) data = _webp.WebPEncode( - im.tobytes(), - im.size[0], - im.size[1], + im.getim(), lossless, float(quality), float(alpha_quality), - im.mode, icc_profile, method, exact, @@ -357,7 +312,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: Image.register_open(WebPImageFile.format, WebPImageFile, _accept) if SUPPORTED: Image.register_save(WebPImageFile.format, _save) - if _webp.HAVE_WEBPANIM: - Image.register_save_all(WebPImageFile.format, _save_all) + 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 3d5cddcc8f5..f5e244782fc 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -45,10 +45,10 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: class WmfHandler(ImageFile.StubHandler): def open(self, im: ImageFile.StubImageFile) -> None: - im._mode = "RGB" self.bbox = im.info["wmf_bbox"] def load(self, im: ImageFile.StubImageFile) -> Image.Image: + assert im.fp is not None im.fp.seek(0) # rewind return Image.frombytes( "RGB", @@ -68,9 +68,7 @@ def load(self, im: ImageFile.StubImageFile) -> Image.Image: def _accept(prefix: bytes) -> bool: - return ( - prefix[:6] == b"\xd7\xcd\xc6\x9a\x00\x00" or prefix[:4] == b"\x01\x00\x00\x00" - ) + return prefix.startswith((b"\xd7\xcd\xc6\x9a\x00\x00", b"\x01\x00\x00\x00")) ## @@ -82,16 +80,19 @@ class WmfStubImageFile(ImageFile.StubImageFile): format_description = "Windows Metafile" def _open(self) -> None: - self._inch = None + # check placeable header + assert self.fp is not None + s = self.fp.read(44) - # check placable header - s = self.fp.read(80) - - if s[:6] == b"\xd7\xcd\xc6\x9a\x00\x00": + if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"): # placeable windows metafile # get units per inch - self._inch = word(s, 14) + inch = word(s, 14) + if inch == 0: + msg = "Invalid inch" + raise ValueError(msg) + self._inch: tuple[float, float] = inch, inch # get bounding box x0 = short(s, 6) @@ -102,8 +103,8 @@ def _open(self) -> None: # normalize size to 72 dots per inch self.info["dpi"] = 72 size = ( - (x1 - x0) * self.info["dpi"] // self._inch, - (y1 - y0) * self.info["dpi"] // self._inch, + (x1 - x0) * self.info["dpi"] // inch, + (y1 - y0) * self.info["dpi"] // inch, ) self.info["wmf_bbox"] = x0, y0, x1, y1 @@ -113,7 +114,7 @@ def _open(self) -> None: msg = "Unsupported WMF file format" raise SyntaxError(msg) - elif s[:4] == b"\x01\x00\x00\x00" and s[40:44] == b" EMF": + elif s.startswith(b"\x01\x00\x00\x00") and s[40:44] == b" EMF": # enhanced metafile # get bounding box @@ -128,7 +129,7 @@ def _open(self) -> None: size = x1 - x0, y1 - y0 # calculate dots per inch from bbox and frame - xdpi = 2540.0 * (x1 - y0) / (frame[2] - frame[0]) + xdpi = 2540.0 * (x1 - x0) / (frame[2] - frame[0]) ydpi = 2540.0 * (y1 - y0) / (frame[3] - frame[1]) self.info["wmf_bbox"] = x0, y0, x1, y1 @@ -137,6 +138,7 @@ def _open(self) -> None: self.info["dpi"] = xdpi else: self.info["dpi"] = xdpi, ydpi + self._inch = xdpi, ydpi else: msg = "Unsupported file format" @@ -145,20 +147,20 @@ def _open(self) -> None: self._mode = "RGB" self._size = size - loader = self._load() - if loader: - loader.open(self) - def _load(self) -> ImageFile.StubHandler | None: return _handler - def load(self, dpi=None): - if dpi is not None and self._inch is not None: + def load( + self, dpi: float | tuple[float, float] | None = None + ) -> Image.core.PixelAccess | None: + if dpi is not None: self.info["dpi"] = dpi x0, y0, x1, y1 = self.info["wmf_bbox"] + if not isinstance(dpi, tuple): + dpi = dpi, dpi self._size = ( - (x1 - x0) * self.info["dpi"] // self._inch, - (y1 - y0) * self.info["dpi"] // self._inch, + int((x1 - x0) * dpi[0] / self._inch[0]), + int((y1 - y0) * dpi[1] / self._inch[1]), ) return super().load() diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index c84adaca215..192c041d94e 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -34,7 +34,7 @@ def _accept(prefix: bytes) -> bool: - return prefix[:6] == _MAGIC + return prefix.startswith(_MAGIC) ## @@ -66,14 +66,16 @@ def _open(self) -> None: break # parse header line (already read) - s = s.strip().split() + w, h = s.strip().split(maxsplit=2)[:2] self._mode = "P" - self._size = int(s[0]), int(s[1]) + self._size = int(w), int(h) self.palette = ImagePalette.raw("RGB", PALETTE) - self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1))] + self.tile = [ + ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), self.mode) + ] # -------------------------------------------------------------------- diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 6d11bbfcf6b..1e57aa162ea 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -38,7 +38,7 @@ def _accept(prefix: bytes) -> bool: - return prefix.lstrip()[:7] == b"#define" + return prefix.lstrip().startswith(b"#define") ## @@ -67,7 +67,7 @@ def _open(self) -> None: self._mode = "1" self._size = xsize, ysize - self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] + self.tile = [ImageFile._Tile("xbm", (0, 0) + self.size, m.end())] def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: @@ -85,7 +85,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(b"static char im_bits[] = {\n") - ImageFile._save(im, fp, [("xbm", (0, 0) + im.size, 0, None)]) + ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size)]) fp.write(b"};\n") diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 8d56331e6aa..3be240fbc1a 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -25,7 +25,7 @@ def _accept(prefix: bytes) -> bool: - return prefix[:9] == b"/* XPM */" + return prefix.startswith(b"/* XPM */") ## @@ -37,43 +37,36 @@ class XpmImageFile(ImageFile.ImageFile): format_description = "X11 Pixel Map" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(9)): msg = "not an XPM file" raise SyntaxError(msg) # skip forward to next string while True: - s = self.fp.readline() - if not s: + line = self.fp.readline() + if not line: msg = "broken XPM file" raise SyntaxError(msg) - m = xpm_head.match(s) + m = xpm_head.match(line) if m: break self._size = int(m.group(1)), int(m.group(2)) - pal = int(m.group(3)) + palette_length = int(m.group(3)) bpp = int(m.group(4)) - if pal > 256 or bpp != 1: - msg = "cannot read this XPM file" - raise ValueError(msg) - # # load palette description - palette = [b"\0\0\0"] * 256 + palette = {} - for _ in range(pal): - s = self.fp.readline() - if s[-2:] == b"\r\n": - s = s[:-2] - elif s[-1:] in b"\r\n": - s = s[:-1] + for _ in range(palette_length): + line = self.fp.readline().rstrip() - c = s[1] - s = s[2:-2].split() + c = line[1 : bpp + 1] + s = line[bpp + 1 : -2].split() for i in range(0, len(s), 2): if s[i] == b"c": @@ -81,11 +74,12 @@ def _open(self) -> None: rgb = s[i + 1] if rgb == b"None": self.info["transparency"] = c - elif rgb[:1] == b"#": - # FIXME: handle colour names (see ImagePalette.py) - rgb = int(rgb[1:], 16) + elif rgb.startswith(b"#"): + rgb_int = int(rgb[1:], 16) palette[c] = ( - o8((rgb >> 16) & 255) + o8((rgb >> 8) & 255) + o8(rgb & 255) + o8((rgb_int >> 16) & 255) + + o8((rgb_int >> 8) & 255) + + o8(rgb_int & 255) ) else: # unknown colour @@ -98,10 +92,16 @@ def _open(self) -> None: msg = "cannot read this XPM file" raise ValueError(msg) - self._mode = "P" - self.palette = ImagePalette.raw("RGB", b"".join(palette)) + args: tuple[int, dict[bytes, bytes] | tuple[bytes, ...]] + if palette_length > 256: + self._mode = "RGB" + args = (bpp, palette) + else: + self._mode = "P" + self.palette = ImagePalette.raw("RGB", b"".join(palette.values())) + args = (bpp, tuple(palette.keys())) - self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] + self.tile = [ImageFile._Tile("xpm", (0, 0) + self.size, self.fp.tell(), args)] def load_read(self, read_bytes: int) -> bytes: # @@ -109,16 +109,48 @@ def load_read(self, read_bytes: int) -> bytes: xsize, ysize = self.size + assert self.fp is not None s = [self.fp.readline()[1 : xsize + 1].ljust(xsize) for i in range(ysize)] return b"".join(s) +class XpmDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: + assert self.fd is not None + + data = bytearray() + bpp, palette = self.args + dest_length = self.state.xsize * self.state.ysize + if self.mode == "RGB": + dest_length *= 3 + pixel_header = False + while len(data) < dest_length: + line = self.fd.readline() + if not line: + break + if line.rstrip() == b"/* pixels */" and not pixel_header: + pixel_header = True + continue + line = b'"'.join(line.split(b'"')[1:-1]) + for i in range(0, len(line), bpp): + key = line[i : i + bpp] + if self.mode == "RGB": + data += palette[key] + else: + data += o8(palette.index(key)) + self.set_as_raw(bytes(data)) + return -1, 0 + + # # Registry Image.register_open(XpmImageFile.format, XpmImageFile, _accept) +Image.register_decoder("xpm", XpmDecoder) Image.register_extension(XpmImageFile.format, ".xpm") diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 09546fe6333..faf3e76e0ae 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -1,6 +1,6 @@ """Pillow (Fork of the Python Imaging Library) -Pillow is the friendly PIL fork by Jeffrey A. Clark and contributors. +Pillow is the friendly PIL fork by Jeffrey 'Alex' Clark and contributors. https://github.com/python-pillow/Pillow/ Pillow is forked from PIL 1.1.7. @@ -25,6 +25,7 @@ _plugins = [ + "AvifImagePlugin", "BlpImagePlugin", "BmpImagePlugin", "BufrStubImagePlugin", diff --git a/src/PIL/_avif.pyi b/src/PIL/_avif.pyi new file mode 100644 index 00000000000..e27843e5338 --- /dev/null +++ b/src/PIL/_avif.pyi @@ -0,0 +1,3 @@ +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index 4594ccce361..d3236c17abb 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -13,6 +13,7 @@ """Binary input/output support routines.""" + from __future__ import annotations from struct import pack, unpack_from diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 33a0e07b3f4..711c62ab2b1 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -12,6 +12,7 @@ def deprecate( *, action: str | None = None, plural: bool = False, + stacklevel: int = 3, ) -> None: """ Deprecations helper. @@ -45,10 +46,10 @@ def deprecate( elif when <= int(__version__.split(".")[0]): msg = f"{deprecated} {is_} deprecated and should be removed." raise RuntimeError(msg) - elif when == 11: - removed = "Pillow 11 (2024-10-15)" - elif when == 12: - removed = "Pillow 12 (2025-10-15)" + elif when == 13: + removed = "Pillow 13 (2026-10-15)" + elif when == 14: + removed = "Pillow 14 (2027-10-15)" else: msg = f"Unknown removal version: {when}. Update {__name__}?" raise ValueError(msg) @@ -67,5 +68,5 @@ def deprecate( warnings.warn( f"{deprecated} {is_} deprecated and will be removed in {removed}{action}", DeprecationWarning, - stacklevel=3, + stacklevel=stacklevel, ) diff --git a/src/PIL/_imaging.pyi b/src/PIL/_imaging.pyi index 8cccd3ac73e..81028a5960a 100644 --- a/src/PIL/_imaging.pyi +++ b/src/PIL/_imaging.pyi @@ -1,6 +1,7 @@ from typing import Any class ImagingCore: + def __getitem__(self, index: int) -> float | tuple[int, ...] | None: ... def __getattr__(self, name: str) -> Any: ... class ImagingFont: diff --git a/src/PIL/_imagingcms.pyi b/src/PIL/_imagingcms.pyi index 2abd6d0f7a9..4fc0d60ab79 100644 --- a/src/PIL/_imagingcms.pyi +++ b/src/PIL/_imagingcms.pyi @@ -1,12 +1,14 @@ import datetime import sys -from typing import Literal, SupportsFloat, TypedDict +from typing import Literal, SupportsFloat, TypeAlias, TypedDict + +from ._typing import CapsuleType littlecms_version: str | None -_Tuple3f = tuple[float, float, float] -_Tuple2x3f = tuple[_Tuple3f, _Tuple3f] -_Tuple3x3f = tuple[_Tuple3f, _Tuple3f, _Tuple3f] +_Tuple3f: TypeAlias = tuple[float, float, float] +_Tuple2x3f: TypeAlias = tuple[_Tuple3f, _Tuple3f] +_Tuple3x3f: TypeAlias = tuple[_Tuple3f, _Tuple3f, _Tuple3f] class _IccMeasurementCondition(TypedDict): observer: int @@ -108,7 +110,7 @@ class CmsProfile: def is_intent_supported(self, intent: int, direction: int, /) -> int: ... class CmsTransform: - def apply(self, id_in: int, id_out: int) -> int: ... + def apply(self, id_in: CapsuleType, id_out: CapsuleType) -> int: ... def profile_open(profile: str, /) -> CmsProfile: ... def profile_frombytes(profile: bytes, /) -> CmsProfile: ... diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index 5e97b40b2e0..2136810ba6a 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -1,12 +1,7 @@ -from typing import Any, TypedDict +from collections.abc import Callable +from typing import Any -from . import _imaging - -class _Axis(TypedDict): - minimum: int | None - default: int | None - maximum: int | None - name: bytes | None +from . import ImageFont, _imaging class Font: @property @@ -28,42 +23,48 @@ class Font: def render( self, string: str | bytes, - fill, - mode=..., - dir=..., - features=..., - lang=..., - stroke_width=..., - anchor=..., - foreground_ink_long=..., - x_start=..., - y_start=..., + fill: Callable[[int, int], _imaging.ImagingCore], + mode: str, + dir: str | None, + features: list[str] | None, + lang: str | None, + stroke_width: float, + stroke_filled: bool, + anchor: str | None, + foreground_ink_long: int, + start: tuple[float, float], /, ) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ... def getsize( self, string: str | bytes | bytearray, - mode=..., - dir=..., - features=..., - lang=..., - anchor=..., + mode: str, + dir: str | None, + features: list[str] | None, + lang: str | None, + anchor: str | None, /, ) -> tuple[tuple[int, int], tuple[int, int]]: ... def getlength( - self, string: str | bytes, mode=..., dir=..., features=..., lang=..., / + self, + string: str | bytes, + mode: str, + dir: str | None, + features: list[str] | None, + lang: str | None, + /, ) -> float: ... def getvarnames(self) -> list[bytes]: ... - def getvaraxes(self) -> list[_Axis] | None: ... + def getvaraxes(self) -> list[ImageFont.Axis]: ... def setvarname(self, instance_index: int, /) -> None: ... def setvaraxes(self, axes: list[float], /) -> None: ... def getfont( filename: str | bytes, size: float, - index=..., - encoding=..., - font_bytes=..., - layout_engine=..., + index: int, + encoding: str, + font_bytes: bytes, + layout_engine: int, ) -> Font: ... def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_imagingtk.pyi b/src/PIL/_imagingtk.pyi new file mode 100644 index 00000000000..e27843e5338 --- /dev/null +++ b/src/PIL/_imagingtk.pyi @@ -0,0 +1,3 @@ +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index beddfb0628a..9c0143003a7 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -1,5 +1,4 @@ -""" Find compiled module linking to Tcl / Tk libraries -""" +"""Find compiled module linking to Tcl / Tk libraries""" from __future__ import annotations diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 09ece18fa60..a941f89806f 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -2,38 +2,44 @@ import os import sys -from typing import Any, Protocol, Sequence, TypeVar, Union +from collections.abc import Sequence +from typing import Any, Protocol, TypeVar -try: - import numpy.typing as npt +TYPE_CHECKING = False +if TYPE_CHECKING: + from numbers import _IntegralLike as IntegralLike - NumpyArray = npt.NDArray[Any] -except ImportError: - pass - -if sys.version_info >= (3, 10): - from typing import TypeGuard -else: try: - from typing_extensions import TypeGuard + import numpy.typing as npt + + NumpyArray = npt.NDArray[Any] except ImportError: + pass + +if sys.version_info >= (3, 13): + from types import CapsuleType +else: + CapsuleType = object + +if sys.version_info >= (3, 12): + from collections.abc import Buffer +else: + Buffer = Any - class TypeGuard: # type: ignore[no-redef] - def __class_getitem__(cls, item: Any) -> type[bool]: - return bool +_Ink = float | tuple[int, ...] | str -Coords = Union[Sequence[float], Sequence[Sequence[float]]] +Coords = Sequence[float] | Sequence[Sequence[float]] _T_co = TypeVar("_T_co", covariant=True) class SupportsRead(Protocol[_T_co]): - def read(self, __length: int = ...) -> _T_co: ... + def read(self, length: int = ..., /) -> _T_co: ... -StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] +StrOrBytesPath = str | bytes | os.PathLike[str] | os.PathLike[bytes] -__all__ = ["TypeGuard", "StrOrBytesPath", "SupportsRead"] +__all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead"] diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 6bc76281616..b1fa6a0f39e 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,20 +1,18 @@ from __future__ import annotations import os -from typing import Any, NoReturn -from ._typing import StrOrBytesPath, TypeGuard +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Any, NoReturn, TypeGuard + + from ._typing import StrOrBytesPath def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: return isinstance(f, (bytes, str, os.PathLike)) -def is_directory(f: Any) -> TypeGuard[StrOrBytesPath]: - """Checks if an object is a string, and that it points to a directory.""" - return is_path(f) and os.path.isdir(f) - - class DeferredError: def __init__(self, ex: BaseException): self.ex = ex diff --git a/src/PIL/_version.py b/src/PIL/_version.py index cebfd868cfd..72d11ae9267 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "10.4.0" +__version__ = "12.2.0" diff --git a/src/PIL/features.py b/src/PIL/features.py index 13908c4eb78..ff32c251045 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -16,6 +16,7 @@ "freetype2": ("PIL._imagingft", "freetype2_version"), "littlecms2": ("PIL._imagingcms", "littlecms_version"), "webp": ("PIL._webp", "webpdecoder_version"), + "avif": ("PIL._avif", "libavif_version"), } @@ -118,14 +119,13 @@ def get_supported_codecs() -> list[str]: return [f for f in codecs if check_codec(f)] -features = { - "webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None), - "webp_mux": ("PIL._webp", "HAVE_WEBPMUX", None), - "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY", None), +features: dict[str, tuple[str, str, str | 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"), + "mozjpeg": ("PIL._imaging", "HAVE_MOZJPEG", "libjpeg_turbo_version"), + "zlib_ng": ("PIL._imaging", "HAVE_ZLIBNG", "zlib_ng_version"), "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"), "xcb": ("PIL._imaging", "HAVE_XCB", None), } @@ -271,9 +271,7 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: ("freetype2", "FREETYPE2"), ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), - ("transp_webp", "WEBP Transparency"), - ("webp_mux", "WEBPMUX"), - ("webp_anim", "WEBP Animation"), + ("avif", "AVIF"), ("jpg", "JPEG"), ("jpg_2000", "OPENJPEG (JPEG2000)"), ("zlib", "ZLIB (PNG/ZIP)"), @@ -287,7 +285,8 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: if name == "jpg": libjpeg_turbo_version = version_feature("libjpeg_turbo") if libjpeg_turbo_version is not None: - v = "libjpeg-turbo " + libjpeg_turbo_version + v = "mozjpeg" if check_feature("mozjpeg") else "libjpeg-turbo" + v += " " + libjpeg_turbo_version if v is None: v = version(name) if v is not None: @@ -296,7 +295,11 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: # 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": + if name == "zlib": + zlib_ng_version = version_feature("zlib_ng") + if zlib_ng_version is not None: + v += ", compiled for zlib-ng " + zlib_ng_version + elif name == "raqm": for f in ("fribidi", "harfbuzz"): v2 = version_feature(f) if v2 is not None: diff --git a/src/Tk/_tkmini.h b/src/Tk/_tkmini.h index 68247bc472d..a260fa0d1dc 100644 --- a/src/Tk/_tkmini.h +++ b/src/Tk/_tkmini.h @@ -80,7 +80,8 @@ typedef struct Tcl_Command_ *Tcl_Command; typedef void *ClientData; typedef int(Tcl_CmdProc)( - ClientData clientData, Tcl_Interp *interp, int argc, const char *argv[]); + ClientData clientData, Tcl_Interp *interp, int argc, const char *argv[] +); typedef void(Tcl_CmdDeleteProc)(ClientData clientData); /* Typedefs derived from function signatures in Tcl header */ @@ -90,7 +91,8 @@ typedef Tcl_Command (*Tcl_CreateCommand_t)( const char *cmdName, Tcl_CmdProc *proc, ClientData clientData, - Tcl_CmdDeleteProc *deleteProc); + Tcl_CmdDeleteProc *deleteProc +); /* Tcl_AppendResult */ typedef void (*Tcl_AppendResult_t)(Tcl_Interp *interp, ...); @@ -127,7 +129,8 @@ typedef int (*Tk_PhotoPutBlock_t)( int y, int width, int height, - int compRule); + int compRule +); /* Tk_FindPhoto */ typedef Tk_PhotoHandle (*Tk_FindPhoto_t)(Tcl_Interp *interp, const char *imageName); /* Tk_PhotoGetImage */ diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index ef1c00a94e8..3e35f885f61 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -56,31 +56,48 @@ static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK; static Imaging ImagingFind(const char *name) { - Py_ssize_t id; + PyObject *capsule; + int direct_pointer = 0; + const char *expected = "capsule object \"" IMAGING_MAGIC "\" at 0x"; - /* FIXME: use CObject instead? */ -#if defined(_WIN64) - id = _atoi64(name); -#else - id = atol(name); -#endif - if (!id) { + if (name[0] == '<') { + name++; + } else { + // Special case for PyPy, where the string representation of a Capsule + // refers directly to the pointer itself, not to the PyCapsule object. + direct_pointer = 1; + } + + if (strncmp(name, expected, strlen(expected))) { + return NULL; + } + + capsule = (PyObject *)strtoull(name + strlen(expected), NULL, 16); + + if (direct_pointer) { + return (Imaging)capsule; + } + + if (!PyCapsule_IsValid(capsule, IMAGING_MAGIC)) { + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } - return (Imaging)id; + return (Imaging)PyCapsule_GetPointer(capsule, IMAGING_MAGIC); } static int PyImagingPhotoPut( - ClientData clientdata, Tcl_Interp *interp, int argc, const char **argv) { + 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); + interp, "usage: ", argv[0], " destPhoto srcImage", (char *)NULL + ); return TCL_ERROR; } @@ -104,15 +121,18 @@ PyImagingPhotoPut( /* Mode */ - if (strcmp(im->mode, "1") == 0 || strcmp(im->mode, "L") == 0) { + if (im->mode == IMAGING_MODE_1 || im->mode == IMAGING_MODE_L) { block.pixelSize = 1; block.offset[0] = block.offset[1] = block.offset[2] = block.offset[3] = 0; - } else if (strncmp(im->mode, "RGB", 3) == 0) { + } else if ( + im->mode == IMAGING_MODE_RGB || im->mode == IMAGING_MODE_RGBA || + im->mode == IMAGING_MODE_RGBX || im->mode == IMAGING_MODE_RGBa + ) { block.pixelSize = 4; block.offset[0] = 0; block.offset[1] = 1; block.offset[2] = 2; - if (strcmp(im->mode, "RGBA") == 0) { + if (im->mode == IMAGING_MODE_RGBA) { block.offset[3] = 3; /* alpha (or reserved, under Tk 8.2) */ } else { block.offset[3] = 0; /* no alpha */ @@ -128,14 +148,16 @@ PyImagingPhotoPut( block.pixelPtr = (unsigned char *)im->block; TK_PHOTO_PUT_BLOCK( - interp, photo, &block, 0, 0, block.width, block.height, TK_PHOTO_COMPOSITE_SET); + interp, photo, &block, 0, 0, block.width, block.height, TK_PHOTO_COMPOSITE_SET + ); return TCL_OK; } static int PyImagingPhotoGet( - ClientData clientdata, Tcl_Interp *interp, int argc, const char **argv) { + ClientData clientdata, Tcl_Interp *interp, int argc, const char **argv +) { Imaging im; Tk_PhotoHandle photo; Tk_PhotoImageBlock block; @@ -143,7 +165,8 @@ PyImagingPhotoGet( if (argc != 3) { TCL_APPEND_RESULT( - interp, "usage: ", argv[0], " srcPhoto destImage", (char *)NULL); + interp, "usage: ", argv[0], " srcPhoto destImage", (char *)NULL + ); return TCL_ERROR; } @@ -183,13 +206,15 @@ TkImaging_Init(Tcl_Interp *interp) { "PyImagingPhoto", PyImagingPhotoPut, (ClientData)0, - (Tcl_CmdDeleteProc *)NULL); + (Tcl_CmdDeleteProc *)NULL + ); TCL_CREATE_COMMAND( interp, "PyImagingPhotoGet", PyImagingPhotoGet, (ClientData)0, - (Tcl_CmdDeleteProc *)NULL); + (Tcl_CmdDeleteProc *)NULL + ); } /* @@ -394,7 +419,8 @@ _func_loader(void *lib) { } return ( (TK_PHOTO_PUT_BLOCK = (Tk_PhotoPutBlock_t)_dfunc(lib, "Tk_PhotoPutBlock")) == - NULL); + NULL + ); } int diff --git a/src/_avif.c b/src/_avif.c new file mode 100644 index 00000000000..1fe0cb98676 --- /dev/null +++ b/src/_avif.c @@ -0,0 +1,918 @@ +#define PY_SSIZE_T_CLEAN + +#include +#include "avif/avif.h" + +// Encoder type +typedef struct { + PyObject_HEAD avifEncoder *encoder; + avifImage *image; + int first_frame; +} AvifEncoderObject; + +static PyTypeObject AvifEncoder_Type; + +// Decoder type +typedef struct { + PyObject_HEAD avifDecoder *decoder; + Py_buffer buffer; +} AvifDecoderObject; + +static PyTypeObject AvifDecoder_Type; + +static int +normalize_tiles_log2(int value) { + if (value < 0) { + return 0; + } else if (value > 6) { + return 6; + } else { + return value; + } +} + +static PyObject * +exc_type_for_avif_result(avifResult result) { + switch (result) { + case AVIF_RESULT_INVALID_EXIF_PAYLOAD: + case AVIF_RESULT_INVALID_CODEC_SPECIFIC_OPTION: + return PyExc_ValueError; + case AVIF_RESULT_INVALID_FTYP: + case AVIF_RESULT_BMFF_PARSE_FAILED: + case AVIF_RESULT_TRUNCATED_DATA: + case AVIF_RESULT_NO_CONTENT: + return PyExc_SyntaxError; + default: + return PyExc_RuntimeError; + } +} + +static uint8_t +irot_imir_to_exif_orientation(const avifImage *image) { + uint8_t axis = image->imir.axis; + int imir = image->transformFlags & AVIF_TRANSFORM_IMIR; + int irot = image->transformFlags & AVIF_TRANSFORM_IROT; + if (irot) { + uint8_t angle = image->irot.angle; + if (angle == 1) { + if (imir) { + return axis ? 7 // 90 degrees anti-clockwise then swap left and right. + : 5; // 90 degrees anti-clockwise then swap top and bottom. + } + return 8; // 90 degrees anti-clockwise. + } + if (angle == 2) { + if (imir) { + return axis + ? 4 // 180 degrees anti-clockwise then swap left and right. + : 2; // 180 degrees anti-clockwise then swap top and bottom. + } + return 3; // 180 degrees anti-clockwise. + } + if (angle == 3) { + if (imir) { + return axis + ? 5 // 270 degrees anti-clockwise then swap left and right. + : 7; // 270 degrees anti-clockwise then swap top and bottom. + } + return 6; // 270 degrees anti-clockwise. + } + } + if (imir) { + return axis ? 2 // Swap left and right. + : 4; // Swap top and bottom. + } + return 1; // Default orientation ("top-left", no-op). +} + +static void +exif_orientation_to_irot_imir(avifImage *image, int orientation) { + // Mapping from Exif orientation as defined in JEITA CP-3451C section 4.6.4.A + // Orientation to irot and imir boxes as defined in HEIF ISO/IEC 28002-12:2021 + // sections 6.5.10 and 6.5.12. + switch (orientation) { + case 2: // The 0th row is at the visual top of the image, and the 0th column is + // the visual right-hand side. + image->transformFlags |= AVIF_TRANSFORM_IMIR; + image->imir.axis = 1; + break; + case 3: // The 0th row is at the visual bottom of the image, and the 0th column + // is the visual right-hand side. + image->transformFlags |= AVIF_TRANSFORM_IROT; + image->irot.angle = 2; + break; + case 4: // The 0th row is at the visual bottom of the image, and the 0th column + // is the visual left-hand side. + image->transformFlags |= AVIF_TRANSFORM_IMIR; + break; + case 5: // The 0th row is the visual left-hand side of the image, and the 0th + // column is the visual top. + image->transformFlags |= AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR; + image->irot.angle = 1; // applied before imir according to MIAF spec + // ISO/IEC 28002-12:2021 - section 7.3.6.7 + break; + case 6: // The 0th row is the visual right-hand side of the image, and the 0th + // column is the visual top. + image->transformFlags |= AVIF_TRANSFORM_IROT; + image->irot.angle = 3; + break; + case 7: // The 0th row is the visual right-hand side of the image, and the 0th + // column is the visual bottom. + image->transformFlags |= AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR; + image->irot.angle = 3; // applied before imir according to MIAF spec + // ISO/IEC 28002-12:2021 - section 7.3.6.7 + break; + case 8: // The 0th row is the visual left-hand side of the image, and the 0th + // column is the visual bottom. + image->transformFlags |= AVIF_TRANSFORM_IROT; + image->irot.angle = 1; + break; + } +} + +static int +_codec_available(const char *name, avifCodecFlags flags) { + avifCodecChoice codec = avifCodecChoiceFromName(name); + if (codec == AVIF_CODEC_CHOICE_AUTO) { + return 0; + } + const char *codec_name = avifCodecName(codec, flags); + return (codec_name == NULL) ? 0 : 1; +} + +PyObject * +_decoder_codec_available(PyObject *self, PyObject *args) { + char *codec_name; + if (!PyArg_ParseTuple(args, "s", &codec_name)) { + return NULL; + } + int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_DECODE); + return PyBool_FromLong(is_available); +} + +PyObject * +_encoder_codec_available(PyObject *self, PyObject *args) { + char *codec_name; + if (!PyArg_ParseTuple(args, "s", &codec_name)) { + return NULL; + } + int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_ENCODE); + return PyBool_FromLong(is_available); +} + +PyObject * +_codec_versions(PyObject *self, PyObject *args) { + char buffer[256]; + avifCodecVersions(buffer); + return PyUnicode_FromString(buffer); +} + +static int +_add_codec_specific_options(avifEncoder *encoder, PyObject *opts) { + Py_ssize_t i, size; + PyObject *keyval, *py_key, *py_val; + if (!PyTuple_Check(opts)) { + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; + } + size = PyTuple_GET_SIZE(opts); + + for (i = 0; i < size; i++) { + keyval = PyTuple_GetItem(opts, i); + if (!PyTuple_Check(keyval) || PyTuple_GET_SIZE(keyval) != 2) { + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; + } + py_key = PyTuple_GetItem(keyval, 0); + py_val = PyTuple_GetItem(keyval, 1); + if (!PyUnicode_Check(py_key) || !PyUnicode_Check(py_val)) { + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; + } + const char *key = PyUnicode_AsUTF8(py_key); + const char *val = PyUnicode_AsUTF8(py_val); + if (key == NULL || val == NULL) { + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; + } + + avifResult result = avifEncoderSetCodecSpecificOption(encoder, key, val); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting advanced codec options failed: %s", + avifResultToString(result) + ); + return 1; + } + } + return 0; +} + +// Encoder functions +PyObject * +AvifEncoderNew(PyObject *self_, PyObject *args) { + unsigned int width, height; + AvifEncoderObject *self = NULL; + avifEncoder *encoder = NULL; + + char *subsampling; + int quality; + int speed; + int exif_orientation; + int max_threads; + Py_buffer icc_buffer; + Py_buffer exif_buffer; + Py_buffer xmp_buffer; + int alpha_premultiplied; + int autotiling; + int tile_rows_log2; + int tile_cols_log2; + + char *codec; + char *range; + + PyObject *advanced; + int error = 0; + + if (!PyArg_ParseTuple( + args, + "(II)siiissiippy*y*iy*O", + &width, + &height, + &subsampling, + &quality, + &speed, + &max_threads, + &codec, + &range, + &tile_rows_log2, + &tile_cols_log2, + &alpha_premultiplied, + &autotiling, + &icc_buffer, + &exif_buffer, + &exif_orientation, + &xmp_buffer, + &advanced + )) { + return NULL; + } + + // Create a new animation encoder and picture frame + avifImage *image = avifImageCreateEmpty(); + if (image == NULL) { + PyErr_SetString(PyExc_ValueError, "Image creation failed"); + error = 1; + goto end; + } + + // Set these in advance so any upcoming RGB -> YUV use the proper coefficients + if (strcmp(range, "full") == 0) { + image->yuvRange = AVIF_RANGE_FULL; + } else if (strcmp(range, "limited") == 0) { + image->yuvRange = AVIF_RANGE_LIMITED; + } else { + PyErr_SetString(PyExc_ValueError, "Invalid range"); + error = 1; + goto end; + } + if (strcmp(subsampling, "4:0:0") == 0) { + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV400; + } else if (strcmp(subsampling, "4:2:0") == 0) { + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV420; + } else if (strcmp(subsampling, "4:2:2") == 0) { + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV422; + } else if (strcmp(subsampling, "4:4:4") == 0) { + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV444; + } else { + PyErr_Format(PyExc_ValueError, "Invalid subsampling: %s", subsampling); + error = 1; + goto end; + } + + // Validate canvas dimensions + if (width == 0 || height == 0) { + PyErr_SetString(PyExc_ValueError, "invalid canvas dimensions"); + error = 1; + goto end; + } + image->width = width; + image->height = height; + + image->depth = 8; + image->alphaPremultiplied = alpha_premultiplied ? AVIF_TRUE : AVIF_FALSE; + + encoder = avifEncoderCreate(); + if (!encoder) { + PyErr_SetString(PyExc_MemoryError, "Can't allocate encoder"); + error = 1; + goto end; + } + + int is_aom_encode = strcmp(codec, "aom") == 0 || + (strcmp(codec, "auto") == 0 && + _codec_available("aom", AVIF_CODEC_FLAG_CAN_ENCODE)); + encoder->maxThreads = is_aom_encode && max_threads > 64 ? 64 : max_threads; + + encoder->quality = quality; + + if (strcmp(codec, "auto") == 0) { + encoder->codecChoice = AVIF_CODEC_CHOICE_AUTO; + } else { + encoder->codecChoice = avifCodecChoiceFromName(codec); + } + if (speed < AVIF_SPEED_SLOWEST) { + speed = AVIF_SPEED_SLOWEST; + } else if (speed > AVIF_SPEED_FASTEST) { + speed = AVIF_SPEED_FASTEST; + } + encoder->speed = speed; + encoder->timescale = (uint64_t)1000; + + encoder->autoTiling = autotiling ? AVIF_TRUE : AVIF_FALSE; + if (!autotiling) { + encoder->tileRowsLog2 = normalize_tiles_log2(tile_rows_log2); + encoder->tileColsLog2 = normalize_tiles_log2(tile_cols_log2); + } + + if (advanced != Py_None && _add_codec_specific_options(encoder, advanced)) { + error = 1; + goto end; + } + + self = PyObject_New(AvifEncoderObject, &AvifEncoder_Type); + if (!self) { + PyErr_SetString(PyExc_RuntimeError, "could not create encoder object"); + error = 1; + goto end; + } + self->first_frame = 1; + + avifResult result; + if (icc_buffer.len) { + result = avifImageSetProfileICC(image, icc_buffer.buf, icc_buffer.len); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting ICC profile failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + // colorPrimaries and transferCharacteristics are ignored when an ICC + // profile is present, so set them to UNSPECIFIED. + image->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED; + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; + } else { + image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; + } + image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; + + if (exif_buffer.len) { + result = avifImageSetMetadataExif(image, exif_buffer.buf, exif_buffer.len); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting EXIF data failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + } + + if (xmp_buffer.len) { + result = avifImageSetMetadataXMP(image, xmp_buffer.buf, xmp_buffer.len); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting XMP data failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + } + + if (exif_orientation > 1) { + exif_orientation_to_irot_imir(image, exif_orientation); + } + + self->image = image; + self->encoder = encoder; + +end: + PyBuffer_Release(&icc_buffer); + PyBuffer_Release(&exif_buffer); + PyBuffer_Release(&xmp_buffer); + + if (error) { + if (image) { + avifImageDestroy(image); + } + if (encoder) { + avifEncoderDestroy(encoder); + } + if (self) { + PyObject_Del(self); + } + return NULL; + } + + return (PyObject *)self; +} + +void +_encoder_dealloc(AvifEncoderObject *self) { + if (self->encoder) { + avifEncoderDestroy(self->encoder); + } + if (self->image) { + avifImageDestroy(self->image); + } + Py_TYPE(self)->tp_free(self); +} + +PyObject * +_encoder_add(AvifEncoderObject *self, PyObject *args) { + uint8_t *rgb_bytes; + Py_ssize_t size; + unsigned int duration; + unsigned int width; + unsigned int height; + char *mode; + unsigned int is_single_frame; + int error = 0; + + avifRGBImage rgb; + avifResult result; + + avifEncoder *encoder = self->encoder; + avifImage *image = self->image; + avifImage *frame = NULL; + + if (!PyArg_ParseTuple( + args, + "y#I(II)sp", + (char **)&rgb_bytes, + &size, + &duration, + &width, + &height, + &mode, + &is_single_frame + )) { + return NULL; + } + + if (image->width != width || image->height != height) { + PyErr_Format( + PyExc_ValueError, + "Image sequence dimensions mismatch, %ux%u != %ux%u", + image->width, + image->height, + width, + height + ); + return NULL; + } + + if (self->first_frame) { + // If we don't have an image populated with yuv planes, this is the first frame + frame = image; + } else { + frame = avifImageCreateEmpty(); + if (frame == NULL) { + PyErr_SetString(PyExc_ValueError, "Image creation failed"); + return NULL; + } + + frame->width = width; + frame->height = height; + frame->colorPrimaries = image->colorPrimaries; + frame->transferCharacteristics = image->transferCharacteristics; + frame->matrixCoefficients = image->matrixCoefficients; + frame->yuvRange = image->yuvRange; + frame->yuvFormat = image->yuvFormat; + frame->depth = image->depth; + frame->alphaPremultiplied = image->alphaPremultiplied; + } + + avifRGBImageSetDefaults(&rgb, frame); + + if (strcmp(mode, "RGBA") == 0) { + rgb.format = AVIF_RGB_FORMAT_RGBA; + } else { + rgb.format = AVIF_RGB_FORMAT_RGB; + } + + result = avifRGBImageAllocatePixels(&rgb); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Pixel allocation failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + + if (rgb.rowBytes * rgb.height != size) { + PyErr_Format( + PyExc_RuntimeError, + "rgb data has incorrect size: %u * %u (%u) != %u", + rgb.rowBytes, + rgb.height, + rgb.rowBytes * rgb.height, + size + ); + error = 1; + goto end; + } + + // rgb.pixels is safe for writes + memcpy(rgb.pixels, rgb_bytes, size); + + Py_BEGIN_ALLOW_THREADS; + result = avifImageRGBToYUV(frame, &rgb); + Py_END_ALLOW_THREADS; + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Conversion to YUV failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + + uint32_t addImageFlags = + is_single_frame ? AVIF_ADD_IMAGE_FLAG_SINGLE : AVIF_ADD_IMAGE_FLAG_NONE; + + Py_BEGIN_ALLOW_THREADS; + result = avifEncoderAddImage(encoder, frame, duration, addImageFlags); + Py_END_ALLOW_THREADS; + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to encode image: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + +end: + avifRGBImageFreePixels(&rgb); + if (!self->first_frame) { + avifImageDestroy(frame); + } + + if (error) { + return NULL; + } + self->first_frame = 0; + Py_RETURN_NONE; +} + +PyObject * +_encoder_finish(AvifEncoderObject *self) { + avifEncoder *encoder = self->encoder; + + avifRWData raw = AVIF_DATA_EMPTY; + avifResult result; + PyObject *ret = NULL; + + Py_BEGIN_ALLOW_THREADS; + result = avifEncoderFinish(encoder, &raw); + Py_END_ALLOW_THREADS; + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to finish encoding: %s", + avifResultToString(result) + ); + avifRWDataFree(&raw); + return NULL; + } + + ret = PyBytes_FromStringAndSize((char *)raw.data, raw.size); + + avifRWDataFree(&raw); + + return ret; +} + +// Decoder functions +PyObject * +AvifDecoderNew(PyObject *self_, PyObject *args) { + Py_buffer buffer; + AvifDecoderObject *self = NULL; + avifDecoder *decoder; + + char *codec_str; + avifCodecChoice codec; + int max_threads; + + avifResult result; + + if (!PyArg_ParseTuple(args, "y*si", &buffer, &codec_str, &max_threads)) { + return NULL; + } + + if (strcmp(codec_str, "auto") == 0) { + codec = AVIF_CODEC_CHOICE_AUTO; + } else { + codec = avifCodecChoiceFromName(codec_str); + } + + self = PyObject_New(AvifDecoderObject, &AvifDecoder_Type); + if (!self) { + PyErr_SetString(PyExc_RuntimeError, "could not create decoder object"); + PyBuffer_Release(&buffer); + return NULL; + } + + decoder = avifDecoderCreate(); + if (!decoder) { + PyErr_SetString(PyExc_MemoryError, "Can't allocate decoder"); + PyBuffer_Release(&buffer); + PyObject_Del(self); + return NULL; + } + decoder->maxThreads = max_threads; + // Turn off libavif's 'clap' (clean aperture) property validation. + decoder->strictFlags &= ~AVIF_STRICT_CLAP_VALID; + // Allow the PixelInformationProperty ('pixi') to be missing in AV1 image + // items. libheif v1.11.0 and older does not add the 'pixi' item property to + // AV1 image items. + decoder->strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED; + decoder->codecChoice = codec; + + result = avifDecoderSetIOMemory(decoder, buffer.buf, buffer.len); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting IO memory failed: %s", + avifResultToString(result) + ); + avifDecoderDestroy(decoder); + PyBuffer_Release(&buffer); + PyObject_Del(self); + return NULL; + } + + result = avifDecoderParse(decoder); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to decode image: %s", + avifResultToString(result) + ); + avifDecoderDestroy(decoder); + PyBuffer_Release(&buffer); + PyObject_Del(self); + return NULL; + } + + self->decoder = decoder; + self->buffer = buffer; + + return (PyObject *)self; +} + +void +_decoder_dealloc(AvifDecoderObject *self) { + if (self->decoder) { + avifDecoderDestroy(self->decoder); + } + PyBuffer_Release(&self->buffer); + Py_TYPE(self)->tp_free(self); +} + +PyObject * +_decoder_get_info(AvifDecoderObject *self) { + avifDecoder *decoder = self->decoder; + avifImage *image = decoder->image; + + PyObject *icc = NULL; + PyObject *exif = NULL; + PyObject *xmp = NULL; + PyObject *ret = NULL; + + if (image->xmp.size) { + xmp = PyBytes_FromStringAndSize((const char *)image->xmp.data, image->xmp.size); + if (!xmp) { + return NULL; + } + } + + if (image->exif.size) { + exif = + PyBytes_FromStringAndSize((const char *)image->exif.data, image->exif.size); + if (!exif) { + Py_XDECREF(xmp); + return NULL; + } + } + + if (image->icc.size) { + icc = PyBytes_FromStringAndSize((const char *)image->icc.data, image->icc.size); + if (!icc) { + Py_XDECREF(xmp); + Py_XDECREF(exif); + return NULL; + } + } + + ret = Py_BuildValue( + "(II)IsSSIS", + image->width, + image->height, + decoder->imageCount, + decoder->alphaPresent ? "RGBA" : "RGB", + NULL == icc ? Py_None : icc, + NULL == exif ? Py_None : exif, + irot_imir_to_exif_orientation(image), + NULL == xmp ? Py_None : xmp + ); + + Py_XDECREF(xmp); + Py_XDECREF(exif); + Py_XDECREF(icc); + + return ret; +} + +PyObject * +_decoder_get_frame(AvifDecoderObject *self, PyObject *args) { + PyObject *bytes; + PyObject *ret; + Py_ssize_t size; + avifResult result; + avifRGBImage rgb; + avifDecoder *decoder; + avifImage *image; + uint32_t frame_index; + + decoder = self->decoder; + + if (!PyArg_ParseTuple(args, "I", &frame_index)) { + return NULL; + } + + result = avifDecoderNthImage(decoder, frame_index); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to decode frame %u: %s", + frame_index, + avifResultToString(result) + ); + return NULL; + } + + image = decoder->image; + + avifRGBImageSetDefaults(&rgb, image); + + rgb.depth = 8; + rgb.format = decoder->alphaPresent ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB; + + result = avifRGBImageAllocatePixels(&rgb); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Pixel allocation failed: %s", + avifResultToString(result) + ); + return NULL; + } + + Py_BEGIN_ALLOW_THREADS; + result = avifImageYUVToRGB(image, &rgb); + Py_END_ALLOW_THREADS; + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Conversion from YUV failed: %s", + avifResultToString(result) + ); + avifRGBImageFreePixels(&rgb); + return NULL; + } + + if (rgb.height > PY_SSIZE_T_MAX / rgb.rowBytes) { + PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size"); + avifRGBImageFreePixels(&rgb); + return NULL; + } + + size = rgb.rowBytes * rgb.height; + + bytes = PyBytes_FromStringAndSize((char *)rgb.pixels, size); + avifRGBImageFreePixels(&rgb); + if (!bytes) { + return NULL; + } + + ret = Py_BuildValue( + "SKKK", + bytes, + decoder->timescale, + decoder->imageTiming.ptsInTimescales, + decoder->imageTiming.durationInTimescales + ); + + Py_DECREF(bytes); + + return ret; +} + +/* -------------------------------------------------------------------- */ +/* Type Definitions */ +/* -------------------------------------------------------------------- */ + +// AvifEncoder methods +static struct PyMethodDef _encoder_methods[] = { + {"add", (PyCFunction)_encoder_add, METH_VARARGS}, + {"finish", (PyCFunction)_encoder_finish, METH_NOARGS}, + {NULL, NULL} /* sentinel */ +}; + +// AvifEncoder type definition +static PyTypeObject AvifEncoder_Type = { + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifEncoder", + .tp_basicsize = sizeof(AvifEncoderObject), + .tp_dealloc = (destructor)_encoder_dealloc, + .tp_methods = _encoder_methods, +}; + +// AvifDecoder methods +static struct PyMethodDef _decoder_methods[] = { + {"get_info", (PyCFunction)_decoder_get_info, METH_NOARGS}, + {"get_frame", (PyCFunction)_decoder_get_frame, METH_VARARGS}, + {NULL, NULL} /* sentinel */ +}; + +// AvifDecoder type definition +static PyTypeObject AvifDecoder_Type = { + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifDecoder", + .tp_basicsize = sizeof(AvifDecoderObject), + .tp_dealloc = (destructor)_decoder_dealloc, + .tp_methods = _decoder_methods, +}; + +/* -------------------------------------------------------------------- */ +/* Module Setup */ +/* -------------------------------------------------------------------- */ + +static PyMethodDef avifMethods[] = { + {"AvifDecoder", AvifDecoderNew, METH_VARARGS}, + {"AvifEncoder", AvifEncoderNew, METH_VARARGS}, + {"decoder_codec_available", _decoder_codec_available, METH_VARARGS}, + {"encoder_codec_available", _encoder_codec_available, METH_VARARGS}, + {"codec_versions", _codec_versions, METH_NOARGS}, + {NULL, NULL} +}; + +static int +setup_module(PyObject *m) { + if (PyType_Ready(&AvifDecoder_Type) < 0 || PyType_Ready(&AvifEncoder_Type) < 0) { + return -1; + } + + PyObject *d = PyModule_GetDict(m); + PyObject *v = PyUnicode_FromString(avifVersion()); + PyDict_SetItemString(d, "libavif_version", v ? v : Py_None); + Py_XDECREF(v); + + return 0; +} + +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + +PyMODINIT_FUNC +PyInit__avif(void) { + static PyModuleDef module_def = { + PyModuleDef_HEAD_INIT, + .m_name = "_avif", + .m_methods = avifMethods, + .m_slots = slots + }; + + return PyModuleDef_Init(&module_def); +} diff --git a/src/_imaging.c b/src/_imaging.c index ddc8d288550..980f827ae78 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -76,6 +76,13 @@ #ifdef HAVE_LIBJPEG #include "jconfig.h" +#ifdef LIBJPEG_TURBO_VERSION +#define JCONFIG_INCLUDED +#ifdef __CYGWIN__ +#define _BASETSD_H +#endif +#include "jpeglib.h" +#endif #endif #ifdef HAVE_LIBZ @@ -92,19 +99,7 @@ #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_UNSHARPMASK /* Kevin Cazabon's unsharpmask module */ +#include #undef VERBOSE @@ -123,8 +118,6 @@ typedef struct { static PyTypeObject Imaging_Type; -#ifdef WITH_IMAGEDRAW - typedef struct { /* to write a character, cut out sxy from glyph data, place at current position plus dxy, and advance by (dx, dy) */ @@ -151,8 +144,6 @@ typedef struct { static PyTypeObject ImagingDraw_Type; -#endif - typedef struct { PyObject_HEAD ImagingObject *image; int readonly; @@ -215,16 +206,12 @@ PyImaging_AsImaging(PyObject *op) { void ImagingSectionEnter(ImagingSectionCookie *cookie) { -#ifdef WITH_THREADING *cookie = (PyThreadState *)PyEval_SaveThread(); -#endif } void ImagingSectionLeave(ImagingSectionCookie *cookie) { -#ifdef WITH_THREADING PyEval_RestoreThread((PyThreadState *)*cookie); -#endif } /* -------------------------------------------------------------------- */ @@ -243,6 +230,108 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) { return PyObject_GetBuffer(buffer, view, PyBUF_SIMPLE); } +/* -------------------------------------------------------------------- */ +/* Arrow HANDLING */ +/* -------------------------------------------------------------------- */ + +PyObject * +ArrowError(int err) { + if (err == IMAGING_CODEC_MEMORY) { + return ImagingError_MemoryError(); + } + if (err == IMAGING_ARROW_INCOMPATIBLE_MODE) { + return ImagingError_ValueError("Incompatible Pillow mode for Arrow array"); + } + if (err == IMAGING_ARROW_MEMORY_LAYOUT) { + return ImagingError_ValueError( + "Image is in multiple array blocks, use imaging_new_block for zero copy" + ); + } + return ImagingError_ValueError("Unknown error"); +} + +void +ReleaseArrowSchemaPyCapsule(PyObject *capsule) { + struct ArrowSchema *schema = + (struct ArrowSchema *)PyCapsule_GetPointer(capsule, "arrow_schema"); + if (!schema) { + return; + } + if (schema->release != NULL) { + schema->release(schema); + } + free(schema); +} + +PyObject * +ExportArrowSchemaPyCapsule(ImagingObject *self) { + struct ArrowSchema *schema = + (struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema)); + if (!schema) { + return ArrowError(IMAGING_CODEC_MEMORY); + } + int err = export_imaging_schema(self->image, schema); + if (err == 0) { + return PyCapsule_New(schema, "arrow_schema", ReleaseArrowSchemaPyCapsule); + } + free(schema); + return ArrowError(err); +} + +void +ReleaseArrowArrayPyCapsule(PyObject *capsule) { + struct ArrowArray *array = + (struct ArrowArray *)PyCapsule_GetPointer(capsule, "arrow_array"); + if (!array) { + return; + } + if (array->release != NULL) { + array->release(array); + } + free(array); +} + +PyObject * +ExportArrowArrayPyCapsule(ImagingObject *self) { + struct ArrowArray *array = + (struct ArrowArray *)calloc(1, sizeof(struct ArrowArray)); + if (!array) { + return ArrowError(IMAGING_CODEC_MEMORY); + } + int err = export_imaging_array(self->image, array); + if (err == 0) { + return PyCapsule_New(array, "arrow_array", ReleaseArrowArrayPyCapsule); + } + free(array); + return ArrowError(err); +} + +static PyObject * +_new_arrow(PyObject *self, PyObject *args) { + char *mode; + ModeID mode_id; + int xsize, ysize; + PyObject *schema_capsule, *array_capsule; + PyObject *ret; + + if (!PyArg_ParseTuple( + args, "s(ii)OO", &mode, &xsize, &ysize, &schema_capsule, &array_capsule + )) { + return NULL; + } + + mode_id = findModeID(mode); + + // ImagingBorrowArrow is responsible for retaining the array_capsule + ret = PyImagingNew( + ImagingNewArrow(mode_id, xsize, ysize, schema_capsule, array_capsule) + ); + if (!ret) { + return ImagingError_ValueError("Invalid Arrow array mode or size mismatch"); + } + return ret; +} + /* -------------------------------------------------------------------- */ /* EXCEPTION REROUTING */ /* -------------------------------------------------------------------- */ @@ -264,12 +353,6 @@ 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_OSError(void) { - PyErr_SetString(PyExc_OSError, "error when accessing file"); - return NULL; -} - void * ImagingError_MemoryError(void) { return PyErr_NoMemory(); @@ -290,21 +373,17 @@ ImagingError_ModeError(void) { void * 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) { - PyErr_Clear(); -} - /* -------------------------------------------------------------------- */ /* HELPERS */ /* -------------------------------------------------------------------- */ static int -getbands(const char *mode) { +getbands(const ModeID mode) { Imaging im; int bands; @@ -467,7 +546,8 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) { 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]); + "BBBB", pixel.b[0], pixel.b[1], pixel.b[2], pixel.b[3] + ); } break; case IMAGING_TYPE_INT32: @@ -475,17 +555,11 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) { case IMAGING_TYPE_FLOAT32: return PyFloat_FromDouble(pixel.f); case IMAGING_TYPE_SPECIAL: - if (im->bands == 1) { - return PyLong_FromLong(pixel.h); - } else { - return Py_BuildValue("BBB", pixel.b[0], pixel.b[1], pixel.b[2]); - } - break; + return PyLong_FromLong(pixel.h); } /* unknown type */ - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static char * @@ -518,7 +592,8 @@ getink(PyObject *color, Imaging im, char *ink) { rIsInt = 1; } else if (im->bands == 1) { PyErr_SetString( - PyExc_TypeError, "color must be int or single-element tuple"); + PyExc_TypeError, "color must be int or single-element tuple" + ); return NULL; } else if (tupleSize == -1) { PyErr_SetString(PyExc_TypeError, "color must be int or tuple"); @@ -534,8 +609,8 @@ getink(PyObject *color, Imaging im, char *ink) { if (rIsInt != 1) { if (tupleSize != 1) { PyErr_SetString( - PyExc_TypeError, - "color must be int or single-element tuple"); + PyExc_TypeError, "color must be int or single-element tuple" + ); return NULL; } else if (!PyArg_ParseTuple(color, "L", &r)) { return NULL; @@ -556,7 +631,8 @@ getink(PyObject *color, Imaging im, char *ink) { if (tupleSize != 1 && tupleSize != 2) { PyErr_SetString( PyExc_TypeError, - "color must be int, or tuple of one or two elements"); + "color must be int, or tuple of one or two elements" + ); return NULL; } else if (!PyArg_ParseTuple(color, "L|i", &r, &a)) { return NULL; @@ -567,7 +643,8 @@ getink(PyObject *color, Imaging im, char *ink) { PyErr_SetString( PyExc_TypeError, "color must be int, or tuple of one, three or four " - "elements"); + "elements" + ); return NULL; } else if (!PyArg_ParseTuple(color, "Lii|i", &r, &g, &b, &a)) { return NULL; @@ -595,49 +672,10 @@ getink(PyObject *color, Imaging im, char *ink) { 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; - } else { - if (rIsInt) { - b = (UINT8)(r >> 16); - g = (UINT8)(r >> 8); - r = (UINT8)r; - } else if (tupleSize != 3) { - PyErr_SetString( - PyExc_TypeError, - "color must be int, or tuple of one or three elements"); - return NULL; - } else if (!PyArg_ParseTuple(color, "iiL", &b, &g, &r)) { - return NULL; - } - if (!strcmp(im->mode, "BGR;15")) { - UINT16 v = ((((UINT16)r) << 7) & 0x7c00) + - ((((UINT16)g) << 2) & 0x03e0) + - ((((UINT16)b) >> 3) & 0x001f); - - ink[0] = (UINT8)v; - ink[1] = (UINT8)(v >> 8); - ink[2] = ink[3] = 0; - return ink; - } else if (!strcmp(im->mode, "BGR;16")) { - UINT16 v = ((((UINT16)r) << 8) & 0xf800) + - ((((UINT16)g) << 3) & 0x07e0) + - ((((UINT16)b) >> 3) & 0x001f); - ink[0] = (UINT8)v; - ink[1] = (UINT8)(v >> 8); - ink[2] = ink[3] = 0; - return ink; - } else if (!strcmp(im->mode, "BGR;24")) { - ink[0] = (UINT8)b; - ink[1] = (UINT8)g; - ink[2] = (UINT8)r; - ink[3] = 0; - return ink; - } - } + ink[0] = (UINT8)r; + ink[1] = (UINT8)(r >> 8); + ink[2] = ink[3] = 0; + return ink; } PyErr_SetString(PyExc_ValueError, wrong_mode); @@ -650,7 +688,7 @@ getink(PyObject *color, Imaging im, char *ink) { static PyObject * _fill(PyObject *self, PyObject *args) { - char *mode; + char *mode_name; int xsize, ysize; PyObject *color; char buffer[4]; @@ -659,10 +697,12 @@ _fill(PyObject *self, PyObject *args) { xsize = ysize = 256; color = NULL; - if (!PyArg_ParseTuple(args, "s|(ii)O", &mode, &xsize, &ysize, &color)) { + if (!PyArg_ParseTuple(args, "s|(ii)O", &mode_name, &xsize, &ysize, &color)) { return NULL; } + const ModeID mode = findModeID(mode_name); + im = ImagingNewDirty(mode, xsize, ysize); if (!im) { return NULL; @@ -683,47 +723,55 @@ _fill(PyObject *self, PyObject *args) { static PyObject * _new(PyObject *self, PyObject *args) { - char *mode; + char *mode_name; int xsize, ysize; - if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { + if (!PyArg_ParseTuple(args, "s(ii)", &mode_name, &xsize, &ysize)) { return NULL; } + const ModeID mode = findModeID(mode_name); + return PyImagingNew(ImagingNew(mode, xsize, ysize)); } static PyObject * _new_block(PyObject *self, PyObject *args) { - char *mode; + char *mode_name; int xsize, ysize; - if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { + if (!PyArg_ParseTuple(args, "s(ii)", &mode_name, &xsize, &ysize)) { return NULL; } + const ModeID mode = findModeID(mode_name); + return PyImagingNew(ImagingNewBlock(mode, xsize, ysize)); } static PyObject * _linear_gradient(PyObject *self, PyObject *args) { - char *mode; + char *mode_name; - if (!PyArg_ParseTuple(args, "s", &mode)) { + if (!PyArg_ParseTuple(args, "s", &mode_name)) { return NULL; } + const ModeID mode = findModeID(mode_name); + return PyImagingNew(ImagingFillLinearGradient(mode)); } static PyObject * _radial_gradient(PyObject *self, PyObject *args) { - char *mode; + char *mode_name; - if (!PyArg_ParseTuple(args, "s", &mode)) { + if (!PyArg_ParseTuple(args, "s", &mode_name)) { return NULL; } + const ModeID mode = findModeID(mode_name); + return PyImagingNew(ImagingFillRadialGradient(mode)); } @@ -733,7 +781,8 @@ _alpha_composite(ImagingObject *self, PyObject *args) { ImagingObject *imagep2; if (!PyArg_ParseTuple( - args, "O!O!", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2)) { + args, "O!O!", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2 + )) { return NULL; } @@ -748,7 +797,8 @@ _blend(ImagingObject *self, PyObject *args) { alpha = 0.5; if (!PyArg_ParseTuple( - args, "O!O!|d", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2, &alpha)) { + args, "O!O!|d", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2, &alpha + )) { return NULL; } @@ -827,7 +877,8 @@ _prepare_lut_table(PyObject *table, Py_ssize_t table_size) { break; case TYPE_FLOAT32: memcpy( - &item, ((char *)table_data) + i * sizeof(FLOAT32), sizeof(FLOAT32)); + &item, ((char *)table_data) + i * sizeof(FLOAT32), sizeof(FLOAT32) + ); break; case TYPE_DOUBLE: memcpy(&dtmp, ((char *)table_data) + i * sizeof(dtmp), sizeof(dtmp)); @@ -860,7 +911,7 @@ _prepare_lut_table(PyObject *table, Py_ssize_t table_size) { static PyObject * _color_lut_3d(ImagingObject *self, PyObject *args) { - char *mode; + char *mode_name; int filter; int table_channels; int size1D, size2D, size3D; @@ -871,17 +922,20 @@ _color_lut_3d(ImagingObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "siiiiiO:color_lut_3d", - &mode, + "sii(iii)O:color_lut_3d", + &mode_name, &filter, &table_channels, &size1D, &size2D, &size3D, - &table)) { + &table + )) { return NULL; } + const ModeID mode = findModeID(mode_name); + /* actually, it is trilinear */ if (filter != IMAGING_TRANSFORM_BILINEAR) { PyErr_SetString(PyExc_ValueError, "Only LINEAR filter is supported."); @@ -896,7 +950,8 @@ _color_lut_3d(ImagingObject *self, PyObject *args) { 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"); + PyExc_ValueError, "Table size in any dimension should be from 2 to 65" + ); return NULL; } @@ -913,13 +968,8 @@ _color_lut_3d(ImagingObject *self, PyObject *args) { } if (!ImagingColorLUT3D_linear( - imOut, - self->image, - table_channels, - size1D, - size2D, - size3D, - prepared_table)) { + imOut, self->image, table_channels, size1D, size2D, size3D, prepared_table + )) { free(prepared_table); ImagingDelete(imOut); return NULL; @@ -932,18 +982,19 @@ _color_lut_3d(ImagingObject *self, PyObject *args) { static PyObject * _convert(ImagingObject *self, PyObject *args) { - char *mode; + char *mode_name; int dither = 0; ImagingObject *paletteimage = NULL; - if (!PyArg_ParseTuple(args, "s|iO", &mode, &dither, &paletteimage)) { + if (!PyArg_ParseTuple(args, "s|iO", &mode_name, &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'"); + PyExc_ValueError, "palette argument must be image with mode 'P'" + ); return NULL; } if (paletteimage->image->palette == NULL) { @@ -952,8 +1003,11 @@ _convert(ImagingObject *self, PyObject *args) { } } + const ModeID mode = findModeID(mode_name); + return PyImagingNew(ImagingConvert( - self->image, mode, paletteimage ? paletteimage->image->palette : NULL, dither)); + self->image, mode, paletteimage ? paletteimage->image->palette : NULL, dither + )); } static PyObject * @@ -961,7 +1015,8 @@ _convert2(ImagingObject *self, PyObject *args) { ImagingObject *imagep1; ImagingObject *imagep2; if (!PyArg_ParseTuple( - args, "O!O!", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2)) { + args, "O!O!", &Imaging_Type, &imagep1, &Imaging_Type, &imagep2 + )) { return NULL; } @@ -969,20 +1024,19 @@ _convert2(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * _convert_matrix(ImagingObject *self, PyObject *args) { - char *mode; + char *mode_name; 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_name, m + 0, m + 1, m + 2, m + 3)) { PyErr_Clear(); if (!PyArg_ParseTuple( args, "s(ffffffffffff)", - &mode, + &mode_name, m + 0, m + 1, m + 2, @@ -994,23 +1048,28 @@ _convert_matrix(ImagingObject *self, PyObject *args) { m + 8, m + 9, m + 10, - m + 11)) { + m + 11 + )) { return NULL; } } + const ModeID mode = findModeID(mode_name); + return PyImagingNew(ImagingConvertMatrix(self->image, mode, m)); } static PyObject * _convert_transparent(ImagingObject *self, PyObject *args) { - char *mode; + char *mode_name; int r, g, b; - if (PyArg_ParseTuple(args, "s(iii)", &mode, &r, &g, &b)) { + if (PyArg_ParseTuple(args, "s(iii)", &mode_name, &r, &g, &b)) { + const ModeID mode = findModeID(mode_name); return PyImagingNew(ImagingConvertTransparent(self->image, mode, r, g, b)); } PyErr_Clear(); - if (PyArg_ParseTuple(args, "si", &mode, &r)) { + if (PyArg_ParseTuple(args, "si", &mode_name, &r)) { + const ModeID mode = findModeID(mode_name); return PyImagingNew(ImagingConvertTransparent(self->image, mode, r, 0, 0)); } return NULL; @@ -1018,10 +1077,6 @@ _convert_transparent(ImagingObject *self, PyObject *args) { static PyObject * _copy(ImagingObject *self, PyObject *args) { - if (!PyArg_ParseTuple(args, "")) { - return NULL; - } - return PyImagingNew(ImagingCopy(self->image)); } @@ -1055,7 +1110,8 @@ _filter(ImagingObject *self, PyObject *args) { float divisor, offset; PyObject *kernel = NULL; if (!PyArg_ParseTuple( - args, "(ii)ffO", &xsize, &ysize, &divisor, &offset, &kernel)) { + args, "(ii)ffO", &xsize, &ysize, &divisor, &offset, &kernel + )) { return NULL; } @@ -1080,7 +1136,6 @@ _filter(ImagingObject *self, PyObject *args) { return imOut; } -#ifdef WITH_UNSHARPMASK static PyObject * _gaussian_blur(ImagingObject *self, PyObject *args) { Imaging imIn; @@ -1105,7 +1160,6 @@ _gaussian_blur(ImagingObject *self, PyObject *args) { return PyImagingNew(imOut); } -#endif static PyObject * _getpalette(ImagingObject *self, PyObject *args) { @@ -1114,9 +1168,9 @@ _getpalette(ImagingObject *self, PyObject *args) { int bits; ImagingShuffler pack; - char *mode = "RGB"; - char *rawmode = "RGB"; - if (!PyArg_ParseTuple(args, "|ss", &mode, &rawmode)) { + char *mode_name = "RGB"; + char *rawmode_name = "RGB"; + if (!PyArg_ParseTuple(args, "|ss", &mode_name, &rawmode_name)) { return NULL; } @@ -1125,6 +1179,9 @@ _getpalette(ImagingObject *self, PyObject *args) { return NULL; } + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); + pack = ImagingFindPacker(mode, rawmode, &bits); if (!pack) { PyErr_SetString(PyExc_ValueError, wrong_raw_mode); @@ -1138,7 +1195,8 @@ _getpalette(ImagingObject *self, PyObject *args) { } pack( - (UINT8 *)PyBytes_AsString(palette), self->image->palette->palette, palettesize); + (UINT8 *)PyBytes_AsString(palette), self->image->palette->palette, palettesize + ); return palette; } @@ -1150,7 +1208,7 @@ _getpalettemode(ImagingObject *self) { return NULL; } - return PyUnicode_FromString(self->image->palette->mode); + return PyUnicode_FromString(getModeData(self->image->palette->mode)->name); } static inline int @@ -1170,7 +1228,9 @@ _getxy(PyObject *xy, int *x, int *y) { PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); if (int_value != NULL && PyLong_Check(int_value)) { *x = PyLong_AS_LONG(int_value); + Py_DECREF(int_value); } else { + Py_XDECREF(int_value); goto badval; } } @@ -1184,7 +1244,9 @@ _getxy(PyObject *xy, int *x, int *y) { PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); if (int_value != NULL && PyLong_Check(int_value)) { *y = PyLong_AS_LONG(int_value); + Py_DECREF(int_value); } else { + Py_XDECREF(int_value); goto badval; } } @@ -1217,8 +1279,7 @@ _getpixel(ImagingObject *self, PyObject *args) { } if (self->access == NULL) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return getpixel(self->image, self->access, x, y); @@ -1232,7 +1293,8 @@ union hist_extrema { static union hist_extrema * parse_histogram_extremap( - ImagingObject *self, PyObject *extremap, union hist_extrema *ep) { + ImagingObject *self, PyObject *extremap, union hist_extrema *ep +) { int i0, i1; double f0, f1; @@ -1361,7 +1423,6 @@ _entropy(ImagingObject *self, PyObject *args) { return PyFloat_FromDouble(-entropy); } -#ifdef WITH_MODEFILTER static PyObject * _modefilter(ImagingObject *self, PyObject *args) { int size; @@ -1371,7 +1432,6 @@ _modefilter(ImagingObject *self, PyObject *args) { return PyImagingNew(ImagingModeFilter(self->image, size)); } -#endif static PyObject * _offset(ImagingObject *self, PyObject *args) { @@ -1392,7 +1452,8 @@ _paste(ImagingObject *self, PyObject *args) { int x0, y0, x1, y1; ImagingObject *maskp = NULL; if (!PyArg_ParseTuple( - args, "O(iiii)|O!", &source, &x0, &y0, &x1, &y1, &Imaging_Type, &maskp)) { + args, "O(iiii)|O!", &source, &x0, &y0, &x1, &y1, &Imaging_Type, &maskp + )) { return NULL; } @@ -1404,22 +1465,23 @@ _paste(ImagingObject *self, PyObject *args) { x0, y0, x1, - y1); + y1 + ); } 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) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1431,12 +1493,14 @@ _point(ImagingObject *self, PyObject *args) { Imaging im; PyObject *list; - char *mode; - if (!PyArg_ParseTuple(args, "Oz", &list, &mode)) { + char *mode_name; + if (!PyArg_ParseTuple(args, "Oz", &list, &mode_name)) { return NULL; } - if (mode && !strcmp(mode, "F")) { + const ModeID mode = findModeID(mode_name); + + if (mode == IMAGING_MODE_F) { FLOAT32 *data; /* map from 8-bit data to floating point */ @@ -1447,8 +1511,7 @@ _point(ImagingObject *self, PyObject *args) { } im = ImagingPoint(self->image, mode, (void *)data); free(data); - - } else if (!strcmp(self->image->mode, "I") && mode && !strcmp(mode, "L")) { + } else if (self->image->mode == IMAGING_MODE_I && mode == IMAGING_MODE_L) { UINT8 *data; /* map from 16-bit subset of 32-bit data to 8-bit */ @@ -1460,7 +1523,6 @@ _point(ImagingObject *self, PyObject *args) { } im = ImagingPoint(self->image, mode, (void *)data); free(data); - } else { INT32 *data; UINT8 lut[1024]; @@ -1481,7 +1543,7 @@ _point(ImagingObject *self, PyObject *args) { return NULL; } - if (mode && !strcmp(mode, "I")) { + if (mode == IMAGING_MODE_I) { im = ImagingPoint(self->image, mode, (void *)data); } else if (mode && bands > 1) { for (i = 0; i < 256; i++) { @@ -1583,57 +1645,33 @@ _putdata(ImagingObject *self, PyObject *args) { return NULL; } double value; - if (image->bands == 1) { - int bigendian = 0; - if (image->type == IMAGING_TYPE_SPECIAL) { - // I;16* - if (strcmp(image->mode, "I;16N") == 0) { + int bigendian = 0; + if (image->type == IMAGING_TYPE_SPECIAL) { + // I;16* + if ( + image->mode == IMAGING_MODE_I_16B #ifdef WORDS_BIGENDIAN - bigendian = 1; -#else - bigendian = 0; + || image->mode == IMAGING_MODE_I_16N #endif - } else if (strcmp(image->mode, "I;16B") == 0) { - bigendian = 1; - } else { - bigendian = 0; - } + ) { + bigendian = 1; } - for (i = x = y = 0; i < n; i++) { - set_value_to_item(seq, i); - if (scale != 1.0 || offset != 0.0) { - value = value * scale + offset; - } - if (image->type == IMAGING_TYPE_SPECIAL) { - image->image8[y][x * 2 + (bigendian ? 1 : 0)] = - CLIP8((int)value % 256); - image->image8[y][x * 2 + (bigendian ? 0 : 1)] = - CLIP8((int)value >> 8); - } else { - image->image8[y][x] = (UINT8)CLIP8(value); - } - if (++x >= (int)image->xsize) { - x = 0, y++; - } + } + for (i = x = y = 0; i < n; i++) { + set_value_to_item(seq, i); + if (scale != 1.0 || offset != 0.0) { + value = value * scale + offset; } - } else { - // BGR;* - int b; - for (i = x = y = 0; i < n; i++) { - char ink[4]; - - op = PySequence_Fast_GET_ITEM(seq, i); - if (!op || !getink(op, image, ink)) { - Py_DECREF(seq); - return NULL; - } - /* FIXME: what about scale and offset? */ - for (b = 0; b < image->pixelsize; b++) { - image->image8[y][x * image->pixelsize + b] = ink[b]; - } - if (++x >= (int)image->xsize) { - x = 0, y++; - } + if (image->type == IMAGING_TYPE_SPECIAL) { + image->image8[y][x * 2 + (bigendian ? 1 : 0)] = + CLIP8((int)value % 256); + image->image8[y][x * 2 + (bigendian ? 0 : 1)] = + CLIP8((int)value >> 8); + } else { + image->image8[y][x] = (UINT8)CLIP8(value); + } + if (++x >= (int)image->xsize) { + x = 0, y++; } } PyErr_Clear(); /* Avoid weird exceptions */ @@ -1696,12 +1734,9 @@ _putdata(ImagingObject *self, PyObject *args) { Py_XDECREF(seq); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } -#ifdef WITH_QUANTIZE - static PyObject * _quantize(ImagingObject *self, PyObject *args) { int colours = 256; @@ -1713,32 +1748,46 @@ _quantize(ImagingObject *self, PyObject *args) { 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(IMAGING_MODE_P, self->image->xsize, self->image->ysize) + ); } return PyImagingNew(ImagingQuantize(self->image, colours, method, kmeans)); } -#endif static PyObject * _putpalette(ImagingObject *self, PyObject *args) { ImagingShuffler unpack; int bits; - char *palette_mode, *rawmode; + char *palette_mode_name, *rawmode_name; UINT8 *palette; Py_ssize_t palettesize; if (!PyArg_ParseTuple( - args, "ssy#", &palette_mode, &rawmode, &palette, &palettesize)) { + args, "ssy#", &palette_mode_name, &rawmode_name, &palette, &palettesize + )) { + return NULL; + } + + if (self->image->mode != IMAGING_MODE_L && self->image->mode != IMAGING_MODE_LA && + self->image->mode != IMAGING_MODE_P && self->image->mode != IMAGING_MODE_PA) { + PyErr_SetString(PyExc_ValueError, wrong_mode); return NULL; } - if (strcmp(self->image->mode, "L") && strcmp(self->image->mode, "LA") && - strcmp(self->image->mode, "P") && strcmp(self->image->mode, "PA")) { + const ModeID palette_mode = findModeID(palette_mode_name); + if (palette_mode == IMAGING_MODE_UNKNOWN) { PyErr_SetString(PyExc_ValueError, wrong_mode); return NULL; } + const RawModeID rawmode = findRawModeID(rawmode_name); + if (rawmode == IMAGING_RAWMODE_UNKNOWN) { + PyErr_SetString(PyExc_ValueError, wrong_raw_mode); + return NULL; + } + unpack = ImagingFindUnpacker(palette_mode, rawmode, &bits); if (!unpack) { PyErr_SetString(PyExc_ValueError, wrong_raw_mode); @@ -1752,15 +1801,20 @@ _putpalette(ImagingObject *self, PyObject *args) { ImagingPaletteDelete(self->image->palette); - strcpy(self->image->mode, strlen(self->image->mode) == 2 ? "PA" : "P"); + if (self->image->mode == IMAGING_MODE_LA) { + self->image->mode = IMAGING_MODE_PA; + } else if (self->image->mode == IMAGING_MODE_L) { + self->image->mode = IMAGING_MODE_P; + } else { + // The image already has a palette mode so we don't need to change it. + } self->image->palette = ImagingPaletteNew(palette_mode); self->image->palette->size = palettesize * 8 / bits; unpack(self->image->palette->palette, palette, self->image->palette->size); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1781,11 +1835,10 @@ _putpalettealpha(ImagingObject *self, PyObject *args) { return NULL; } - strcpy(self->image->palette->mode, "RGBA"); + self->image->palette->mode = IMAGING_MODE_RGBA; self->image->palette->palette[index * 4 + 3] = (UINT8)alpha; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1807,13 +1860,12 @@ _putpalettealphas(ImagingObject *self, PyObject *args) { return NULL; } - strcpy(self->image->palette->mode, "RGBA"); + self->image->palette->mode = IMAGING_MODE_RGBA; for (i = 0; i < length; i++) { self->image->palette->palette[i * 4 + 3] = (UINT8)values[i]; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1849,11 +1901,9 @@ _putpixel(ImagingObject *self, PyObject *args) { self->access->put_pixel(im, x, y, ink); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } -#ifdef WITH_RANKFILTER static PyObject * _rankfilter(ImagingObject *self, PyObject *args) { int size, rank; @@ -1863,7 +1913,6 @@ _rankfilter(ImagingObject *self, PyObject *args) { return PyImagingNew(ImagingRankFilter(self->image, size, rank)); } -#endif static PyObject * _resize(ImagingObject *self, PyObject *args) { @@ -1887,7 +1936,8 @@ _resize(ImagingObject *self, PyObject *args) { &box[0], &box[1], &box[2], - &box[3])) { + &box[3] + )) { return NULL; } @@ -1923,7 +1973,8 @@ _resize(ImagingObject *self, PyObject *args) { imOut = ImagingNewDirty(imIn->mode, xsize, ysize); imOut = ImagingTransform( - imOut, imIn, IMAGING_TRANSFORM_AFFINE, 0, 0, xsize, ysize, a, filter, 1); + imOut, imIn, IMAGING_TRANSFORM_AFFINE, 0, 0, xsize, ysize, a, filter, 1 + ); } else { imOut = ImagingResample(imIn, xsize, ysize, filter, box); } @@ -1944,14 +1995,8 @@ _reduce(ImagingObject *self, PyObject *args) { box[3] = imIn->ysize; if (!PyArg_ParseTuple( - args, - "(ii)|(iiii)", - &xscale, - &yscale, - &box[0], - &box[1], - &box[2], - &box[3])) { + args, "(ii)|(iiii)", &xscale, &yscale, &box[0], &box[1], &box[2], &box[3] + )) { return NULL; } @@ -1983,8 +2028,11 @@ _reduce(ImagingObject *self, PyObject *args) { return PyImagingNew(imOut); } -#define IS_RGB(mode) \ - (!strcmp(mode, "RGB") || !strcmp(mode, "RGBA") || !strcmp(mode, "RGBX")) +static int +isRGB(const ModeID mode) { + return mode == IMAGING_MODE_RGB || mode == IMAGING_MODE_RGBA || + mode == IMAGING_MODE_RGBX; +} static PyObject * im_setmode(ImagingObject *self, PyObject *args) { @@ -1992,23 +2040,25 @@ im_setmode(ImagingObject *self, PyObject *args) { Imaging im; - char *mode; + char *mode_name; Py_ssize_t modelen; - if (!PyArg_ParseTuple(args, "s#:setmode", &mode, &modelen)) { + if (!PyArg_ParseTuple(args, "s#:setmode", &mode_name, &modelen)) { return NULL; } + const ModeID mode = findModeID(mode_name); + im = self->image; /* move all logic in here to the libImaging primitive */ - if (!strcmp(im->mode, mode)) { + if (im->mode == mode) { ; /* same mode; always succeeds */ - } else if (IS_RGB(im->mode) && IS_RGB(mode)) { + } else if (isRGB(im->mode) && isRGB(mode)) { /* color to color */ - strcpy(im->mode, mode); + im->mode = mode; im->bands = modelen; - if (!strcmp(mode, "RGBA")) { + if (mode == IMAGING_MODE_RGBA) { (void)ImagingFillBand(im, 3, 255); } } else { @@ -2023,8 +2073,7 @@ im_setmode(ImagingObject *self, PyObject *args) { } self->access = ImagingAccessNew(im); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2053,7 +2102,8 @@ _transform(ImagingObject *self, PyObject *args) { &method, &data, &filter, - &fill)) { + &fill + )) { return NULL; } @@ -2077,7 +2127,8 @@ _transform(ImagingObject *self, PyObject *args) { } 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); @@ -2085,8 +2136,7 @@ _transform(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2147,7 +2197,6 @@ _transpose(ImagingObject *self, PyObject *args) { return PyImagingNew(imOut); } -#ifdef WITH_UNSHARPMASK static PyObject * _unsharp_mask(ImagingObject *self, PyObject *args) { Imaging imIn; @@ -2166,12 +2215,12 @@ _unsharp_mask(ImagingObject *self, PyObject *args) { } if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) { + ImagingDelete(imOut); return NULL; } return PyImagingNew(imOut); } -#endif static PyObject * _box_blur(ImagingObject *self, PyObject *args) { @@ -2215,8 +2264,7 @@ _getbbox(ImagingObject *self, PyObject *args) { } if (!ImagingGetBBox(self->image, bbox, alpha_only)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return Py_BuildValue("iiii", bbox[0], bbox[1], bbox[2], bbox[3]); @@ -2250,7 +2298,13 @@ _getcolors(ImagingObject *self, PyObject *args) { 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)); + "iN", v->count, getpixel(self->image, self->access, v->x, v->y) + ); + if (item == NULL) { + Py_DECREF(out); + free(items); + return NULL; + } PyList_SetItem(out, i, item); } } @@ -2284,14 +2338,13 @@ _getextrema(ImagingObject *self) { 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) { + if (self->image->mode == IMAGING_MODE_I_16) { return Py_BuildValue("HH", extrema.s[0], extrema.s[1]); } } } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2311,14 +2364,16 @@ _getprojection(ImagingObject *self) { } ImagingGetProjection( - self->image, (unsigned char *)xprofile, (unsigned char *)yprofile); + self->image, (unsigned char *)xprofile, (unsigned char *)yprofile + ); result = Py_BuildValue( "y#y#", xprofile, (Py_ssize_t)self->image->xsize, yprofile, - (Py_ssize_t)self->image->ysize); + (Py_ssize_t)self->image->ysize + ); free(xprofile); free(yprofile); @@ -2352,8 +2407,7 @@ _fillband(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2368,13 +2422,12 @@ _putband(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * _merge(PyObject *self, PyObject *args) { - char *mode; + char *mode_name; ImagingObject *band0 = NULL; ImagingObject *band1 = NULL; ImagingObject *band2 = NULL; @@ -2384,7 +2437,7 @@ _merge(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, "sO!|O!O!O!", - &mode, + &mode_name, &Imaging_Type, &band0, &Imaging_Type, @@ -2392,10 +2445,13 @@ _merge(PyObject *self, PyObject *args) { &Imaging_Type, &band2, &Imaging_Type, - &band3)) { + &band3 + )) { return NULL; } + const ModeID mode = findModeID(mode_name); + if (band0) { bands[0] = band0->image; } @@ -2409,12 +2465,16 @@ _merge(PyObject *self, PyObject *args) { bands[3] = band3->image; } - return PyImagingNew(ImagingMerge(mode, bands)); + Imaging imOut = ImagingMerge(mode, bands); + if (!imOut) { + return NULL; + } + ImagingCopyPalette(imOut, bands[0]); + return PyImagingNew(imOut); } static PyObject * _split(ImagingObject *self) { - int fails = 0; Py_ssize_t i; PyObject *list; PyObject *imaging_object; @@ -2425,23 +2485,22 @@ _split(ImagingObject *self) { } list = PyTuple_New(self->image->bands); + if (!list) { + return NULL; + } for (i = 0; i < self->image->bands; i++) { imaging_object = PyImagingNew(bands[i]); if (!imaging_object) { - fails += 1; + Py_DECREF(list); + list = NULL; + break; } PyTuple_SET_ITEM(list, i, imaging_object); } - if (fails) { - Py_DECREF(list); - list = NULL; - } return list; } -/* -------------------------------------------------------------------- */ - -#ifdef WITH_IMAGECHOPS +/* Channel operations (ImageChops) ------------------------------------ */ static PyObject * _chop_invert(ImagingObject *self) { @@ -2622,11 +2681,8 @@ _chop_overlay(ImagingObject *self, PyObject *args) { return PyImagingNew(ImagingOverlay(self->image, imagep->image)); } -#endif - -/* -------------------------------------------------------------------- */ -#ifdef WITH_IMAGEDRAW +/* Fonts (ImageDraw and ImageFont) ------------------------------------ */ static PyObject * _font_new(PyObject *self_, PyObject *args) { @@ -2638,7 +2694,8 @@ _font_new(PyObject *self_, PyObject *args) { unsigned char *glyphdata; Py_ssize_t glyphdata_length; if (!PyArg_ParseTuple( - args, "O!y#", &Imaging_Type, &imagep, &glyphdata, &glyphdata_length)) { + args, "O!y#", &Imaging_Type, &imagep, &glyphdata, &glyphdata_length + )) { return NULL; } @@ -2796,7 +2853,8 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { if (i == 0 || text[i] != text[i - 1]) { ImagingDelete(bitmap); bitmap = ImagingCrop( - self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1); + self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1 + ); if (!bitmap) { goto failed; } @@ -2808,7 +2866,8 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { glyph->dx0 + x, glyph->dy0 + b, glyph->dx1 + x, - glyph->dy1 + b); + glyph->dy1 + b + ); if (status < 0) { goto failed; } @@ -2852,7 +2911,7 @@ static struct PyMethodDef _font_methods[] = { {NULL, NULL} /* sentinel */ }; -/* -------------------------------------------------------------------- */ +/* Graphics (ImageDraw) ----------------------------------------------- */ static PyObject * _draw_new(PyObject *self_, PyObject *args) { @@ -2947,7 +3006,8 @@ _draw_arc(ImagingDrawObject *self, PyObject *args) { end, &ink, width, - self->blend); + self->blend + ); free(xy); @@ -2955,8 +3015,7 @@ _draw_arc(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2977,13 +3036,15 @@ _draw_bitmap(ImagingDrawObject *self, PyObject *args) { } if (n != 1) { PyErr_SetString( - PyExc_TypeError, "coordinate list must contain exactly 1 coordinate"); + 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); @@ -2991,8 +3052,7 @@ _draw_bitmap(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3039,7 +3099,8 @@ _draw_chord(ImagingDrawObject *self, PyObject *args) { &ink, fill, width, - self->blend); + self->blend + ); free(xy); @@ -3047,8 +3108,7 @@ _draw_chord(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3093,7 +3153,8 @@ _draw_ellipse(ImagingDrawObject *self, PyObject *args) { &ink, fill, width, - self->blend); + self->blend + ); free(xy); @@ -3101,8 +3162,7 @@ _draw_ellipse(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3133,14 +3193,16 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) { (int)p[2], (int)p[3], &ink, - self->blend) < 0) { + self->blend + ) < 0) { free(xy); return NULL; } } 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++) { @@ -3153,7 +3215,9 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) { (int)p[3], &ink, width, - self->blend) < 0) { + self->blend, + NULL + ) < 0) { free(xy); return NULL; } @@ -3162,8 +3226,7 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) { free(xy); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3185,7 +3248,8 @@ _draw_points(ImagingDrawObject *self, PyObject *args) { 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) { + self->image->image, (int)p[0], (int)p[1], &ink, self->blend + ) < 0) { free(xy); return NULL; } @@ -3193,12 +3257,9 @@ _draw_points(ImagingDrawObject *self, PyObject *args) { free(xy); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } -#ifdef WITH_ARROW - /* from outline.c */ extern ImagingOutline PyOutline_AsOutline(PyObject *outline); @@ -3224,12 +3285,9 @@ _draw_outline(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } -#endif - static PyObject * _draw_pieslice(ImagingDrawObject *self, PyObject *args) { double *xy; @@ -3274,7 +3332,8 @@ _draw_pieslice(ImagingDrawObject *self, PyObject *args) { &ink, fill, width, - self->blend); + self->blend + ); free(xy); @@ -3282,8 +3341,7 @@ _draw_pieslice(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3296,7 +3354,10 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) { int ink; int fill = 0; int width = 0; - if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) { + ImagingObject *maskp = NULL; + if (!PyArg_ParseTuple( + args, "Oi|iiO!", &data, &ink, &fill, &width, &Imaging_Type, &maskp + )) { return NULL; } @@ -3306,7 +3367,8 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) { } if (n < 2) { PyErr_SetString( - PyExc_TypeError, "coordinate list must contain at least 2 coordinates"); + PyExc_TypeError, "coordinate list must contain at least 2 coordinates" + ); free(xy); return NULL; } @@ -3325,16 +3387,23 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) { free(xy); - if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, width, self->blend) < - 0) { + if (ImagingDrawPolygon( + self->image->image, + n, + ixy, + &ink, + fill, + width, + self->blend, + maskp ? maskp->image : NULL + ) < 0) { free(ixy); return NULL; } free(ixy); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3379,7 +3448,8 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { &ink, fill, width, - self->blend); + self->blend + ); free(xy); @@ -3387,17 +3457,13 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static struct PyMethodDef _draw_methods[] = { -#ifdef WITH_IMAGEDRAW /* Graphics (ImageDraw) */ {"draw_lines", (PyCFunction)_draw_lines, METH_VARARGS}, -#ifdef WITH_ARROW {"draw_outline", (PyCFunction)_draw_outline, METH_VARARGS}, -#endif {"draw_polygon", (PyCFunction)_draw_polygon, METH_VARARGS}, {"draw_rectangle", (PyCFunction)_draw_rectangle, METH_VARARGS}, {"draw_points", (PyCFunction)_draw_points, METH_VARARGS}, @@ -3407,12 +3473,9 @@ static struct PyMethodDef _draw_methods[] = { {"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) { PixelAccessObject *self; @@ -3493,11 +3556,9 @@ pixel_access_setitem(PixelAccessObject *self, PyObject *xy, PyObject *color) { } /* -------------------------------------------------------------------- */ -/* EFFECTS (experimental) */ +/* EFFECTS (experimental) */ /* -------------------------------------------------------------------- */ -#ifdef WITH_EFFECTS - static PyObject * _effect_mandelbrot(ImagingObject *self, PyObject *args) { int xsize = 512; @@ -3519,7 +3580,8 @@ _effect_mandelbrot(ImagingObject *self, PyObject *args) { &extent[1], &extent[2], &extent[3], - &quality)) { + &quality + )) { return NULL; } @@ -3548,8 +3610,6 @@ _effect_spread(ImagingObject *self, PyObject *args) { return PyImagingNew(ImagingEffectSpread(self->image, dist)); } -#endif - /* -------------------------------------------------------------------- */ /* UTILITIES */ /* -------------------------------------------------------------------- */ @@ -3602,8 +3662,7 @@ _save_ppm(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /* -------------------------------------------------------------------- */ @@ -3630,20 +3689,14 @@ static struct PyMethodDef methods[] = { {"filter", (PyCFunction)_filter, METH_VARARGS}, {"histogram", (PyCFunction)_histogram, METH_VARARGS}, {"entropy", (PyCFunction)_entropy, METH_VARARGS}, -#ifdef WITH_MODEFILTER {"modefilter", (PyCFunction)_modefilter, METH_VARARGS}, -#endif {"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, METH_VARARGS}, -#endif -#ifdef WITH_RANKFILTER {"rankfilter", (PyCFunction)_rankfilter, METH_VARARGS}, -#endif {"resize", (PyCFunction)_resize, METH_VARARGS}, {"reduce", (PyCFunction)_reduce, METH_VARARGS}, {"transpose", (PyCFunction)_transpose, METH_VARARGS}, @@ -3669,7 +3722,6 @@ static struct PyMethodDef methods[] = { {"putpalettealpha", (PyCFunction)_putpalettealpha, METH_VARARGS}, {"putpalettealphas", (PyCFunction)_putpalettealphas, METH_VARARGS}, -#ifdef WITH_IMAGECHOPS /* Channel operations (ImageChops) */ {"chop_invert", (PyCFunction)_chop_invert, METH_NOARGS}, {"chop_lighter", (PyCFunction)_chop_lighter, METH_VARARGS}, @@ -3688,26 +3740,21 @@ static struct PyMethodDef methods[] = { {"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 */ + /* Unsharpmask extension */ {"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS}, {"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS}, -#endif - {"box_blur", (PyCFunction)_box_blur, METH_VARARGS}, -#ifdef WITH_EFFECTS /* Special effects */ {"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS}, -#endif /* Misc. */ - {"new_block", (PyCFunction)_new_block, METH_VARARGS}, - {"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS}, + /* arrow */ + {"__arrow_c_schema__", (PyCFunction)ExportArrowSchemaPyCapsule, METH_VARARGS}, + {"__arrow_c_array__", (PyCFunction)ExportArrowArrayPyCapsule, METH_VARARGS}, + {NULL, NULL} /* sentinel */ }; @@ -3715,7 +3762,7 @@ static struct PyMethodDef methods[] = { static PyObject * _getattr_mode(ImagingObject *self, void *closure) { - return PyUnicode_FromString(self->image->mode); + return PyUnicode_FromString(getModeData(self->image->mode)->name); } static PyObject * @@ -3728,36 +3775,36 @@ _getattr_bands(ImagingObject *self, void *closure) { return PyLong_FromLong(self->image->bands); } -static PyObject * -_getattr_id(ImagingObject *self, void *closure) { - return PyLong_FromSsize_t((Py_ssize_t)self->image); +static void +_ptr_destructor(PyObject *capsule) { + PyObject *self = (PyObject *)PyCapsule_GetContext(capsule); + Py_DECREF(self); } static PyObject * _getattr_ptr(ImagingObject *self, void *closure) { - return PyCapsule_New(self->image, IMAGING_MAGIC, NULL); + PyObject *capsule = PyCapsule_New(self->image, IMAGING_MAGIC, _ptr_destructor); + if (!capsule) { + return NULL; + } + Py_INCREF(self); + PyCapsule_SetContext(capsule, self); + return capsule; } 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); +_getattr_readonly(ImagingObject *self, void *closure) { + return PyLong_FromLong(self->image->read_only); } 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}}; + {"readonly", (getter)_getattr_readonly}, + {NULL} +}; /* basic sequence semantics */ @@ -3796,108 +3843,28 @@ static PySequenceMethods image_as_sequence = { /* type description */ static PyTypeObject Imaging_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingCore", /*tp_name*/ - sizeof(ImagingObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 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*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingCore", + .tp_basicsize = sizeof(ImagingObject), + .tp_dealloc = (destructor)_dealloc, + .tp_as_sequence = &image_as_sequence, + .tp_methods = methods, + .tp_getset = getsetters, }; -#ifdef WITH_IMAGEDRAW - static PyTypeObject ImagingFont_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingFont", /*tp_name*/ - sizeof(ImagingFontObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_font_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 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*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingFont", + .tp_basicsize = sizeof(ImagingFontObject), + .tp_dealloc = (destructor)_font_dealloc, + .tp_methods = _font_methods, }; static PyTypeObject ImagingDraw_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingDraw", /*tp_name*/ - sizeof(ImagingDrawObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_draw_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 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*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingDraw", + .tp_basicsize = sizeof(ImagingDrawObject), + .tp_dealloc = (destructor)_draw_dealloc, + .tp_methods = _draw_methods, }; -#endif - static PyMappingMethods pixel_access_as_mapping = { (lenfunc)NULL, /*mp_length*/ (binaryfunc)pixel_access_getitem, /*mp_subscript*/ @@ -3907,20 +3874,10 @@ static PyMappingMethods pixel_access_as_mapping = { /* type description */ static PyTypeObject PixelAccess_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "PixelAccess", /*tp_name*/ - sizeof(PixelAccessObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)pixel_access_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - &pixel_access_as_mapping, /*tp_as_mapping*/ - 0 /*tp_hash*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "PixelAccess", + .tp_basicsize = sizeof(PixelAccessObject), + .tp_dealloc = (destructor)pixel_access_dealloc, + .tp_as_mapping = &pixel_access_as_mapping, }; /* -------------------------------------------------------------------- */ @@ -3929,7 +3886,6 @@ static PyObject * _get_stats(PyObject *self, PyObject *args) { PyObject *d; PyObject *v; - ImagingMemoryArena arena = &ImagingDefaultArena; if (!PyArg_ParseTuple(args, ":get_stats")) { return NULL; @@ -3939,6 +3895,10 @@ _get_stats(PyObject *self, PyObject *args) { if (!d) { return NULL; } + + MUTEX_LOCK(&ImagingDefaultArena.mutex); + ImagingMemoryArena arena = &ImagingDefaultArena; + v = PyLong_FromLong(arena->stats_new_count); PyDict_SetItemString(d, "new_count", v ? v : Py_None); Py_XDECREF(v); @@ -3962,25 +3922,27 @@ _get_stats(PyObject *self, PyObject *args) { v = PyLong_FromLong(arena->blocks_cached); PyDict_SetItemString(d, "blocks_cached", v ? v : Py_None); Py_XDECREF(v); + + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); return d; } static PyObject * _reset_stats(PyObject *self, PyObject *args) { - ImagingMemoryArena arena = &ImagingDefaultArena; - if (!PyArg_ParseTuple(args, ":reset_stats")) { return NULL; } + MUTEX_LOCK(&ImagingDefaultArena.mutex); + ImagingMemoryArena arena = &ImagingDefaultArena; arena->stats_new_count = 0; arena->stats_allocated_blocks = 0; arena->stats_reused_blocks = 0; arena->stats_reallocated_blocks = 0; arena->stats_freed_blocks = 0; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3989,7 +3951,10 @@ _get_alignment(PyObject *self, PyObject *args) { return NULL; } - return PyLong_FromLong(ImagingDefaultArena.alignment); + MUTEX_LOCK(&ImagingDefaultArena.mutex); + int alignment = ImagingDefaultArena.alignment; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + return PyLong_FromLong(alignment); } static PyObject * @@ -3998,7 +3963,10 @@ _get_block_size(PyObject *self, PyObject *args) { return NULL; } - return PyLong_FromLong(ImagingDefaultArena.block_size); + MUTEX_LOCK(&ImagingDefaultArena.mutex); + int block_size = ImagingDefaultArena.block_size; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + return PyLong_FromLong(block_size); } static PyObject * @@ -4007,7 +3975,10 @@ _get_blocks_max(PyObject *self, PyObject *args) { return NULL; } - return PyLong_FromLong(ImagingDefaultArena.blocks_max); + MUTEX_LOCK(&ImagingDefaultArena.mutex); + int blocks_max = ImagingDefaultArena.blocks_max; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + return PyLong_FromLong(blocks_max); } static PyObject * @@ -4027,10 +3998,11 @@ _set_alignment(PyObject *self, PyObject *args) { return NULL; } + MUTEX_LOCK(&ImagingDefaultArena.mutex); ImagingDefaultArena.alignment = alignment; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -4050,10 +4022,11 @@ _set_block_size(PyObject *self, PyObject *args) { return NULL; } + MUTEX_LOCK(&ImagingDefaultArena.mutex); ImagingDefaultArena.block_size = block_size; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -4066,19 +4039,37 @@ _set_blocks_max(PyObject *self, PyObject *args) { if (blocks_max < 0) { PyErr_SetString(PyExc_ValueError, "blocks_max should be greater than 0"); return NULL; - } else if ( - (unsigned long)blocks_max > + } + + 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)) { + MUTEX_LOCK(&ImagingDefaultArena.mutex); + int status = ImagingMemorySetBlocksMax(&ImagingDefaultArena, blocks_max); + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + if (!status) { return ImagingError_MemoryError(); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; +} + +static PyObject * +_set_use_block_allocator(PyObject *self, PyObject *args) { + int use_block_allocator; + if (!PyArg_ParseTuple(args, "i:set_use_block_allocator", &use_block_allocator)) { + return NULL; + } + ImagingMemorySetBlockAllocator(&ImagingDefaultArena, use_block_allocator); + Py_RETURN_NONE; +} + +static PyObject * +_get_use_block_allocator(PyObject *self, PyObject *args) { + return PyLong_FromLong(ImagingDefaultArena.use_block_allocator); } static PyObject * @@ -4089,10 +4080,11 @@ _clear_cache(PyObject *self, PyObject *args) { return NULL; } + MUTEX_LOCK(&ImagingDefaultArena.mutex); ImagingMemoryClearCache(&ImagingDefaultArena, i); + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /* -------------------------------------------------------------------- */ @@ -4138,6 +4130,8 @@ PyImaging_ZipDecoderNew(PyObject *self, PyObject *args); /* Encoders (in encode.c) */ extern PyObject * +PyImaging_BcnEncoderNew(PyObject *self, PyObject *args); +extern PyObject * PyImaging_EpsEncoderNew(PyObject *self, PyObject *args); extern PyObject * PyImaging_GifEncoderNew(PyObject *self, PyObject *args); @@ -4198,6 +4192,8 @@ static PyMethodDef functions[] = { {"blend", (PyCFunction)_blend, METH_VARARGS}, {"fill", (PyCFunction)_fill, METH_VARARGS}, {"new", (PyCFunction)_new, METH_VARARGS}, + {"new_block", (PyCFunction)_new_block, METH_VARARGS}, + {"new_arrow", (PyCFunction)_new_arrow, METH_VARARGS}, {"merge", (PyCFunction)_merge, METH_VARARGS}, /* Functions */ @@ -4205,6 +4201,7 @@ static PyMethodDef functions[] = { /* Codecs */ {"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, METH_VARARGS}, + {"bcn_encoder", (PyCFunction)PyImaging_BcnEncoderNew, METH_VARARGS}, {"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, METH_VARARGS}, {"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS}, {"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, METH_VARARGS}, @@ -4241,13 +4238,11 @@ static PyMethodDef functions[] = { {"zip_encoder", (PyCFunction)PyImaging_ZipEncoderNew, METH_VARARGS}, #endif -/* Memory mapping */ -#ifdef WITH_MAPPING + /* Memory mapping */ {"map_buffer", (PyCFunction)PyImaging_MapBuffer, METH_VARARGS}, -#endif -/* Display support */ #ifdef _WIN32 + /* Display support */ {"display", (PyCFunction)PyImaging_DisplayWin32, METH_VARARGS}, {"display_mode", (PyCFunction)PyImaging_DisplayModeWin32, METH_VARARGS}, {"grabscreen_win32", (PyCFunction)PyImaging_GrabScreenWin32, METH_VARARGS}, @@ -4263,30 +4258,21 @@ static PyMethodDef functions[] = { /* Utilities */ {"getcodecstatus", (PyCFunction)_getcodecstatus, METH_VARARGS}, -/* Special effects (experimental) */ -#ifdef WITH_EFFECTS + /* Special effects (experimental) */ {"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, METH_VARARGS}, {"effect_noise", (PyCFunction)_effect_noise, METH_VARARGS}, {"linear_gradient", (PyCFunction)_linear_gradient, METH_VARARGS}, {"radial_gradient", (PyCFunction)_radial_gradient, METH_VARARGS}, - {"wedge", (PyCFunction)_linear_gradient, METH_VARARGS}, /* Compatibility */ -#endif -/* Drawing support stuff */ -#ifdef WITH_IMAGEDRAW + /* Drawing support stuff */ {"font", (PyCFunction)_font_new, METH_VARARGS}, {"draw", (PyCFunction)_draw_new, METH_VARARGS}, -#endif -/* Experimental path stuff */ -#ifdef WITH_IMAGEPATH + /* Experimental path stuff */ {"path", (PyCFunction)PyPath_Create, METH_VARARGS}, -#endif -/* Experimental arrow graphics stuff */ -#ifdef WITH_ARROW + /* Experimental arrow graphics stuff */ {"outline", (PyCFunction)PyOutline_Create, METH_VARARGS}, -#endif /* Resource management */ {"get_stats", (PyCFunction)_get_stats, METH_VARARGS}, @@ -4294,9 +4280,11 @@ static PyMethodDef functions[] = { {"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}, + {"get_use_block_allocator", (PyCFunction)_get_use_block_allocator, 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}, + {"set_use_block_allocator", (PyCFunction)_set_use_block_allocator, METH_VARARGS}, {"clear_cache", (PyCFunction)_clear_cache, METH_VARARGS}, {NULL, NULL} /* sentinel */ @@ -4311,22 +4299,16 @@ setup_module(PyObject *m) { if (PyType_Ready(&Imaging_Type) < 0) { return -1; } - -#ifdef WITH_IMAGEDRAW if (PyType_Ready(&ImagingFont_Type) < 0) { return -1; } - if (PyType_Ready(&ImagingDraw_Type) < 0) { return -1; } -#endif if (PyType_Ready(&PixelAccess_Type) < 0) { return -1; } - ImagingAccessInit(); - #ifdef HAVE_LIBJPEG { extern const char *ImagingJpegVersion(void); @@ -4360,8 +4342,19 @@ setup_module(PyObject *m) { #else have_libjpegturbo = Py_False; #endif - Py_INCREF(have_libjpegturbo); - PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo); + if (PyModule_AddObjectRef(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo) < 0) { + return -1; + } + + PyObject *have_mozjpeg; +#ifdef JPEG_C_PARAM_SUPPORTED + have_mozjpeg = Py_True; +#else + have_mozjpeg = Py_False; +#endif + if (PyModule_AddObjectRef(m, "HAVE_MOZJPEG", have_mozjpeg) < 0) { + return -1; + } PyObject *have_libimagequant; #ifdef HAVE_LIBIMAGEQUANT @@ -4375,8 +4368,9 @@ setup_module(PyObject *m) { #else have_libimagequant = Py_False; #endif - Py_INCREF(have_libimagequant); - PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", have_libimagequant); + if (PyModule_AddObjectRef(m, "HAVE_LIBIMAGEQUANT", have_libimagequant) < 0) { + return -1; + } #ifdef HAVE_LIBZ /* zip encoding strategies */ @@ -4393,22 +4387,27 @@ setup_module(PyObject *m) { } #endif + PyObject *have_zlibng; +#ifdef ZLIBNG_VERSION + have_zlibng = Py_True; + { + PyObject *v = PyUnicode_FromString(ZLIBNG_VERSION); + PyDict_SetItemString(d, "zlib_ng_version", v ? v : Py_None); + Py_XDECREF(v); + } +#else + have_zlibng = Py_False; +#endif + if (PyModule_AddObjectRef(m, "HAVE_ZLIBNG", have_zlibng) < 0) { + return -1; + } + #ifdef HAVE_LIBTIFF { extern const char *ImagingTiffVersion(void); PyObject *v = PyUnicode_FromString(ImagingTiffVersion()); PyDict_SetItemString(d, "libtiff_version", v ? v : Py_None); Py_XDECREF(v); - - // 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 @@ -4418,35 +4417,35 @@ setup_module(PyObject *m) { #else have_xcb = Py_False; #endif - Py_INCREF(have_xcb); - PyModule_AddObject(m, "HAVE_XCB", have_xcb); + if (PyModule_AddObjectRef(m, "HAVE_XCB", have_xcb) < 0) { + return -1; + } PyObject *pillow_version = PyUnicode_FromString(version); PyDict_SetItemString( - d, "PILLOW_VERSION", pillow_version ? pillow_version : Py_None); + d, "PILLOW_VERSION", pillow_version ? pillow_version : Py_None + ); Py_XDECREF(pillow_version); return 0; } +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__imaging(void) { - PyObject *m; - static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imaging", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - functions, /* m_methods */ + .m_name = "_imaging", + .m_methods = functions, + .m_slots = slots }; - m = PyModule_Create(&module_def); - - if (setup_module(m) < 0) { - Py_DECREF(m); - return NULL; - } - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 590e1b983a9..469be05f4a3 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -212,32 +212,44 @@ cms_transform_dealloc(CmsTransformObject *self) { /* internal functions */ static cmsUInt32Number -findLCMStype(char *PILmode) { - if (strcmp(PILmode, "RGB") == 0 || strcmp(PILmode, "RGBA") == 0 || - strcmp(PILmode, "RGBX") == 0) { - return TYPE_RGBA_8; +findLCMStype(const char *const mode_name) { + const ModeID mode = findModeID(mode_name); + switch (mode) { + case IMAGING_MODE_RGB: + case IMAGING_MODE_RGBA: + case IMAGING_MODE_RGBX: + return TYPE_RGBA_8; + case IMAGING_MODE_CMYK: + return TYPE_CMYK_8; + case IMAGING_MODE_I_16: + case IMAGING_MODE_I_16L: + return TYPE_GRAY_16; + case IMAGING_MODE_I_16B: + return TYPE_GRAY_16_SE; + case IMAGING_MODE_YCbCr: + return TYPE_YCbCr_8; + case IMAGING_MODE_LAB: + // 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) + ); + default: + // This function only accepts a subset of the imaging modes Pillow has. + break; } - if (strcmp(PILmode, "RGBA;16B") == 0) { + // The following modes are not valid PIL Image modes. + if (strcmp(mode_name, "RGBA;16B") == 0) { return TYPE_RGBA_16; } - if (strcmp(PILmode, "CMYK") == 0) { - return TYPE_CMYK_8; - } - if (strcmp(PILmode, "I;16") == 0 || strcmp(PILmode, "I;16L") == 0 || - strcmp(PILmode, "L;16") == 0) { + if (strcmp(mode_name, "L;16") == 0) { return TYPE_GRAY_16; } - if (strcmp(PILmode, "I;16B") == 0 || strcmp(PILmode, "L;16B") == 0) { + if (strcmp(mode_name, "L;16B") == 0) { return TYPE_GRAY_16_SE; } - if (strcmp(PILmode, "YCbCr") == 0 || strcmp(PILmode, "YCCA") == 0 || - strcmp(PILmode, "YCC") == 0) { + if (strcmp(mode_name, "YCCA") == 0 || strcmp(mode_name, "YCC") == 0) { return TYPE_YCbCr_8; } - 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)); - } /* presume "1" or "L" by default */ return TYPE_GRAY_8; } @@ -331,7 +343,8 @@ pyCMScopyAux(cmsHTRANSFORM hTransform, Imaging imDst, const Imaging imSrc) { memcpy( pDstExtras + x * dstChunkSize, pSrcExtras + x * srcChunkSize, - channelSize); + channelSize + ); } } } @@ -345,10 +358,10 @@ pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) { return -1; } - Py_BEGIN_ALLOW_THREADS + 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); } @@ -361,9 +374,9 @@ pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) { // enough available on all platforms, so we polyfill it here for now. pyCMScopyAux(hTransform, imOut, im); - Py_END_ALLOW_THREADS + Py_END_ALLOW_THREADS; - return 0; + return 0; } static cmsHTRANSFORM @@ -373,19 +386,21 @@ _buildTransform( char *sInMode, char *sOutMode, int iRenderingIntent, - cmsUInt32Number cmsFLAGS) { + cmsUInt32Number cmsFLAGS +) { cmsHTRANSFORM hTransform; - Py_BEGIN_ALLOW_THREADS + 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; @@ -405,21 +420,23 @@ _buildProofTransform( char *sOutMode, int iRenderingIntent, int iProofIntent, - cmsUInt32Number cmsFLAGS) { + cmsUInt32Number cmsFLAGS +) { cmsHTRANSFORM hTransform; - Py_BEGIN_ALLOW_THREADS + 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; @@ -454,7 +471,8 @@ buildTransform(PyObject *self, PyObject *args) { &sInMode, &sOutMode, &iRenderingIntent, - &cmsFLAGS)) { + &cmsFLAGS + )) { return NULL; } @@ -464,7 +482,8 @@ buildTransform(PyObject *self, PyObject *args) { sInMode, sOutMode, iRenderingIntent, - cmsFLAGS); + cmsFLAGS + ); if (!transform) { return NULL; @@ -499,7 +518,8 @@ buildProofTransform(PyObject *self, PyObject *args) { &sOutMode, &iRenderingIntent, &iProofIntent, - &cmsFLAGS)) { + &cmsFLAGS + )) { return NULL; } @@ -511,7 +531,8 @@ buildProofTransform(PyObject *self, PyObject *args) { sOutMode, iRenderingIntent, iProofIntent, - cmsFLAGS); + cmsFLAGS + ); if (!transform) { return NULL; @@ -522,23 +543,30 @@ buildProofTransform(PyObject *self, PyObject *args) { static PyObject * cms_transform_apply(CmsTransformObject *self, PyObject *args) { - Py_ssize_t idIn; - Py_ssize_t idOut; + PyObject *i0, *i1; Imaging im; Imaging imOut; - int result; - - if (!PyArg_ParseTuple(args, "nn:apply", &idIn, &idOut)) { + if (!PyArg_ParseTuple(args, "OO:apply", &i0, &i1)) { return NULL; } - im = (Imaging)idIn; - imOut = (Imaging)idOut; + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || + !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); + return NULL; + } - result = pyCMSdoTransform(im, imOut, self->transform); + im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!im) { + return NULL; + } + imOut = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + if (!imOut) { + return NULL; + } - return Py_BuildValue("i", result); + return Py_BuildValue("i", pyCMSdoTransform(im, imOut, self->transform)); } /* -------------------------------------------------------------------- */ @@ -563,7 +591,8 @@ createProfile(PyObject *self, PyObject *args) { PyErr_SetString( PyExc_ValueError, "ERROR: Could not calculate white point from color temperature " - "provided, must be float in degrees Kelvin"); + "provided, must be float in degrees Kelvin" + ); return NULL; } hProfile = cmsCreateLab2Profile(&whitePoint); @@ -624,7 +653,8 @@ cms_get_display_profile_win32(PyObject *self, PyObject *args) { HANDLE handle = 0; int is_dc = 0; if (!PyArg_ParseTuple( - args, "|" F_HANDLE "i:get_display_profile", &handle, &is_dc)) { + args, "|" F_HANDLE "i:get_display_profile", &handle, &is_dc + )) { return NULL; } @@ -642,8 +672,7 @@ cms_get_display_profile_win32(PyObject *self, PyObject *args) { return PyUnicode_FromStringAndSize(filename, filename_size - 1); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } #endif @@ -660,20 +689,17 @@ _profile_read_mlu(CmsProfileObject *self, cmsTagSignature info) { wchar_t *buf; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } mlu = cmsReadTag(self->profile, info); if (!mlu) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } len = cmsMLUgetWide(mlu, lc, cc, NULL, 0); if (len == 0) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } buf = malloc(len); @@ -711,14 +737,12 @@ _profile_read_signature(CmsProfileObject *self, cmsTagSignature info) { unsigned int *sig; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } sig = (unsigned int *)cmsReadTag(self->profile, info); if (!sig) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _profile_read_int_as_string(*sig); @@ -729,7 +753,8 @@ _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); + "((d,d,d),(d,d,d))", XYZ->X, XYZ->Y, XYZ->Z, xyY.x, xyY.y, xyY.Y + ); } static PyObject * @@ -758,7 +783,8 @@ _xyz3_py(cmsCIEXYZ *XYZ) { xyY[1].Y, xyY[2].x, xyY[2].y, - xyY[2].Y); + xyY[2].Y + ); } static PyObject * @@ -766,14 +792,12 @@ _profile_read_ciexyz(CmsProfileObject *self, cmsTagSignature info, int multi) { cmsCIEXYZ *XYZ; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } XYZ = (cmsCIEXYZ *)cmsReadTag(self->profile, info); if (!XYZ) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } if (multi) { return _xyz3_py(XYZ); @@ -787,14 +811,12 @@ _profile_read_ciexyy_triple(CmsProfileObject *self, cmsTagSignature info) { cmsCIExyYTRIPLE *triple; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } triple = (cmsCIExyYTRIPLE *)cmsReadTag(self->profile, info); if (!triple) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /* Note: lcms does all the heavy lifting and error checking (nr of @@ -809,7 +831,8 @@ _profile_read_ciexyy_triple(CmsProfileObject *self, cmsTagSignature info) { triple->Green.Y, triple->Blue.x, triple->Blue.y, - triple->Blue.Y); + triple->Blue.Y + ); } static PyObject * @@ -820,21 +843,18 @@ _profile_read_named_color_list(CmsProfileObject *self, cmsTagSignature info) { PyObject *result; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } ncl = (cmsNAMEDCOLORLIST *)cmsReadTag(self->profile, info); if (ncl == NULL) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } n = cmsNamedColorCount(ncl); result = PyList_New(n); if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } for (i = 0; i < n; i++) { @@ -843,8 +863,7 @@ _profile_read_named_color_list(CmsProfileObject *self, cmsTagSignature info) { str = PyUnicode_FromString(name); if (str == NULL) { Py_DECREF(result); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } PyList_SET_ITEM(result, i, str); } @@ -873,7 +892,8 @@ _calculate_rgb_primaries(CmsProfileObject *self, cmsCIEXYZTRIPLE *result) { hXYZ, TYPE_XYZ_DBL, INTENT_RELATIVE_COLORIMETRIC, - cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE); + cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE + ); cmsCloseProfile(hXYZ); if (hTransform == NULL) { return 0; @@ -889,7 +909,8 @@ _check_intent( int clut, cmsHPROFILE hProfile, cmsUInt32Number Intent, - cmsUInt32Number UsedDirection) { + cmsUInt32Number UsedDirection +) { if (clut) { return cmsIsCLUT(hProfile, Intent, UsedDirection); } else { @@ -909,8 +930,7 @@ _is_intent_supported(CmsProfileObject *self, int clut) { result = PyDict_New(); if (result == NULL) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } n = cmsGetSupportedIntents(INTENTS, intent_ids, intent_descs); @@ -934,13 +954,13 @@ _is_intent_supported(CmsProfileObject *self, int clut) { _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); + : Py_False + ); if (id == NULL || entry == NULL) { Py_XDECREF(id); Py_XDECREF(entry); Py_XDECREF(result); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } PyDict_SetItem(result, id, entry); Py_DECREF(id); @@ -968,7 +988,8 @@ static PyMethodDef pyCMSdll_methods[] = { {"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, METH_VARARGS}, @@ -1023,12 +1044,12 @@ cms_profile_getattr_creation_date(CmsProfileObject *self, void *closure) { result = cmsGetHeaderCreationDateTime(self->profile, &ct); if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return PyDateTime_FromDateAndTime( - 1900 + ct.tm_year, ct.tm_mon, ct.tm_mday, ct.tm_hour, ct.tm_min, ct.tm_sec, 0); + 1900 + ct.tm_year, ct.tm_mon, ct.tm_mday, ct.tm_hour, ct.tm_min, ct.tm_sec, 0 + ); } static PyObject * @@ -1106,21 +1127,22 @@ cms_profile_getattr_colorimetric_intent(CmsProfileObject *self, void *closure) { static PyObject * cms_profile_getattr_perceptual_rendering_intent_gamut( - CmsProfileObject *self, void *closure) { + CmsProfileObject *self, void *closure +) { return _profile_read_signature(self, cmsSigPerceptualRenderingIntentGamutTag); } static PyObject * cms_profile_getattr_saturation_rendering_intent_gamut( - CmsProfileObject *self, void *closure) { + CmsProfileObject *self, void *closure +) { return _profile_read_signature(self, cmsSigSaturationRenderingIntentGamutTag); } static PyObject * cms_profile_getattr_red_colorant(CmsProfileObject *self, void *closure) { if (!cmsIsMatrixShaper(self->profile)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _profile_read_ciexyz(self, cmsSigRedColorantTag, 0); } @@ -1128,8 +1150,7 @@ cms_profile_getattr_red_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; + Py_RETURN_NONE; } return _profile_read_ciexyz(self, cmsSigGreenColorantTag, 0); } @@ -1137,15 +1158,15 @@ cms_profile_getattr_green_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; + Py_RETURN_NONE; } return _profile_read_ciexyz(self, cmsSigBlueColorantTag, 0); } static PyObject * cms_profile_getattr_media_white_point_temperature( - CmsProfileObject *self, void *closure) { + CmsProfileObject *self, void *closure +) { cmsCIEXYZ *XYZ; cmsCIExyY xyY; cmsFloat64Number tempK; @@ -1153,21 +1174,18 @@ cms_profile_getattr_media_white_point_temperature( cmsBool result; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } XYZ = (cmsCIEXYZ *)cmsReadTag(self->profile, info); if (XYZ == NULL || XYZ->X == 0) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } cmsXYZ2xyY(&xyY, XYZ); result = cmsTempFromWhitePoint(&tempK, &xyY); if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return PyFloat_FromDouble(tempK); } @@ -1206,8 +1224,7 @@ cms_profile_getattr_red_primary(CmsProfileObject *self, void *closure) { result = _calculate_rgb_primaries(self, &primaries); } if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _xyz_py(&primaries.Red); @@ -1222,8 +1239,7 @@ cms_profile_getattr_green_primary(CmsProfileObject *self, void *closure) { result = _calculate_rgb_primaries(self, &primaries); } if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _xyz_py(&primaries.Green); @@ -1238,8 +1254,7 @@ cms_profile_getattr_blue_primary(CmsProfileObject *self, void *closure) { result = _calculate_rgb_primaries(self, &primaries); } if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _xyz_py(&primaries.Blue); @@ -1298,14 +1313,12 @@ cms_profile_getattr_icc_measurement_condition(CmsProfileObject *self, void *clos const char *geo; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } mc = (cmsICCMeasurementConditions *)cmsReadTag(self->profile, info); if (!mc) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } if (mc->Geometry == 1) { @@ -1329,7 +1342,8 @@ cms_profile_getattr_icc_measurement_condition(CmsProfileObject *self, void *clos "flare", mc->Flare, "illuminant_type", - _illu_map(mc->IlluminantType)); + _illu_map(mc->IlluminantType) + ); } static PyObject * @@ -1338,14 +1352,12 @@ cms_profile_getattr_icc_viewing_condition(CmsProfileObject *self, void *closure) cmsTagSignature info = cmsSigViewingConditionsTag; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } vc = (cmsICCViewingConditions *)cmsReadTag(self->profile, info); if (!vc) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return Py_BuildValue( @@ -1359,7 +1371,8 @@ cms_profile_getattr_icc_viewing_condition(CmsProfileObject *self, void *closure) vc->SurroundXYZ.Y, vc->SurroundXYZ.Z, "illuminant_type", - _illu_map(vc->IlluminantType)); + _illu_map(vc->IlluminantType) + ); } static struct PyGetSetDef cms_profile_getsetters[] = { @@ -1411,39 +1424,15 @@ static struct PyGetSetDef cms_profile_getsetters[] = { (getter)cms_profile_getattr_icc_measurement_condition}, {"icc_viewing_condition", (getter)cms_profile_getattr_icc_viewing_condition}, - {NULL}}; + {NULL} +}; static PyTypeObject CmsProfile_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "PIL.ImageCms.core.CmsProfile", /*tp_name*/ - sizeof(CmsProfileObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)cms_profile_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 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*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "PIL.ImageCms.core.CmsProfile", + .tp_basicsize = sizeof(CmsProfileObject), + .tp_dealloc = (destructor)cms_profile_dealloc, + .tp_methods = cms_profile_methods, + .tp_getset = cms_profile_getsetters, }; static struct PyMethodDef cms_transform_methods[] = { @@ -1451,36 +1440,10 @@ static struct PyMethodDef cms_transform_methods[] = { }; static PyTypeObject CmsTransform_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "PIL.ImageCms.core.CmsTransform", /*tp_name*/ - sizeof(CmsTransformObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)cms_transform_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 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*/ - 0, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "PIL.ImageCms.core.CmsTransform", + .tp_basicsize = sizeof(CmsTransformObject), + .tp_dealloc = (destructor)cms_transform_dealloc, + .tp_methods = cms_transform_methods, }; static int @@ -1490,14 +1453,14 @@ setup_module(PyObject *m) { int vn; /* Ready object types */ - PyType_Ready(&CmsProfile_Type); - PyType_Ready(&CmsTransform_Type); - - Py_INCREF(&CmsProfile_Type); - PyModule_AddObject(m, "CmsProfile", (PyObject *)&CmsProfile_Type); + if (PyType_Ready(&CmsProfile_Type) < 0 || PyType_Ready(&CmsTransform_Type) < 0) { + return -1; + } - Py_INCREF(&CmsTransform_Type); - PyModule_AddObject(m, "CmsTransform", (PyObject *)&CmsTransform_Type); + if (PyModule_AddObjectRef(m, "CmsProfile", (PyObject *)&CmsProfile_Type) < 0 || + PyModule_AddObjectRef(m, "CmsTransform", (PyObject *)&CmsTransform_Type) < 0) { + return -1; + } d = PyModule_GetDict(m); @@ -1518,25 +1481,24 @@ setup_module(PyObject *m) { return 0; } +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__imagingcms(void) { - PyObject *m; + PyDateTime_IMPORT; static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingcms", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - pyCMSdll_methods, /* m_methods */ + .m_name = "_imagingcms", + .m_methods = pyCMSdll_methods, + .m_slots = slots }; - m = PyModule_Create(&module_def); - - if (setup_module(m) < 0) { - return NULL; - } - - PyDateTime_IMPORT; - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingft.c b/src/_imagingft.c index ba36cc72c23..8330439f0b1 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -20,6 +20,7 @@ #define PY_SSIZE_T_CLEAN #include "Python.h" +#include "thirdparty/pythoncapi_compat.h" #include "libImaging/Imaging.h" #include @@ -81,6 +82,9 @@ struct { /* font objects */ static FT_Library library; +#ifdef Py_GIL_DISABLED +static PyMutex ft_library_mutex; +#endif typedef struct { PyObject_HEAD FT_Face face; @@ -125,7 +129,8 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { unsigned char *font_bytes; Py_ssize_t font_bytes_size = 0; static char *kwlist[] = { - "filename", "size", "index", "encoding", "font_bytes", "layout_engine", NULL}; + "filename", "size", "index", "encoding", "font_bytes", "layout_engine", NULL + }; if (!library) { PyErr_SetString(PyExc_OSError, "failed to initialize FreeType library"); @@ -147,7 +152,8 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { &encoding, &font_bytes, &font_bytes_size, - &layout_engine)) { + &layout_engine + )) { PyConfig_Clear(&config); return NULL; } @@ -165,7 +171,8 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { &encoding, &font_bytes, &font_bytes_size, - &layout_engine)) { + &layout_engine + )) { return NULL; } #endif @@ -183,7 +190,9 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { if (filename && font_bytes_size <= 0) { self->font_bytes = NULL; + MUTEX_LOCK(&ft_library_mutex); error = FT_New_Face(library, filename, index, &self->face); + MUTEX_UNLOCK(&ft_library_mutex); } else { /* need to have allocated storage for font_bytes for the life of the object.*/ /* Don't free this before FT_Done_Face */ @@ -193,12 +202,15 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { } if (!error) { memcpy(self->font_bytes, font_bytes, (size_t)font_bytes_size); + MUTEX_LOCK(&ft_library_mutex); error = FT_New_Memory_Face( library, (FT_Byte *)self->font_bytes, font_bytes_size, index, - &self->face); + &self->face + ); + MUTEX_UNLOCK(&ft_library_mutex); } } @@ -242,7 +254,8 @@ text_layout_raqm( const char *dir, PyObject *features, const char *lang, - GlyphInfo **glyph_info) { + GlyphInfo **glyph_info +) { size_t i = 0, count = 0, start = 0; raqm_t *rq; raqm_glyph_t *glyphs = NULL; @@ -262,11 +275,12 @@ text_layout_raqm( if (!text || !size) { /* return 0 and clean up, no glyphs==no size, and raqm fails with empty strings */ + PyMem_Free(text); goto failed; } set_text = raqm_set_text(rq, text, size); PyMem_Free(text); - } else { + } else if (PyBytes_Check(string)) { char *buffer; PyBytes_AsStringAndSize(string, &buffer, &size); if (!buffer || !size) { @@ -275,6 +289,9 @@ text_layout_raqm( goto failed; } set_text = raqm_set_text_utf8(rq, buffer, size); + } else { + PyErr_SetString(PyExc_TypeError, "expected string or bytes"); + goto failed; } if (!set_text) { PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); @@ -296,13 +313,14 @@ text_layout_raqm( #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"); + 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'"); + PyExc_ValueError, "direction must be either 'rtl', 'ltr' or 'ttb'" + ); goto failed; } } @@ -322,29 +340,23 @@ text_layout_raqm( 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 (!PyUnicode_Check(item)) { Py_DECREF(seq); PyErr_SetString(PyExc_TypeError, "expected a string"); goto failed; } - bytes = PyUnicode_AsUTF8String(item); - if (bytes == NULL) { + + Py_ssize_t size; + const char *feature = PyUnicode_AsUTF8AndSize(item, &size); + if (feature == NULL) { Py_DECREF(seq); goto failed; } - feature = PyBytes_AS_STRING(bytes); - size = PyBytes_GET_SIZE(bytes); if (!raqm_add_font_feature(rq, feature, size)) { Py_DECREF(seq); - Py_DECREF(bytes); PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); goto failed; } - Py_DECREF(bytes); } Py_DECREF(seq); } @@ -398,7 +410,8 @@ text_layout_fallback( const char *lang, GlyphInfo **glyph_info, int mask, - int color) { + int color +) { int error, load_flags, i; char *buffer = NULL; FT_ULong ch; @@ -411,13 +424,18 @@ text_layout_fallback( PyErr_SetString( PyExc_KeyError, "setting text direction, language or font features is not supported " - "without libraqm"); + "without libraqm" + ); + return 0; } if (PyUnicode_Check(string)) { count = PyUnicode_GET_LENGTH(string); - } else { + } else if (PyBytes_Check(string)) { PyBytes_AsStringAndSize(string, &buffer, &count); + } else { + PyErr_SetString(PyExc_TypeError, "expected string or bytes"); + return 0; } if (count == 0) { return 0; @@ -458,7 +476,8 @@ text_layout_fallback( last_index, (*glyph_info)[i].index, ft_kerning_default, - &delta) == 0) { + &delta + ) == 0) { (*glyph_info)[i - 1].x_advance += PIXEL(delta.x); (*glyph_info)[i - 1].y_advance += PIXEL(delta.y); } @@ -482,7 +501,8 @@ text_layout( const char *lang, GlyphInfo **glyph_info, int mask, - int color) { + int color +) { size_t count; #ifdef HAVE_RAQM if (have_raqm && self->layout_engine == LAYOUT_RAQM) { @@ -491,20 +511,21 @@ text_layout( #endif { count = text_layout_fallback( - string, self, dir, features, lang, glyph_info, mask, color); + string, self, dir, features, lang, glyph_info, mask, color + ); } return count; } static PyObject * -font_getlength(FontObject *self, PyObject *args) { +font_getlength_impl(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 *mode_name = NULL; const char *dir = NULL; const char *lang = NULL; PyObject *features = Py_None; @@ -513,14 +534,16 @@ font_getlength(FontObject *self, PyObject *args) { /* calculate size and bearing for a given string */ if (!PyArg_ParseTuple( - args, "O|zzOz:getlength", &string, &mode, &dir, &features, &lang)) { + args, "O|zzOz:getlength", &string, &mode_name, &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; + const ModeID mode = findModeID(mode_name); + mask = mode == IMAGING_MODE_1; + color = mode == IMAGING_MODE_RGBA; count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); if (PyErr_Occurred()) { @@ -544,6 +567,15 @@ font_getlength(FontObject *self, PyObject *args) { return PyLong_FromLong(length); } +static PyObject * +font_getlength(FontObject *self, PyObject *args) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_getlength_impl(self, args); + Py_END_CRITICAL_SECTION(); + return result; +} + static int bounding_box_and_anchors( FT_Face face, @@ -555,10 +587,11 @@ bounding_box_and_anchors( int *width, int *height, int *x_offset, - int *y_offset) { - 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 *y_offset +) { + long position; /* pen position along primary axis, in 26.6 precision */ + long 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 error; @@ -660,7 +693,8 @@ bounding_box_and_anchors( case 'm': // middle (ascender + descender) / 2 y_anchor = PIXEL( (face->size->metrics.ascender + face->size->metrics.descender) / - 2); + 2 + ); break; case 's': // horizontal baseline y_anchor = 0; @@ -721,7 +755,7 @@ bounding_box_and_anchors( } static PyObject * -font_getsize(FontObject *self, PyObject *args) { +font_getsize_impl(FontObject *self, PyObject *args) { int width, height, x_offset, y_offset; int load_flags; /* FreeType load_flags parameter */ int error; @@ -730,7 +764,7 @@ font_getsize(FontObject *self, PyObject *args) { 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 *mode_name = NULL; const char *dir = NULL; const char *lang = NULL; const char *anchor = NULL; @@ -740,14 +774,23 @@ font_getsize(FontObject *self, PyObject *args) { /* calculate size and bearing for a given string */ if (!PyArg_ParseTuple( - args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { + args, + "O|zzOzz:getsize", + &string, + &mode_name, + &dir, + &features, + &lang, + &anchor + )) { return NULL; } horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; - mask = mode && strcmp(mode, "1") == 0; - color = mode && strcmp(mode, "RGBA") == 0; + const ModeID mode = findModeID(mode_name); + mask = mode == IMAGING_MODE_1; + color = mode == IMAGING_MODE_RGBA; count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); if (PyErr_Occurred()) { @@ -772,7 +815,8 @@ font_getsize(FontObject *self, PyObject *args) { &width, &height, &x_offset, - &y_offset); + &y_offset + ); if (glyph_info) { PyMem_Free(glyph_info); glyph_info = NULL; @@ -785,7 +829,16 @@ font_getsize(FontObject *self, PyObject *args) { } static PyObject * -font_render(FontObject *self, PyObject *args) { +font_getsize(FontObject *self, PyObject *args) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_getsize_impl(self, args); + Py_END_CRITICAL_SECTION(); + return result; +} + +static PyObject * +font_render_impl(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 */ @@ -807,13 +860,13 @@ font_render(FontObject *self, PyObject *args) { unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ PyObject *image; Imaging im; - Py_ssize_t id; int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ int color = 0; /* is FT_LOAD_COLOR enabled? */ - int stroke_width = 0; + float stroke_width = 0; + int stroke_filled = 0; PY_LONG_LONG foreground_ink_long = 0; unsigned int foreground_ink; - const char *mode = NULL; + const char *mode_name = NULL; const char *dir = NULL; const char *lang = NULL; const char *anchor = NULL; @@ -830,23 +883,26 @@ font_render(FontObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "OO|zzOzizLffO:render", + "OO|zzOzfpzL(ff):render", &string, &fill, - &mode, + &mode_name, &dir, &features, &lang, &stroke_width, + &stroke_filled, &anchor, &foreground_ink_long, &x_start, - &y_start)) { + &y_start + )) { return NULL; } - mask = mode && strcmp(mode, "1") == 0; - color = mode && strcmp(mode, "RGBA") == 0; + const ModeID mode = findModeID(mode_name); + mask = mode == IMAGING_MODE_1; + color = mode == IMAGING_MODE_RGBA; foreground_ink = foreground_ink_long; @@ -888,29 +944,36 @@ font_render(FontObject *self, PyObject *args) { &width, &height, &x_offset, - &y_offset); + &y_offset + ); if (error) { PyMem_Del(glyph_info); return NULL; } - width += stroke_width * 2 + ceil(x_start); - height += stroke_width * 2 + ceil(y_start); + width += ceil(stroke_width * 2 + x_start); + height += ceil(stroke_width * 2 + y_start); image = PyObject_CallFunction(fill, "ii", width, height); - if (image == Py_None) { + if (image == NULL) { + PyMem_Del(glyph_info); + return NULL; + } + PyObject *imagePtr = PyObject_GetAttrString(image, "ptr"); + if (!imagePtr) { PyMem_Del(glyph_info); - return Py_BuildValue("N(ii)", image, 0, 0); - } else if (image == NULL) { + Py_DECREF(image); + return NULL; + } + im = (Imaging)PyCapsule_GetPointer(imagePtr, IMAGING_MAGIC); + Py_DECREF(imagePtr); + if (!im) { PyMem_Del(glyph_info); + Py_DECREF(image); return NULL; } - PyObject *imageId = PyObject_GetAttrString(image, "id"); - id = PyLong_AsSsize_t(imageId); - Py_XDECREF(imageId); - im = (Imaging)id; - x_offset -= stroke_width; - y_offset -= stroke_width; + x_offset = round(x_offset - stroke_width); + y_offset = round(y_offset - stroke_width); if (count == 0 || width == 0 || height == 0) { PyMem_Del(glyph_info); return Py_BuildValue("N(ii)", image, x_offset, y_offset); @@ -925,10 +988,11 @@ font_render(FontObject *self, PyObject *args) { FT_Stroker_Set( stroker, - (FT_Fixed)stroke_width * 64, + (FT_Fixed)round(stroke_width * 64), FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, - 0); + 0 + ); } /* @@ -962,8 +1026,8 @@ font_render(FontObject *self, PyObject *args) { } /* set pen position to text origin */ - x = (-x_min + stroke_width + x_start) * 64; - y = (-y_max + (-stroke_width) - y_start) * 64; + x = round((-x_min + stroke_width + x_start) * 64); + y = round((-y_max + (-stroke_width) - y_start) * 64); if (stroker == NULL) { load_flags |= FT_LOAD_RENDER; @@ -983,7 +1047,8 @@ font_render(FontObject *self, PyObject *args) { if (stroker != NULL) { error = FT_Get_Glyph(glyph_slot, &glyph); if (!error) { - error = FT_Glyph_Stroke(&glyph, stroker, 1); + error = stroke_filled ? FT_Glyph_StrokeBorder(&glyph, stroker, 0, 1) + : FT_Glyph_Stroke(&glyph, stroker, 1); } if (!error) { FT_Vector origin = {0, 0}; @@ -1005,160 +1070,169 @@ font_render(FontObject *self, PyObject *args) { yy = -(py + glyph_slot->bitmap_top); } - // Null buffer, is dereferenced in FT_Bitmap_Convert - if (!bitmap.buffer && bitmap.rows) { - PyErr_SetString(PyExc_OSError, "Bitmap missing for glyph"); - goto glyph_error; - } - - /* 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; - case FT_PIXEL_MODE_BGRA: - if (color) { + if (bitmap.buffer) { + /* convert non-8bpp bitmaps */ + switch (bitmap.pixel_mode) { + case FT_PIXEL_MODE_MONO: + convert_scale = 255; break; - } - /* we didn't ask for color, fall through to default */ - default: - PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); - goto glyph_error; - } + 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; + case FT_PIXEL_MODE_BGRA: + if (color) { + break; + } + /* we didn't ask for color, fall through to default */ + default: + PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); + goto glyph_error; + } - /* clip glyph bitmap width to target image bounds */ - x0 = 0; - x1 = bitmap.width; - if (xx < 0) { - x0 = -xx; - } - if (xx + x1 > im->xsize) { - x1 = im->xsize - xx; - } + /* clip glyph bitmap width to target image bounds */ + x0 = 0; + x1 = bitmap.width; + if (xx < 0) { + x0 = -xx; + } + if (xx + x1 > im->xsize) { + x1 = im->xsize - xx; + } - 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 *target; - unsigned int tmp; - 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; - } - if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) { - /* paste color glyph */ - for (k = x0; k < x1; k++) { - unsigned int src_alpha = source[k * 4 + 3]; - - /* paste only if source has data */ - if (src_alpha > 0) { - /* unpremultiply BGRa */ - int src_red = - CLIP8((255 * (int)source[k * 4 + 2]) / src_alpha); - int src_green = - CLIP8((255 * (int)source[k * 4 + 1]) / src_alpha); - int src_blue = - CLIP8((255 * (int)source[k * 4 + 0]) / src_alpha); - - /* blend required if target has data */ - if (target[k * 4 + 3] > 0) { - /* blend RGBA colors */ - target[k * 4 + 0] = - BLEND(src_alpha, target[k * 4 + 0], src_red, tmp); - target[k * 4 + 1] = - BLEND(src_alpha, target[k * 4 + 1], src_green, tmp); - target[k * 4 + 2] = - BLEND(src_alpha, target[k * 4 + 2], src_blue, tmp); - target[k * 4 + 3] = CLIP8( - src_alpha + - MULDIV255( - target[k * 4 + 3], (255 - src_alpha), tmp)); - } else { - /* paste unpremultiplied RGBA values */ - target[k * 4 + 0] = src_red; - target[k * 4 + 1] = src_green; - target[k * 4 + 2] = src_blue; - target[k * 4 + 3] = src_alpha; - } - } - } - } else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { + 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 *target; + unsigned int tmp; if (color) { - unsigned char *ink = (unsigned char *)&foreground_ink; + /* 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; + } + if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) { + /* paste color glyph */ for (k = x0; k < x1; k++) { - unsigned int src_alpha = source[k] * convert_scale; + unsigned int src_alpha = source[k * 4 + 3]; + + /* paste only if source has data */ if (src_alpha > 0) { + /* unpremultiply BGRa */ + int src_red = + CLIP8((255 * (int)source[k * 4 + 2]) / src_alpha); + int src_green = + CLIP8((255 * (int)source[k * 4 + 1]) / src_alpha); + int src_blue = + CLIP8((255 * (int)source[k * 4 + 0]) / src_alpha); + + /* blend required if target has data */ if (target[k * 4 + 3] > 0) { + /* blend RGBA colors */ target[k * 4 + 0] = BLEND( - src_alpha, target[k * 4 + 0], ink[0], tmp); + src_alpha, target[k * 4 + 0], src_red, tmp + ); target[k * 4 + 1] = BLEND( - src_alpha, target[k * 4 + 1], ink[1], tmp); + src_alpha, target[k * 4 + 1], src_green, tmp + ); target[k * 4 + 2] = BLEND( - src_alpha, target[k * 4 + 2], ink[2], tmp); + src_alpha, target[k * 4 + 2], src_blue, tmp + ); target[k * 4 + 3] = CLIP8( src_alpha + MULDIV255( - target[k * 4 + 3], (255 - src_alpha), tmp)); + target[k * 4 + 3], (255 - src_alpha), tmp + ) + ); } else { - target[k * 4 + 0] = ink[0]; - target[k * 4 + 1] = ink[1]; - target[k * 4 + 2] = ink[2]; + /* paste unpremultiplied RGBA values */ + target[k * 4 + 0] = src_red; + target[k * 4 + 1] = src_green; + target[k * 4 + 2] = src_blue; target[k * 4 + 3] = src_alpha; } } } - } else { - for (k = x0; k < x1; k++) { - unsigned int src_alpha = source[k] * convert_scale; - if (src_alpha > 0) { - target[k] = - target[k] > 0 - ? CLIP8( - src_alpha + - MULDIV255( - target[k], (255 - src_alpha), tmp)) - : src_alpha; + } else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { + if (color) { + unsigned char *ink = (unsigned char *)&foreground_ink; + for (k = x0; k < x1; k++) { + unsigned int src_alpha = source[k] * convert_scale; + if (src_alpha > 0) { + if (target[k * 4 + 3] > 0) { + target[k * 4 + 0] = BLEND( + src_alpha, target[k * 4 + 0], ink[0], tmp + ); + target[k * 4 + 1] = BLEND( + src_alpha, target[k * 4 + 1], ink[1], tmp + ); + target[k * 4 + 2] = BLEND( + src_alpha, target[k * 4 + 2], ink[2], tmp + ); + target[k * 4 + 3] = CLIP8( + src_alpha + MULDIV255( + target[k * 4 + 3], + (255 - src_alpha), + tmp + ) + ); + } else { + target[k * 4 + 0] = ink[0]; + target[k * 4 + 1] = ink[1]; + target[k * 4 + 2] = ink[2]; + target[k * 4 + 3] = src_alpha; + } + } + } + } else { + for (k = x0; k < x1; k++) { + unsigned int src_alpha = source[k] * convert_scale; + if (src_alpha > 0) { + target[k] = + target[k] > 0 + ? CLIP8( + src_alpha + + MULDIV255( + target[k], (255 - src_alpha), tmp + ) + ) + : src_alpha; + } } } + } else { + PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); + goto glyph_error; } - } else { - PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); - goto glyph_error; } + source += bitmap.pitch; } - source += bitmap.pitch; } x += glyph_info[i].x_advance; y += glyph_info[i].y_advance; @@ -1187,8 +1261,15 @@ font_render(FontObject *self, PyObject *args) { return NULL; } -#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ - (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) +static PyObject * +font_render(FontObject *self, PyObject *args) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_render_impl(self, args); + Py_END_CRITICAL_SECTION(); + return result; +} + static PyObject * font_getvarnames(FontObject *self) { int error; @@ -1209,30 +1290,48 @@ font_getvarnames(FontObject *self) { return NULL; } + int *list_names_filled = PyMem_Malloc(num_namedstyles * sizeof(int)); + if (list_names_filled == NULL) { + Py_DECREF(list_names); + FT_Done_MM_Var(library, master); + return PyErr_NoMemory(); + } + + for (unsigned int i = 0; i < num_namedstyles; i++) { + list_names_filled[i] = 0; + } + 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) { + PyMem_Free(list_names_filled); Py_DECREF(list_names); FT_Done_MM_Var(library, master); return geterror(error); } for (j = 0; j < num_namedstyles; j++) { - if (PyList_GetItem(list_names, j) != NULL) { + if (list_names_filled[j]) { continue; } if (master->namedstyle[j].strid == name.name_id) { list_name = Py_BuildValue("y#", name.string, name.string_len); + if (list_name == NULL) { + PyMem_Free(list_names_filled); + Py_DECREF(list_names); + FT_Done_MM_Var(library, master); + return NULL; + } PyList_SetItem(list_names, j, list_name); - break; + list_names_filled[j] = 1; } } } + PyMem_Free(list_names_filled); FT_Done_MM_Var(library, master); - return list_names; } @@ -1289,9 +1388,14 @@ font_getvaraxes(FontObject *self) { if (name.name_id == axis.strid) { axis_name = Py_BuildValue("y#", name.string, name.string_len); - PyDict_SetItemString( - list_axis, "name", axis_name ? axis_name : Py_None); - Py_XDECREF(axis_name); + if (axis_name == NULL) { + Py_DECREF(list_axis); + Py_DECREF(list_axes); + FT_Done_MM_Var(library, master); + return NULL; + } + PyDict_SetItemString(list_axis, "name", axis_name); + Py_DECREF(axis_name); break; } } @@ -1305,7 +1409,7 @@ font_getvaraxes(FontObject *self) { } static PyObject * -font_setvarname(FontObject *self, PyObject *args) { +font_setvarname_impl(FontObject *self, PyObject *args) { int error; int instance_index; @@ -1318,12 +1422,20 @@ font_setvarname(FontObject *self, PyObject *args) { return geterror(error); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * -font_setvaraxes(FontObject *self, PyObject *args) { +font_setvarname(FontObject *self, PyObject *args) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_setvarname_impl(self, args); + Py_END_CRITICAL_SECTION(); + return result; +} + +static PyObject * +font_setvaraxes_impl(FontObject *self, PyObject *args) { int error; PyObject *axes, *item; @@ -1345,7 +1457,12 @@ font_setvaraxes(FontObject *self, PyObject *args) { return PyErr_NoMemory(); } for (i = 0; i < num_coords; i++) { - item = PyList_GET_ITEM(axes, i); + item = PyList_GetItemRef(axes, i); + if (item == NULL) { + free(coords); + return NULL; + } + if (PyFloat_Check(item)) { coord = PyFloat_AS_DOUBLE(item); } else if (PyLong_Check(item)) { @@ -1353,10 +1470,12 @@ font_setvaraxes(FontObject *self, PyObject *args) { } else if (PyNumber_Check(item)) { coord = PyFloat_AsDouble(item); } else { + Py_DECREF(item); free(coords); PyErr_SetString(PyExc_TypeError, "list must contain numbers"); return NULL; } + Py_DECREF(item); coords[i] = coord * 65536; } @@ -1366,15 +1485,24 @@ font_setvaraxes(FontObject *self, PyObject *args) { return geterror(error); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; +} + +static PyObject * +font_setvaraxes(FontObject *self, PyObject *args) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_setvaraxes_impl(self, args); + Py_END_CRITICAL_SECTION(); + return result; } -#endif static void font_dealloc(FontObject *self) { if (self->face) { + MUTEX_LOCK(&ft_library_mutex); FT_Done_Face(self->face); + MUTEX_UNLOCK(&ft_library_mutex); } if (self->font_bytes) { PyMem_Free(self->font_bytes); @@ -1386,14 +1514,12 @@ static PyMethodDef font_methods[] = { {"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}}; + {NULL, NULL} +}; static PyObject * font_getattr_family(FontObject *self, void *closure) { @@ -1412,30 +1538,75 @@ font_getattr_style(FontObject *self, void *closure) { } static PyObject * -font_getattr_ascent(FontObject *self, void *closure) { +font_getattr_ascent_impl(FontObject *self, void *closure) { return PyLong_FromLong(PIXEL(self->face->size->metrics.ascender)); } static PyObject * -font_getattr_descent(FontObject *self, void *closure) { +font_getattr_ascent(FontObject *self, void *closure) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_getattr_ascent_impl(self, closure); + Py_END_CRITICAL_SECTION(); + return result; +} + +static PyObject * +font_getattr_descent_impl(FontObject *self, void *closure) { return PyLong_FromLong(-PIXEL(self->face->size->metrics.descender)); } static PyObject * -font_getattr_height(FontObject *self, void *closure) { +font_getattr_descent(FontObject *self, void *closure) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_getattr_descent_impl(self, closure); + Py_END_CRITICAL_SECTION(); + return result; +} + +static PyObject * +font_getattr_height_impl(FontObject *self, void *closure) { return PyLong_FromLong(PIXEL(self->face->size->metrics.height)); } static PyObject * -font_getattr_x_ppem(FontObject *self, void *closure) { +font_getattr_height(FontObject *self, void *closure) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_getattr_height_impl(self, closure); + Py_END_CRITICAL_SECTION(); + return result; +} + +static PyObject * +font_getattr_x_ppem_impl(FontObject *self, void *closure) { return PyLong_FromLong(self->face->size->metrics.x_ppem); } static PyObject * -font_getattr_y_ppem(FontObject *self, void *closure) { +font_getattr_x_ppem(FontObject *self, void *closure) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_getattr_x_ppem_impl(self, closure); + Py_END_CRITICAL_SECTION(); + return result; +} + +static PyObject * +font_getattr_y_ppem_impl(FontObject *self, void *closure) { return PyLong_FromLong(self->face->size->metrics.y_ppem); } +static PyObject * +font_getattr_y_ppem(FontObject *self, void *closure) { + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(self); + result = font_getattr_y_ppem_impl(self, closure); + Py_END_CRITICAL_SECTION(); + return result; +} + static PyObject * font_getattr_glyphs(FontObject *self, void *closure) { return PyLong_FromLong(self->face->num_glyphs); @@ -1450,43 +1621,20 @@ static struct PyGetSetDef font_getsetters[] = { {"x_ppem", (getter)font_getattr_x_ppem}, {"y_ppem", (getter)font_getattr_y_ppem}, {"glyphs", (getter)font_getattr_glyphs}, - {NULL}}; + {NULL} +}; static PyTypeObject Font_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "Font", /*tp_name*/ - sizeof(FontObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)font_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 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*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "Font", + .tp_basicsize = sizeof(FontObject), + .tp_dealloc = (destructor)font_dealloc, + .tp_methods = font_methods, + .tp_getset = font_getsetters, }; 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) { @@ -1497,7 +1645,9 @@ setup_module(PyObject *m) { d = PyModule_GetDict(m); /* Ready object type */ - PyType_Ready(&Font_Type); + if (PyType_Ready(&Font_Type) < 0) { + return -1; + } if (FT_Init_FreeType(&library)) { return 0; /* leave it uninitialized */ @@ -1558,23 +1708,22 @@ setup_module(PyObject *m) { return 0; } +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__imagingft(void) { - PyObject *m; - static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingft", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - _functions, /* m_methods */ + .m_name = "_imagingft", + .m_methods = _functions, + .m_slots = slots }; - m = PyModule_Create(&module_def); - - if (setup_module(m) < 0) { - return NULL; - } - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingmath.c b/src/_imagingmath.c index 067c165b248..c04361468ed 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -23,6 +23,9 @@ #define MAX_INT32 2147483647.0 #define MIN_INT32 -2147483648.0 +#define MATH_FUNC_UNOP_MAGIC "Pillow Math unary func" +#define MATH_FUNC_BINOP_MAGIC "Pillow Math binary func" + #define UNOP(name, op, type) \ void name(Imaging out, Imaging im1) { \ int x, y; \ @@ -168,20 +171,37 @@ _unop(PyObject *self, PyObject *args) { Imaging im1; void (*unop)(Imaging, Imaging); - Py_ssize_t op, i0, i1; - if (!PyArg_ParseTuple(args, "nnn", &op, &i0, &i1)) { + PyObject *op, *i0, *i1; + if (!PyArg_ParseTuple(args, "OOO", &op, &i0, &i1)) { return NULL; } - out = (Imaging)i0; - im1 = (Imaging)i1; + if (!PyCapsule_IsValid(op, MATH_FUNC_UNOP_MAGIC)) { + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", MATH_FUNC_UNOP_MAGIC); + return NULL; + } + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || + !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); + return NULL; + } - unop = (void *)op; + unop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_UNOP_MAGIC); + if (!unop) { + return NULL; + } + out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!out) { + return NULL; + } + im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + if (!im1) { + return NULL; + } unop(out, im1); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -191,29 +211,60 @@ _binop(PyObject *self, PyObject *args) { Imaging im2; void (*binop)(Imaging, Imaging, Imaging); - Py_ssize_t op, i0, i1, i2; - if (!PyArg_ParseTuple(args, "nnnn", &op, &i0, &i1, &i2)) { + PyObject *op, *i0, *i1, *i2; + if (!PyArg_ParseTuple(args, "OOOO", &op, &i0, &i1, &i2)) { return NULL; } - out = (Imaging)i0; - im1 = (Imaging)i1; - im2 = (Imaging)i2; + if (!PyCapsule_IsValid(op, MATH_FUNC_BINOP_MAGIC)) { + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", MATH_FUNC_BINOP_MAGIC); + return NULL; + } + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || + !PyCapsule_IsValid(i1, IMAGING_MAGIC) || + !PyCapsule_IsValid(i2, IMAGING_MAGIC)) { + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); + return NULL; + } - binop = (void *)op; + binop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_BINOP_MAGIC); + if (!binop) { + return NULL; + } + out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!out) { + return NULL; + } + im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + if (!im1) { + return NULL; + } + im2 = (Imaging)PyCapsule_GetPointer(i2, IMAGING_MAGIC); + if (!im2) { + return NULL; + } binop(out, im1, im2); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyMethodDef _functions[] = { - {"unop", _unop, 1}, {"binop", _binop, 1}, {NULL, NULL}}; + {"unop", _unop, 1}, {"binop", _binop, 1}, {NULL, NULL} +}; + +static void +install_unary(PyObject *d, char *name, void *func) { + PyObject *v = PyCapsule_New(func, MATH_FUNC_UNOP_MAGIC, NULL); + if (!v || PyDict_SetItemString(d, name, v)) { + PyErr_Clear(); + } + Py_XDECREF(v); +} static void -install(PyObject *d, char *name, void *value) { - PyObject *v = PyLong_FromSsize_t((Py_ssize_t)value); +install_binary(PyObject *d, char *name, void *func) { + PyObject *v = PyCapsule_New(func, MATH_FUNC_BINOP_MAGIC, NULL); if (!v || PyDict_SetItemString(d, name, v)) { PyErr_Clear(); } @@ -224,71 +275,70 @@ static int setup_module(PyObject *m) { PyObject *d = PyModule_GetDict(m); - install(d, "abs_I", abs_I); - install(d, "neg_I", neg_I); - install(d, "add_I", add_I); - install(d, "sub_I", sub_I); - install(d, "diff_I", diff_I); - install(d, "mul_I", mul_I); - install(d, "div_I", div_I); - install(d, "mod_I", mod_I); - install(d, "min_I", min_I); - install(d, "max_I", max_I); - install(d, "pow_I", pow_I); - - install(d, "invert_I", invert_I); - install(d, "and_I", and_I); - install(d, "or_I", or_I); - install(d, "xor_I", xor_I); - install(d, "lshift_I", lshift_I); - install(d, "rshift_I", rshift_I); - - install(d, "eq_I", eq_I); - install(d, "ne_I", ne_I); - install(d, "lt_I", lt_I); - install(d, "le_I", le_I); - install(d, "gt_I", gt_I); - install(d, "ge_I", ge_I); - - install(d, "abs_F", abs_F); - install(d, "neg_F", neg_F); - install(d, "add_F", add_F); - install(d, "sub_F", sub_F); - install(d, "diff_F", diff_F); - install(d, "mul_F", mul_F); - install(d, "div_F", div_F); - install(d, "mod_F", mod_F); - install(d, "min_F", min_F); - install(d, "max_F", max_F); - install(d, "pow_F", pow_F); - - install(d, "eq_F", eq_F); - install(d, "ne_F", ne_F); - install(d, "lt_F", lt_F); - install(d, "le_F", le_F); - install(d, "gt_F", gt_F); - install(d, "ge_F", ge_F); + install_unary(d, "abs_I", abs_I); + install_unary(d, "neg_I", neg_I); + install_binary(d, "add_I", add_I); + install_binary(d, "sub_I", sub_I); + install_binary(d, "diff_I", diff_I); + install_binary(d, "mul_I", mul_I); + install_binary(d, "div_I", div_I); + install_binary(d, "mod_I", mod_I); + install_binary(d, "min_I", min_I); + install_binary(d, "max_I", max_I); + install_binary(d, "pow_I", pow_I); + + install_unary(d, "invert_I", invert_I); + install_binary(d, "and_I", and_I); + install_binary(d, "or_I", or_I); + install_binary(d, "xor_I", xor_I); + install_binary(d, "lshift_I", lshift_I); + install_binary(d, "rshift_I", rshift_I); + + install_binary(d, "eq_I", eq_I); + install_binary(d, "ne_I", ne_I); + install_binary(d, "lt_I", lt_I); + install_binary(d, "le_I", le_I); + install_binary(d, "gt_I", gt_I); + install_binary(d, "ge_I", ge_I); + + install_unary(d, "abs_F", abs_F); + install_unary(d, "neg_F", neg_F); + install_binary(d, "add_F", add_F); + install_binary(d, "sub_F", sub_F); + install_binary(d, "diff_F", diff_F); + install_binary(d, "mul_F", mul_F); + install_binary(d, "div_F", div_F); + install_binary(d, "mod_F", mod_F); + install_binary(d, "min_F", min_F); + install_binary(d, "max_F", max_F); + install_binary(d, "pow_F", pow_F); + + install_binary(d, "eq_F", eq_F); + install_binary(d, "ne_F", ne_F); + install_binary(d, "lt_F", lt_F); + install_binary(d, "le_F", le_F); + install_binary(d, "gt_F", gt_F); + install_binary(d, "ge_F", ge_F); return 0; } +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__imagingmath(void) { - PyObject *m; - static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingmath", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - _functions, /* m_methods */ + .m_name = "_imagingmath", + .m_methods = _functions, + .m_slots = slots }; - m = PyModule_Create(&module_def); - - if (setup_module(m) < 0) { - return NULL; - } - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 8815c2b7ec6..b6f307c849e 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -11,7 +11,6 @@ * See the README file for information on usage and redistribution. */ -#include "Python.h" #include "libImaging/Imaging.h" #define LUT_SIZE (1 << 9) @@ -30,43 +29,42 @@ static PyObject * apply(PyObject *self, PyObject *args) { const char *lut; - PyObject *py_lut; - Py_ssize_t lut_len, i0, i1; + Py_ssize_t lut_len; + PyObject *i0, *i1; Imaging imgin, imgout; int width, height; int row_idx, col_idx; UINT8 **inrows, **outrows; int num_changed_pixels = 0; - if (!PyArg_ParseTuple(args, "Onn", &py_lut, &i0, &i1)) { - PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); + if (!PyArg_ParseTuple(args, "s#OO", &lut, &lut_len, &i0, &i1)) { return NULL; } - if (!PyBytes_Check(py_lut)) { - PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object"); - return NULL; - } - - lut_len = PyBytes_Size(py_lut); - if (lut_len < LUT_SIZE) { PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); return NULL; } - lut = PyBytes_AsString(py_lut); + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || + !PyCapsule_IsValid(i1, IMAGING_MAGIC)) { + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); + return NULL; + } - imgin = (Imaging)i0; - imgout = (Imaging)i1; + imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!imgin) { + return NULL; + } + imgout = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + if (!imgout) { + return NULL; + } width = imgin->xsize; height = imgin->ysize; - 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 (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1 || + imgout->type != IMAGING_TYPE_UINT8 || imgout->bands != 1) { PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; } @@ -129,46 +127,42 @@ apply(PyObject *self, PyObject *args) { static PyObject * match(PyObject *self, PyObject *args) { const char *lut; - PyObject *py_lut; - Py_ssize_t lut_len, i0; + Py_ssize_t lut_len; + PyObject *i0; Imaging imgin; int width, height; int row_idx, col_idx; UINT8 **inrows; - PyObject *ret = PyList_New(0); - if (ret == NULL) { + + if (!PyArg_ParseTuple(args, "s#O", &lut, &lut_len, &i0)) { return NULL; } - if (!PyArg_ParseTuple(args, "On", &py_lut, &i0)) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); + if (lut_len < LUT_SIZE) { + PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); return NULL; } - if (!PyBytes_Check(py_lut)) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object"); + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); return NULL; } - lut_len = PyBytes_Size(py_lut); - - if (lut_len < LUT_SIZE) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); + imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!imgin) { return NULL; } - lut = PyBytes_AsString(py_lut); - imgin = (Imaging)i0; - if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { - Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; } + PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } + inrows = imgin->image8; width = imgin->xsize; height = imgin->ysize; @@ -200,6 +194,10 @@ match(PyObject *self, PyObject *args) { (b6 << 6) | (b7 << 7) | (b8 << 8)); if (lut[lut_idx]) { PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); + if (!coordObj) { + Py_DECREF(ret); + return NULL; + } PyList_Append(ret, coordObj); Py_XDECREF(coordObj); } @@ -215,31 +213,43 @@ match(PyObject *self, PyObject *args) { */ static PyObject * get_on_pixels(PyObject *self, PyObject *args) { - Py_ssize_t i0; + PyObject *i0; Imaging img; UINT8 **rows; int row_idx, col_idx; int width, height; - PyObject *ret = PyList_New(0); - if (ret == NULL) { + + if (!PyArg_ParseTuple(args, "O", &i0)) { return NULL; } - if (!PyArg_ParseTuple(args, "n", &i0)) { - Py_DECREF(ret); - PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); + return NULL; + } + + img = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!img) { return NULL; } - img = (Imaging)i0; rows = img->image8; width = img->xsize; height = img->ysize; + PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } + for (row_idx = 0; row_idx < height; row_idx++) { UINT8 *row = rows[row_idx]; for (col_idx = 0; col_idx < width; col_idx++) { if (row[col_idx]) { PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); + if (!coordObj) { + Py_DECREF(ret); + return NULL; + } PyList_Append(ret, coordObj); Py_XDECREF(coordObj); } @@ -253,21 +263,25 @@ static PyMethodDef functions[] = { {"apply", (PyCFunction)apply, METH_VARARGS, NULL}, {"get_on_pixels", (PyCFunction)get_on_pixels, METH_VARARGS, NULL}, {"match", (PyCFunction)match, METH_VARARGS, NULL}, - {NULL, NULL, 0, NULL}}; + {NULL, NULL, 0, NULL} +}; + +static PyModuleDef_Slot slots[] = { +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; PyMODINIT_FUNC PyInit__imagingmorph(void) { - 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 */ + .m_name = "_imagingmorph", + .m_doc = "A module for doing image morphology", + .m_methods = functions, + .m_slots = slots }; - m = PyModule_Create(&module_def); - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingtk.c b/src/_imagingtk.c index efa7fc1b6fa..68d7bf4cd11 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -37,8 +37,7 @@ _tkinit(PyObject *self, PyObject *args) { /* This will bomb if interp is invalid... */ TkImaging_Init(interp); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyMethodDef functions[] = { @@ -47,20 +46,22 @@ static PyMethodDef functions[] = { {NULL, NULL} /* sentinel */ }; +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, load_tkinter_funcs}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__imagingtk(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingtk", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - functions, /* m_methods */ + .m_name = "_imagingtk", + .m_methods = functions, + .m_slots = slots }; - PyObject *m; - m = PyModule_Create(&module_def); - if (load_tkinter_funcs() != 0) { - Py_DECREF(m); - return NULL; - } - return m; + + return PyModuleDef_Init(&module_def); } diff --git a/src/_webp.c b/src/_webp.c index 0a70e335757..115141273f5 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -4,8 +4,6 @@ #include #include #include - -#ifdef HAVE_WEBPMUX #include #include @@ -13,12 +11,10 @@ * 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 had some significant differences, so we require later - * versions, before enabling animation support. + * versions. */ -#if WEBP_MUX_ABI_VERSION >= 0x0104 && WEBP_DEMUX_ABI_VERSION >= 0x0105 -#define HAVE_WEBPANIM -#endif - +#if WEBP_MUX_ABI_VERSION < 0x0106 || WEBP_DEMUX_ABI_VERSION < 0x0107 +#error libwebp 0.5.0 and above is required. Upgrade libwebp or build Pillow with --disable-webp flag #endif void @@ -35,14 +31,13 @@ ImagingSectionLeave(ImagingSectionCookie *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"}; + "WEBP_MUX_NOT_ENOUGH_DATA" +}; PyObject * HandleMuxError(WebPMuxError err, char *chunk) { @@ -61,7 +56,8 @@ HandleMuxError(WebPMuxError err, char *chunk) { sprintf(message, "could not assemble chunks: %s", kErrorMessages[-err]); } else { message_len = sprintf( - message, "could not set %.4s chunk: %s", chunk, kErrorMessages[-err]); + message, "could not set %.4s chunk: %s", chunk, kErrorMessages[-err] + ); } if (message_len < 0) { PyErr_SetString(PyExc_RuntimeError, "failed to construct error message"); @@ -87,14 +83,53 @@ HandleMuxError(WebPMuxError err, char *chunk) { return NULL; } -#endif +/* -------------------------------------------------------------------- */ +/* Frame import */ +/* -------------------------------------------------------------------- */ + +static int +import_frame_libwebp(WebPPicture *frame, Imaging im) { + if (im->mode != IMAGING_MODE_RGBA && im->mode != IMAGING_MODE_RGB && + im->mode != IMAGING_MODE_RGBX) { + PyErr_SetString(PyExc_ValueError, "unsupported image mode"); + return -1; + } + + frame->width = im->xsize; + frame->height = im->ysize; + frame->use_argb = 1; // Don't convert RGB pixels to YUV + + if (!WebPPictureAlloc(frame)) { + PyErr_SetString(PyExc_MemoryError, "can't allocate picture frame"); + return -2; + } + + int ignore_fourth_channel = im->mode != IMAGING_MODE_RGBA; + for (int y = 0; y < im->ysize; ++y) { + UINT8 *src = (UINT8 *)im->image32[y]; + UINT32 *dst = frame->argb + frame->argb_stride * y; + if (ignore_fourth_channel) { + for (int x = 0; x < im->xsize; ++x) { + dst[x] = + ((UINT32)(src[x * 4 + 2]) | ((UINT32)(src[x * 4 + 1]) << 8) | + ((UINT32)(src[x * 4]) << 16) | (0xff << 24)); + } + } else { + for (int x = 0; x < im->xsize; ++x) { + dst[x] = + ((UINT32)(src[x * 4 + 2]) | ((UINT32)(src[x * 4 + 1]) << 8) | + ((UINT32)(src[x * 4]) << 16) | ((UINT32)(src[x * 4 + 3]) << 24)); + } + } + } + + return 0; +} /* -------------------------------------------------------------------- */ /* WebP Animation Support */ /* -------------------------------------------------------------------- */ -#ifdef HAVE_WEBPANIM - // Encoder type typedef struct { PyObject_HEAD WebPAnimEncoder *enc; @@ -108,7 +143,7 @@ typedef struct { PyObject_HEAD WebPAnimDecoder *dec; WebPAnimInfo info; WebPData data; - char *mode; + ModeID mode; } WebPAnimDecoderObject; static PyTypeObject WebPAnimDecoder_Type; @@ -129,7 +164,7 @@ _anim_encoder_new(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "iiIiiiiii", + "(ii)Iiiiiii", &width, &height, &bgcolor, @@ -138,7 +173,8 @@ _anim_encoder_new(PyObject *self, PyObject *args) { &kmin, &kmax, &allow_mixed, - &verbose)) { + &verbose + )) { return NULL; } @@ -183,20 +219,19 @@ _anim_encoder_dealloc(PyObject *self) { WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; WebPPictureFree(&(encp->frame)); WebPAnimEncoderDelete(encp->enc); + Py_TYPE(self)->tp_free(self); } PyObject * _anim_encoder_add(PyObject *self, PyObject *args) { - uint8_t *rgb; - Py_ssize_t size; + PyObject *i0; + Imaging im; int timestamp; - int width; - int height; - char *mode; int lossless; float quality_factor; float alpha_quality_factor; int method; + ImagingSectionCookie cookie; WebPConfig config; WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; WebPAnimEncoder *enc = encp->enc; @@ -204,26 +239,33 @@ _anim_encoder_add(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "z#iiisiffi", - (char **)&rgb, - &size, + "Oiiffi", + &i0, ×tamp, - &width, - &height, - &mode, &lossless, &quality_factor, &alpha_quality_factor, - &method)) { + &method + )) { return NULL; } // Check for NULL frame, which sets duration of final frame - if (!rgb) { + if (i0 == Py_None) { WebPAnimEncoderAdd(enc, NULL, timestamp, NULL); Py_RETURN_NONE; } + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); + return NULL; + } + + im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!im) { + return NULL; + } + // Setup config for this frame if (!WebPConfigInit(&config)) { PyErr_SetString(PyExc_RuntimeError, "failed to initialize config!"); @@ -240,20 +282,15 @@ _anim_encoder_add(PyObject *self, PyObject *args) { return NULL; } - // 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) { - WebPPictureImportRGBA(frame, rgb, 4 * width); - } else if (strcmp(mode, "RGBX") == 0) { - WebPPictureImportRGBX(frame, rgb, 4 * width); - } else { - WebPPictureImportRGB(frame, rgb, 3 * width); + if (import_frame_libwebp(frame, im)) { + return NULL; } - // Add the frame to the encoder - if (!WebPAnimEncoderAdd(enc, frame, timestamp, &config)) { + ImagingSectionEnter(&cookie); + int ok = WebPAnimEncoderAdd(enc, frame, timestamp, &config); + ImagingSectionLeave(&cookie); + + if (!ok) { PyErr_SetString(PyExc_RuntimeError, WebPAnimEncoderGetError(enc)); return NULL; } @@ -283,7 +320,8 @@ _anim_encoder_assemble(PyObject *self, PyObject *args) { &exif_bytes, &exif_size, &xmp_bytes, - &xmp_size)) { + &xmp_size + )) { return NULL; } @@ -362,7 +400,7 @@ _anim_decoder_new(PyObject *self, PyObject *args) { const uint8_t *webp; Py_ssize_t size; WebPData webp_src; - char *mode; + ModeID mode; WebPDecoderConfig config; WebPAnimDecoderObject *decp = NULL; WebPAnimDecoder *dec = NULL; @@ -375,10 +413,10 @@ _anim_decoder_new(PyObject *self, PyObject *args) { webp_src.size = size; // Sniff the mode, since the decoder API doesn't tell us - mode = "RGBA"; + mode = IMAGING_MODE_RGBA; if (WebPGetFeatures(webp, size, &config.input) == VP8_STATUS_OK) { if (!config.input.has_alpha) { - mode = "RGBX"; + mode = IMAGING_MODE_RGBX; } } @@ -407,6 +445,7 @@ _anim_decoder_dealloc(PyObject *self) { WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; WebPDataClear(&(decp->data)); WebPAnimDecoderDelete(decp->dec); + Py_TYPE(self)->tp_free(self); } PyObject * @@ -415,13 +454,14 @@ _anim_decoder_get_info(PyObject *self) { WebPAnimInfo *info = &(decp->info); return Py_BuildValue( - "IIIIIs", + "(II)IIIs", info->canvas_width, info->canvas_height, info->loop_count, info->bgcolor, info->frame_count, - decp->mode); + getModeData(decp->mode)->name + ); } PyObject * @@ -466,7 +506,11 @@ _anim_decoder_get_next(PyObject *self) { } bytes = PyBytes_FromStringAndSize( - (char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height); + (char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height + ); + if (!bytes) { + return NULL; + } ret = Py_BuildValue("Si", bytes, timestamp); @@ -494,36 +538,10 @@ static struct PyMethodDef _anim_encoder_methods[] = { // WebPAnimEncoder type definition static PyTypeObject WebPAnimEncoder_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "WebPAnimEncoder", /*tp_name */ - sizeof(WebPAnimEncoderObject), /*tp_basicsize */ - 0, /*tp_itemsize */ - /* methods */ - (destructor)_anim_encoder_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 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*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "WebPAnimEncoder", + .tp_basicsize = sizeof(WebPAnimEncoderObject), + .tp_dealloc = (destructor)_anim_encoder_dealloc, + .tp_methods = _anim_encoder_methods, }; // WebPAnimDecoder methods @@ -537,66 +555,33 @@ static struct PyMethodDef _anim_decoder_methods[] = { // WebPAnimDecoder type definition static PyTypeObject WebPAnimDecoder_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "WebPAnimDecoder", /*tp_name */ - sizeof(WebPAnimDecoderObject), /*tp_basicsize */ - 0, /*tp_itemsize */ - /* methods */ - (destructor)_anim_decoder_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 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*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "WebPAnimDecoder", + .tp_basicsize = sizeof(WebPAnimDecoderObject), + .tp_dealloc = (destructor)_anim_decoder_dealloc, + .tp_methods = _anim_decoder_methods, }; -#endif - /* -------------------------------------------------------------------- */ /* Legacy WebP Support */ /* -------------------------------------------------------------------- */ PyObject * WebPEncode_wrapper(PyObject *self, PyObject *args) { - int width; - int height; int lossless; float quality_factor; float alpha_quality_factor; int method; int exact; - uint8_t *rgb; + Imaging im; + PyObject *i0; 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; @@ -605,15 +590,11 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "y#iiiffss#iis#s#", - (char **)&rgb, - &size, - &width, - &height, + "Oiffs#iis#s#", + &i0, &lossless, &quality_factor, &alpha_quality_factor, - &mode, &icc_bytes, &icc_size, &method, @@ -621,18 +602,19 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { &exif_bytes, &exif_size, &xmp_bytes, - &xmp_size)) { + &xmp_size + )) { return NULL; } - rgba_mode = strcmp(mode, "RGBA") == 0; - if (!rgba_mode && strcmp(mode, "RGB") != 0) { - Py_RETURN_NONE; + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC); + return NULL; } - channels = rgba_mode ? 4 : 3; - if (size < width * height * channels) { - Py_RETURN_NONE; + im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!im) { + return NULL; } // Setup config for this frame @@ -644,10 +626,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { config.quality = quality_factor; config.alpha_quality = alpha_quality_factor; config.method = method; -#if WEBP_ENCODER_ABI_VERSION >= 0x0209 - // the "exact" flag is only available in libwebp 0.5.0 and later config.exact = exact; -#endif // Validate the config if (!WebPValidateConfig(&config)) { @@ -659,14 +638,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { 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 { - WebPPictureImportRGB(&pic, rgb, channels * width); + if (import_frame_libwebp(&pic, im)) { + return NULL; } WebPMemoryWriterInit(&writer); @@ -678,20 +652,25 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { 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); + if (!ok) { + int error_code = (&pic)->error_code; + char message[50] = ""; + if (error_code == VP8_ENC_ERROR_BAD_DIMENSION) { + sprintf( + message, + ": Image size exceeds WebP limit of %d pixels", + WEBP_MAX_DIMENSION + ); + } + PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); free(output); - return ret; + return NULL; } -#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 @@ -767,127 +746,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { return ret; } } -#endif Py_RETURN_NONE; } -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; - WebPDecoderConfig config; - VP8StatusCode vp8_status_code = VP8_STATUS_OK; - char *mode = "RGB"; - - if (!PyArg_ParseTuple(args, "S", &webp_string)) { - return NULL; - } - - if (!WebPInitDecoderConfig(&config)) { - Py_RETURN_NONE; - } - - PyBytes_AsStringAndSize((PyObject *)webp_string, (char **)&webp, &size); - - vp8_status_code = WebPGetFeatures(webp, size, &config.input); - if (vp8_status_code == VP8_STATUS_OK) { - // If we don't set it, we don't get alpha. - // Initialized to MODE_RGB - if (config.input.has_alpha) { - config.output.colorspace = MODE_RGBA; - mode = "RGBA"; - } - -#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; - } - - webp = image.bitstream.bytes; - size = image.bitstream.size; - - 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, "EXIF", &exif_data)) { - exif = PyBytes_FromStringAndSize( - (const char *)exif_data.bytes, exif_data.size); - } - - WebPDataClear(&image.bitstream); - WebPMuxDelete(mux); - } -#endif - } - - 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); - } 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); - } - - pymode = PyUnicode_FromString(mode); - 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); - - Py_XDECREF(bytes); - Py_XDECREF(pymode); - Py_XDECREF(icc_profile); - Py_XDECREF(exif); - - 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() { - return Py_BuildValue("i", WebPGetDecoderVersion()); -} - // Version as string const char * WebPDecoderVersion_str(void) { @@ -898,88 +759,31 @@ WebPDecoderVersion_str(void) { "%d.%d.%d", version_number >> 16, (version_number >> 8) % 0x100, - version_number % 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; -} - -PyObject * -WebPDecoderBuggyAlpha_wrapper() { - return Py_BuildValue("i", WebPDecoderBuggyAlpha()); -} - /* -------------------------------------------------------------------- */ /* Module Setup */ /* -------------------------------------------------------------------- */ 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_NOARGS, "WebPVersion"}, - {"WebPDecoderBuggyAlpha", - WebPDecoderBuggyAlpha_wrapper, - METH_NOARGS, - "WebPDecoderBuggyAlpha"}, - {NULL, NULL}}; - -void -addMuxFlagToModule(PyObject *m) { - PyObject *have_webpmux; -#ifdef HAVE_WEBPMUX - have_webpmux = Py_True; -#else - have_webpmux = Py_False; -#endif - Py_INCREF(have_webpmux); - PyModule_AddObject(m, "HAVE_WEBPMUX", have_webpmux); -} - -void -addAnimFlagToModule(PyObject *m) { - PyObject *have_webpanim; -#ifdef HAVE_WEBPANIM - have_webpanim = Py_True; -#else - have_webpanim = Py_False; -#endif - Py_INCREF(have_webpanim); - PyModule_AddObject(m, "HAVE_WEBPANIM", have_webpanim); -} - -void -addTransparencyFlagToModule(PyObject *m) { - PyObject *have_transparency = PyBool_FromLong(!WebPDecoderBuggyAlpha()); - if (PyModule_AddObject(m, "HAVE_TRANSPARENCY", have_transparency)) { - Py_DECREF(have_transparency); - } -} + {NULL, NULL} +}; static int setup_module(PyObject *m) { -#ifdef HAVE_WEBPANIM /* Ready object types */ if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || PyType_Ready(&WebPAnimEncoder_Type) < 0) { return -1; } -#endif - PyObject *d = PyModule_GetDict(m); - addMuxFlagToModule(m); - addAnimFlagToModule(m); - addTransparencyFlagToModule(m); + PyObject *d = PyModule_GetDict(m); PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str()); PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None); Py_XDECREF(v); @@ -987,23 +791,22 @@ setup_module(PyObject *m) { return 0; } +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__webp(void) { - PyObject *m; - static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_webp", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - webpMethods, /* m_methods */ + .m_name = "_webp", + .m_methods = webpMethods, + .m_slots = slots }; - m = PyModule_Create(&module_def); - if (setup_module(m) < 0) { - Py_DECREF(m); - return NULL; - } - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/decode.c b/src/decode.c index ea2f3af8012..5c6c250987d 100644 --- a/src/decode.c +++ b/src/decode.c @@ -46,7 +46,8 @@ typedef struct { PyObject_HEAD int (*decode)( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes + ); int (*cleanup)(ImagingCodecState state); struct ImagingCodecStateInstance state; Imaging im; @@ -154,43 +155,65 @@ PyImaging_AsImaging(PyObject *op); static PyObject * _setimage(ImagingDecoderObject *decoder, PyObject *args) { - PyObject *op; + PyObject *op, *extents; Imaging im; ImagingCodecState state; int x0, y0, x1, y1; - 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, "OO", &op, &extents)) { return NULL; } im = PyImaging_AsImaging(op); if (!im) { return NULL; } - - decoder->im = im; - - state = &decoder->state; - - /* Setup decoding tile extent */ - if (x0 == 0 && x1 == 0) { - state->xsize = im->xsize; - state->ysize = im->ysize; + if (extents == Py_None) { + x0 = 0; + y0 = 0; + x1 = im->xsize; + y1 = im->ysize; } else { - state->xoff = x0; - state->yoff = y0; - state->xsize = x1 - x0; - state->ysize = y1 - y0; + if (!PyTuple_Check(extents) || PyTuple_GET_SIZE(extents) != 4) { + PyErr_SetString(PyExc_ValueError, "invalid extents"); + return NULL; + } + for (int i = 0; i < 4; i++) { + PyObject *extent = PyTuple_GetItem(extents, i); + if (!PyLong_Check(extent)) { + PyErr_SetString(PyExc_ValueError, "invalid extents"); + return NULL; + } + int e = (int)PyLong_AsLong(extent); + + if (i == 0) { + x0 = e; + } else if (i == 1) { + y0 = e; + } else if (i == 2) { + x1 = e; + } else { + y1 = e; + } + } } - if (state->xsize <= 0 || state->xsize + state->xoff > (int)im->xsize || - state->ysize <= 0 || state->ysize + state->yoff > (int)im->ysize) { + if (x0 < 0 || y0 < 0 || x1 <= x0 || y1 <= y0 || x1 > (int)im->xsize || + y1 > (int)im->ysize) { PyErr_SetString(PyExc_ValueError, "tile cannot extend outside image"); return NULL; } + decoder->im = im; + + state = &decoder->state; + + /* Setup decoding tile extent */ + state->xoff = x0; + state->yoff = y0; + state->xsize = x1 - x0; + state->ysize = y1 - y0; + /* Allocate memory buffer (if bits field is set) */ if (state->bits > 0) { if (!state->bytes) { @@ -212,8 +235,7 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) { Py_XDECREF(decoder->lock); decoder->lock = op; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -230,8 +252,7 @@ _setfd(ImagingDecoderObject *decoder, PyObject *args) { Py_XINCREF(fd); state->fd = fd; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -257,42 +278,19 @@ static struct PyGetSetDef getseters[] = { }; static PyTypeObject ImagingDecoderType = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingDecoder", /*tp_name*/ - sizeof(ImagingDecoderObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 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*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingDecoder", + .tp_basicsize = sizeof(ImagingDecoderObject), + .tp_dealloc = (destructor)_dealloc, + .tp_methods = methods, + .tp_getset = getseters, }; /* -------------------------------------------------------------------- */ int -get_unpacker(ImagingDecoderObject *decoder, const char *mode, const char *rawmode) { +get_unpacker( + ImagingDecoderObject *decoder, const ModeID mode, const RawModeID rawmode +) { int bits; ImagingShuffler unpack; @@ -317,17 +315,20 @@ PyObject * PyImaging_BitDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; + const char *mode_name; 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_name, &bits, &pad, &fill, &sign, &ystep + )) { return NULL; } - if (strcmp(mode, "F") != 0) { + const ModeID mode = findModeID(mode_name); + if (mode != IMAGING_MODE_F) { PyErr_SetString(PyExc_ValueError, "bad image mode"); return NULL; } @@ -357,34 +358,36 @@ PyObject * PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *actual; + char *mode_name; int n = 0; char *pixel_format = ""; - if (!PyArg_ParseTuple(args, "si|s", &mode, &n, &pixel_format)) { + if (!PyArg_ParseTuple(args, "si|s", &mode_name, &n, &pixel_format)) { return NULL; } + const ModeID mode = findModeID(mode_name); + ModeID actual; + 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 7: /* BC7: 4-channel 8-bit via everything */ - actual = "RGBA"; + actual = IMAGING_MODE_RGBA; break; case 4: /* BC4: 1-channel 8-bit via 1 BC3 alpha block */ - actual = "L"; + actual = IMAGING_MODE_L; break; case 5: /* BC5: 2-channel 8-bit via 2 BC3 alpha blocks */ case 6: /* BC6: 3-channel 16-bit float */ - actual = "RGB"; + actual = IMAGING_MODE_RGB; break; default: PyErr_SetString(PyExc_ValueError, "block compression type unknown"); return NULL; } - if (strcmp(mode, actual) != 0) { + if (mode != actual) { PyErr_SetString(PyExc_ValueError, "bad image mode"); return NULL; } @@ -427,15 +430,18 @@ PyObject * PyImaging_GifDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; + const char *mode_name; int bits = 8; int interlace = 0; int transparency = -1; - if (!PyArg_ParseTuple(args, "s|iii", &mode, &bits, &interlace, &transparency)) { + if (!PyArg_ParseTuple( + args, "s|iii", &mode_name, &bits, &interlace, &transparency + )) { return NULL; } - if (strcmp(mode, "L") != 0 && strcmp(mode, "P") != 0) { + const ModeID mode = findModeID(mode_name); + if (mode != IMAGING_MODE_L && mode != IMAGING_MODE_P) { PyErr_SetString(PyExc_ValueError, "bad image mode"); return NULL; } @@ -462,12 +468,14 @@ PyObject * PyImaging_HexDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; - if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { + char *mode_name, *rawmode_name; + if (!PyArg_ParseTuple(args, "ss", &mode_name, &rawmode_name)) { return NULL; } + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); + decoder = PyImaging_DecoderNew(0); if (decoder == NULL) { return NULL; @@ -495,16 +503,21 @@ PyImaging_HexDecoderNew(PyObject *self, PyObject *args) { PyObject * PyImaging_LibTiffDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; + char *mode_name; + char *rawmode_name; char *compname; int fp; uint32_t ifdoffset; - if (!PyArg_ParseTuple(args, "sssiI", &mode, &rawmode, &compname, &fp, &ifdoffset)) { + if (!PyArg_ParseTuple( + args, "sssiI", &mode_name, &rawmode_name, &compname, &fp, &ifdoffset + )) { return NULL; } + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); + TRACE(("new tiff decoder %s\n", compname)); decoder = PyImaging_DecoderNew(sizeof(TIFFSTATE)); @@ -537,12 +550,15 @@ PyObject * PyImaging_PackbitsDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; - if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { + char *mode_name; + char *rawmode_name; + if (!PyArg_ParseTuple(args, "ss", &mode_name, &rawmode_name)) { return NULL; } + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); + decoder = PyImaging_DecoderNew(0); if (decoder == NULL) { return NULL; @@ -571,7 +587,7 @@ PyImaging_PcdDecoderNew(PyObject *self, PyObject *args) { } /* Unpack from PhotoYCC to RGB */ - if (get_unpacker(decoder, "RGB", "YCC;P") < 0) { + if (get_unpacker(decoder, IMAGING_MODE_RGB, IMAGING_RAWMODE_YCC_P) < 0) { return NULL; } @@ -588,13 +604,15 @@ PyObject * PyImaging_PcxDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; + char *mode_name, *rawmode_name; int stride; - if (!PyArg_ParseTuple(args, "ssi", &mode, &rawmode, &stride)) { + if (!PyArg_ParseTuple(args, "ssi", &mode_name, &rawmode_name, &stride)) { return NULL; } + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); + decoder = PyImaging_DecoderNew(0); if (decoder == NULL) { return NULL; @@ -619,14 +637,16 @@ PyObject * PyImaging_RawDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; + char *mode_name, *rawmode_name; int stride = 0; int ystep = 1; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &stride, &ystep)) { + if (!PyArg_ParseTuple(args, "ss|ii", &mode_name, &rawmode_name, &stride, &ystep)) { return NULL; } + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); + decoder = PyImaging_DecoderNew(sizeof(RAWSTATE)); if (decoder == NULL) { return NULL; @@ -653,14 +673,16 @@ PyObject * PyImaging_SgiRleDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; + char *mode_name, *rawmode_name; int ystep = 1; int bpc = 1; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &ystep, &bpc)) { + if (!PyArg_ParseTuple(args, "ss|ii", &mode_name, &rawmode_name, &ystep, &bpc)) { return NULL; } + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); + decoder = PyImaging_DecoderNew(sizeof(SGISTATE)); if (decoder == NULL) { return NULL; @@ -687,12 +709,14 @@ PyObject * PyImaging_SunRleDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; - if (!PyArg_ParseTuple(args, "ss", &mode, &rawmode)) { + char *mode_name, *rawmode_name; + if (!PyArg_ParseTuple(args, "ss", &mode_name, &rawmode_name)) { return NULL; } + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); + decoder = PyImaging_DecoderNew(0); if (decoder == NULL) { return NULL; @@ -715,14 +739,16 @@ PyObject * PyImaging_TgaRleDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; + char *mode_name, *rawmode_name; int ystep = 1; int depth = 8; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &ystep, &depth)) { + if (!PyArg_ParseTuple(args, "ss|ii", &mode_name, &rawmode_name, &ystep, &depth)) { return NULL; } + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); + decoder = PyImaging_DecoderNew(0); if (decoder == NULL) { return NULL; @@ -753,7 +779,7 @@ PyImaging_XbmDecoderNew(PyObject *self, PyObject *args) { return NULL; } - if (get_unpacker(decoder, "1", "1;R") < 0) { + if (get_unpacker(decoder, IMAGING_MODE_1, IMAGING_RAWMODE_1_R) < 0) { return NULL; } @@ -774,13 +800,15 @@ PyObject * PyImaging_ZipDecoderNew(PyObject *self, PyObject *args) { ImagingDecoderObject *decoder; - char *mode; - char *rawmode; + char *mode_name, *rawmode_name; int interlaced = 0; - if (!PyArg_ParseTuple(args, "ss|i", &mode, &rawmode, &interlaced)) { + if (!PyArg_ParseTuple(args, "ss|i", &mode_name, &rawmode_name, &interlaced)) { return NULL; } + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); + decoder = PyImaging_DecoderNew(sizeof(ZIPSTATE)); if (decoder == NULL) { return NULL; @@ -824,19 +852,21 @@ 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 */ + char *mode_name; + char *rawmode_name; /* what we want from the decoder */ + char *jpegmode_name; /* 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_name, &rawmode_name, &jpegmode_name, &scale, &draft + )) { return NULL; } - if (!jpegmode) { - jpegmode = ""; - } + const ModeID mode = findModeID(mode_name); + RawModeID rawmode = findRawModeID(rawmode_name); + const RawModeID jpegmode = findRawModeID(jpegmode_name); decoder = PyImaging_DecoderNew(sizeof(JPEGSTATE)); if (decoder == NULL) { @@ -846,8 +876,8 @@ PyImaging_JpegDecoderNew(PyObject *self, PyObject *args) { // libjpeg-turbo supports different output formats. // We are choosing Pillow's native format (3 color bytes + 1 padding) // to avoid extra conversion in Unpack.c. - if (ImagingJpegUseJCSExtensions() && strcmp(rawmode, "RGB") == 0) { - rawmode = "RGBX"; + if (ImagingJpegUseJCSExtensions() && rawmode == IMAGING_RAWMODE_RGB) { + rawmode = IMAGING_RAWMODE_RGBX; } if (get_unpacker(decoder, mode, rawmode) < 0) { @@ -857,11 +887,13 @@ PyImaging_JpegDecoderNew(PyObject *self, PyObject *args) { decoder->decode = ImagingJpegDecode; decoder->cleanup = ImagingJpegDecodeCleanup; - strncpy(((JPEGSTATE *)decoder->state.context)->rawmode, rawmode, 8); - strncpy(((JPEGSTATE *)decoder->state.context)->jpegmode, jpegmode, 8); + JPEGSTATE *jpeg_decoder_state_context = (JPEGSTATE *)decoder->state.context; + + jpeg_decoder_state_context->rawmode = rawmode; + jpeg_decoder_state_context->jpegmode = jpegmode; - ((JPEGSTATE *)decoder->state.context)->scale = scale; - ((JPEGSTATE *)decoder->state.context)->draft = draft; + jpeg_decoder_state_context->scale = scale; + jpeg_decoder_state_context->draft = draft; return (PyObject *)decoder; } @@ -889,17 +921,17 @@ PyImaging_Jpeg2KDecoderNew(PyObject *self, PyObject *args) { PY_LONG_LONG length = -1; if (!PyArg_ParseTuple( - args, "ss|iiiL", &mode, &format, &reduce, &layers, &fd, &length)) { + args, "ss|iiiL", &mode, &format, &reduce, &layers, &fd, &length + )) { return NULL; } if (strcmp(format, "j2k") == 0) { codec_format = OPJ_CODEC_J2K; - } else if (strcmp(format, "jpt") == 0) { - codec_format = OPJ_CODEC_JPT; } else if (strcmp(format, "jp2") == 0) { codec_format = OPJ_CODEC_JP2; } else { + PyErr_SetString(PyExc_ValueError, "unknown codec format"); return NULL; } diff --git a/src/display.c b/src/display.c index 990f4b0a5c9..944c60b704a 100644 --- a/src/display.c +++ b/src/display.c @@ -47,7 +47,7 @@ typedef struct { static PyTypeObject ImagingDisplayType; static ImagingDisplayObject * -_new(const char *mode, int xsize, int ysize) { +_new(const ModeID mode, int xsize, int ysize) { ImagingDisplayObject *display; if (PyType_Ready(&ImagingDisplayType) < 0) { @@ -85,8 +85,7 @@ _expose(ImagingDisplayObject *display, PyObject *args) { ImagingExposeDIB(display->dib, hdc); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -105,14 +104,14 @@ _draw(ImagingDisplayObject *display, PyObject *args) { src + 0, src + 1, src + 2, - src + 3)) { + src + 3 + )) { return NULL; } ImagingDrawDIB(display->dib, hdc, dst, src); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } extern Imaging @@ -142,8 +141,7 @@ _paste(ImagingDisplayObject *display, PyObject *args) { ImagingPasteDIB(display->dib, im, xy); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -189,8 +187,7 @@ _releasedc(ImagingDisplayObject *display, PyObject *args) { ReleaseDC(window, dc); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -210,8 +207,7 @@ _frombytes(ImagingDisplayObject *display, PyObject *args) { memcpy(display->dib->bits, buffer.buf, buffer.len); PyBuffer_Release(&buffer); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -221,7 +217,8 @@ _tobytes(ImagingDisplayObject *display, PyObject *args) { } 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[] = { @@ -238,7 +235,7 @@ static struct PyMethodDef methods[] = { static PyObject * _getattr_mode(ImagingDisplayObject *self, void *closure) { - return Py_BuildValue("s", self->dib->mode); + return Py_BuildValue("s", getModeData(self->dib->mode)->name); } static PyObject * @@ -247,51 +244,28 @@ _getattr_size(ImagingDisplayObject *self, void *closure) { } 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_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_delete, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 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).tp_name = "ImagingDisplay", + .tp_basicsize = sizeof(ImagingDisplayObject), + .tp_dealloc = (destructor)_delete, + .tp_methods = methods, + .tp_getset = getsetters, }; PyObject * PyImaging_DisplayWin32(PyObject *self, PyObject *args) { ImagingDisplayObject *display; - char *mode; + char *mode_name; int xsize, ysize; - if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { + if (!PyArg_ParseTuple(args, "s(ii)", &mode_name, &xsize, &ysize)) { return NULL; } + const ModeID mode = findModeID(mode_name); display = _new(mode, xsize, ysize); if (display == NULL) { return NULL; @@ -302,58 +276,80 @@ PyImaging_DisplayWin32(PyObject *self, PyObject *args) { PyObject * PyImaging_DisplayModeWin32(PyObject *self, PyObject *args) { - char *mode; int size[2]; - - mode = ImagingGetModeDIB(size); - - return Py_BuildValue("s(ii)", mode, size[0], size[1]); + const ModeID mode = ImagingGetModeDIB(size); + return Py_BuildValue("s(ii)", getModeData(mode)->name, size[0], size[1]); } /* -------------------------------------------------------------------- */ /* Windows screen grabber */ +typedef HANDLE(__stdcall *Func_GetWindowDpiAwarenessContext)(HANDLE); 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; + int x = 0, y = 0, width = -1, height; + int includeLayeredWindows = 0, screens = 0; HBITMAP bitmap; BITMAPCOREHEADER core; HDC screen, screen_copy; + HWND wnd; DWORD rop; PyObject *buffer; - HANDLE dpiAwareness; + HANDLE dpiAwareness = NULL; HMODULE user32; + Func_GetWindowDpiAwarenessContext GetWindowDpiAwarenessContext_function; Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function; - if (!PyArg_ParseTuple(args, "|ii", &includeLayeredWindows, &all_screens)) { + if (!PyArg_ParseTuple( + args, "|ii" F_HANDLE, &includeLayeredWindows, &screens, &wnd + )) { return NULL; } /* step 1: create a memory DC large enough to hold the entire screen */ - screen = CreateDC("DISPLAY", NULL, NULL, NULL); + if (screens == -1) { + screen = GetDC(wnd); + if (screen == NULL) { + PyErr_SetString(PyExc_OSError, "unable to get device context for handle"); + return NULL; + } + } else { + screen = CreateDC("DISPLAY", NULL, NULL, NULL); + } screen_copy = CreateCompatibleDC(screen); // added in Windows 10 (1607) // loaded dynamically to avoid link errors user32 = LoadLibraryA("User32.dll"); - SetThreadDpiAwarenessContext_function = - (Func_SetThreadDpiAwarenessContext)GetProcAddress( - user32, "SetThreadDpiAwarenessContext"); + SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext) + GetProcAddress(user32, "SetThreadDpiAwarenessContext"); if (SetThreadDpiAwarenessContext_function != NULL) { + GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext) + GetProcAddress(user32, "GetWindowDpiAwarenessContext"); + if (screens == -1 && GetWindowDpiAwarenessContext_function != NULL) { + dpiAwareness = GetWindowDpiAwarenessContext_function(wnd); + } // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3) - dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE)-3); + dpiAwareness = SetThreadDpiAwarenessContext_function( + dpiAwareness == NULL ? (HANDLE)-3 : dpiAwareness + ); } - if (all_screens) { + if (screens == 1) { x = GetSystemMetrics(SM_XVIRTUALSCREEN); y = GetSystemMetrics(SM_YVIRTUALSCREEN); width = GetSystemMetrics(SM_CXVIRTUALSCREEN); height = GetSystemMetrics(SM_CYVIRTUALSCREEN); + } else if (screens == -1) { + RECT rect; + if (GetClientRect(wnd, &rect)) { + width = rect.right; + height = rect.bottom; + } } else { width = GetDeviceCaps(screen, HORZRES); height = GetDeviceCaps(screen, VERTRES); @@ -365,6 +361,10 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { FreeLibrary(user32); + if (width == -1) { + goto error; + } + bitmap = CreateCompatibleBitmap(screen, width, height); if (!bitmap) { goto error; @@ -403,13 +403,18 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { height, PyBytes_AS_STRING(buffer), (BITMAPINFO *)&core, - DIB_RGB_COLORS)) { + DIB_RGB_COLORS + )) { goto error; } DeleteObject(bitmap); DeleteDC(screen_copy); - DeleteDC(screen); + if (screens == -1) { + ReleaseDC(wnd, screen); + } else { + DeleteDC(screen); + } return Py_BuildValue("(ii)(ii)N", x, y, width, height, buffer); @@ -417,7 +422,11 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { PyErr_SetString(PyExc_OSError, "screen grab failed"); DeleteDC(screen_copy); - DeleteDC(screen); + if (screens == -1) { + ReleaseDC(wnd, screen); + } else { + DeleteDC(screen); + } return NULL; } @@ -471,6 +480,9 @@ PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) { GlobalUnlock(handle); CloseClipboard(); + if (!result) { + return NULL; + } return Py_BuildValue("zN", format_names[format], result); } @@ -547,7 +559,8 @@ windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) { ps.rcPaint.left, ps.rcPaint.top, ps.rcPaint.right, - ps.rcPaint.bottom); + ps.rcPaint.bottom + ); if (result) { Py_DECREF(result); } else { @@ -562,7 +575,8 @@ windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) { 0, 0, rect.right - rect.left, - rect.bottom - rect.top); + rect.bottom - rect.top + ); if (result) { Py_DECREF(result); } else { @@ -577,7 +591,8 @@ windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) { 0, 0, rect.right - rect.left, - rect.bottom - rect.top); + rect.bottom - rect.top + ); if (result) { Py_DECREF(result); } else { @@ -591,7 +606,8 @@ windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) { case WM_SIZE: /* resize window */ result = PyObject_CallFunction( - callback, "sii", "resize", LOWORD(lParam), HIWORD(lParam)); + callback, "sii", "resize", LOWORD(lParam), HIWORD(lParam) + ); if (result) { InvalidateRect(wnd, NULL, 1); Py_DECREF(result); @@ -670,7 +686,8 @@ PyImaging_CreateWindowWin32(PyObject *self, PyObject *args) { HWND_DESKTOP, NULL, NULL, - NULL); + NULL + ); if (!wnd) { PyErr_SetString(PyExc_OSError, "failed to create window"); @@ -682,25 +699,26 @@ PyImaging_CreateWindowWin32(PyObject *self, PyObject *args) { 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 + Py_END_ALLOW_THREADS; - return Py_BuildValue(F_HANDLE, wnd); + return Py_BuildValue(F_HANDLE, wnd); } 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_END_ALLOW_THREADS; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /* -------------------------------------------------------------------- */ @@ -708,6 +726,14 @@ PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { #define GET32(p, o) ((DWORD *)(p + o))[0] +static int CALLBACK +enhMetaFileProc( + HDC hdc, HANDLETABLE *lpht, const ENHMETARECORD *lpmr, int nHandles, LPARAM data +) { + PlayEnhMetaFileRecord(hdc, lpht, lpmr, nHandles); + return 1; +} + PyObject * PyImaging_DrawWmf(PyObject *self, PyObject *args) { HBITMAP bitmap; @@ -732,7 +758,8 @@ PyImaging_DrawWmf(PyObject *self, PyObject *args) { &x0, &x1, &y0, - &y1)) { + &y1 + )) { return NULL; } @@ -787,10 +814,7 @@ PyImaging_DrawWmf(PyObject *self, PyObject *args) { /* FIXME: make background transparent? configurable? */ FillRect(dc, &rect, GetStockObject(WHITE_BRUSH)); - if (!PlayEnhMetaFile(dc, meta, &rect)) { - PyErr_SetString(PyExc_OSError, "cannot render metafile"); - goto error; - } + EnumEnhMetaFile(dc, meta, enhMetaFileProc, NULL, &rect); /* step 4: extract bits from bitmap */ @@ -844,7 +868,8 @@ PyImaging_GrabScreenX11(PyObject *self, PyObject *args) { PyErr_Format( PyExc_OSError, "X connection failed: error %i", - xcb_connection_has_error(connection)); + xcb_connection_has_error(connection) + ); xcb_disconnect(connection); return NULL; } @@ -878,8 +903,10 @@ PyImaging_GrabScreenX11(PyObject *self, PyObject *args) { 0, width, height, - 0x00ffffff), - &error); + 0x00ffffff + ), + &error + ); if (reply == NULL) { PyErr_Format( PyExc_OSError, @@ -887,7 +914,8 @@ PyImaging_GrabScreenX11(PyObject *self, PyObject *args) { error->error_code, error->major_code, error->minor_code, - error->resource_id); + error->resource_id + ); free(error); xcb_disconnect(connection); return NULL; @@ -897,7 +925,8 @@ PyImaging_GrabScreenX11(PyObject *self, PyObject *args) { if (reply->depth == 24) { buffer = PyBytes_FromStringAndSize( - (char *)xcb_get_image_data(reply), xcb_get_image_data_length(reply)); + (char *)xcb_get_image_data(reply), xcb_get_image_data_length(reply) + ); } else { PyErr_Format(PyExc_OSError, "unsupported bit depth: %i", reply->depth); } diff --git a/src/encode.c b/src/encode.c index 442b5d04f88..4b149653484 100644 --- a/src/encode.c +++ b/src/encode.c @@ -25,7 +25,9 @@ #define PY_SSIZE_T_CLEAN #include "Python.h" +#include "thirdparty/pythoncapi_compat.h" #include "libImaging/Imaging.h" +#include "libImaging/Bcn.h" #include "libImaging/Gif.h" #ifdef HAVE_UNISTD_H @@ -38,7 +40,8 @@ typedef struct { PyObject_HEAD int (*encode)( - Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); + Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes + ); int (*cleanup)(ImagingCodecState state); struct ImagingCodecStateInstance state; Imaging im; @@ -134,7 +137,8 @@ _encode(ImagingEncoderObject *encoder, PyObject *args) { } status = encoder->encode( - encoder->im, &encoder->state, (UINT8 *)PyBytes_AsString(buf), bufsize); + 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) { @@ -218,44 +222,67 @@ PyImaging_AsImaging(PyObject *op); static PyObject * _setimage(ImagingEncoderObject *encoder, PyObject *args) { - PyObject *op; + PyObject *op, *extents; Imaging im; ImagingCodecState state; 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|(nnnn)", &op, &x0, &y0, &x1, &y1)) { + if (!PyArg_ParseTuple(args, "OO", &op, &extents)) { return NULL; } im = PyImaging_AsImaging(op); if (!im) { return NULL; } - - encoder->im = im; - - state = &encoder->state; - - if (x0 == 0 && x1 == 0) { - state->xsize = im->xsize; - state->ysize = im->ysize; + if (extents == Py_None) { + x0 = 0; + y0 = 0; + x1 = im->xsize; + y1 = im->ysize; } else { - state->xoff = x0; - state->yoff = y0; - state->xsize = x1 - x0; - state->ysize = y1 - y0; + if (!PyTuple_Check(extents) || PyTuple_GET_SIZE(extents) != 4) { + PyErr_SetString(PyExc_ValueError, "invalid extents"); + return NULL; + } + for (int i = 0; i < 4; i++) { + PyObject *extent = PyTuple_GetItem(extents, i); + if (!PyLong_Check(extent)) { + PyErr_SetString(PyExc_ValueError, "invalid extents"); + return NULL; + } + Py_ssize_t e = (Py_ssize_t)PyLong_AsLong(extent); + + if (i == 0) { + x0 = e; + } else if (i == 1) { + y0 = e; + } else if (i == 2) { + x1 = e; + } else { + y1 = e; + } + } + } + if (im->xsize == 0 || im->ysize == 0) { + PyErr_SetString(PyExc_ValueError, "cannot write empty image"); + return NULL; } - if (state->xsize <= 0 || state->xsize + state->xoff > im->xsize || - state->ysize <= 0 || state->ysize + state->yoff > im->ysize) { + if (x0 < 0 || y0 < 0 || x1 <= x0 || y1 <= y0 || x1 > im->xsize || y1 > im->ysize) { PyErr_SetString(PyExc_SystemError, "tile cannot extend outside image"); return NULL; } + encoder->im = im; + + state = &encoder->state; + + state->xoff = x0; + state->yoff = y0; + state->xsize = x1 - x0; + state->ysize = y1 - y0; + /* Allocate memory buffer (if bits field is set) */ if (state->bits > 0) { if (state->xsize > ((INT_MAX / state->bits) - 7)) { @@ -275,8 +302,7 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) { Py_XDECREF(encoder->lock); encoder->lock = op; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -293,8 +319,7 @@ _setfd(ImagingEncoderObject *encoder, PyObject *args) { Py_XINCREF(fd); state->fd = fd; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -322,49 +347,29 @@ static struct PyGetSetDef getseters[] = { }; static PyTypeObject ImagingEncoderType = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingEncoder", /*tp_name*/ - sizeof(ImagingEncoderObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 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*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingEncoder", + .tp_basicsize = sizeof(ImagingEncoderObject), + .tp_dealloc = (destructor)_dealloc, + .tp_methods = methods, + .tp_getset = getseters, }; /* -------------------------------------------------------------------- */ int -get_packer(ImagingEncoderObject *encoder, const char *mode, const char *rawmode) { +get_packer(ImagingEncoderObject *encoder, const ModeID mode, const RawModeID rawmode) { int bits; ImagingShuffler pack; pack = ImagingFindPacker(mode, rawmode, &bits); if (!pack) { Py_DECREF(encoder); - PyErr_Format(PyExc_ValueError, "No packer found from %s to %s", mode, rawmode); + PyErr_Format( + PyExc_ValueError, + "No packer found from %s to %s", + getModeData(mode)->name, + getRawModeData(rawmode)->name + ); return -1; } @@ -374,6 +379,31 @@ get_packer(ImagingEncoderObject *encoder, const char *mode, const char *rawmode) return 0; } +/* -------------------------------------------------------------------- */ +/* BCN */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_BcnEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; + + char *mode; + int n; + if (!PyArg_ParseTuple(args, "si", &mode, &n)) { + return NULL; + } + + encoder = PyImaging_EncoderNew(0); + if (encoder == NULL) { + return NULL; + } + + encoder->encode = ImagingBcnEncode; + encoder->state.state = n; + + return (PyObject *)encoder; +} + /* -------------------------------------------------------------------- */ /* EPS */ /* -------------------------------------------------------------------- */ @@ -400,11 +430,13 @@ PyObject * PyImaging_GifEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; - char *mode; - char *rawmode; + char *mode_name; + char *rawmode_name; Py_ssize_t bits = 8; Py_ssize_t interlace = 0; - if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &bits, &interlace)) { + if (!PyArg_ParseTuple( + args, "ss|nn", &mode_name, &rawmode_name, &bits, &interlace + )) { return NULL; } @@ -413,6 +445,9 @@ PyImaging_GifEncoderNew(PyObject *self, PyObject *args) { return NULL; } + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); + if (get_packer(encoder, mode, rawmode) < 0) { return NULL; } @@ -433,11 +468,11 @@ PyObject * PyImaging_PcxEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; - char *mode; - char *rawmode; + char *mode_name; + char *rawmode_name; Py_ssize_t bits = 8; - if (!PyArg_ParseTuple(args, "ss|n", &mode, &rawmode, &bits)) { + if (!PyArg_ParseTuple(args, "ss|n", &mode_name, &rawmode_name, &bits)) { return NULL; } @@ -446,6 +481,9 @@ PyImaging_PcxEncoderNew(PyObject *self, PyObject *args) { return NULL; } + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); + if (get_packer(encoder, mode, rawmode) < 0) { return NULL; } @@ -463,12 +501,12 @@ PyObject * PyImaging_RawEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; - char *mode; - char *rawmode; + char *mode_name; + char *rawmode_name; Py_ssize_t stride = 0; Py_ssize_t ystep = 1; - if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &stride, &ystep)) { + if (!PyArg_ParseTuple(args, "ss|nn", &mode_name, &rawmode_name, &stride, &ystep)) { return NULL; } @@ -477,6 +515,9 @@ PyImaging_RawEncoderNew(PyObject *self, PyObject *args) { return NULL; } + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); + if (get_packer(encoder, mode, rawmode) < 0) { return NULL; } @@ -497,11 +538,11 @@ PyObject * PyImaging_TgaRleEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; - char *mode; - char *rawmode; + char *mode_name; + char *rawmode_name; Py_ssize_t ystep = 1; - if (!PyArg_ParseTuple(args, "ss|n", &mode, &rawmode, &ystep)) { + if (!PyArg_ParseTuple(args, "ss|n", &mode_name, &rawmode_name, &ystep)) { return NULL; } @@ -510,6 +551,9 @@ PyImaging_TgaRleEncoderNew(PyObject *self, PyObject *args) { return NULL; } + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); + if (get_packer(encoder, mode, rawmode) < 0) { return NULL; } @@ -534,7 +578,7 @@ PyImaging_XbmEncoderNew(PyObject *self, PyObject *args) { return NULL; } - if (get_packer(encoder, "1", "1;R") < 0) { + if (get_packer(encoder, IMAGING_MODE_1, IMAGING_RAWMODE_1_R) < 0) { return NULL; } @@ -555,8 +599,8 @@ PyObject * PyImaging_ZipEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; - char *mode; - char *rawmode; + char *mode_name; + char *rawmode_name; Py_ssize_t optimize = 0; Py_ssize_t compress_level = -1; Py_ssize_t compress_type = -1; @@ -565,13 +609,14 @@ PyImaging_ZipEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, "ss|nnny#", - &mode, - &rawmode, + &mode_name, + &rawmode_name, &optimize, &compress_level, &compress_type, &dictionary, - &dictionary_size)) { + &dictionary_size + )) { return NULL; } @@ -594,6 +639,9 @@ PyImaging_ZipEncoderNew(PyObject *self, PyObject *args) { return NULL; } + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); + if (get_packer(encoder, mode, rawmode) < 0) { free(dictionary); return NULL; @@ -602,7 +650,7 @@ PyImaging_ZipEncoderNew(PyObject *self, PyObject *args) { encoder->encode = ImagingZipEncode; encoder->cleanup = ImagingZipEncodeCleanup; - if (rawmode[0] == 'P') { + if (rawmode == IMAGING_RAWMODE_P || rawmode == IMAGING_RAWMODE_PA) { /* disable filtering */ ((ZIPSTATE *)encoder->state.context)->mode = ZIP_PNG_PALETTE; } @@ -631,8 +679,8 @@ PyObject * PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; - char *mode; - char *rawmode; + char *mode_name; + char *rawmode_name; char *compname; char *filename; Py_ssize_t fp; @@ -643,10 +691,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { int key_int, status, is_core_tag, is_var_length, num_core_tags, i; TIFFDataType type = TIFF_NOTYPE; // This list also exists in TiffTags.py - const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274, - 277, 278, 280, 281, 340, 341, 282, 283, 284, - 286, 287, 296, 297, 320, 321, 338, 32995, 32998, - 32996, 339, 32997, 330, 531, 530, 65537, 301, 532}; + const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274, 277, + 278, 280, 281, 282, 283, 284, 286, 287, 296, 297, + 301, 320, 321, 330, 333, 338, 339, 340, 341, 530, + 531, 532, 32995, 32996, 32997, 32998, 65537}; Py_ssize_t tags_size; PyObject *item; @@ -654,13 +702,14 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, "sssnsOO", - &mode, - &rawmode, + &mode_name, + &rawmode_name, &compname, &fp, &filename, &tags, - &types)) { + &types + )) { return NULL; } @@ -671,11 +720,17 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { 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); + item = PyList_GetItemRef(tags, pos); + if (item == NULL) { + return NULL; + } + if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) { + Py_DECREF(item); PyErr_SetString(PyExc_ValueError, "Invalid tags list"); return NULL; } + Py_DECREF(item); } pos = 0; } @@ -691,7 +746,11 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { return NULL; } + const ModeID mode = findModeID(mode_name); + const RawModeID rawmode = findRawModeID(rawmode_name); + if (get_packer(encoder, mode, rawmode) < 0) { + Py_DECREF(encoder); return NULL; } @@ -701,13 +760,22 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { return NULL; } + encoder->cleanup = ImagingLibTiffEncodeCleanup; + num_core_tags = sizeof(core_tags) / sizeof(int); for (pos = 0; pos < tags_size; pos++) { - item = PyList_GetItem(tags, pos); + item = PyList_GetItemRef(tags, pos); + if (item == NULL) { + Py_DECREF(encoder); + return NULL; + } + // We already checked that tags is a 2-tuple list. - key = PyTuple_GetItem(item, 0); + key = PyTuple_GET_ITEM(item, 0); key_int = (int)PyLong_AsLong(key); - value = PyTuple_GetItem(item, 1); + value = PyTuple_GET_ITEM(item, 1); + Py_DECREF(item); + status = 0; is_core_tag = 0; is_var_length = 0; @@ -721,10 +789,14 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { } if (!is_core_tag) { - PyObject *tag_type = PyDict_GetItem(types, key); + PyObject *tag_type; + if (PyDict_GetItemRef(types, key, &tag_type) < 0) { + Py_DECREF(encoder); + return NULL; // Exception has been already set + } if (tag_type) { int type_int = PyLong_AsLong(tag_type); - if (type_int >= TIFF_BYTE && type_int <= TIFF_DOUBLE) { + if (type_int >= TIFF_BYTE && type_int <= TIFF_LONG8) { type = (TIFFDataType)type_int; } } @@ -769,17 +841,20 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { is_var_length = 1; } if (ImagingLibTiffMergeFieldInfo( - &encoder->state, type, key_int, is_var_length)) { + &encoder->state, type, key_int, is_var_length + )) { continue; } } - if (type == TIFF_BYTE || type == TIFF_UNDEFINED) { + if (type == TIFF_BYTE || type == TIFF_UNDEFINED || + key_int == TIFFTAG_INKNAMES) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, PyBytes_Size(value), - PyBytes_AsString(value)); + PyBytes_AsString(value) + ); } else if (is_var_length) { Py_ssize_t len, i; TRACE(("Setting from Tuple: %d \n", key_int)); @@ -788,8 +863,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { if (key_int == TIFFTAG_COLORMAP) { int stride = 256; if (len != 768) { + Py_DECREF(encoder); PyErr_SetString( - PyExc_ValueError, "Requiring 768 items for Colormap"); + PyExc_ValueError, "Requiring 768 items for Colormap" + ); return NULL; } UINT16 *av; @@ -804,7 +881,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { (ttag_t)key_int, av, av + stride, - av + stride * 2); + av + stride * 2 + ); free(av); } } else if (key_int == TIFFTAG_YCBCRSUBSAMPLING) { @@ -812,7 +890,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { &encoder->state, (ttag_t)key_int, (UINT16)PyLong_AsLong(PyTuple_GetItem(value, 0)), - (UINT16)PyLong_AsLong(PyTuple_GetItem(value, 1))); + (UINT16)PyLong_AsLong(PyTuple_GetItem(value, 1)) + ); } else if (type == TIFF_SHORT) { UINT16 *av; /* malloc check ok, calloc checks for overflow */ @@ -822,7 +901,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { av[i] = (UINT16)PyLong_AsLong(PyTuple_GetItem(value, i)); } status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); + &encoder->state, (ttag_t)key_int, len, av + ); free(av); } } else if (type == TIFF_LONG) { @@ -834,7 +914,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { av[i] = (UINT32)PyLong_AsLong(PyTuple_GetItem(value, i)); } status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); + &encoder->state, (ttag_t)key_int, len, av + ); free(av); } } else if (type == TIFF_SBYTE) { @@ -846,7 +927,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { av[i] = (INT8)PyLong_AsLong(PyTuple_GetItem(value, i)); } status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); + &encoder->state, (ttag_t)key_int, len, av + ); free(av); } } else if (type == TIFF_SSHORT) { @@ -858,7 +940,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { av[i] = (INT16)PyLong_AsLong(PyTuple_GetItem(value, i)); } status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); + &encoder->state, (ttag_t)key_int, len, av + ); free(av); } } else if (type == TIFF_SLONG) { @@ -870,7 +953,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { av[i] = (INT32)PyLong_AsLong(PyTuple_GetItem(value, i)); } status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); + &encoder->state, (ttag_t)key_int, len, av + ); free(av); } } else if (type == TIFF_FLOAT) { @@ -882,7 +966,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { av[i] = (FLOAT32)PyFloat_AsDouble(PyTuple_GetItem(value, i)); } status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); + &encoder->state, (ttag_t)key_int, len, av + ); free(av); } } else if (type == TIFF_DOUBLE) { @@ -894,43 +979,68 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { av[i] = PyFloat_AsDouble(PyTuple_GetItem(value, i)); } status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, len, av); + &encoder->state, (ttag_t)key_int, len, av + ); + free(av); + } + } else if (type == TIFF_RATIONAL) { + 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, 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) { + &encoder->state, (ttag_t)key_int, (UINT16)PyLong_AsLong(value) + ); + } else if (type == TIFF_LONG || type == TIFF_IFD) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value)); + &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)); + &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)); + &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)); + &encoder->state, (ttag_t)key_int, (FLOAT32)PyFloat_AsDouble(value) + ); } else if (type == TIFF_SBYTE) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (INT8)PyLong_AsLong(value)); + &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) { + &encoder->state, (ttag_t)key_int, PyBytes_AsString(value) + ); + } else if ( + type == TIFF_DOUBLE || type == TIFF_SRATIONAL || type == TIFF_RATIONAL + ) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value) + ); + } else if (type == TIFF_LONG8) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value)); + &encoder->state, (ttag_t)key_int, (uint64_t)PyLong_AsLongLong(value) + ); } else { TRACE( ("Unhandled type for key %d : %s \n", key_int, - PyBytes_AsString(PyObject_Str(value)))); + PyBytes_AsString(PyObject_Str(value))) + ); } } if (!status) { @@ -991,7 +1101,8 @@ get_qtables_arrays(PyObject *qtables, int *qtablesLen) { if (num_tables < 1 || num_tables > NUM_QUANT_TBLS) { PyErr_SetString( PyExc_ValueError, - "Not a valid number of quantization tables. Should be between 1 and 4."); + "Not a valid number of quantization tables. Should be between 1 and 4." + ); Py_DECREF(tables); return NULL; } @@ -1036,8 +1147,8 @@ PyObject * PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; - char *mode; - char *rawmode; + char *mode_name; + char *rawmode_name; Py_ssize_t quality = 0; Py_ssize_t progressive = 0; Py_ssize_t smooth = 0; @@ -1060,9 +1171,9 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|nnnnpnnnnnnOz#y#y#", - &mode, - &rawmode, + "ss|nnnnpn(nn)nnnOz#y#y#", + &mode_name, + &rawmode_name, &quality, &progressive, &smooth, @@ -1080,7 +1191,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { &extra, &extra_size, &rawExif, - &rawExifLen)) { + &rawExifLen + )) { return NULL; } @@ -1089,11 +1201,14 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { return NULL; } + const ModeID mode = findModeID(mode_name); + RawModeID rawmode = findRawModeID(rawmode_name); + // libjpeg-turbo supports different output formats. // We are choosing Pillow's native format (3 color bytes + 1 padding) // to avoid extra conversion in Pack.c. - if (ImagingJpegUseJCSExtensions() && strcmp(rawmode, "RGB") == 0) { - rawmode = "RGBX"; + if (ImagingJpegUseJCSExtensions() && rawmode == IMAGING_RAWMODE_RGB) { + rawmode = IMAGING_RAWMODE_RGBX; } if (get_packer(encoder, mode, rawmode) < 0) { @@ -1150,29 +1265,27 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { encoder->encode = ImagingJpegEncode; - strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8); - - ((JPEGENCODERSTATE *)encoder->state.context)->keep_rgb = keep_rgb; - ((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)->restart_marker_blocks = - restart_marker_blocks; - ((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_rows = - restart_marker_rows; - ((JPEGENCODERSTATE *)encoder->state.context)->comment = comment; - ((JPEGENCODERSTATE *)encoder->state.context)->comment_size = comment_size; - ((JPEGENCODERSTATE *)encoder->state.context)->extra = extra; - ((JPEGENCODERSTATE *)encoder->state.context)->extra_size = extra_size; - ((JPEGENCODERSTATE *)encoder->state.context)->rawExif = rawExif; - ((JPEGENCODERSTATE *)encoder->state.context)->rawExifLen = rawExifLen; + JPEGENCODERSTATE *jpeg_encoder_state = (JPEGENCODERSTATE *)encoder->state.context; + jpeg_encoder_state->rawmode = rawmode; + jpeg_encoder_state->keep_rgb = keep_rgb; + jpeg_encoder_state->quality = quality; + jpeg_encoder_state->qtables = qarrays; + jpeg_encoder_state->qtablesLen = qtablesLen; + jpeg_encoder_state->subsampling = subsampling; + jpeg_encoder_state->progressive = progressive; + jpeg_encoder_state->smooth = smooth; + jpeg_encoder_state->optimize = optimize; + jpeg_encoder_state->streamtype = streamtype; + jpeg_encoder_state->xdpi = xdpi; + jpeg_encoder_state->ydpi = ydpi; + jpeg_encoder_state->restart_marker_blocks = restart_marker_blocks; + jpeg_encoder_state->restart_marker_rows = restart_marker_rows; + jpeg_encoder_state->comment = comment; + jpeg_encoder_state->comment_size = comment_size; + jpeg_encoder_state->extra = extra; + jpeg_encoder_state->extra_size = extra_size; + jpeg_encoder_state->rawExif = rawExif; + jpeg_encoder_state->rawExifLen = rawExifLen; return (PyObject *)encoder; } @@ -1217,7 +1330,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { PyObject *quality_layers = NULL; Py_ssize_t num_resolutions = 0; PyObject *cblk_size = NULL, *precinct_size = NULL; - PyObject *irreversible = NULL; + int irreversible = 0; char *progression = "LRCP"; OPJ_PROG_ORDER prog_order; char *cinema_mode = "no"; @@ -1231,7 +1344,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|OOOsOnOOOssbbnz#p", + "ss|OOOsOnOOpssbbnz#p", &mode, &format, &offset, @@ -1250,17 +1363,17 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { &fd, &comment, &comment_size, - &plt)) { + &plt + )) { return NULL; } if (strcmp(format, "j2k") == 0) { codec_format = OPJ_CODEC_J2K; - } else if (strcmp(format, "jpt") == 0) { - codec_format = OPJ_CODEC_JPT; } else if (strcmp(format, "jp2") == 0) { codec_format = OPJ_CODEC_JP2; } else { + PyErr_SetString(PyExc_ValueError, "unknown codec format"); return NULL; } @@ -1275,6 +1388,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } else if (strcmp(progression, "CPRL") == 0) { prog_order = OPJ_CPRL; } else { + PyErr_SetString(PyExc_ValueError, "unknown progression"); return NULL; } @@ -1287,6 +1401,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } else if (strcmp(cinema_mode, "cinema4k-24") == 0) { cine_mode = OPJ_CINEMA4K_24; } else { + PyErr_SetString(PyExc_ValueError, "unknown cinema mode"); return NULL; } @@ -1307,7 +1422,8 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { 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); + 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 */ @@ -1317,7 +1433,8 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { PyErr_SetString( PyExc_ValueError, "JPEG 2000 tile offset too small; top left tile must " - "intersect image area"); + "intersect image area" + ); Py_DECREF(encoder); return NULL; } @@ -1325,8 +1442,8 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { 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"); + PyExc_ValueError, "JPEG 2000 tile offset too large to cover image area" + ); Py_DECREF(encoder); return NULL; } @@ -1360,9 +1477,10 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { j2k_decode_coord_tuple(cblk_size, &context->cblk_width, &context->cblk_height); j2k_decode_coord_tuple( - precinct_size, &context->precinct_width, &context->precinct_height); + precinct_size, &context->precinct_width, &context->precinct_height + ); - context->irreversible = PyObject_IsTrue(irreversible); + context->irreversible = irreversible; context->progression = prog_order; context->cinema_mode = cine_mode; context->mct = mct; @@ -1373,10 +1491,3 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } #endif - -/* - * Local Variables: - * c-basic-offset: 4 - * End: - * - */ diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 3a5e918e837..c77a9c21c66 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -11,38 +11,6 @@ #include "Imaging.h" -/* use make_hash.py from the pillow-scripts repository to calculate these values */ -#define ACCESS_TABLE_SIZE 35 -#define ACCESS_TABLE_HASH 8940 - -static struct ImagingAccessInstance access_table[ACCESS_TABLE_SIZE]; - -static inline UINT32 -hash(const char *mode) { - UINT32 i = ACCESS_TABLE_HASH; - while (*mode) { - i = ((i << 5) + i) ^ (UINT8)*mode++; - } - return i % ACCESS_TABLE_SIZE; -} - -static ImagingAccess -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); - exit(1); - } - access_table[i].mode = mode; - return &access_table[i]; -} - /* fetch individual pixel */ static void @@ -63,7 +31,7 @@ static void get_pixel_16L(Imaging im, int x, int y, void *color) { UINT8 *in = (UINT8 *)&im->image[y][x + x]; #ifdef WORDS_BIGENDIAN - UINT16 out = in[0] + (in[1] << 8); + UINT16 out = in[0] + ((UINT16)in[1] << 8); memcpy(color, &out, sizeof(out)); #else memcpy(color, in, sizeof(UINT16)); @@ -76,63 +44,16 @@ get_pixel_16B(Imaging im, int x, int y, void *color) { #ifdef WORDS_BIGENDIAN memcpy(color, in, sizeof(UINT16)); #else - UINT16 out = in[1] + (in[0] << 8); + UINT16 out = in[1] + ((UINT16)in[0] << 8); memcpy(color, &out, sizeof(out)); #endif } -static void -get_pixel_BGR15(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image8[y][x * 2]; - UINT16 pixel = in[0] + (in[1] << 8); - char *out = color; - out[0] = (pixel & 31) * 255 / 31; - out[1] = ((pixel >> 5) & 31) * 255 / 31; - out[2] = ((pixel >> 10) & 31) * 255 / 31; -} - -static void -get_pixel_BGR16(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image8[y][x * 2]; - UINT16 pixel = in[0] + (in[1] << 8); - char *out = color; - out[0] = (pixel & 31) * 255 / 31; - out[1] = ((pixel >> 5) & 63) * 255 / 63; - out[2] = ((pixel >> 11) & 31) * 255 / 31; -} - -static void -get_pixel_BGR24(Imaging im, int x, int y, void *color) { - memcpy(color, &im->image8[y][x * 3], sizeof(UINT8) * 3); -} - static void 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]; -#ifdef WORDS_BIGENDIAN - INT32 out = in[0] + (in[1] << 8) + (in[2] << 16) + (in[3] << 24); - memcpy(color, &out, sizeof(out)); -#else - 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]; -#ifdef WORDS_BIGENDIAN - memcpy(color, in, sizeof(INT32)); -#else - INT32 out = in[3] + (in[2] << 8) + (in[1] << 16) + (in[0] << 24); - memcpy(color, &out, sizeof(out)); -#endif -} - /* store individual pixel */ static void @@ -153,84 +74,46 @@ put_pixel_16B(Imaging im, int x, int y, const void *color) { out[1] = in[0]; } -static void -put_pixel_BGR1516(Imaging im, int x, int y, const void *color) { - memcpy(&im->image8[y][x * 2], color, 2); -} - -static void -put_pixel_BGR24(Imaging im, int x, int y, const void *color) { - memcpy(&im->image8[y][x * 3], color, 3); -} - -static void -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]; - out[0] = in[3]; - out[1] = in[2]; - out[2] = in[1]; - out[3] = in[0]; -} - static void 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_, get_pixel_, put_pixel_) \ - { \ - ImagingAccess access = add_item(mode_); \ - access->get_pixel = get_pixel_; \ - access->put_pixel = put_pixel_; \ - } - - /* populate access table */ - ADD("1", get_pixel_8, put_pixel_8); - ADD("L", get_pixel_8, put_pixel_8); - ADD("LA", get_pixel_32_2bands, put_pixel_32); - ADD("La", get_pixel_32_2bands, put_pixel_32); - ADD("I", get_pixel_32, put_pixel_32); - ADD("I;16", get_pixel_16L, put_pixel_16L); - ADD("I;16L", get_pixel_16L, put_pixel_16L); - ADD("I;16B", get_pixel_16B, put_pixel_16B); +static struct ImagingAccessInstance accessors[] = { + {IMAGING_MODE_1, get_pixel_8, put_pixel_8}, + {IMAGING_MODE_L, get_pixel_8, put_pixel_8}, + {IMAGING_MODE_LA, get_pixel_32_2bands, put_pixel_32}, + {IMAGING_MODE_La, get_pixel_32_2bands, put_pixel_32}, + {IMAGING_MODE_I, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_I_16, get_pixel_16L, put_pixel_16L}, + {IMAGING_MODE_I_16L, get_pixel_16L, put_pixel_16L}, + {IMAGING_MODE_I_16B, get_pixel_16B, put_pixel_16B}, #ifdef WORDS_BIGENDIAN - ADD("I;16N", get_pixel_16B, put_pixel_16B); + {IMAGING_MODE_I_16N, get_pixel_16B, put_pixel_16B}, #else - ADD("I;16N", get_pixel_16L, put_pixel_16L); + {IMAGING_MODE_I_16N, get_pixel_16L, put_pixel_16L}, #endif - ADD("I;32L", get_pixel_32L, put_pixel_32L); - ADD("I;32B", get_pixel_32B, put_pixel_32B); - ADD("F", get_pixel_32, put_pixel_32); - ADD("P", get_pixel_8, put_pixel_8); - ADD("PA", get_pixel_32_2bands, put_pixel_32); - ADD("BGR;15", get_pixel_BGR15, put_pixel_BGR1516); - ADD("BGR;16", get_pixel_BGR16, put_pixel_BGR1516); - ADD("BGR;24", get_pixel_BGR24, put_pixel_BGR24); - ADD("RGB", get_pixel_32, put_pixel_32); - ADD("RGBA", get_pixel_32, put_pixel_32); - ADD("RGBa", get_pixel_32, put_pixel_32); - ADD("RGBX", get_pixel_32, put_pixel_32); - ADD("CMYK", get_pixel_32, put_pixel_32); - ADD("YCbCr", get_pixel_32, put_pixel_32); - ADD("LAB", get_pixel_32, put_pixel_32); - ADD("HSV", get_pixel_32, put_pixel_32); -} + {IMAGING_MODE_F, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_P, get_pixel_8, put_pixel_8}, + {IMAGING_MODE_PA, get_pixel_32_2bands, put_pixel_32}, + {IMAGING_MODE_RGB, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_RGBA, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_RGBa, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_RGBX, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_CMYK, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_YCbCr, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_LAB, get_pixel_32, put_pixel_32}, + {IMAGING_MODE_HSV, get_pixel_32, put_pixel_32}, +}; ImagingAccess -ImagingAccessNew(Imaging im) { - ImagingAccess access = &access_table[hash(im->mode)]; - if (im->mode[0] != access->mode[0] || strcmp(im->mode, access->mode) != 0) { - return NULL; +ImagingAccessNew(const Imaging im) { + for (size_t i = 0; i < sizeof(accessors) / sizeof(*accessors); i++) { + if (im->mode == accessors[i].mode) { + return &accessors[i]; + } } - return access; + return NULL; } void diff --git a/src/libImaging/AlphaComposite.c b/src/libImaging/AlphaComposite.c index 6d728f9088b..280277e835a 100644 --- a/src/libImaging/AlphaComposite.c +++ b/src/libImaging/AlphaComposite.c @@ -25,13 +25,12 @@ ImagingAlphaComposite(Imaging imDst, Imaging imSrc) { int x, y; /* Check arguments */ - if (!imDst || !imSrc || strcmp(imDst->mode, "RGBA") || - imDst->type != IMAGING_TYPE_UINT8 || imDst->bands != 4) { + if (!imDst || !imSrc || + (imDst->mode != IMAGING_MODE_RGBA && imDst->mode != IMAGING_MODE_LA)) { return ImagingError_ModeError(); } - if (strcmp(imDst->mode, imSrc->mode) || imDst->type != imSrc->type || - imDst->bands != imSrc->bands || imDst->xsize != imSrc->xsize || + if (imDst->mode != imSrc->mode || imDst->xsize != imSrc->xsize || imDst->ysize != imSrc->ysize) { return ImagingError_Mismatch(); } diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c new file mode 100644 index 00000000000..3ca227d4f8e --- /dev/null +++ b/src/libImaging/Arrow.c @@ -0,0 +1,431 @@ + +#include "Arrow.h" +#include "Imaging.h" +#include + +/* struct ArrowSchema* */ +/* _arrow_schema_channel(char* channel, char* format) { */ + +/* } */ + +static void +ReleaseExportedSchema(struct ArrowSchema *array) { + // TODO here: release and/or deallocate all data directly owned by + // the ArrowArray struct, such as the private_data. + + if (!array->release) { + return; + } + if (array->format) { + free((void *)array->format); + array->format = NULL; + } + if (array->name) { + free((void *)array->name); + array->name = NULL; + } + if (array->metadata) { + free((void *)array->metadata); + array->metadata = NULL; + } + + // Release children + if (array->children) { + for (int64_t i = 0; i < array->n_children; ++i) { + struct ArrowSchema *child = array->children[i]; + if (child != NULL) { + if (child->release != NULL) { + child->release(child); + child->release = NULL; + } + free(array->children[i]); + } + } + free(array->children); + array->children = NULL; + } + + // Release dictionary + struct ArrowSchema *dict = array->dictionary; + if (dict != NULL) { + if (dict->release != NULL) { + dict->release(dict); + dict->release = NULL; + } + free(dict); + array->dictionary = NULL; + } + + // Mark array released + array->release = NULL; +} + +char * +image_band_json(Imaging im) { + char *format = "{\"bands\": [\"%s\", \"%s\", \"%s\", \"%s\"]}"; + char *json; + // Bands can be 4 bands * 2 characters each + int len = strlen(format) + 8 + 1; + int err; + + json = calloc(1, len); + + if (!json) { + return NULL; + } + + err = PyOS_snprintf( + json, + len, + format, + im->band_names[0], + im->band_names[1], + im->band_names[2], + im->band_names[3] + ); + if (err < 0) { + return NULL; + } + return json; +} + +char * +single_band_json(Imaging im) { + char *format = "{\"bands\": [\"%s\"]}"; + char *json; + // Bands can be 1 band * (maybe but probably not) 2 characters each + int len = strlen(format) + 2 + 1; + int err; + + json = calloc(1, len); + + if (!json) { + return NULL; + } + + err = PyOS_snprintf(json, len, format, im->band_names[0]); + if (err < 0) { + return NULL; + } + return json; +} + +char * +assemble_metadata(const char *band_json) { + /* format is + int32: number of key/value pairs (noted N below) + int32: byte length of key 0 + key 0 (not null-terminated) + int32: byte length of value 0 + value 0 (not null-terminated) + ... + int32: byte length of key N - 1 + key N - 1 (not null-terminated) + int32: byte length of value N - 1 + value N - 1 (not null-terminated) + */ + const char *key = "image"; + INT32 key_len = strlen(key); + INT32 band_json_len = strlen(band_json); + + char *buf; + INT32 *dest_int; + char *dest; + + buf = calloc(1, key_len + band_json_len + 4 + 1 * 8); + if (!buf) { + return NULL; + } + + dest_int = (void *)buf; + + dest_int[0] = 1; + dest_int[1] = key_len; + dest_int += 2; + dest = (void *)dest_int; + memcpy(dest, key, key_len); + dest += key_len; + dest_int = (void *)dest; + dest_int[0] = band_json_len; + dest_int += 1; + memcpy(dest_int, band_json, band_json_len); + + return buf; +} + +int +export_named_type(struct ArrowSchema *schema, char *format, const char *name) { + char *formatp; + char *namep; + size_t format_len = strlen(format) + 1; + size_t name_len = strlen(name) + 1; + + formatp = calloc(format_len, 1); + + if (!formatp) { + return IMAGING_CODEC_MEMORY; + } + + namep = calloc(name_len, 1); + if (!namep) { + free(formatp); + return IMAGING_CODEC_MEMORY; + } + + strncpy(formatp, format, format_len); + strncpy(namep, name, name_len); + + *schema = (struct ArrowSchema){ + // Type description + .format = formatp, + .name = namep, + .metadata = NULL, + .flags = 0, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &ReleaseExportedSchema + }; + return 0; +} + +int +export_imaging_schema(Imaging im, struct ArrowSchema *schema) { + int retval = 0; + char *band_json; + + if (strcmp(im->arrow_band_format, "") == 0) { + return IMAGING_ARROW_INCOMPATIBLE_MODE; + } + + /* for now, single block images */ + if (im->blocks_count > 1) { + return IMAGING_ARROW_MEMORY_LAYOUT; + } + + if (im->bands == 1) { + retval = export_named_type(schema, im->arrow_band_format, im->band_names[0]); + if (retval != 0) { + return retval; + } + // band related metadata + band_json = single_band_json(im); + if (band_json) { + schema->metadata = assemble_metadata(band_json); + free(band_json); + } + return retval; + } + + retval = export_named_type(schema, "+w:4", ""); + if (retval != 0) { + return retval; + } + // if it's not 1 band, it's an int32 at the moment. 4 uint8 bands. + schema->n_children = 1; + schema->children = calloc(1, sizeof(struct ArrowSchema *)); + if (!schema->children) { + schema->release(schema); + return IMAGING_CODEC_MEMORY; + } + schema->children[0] = (struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema)); + if (!schema->children[0]) { + schema->release(schema); + return IMAGING_CODEC_MEMORY; + } + retval = export_named_type( + schema->children[0], im->arrow_band_format, getModeData(im->mode)->name + ); + if (retval != 0) { + schema->release(schema); + return retval; + } + + // band related metadata + band_json = image_band_json(im); + if (band_json) { + // adding the metadata to the child array. + // Accessible in pyarrow via pa.array(img).type.field(0).metadata + // adding it to the top level is not accessible. + schema->children[0]->metadata = assemble_metadata(band_json); + free(band_json); + } + + return 0; +} + +static void +release_const_array(struct ArrowArray *array) { + Imaging im = (Imaging)array->private_data; + + ImagingDelete(im); + + // Free the buffers and the buffers array + if (array->buffers) { + free(array->buffers); + array->buffers = NULL; + } + if (array->children) { + for (int i = 0; i < array->n_children; i++) { + if (array->children[i]) { + if (array->children[i]->release) { + array->children[i]->release(array->children[i]); + array->children[i]->release = NULL; + } + free(array->children[i]); + } + } + free(array->children); + array->children = NULL; + } + // Mark released + array->release = NULL; +} + +int +export_single_channel_array(Imaging im, struct ArrowArray *array) { + int length = im->xsize * im->ysize; + + /* for now, single block images */ + if (im->blocks_count > 1) { + return IMAGING_ARROW_MEMORY_LAYOUT; + } + + if (im->lines_per_block && im->lines_per_block < im->ysize) { + length = im->xsize * im->lines_per_block; + } + + MUTEX_LOCK(&im->mutex); + im->refcount++; + MUTEX_UNLOCK(&im->mutex); + // Initialize primitive fields + *array = (struct ArrowArray){ + // Data description + .length = length, + .offset = 0, + .null_count = 0, + .n_buffers = 2, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im + }; + + // Allocate list of buffers + array->buffers = (const void **)calloc(1, sizeof(void *) * array->n_buffers); + if (!array->buffers) { + array->release(array); + return IMAGING_CODEC_MEMORY; + } + array->buffers[0] = NULL; // no nulls, null bitmap can be omitted + + if (im->block) { + array->buffers[1] = im->block; + } else { + array->buffers[1] = im->blocks[0].ptr; + } + return 0; +} + +int +export_fixed_pixel_array(Imaging im, struct ArrowArray *array) { + int length = im->xsize * im->ysize; + + /* for now, single block images */ + if (im->blocks_count > 1) { + return IMAGING_ARROW_MEMORY_LAYOUT; + } + + if (im->lines_per_block && im->lines_per_block < im->ysize) { + length = im->xsize * im->lines_per_block; + } + + MUTEX_LOCK(&im->mutex); + im->refcount++; + MUTEX_UNLOCK(&im->mutex); + // Initialize primitive fields + // Fixed length arrays are 1 buffer of validity, and the length in pixels. + // Data is in a child array. + *array = (struct ArrowArray){ + // Data description + .length = length, + .offset = 0, + .null_count = 0, + .n_buffers = 1, + .n_children = 1, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im + }; + + // Allocate list of buffers + array->buffers = (const void **)calloc(1, sizeof(void *) * array->n_buffers); + if (!array->buffers) { + goto err; + } + // assert(array->buffers != NULL); + array->buffers[0] = NULL; // no nulls, null bitmap can be omitted + + // if it's not 1 band, it's an int32 at the moment. 4 uint8 bands. + array->n_children = 1; + array->children = calloc(1, sizeof(struct ArrowArray *)); + if (!array->children) { + goto err; + } + array->children[0] = (struct ArrowArray *)calloc(1, sizeof(struct ArrowArray)); + if (!array->children[0]) { + goto err; + } + + MUTEX_LOCK(&im->mutex); + im->refcount++; + MUTEX_UNLOCK(&im->mutex); + *array->children[0] = (struct ArrowArray){ + // Data description + .length = length * 4, + .offset = 0, + .null_count = 0, + .n_buffers = 2, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im + }; + + array->children[0]->buffers = + (const void **)calloc(2, sizeof(void *) * array->n_buffers); + if (!array->children[0]->buffers) { + goto err; + } + + if (im->block) { + array->children[0]->buffers[1] = im->block; + } else { + array->children[0]->buffers[1] = im->blocks[0].ptr; + } + return 0; + +err: + array->release(array); + return IMAGING_CODEC_MEMORY; +} + +int +export_imaging_array(Imaging im, struct ArrowArray *array) { + if (strcmp(im->arrow_band_format, "") == 0) { + return IMAGING_ARROW_INCOMPATIBLE_MODE; + } + + if (im->bands == 1) { + return export_single_channel_array(im, array); + } + + return export_fixed_pixel_array(im, array); +} diff --git a/src/libImaging/Arrow.h b/src/libImaging/Arrow.h new file mode 100644 index 00000000000..0b285fe8053 --- /dev/null +++ b/src/libImaging/Arrow.h @@ -0,0 +1,48 @@ +#include +#include + +// Apache License 2.0. +// Source apache arrow project +// https://arrow.apache.org/docs/format/CDataInterface.html + +#ifndef ARROW_C_DATA_INTERFACE +#define ARROW_C_DATA_INTERFACE + +#define ARROW_FLAG_DICTIONARY_ORDERED 1 +#define ARROW_FLAG_NULLABLE 2 +#define ARROW_FLAG_MAP_KEYS_SORTED 4 + +struct ArrowSchema { + // Array type description + const char *format; + const char *name; + const char *metadata; + int64_t flags; + int64_t n_children; + struct ArrowSchema **children; + struct ArrowSchema *dictionary; + + // Release callback + void (*release)(struct ArrowSchema *); + // Opaque producer-specific data + void *private_data; +}; + +struct ArrowArray { + // Array data description + int64_t length; + int64_t null_count; + int64_t offset; + int64_t n_buffers; + int64_t n_children; + const void **buffers; + struct ArrowArray **children; + struct ArrowArray *dictionary; + + // Release callback + void (*release)(struct ArrowArray *); + // Opaque producer-specific data + void *private_data; +}; + +#endif // ARROW_C_DATA_INTERFACE diff --git a/src/libImaging/Bands.c b/src/libImaging/Bands.c index e1b16b34ac0..d1b0ebc4ed8 100644 --- a/src/libImaging/Bands.c +++ b/src/libImaging/Bands.c @@ -41,7 +41,7 @@ ImagingGetBand(Imaging imIn, int band) { band = 3; } - imOut = ImagingNewDirty("L", imIn->xsize, imIn->ysize); + imOut = ImagingNewDirty(IMAGING_MODE_L, imIn->xsize, imIn->ysize); if (!imOut) { return NULL; } @@ -82,7 +82,7 @@ ImagingSplit(Imaging imIn, Imaging bands[4]) { } for (i = 0; i < imIn->bands; i++) { - bands[i] = ImagingNewDirty("L", imIn->xsize, imIn->ysize); + bands[i] = ImagingNewDirty(IMAGING_MODE_L, imIn->xsize, imIn->ysize); if (!bands[i]) { for (j = 0; j < i; ++j) { ImagingDelete(bands[j]); @@ -240,7 +240,7 @@ ImagingFillBand(Imaging imOut, int band, int color) { } Imaging -ImagingMerge(const char *mode, Imaging bands[4]) { +ImagingMerge(const ModeID mode, Imaging bands[4]) { int i, x, y; int bandsCount = 0; Imaging imOut; diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c index 72f478d8d60..d99b0e28e8a 100644 --- a/src/libImaging/BcnDecode.c +++ b/src/libImaging/BcnDecode.c @@ -25,7 +25,6 @@ typedef struct { typedef struct { UINT16 c0, c1; - UINT32 lut; } bc1_color; typedef struct { @@ -40,13 +39,10 @@ typedef struct { #define LOAD16(p) (p)[0] | ((p)[1] << 8) -#define LOAD32(p) (p)[0] | ((p)[1] << 8) | ((p)[2] << 16) | ((p)[3] << 24) - 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 @@ -70,7 +66,7 @@ static void decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) { bc1_color col; rgba p[4]; - int n, cw; + int n, o, cw; UINT16 r0, g0, b0, r1, g1, b1; bc1_color_load(&col, src); @@ -103,9 +99,11 @@ decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) { p[3].b = 0; p[3].a = 0; } - for (n = 0; n < 16; n++) { - cw = 3 & (col.lut >> (2 * n)); - dst[n] = p[cw]; + for (n = 0; n < 4; n++) { + for (o = 0; o < 4; o++) { + cw = 3 & ((src + 4)[n] >> (2 * o)); + dst[n * 4 + o] = p[cw]; + } } } @@ -243,7 +241,8 @@ static const bc7_mode_info bc7_modes[] = { {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}}; + {2, 6, 0, 0, 5, 5, 1, 0, 2, 0} +}; /* Subset indices: Table.P2, 1 bit per index */ @@ -254,7 +253,8 @@ static const UINT16 bc7_si2[] = { 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}; + 0xccf0, 0x0fcc, 0x7744, 0xee22 +}; /* Table.P3, 2 bits per index */ static const UINT32 bc7_si3[] = { @@ -267,20 +267,23 @@ static const UINT32 bc7_si3[] = { 0x66660000, 0xa5a0a5a0, 0x50a050a0, 0x69286928, 0x44aaaa44, 0x66666600, 0xaa444444, 0x54a854a8, 0x95809580, 0x96969600, 0xa85454a8, 0x80959580, 0xaa141414, 0x96960000, 0xaaaa1414, 0xa05050a0, 0xa0a5a5a0, 0x96000000, 0x40804080, 0xa9a8a9a8, 0xaaaaaa44, - 0x2a4a5254}; + 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}; +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}; /* 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}; +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}; /* Table.A3b */ static const char bc7_ai2[] = {15, 8, 8, 3, 15, 15, 3, 8, 15, 15, 15, 15, 15, @@ -293,7 +296,8 @@ static const char bc7_ai2[] = {15, 8, 8, 3, 15, 15, 3, 8, 15, 15, 15, 15, 1 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}; + 0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64 +}; static const char * bc7_get_weights(int n) { @@ -526,7 +530,8 @@ static const bc6_mode_info bc6_modes[] = { {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}}; + {1, 1, 0, 16, 4, 4, 4} +}; /* Table.F, encoded as a sequence of bit indices */ static const UINT8 bc6_bit_packings[][75] = { @@ -591,13 +596,14 @@ static const UINT8 bc6_bit_packings[][75] = { 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}}; + 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; + x |= -(1 << prec); } *v = (UINT16)x; } @@ -657,7 +663,7 @@ half_to_float(UINT16 h) { if (o.f >= m.f) { o.u |= 255 << 23; } - o.u |= (h & 0x8000) << 16; + o.u |= (UINT32)(h & 0x8000) << 16; return o.f; } @@ -830,7 +836,8 @@ decode_bcn( int bytes, int N, int C, - char *pixel_format) { + char *pixel_format +) { int ymax = state->ysize + state->yoff; const UINT8 *ptr = src; switch (N) { diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c new file mode 100644 index 00000000000..c6989dc1cc4 --- /dev/null +++ b/src/libImaging/BcnEncode.c @@ -0,0 +1,302 @@ +/* + * The Python Imaging Library + * + * encoder for DXT1-compressed data + * + * Format documentation: + * https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt + * + */ + +#include "Imaging.h" + +typedef struct { + UINT8 color[3]; +} rgb; + +typedef struct { + UINT8 color[4]; +} rgba; + +static rgb +decode_565(UINT16 x) { + rgb item; + int r, g, b; + r = (x & 0xf800) >> 8; + r |= r >> 5; + item.color[0] = r; + g = (x & 0x7e0) >> 3; + g |= g >> 6; + item.color[1] = g; + b = (x & 0x1f) << 3; + b |= b >> 5; + item.color[2] = b; + return item; +} + +static UINT16 +encode_565(rgba item) { + UINT16 r = item.color[0] >> (8 - 5); + UINT8 g = item.color[1] >> (8 - 6); + UINT8 b = item.color[2] >> (8 - 5); + return (r << (5 + 6)) | (g << 5) | b; +} + +static void +encode_bc1_color(Imaging im, ImagingCodecState state, UINT8 *dst, int separate_alpha) { + int i, j, k; + UINT16 color_min = 0, color_max = 0; + rgb color_min_rgb, color_max_rgb; + rgba block[16], *current_rgba; + + // Determine the min and max colors in this 4x4 block + int first = 1; + int transparency = 0; + for (i = 0; i < 4; i++) { + for (j = 0; j < 4; j++) { + current_rgba = &block[i + j * 4]; + + int x = state->x + i * im->pixelsize; + int y = state->y + j; + if (x >= state->xsize * im->pixelsize || y >= state->ysize) { + // The 4x4 block extends past the edge of the image + for (k = 0; k < 3; k++) { + current_rgba->color[k] = 0; + } + continue; + } + + for (k = 0; k < 3; k++) { + current_rgba->color[k] = + (UINT8)im->image[y][x + (im->pixelsize == 1 ? 0 : k)]; + } + if (separate_alpha) { + if ((UINT8)im->image[y][x + 3] == 0) { + current_rgba->color[3] = 0; + transparency = 1; + continue; + } else { + current_rgba->color[3] = 1; + } + } + + UINT16 color = encode_565(*current_rgba); + if (first || color < color_min) { + color_min = color; + } + if (first || color > color_max) { + color_max = color; + } + first = 0; + } + } + + if (transparency) { + *dst++ = color_min; + *dst++ = color_min >> 8; + } + *dst++ = color_max; + *dst++ = color_max >> 8; + if (!transparency) { + *dst++ = color_min; + *dst++ = color_min >> 8; + } + + color_min_rgb = decode_565(color_min); + color_max_rgb = decode_565(color_max); + for (i = 0; i < 4; i++) { + UINT8 l = 0; + for (j = 3; j > -1; j--) { + current_rgba = &block[i * 4 + j]; + if (transparency && !current_rgba->color[3]) { + l |= 3 << (j * 2); + continue; + } + + float distance = 0; + int total = 0; + for (k = 0; k < 3; k++) { + float denom = + (float)abs(color_max_rgb.color[k] - color_min_rgb.color[k]); + if (denom != 0) { + distance += + abs(current_rgba->color[k] - color_min_rgb.color[k]) / denom; + total += 1; + } + } + if (total == 0) { + continue; + } + if (transparency) { + distance *= 4 / total; + if (distance < 1) { + // color_max + } else if (distance < 3) { + l |= 2 << (j * 2); // 1/2 * color_min + 1/2 * color_max + } else { + l |= 1 << (j * 2); // color_min + } + } else { + distance *= 6 / total; + if (distance < 1) { + l |= 1 << (j * 2); // color_min + } else if (distance < 3) { + l |= 3 << (j * 2); // 1/3 * color_min + 2/3 * color_max + } else if (distance < 5) { + l |= 2 << (j * 2); // 2/3 * color_min + 1/3 * color_max + } else { + // color_max + } + } + } + *dst++ = l; + } +} + +static void +encode_bc2_block(Imaging im, ImagingCodecState state, UINT8 *dst) { + int i, j; + UINT8 block[16]; + UINT32 current_alpha; + for (i = 0; i < 4; i++) { + for (j = 0; j < 4; j++) { + int x = state->x + i * im->pixelsize; + int y = state->y + j; + if (x >= state->xsize * im->pixelsize || y >= state->ysize) { + // The 4x4 block extends past the edge of the image + block[i + j * 4] = 0; + continue; + } + + current_alpha = (UINT8)im->image[y][x + 3]; + block[i + j * 4] = current_alpha; + } + } + + for (i = 0; i < 4; i++) { + UINT16 l = 0; + for (j = 3; j > -1; j--) { + current_alpha = block[i * 4 + j]; + l |= current_alpha << (j * 4); + } + *dst++ = l; + *dst++ = l >> 8; + } +} + +static void +encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst, int o) { + int i, j; + UINT8 alpha_min = 0, alpha_max = 0; + UINT8 block[16], current_alpha; + + // Determine the min and max colors in this 4x4 block + int first = 1; + for (i = 0; i < 4; i++) { + for (j = 0; j < 4; j++) { + int x = state->x + i * im->pixelsize; + int y = state->y + j; + if (x >= state->xsize * im->pixelsize || y >= state->ysize) { + // The 4x4 block extends past the edge of the image + block[i + j * 4] = 0; + continue; + } + + current_alpha = (UINT8)im->image[y][x + o]; + block[i + j * 4] = current_alpha; + + if (first || current_alpha < alpha_min) { + alpha_min = current_alpha; + } + if (first || current_alpha > alpha_max) { + alpha_max = current_alpha; + } + first = 0; + } + } + + *dst++ = alpha_min; + *dst++ = alpha_max; + + float denom = (float)abs(alpha_max - alpha_min); + for (i = 0; i < 2; i++) { + UINT32 l = 0; + for (j = 7; j > -1; j--) { + current_alpha = block[i * 8 + j]; + if (!current_alpha) { + l |= 6 << (j * 3); + continue; + } else if (current_alpha == 255) { + l |= 7 << (j * 3); + continue; + } + + float distance = + denom == 0 ? 0 : abs(current_alpha - alpha_min) / denom * 10; + if (distance < 3) { + l |= 2 << (j * 3); // 4/5 * alpha_min + 1/5 * alpha_max + } else if (distance < 5) { + l |= 3 << (j * 3); // 3/5 * alpha_min + 2/5 * alpha_max + } else if (distance < 7) { + l |= 4 << (j * 3); // 2/5 * alpha_min + 3/5 * alpha_max + } else { + l |= 5 << (j * 3); // 1/5 * alpha_min + 4/5 * alpha_max + } + } + *dst++ = l; + *dst++ = l >> 8; + *dst++ = l >> 16; + } +} + +int +ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + int n = state->state; + int has_alpha_channel = + im->mode == IMAGING_MODE_RGBA || im->mode == IMAGING_MODE_LA; + + UINT8 *dst = buf; + + int will_write = (n == 2 || n == 3 || n == 5) ? 16 : 8; + for (;;) { + if (dst + will_write >= bytes + buf) { + break; + } + if (n == 5) { + encode_bc3_alpha(im, state, dst, 0); + dst += 8; + + encode_bc3_alpha(im, state, dst, 1); + } else { + if (n == 2 || n == 3) { + if (has_alpha_channel) { + if (n == 2) { + encode_bc2_block(im, state, dst); + } else { + encode_bc3_alpha(im, state, dst, 3); + } + dst += 8; + } else { + for (int i = 0; i < 8; i++) { + *dst++ = 0xff; + } + } + } + encode_bc1_color(im, state, dst, n == 1 && has_alpha_channel); + } + dst += 8; + + state->x += im->pixelsize * 4; + + if (state->x >= state->xsize * im->pixelsize) { + state->x = 0; + state->y += 4; + if (state->y >= state->ysize) { + state->errcode = IMAGING_CODEC_END; + break; + } + } + } + + return dst - buf; +} diff --git a/src/libImaging/Blend.c b/src/libImaging/Blend.c index a53ae0fad53..df94920f62e 100644 --- a/src/libImaging/Blend.c +++ b/src/libImaging/Blend.c @@ -24,8 +24,8 @@ ImagingBlend(Imaging imIn1, Imaging imIn2, float alpha) { /* Check arguments */ if (!imIn1 || !imIn2 || imIn1->type != IMAGING_TYPE_UINT8 || imIn1->palette || - strcmp(imIn1->mode, "1") == 0 || imIn2->palette || - strcmp(imIn2->mode, "1") == 0) { + imIn1->mode == IMAGING_MODE_1 || imIn2->palette || + imIn2->mode == IMAGING_MODE_1) { return ImagingError_ModeError(); } diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 4ea9c77178a..4fea4fe44b8 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -13,7 +13,8 @@ void static inline ImagingLineBoxBlur32( int edgeA, int edgeB, UINT32 ww, - UINT32 fw) { + UINT32 fw +) { int x; UINT32 acc[4]; UINT32 bulk[4]; @@ -109,7 +110,8 @@ void static inline ImagingLineBoxBlur8( int edgeA, int edgeB, UINT32 ww, - UINT32 fw) { + UINT32 fw +) { int x; UINT32 acc; UINT32 bulk; @@ -198,7 +200,8 @@ ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) { edgeA, edgeB, ww, - fw); + fw + ); if (imIn == imOut) { // Commit. memcpy(imOut->image8[y], lineOut, imIn->xsize); @@ -214,7 +217,8 @@ ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) { edgeA, edgeB, ww, - fw); + fw + ); if (imIn == imOut) { // Commit. memcpy(imOut->image32[y], lineOut, imIn->xsize * 4); @@ -234,6 +238,9 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n) int i; Imaging imTransposed; + if (imOut->xsize == 0 || imOut->ysize == 0) { + return imOut; + } if (n < 1) { return ImagingError_ValueError("number of passes must be greater than zero"); } @@ -241,7 +248,7 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n) return ImagingError_ValueError("radius must be >= 0"); } - if (strcmp(imIn->mode, imOut->mode) || imIn->type != imOut->type || + if (imIn->mode != imOut->mode || imIn->type != imOut->type || imIn->bands != imOut->bands || imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { return ImagingError_Mismatch(); @@ -251,10 +258,10 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n) 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 (imIn->mode != IMAGING_MODE_RGB && imIn->mode != IMAGING_MODE_RGBA && + imIn->mode != IMAGING_MODE_RGBa && imIn->mode != IMAGING_MODE_RGBX && + imIn->mode != IMAGING_MODE_CMYK && imIn->mode != IMAGING_MODE_L && + imIn->mode != IMAGING_MODE_LA && imIn->mode != IMAGING_MODE_La) { return ImagingError_ModeError(); } @@ -314,11 +321,13 @@ _gaussian_blur_radius(float radius, int passes) { Imaging ImagingGaussianBlur( - Imaging imOut, Imaging imIn, float xradius, float yradius, int passes) { + Imaging imOut, Imaging imIn, float xradius, float yradius, int passes +) { return ImagingBoxBlur( imOut, imIn, _gaussian_blur_radius(xradius, passes), _gaussian_blur_radius(yradius, passes), - passes); + passes + ); } diff --git a/src/libImaging/Chops.c b/src/libImaging/Chops.c index f9c005efe3a..3ce8a0903e0 100644 --- a/src/libImaging/Chops.c +++ b/src/libImaging/Chops.c @@ -18,28 +18,28 @@ #include "Imaging.h" -#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; \ - } \ - } \ - } \ +#define CHOP(operation) \ + int x, y; \ + Imaging imOut; \ + imOut = create(imIn1, imIn2, IMAGING_MODE_UNKNOWN); \ + 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) \ @@ -60,11 +60,12 @@ return imOut; static Imaging -create(Imaging im1, Imaging im2, char *mode) { +create(Imaging im1, Imaging im2, const ModeID mode) { int xsize, ysize; if (!im1 || !im2 || im1->type != IMAGING_TYPE_UINT8 || - (mode != NULL && (strcmp(im1->mode, "1") || strcmp(im2->mode, "1")))) { + (mode != IMAGING_MODE_UNKNOWN && + (im1->mode != IMAGING_MODE_1 || im2->mode != IMAGING_MODE_1))) { return (Imaging)ImagingError_ModeError(); } if (im1->type != im2->type || im1->bands != im2->bands) { @@ -114,27 +115,27 @@ ImagingChopSubtract(Imaging imIn1, Imaging imIn2, float scale, int offset) { Imaging ImagingChopAnd(Imaging imIn1, Imaging imIn2) { - CHOP2((in1[x] && in2[x]) ? 255 : 0, "1"); + CHOP2((in1[x] && in2[x]) ? 255 : 0, IMAGING_MODE_1); } Imaging ImagingChopOr(Imaging imIn1, Imaging imIn2) { - CHOP2((in1[x] || in2[x]) ? 255 : 0, "1"); + CHOP2((in1[x] || in2[x]) ? 255 : 0, IMAGING_MODE_1); } Imaging ImagingChopXor(Imaging imIn1, Imaging imIn2) { - CHOP2(((in1[x] != 0) ^ (in2[x] != 0)) ? 255 : 0, "1"); + CHOP2(((in1[x] != 0) ^ (in2[x] != 0)) ? 255 : 0, IMAGING_MODE_1); } Imaging ImagingChopAddModulo(Imaging imIn1, Imaging imIn2) { - CHOP2(in1[x] + in2[x], NULL); + CHOP2(in1[x] + in2[x], IMAGING_MODE_UNKNOWN); } Imaging ImagingChopSubtractModulo(Imaging imIn1, Imaging imIn2) { - CHOP2(in1[x] - in2[x], NULL); + CHOP2(in1[x] - in2[x], IMAGING_MODE_UNKNOWN); } Imaging @@ -142,7 +143,8 @@ 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_MODE_UNKNOWN + ); } Imaging @@ -150,7 +152,8 @@ ImagingChopHardLight(Imaging imIn1, Imaging imIn2) { CHOP2( (in2[x] < 128) ? ((in1[x] * in2[x]) / 127) : 255 - (((255 - in2[x]) * (255 - in1[x])) / 127), - NULL); + IMAGING_MODE_UNKNOWN + ); } Imaging @@ -158,5 +161,6 @@ ImagingOverlay(Imaging imIn1, Imaging imIn2) { CHOP2( (in1[x] < 128) ? ((in1[x] * in2[x]) / 127) : 255 - (((255 - in1[x]) * (255 - in2[x])) / 127), - NULL); + IMAGING_MODE_UNKNOWN + ); } diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c index aee7cda067d..5559de689b2 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -63,7 +63,8 @@ ImagingColorLUT3D_linear( int size1D, int size2D, int size3D, - INT16 *table) { + 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, @@ -112,7 +113,8 @@ ImagingColorLUT3D_linear( index2D >> SCALE_BITS, index3D >> SCALE_BITS, size1D, - size1D_2D); + size1D_2D + ); INT16 result[4], left[4], right[4]; INT16 leftleft[4], leftright[4], rightleft[4], rightright[4]; @@ -123,19 +125,22 @@ ImagingColorLUT3D_linear( leftright, &table[idx + size1D * 3], &table[idx + size1D * 3 + 3], - shift1D); + shift1D + ); interpolate3(left, leftleft, leftright, shift2D); interpolate3( rightleft, &table[idx + size1D_2D * 3], &table[idx + size1D_2D * 3 + 3], - shift1D); + shift1D + ); interpolate3( rightright, &table[idx + size1D_2D * 3 + size1D * 3], &table[idx + size1D_2D * 3 + size1D * 3 + 3], - shift1D); + shift1D + ); interpolate3(right, rightleft, rightright, shift2D); interpolate3(result, left, right, shift3D); @@ -144,7 +149,8 @@ ImagingColorLUT3D_linear( clip8(result[0]), clip8(result[1]), clip8(result[2]), - rowIn[x * 4 + 3]); + rowIn[x * 4 + 3] + ); memcpy(rowOut + x * sizeof(v), &v, sizeof(v)); } @@ -155,19 +161,22 @@ ImagingColorLUT3D_linear( leftright, &table[idx + size1D * 4], &table[idx + size1D * 4 + 4], - shift1D); + shift1D + ); interpolate4(left, leftleft, leftright, shift2D); interpolate4( rightleft, &table[idx + size1D_2D * 4], &table[idx + size1D_2D * 4 + 4], - shift1D); + shift1D + ); interpolate4( rightright, &table[idx + size1D_2D * 4 + size1D * 4], &table[idx + size1D_2D * 4 + size1D * 4 + 4], - shift1D); + shift1D + ); interpolate4(right, rightleft, rightright, shift2D); interpolate4(result, left, right, shift3D); @@ -176,7 +185,8 @@ ImagingColorLUT3D_linear( clip8(result[0]), clip8(result[1]), clip8(result[2]), - clip8(result[3])); + clip8(result[3]) + ); memcpy(rowOut + x * sizeof(v), &v, sizeof(v)); } } diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index fcb5f7ad95e..002497c3264 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -277,38 +277,6 @@ rgb2f(UINT8 *out_, const UINT8 *in, int xsize) { } } -static void -rgb2bgr15(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - 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) { - int x; - 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) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - *out++ = in[2]; - *out++ = in[1]; - *out++ = in[0]; - } -} - static void rgb2hsv_row(UINT8 *out, const UINT8 *in) { // following colorsys.py float h, s, rc, gc, bc, cr; @@ -909,149 +877,12 @@ I16_RGB(UINT8 *out, const UINT8 *in, int xsize) { } } -static struct { - 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}, - {"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", "La", rgb2la}, - {"RGB", "I", rgb2i}, - {"RGB", "I;16", rgb2i16l}, - {"RGB", "I;16L", rgb2i16l}, - {"RGB", "I;16B", rgb2i16b}, -#ifdef WORDS_BIGENDIAN - {"RGB", "I;16N", rgb2i16b}, -#else - {"RGB", "I;16N", rgb2i16l}, -#endif - {"RGB", "F", rgb2f}, - {"RGB", "BGR;15", rgb2bgr15}, - {"RGB", "BGR;16", rgb2bgr16}, - {"RGB", "BGR;24", rgb2bgr24}, - {"RGB", "RGBA", rgb2rgba}, - {"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}, - {"RGBa", "RGB", rgba2rgb_}, - - {"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}, - {"I;16", "RGB", I16_RGB}, - {"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}, -#ifdef WORDS_BIGENDIAN - {"L", "I;16N", L_I16B}, - {"I;16N", "L", I16B_L}, -#else - {"L", "I;16N", L_I16L}, - {"I;16N", "L", I16L_L}, -#endif - - {"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 */ - /* ------------------- */ /* Palette conversions */ /* ------------------- */ +/* FIXME: translate indexed versions to pointer versions below this line */ + static void p2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; @@ -1099,13 +930,13 @@ pa2p(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { static void p2pa(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; - int rgb = strcmp(palette->mode, "RGB"); + const int rgb = palette->mode == IMAGING_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]; + *out++ = rgb ? 255 : rgba[3]; } } @@ -1259,7 +1090,7 @@ pa2ycbcr(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { } static Imaging -frompalette(Imaging imOut, Imaging imIn, const char *mode) { +frompalette(Imaging imOut, Imaging imIn, const ModeID mode) { ImagingSectionCookie cookie; int alpha; int y; @@ -1271,31 +1102,31 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { return (Imaging)ImagingError_ValueError("no palette"); } - alpha = !strcmp(imIn->mode, "PA"); + alpha = imIn->mode == IMAGING_MODE_PA; - if (strcmp(mode, "1") == 0) { + if (mode == IMAGING_MODE_1) { convert = alpha ? pa2bit : p2bit; - } else if (strcmp(mode, "L") == 0) { + } else if (mode == IMAGING_MODE_L) { convert = alpha ? pa2l : p2l; - } else if (strcmp(mode, "LA") == 0) { + } else if (mode == IMAGING_MODE_LA) { convert = alpha ? pa2la : p2la; - } else if (strcmp(mode, "P") == 0) { + } else if (mode == IMAGING_MODE_P) { convert = pa2p; - } else if (strcmp(mode, "PA") == 0) { + } else if (mode == IMAGING_MODE_PA) { convert = p2pa; - } else if (strcmp(mode, "I") == 0) { + } else if (mode == IMAGING_MODE_I) { convert = alpha ? pa2i : p2i; - } else if (strcmp(mode, "F") == 0) { + } else if (mode == IMAGING_MODE_F) { convert = alpha ? pa2f : p2f; - } else if (strcmp(mode, "RGB") == 0) { + } else if (mode == IMAGING_MODE_RGB) { convert = alpha ? pa2rgb : p2rgb; - } else if (strcmp(mode, "RGBA") == 0 || strcmp(mode, "RGBX") == 0) { + } else if (mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_RGBX) { convert = alpha ? pa2rgba : p2rgba; - } else if (strcmp(mode, "CMYK") == 0) { + } else if (mode == IMAGING_MODE_CMYK) { convert = alpha ? pa2cmyk : p2cmyk; - } else if (strcmp(mode, "YCbCr") == 0) { + } else if (mode == IMAGING_MODE_YCbCr) { convert = alpha ? pa2ycbcr : p2ycbcr; - } else if (strcmp(mode, "HSV") == 0) { + } else if (mode == IMAGING_MODE_HSV) { convert = alpha ? pa2hsv : p2hsv; } else { return (Imaging)ImagingError_ValueError("conversion not supported"); @@ -1305,7 +1136,7 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { if (!imOut) { return NULL; } - if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) { + if (mode == IMAGING_MODE_P || mode == IMAGING_MODE_PA) { ImagingPaletteDelete(imOut->palette); imOut->palette = ImagingPaletteDuplicate(imIn->palette); } @@ -1316,7 +1147,8 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { (UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize, - imIn->palette); + imIn->palette + ); } ImagingSectionLeave(&cookie); @@ -1328,27 +1160,26 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { #endif static Imaging topalette( - Imaging imOut, - Imaging imIn, - const char *mode, - ImagingPalette inpalette, - int dither) { + Imaging imOut, Imaging imIn, const ModeID mode, ImagingPalette inpalette, int dither +) { ImagingSectionCookie cookie; int alpha; int x, y; ImagingPalette palette = inpalette; - /* Map L or RGB/RGBX/RGBA to palette image */ - if (strcmp(imIn->mode, "L") != 0 && strncmp(imIn->mode, "RGB", 3) != 0) { + /* Map L or RGB/RGBX/RGBA/RGBa to palette image */ + if (imIn->mode != IMAGING_MODE_L && imIn->mode != IMAGING_MODE_RGB && + imIn->mode != IMAGING_MODE_RGBX && imIn->mode != IMAGING_MODE_RGBA && + imIn->mode != IMAGING_MODE_RGBa) { return (Imaging)ImagingError_ValueError("conversion not supported"); } - alpha = !strcmp(mode, "PA"); + alpha = mode == IMAGING_MODE_PA; if (palette == NULL) { /* FIXME: make user configurable */ if (imIn->bands == 1) { - palette = ImagingPaletteNew("RGB"); + palette = ImagingPaletteNew(IMAGING_MODE_RGB); palette->size = 256; int i; @@ -1535,11 +1366,11 @@ tobilevel(Imaging imOut, Imaging imIn) { int *errors; /* Map L or RGB to dithered 1 image */ - if (strcmp(imIn->mode, "L") != 0 && strcmp(imIn->mode, "RGB") != 0) { + if (imIn->mode != IMAGING_MODE_L && imIn->mode != IMAGING_MODE_RGB) { return (Imaging)ImagingError_ValueError("conversion not supported"); } - imOut = ImagingNew2Dirty("1", imOut, imIn); + imOut = ImagingNew2Dirty(IMAGING_MODE_1, imOut, imIn); if (!imOut) { return NULL; } @@ -1621,18 +1452,152 @@ tobilevel(Imaging imOut, Imaging imIn) { #pragma optimize("", on) #endif +/* ------------------- */ +/* Conversion handlers */ +/* ------------------- */ + +static struct { + const ModeID from; + const ModeID to; + ImagingShuffler convert; +} converters[] = { + {IMAGING_MODE_1, IMAGING_MODE_L, bit2l}, + {IMAGING_MODE_1, IMAGING_MODE_I, bit2i}, + {IMAGING_MODE_1, IMAGING_MODE_F, bit2f}, + {IMAGING_MODE_1, IMAGING_MODE_RGB, bit2rgb}, + {IMAGING_MODE_1, IMAGING_MODE_RGBA, bit2rgb}, + {IMAGING_MODE_1, IMAGING_MODE_RGBX, bit2rgb}, + {IMAGING_MODE_1, IMAGING_MODE_CMYK, bit2cmyk}, + {IMAGING_MODE_1, IMAGING_MODE_YCbCr, bit2ycbcr}, + {IMAGING_MODE_1, IMAGING_MODE_HSV, bit2hsv}, + + {IMAGING_MODE_L, IMAGING_MODE_1, l2bit}, + {IMAGING_MODE_L, IMAGING_MODE_LA, l2la}, + {IMAGING_MODE_L, IMAGING_MODE_I, l2i}, + {IMAGING_MODE_L, IMAGING_MODE_F, l2f}, + {IMAGING_MODE_L, IMAGING_MODE_RGB, l2rgb}, + {IMAGING_MODE_L, IMAGING_MODE_RGBA, l2rgb}, + {IMAGING_MODE_L, IMAGING_MODE_RGBX, l2rgb}, + {IMAGING_MODE_L, IMAGING_MODE_CMYK, l2cmyk}, + {IMAGING_MODE_L, IMAGING_MODE_YCbCr, l2ycbcr}, + {IMAGING_MODE_L, IMAGING_MODE_HSV, l2hsv}, + + {IMAGING_MODE_LA, IMAGING_MODE_L, la2l}, + {IMAGING_MODE_LA, IMAGING_MODE_La, lA2la}, + {IMAGING_MODE_LA, IMAGING_MODE_RGB, la2rgb}, + {IMAGING_MODE_LA, IMAGING_MODE_RGBA, la2rgb}, + {IMAGING_MODE_LA, IMAGING_MODE_RGBX, la2rgb}, + {IMAGING_MODE_LA, IMAGING_MODE_CMYK, la2cmyk}, + {IMAGING_MODE_LA, IMAGING_MODE_YCbCr, la2ycbcr}, + {IMAGING_MODE_LA, IMAGING_MODE_HSV, la2hsv}, + + {IMAGING_MODE_La, IMAGING_MODE_LA, la2lA}, + + {IMAGING_MODE_I, IMAGING_MODE_L, i2l}, + {IMAGING_MODE_I, IMAGING_MODE_F, i2f}, + {IMAGING_MODE_I, IMAGING_MODE_RGB, i2rgb}, + {IMAGING_MODE_I, IMAGING_MODE_RGBA, i2rgb}, + {IMAGING_MODE_I, IMAGING_MODE_RGBX, i2rgb}, + {IMAGING_MODE_I, IMAGING_MODE_HSV, i2hsv}, + + {IMAGING_MODE_F, IMAGING_MODE_L, f2l}, + {IMAGING_MODE_F, IMAGING_MODE_I, f2i}, + + {IMAGING_MODE_RGB, IMAGING_MODE_1, rgb2bit}, + {IMAGING_MODE_RGB, IMAGING_MODE_L, rgb2l}, + {IMAGING_MODE_RGB, IMAGING_MODE_LA, rgb2la}, + {IMAGING_MODE_RGB, IMAGING_MODE_La, rgb2la}, + {IMAGING_MODE_RGB, IMAGING_MODE_I, rgb2i}, + {IMAGING_MODE_RGB, IMAGING_MODE_I_16, rgb2i16l}, + {IMAGING_MODE_RGB, IMAGING_MODE_I_16L, rgb2i16l}, + {IMAGING_MODE_RGB, IMAGING_MODE_I_16B, rgb2i16b}, +#ifdef WORDS_BIGENDIAN + {IMAGING_MODE_RGB, IMAGING_MODE_I_16N, rgb2i16b}, +#else + {IMAGING_MODE_RGB, IMAGING_MODE_I_16N, rgb2i16l}, +#endif + {IMAGING_MODE_RGB, IMAGING_MODE_F, rgb2f}, + {IMAGING_MODE_RGB, IMAGING_MODE_RGBA, rgb2rgba}, + {IMAGING_MODE_RGB, IMAGING_MODE_RGBa, rgb2rgba}, + {IMAGING_MODE_RGB, IMAGING_MODE_RGBX, rgb2rgba}, + {IMAGING_MODE_RGB, IMAGING_MODE_CMYK, rgb2cmyk}, + {IMAGING_MODE_RGB, IMAGING_MODE_YCbCr, ImagingConvertRGB2YCbCr}, + {IMAGING_MODE_RGB, IMAGING_MODE_HSV, rgb2hsv}, + + {IMAGING_MODE_RGBA, IMAGING_MODE_1, rgb2bit}, + {IMAGING_MODE_RGBA, IMAGING_MODE_L, rgb2l}, + {IMAGING_MODE_RGBA, IMAGING_MODE_LA, rgba2la}, + {IMAGING_MODE_RGBA, IMAGING_MODE_I, rgb2i}, + {IMAGING_MODE_RGBA, IMAGING_MODE_F, rgb2f}, + {IMAGING_MODE_RGBA, IMAGING_MODE_RGB, rgba2rgb}, + {IMAGING_MODE_RGBA, IMAGING_MODE_RGBa, rgbA2rgba}, + {IMAGING_MODE_RGBA, IMAGING_MODE_RGBX, rgb2rgba}, + {IMAGING_MODE_RGBA, IMAGING_MODE_CMYK, rgb2cmyk}, + {IMAGING_MODE_RGBA, IMAGING_MODE_YCbCr, ImagingConvertRGB2YCbCr}, + {IMAGING_MODE_RGBA, IMAGING_MODE_HSV, rgb2hsv}, + + {IMAGING_MODE_RGBa, IMAGING_MODE_RGBA, rgba2rgbA}, + {IMAGING_MODE_RGBa, IMAGING_MODE_RGB, rgba2rgb_}, + + {IMAGING_MODE_RGBX, IMAGING_MODE_1, rgb2bit}, + {IMAGING_MODE_RGBX, IMAGING_MODE_L, rgb2l}, + {IMAGING_MODE_RGBX, IMAGING_MODE_LA, rgb2la}, + {IMAGING_MODE_RGBX, IMAGING_MODE_I, rgb2i}, + {IMAGING_MODE_RGBX, IMAGING_MODE_F, rgb2f}, + {IMAGING_MODE_RGBX, IMAGING_MODE_RGB, rgba2rgb}, + {IMAGING_MODE_RGBX, IMAGING_MODE_CMYK, rgb2cmyk}, + {IMAGING_MODE_RGBX, IMAGING_MODE_YCbCr, ImagingConvertRGB2YCbCr}, + {IMAGING_MODE_RGBX, IMAGING_MODE_HSV, rgb2hsv}, + + {IMAGING_MODE_CMYK, IMAGING_MODE_RGB, cmyk2rgb}, + {IMAGING_MODE_CMYK, IMAGING_MODE_RGBA, cmyk2rgb}, + {IMAGING_MODE_CMYK, IMAGING_MODE_RGBX, cmyk2rgb}, + {IMAGING_MODE_CMYK, IMAGING_MODE_HSV, cmyk2hsv}, + + {IMAGING_MODE_YCbCr, IMAGING_MODE_L, ycbcr2l}, + {IMAGING_MODE_YCbCr, IMAGING_MODE_LA, ycbcr2la}, + {IMAGING_MODE_YCbCr, IMAGING_MODE_RGB, ImagingConvertYCbCr2RGB}, + + {IMAGING_MODE_HSV, IMAGING_MODE_RGB, hsv2rgb}, + + {IMAGING_MODE_I, IMAGING_MODE_I_16, I_I16L}, + {IMAGING_MODE_I_16, IMAGING_MODE_I, I16L_I}, + {IMAGING_MODE_I_16, IMAGING_MODE_RGB, I16_RGB}, + {IMAGING_MODE_L, IMAGING_MODE_I_16, L_I16L}, + {IMAGING_MODE_I_16, IMAGING_MODE_L, I16L_L}, + + {IMAGING_MODE_I, IMAGING_MODE_I_16L, I_I16L}, + {IMAGING_MODE_I_16L, IMAGING_MODE_I, I16L_I}, + {IMAGING_MODE_I, IMAGING_MODE_I_16B, I_I16B}, + {IMAGING_MODE_I_16B, IMAGING_MODE_I, I16B_I}, + + {IMAGING_MODE_L, IMAGING_MODE_I_16L, L_I16L}, + {IMAGING_MODE_I_16L, IMAGING_MODE_L, I16L_L}, + {IMAGING_MODE_L, IMAGING_MODE_I_16B, L_I16B}, + {IMAGING_MODE_I_16B, IMAGING_MODE_L, I16B_L}, +#ifdef WORDS_BIGENDIAN + {IMAGING_MODE_L, IMAGING_MODE_I_16N, L_I16B}, + {IMAGING_MODE_I_16N, IMAGING_MODE_L, I16B_L}, +#else + {IMAGING_MODE_L, IMAGING_MODE_I_16N, L_I16L}, + {IMAGING_MODE_I_16N, IMAGING_MODE_L, I16L_L}, +#endif + + {IMAGING_MODE_I_16, IMAGING_MODE_F, I16L_F}, + {IMAGING_MODE_I_16L, IMAGING_MODE_F, I16L_F}, + {IMAGING_MODE_I_16B, IMAGING_MODE_F, I16B_F} +}; + static Imaging -convert( - Imaging imOut, Imaging imIn, const char *mode, ImagingPalette palette, int dither) { +convert(Imaging imOut, Imaging imIn, ModeID mode, ImagingPalette palette, int dither) { ImagingSectionCookie cookie; ImagingShuffler convert; - int y; if (!imIn) { return (Imaging)ImagingError_ModeError(); } - if (!mode) { + if (mode == IMAGING_MODE_UNKNOWN) { /* Map palette image to full depth */ if (!imIn->palette) { return (Imaging)ImagingError_ModeError(); @@ -1640,33 +1605,31 @@ convert( mode = imIn->palette->mode; } else { /* Same mode? */ - if (!strcmp(imIn->mode, mode)) { + if (imIn->mode == mode) { return ImagingCopy2(imOut, imIn); } } /* test for special conversions */ - if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "PA") == 0) { + if (imIn->mode == IMAGING_MODE_P || imIn->mode == IMAGING_MODE_PA) { return frompalette(imOut, imIn, mode); } - if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) { + if (mode == IMAGING_MODE_P || mode == IMAGING_MODE_PA) { return topalette(imOut, imIn, mode, palette, dither); } - if (dither && strcmp(mode, "1") == 0) { + if (dither && mode == IMAGING_MODE_1) { return tobilevel(imOut, imIn); } /* standard conversion machinery */ convert = NULL; - - for (y = 0; converters[y].from; y++) { - if (!strcmp(imIn->mode, converters[y].from) && - !strcmp(mode, converters[y].to)) { - convert = converters[y].convert; + for (size_t i = 0; i < sizeof(converters) / sizeof(*converters); i++) { + if (imIn->mode == converters[i].from && mode == converters[i].to) { + convert = converters[i].convert; break; } } @@ -1677,7 +1640,12 @@ convert( #else static char buf[100]; snprintf( - buf, 100, "conversion from %.10s to %.10s not supported", imIn->mode, mode); + buf, + 100, + "conversion from %.10s to %.10s not supported", + getModeData(imIn->mode)->name, + getModeData(mode)->name + ); return (Imaging)ImagingError_ValueError(buf); #endif } @@ -1688,7 +1656,7 @@ convert( } ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) { + for (int y = 0; y < imIn->ysize; y++) { (*convert)((UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize); } ImagingSectionLeave(&cookie); @@ -1697,7 +1665,7 @@ convert( } Imaging -ImagingConvert(Imaging imIn, const char *mode, ImagingPalette palette, int dither) { +ImagingConvert(Imaging imIn, const ModeID mode, ImagingPalette palette, int dither) { return convert(NULL, imIn, mode, palette, dither); } @@ -1707,7 +1675,7 @@ ImagingConvert2(Imaging imOut, Imaging imIn) { } Imaging -ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { +ImagingConvertTransparent(Imaging imIn, const ModeID mode, int r, int g, int b) { ImagingSectionCookie cookie; ImagingShuffler convert; Imaging imOut = NULL; @@ -1721,29 +1689,31 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { return (Imaging)ImagingError_ModeError(); } - if (strcmp(imIn->mode, "RGB") == 0 && - (strcmp(mode, "RGBA") == 0 || strcmp(mode, "RGBa") == 0)) { + if (imIn->mode == IMAGING_MODE_RGB && + (mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_RGBa)) { convert = rgb2rgba; - if (strcmp(mode, "RGBa") == 0) { + if (mode == IMAGING_MODE_RGBa) { premultiplied = 1; } } else if ( - strcmp(imIn->mode, "RGB") == 0 && - (strcmp(mode, "LA") == 0 || strcmp(mode, "La") == 0)) { + imIn->mode == IMAGING_MODE_RGB && + (mode == IMAGING_MODE_LA || mode == IMAGING_MODE_La) + ) { convert = rgb2la; source_transparency = 1; - if (strcmp(mode, "La") == 0) { + if (mode == IMAGING_MODE_La) { premultiplied = 1; } } else if ( - (strcmp(imIn->mode, "1") == 0 || strcmp(imIn->mode, "I") == 0 || - strcmp(imIn->mode, "I;16") == 0 || strcmp(imIn->mode, "L") == 0) && - (strcmp(mode, "RGBA") == 0 || strcmp(mode, "LA") == 0)) { - if (strcmp(imIn->mode, "1") == 0) { + (imIn->mode == IMAGING_MODE_1 || imIn->mode == IMAGING_MODE_I || + imIn->mode == IMAGING_MODE_I_16 || imIn->mode == IMAGING_MODE_L) && + (mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_LA) + ) { + if (imIn->mode == IMAGING_MODE_1) { convert = bit2rgb; - } else if (strcmp(imIn->mode, "I") == 0) { + } else if (imIn->mode == IMAGING_MODE_I) { convert = i2rgb; - } else if (strcmp(imIn->mode, "I;16") == 0) { + } else if (imIn->mode == IMAGING_MODE_I_16) { convert = I16_RGB; } else { convert = l2rgb; @@ -1755,8 +1725,9 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { buf, 100, "conversion from %.10s to %.10s not supported in convert_transparent", - imIn->mode, - mode); + getModeData(imIn->mode)->name, + getModeData(mode)->name + ); return (Imaging)ImagingError_ValueError(buf); } @@ -1778,15 +1749,15 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { } Imaging -ImagingConvertInPlace(Imaging imIn, const char *mode) { +ImagingConvertInPlace(Imaging imIn, const ModeID mode) { ImagingSectionCookie cookie; ImagingShuffler convert; int y; /* limited support for inplace conversion */ - if (strcmp(imIn->mode, "L") == 0 && strcmp(mode, "1") == 0) { + if (imIn->mode == IMAGING_MODE_L && mode == IMAGING_MODE_1) { convert = l2bit; - } else if (strcmp(imIn->mode, "1") == 0 && strcmp(mode, "L") == 0) { + } else if (imIn->mode == IMAGING_MODE_1 && mode == IMAGING_MODE_L) { convert = bit2l; } else { return ImagingError_ModeError(); diff --git a/src/libImaging/ConvertYCbCr.c b/src/libImaging/ConvertYCbCr.c index 142f065e57d..285b4332740 100644 --- a/src/libImaging/ConvertYCbCr.c +++ b/src/libImaging/ConvertYCbCr.c @@ -47,7 +47,8 @@ static INT16 Y_R[] = { 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}; + 4822, 4841, 4861, 4880 +}; static INT16 Y_G[] = { 0, 38, 75, 113, 150, 188, 225, 263, 301, 338, 376, 413, 451, 488, @@ -68,7 +69,8 @@ static INT16 Y_G[] = { 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}; + 9467, 9505, 9542, 9580 +}; static INT16 Y_B[] = { 0, 7, 15, 22, 29, 36, 44, 51, 58, 66, 73, 80, 88, 95, @@ -89,7 +91,8 @@ static INT16 Y_B[] = { 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}; + 1839, 1846, 1853, 1860 +}; static INT16 Cb_R[] = { 0, -10, -21, -31, -42, -53, -64, -75, -85, -96, -107, -118, @@ -113,7 +116,8 @@ static INT16 Cb_R[] = { -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}; + -2720, -2731, -2742, -2753 +}; static INT16 Cb_G[] = { 0, -20, -41, -63, -84, -105, -126, -147, -169, -190, -211, -232, @@ -137,7 +141,8 @@ static INT16 Cb_G[] = { -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}; + -5342, -5363, -5384, -5405 +}; static INT16 Cb_B[] = { 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, @@ -158,7 +163,8 @@ static INT16 Cb_B[] = { 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}; + 8064, 8096, 8128, 8160 +}; #define Cr_R Cb_B @@ -184,7 +190,8 @@ static INT16 Cr_G[] = { -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}; + -6752, -6778, -6805, -6832 +}; static INT16 Cr_B[] = { 0, -4, -9, -15, -20, -25, -30, -35, -41, -46, -51, -56, @@ -208,7 +215,8 @@ static INT16 Cr_B[] = { -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}; + -1310, -1316, -1321, -1326 +}; static INT16 R_Cr[] = { -11484, -11394, -11305, -11215, -11125, -11036, -10946, -10856, -10766, -10677, @@ -236,7 +244,8 @@ static INT16 R_Cr[] = { 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}; + 10947, 11037, 11126, 11216, 11306, 11395 +}; static INT16 G_Cb[] = { 2819, 2797, 2775, 2753, 2731, 2709, 2687, 2665, 2643, 2621, 2599, 2577, @@ -260,7 +269,8 @@ static INT16 G_Cb[] = { -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}; + -2730, -2752, -2774, -2796 +}; static INT16 G_Cr[] = { 5850, 5805, 5759, 5713, 5667, 5622, 5576, 5530, 5485, 5439, 5393, 5347, @@ -284,7 +294,8 @@ static INT16 G_Cr[] = { -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}; + -5666, -5712, -5758, -5804 +}; static INT16 B_Cb[] = { -14515, -14402, -14288, -14175, -14062, -13948, -13835, -13721, -13608, -13495, @@ -312,7 +323,8 @@ static INT16 B_Cb[] = { 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}; + 13836, 13949, 14063, 14176, 14289, 14403 +}; void ImagingConvertRGB2YCbCr(UINT8 *out, const UINT8 *in, int pixels) { diff --git a/src/libImaging/Dib.c b/src/libImaging/Dib.c index 269be10587b..2afe71d4ac9 100644 --- a/src/libImaging/Dib.c +++ b/src/libImaging/Dib.c @@ -25,20 +25,17 @@ #include "ImDib.h" -char * +ModeID ImagingGetModeDIB(int size_out[2]) { /* Get device characteristics */ - HDC dc; - char *mode; + const HDC dc = CreateCompatibleDC(NULL); - dc = CreateCompatibleDC(NULL); - - mode = "P"; + ModeID mode = IMAGING_MODE_P; if (!(GetDeviceCaps(dc, RASTERCAPS) & RC_PALETTE)) { - mode = "RGB"; + mode = IMAGING_MODE_RGB; if (GetDeviceCaps(dc, BITSPIXEL) == 1) { - mode = "1"; + mode = IMAGING_MODE_1; } } @@ -53,7 +50,7 @@ ImagingGetModeDIB(int size_out[2]) { } ImagingDIB -ImagingNewDIB(const char *mode, int xsize, int ysize) { +ImagingNewDIB(const ModeID mode, int xsize, int ysize) { /* Create a Windows bitmap */ ImagingDIB dib; @@ -61,10 +58,12 @@ 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) { + if (mode != IMAGING_MODE_1 && mode != IMAGING_MODE_L && mode != IMAGING_MODE_RGB) { return (ImagingDIB)ImagingError_ModeError(); } + const int pixelsize = mode == IMAGING_MODE_RGB ? 3 : 1; + /* Create DIB context and info header */ /* malloc check ok, small constant allocation */ dib = (ImagingDIB)malloc(sizeof(*dib)); @@ -83,7 +82,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 = pixelsize * 8; dib->info->bmiHeader.biCompression = BI_RGB; /* Create DIB */ @@ -95,19 +94,20 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) { } dib->bitmap = CreateDIBSection( - dib->dc, dib->info, DIB_RGB_COLORS, (void **)&dib->bits, NULL, 0); + dib->dc, dib->info, DIB_RGB_COLORS, (void **)&dib->bits, NULL, 0 + ); if (!dib->bitmap) { free(dib->info); free(dib); return (ImagingDIB)ImagingError_MemoryError(); } - strcpy(dib->mode, mode); + dib->mode = mode; dib->xsize = xsize; dib->ysize = ysize; - dib->pixelsize = strlen(mode); - dib->linesize = (xsize * dib->pixelsize + 3) & -4; + dib->pixelsize = pixelsize; + dib->linesize = (xsize * pixelsize + 3) & -4; if (dib->pixelsize == 1) { dib->pack = dib->unpack = (ImagingShuffler)memcpy; @@ -131,7 +131,7 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) { } /* Create an associated palette (for 8-bit displays only) */ - if (strcmp(ImagingGetModeDIB(NULL), "P") == 0) { + if (ImagingGetModeDIB(NULL) == IMAGING_MODE_P) { char palbuf[sizeof(LOGPALETTE) + 256 * sizeof(PALETTEENTRY)]; LPLOGPALETTE pal = (LPLOGPALETTE)palbuf; int i, r, g, b; @@ -141,7 +141,7 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) { pal->palNumEntries = 256; GetSystemPaletteEntries(dib->dc, 0, 256, pal->palPalEntry); - if (strcmp(mode, "L") == 0) { + if (mode == IMAGING_MODE_L) { /* Grayscale DIB. Fill all 236 slots with a grayscale ramp * (this is usually overkill on Windows since VGA only offers * 6 bits grayscale resolution). Ignore the slots already @@ -155,8 +155,7 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) { } dib->palette = CreatePalette(pal); - - } else if (strcmp(mode, "RGB") == 0) { + } else if (mode == IMAGING_MODE_RGB) { #ifdef CUBE216 /* Colour DIB. Create a 6x6x6 colour cube (216 entries) and @@ -218,7 +217,8 @@ ImagingPasteDIB(ImagingDIB dib, Imaging im, int xy[4]) { dib->bits + dib->linesize * (dib->ysize - (xy[1] + y) - 1) + xy[0] * dib->pixelsize, im->image[y], - im->xsize); + im->xsize + ); } } @@ -251,7 +251,8 @@ ImagingDrawDIB(ImagingDIB dib, void *dc, int dst[4], int src[4]) { dib->bits, dib->info, DIB_RGB_COLORS, - SRCCOPY); + SRCCOPY + ); } else { /* stretchblt (displays) */ if (dib->palette != 0) { @@ -268,7 +269,8 @@ ImagingDrawDIB(ImagingDIB dib, void *dc, int dst[4], int src[4]) { src[1], src[2] - src[0], src[3] - src[1], - SRCCOPY); + SRCCOPY + ); } } diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 133696dd86f..0d28069f000 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -63,12 +63,12 @@ typedef struct { } Edge; /* Type used in "polygon*" functions */ -typedef void (*hline_handler)(Imaging, int, int, int, int); +typedef void (*hline_handler)(Imaging, int, int, int, int, Imaging); static inline void 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) { + if (isModeI16(im->mode)) { #ifdef WORDS_BIGENDIAN im->image8[y][x * 2] = (UINT8)(ink >> 8); im->image8[y][x * 2 + 1] = (UINT8)ink; @@ -103,9 +103,7 @@ point32rgba(Imaging im, int x, int y, int ink) { } static inline void -hline8(Imaging im, int x0, int y0, int x1, int ink) { - int pixelwidth; - +hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { if (y0 >= 0 && y0 < im->ysize) { if (x0 < 0) { x0 = 0; @@ -118,17 +116,41 @@ hline8(Imaging im, int x0, int y0, int x1, int ink) { 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); + int bigendian = -1; + if (isModeI16(im->mode)) { + bigendian = + ( +#ifdef WORDS_BIGENDIAN + im->mode == IMAGING_MODE_I_16 || im->mode == IMAGING_MODE_I_16L +#else + im->mode == IMAGING_MODE_I_16B +#endif + ) + ? 1 + : 0; + } + if (mask == NULL && bigendian == -1) { + memset(im->image8[y0] + x0, (UINT8)ink, (x1 - x0 + 1)); + } else { + UINT8 *p = im->image8[y0]; + while (x0 <= x1) { + if (mask == NULL || mask->image8[y0][x0]) { + if (bigendian == -1) { + p[x0] = ink; + } else { + p[x0 * 2 + (bigendian ? 1 : 0)] = ink; + p[x0 * 2 + (bigendian ? 0 : 1)] = ink >> 8; + } + } + x0++; + } + } } } } 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, Imaging mask) { INT32 *p; if (y0 >= 0 && y0 < im->ysize) { @@ -144,13 +166,16 @@ hline32(Imaging im, int x0, int y0, int x1, int ink) { } p = im->image32[y0]; while (x0 <= x1) { - p[x0++] = ink; + if (mask == NULL || mask->image8[y0][x0]) { + p[x0] = ink; + } + x0++; } } } 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, Imaging mask) { unsigned int tmp; if (y0 >= 0 && y0 < im->ysize) { @@ -168,9 +193,11 @@ hline32rgba(Imaging im, int x0, int y0, int x1, int 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], tmp); - out[1] = BLEND(in[3], out[1], in[1], tmp); - out[2] = BLEND(in[3], out[2], in[2], tmp); + if (mask == NULL || mask->image8[y0][x0]) { + out[0] = BLEND(in[3], out[0], in[0], tmp); + out[1] = BLEND(in[3], out[1], in[1], tmp); + out[2] = BLEND(in[3], out[2], in[2], tmp); + } x0++; out += 4; } @@ -408,7 +435,15 @@ x_cmp(const void *x0, const void *x1) { static void draw_horizontal_lines( - Imaging im, int n, Edge *e, int ink, int *x_pos, int y, hline_handler hline) { + Imaging im, + int n, + Edge *e, + int ink, + int *x_pos, + int y, + hline_handler hline, + Imaging mask +) { int i; for (i = 0; i < n; i++) { if (e[i].ymin == y && e[i].ymin == e[i].ymax) { @@ -429,7 +464,7 @@ draw_horizontal_lines( } } - (*hline)(im, xmin, e[i].ymin, xmax, ink); + (*hline)(im, xmin, e[i].ymin, xmax, ink, mask); *x_pos = xmax + 1; } } @@ -440,13 +475,8 @@ draw_horizontal_lines( */ static inline int polygon_generic( - Imaging im, - int n, - Edge *e, - int ink, - int eofill, - hline_handler hline, - int hasAlpha) { + Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, Imaging mask +) { Edge **edge_table; float *xx; int edge_count = 0; @@ -466,6 +496,7 @@ polygon_generic( return -1; } + int hasAlpha = hline == hline32rgba; for (i = 0; i < n; i++) { if (ymin > e[i].ymin) { ymin = e[i].ymin; @@ -475,7 +506,7 @@ polygon_generic( } if (e[i].ymin == e[i].ymax) { if (hasAlpha != 1) { - (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink); + (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink, mask); } continue; } @@ -506,52 +537,52 @@ polygon_generic( // Needed to draw consistent polygons xx[j] = xx[j - 1]; j++; - } else if (current->dx != 0 && roundf(xx[j - 1]) == xx[j - 1]) { + } else if ( + (ymin == current->ymin || ymin == current->ymax) && current->dx != 0 + ) { // 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)) { + if ((ymin != other_edge->ymin && ymin != other_edge->ymax) || + 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) { + if (roundf(xx[j - 1]) == + roundf( + (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; + int offset = ymin == current->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; + if (ymin + offset >= other_edge->ymin && + ymin + offset <= other_edge->ymax) { + adjacent_line_x_other_edge = + (ymin + offset - other_edge->y0) * other_edge->dx + + other_edge->x0; + if (xx[j - 1] > adjacent_line_x + 1 && + xx[j - 1] > adjacent_line_x_other_edge + 1) { + xx[j - 1] = + roundf(fmax( + adjacent_line_x, adjacent_line_x_other_edge + )) + + 1; + } else if ( + xx[j - 1] < adjacent_line_x - 1 && + xx[j - 1] < adjacent_line_x_other_edge - 1 + ) { + xx[j - 1] = + roundf(fmin( + adjacent_line_x, adjacent_line_x_other_edge + )) - + 1; } + break; } - break; } } } @@ -566,7 +597,7 @@ polygon_generic( // Line would be before the current position continue; } - draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); + draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline, mask); if (x_end < x_pos) { // Line would be before the current position continue; @@ -582,13 +613,13 @@ polygon_generic( continue; } } - (*hline)(im, x_start, ymin, x_end, ink); + (*hline)(im, x_start, ymin, x_end, ink, mask); x_pos = x_end + 1; } - draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); + draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline, mask); } else { for (i = 1; i < j; i += 2) { - (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink); + (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink, mask); } } } @@ -598,21 +629,6 @@ polygon_generic( return 0; } -static inline int -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, 0); -} - -static inline int -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) { /* printf("edge %d %d %d %d\n", x0, y0, x1, y1); */ @@ -647,30 +663,29 @@ add_edge(Edge *e, int x0, int y0, int x1, int y1) { typedef struct { void (*point)(Imaging im, int x, int y, int ink); - void (*hline)(Imaging im, int x0, int y0, int x1, int ink); + void (*hline)(Imaging im, int x0, int y0, int x1, int ink, Imaging mask); void (*line)(Imaging im, int x0, int y0, int x1, int y1, int ink); - 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}; +DRAW draw32 = {point32, hline32, line32}; +DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba}; /* -------------------------------------------------------------------- */ /* Interface */ /* -------------------------------------------------------------------- */ -#define DRAWINIT() \ - if (im->image8) { \ - draw = &draw8; \ - if (strncmp(im->mode, "I;16", 4) == 0) { \ - ink = INK16(ink_); \ - } else { \ - ink = INK8(ink_); \ - } \ - } else { \ - draw = (op) ? &draw32rgba : &draw32; \ - memcpy(&ink, ink_, sizeof(ink)); \ +#define DRAWINIT() \ + if (im->image8) { \ + draw = &draw8; \ + if (isModeI16(im->mode)) { \ + ink = INK16(ink_); \ + } else { \ + ink = INK8(ink_); \ + } \ + } else { \ + draw = (op) ? &draw32rgba : &draw32; \ + memcpy(&ink, ink_, sizeof(ink)); \ } int @@ -699,7 +714,16 @@ ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink_, in int ImagingDrawWideLine( - Imaging im, int x0, int y0, int x1, int y1, const void *ink_, int width, int op) { + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink_, + int width, + int op, + Imaging mask +) { DRAW *draw; INT32 ink; int dx, dy; @@ -730,14 +754,15 @@ ImagingDrawWideLine( {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]); - draw->polygon(im, 4, e, ink, 0); + polygon_generic(im, 4, e, ink, 0, draw->hline, mask); } return 0; } @@ -752,7 +777,8 @@ ImagingDrawRectangle( const void *ink_, int fill, int width, - int op) { + int op +) { int i; int y; int tmp; @@ -779,7 +805,7 @@ ImagingDrawRectangle( } for (y = y0; y <= y1; y++) { - draw->hline(im, x0, y, x1, ink); + draw->hline(im, x0, y, x1, ink, NULL); } } else { @@ -788,8 +814,8 @@ ImagingDrawRectangle( 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->hline(im, x0, y0 + i, x1, ink, NULL); + draw->hline(im, x0, y1 - i, x1, ink, NULL); 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); } @@ -800,7 +826,15 @@ ImagingDrawRectangle( int ImagingDrawPolygon( - Imaging im, int count, int *xy, const void *ink_, int fill, int width, int op) { + Imaging im, + int count, + int *xy, + const void *ink_, + int fill, + int width, + int op, + Imaging mask +) { int i, n, x0, y0, x1, y1; DRAW *draw; INT32 ink; @@ -843,7 +877,7 @@ ImagingDrawPolygon( 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); + polygon_generic(im, n, e, ink, 0, draw->hline, mask); free(e); } else { @@ -851,7 +885,8 @@ ImagingDrawPolygon( 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); + 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 { @@ -864,10 +899,13 @@ ImagingDrawPolygon( xy[i * 2 + 3], ink_, width, - op); + op, + mask + ); } ImagingDrawWideLine( - im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op); + im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op, mask + ); } } @@ -877,7 +915,8 @@ ImagingDrawPolygon( int 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 + ); } /* -------------------------------------------------------------------- */ @@ -1086,7 +1125,8 @@ clip_tree_transpose(clip_node *root) { // 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) { + 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) { @@ -1223,7 +1263,8 @@ typedef struct { } clip_ellipse_state; typedef void (*clip_ellipse_init)( - clip_ellipse_state *, int32_t, int32_t, int32_t, float, float); + clip_ellipse_state *, int32_t, int32_t, int32_t, float, float +); void debug_clip_tree(clip_node *root, int space) { @@ -1335,7 +1376,8 @@ arc_init(clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float // A chord line. void chord_line_init( - clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float ar) { + 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; @@ -1362,7 +1404,8 @@ chord_line_init( // Pie side. void pie_side_init( - clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float _) { + 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; @@ -1478,7 +1521,8 @@ clip_ellipse_free(clip_ellipse_state *s) { int8_t clip_ellipse_next( - clip_ellipse_state *s, int32_t *ret_x0, int32_t *ret_y, int32_t *ret_x1) { + 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) { @@ -1512,7 +1556,8 @@ ellipseNew( const void *ink_, int fill, int width, - int op) { + int op +) { DRAW *draw; INT32 ink; DRAWINIT(); @@ -1530,7 +1575,9 @@ ellipseNew( 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); + draw->hline( + im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink, NULL + ); } return 0; } @@ -1547,7 +1594,8 @@ clipEllipseNew( const void *ink_, int width, int op, - clip_ellipse_init init) { + clip_ellipse_init init +) { DRAW *draw; INT32 ink; DRAWINIT(); @@ -1564,7 +1612,9 @@ clipEllipseNew( 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); + draw->hline( + im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink, NULL + ); } clip_ellipse_free(&st); return next_code == -1 ? 0 : -1; @@ -1580,7 +1630,8 @@ arcNew( float end, const void *ink_, int width, - int op) { + int op +) { return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, arc_init); } @@ -1595,7 +1646,8 @@ chordNew( float end, const void *ink_, int width, - int op) { + int op +) { return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, chord_init); } @@ -1610,9 +1662,11 @@ chordLineNew( float end, const void *ink_, int width, - int op) { + int op +) { return clipEllipseNew( - im, x0, y0, x1, y1, start, end, ink_, width, op, chord_line_init); + im, x0, y0, x1, y1, start, end, ink_, width, op, chord_line_init + ); } static int @@ -1626,7 +1680,8 @@ pieNew( float end, const void *ink_, int width, - int op) { + int op +) { return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, pie_init); } @@ -1640,7 +1695,8 @@ pieSideNew( float start, const void *ink_, int width, - int op) { + int op +) { return clipEllipseNew(im, x0, y0, x1, y1, start, 0, ink_, width, op, pie_side_init); } @@ -1654,7 +1710,8 @@ ImagingDrawEllipse( const void *ink, int fill, int width, - int op) { + int op +) { return ellipseNew(im, x0, y0, x1, y1, ink, fill, width, op); } @@ -1669,7 +1726,8 @@ ImagingDrawArc( float end, const void *ink, int width, - int op) { + int op +) { normalize_angles(&start, &end); if (start + 360 == end) { return ImagingDrawEllipse(im, x0, y0, x1, y1, ink, 0, width, op); @@ -1692,7 +1750,8 @@ ImagingDrawChord( const void *ink, int fill, int width, - int op) { + int op +) { normalize_angles(&start, &end); if (start + 360 == end) { return ImagingDrawEllipse(im, x0, y0, x1, y1, ink, fill, width, op); @@ -1722,7 +1781,8 @@ ImagingDrawPieslice( const void *ink, int fill, int width, - int op) { + int op +) { normalize_angles(&start, &end); if (start + 360 == end) { return ellipseNew(im, x0, y0, x1, y1, ink, fill, width, op); @@ -1850,13 +1910,8 @@ ImagingOutlineLine(ImagingOutline outline, float x1, float y1) { int ImagingOutlineCurve( - ImagingOutline outline, - float x1, - float y1, - float x2, - float y2, - float x3, - float y3) { + ImagingOutline outline, float x1, float y1, float x2, float y2, float x3, float y3 +) { Edge *e; int i; float xo, yo; @@ -1970,13 +2025,14 @@ ImagingOutlineTransform(ImagingOutline outline, double a[6]) { int ImagingDrawOutline( - Imaging im, ImagingOutline outline, const void *ink_, int fill, int op) { + Imaging im, ImagingOutline outline, const void *ink_, int fill, int op +) { DRAW *draw; INT32 ink; DRAWINIT(); - draw->polygon(im, outline->count, outline->edges, ink, 0); + polygon_generic(im, outline->count, outline->edges, ink, 0, draw->hline, NULL); return 0; } diff --git a/src/libImaging/Effects.c b/src/libImaging/Effects.c index 93e7af0bce9..c05c5764e44 100644 --- a/src/libImaging/Effects.c +++ b/src/libImaging/Effects.c @@ -36,7 +36,7 @@ ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality) { return (Imaging)ImagingError_ValueError(NULL); } - im = ImagingNewDirty("L", xsize, ysize); + im = ImagingNewDirty(IMAGING_MODE_L, xsize, ysize); if (!im) { return NULL; } @@ -80,7 +80,7 @@ ImagingEffectNoise(int xsize, int ysize, float sigma) { int nextok; double this, next; - imOut = ImagingNewDirty("L", xsize, ysize); + imOut = ImagingNewDirty(IMAGING_MODE_L, xsize, ysize); if (!imOut) { return NULL; } diff --git a/src/libImaging/Except.c b/src/libImaging/Except.c deleted file mode 100644 index f42ff9aec9e..00000000000 --- a/src/libImaging/Except.c +++ /dev/null @@ -1,72 +0,0 @@ -/* - * The Python Imaging Library - * $Id$ - * - * default exception handling - * - * This module is usually overridden by application code (e.g. - * _imaging.c for PIL's standard Python bindings). If you get - * linking errors, remove this file from your project/library. - * - * history: - * 1995-06-15 fl Created - * 1998-12-29 fl Minor tweaks - * 2003-09-13 fl Added ImagingEnter/LeaveSection() - * - * Copyright (c) 1997-2003 by Secret Labs AB. - * Copyright (c) 1995-2003 by Fredrik Lundh. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -/* exception state */ - -void * -ImagingError_OSError(void) { - fprintf(stderr, "*** exception: file access error\n"); - return NULL; -} - -void * -ImagingError_MemoryError(void) { - fprintf(stderr, "*** exception: out of memory\n"); - return NULL; -} - -void * -ImagingError_ModeError(void) { - return ImagingError_ValueError("bad image mode"); -} - -void * -ImagingError_Mismatch(void) { - return ImagingError_ValueError("images don't match"); -} - -void * -ImagingError_ValueError(const char *message) { - if (!message) { - message = "exception: bad argument to function"; - } - fprintf(stderr, "*** %s\n", message); - return NULL; -} - -void -ImagingError_Clear(void) { - /* nop */; -} - -/* thread state */ - -void -ImagingSectionEnter(ImagingSectionCookie *cookie) { - /* pass */ -} - -void -ImagingSectionLeave(ImagingSectionCookie *cookie) { - /* pass */ -} diff --git a/src/libImaging/File.c b/src/libImaging/File.c index 76d0abccc4f..435dbeca0d4 100644 --- a/src/libImaging/File.c +++ b/src/libImaging/File.c @@ -23,14 +23,13 @@ int ImagingSaveRaw(Imaging im, FILE *fp) { int x, y, i; - if (strcmp(im->mode, "1") == 0 || strcmp(im->mode, "L") == 0) { + if (im->mode == IMAGING_MODE_1 || im->mode == IMAGING_MODE_L) { /* @PIL227: FIXME: for mode "1", map != 0 to 255 */ /* PGM "L" */ 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++) { @@ -54,14 +53,14 @@ ImagingSavePPM(Imaging im, const char *outfile) { fp = fopen(outfile, "wb"); if (!fp) { - (void)ImagingError_OSError(); + PyErr_SetString(PyExc_OSError, "error when accessing file"); return 0; } - if (strcmp(im->mode, "1") == 0 || strcmp(im->mode, "L") == 0) { + if (im->mode == IMAGING_MODE_1 || im->mode == IMAGING_MODE_L) { /* Write "PGM" */ fprintf(fp, "P5\n%d %d\n255\n", im->xsize, im->ysize); - } else if (strcmp(im->mode, "RGB") == 0) { + } else if (im->mode == IMAGING_MODE_RGB) { /* Write "PPM" */ fprintf(fp, "P6\n%d %d\n255\n", im->xsize, im->ysize); } else { diff --git a/src/libImaging/Fill.c b/src/libImaging/Fill.c index 5b6bfb89cd8..cbd303204d5 100644 --- a/src/libImaging/Fill.c +++ b/src/libImaging/Fill.c @@ -68,11 +68,12 @@ ImagingFill(Imaging im, const void *colour) { } Imaging -ImagingFillLinearGradient(const char *mode) { +ImagingFillLinearGradient(const ModeID mode) { Imaging im; int y; - if (strlen(mode) != 1) { + if (mode != IMAGING_MODE_1 && mode != IMAGING_MODE_F && mode != IMAGING_MODE_I && + mode != IMAGING_MODE_L && mode != IMAGING_MODE_P) { return (Imaging)ImagingError_ModeError(); } @@ -102,12 +103,13 @@ ImagingFillLinearGradient(const char *mode) { } Imaging -ImagingFillRadialGradient(const char *mode) { +ImagingFillRadialGradient(const ModeID mode) { Imaging im; int x, y; int d; - if (strlen(mode) != 1) { + if (mode != IMAGING_MODE_1 && mode != IMAGING_MODE_F && mode != IMAGING_MODE_I && + mode != IMAGING_MODE_L && mode != IMAGING_MODE_P) { return (Imaging)ImagingError_ModeError(); } @@ -119,7 +121,8 @@ 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); + (double)((x - 128) * (x - 128) + (y - 128) * (y - 128)) * 2.0 + ); if (d >= 255) { d = 255; } diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index 85de77fcbbc..cefb8fcdc36 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -26,6 +26,8 @@ #include "Imaging.h" +#define ROUND_UP(f) ((int)((f) >= 0.0 ? (f) + 0.5F : (f) - 0.5F)) + static inline UINT8 clip8(float in) { if (in <= 0.0) { @@ -59,7 +61,8 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin) { } imOut = ImagingNewDirty( - imIn->mode, imIn->xsize + 2 * xmargin, imIn->ysize + 2 * ymargin); + imIn->mode, imIn->xsize + 2 * xmargin, imIn->ysize + 2 * ymargin + ); if (!imOut) { return NULL; } @@ -104,6 +107,22 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin) { return imOut; } +float +kernel_i16(int size, UINT8 *in0, int x, const float *kernel, int bigendian) { + int i; + float result = 0; + int half_size = (size - 1) / 2; + for (i = 0; i < size; i++) { + int x1 = x + i - half_size; + result += _i2f( + in0[x1 * 2 + (bigendian ? 1 : 0)] + + (in0[x1 * 2 + (bigendian ? 0 : 1)] >> 8) + ) * + kernel[i]; + } + return result; +} + void ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { #define KERNEL1x3(in0, x, kernel, d) \ @@ -134,6 +153,17 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { out[x] = in0[x]; } } else { + int bigendian = 0; + if (im->type == IMAGING_TYPE_SPECIAL) { + if ( + im->mode == IMAGING_MODE_I_16B +#ifdef WORDS_BIGENDIAN + || im->mode == IMAGING_MODE_I_16N +#endif + ) { + bigendian = 1; + } + } for (y = 1; y < im->ysize - 1; y++) { UINT8 *in_1 = (UINT8 *)im->image[y - 1]; UINT8 *in0 = (UINT8 *)im->image[y]; @@ -141,14 +171,31 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { UINT8 *out = (UINT8 *)imOut->image[y]; out[0] = in0[0]; + if (im->type == IMAGING_TYPE_SPECIAL) { + out[1] = in0[1]; + } 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); + if (im->type == IMAGING_TYPE_SPECIAL) { + ss += kernel_i16(3, in1, x, &kernel[0], bigendian); + ss += kernel_i16(3, in0, x, &kernel[3], bigendian); + ss += kernel_i16(3, in_1, x, &kernel[6], bigendian); + int ss_int = ROUND_UP(ss); + out[x * 2 + (bigendian ? 1 : 0)] = clip8(ss_int % 256); + out[x * 2 + (bigendian ? 0 : 1)] = clip8(ss_int >> 8); + } else { + 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); + } + } + if (im->type == IMAGING_TYPE_SPECIAL) { + out[x * 2] = in0[x * 2]; + out[x * 2 + 1] = in0[x * 2 + 1]; + } else { + out[x] = in0[x]; } - out[x] = in0[x]; } } } else { @@ -260,6 +307,17 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { out[x + 1] = in0[x + 1]; } } else { + int bigendian = 0; + if (im->type == IMAGING_TYPE_SPECIAL) { + if ( + im->mode == IMAGING_MODE_I_16B +#ifdef WORDS_BIGENDIAN + || im->mode == IMAGING_MODE_I_16N +#endif + ) { + bigendian = 1; + } + } for (y = 2; y < im->ysize - 2; y++) { UINT8 *in_2 = (UINT8 *)im->image[y - 2]; UINT8 *in_1 = (UINT8 *)im->image[y - 1]; @@ -270,17 +328,39 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { out[0] = in0[0]; out[1] = in0[1]; + if (im->type == IMAGING_TYPE_SPECIAL) { + out[2] = in0[2]; + out[3] = in0[3]; + } 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); - ss += KERNEL1x5(in0, x, &kernel[10], 1); - ss += KERNEL1x5(in_1, x, &kernel[15], 1); - ss += KERNEL1x5(in_2, x, &kernel[20], 1); - out[x] = clip8(ss); + if (im->type == IMAGING_TYPE_SPECIAL) { + ss += kernel_i16(5, in2, x, &kernel[0], bigendian); + ss += kernel_i16(5, in1, x, &kernel[5], bigendian); + ss += kernel_i16(5, in0, x, &kernel[10], bigendian); + ss += kernel_i16(5, in_1, x, &kernel[15], bigendian); + ss += kernel_i16(5, in_2, x, &kernel[20], bigendian); + int ss_int = ROUND_UP(ss); + out[x * 2 + (bigendian ? 1 : 0)] = clip8(ss_int % 256); + out[x * 2 + (bigendian ? 0 : 1)] = clip8(ss_int >> 8); + } else { + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip8(ss); + } + } + if (im->type == IMAGING_TYPE_SPECIAL) { + out[x * 2 + 0] = in0[x * 2 + 0]; + out[x * 2 + 1] = in0[x * 2 + 1]; + out[x * 2 + 2] = in0[x * 2 + 2]; + out[x * 2 + 3] = in0[x * 2 + 3]; + } else { + 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 { @@ -369,7 +449,8 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { } } memcpy( - out + x * sizeof(UINT32), in0 + x * sizeof(UINT32), sizeof(UINT32) * 2); + out + x * sizeof(UINT32), in0 + x * sizeof(UINT32), sizeof(UINT32) * 2 + ); } } memcpy(imOut->image[y], im->image[y], im->linesize); @@ -381,7 +462,8 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 o Imaging imOut; ImagingSectionCookie cookie; - if (im->type != IMAGING_TYPE_UINT8 && im->type != IMAGING_TYPE_INT32) { + if (im->type == IMAGING_TYPE_FLOAT32 || + (im->type == IMAGING_TYPE_SPECIAL && im->bands != 1)) { return (Imaging)ImagingError_ModeError(); } diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c index 6b2518d35ce..9b494dfa2ca 100644 --- a/src/libImaging/FliDecode.c +++ b/src/libImaging/FliDecode.c @@ -16,9 +16,11 @@ #include "Imaging.h" -#define I16(ptr) ((ptr)[0] + ((ptr)[1] << 8)) +#define I16(ptr) ((ptr)[0] + ((int)(ptr)[1] << 8)) -#define I32(ptr) ((ptr)[0] + ((ptr)[1] << 8) + ((ptr)[2] << 16) + ((ptr)[3] << 24)) +#define I32(ptr) \ + ((ptr)[0] + ((unsigned long)(ptr)[1] << 8) + ((unsigned long)(ptr)[2] << 16) + \ + ((unsigned long)(ptr)[3] << 24)) #define ERR_IF_DATA_OOB(offset) \ if ((data + (offset)) > ptr + bytes) { \ @@ -29,8 +31,8 @@ int ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { UINT8 *ptr; - int framesize; - int c, chunks, advance; + unsigned long framesize, advance; + int c, chunks; int l, lines; int i, j, x = 0, y, ymax; @@ -224,7 +226,7 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt break; case 16: /* COPY chunk */ - if (INT32_MAX / state->xsize < state->ysize) { + if (INT32_MAX < (uint64_t)state->xsize * state->ysize) { /* Integer overflow, bail */ state->errcode = IMAGING_CODEC_OVERRUN; return -1; diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index cf3bc997942..2186f95f8e5 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -19,7 +19,7 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int x, y, xr; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { @@ -41,7 +41,7 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn) { ImagingSectionEnter(&cookie); if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { + if (isModeI16(imIn->mode)) { FLIP_LEFT_RIGHT(UINT16, image8) } else { FLIP_LEFT_RIGHT(UINT8, image8) @@ -62,7 +62,7 @@ ImagingFlipTopBottom(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int y, yr; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { @@ -89,7 +89,7 @@ ImagingRotate90(Imaging imOut, Imaging imIn) { int x, y, xx, yy, xr, xxsize, yysize; int xxx, yyy, xxxsize, yyysize; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { @@ -127,7 +127,7 @@ ImagingRotate90(Imaging imOut, Imaging imIn) { ImagingSectionEnter(&cookie); if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { + if (isModeI16(imIn->mode)) { ROTATE_90(UINT16, image8); } else { ROTATE_90(UINT8, image8); @@ -149,7 +149,7 @@ ImagingTranspose(Imaging imOut, Imaging imIn) { int x, y, xx, yy, xxsize, yysize; int xxx, yyy, xxxsize, yyysize; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { @@ -186,7 +186,7 @@ ImagingTranspose(Imaging imOut, Imaging imIn) { ImagingSectionEnter(&cookie); if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { + if (isModeI16(imIn->mode)) { TRANSPOSE(UINT16, image8); } else { TRANSPOSE(UINT8, image8); @@ -208,7 +208,7 @@ ImagingTransverse(Imaging imOut, Imaging imIn) { int x, y, xr, yr, xx, yy, xxsize, yysize; int xxx, yyy, xxxsize, yyysize; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { @@ -247,7 +247,7 @@ ImagingTransverse(Imaging imOut, Imaging imIn) { ImagingSectionEnter(&cookie); if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { + if (isModeI16(imIn->mode)) { TRANSVERSE(UINT16, image8); } else { TRANSVERSE(UINT8, image8); @@ -268,7 +268,7 @@ ImagingRotate180(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int x, y, xr, yr; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) { @@ -291,7 +291,7 @@ ImagingRotate180(Imaging imOut, Imaging imIn) { yr = imIn->ysize - 1; if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { + if (isModeI16(imIn->mode)) { ROTATE_180(UINT16, image8) } else { ROTATE_180(UINT8, image8) @@ -313,7 +313,7 @@ ImagingRotate270(Imaging imOut, Imaging imIn) { int x, y, xx, yy, yr, xxsize, yysize; int xxx, yyy, xxxsize, yyysize; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) { @@ -351,7 +351,7 @@ ImagingRotate270(Imaging imOut, Imaging imIn) { ImagingSectionEnter(&cookie); if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { + if (isModeI16(imIn->mode)) { ROTATE_270(UINT16, image8); } else { ROTATE_270(UINT8, image8); @@ -714,14 +714,7 @@ getfilter(Imaging im, int filterid) { 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; - } + return nearest_filter16; } } else { return nearest_filter32; @@ -781,7 +774,8 @@ ImagingGenericTransform( ImagingTransformMap transform, void *transform_data, int filterid, - int fill) { + int fill +) { /* slow generic transformation. use ImagingTransformAffine or ImagingScaleAffine where possible. */ @@ -790,15 +784,15 @@ ImagingGenericTransform( char *out; double xx, yy; + if (!imOut || !imIn || imIn->mode != imOut->mode) { + return (Imaging)ImagingError_ModeError(); + } + ImagingTransformFilter filter = getfilter(imIn, filterid); if (!filter) { return (Imaging)ImagingError_ValueError("bad filter number"); } - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { - return (Imaging)ImagingError_ModeError(); - } - ImagingCopyPalette(imOut, imIn); ImagingSectionEnter(&cookie); @@ -836,14 +830,8 @@ ImagingGenericTransform( static Imaging ImagingScaleAffine( - Imaging imOut, - Imaging imIn, - int x0, - int y0, - int x1, - int y1, - double a[6], - int fill) { + Imaging imOut, Imaging imIn, int x0, int y0, int x1, int y1, double a[6], int fill +) { /* scale, nearest neighbour resampling */ ImagingSectionCookie cookie; @@ -853,7 +841,7 @@ ImagingScaleAffine( int xmin, xmax; int *xintab; - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { + if (!imOut || !imIn || imIn->mode != imOut->mode) { return (Imaging)ImagingError_ModeError(); } @@ -936,7 +924,8 @@ 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); + fabs(x * a[3] + y * a[4] + a[5]) < 32768.0 + ); } static inline Imaging @@ -949,7 +938,8 @@ affine_fixed( int y1, double a[6], int filterid, - int fill) { + int fill +) { /* affine transform, nearest neighbour resampling, fixed point arithmetics */ @@ -1026,7 +1016,8 @@ ImagingTransformAffine( int y1, double a[6], int filterid, - int fill) { + int fill +) { /* affine transform, nearest neighbour resampling, floating point arithmetics*/ @@ -1037,9 +1028,14 @@ ImagingTransformAffine( double xx, yy; double xo, yo; + if (!imOut || !imIn || imIn->mode != imOut->mode) { + return (Imaging)ImagingError_ModeError(); + } + 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) { @@ -1047,10 +1043,6 @@ ImagingTransformAffine( return ImagingScaleAffine(imOut, imIn, x0, y0, x1, y1, a, fill); } - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { - return (Imaging)ImagingError_ModeError(); - } - if (x0 < 0) { x0 = 0; } @@ -1134,13 +1126,15 @@ ImagingTransform( int y1, double a[8], int filterid, - int fill) { + int fill +) { ImagingTransformMap transform; switch (method) { case IMAGING_TRANSFORM_AFFINE: return ImagingTransformAffine( - imOut, imIn, x0, y0, x1, y1, a, filterid, fill); + imOut, imIn, x0, y0, x1, y1, a, filterid, fill + ); break; case IMAGING_TRANSFORM_PERSPECTIVE: transform = perspective_transform; @@ -1153,5 +1147,6 @@ ImagingTransform( } 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 bd2a2778cdb..7a57f6894a0 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -30,26 +30,57 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { 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) \ + /* first stage: looking for any pixels from top */ \ + 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; \ + bbox[0] = x; \ + bbox[1] = y; \ + break; \ + } \ + } \ + if (has_data) { \ + break; \ + } \ + } \ + /* Check that we have a box */ \ + if (bbox[1] < 0) { \ + return 0; /* no data */ \ + } \ + /* second stage: looking for any pixels from bottom */ \ + for (y = im->ysize - 1; y >= bbox[1]; 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; \ + } \ + bbox[3] = y + 1; \ + break; \ + } \ + } \ + if (has_data) { \ + break; \ + } \ + } \ + /* third stage: looking for left and right boundaries */ \ + for (y = bbox[1]; y < bbox[3]; y++) { \ + for (x = 0; x < bbox[0]; x++) { \ + if (im->image[y][x] & mask) { \ + bbox[0] = x; \ + break; \ + } \ + } \ + for (x = im->xsize - 1; x >= bbox[2]; x--) { \ + if (im->image[y][x] & mask) { \ + bbox[2] = x + 1; \ + break; \ + } \ + } \ } if (im->image8) { @@ -60,9 +91,10 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { ((UINT8 *)&mask)[3] = 0; } else if ( alpha_only && - (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)) { + (im->mode == IMAGING_MODE_RGBa || im->mode == IMAGING_MODE_RGBA || + im->mode == IMAGING_MODE_La || im->mode == IMAGING_MODE_LA || + im->mode == IMAGING_MODE_PA) + ) { #ifdef WORDS_BIGENDIAN mask = 0x000000ff; #else @@ -72,11 +104,6 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { GETBBOX(image32, mask); } - /* Check that we got a box */ - if (bbox[1] < 0) { - return 0; /* no data */ - } - return 1; /* ok */ } @@ -145,6 +172,9 @@ ImagingGetExtrema(Imaging im, void *extrema) { imax = in[x]; } } + if (imin == 0 && imax == 255) { + break; + } } ((UINT8 *)extrema)[0] = (UINT8)imin; ((UINT8 *)extrema)[1] = (UINT8)imax; @@ -180,11 +210,11 @@ ImagingGetExtrema(Imaging im, void *extrema) { memcpy(((char *)extrema) + sizeof(fmin), &fmax, sizeof(fmax)); break; case IMAGING_TYPE_SPECIAL: - if (strcmp(im->mode, "I;16") == 0) { + if (im->mode == IMAGING_MODE_I_16) { UINT16 v; UINT8 *pixel = *im->image8; #ifdef WORDS_BIGENDIAN - v = pixel[0] + (pixel[1] << 8); + v = pixel[0] + ((UINT16)pixel[1] << 8); #else memcpy(&v, pixel, sizeof(v)); #endif @@ -193,7 +223,7 @@ ImagingGetExtrema(Imaging im, void *extrema) { for (x = 0; x < im->xsize; x++) { pixel = (UINT8 *)im->image[y] + x * sizeof(v); #ifdef WORDS_BIGENDIAN - v = pixel[0] + (pixel[1] << 8); + v = pixel[0] + ((UINT16)pixel[1] << 8); #else memcpy(&v, pixel, sizeof(v)); #endif @@ -246,13 +276,14 @@ getcolors32(Imaging im, int maxcolors, int *size) { code in Python 2.1.3; the exact implementation is borrowed from 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}; + 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}; code_size = code_poly = code_mask = 0; diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c index 45b67616d47..203fb9d0af0 100644 --- a/src/libImaging/GifEncode.c +++ b/src/libImaging/GifEncode.c @@ -79,7 +79,8 @@ glzwe( UINT8 *out_ptr, UINT32 *in_avail, UINT32 *out_avail, - UINT32 end_of_data) { + UINT32 end_of_data +) { switch (st->entry_state) { case LZW_TRY_IN1: get_first_byte: @@ -312,7 +313,8 @@ ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { state->buffer, (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); + state->xsize + ); state->x = 0; /* step forward, according to the interlace settings */ @@ -348,7 +350,8 @@ ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { ptr, &in_avail, &out_avail, - state->state == FINISH); + state->state == FINISH + ); out_used = sub_block_limit - ptr - out_avail; *sub_block_ptr += out_used; ptr += out_used; diff --git a/src/libImaging/HexDecode.c b/src/libImaging/HexDecode.c index bd16cdbe1da..e26c0e9b348 100644 --- a/src/libImaging/HexDecode.c +++ b/src/libImaging/HexDecode.c @@ -49,7 +49,8 @@ ImagingHexDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt if (++state->x >= state->bytes) { /* Got a full line, unpack it */ state->shuffle( - (UINT8 *)im->image[state->y], state->buffer, state->xsize); + (UINT8 *)im->image[state->y], state->buffer, state->xsize + ); state->x = 0; diff --git a/src/libImaging/Histo.c b/src/libImaging/Histo.c index c5a547a647b..7af60003511 100644 --- a/src/libImaging/Histo.c +++ b/src/libImaging/Histo.c @@ -43,10 +43,10 @@ ImagingHistogramNew(Imaging im) { if (!h) { return (ImagingHistogram)ImagingError_MemoryError(); } - strncpy(h->mode, im->mode, IMAGING_MODE_LENGTH - 1); - h->mode[IMAGING_MODE_LENGTH - 1] = 0; + h->mode = im->mode; h->bands = im->bands; + h->histogram = calloc(im->pixelsize, 256 * sizeof(long)); if (!h->histogram) { free(h); @@ -73,7 +73,7 @@ ImagingGetHistogram(Imaging im, Imaging imMask, void *minmax) { if (im->xsize != imMask->xsize || im->ysize != imMask->ysize) { return ImagingError_Mismatch(); } - if (strcmp(imMask->mode, "1") != 0 && strcmp(imMask->mode, "L") != 0) { + if (imMask->mode != IMAGING_MODE_1 && imMask->mode != IMAGING_MODE_L) { return ImagingError_ValueError("bad transparency mask"); } } @@ -132,11 +132,15 @@ ImagingGetHistogram(Imaging im, Imaging imMask, void *minmax) { 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]++; + for (x = 0; x < im->xsize; x++, in += 4) { + h->histogram[*in]++; + if (im->bands == 2) { + h->histogram[*(in + 3) + 256]++; + } else { + h->histogram[*(in + 1) + 256]++; + h->histogram[*(in + 2) + 512]++; + h->histogram[*(in + 3) + 768]++; + } } } ImagingSectionLeave(&cookie); diff --git a/src/libImaging/ImDib.h b/src/libImaging/ImDib.h index 91ff3f322ff..65f090f928a 100644 --- a/src/libImaging/ImDib.h +++ b/src/libImaging/ImDib.h @@ -27,7 +27,7 @@ struct ImagingDIBInstance { UINT8 *bits; HPALETTE palette; /* Used by cut and paste */ - char mode[4]; + ModeID mode; int xsize, ysize; int pixelsize; int linesize; @@ -37,11 +37,11 @@ struct ImagingDIBInstance { typedef struct ImagingDIBInstance *ImagingDIB; -extern char * +extern ModeID ImagingGetModeDIB(int size_out[2]); extern ImagingDIB -ImagingNewDIB(const char *mode, int xsize, int ysize); +ImagingNewDIB(ModeID mode, int xsize, int ysize); extern void ImagingDeleteDIB(ImagingDIB im); diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index f6e7fb6b921..2ce282241d5 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -7,6 +7,7 @@ * Copyright (c) Fredrik Lundh 1995-2003. */ +#define PY_SSIZE_T_CLEAN #include "Python.h" /* Check that we have an ANSI compliant compiler */ @@ -43,8 +44,6 @@ defines their own types with the same names, so we need to be able to undef ours before including the JPEG code. */ -#if __STDC_VERSION__ >= 199901L /* C99+ */ - #include #define INT8 int8_t @@ -54,34 +53,6 @@ #define INT32 int32_t #define UINT32 uint32_t -#else /* < C99 */ - -#define INT8 signed char - -#if SIZEOF_SHORT == 2 -#define INT16 short -#elif SIZEOF_INT == 2 -#define INT16 int -#else -#error Cannot find required 16-bit integer type -#endif - -#if SIZEOF_SHORT == 4 -#define INT32 short -#elif SIZEOF_INT == 4 -#define INT32 int -#elif SIZEOF_LONG == 4 -#define INT32 long -#else -#error Cannot find required 32-bit integer type -#endif - -#define UINT8 unsigned char -#define UINT16 unsigned INT16 -#define UINT32 unsigned INT32 - -#endif /* < C99 */ - #endif /* not WIN */ /* assume IEEE; tweak if necessary (patches are welcome) */ diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 1f2c03e934f..f7049c8925c 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -11,6 +11,7 @@ */ #include "ImPlatform.h" +#include "Mode.h" #if defined(__cplusplus) extern "C" { @@ -20,6 +21,8 @@ extern "C" { #define M_PI 3.1415926535897932384626433832795 #endif +#include "Arrow.h" + /* -------------------------------------------------------------------- */ /* @@ -60,8 +63,8 @@ typedef struct ImagingHistogramInstance *ImagingHistogram; typedef struct ImagingOutlineInstance *ImagingOutline; typedef struct ImagingPaletteInstance *ImagingPalette; -/* handle magics (used with PyCObject). */ -#define IMAGING_MAGIC "PIL Imaging" +/* handle magics (used with PyCapsule). */ +#define IMAGING_MAGIC "Pillow Imaging" /* pixel types */ #define IMAGING_TYPE_UINT8 0 @@ -69,9 +72,6 @@ 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") */ - typedef struct { char *ptr; int size; @@ -79,12 +79,11 @@ typedef struct { 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. */ + ModeID mode; /* Image mode (IMAGING_MODE_*) */ + 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) */ @@ -104,6 +103,21 @@ struct ImagingMemoryInstance { /* Virtual methods */ void (*destroy)(Imaging im); + + /* arrow */ + int refcount; /* Number of arrow arrays that have been allocated */ + char band_names[4][3]; /* names of bands, max 2 char + null terminator */ + char arrow_band_format[2]; /* single character + null terminator */ + + int read_only; /* flag for read-only. set for arrow borrowed arrays */ + PyObject *arrow_array_capsule; /* upstream arrow array source */ + + int blocks_count; /* Number of blocks that have been allocated */ + int lines_per_block; /* Number of lines in a block have been allocated */ + +#ifdef Py_GIL_DISABLED + PyMutex mutex; +#endif }; #define IMAGING_PIXEL_1(im, x, y) ((im)->image8[(y)][(x)]) @@ -123,15 +137,15 @@ struct ImagingMemoryInstance { #define IMAGING_PIXEL_FLOAT32(im, x, y) (((FLOAT32 *)(im)->image32[y])[x]) struct ImagingAccessInstance { - const char *mode; + ModeID mode; 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) */ + ModeID mode; /* Mode ID of corresponding source image */ + int bands; /* Number of bands (1, 2, 3, or 4) */ /* Data */ long *histogram; /* Histogram (bands*256 longs) */ @@ -139,7 +153,7 @@ struct ImagingHistogramInstance { struct ImagingPaletteInstance { /* Format */ - char mode[IMAGING_MODE_LENGTH]; /* Band names */ + ModeID mode; /* Data */ int size; @@ -161,6 +175,10 @@ typedef struct ImagingMemoryArena { int stats_reallocated_blocks; /* Number of blocks which were actually reallocated after retrieving */ int stats_freed_blocks; /* Number of freed blocks */ + int use_block_allocator; /* don't use arena, use block allocator */ +#ifdef Py_GIL_DISABLED + PyMutex mutex; +#endif } *ImagingMemoryArena; /* Objects */ @@ -171,23 +189,34 @@ extern int ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max); extern void ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size); +extern void +ImagingMemorySetBlockAllocator(ImagingMemoryArena arena, int use_block_allocator); extern Imaging -ImagingNew(const char *mode, int xsize, int ysize); +ImagingNew(ModeID mode, int xsize, int ysize); extern Imaging -ImagingNewDirty(const char *mode, int xsize, int ysize); +ImagingNewDirty(ModeID mode, int xsize, int ysize); extern Imaging -ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn); +ImagingNew2Dirty(ModeID mode, Imaging imOut, Imaging imIn); extern void ImagingDelete(Imaging im); extern Imaging -ImagingNewBlock(const char *mode, int xsize, int ysize); +ImagingNewBlock(ModeID mode, int xsize, int ysize); extern Imaging -ImagingNewPrologue(const char *mode, int xsize, int ysize); +ImagingNewArrow( + const ModeID mode, + int xsize, + int ysize, + PyObject *schema_capsule, + PyObject *array_capsule +); + +extern Imaging +ImagingNewPrologue(ModeID mode, int xsize, int ysize); extern Imaging -ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int structure_size); +ImagingNewPrologueSubtype(ModeID mode, int xsize, int ysize, int structure_size); extern void ImagingCopyPalette(Imaging destination, Imaging source); @@ -195,8 +224,6 @@ ImagingCopyPalette(Imaging destination, Imaging source); extern void ImagingHistogramDelete(ImagingHistogram histogram); -extern void -ImagingAccessInit(void); extern ImagingAccess ImagingAccessNew(Imaging im); extern void @@ -204,7 +231,7 @@ _ImagingAccessDelete(Imaging im, ImagingAccess access); #define ImagingAccessDelete(im, access) /* nop, for now */ extern ImagingPalette -ImagingPaletteNew(const char *mode); +ImagingPaletteNew(ModeID mode); extern ImagingPalette ImagingPaletteNewBrowser(void); extern ImagingPalette @@ -238,8 +265,6 @@ ImagingSectionLeave(ImagingSectionCookie *cookie); /* Exceptions */ /* ---------- */ -extern void * -ImagingError_OSError(void); extern void * ImagingError_MemoryError(void); extern void * @@ -248,8 +273,6 @@ extern void * ImagingError_Mismatch(void); /* maps to ValueError by default */ extern void * ImagingError_ValueError(const char *message); -extern void -ImagingError_Clear(void); /* Transform callbacks */ /* ------------------- */ @@ -280,13 +303,13 @@ ImagingBlend(Imaging imIn1, Imaging imIn2, float alpha); extern Imaging ImagingCopy(Imaging im); extern Imaging -ImagingConvert(Imaging im, const char *mode, ImagingPalette palette, int dither); +ImagingConvert(Imaging im, ModeID mode, ImagingPalette palette, int dither); extern Imaging -ImagingConvertInPlace(Imaging im, const char *mode); +ImagingConvertInPlace(Imaging im, ModeID mode); extern Imaging -ImagingConvertMatrix(Imaging im, const char *mode, float m[]); +ImagingConvertMatrix(Imaging im, ModeID mode, float m[]); extern Imaging -ImagingConvertTransparent(Imaging im, const char *mode, int r, int g, int b); +ImagingConvertTransparent(Imaging im, ModeID mode, int r, int g, int b); extern Imaging ImagingCrop(Imaging im, int x0, int y0, int x1, int y1); extern Imaging @@ -295,13 +318,14 @@ 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); + 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); +ImagingFillLinearGradient(ModeID mode); extern Imaging -ImagingFillRadialGradient(const char *mode); +ImagingFillRadialGradient(ModeID mode); extern Imaging ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 offset); extern Imaging @@ -310,11 +334,12 @@ extern Imaging ImagingFlipTopBottom(Imaging imOut, Imaging imIn); extern Imaging ImagingGaussianBlur( - Imaging imOut, Imaging imIn, float xradius, float yradius, int passes); + Imaging imOut, Imaging imIn, float xradius, float yradius, int passes +); extern Imaging ImagingGetBand(Imaging im, int band); extern Imaging -ImagingMerge(const char *mode, Imaging bands[4]); +ImagingMerge(ModeID mode, Imaging bands[4]); extern int ImagingSplit(Imaging im, Imaging bands[4]); extern int @@ -341,7 +366,7 @@ 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); +ImagingPoint(Imaging im, ModeID tablemode, const void *table); extern Imaging ImagingPointTransform(Imaging imIn, double scale, double offset); extern Imaging @@ -373,7 +398,8 @@ ImagingTransform( int y1, double a[8], int filter, - int fill); + int fill +); extern Imaging ImagingUnsharpMask(Imaging imOut, Imaging im, float radius, int percent, int threshold); extern Imaging @@ -386,7 +412,8 @@ ImagingColorLUT3D_linear( int size1D, int size2D, int size3D, - INT16 *table); + INT16 *table +); extern Imaging ImagingCopy2(Imaging imOut, Imaging imIn); @@ -440,7 +467,8 @@ ImagingDrawArc( float end, const void *ink, int width, - int op); + int op +); extern int ImagingDrawBitmap(Imaging im, int x0, int y0, Imaging bitmap, const void *ink, int op); extern int @@ -455,7 +483,8 @@ ImagingDrawChord( const void *ink, int fill, int width, - int op); + int op +); extern int ImagingDrawEllipse( Imaging im, @@ -466,12 +495,22 @@ ImagingDrawEllipse( const void *ink, int fill, int width, - int op); + 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); + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink, + int width, + int op, + Imaging mask +); extern int ImagingDrawPieslice( Imaging im, @@ -484,12 +523,21 @@ ImagingDrawPieslice( const void *ink, int fill, int width, - int op); + 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); + Imaging im, + int points, + int *xy, + const void *ink, + int fill, + int width, + int op, + Imaging mask +); extern int ImagingDrawRectangle( Imaging im, @@ -500,7 +548,8 @@ ImagingDrawRectangle( const void *ink, int fill, int width, - int op); + int op +); /* Level 2 graphics (WORK IN PROGRESS) */ extern ImagingOutline @@ -510,7 +559,8 @@ ImagingOutlineDelete(ImagingOutline outline); extern int ImagingDrawOutline( - Imaging im, ImagingOutline outline, const void *ink, int fill, int op); + Imaging im, ImagingOutline outline, const void *ink, int fill, int op +); extern int ImagingOutlineMove(ImagingOutline outline, float x, float y); @@ -518,7 +568,8 @@ 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); + ImagingOutline outline, float x1, float y1, float x2, float y2, float x3, float y3 +); extern int ImagingOutlineTransform(ImagingOutline outline, double a[6]); @@ -545,11 +596,14 @@ ImagingSavePPM(Imaging im, const char *filename); /* Codecs */ typedef struct ImagingCodecStateInstance *ImagingCodecState; typedef int (*ImagingCodec)( - Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); + Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes +); extern int ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); extern int +ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int 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); @@ -575,7 +629,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes) #ifdef HAVE_OPENJPEG extern int ImagingJpeg2KDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes +); extern int ImagingJpeg2KDecodeCleanup(ImagingCodecState state); extern int @@ -586,19 +641,17 @@ ImagingJpeg2KEncodeCleanup(ImagingCodecState state); #ifdef HAVE_LIBTIFF extern int ImagingLibTiffDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); + 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, Py_ssize_t bytes); -#endif 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); + 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 @@ -611,13 +664,16 @@ extern int ImagingRawEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); extern int ImagingSgiRleDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes +); extern int ImagingSunRleDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes +); extern int ImagingTgaRleDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes +); extern int ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); extern int @@ -651,9 +707,9 @@ extern void ImagingConvertYCbCr2RGB(UINT8 *out, const UINT8 *in, int pixels); extern ImagingShuffler -ImagingFindUnpacker(const char *mode, const char *rawmode, int *bits_out); +ImagingFindUnpacker(ModeID mode, RawModeID rawmode, int *bits_out); extern ImagingShuffler -ImagingFindPacker(const char *mode, const char *rawmode, int *bits_out); +ImagingFindPacker(ModeID mode, RawModeID rawmode, int *bits_out); struct ImagingCodecStateInstance { int count; @@ -679,6 +735,13 @@ _imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence); extern Py_ssize_t _imaging_tell_pyFd(PyObject *fd); +/* Arrow */ + +extern int +export_imaging_array(Imaging im, struct ArrowArray *array); +extern int +export_imaging_schema(Imaging im, struct ArrowSchema *schema); + /* Errcodes */ #define IMAGING_CODEC_END 1 #define IMAGING_CODEC_OVERRUN -1 @@ -686,10 +749,21 @@ _imaging_tell_pyFd(PyObject *fd); #define IMAGING_CODEC_UNKNOWN -3 #define IMAGING_CODEC_CONFIG -8 #define IMAGING_CODEC_MEMORY -9 +#define IMAGING_ARROW_INCOMPATIBLE_MODE -10 +#define IMAGING_ARROW_MEMORY_LAYOUT -11 #include "ImagingUtils.h" extern UINT8 *clip8_lookups; +/* Mutex lock/unlock helpers */ +#ifdef Py_GIL_DISABLED +#define MUTEX_LOCK(m) PyMutex_Lock(m) +#define MUTEX_UNLOCK(m) PyMutex_Unlock(m) +#else +#define MUTEX_LOCK(m) +#define MUTEX_UNLOCK(m) +#endif + #if defined(__cplusplus) } #endif diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index 7cdba902281..e07904fc70c 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -28,12 +28,12 @@ typedef struct { typedef struct { /* CONFIGURATION */ - /* Jpeg file mode (empty if not known) */ - char jpegmode[8 + 1]; + /* Jpeg file mode */ + RawModeID jpegmode; - /* Converter output mode (input to the shuffler). If empty, - convert conversions are disabled */ - char rawmode[8 + 1]; + /* Converter output mode (input to the shuffler) */ + /* If not a valid mode, convert conversions are disabled */ + RawModeID rawmode; /* If set, trade quality for speed */ int draft; @@ -91,7 +91,7 @@ typedef struct { unsigned int restart_marker_rows; /* Converter input mode (input to the shuffler) */ - char rawmode[8 + 1]; + RawModeID rawmode; /* Custom quantization tables () */ unsigned int *qtables; diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index e8d92f7b6bc..6fbbf7311a1 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -104,10 +104,3 @@ typedef struct { int plt; } JPEG2KENCODESTATE; - -/* - * Local Variables: - * c-basic-offset: 4 - * End: - * - */ diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index dd066c10b7a..1123d7bc915 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -67,10 +67,11 @@ j2k_skip(OPJ_OFF_T p_nb_bytes, void *p_user_data) { /* -------------------------------------------------------------------- */ typedef void (*j2k_unpacker_t)( - opj_image_t *in, const JPEG2KTILEINFO *tileInfo, const UINT8 *data, Imaging im); + opj_image_t *in, const JPEG2KTILEINFO *tileInfo, const UINT8 *data, Imaging im +); struct j2k_decode_unpacker { - const char *mode; + const ModeID mode; OPJ_COLOR_SPACE color_space; unsigned components; /* bool indicating if unpacker supports subsampling */ @@ -89,10 +90,8 @@ j2ku_shift(unsigned x, int n) { static void j2ku_gray_l( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { + 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; @@ -145,10 +144,8 @@ j2ku_gray_l( static void j2ku_gray_i( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { + 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; @@ -204,10 +201,8 @@ j2ku_gray_i( static void j2ku_gray_rgb( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { + 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; @@ -268,10 +263,8 @@ j2ku_gray_rgb( static void j2ku_graya_la( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { + 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; @@ -347,10 +340,8 @@ j2ku_graya_la( static void j2ku_srgb_rgb( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { + 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; @@ -413,10 +404,8 @@ j2ku_srgb_rgb( static void j2ku_sycc_rgb( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { + 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; @@ -482,10 +471,8 @@ j2ku_sycc_rgb( static void j2ku_srgba_rgba( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { + 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; @@ -547,10 +534,8 @@ j2ku_srgba_rgba( static void j2ku_sycca_rgba( - opj_image_t *in, - const JPEG2KTILEINFO *tileinfo, - const UINT8 *tiledata, - Imaging im) { + 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; @@ -614,25 +599,27 @@ j2ku_sycca_rgba( } static const struct j2k_decode_unpacker j2k_unpackers[] = { - {"L", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_l}, - {"P", OPJ_CLRSPC_SRGB, 1, 0, j2ku_gray_l}, - {"PA", OPJ_CLRSPC_SRGB, 2, 0, j2ku_graya_la}, - {"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}, - {"CMYK", OPJ_CLRSPC_CMYK, 4, 1, j2ku_srgba_rgba}, + {IMAGING_MODE_L, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_l}, + {IMAGING_MODE_P, OPJ_CLRSPC_SRGB, 1, 0, j2ku_gray_l}, + {IMAGING_MODE_P, OPJ_CLRSPC_CMYK, 1, 0, j2ku_gray_l}, + {IMAGING_MODE_PA, OPJ_CLRSPC_SRGB, 2, 0, j2ku_graya_la}, + {IMAGING_MODE_I_16, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, + {IMAGING_MODE_I_16B, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, + {IMAGING_MODE_LA, OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la}, + {IMAGING_MODE_RGB, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb}, + {IMAGING_MODE_RGB, OPJ_CLRSPC_GRAY, 2, 0, j2ku_gray_rgb}, + {IMAGING_MODE_RGB, OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb}, + {IMAGING_MODE_RGB, OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, + {IMAGING_MODE_RGB, OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgb_rgb}, + {IMAGING_MODE_RGB, OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycc_rgb}, + {IMAGING_MODE_RGBA, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb}, + {IMAGING_MODE_RGBA, OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la}, + {IMAGING_MODE_RGBA, OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb}, + {IMAGING_MODE_RGBA, OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, + {IMAGING_MODE_RGBA, OPJ_CLRSPC_GRAY, 4, 1, j2ku_srgba_rgba}, + {IMAGING_MODE_RGBA, OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgba_rgba}, + {IMAGING_MODE_RGBA, OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycca_rgba}, + {IMAGING_MODE_CMYK, OPJ_CLRSPC_CMYK, 4, 1, j2ku_srgba_rgba}, }; /* -------------------------------------------------------------------- */ @@ -655,7 +642,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { opj_dparameters_t params; OPJ_COLOR_SPACE color_space; j2k_unpacker_t unpack = NULL; - size_t buffer_size = 0, tile_bytes = 0; + size_t tile_bytes = 0; unsigned n, tile_height, tile_width; int subsampling; int total_component_width = 0; @@ -713,8 +700,7 @@ 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) { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; goto quick_exit; @@ -759,7 +745,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { /* Find the correct unpacker */ color_space = image->color_space; - if (color_space == OPJ_CLRSPC_UNSPECIFIED) { + if (color_space == OPJ_CLRSPC_UNKNOWN || color_space == OPJ_CLRSPC_UNSPECIFIED) { switch (image->numcomps) { case 1: case 2: @@ -786,7 +772,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { 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) { + im->mode == j2k_unpackers[n].mode) { unpack = j2k_unpackers[n].unpacker; break; } @@ -815,7 +801,8 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { &tile_info.x1, &tile_info.y1, &tile_info.nb_comps, - &should_continue)) { + &should_continue + )) { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; goto quick_exit; @@ -885,7 +872,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { tile_info.data_size = tile_bytes; } - if (buffer_size < tile_info.data_size) { + if (tile_info.data_size > 0) { /* malloc check ok, overflow and tile size sanity check above */ UINT8 *new = realloc(state->buffer, tile_info.data_size); if (!new) { @@ -898,7 +885,6 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { to valgrind errors. */ memset(new, 0, tile_info.data_size); state->buffer = new; - buffer_size = tile_info.data_size; } if (!opj_decode_tile_data( @@ -906,7 +892,8 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { tile_info.tile_index, (OPJ_BYTE *)state->buffer, tile_info.data_size, - stream)) { + stream + )) { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; goto quick_exit; @@ -993,10 +980,3 @@ ImagingJpeg2KVersion(void) { } #endif /* HAVE_OPENJPEG */ - -/* - * Local Variables: - * c-basic-offset: 4 - * End: - * - */ diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 7f1aeaddb9b..3012783a2e4 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -89,7 +89,8 @@ j2k_seek(OPJ_OFF_T p_nb_bytes, void *p_user_data) { /* -------------------------------------------------------------------- */ typedef void (*j2k_pack_tile_t)( - Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h); + 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) { @@ -157,7 +158,8 @@ j2k_pack_rgb(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsig static void j2k_pack_rgba( - Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) { + 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; @@ -303,31 +305,40 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { #endif /* Setup an opj_image */ - if (strcmp(im->mode, "L") == 0) { + if (im->mode == IMAGING_MODE_L) { components = 1; color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_l; - } else if (strcmp(im->mode, "I;16") == 0 || strcmp(im->mode, "I;16B") == 0) { + } else if (im->mode == IMAGING_MODE_I_16 || im->mode == IMAGING_MODE_I_16B) { components = 1; color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_i16; prec = 16; - } else if (strcmp(im->mode, "LA") == 0) { + } else if (im->mode == IMAGING_MODE_LA) { components = 2; color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_la; - } else if (strcmp(im->mode, "RGB") == 0) { + } else if (im->mode == IMAGING_MODE_RGB) { components = 3; color_space = OPJ_CLRSPC_SRGB; pack = j2k_pack_rgb; - } else if (strcmp(im->mode, "YCbCr") == 0) { + } else if (im->mode == IMAGING_MODE_YCbCr) { components = 3; color_space = OPJ_CLRSPC_SYCC; pack = j2k_pack_rgb; - } else if (strcmp(im->mode, "RGBA") == 0) { + } else if (im->mode == IMAGING_MODE_RGBA) { components = 4; color_space = OPJ_CLRSPC_SRGB; pack = j2k_pack_rgba; +#if ( \ + (OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR == 5 && OPJ_VERSION_BUILD >= 3) || \ + (OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR > 5) || OPJ_VERSION_MAJOR > 2 \ +) + } else if (im->mode == IMAGING_MODE_CMYK) { + components = 4; + color_space = OPJ_CLRSPC_CMYK; + pack = j2k_pack_rgba; +#endif } else { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; @@ -488,9 +499,9 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { goto quick_exit; } - if (strcmp(im->mode, "RGBA") == 0) { + if (im->mode == IMAGING_MODE_RGBA) { image->comps[3].alpha = 1; - } else if (strcmp(im->mode, "LA") == 0) { + } else if (im->mode == IMAGING_MODE_LA) { image->comps[1].alpha = 1; } @@ -650,10 +661,3 @@ ImagingJpeg2KEncodeCleanup(ImagingCodecState state) { } #endif /* HAVE_OPENJPEG */ - -/* - * Local Variables: - * c-basic-offset: 4 - * End: - * - */ diff --git a/src/libImaging/JpegDecode.c b/src/libImaging/JpegDecode.c index 6f75d867068..05cb37554b3 100644 --- a/src/libImaging/JpegDecode.c +++ b/src/libImaging/JpegDecode.c @@ -48,7 +48,7 @@ char *libjpeg_turbo_version = NULL; #endif int -ImagingJpegUseJCSExtensions() { +ImagingJpegUseJCSExtensions(void) { int use_jcs_extensions = 0; #ifdef JCS_EXTENSIONS #if defined(LIBJPEG_TURBO_VERSION_NUMBER) @@ -180,42 +180,43 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t by /* Decoder settings */ - /* jpegmode indicates what's in the file; if not set, we'll - trust the decoder */ - if (strcmp(context->jpegmode, "L") == 0) { + /* jpegmode indicates what's in the file. */ + /* If not valid, we'll trust the decoder. */ + if (context->jpegmode == IMAGING_RAWMODE_L) { context->cinfo.jpeg_color_space = JCS_GRAYSCALE; - } else if (strcmp(context->jpegmode, "RGB") == 0) { + } else if (context->jpegmode == IMAGING_RAWMODE_RGB) { context->cinfo.jpeg_color_space = JCS_RGB; - } else if (strcmp(context->jpegmode, "CMYK") == 0) { + } else if (context->jpegmode == IMAGING_RAWMODE_CMYK) { context->cinfo.jpeg_color_space = JCS_CMYK; - } else if (strcmp(context->jpegmode, "YCbCr") == 0) { + } else if (context->jpegmode == IMAGING_RAWMODE_YCbCr) { context->cinfo.jpeg_color_space = JCS_YCbCr; - } else if (strcmp(context->jpegmode, "YCbCrK") == 0) { + } else if (context->jpegmode == IMAGING_RAWMODE_YCbCrK) { 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) { + /* rawmode indicates what we want from the decoder. */ + /* If not valid, conversions are disabled. */ + if (context->rawmode == IMAGING_RAWMODE_L) { context->cinfo.out_color_space = JCS_GRAYSCALE; - } else if (strcmp(context->rawmode, "RGB") == 0) { + } else if (context->rawmode == IMAGING_RAWMODE_RGB) { context->cinfo.out_color_space = JCS_RGB; } #ifdef JCS_EXTENSIONS - else if (strcmp(context->rawmode, "RGBX") == 0) { + else if (context->rawmode == IMAGING_RAWMODE_RGBX) { context->cinfo.out_color_space = JCS_EXT_RGBX; } #endif else if ( - strcmp(context->rawmode, "CMYK") == 0 || - strcmp(context->rawmode, "CMYK;I") == 0) { + context->rawmode == IMAGING_RAWMODE_CMYK || + context->rawmode == IMAGING_RAWMODE_CMYK_I + ) { context->cinfo.out_color_space = JCS_CMYK; - } else if (strcmp(context->rawmode, "YCbCr") == 0) { + } else if (context->rawmode == IMAGING_RAWMODE_YCbCr) { context->cinfo.out_color_space = JCS_YCbCr; - } else if (strcmp(context->rawmode, "YCbCrK") == 0) { + } else if (context->rawmode == IMAGING_RAWMODE_YCbCrK) { context->cinfo.out_color_space = JCS_YCCK; } else { - /* Disable decoder conversions */ + /* Disable decoder conversions. */ context->cinfo.jpeg_color_space = JCS_UNKNOWN; context->cinfo.out_color_space = JCS_UNKNOWN; } @@ -256,7 +257,8 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t by (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, state->buffer, - state->xsize); + state->xsize + ); state->y++; } if (ok != 1) { diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index ba8353c2d86..098e431fca0 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -114,7 +114,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { break; case 24: context->cinfo.input_components = 3; - if (strcmp(im->mode, "YCbCr") == 0) { + if (im->mode == IMAGING_MODE_YCbCr) { context->cinfo.in_color_space = JCS_YCbCr; } else { context->cinfo.in_color_space = JCS_RGB; @@ -124,17 +124,27 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { context->cinfo.input_components = 4; context->cinfo.in_color_space = JCS_CMYK; #ifdef JCS_EXTENSIONS - if (strcmp(context->rawmode, "RGBX") == 0) { + if (context->rawmode == IMAGING_RAWMODE_RGBX) { context->cinfo.in_color_space = JCS_EXT_RGBX; } #endif break; default: state->errcode = IMAGING_CODEC_CONFIG; + jpeg_destroy_compress(&context->cinfo); return -1; } - /* Compressor configuration */ + /* Compressor configuration */ +#ifdef JPEG_C_PARAM_SUPPORTED + /* MozJPEG */ + if (!context->progressive) { + /* Do not use MozJPEG progressive default */ + jpeg_c_set_int_param( + &context->cinfo, JINT_COMPRESS_PROFILE, JCP_FASTEST + ); + } +#endif jpeg_set_defaults(&context->cinfo); /* Prevent RGB -> YCbCr conversion */ @@ -152,6 +162,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { /* Would subsample the green and blue channels, which doesn't make sense */ state->errcode = IMAGING_CODEC_CONFIG; + jpeg_destroy_compress(&context->cinfo); return -1; } jpeg_set_colorspace(&context->cinfo, JCS_RGB); @@ -164,18 +175,22 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { /* Use custom quantization tables */ if (context->qtables) { int i; - int quality = 100; + int quality = 50; int last_q = 0; + boolean force_baseline = FALSE; if (context->quality != -1) { quality = context->quality; + force_baseline = TRUE; } + int scale_factor = jpeg_quality_scaling(quality); for (i = 0; i < context->qtablesLen; i++) { jpeg_add_quant_table( &context->cinfo, i, &context->qtables[i * DCTSIZE2], - quality, - FALSE); + scale_factor, + force_baseline + ); context->cinfo.comp_info[i].quant_tbl_no = i; last_q = i; } @@ -183,7 +198,12 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { // jpeg_set_defaults created two qtables internally, but we only // wanted one. jpeg_add_quant_table( - &context->cinfo, 1, &context->qtables[0], quality, FALSE); + &context->cinfo, + 1, + &context->qtables[0], + scale_factor, + force_baseline + ); } for (i = last_q; i < context->cinfo.num_components; i++) { context->cinfo.comp_info[i].quant_tbl_no = last_q; @@ -273,7 +293,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { &context->cinfo, JPEG_APP0 + 1, (unsigned char *)context->rawExif, - context->rawExifLen); + context->rawExifLen + ); } state->state++; @@ -289,7 +310,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { memcpy( context->destination.pub.next_output_byte, context->extra + context->extra_offset, - n); + n + ); context->destination.pub.next_output_byte += n; context->destination.pub.free_in_buffer -= n; context->extra_offset += n; @@ -309,7 +331,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { &context->cinfo, JPEG_COM, (unsigned char *)context->comment, - context->comment_size); + context->comment_size + ); } state->state++; @@ -324,7 +347,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { state->buffer, (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); + state->xsize + ); ok = jpeg_write_scanlines(&context->cinfo, &state->buffer, 1); if (ok != 1) { break; diff --git a/src/libImaging/Matrix.c b/src/libImaging/Matrix.c index ec7f4d93e06..acd59ba7f39 100644 --- a/src/libImaging/Matrix.c +++ b/src/libImaging/Matrix.c @@ -18,7 +18,7 @@ #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 ModeID mode, float m[]) { Imaging imOut; int x, y; ImagingSectionCookie cookie; @@ -28,8 +28,8 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { return (Imaging)ImagingError_ModeError(); } - if (strcmp(mode, "L") == 0) { - imOut = ImagingNewDirty("L", im->xsize, im->ysize); + if (mode == IMAGING_MODE_L) { + imOut = ImagingNewDirty(IMAGING_MODE_L, im->xsize, im->ysize); if (!imOut) { return NULL; } @@ -46,8 +46,9 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { } } ImagingSectionLeave(&cookie); - - } else if (strlen(mode) == 3) { + } else if ( + mode == IMAGING_MODE_HSV || mode == IMAGING_MODE_LAB || mode == IMAGING_MODE_RGB + ) { imOut = ImagingNewDirty(mode, im->xsize, im->ysize); if (!imOut) { return NULL; diff --git a/src/libImaging/Mode.c b/src/libImaging/Mode.c new file mode 100644 index 00000000000..2e459c48fb4 --- /dev/null +++ b/src/libImaging/Mode.c @@ -0,0 +1,257 @@ +#include "Mode.h" +#include + +#ifdef NDEBUG +#include +#include +#endif + +const ModeData MODES[] = { + [IMAGING_MODE_UNKNOWN] = {""}, + + [IMAGING_MODE_1] = {"1"}, [IMAGING_MODE_CMYK] = {"CMYK"}, + [IMAGING_MODE_F] = {"F"}, [IMAGING_MODE_HSV] = {"HSV"}, + [IMAGING_MODE_I] = {"I"}, [IMAGING_MODE_L] = {"L"}, + [IMAGING_MODE_LA] = {"LA"}, [IMAGING_MODE_LAB] = {"LAB"}, + [IMAGING_MODE_La] = {"La"}, [IMAGING_MODE_P] = {"P"}, + [IMAGING_MODE_PA] = {"PA"}, [IMAGING_MODE_RGB] = {"RGB"}, + [IMAGING_MODE_RGBA] = {"RGBA"}, [IMAGING_MODE_RGBX] = {"RGBX"}, + [IMAGING_MODE_RGBa] = {"RGBa"}, [IMAGING_MODE_YCbCr] = {"YCbCr"}, + + [IMAGING_MODE_I_16] = {"I;16"}, [IMAGING_MODE_I_16L] = {"I;16L"}, + [IMAGING_MODE_I_16B] = {"I;16B"}, [IMAGING_MODE_I_16N] = {"I;16N"}, +}; + +const ModeID +findModeID(const char *const name) { + if (name == NULL) { + return IMAGING_MODE_UNKNOWN; + } + for (size_t i = 0; i < sizeof(MODES) / sizeof(*MODES); i++) { +#ifdef NDEBUG + if (MODES[i].name == NULL) { + fprintf(stderr, "Mode ID %zu is not defined.\n", (size_t)i); + } else +#endif + if (strcmp(MODES[i].name, name) == 0) { + return (ModeID)i; + } + } + return IMAGING_MODE_UNKNOWN; +} + +const ModeData *const +getModeData(const ModeID id) { + if (id < 0 || id > sizeof(MODES) / sizeof(*MODES)) { + return &MODES[IMAGING_MODE_UNKNOWN]; + } + return &MODES[id]; +} + +const RawModeData RAWMODES[] = { + [IMAGING_RAWMODE_UNKNOWN] = {""}, + + [IMAGING_RAWMODE_1] = {"1"}, + [IMAGING_RAWMODE_CMYK] = {"CMYK"}, + [IMAGING_RAWMODE_F] = {"F"}, + [IMAGING_RAWMODE_HSV] = {"HSV"}, + [IMAGING_RAWMODE_I] = {"I"}, + [IMAGING_RAWMODE_L] = {"L"}, + [IMAGING_RAWMODE_LA] = {"LA"}, + [IMAGING_RAWMODE_LAB] = {"LAB"}, + [IMAGING_RAWMODE_La] = {"La"}, + [IMAGING_RAWMODE_P] = {"P"}, + [IMAGING_RAWMODE_PA] = {"PA"}, + [IMAGING_RAWMODE_RGB] = {"RGB"}, + [IMAGING_RAWMODE_RGBA] = {"RGBA"}, + [IMAGING_RAWMODE_RGBX] = {"RGBX"}, + [IMAGING_RAWMODE_RGBa] = {"RGBa"}, + [IMAGING_RAWMODE_YCbCr] = {"YCbCr"}, + + [IMAGING_RAWMODE_BGR_15] = {"BGR;15"}, + [IMAGING_RAWMODE_BGR_16] = {"BGR;16"}, + + [IMAGING_RAWMODE_I_16] = {"I;16"}, + [IMAGING_RAWMODE_I_16L] = {"I;16L"}, + [IMAGING_RAWMODE_I_16B] = {"I;16B"}, + [IMAGING_RAWMODE_I_16N] = {"I;16N"}, + [IMAGING_RAWMODE_I_32B] = {"I;32B"}, + + [IMAGING_RAWMODE_1_8] = {"1;8"}, + [IMAGING_RAWMODE_1_I] = {"1;I"}, + [IMAGING_RAWMODE_1_IR] = {"1;IR"}, + [IMAGING_RAWMODE_1_R] = {"1;R"}, + [IMAGING_RAWMODE_A] = {"A"}, + [IMAGING_RAWMODE_ABGR] = {"ABGR"}, + [IMAGING_RAWMODE_ARGB] = {"ARGB"}, + [IMAGING_RAWMODE_A_16B] = {"A;16B"}, + [IMAGING_RAWMODE_A_16L] = {"A;16L"}, + [IMAGING_RAWMODE_A_16N] = {"A;16N"}, + [IMAGING_RAWMODE_B] = {"B"}, + [IMAGING_RAWMODE_BGAR] = {"BGAR"}, + [IMAGING_RAWMODE_BGR] = {"BGR"}, + [IMAGING_RAWMODE_BGRA] = {"BGRA"}, + [IMAGING_RAWMODE_BGRA_15] = {"BGRA;15"}, + [IMAGING_RAWMODE_BGRA_15Z] = {"BGRA;15Z"}, + [IMAGING_RAWMODE_BGRA_16B] = {"BGRA;16B"}, + [IMAGING_RAWMODE_BGRA_16L] = {"BGRA;16L"}, + [IMAGING_RAWMODE_BGRX] = {"BGRX"}, + [IMAGING_RAWMODE_BGR_5] = {"BGR;5"}, + [IMAGING_RAWMODE_BGRa] = {"BGRa"}, + [IMAGING_RAWMODE_BGXR] = {"BGXR"}, + [IMAGING_RAWMODE_B_16B] = {"B;16B"}, + [IMAGING_RAWMODE_B_16L] = {"B;16L"}, + [IMAGING_RAWMODE_B_16N] = {"B;16N"}, + [IMAGING_RAWMODE_C] = {"C"}, + [IMAGING_RAWMODE_CMYKX] = {"CMYKX"}, + [IMAGING_RAWMODE_CMYKXX] = {"CMYKXX"}, + [IMAGING_RAWMODE_CMYK_16B] = {"CMYK;16B"}, + [IMAGING_RAWMODE_CMYK_16L] = {"CMYK;16L"}, + [IMAGING_RAWMODE_CMYK_16N] = {"CMYK;16N"}, + [IMAGING_RAWMODE_CMYK_I] = {"CMYK;I"}, + [IMAGING_RAWMODE_CMYK_L] = {"CMYK;L"}, + [IMAGING_RAWMODE_C_I] = {"C;I"}, + [IMAGING_RAWMODE_Cb] = {"Cb"}, + [IMAGING_RAWMODE_Cr] = {"Cr"}, + [IMAGING_RAWMODE_F_16] = {"F;16"}, + [IMAGING_RAWMODE_F_16B] = {"F;16B"}, + [IMAGING_RAWMODE_F_16BS] = {"F;16BS"}, + [IMAGING_RAWMODE_F_16N] = {"F;16N"}, + [IMAGING_RAWMODE_F_16NS] = {"F;16NS"}, + [IMAGING_RAWMODE_F_16S] = {"F;16S"}, + [IMAGING_RAWMODE_F_32] = {"F;32"}, + [IMAGING_RAWMODE_F_32B] = {"F;32B"}, + [IMAGING_RAWMODE_F_32BF] = {"F;32BF"}, + [IMAGING_RAWMODE_F_32BS] = {"F;32BS"}, + [IMAGING_RAWMODE_F_32F] = {"F;32F"}, + [IMAGING_RAWMODE_F_32N] = {"F;32N"}, + [IMAGING_RAWMODE_F_32NF] = {"F;32NF"}, + [IMAGING_RAWMODE_F_32NS] = {"F;32NS"}, + [IMAGING_RAWMODE_F_32S] = {"F;32S"}, + [IMAGING_RAWMODE_F_64BF] = {"F;64BF"}, + [IMAGING_RAWMODE_F_64F] = {"F;64F"}, + [IMAGING_RAWMODE_F_64NF] = {"F;64NF"}, + [IMAGING_RAWMODE_F_8] = {"F;8"}, + [IMAGING_RAWMODE_F_8S] = {"F;8S"}, + [IMAGING_RAWMODE_G] = {"G"}, + [IMAGING_RAWMODE_G_16B] = {"G;16B"}, + [IMAGING_RAWMODE_G_16L] = {"G;16L"}, + [IMAGING_RAWMODE_G_16N] = {"G;16N"}, + [IMAGING_RAWMODE_H] = {"H"}, + [IMAGING_RAWMODE_I_12] = {"I;12"}, + [IMAGING_RAWMODE_I_16BS] = {"I;16BS"}, + [IMAGING_RAWMODE_I_16NS] = {"I;16NS"}, + [IMAGING_RAWMODE_I_16R] = {"I;16R"}, + [IMAGING_RAWMODE_I_16S] = {"I;16S"}, + [IMAGING_RAWMODE_I_32] = {"I;32"}, + [IMAGING_RAWMODE_I_32BS] = {"I;32BS"}, + [IMAGING_RAWMODE_I_32N] = {"I;32N"}, + [IMAGING_RAWMODE_I_32NS] = {"I;32NS"}, + [IMAGING_RAWMODE_I_32S] = {"I;32S"}, + [IMAGING_RAWMODE_I_8] = {"I;8"}, + [IMAGING_RAWMODE_I_8S] = {"I;8S"}, + [IMAGING_RAWMODE_K] = {"K"}, + [IMAGING_RAWMODE_K_I] = {"K;I"}, + [IMAGING_RAWMODE_LA_16B] = {"LA;16B"}, + [IMAGING_RAWMODE_LA_L] = {"LA;L"}, + [IMAGING_RAWMODE_L_16] = {"L;16"}, + [IMAGING_RAWMODE_L_16B] = {"L;16B"}, + [IMAGING_RAWMODE_L_2] = {"L;2"}, + [IMAGING_RAWMODE_L_2I] = {"L;2I"}, + [IMAGING_RAWMODE_L_2IR] = {"L;2IR"}, + [IMAGING_RAWMODE_L_2R] = {"L;2R"}, + [IMAGING_RAWMODE_L_4] = {"L;4"}, + [IMAGING_RAWMODE_L_4I] = {"L;4I"}, + [IMAGING_RAWMODE_L_4IR] = {"L;4IR"}, + [IMAGING_RAWMODE_L_4R] = {"L;4R"}, + [IMAGING_RAWMODE_L_I] = {"L;I"}, + [IMAGING_RAWMODE_L_R] = {"L;R"}, + [IMAGING_RAWMODE_M] = {"M"}, + [IMAGING_RAWMODE_M_I] = {"M;I"}, + [IMAGING_RAWMODE_PA_L] = {"PA;L"}, + [IMAGING_RAWMODE_PX] = {"PX"}, + [IMAGING_RAWMODE_P_1] = {"P;1"}, + [IMAGING_RAWMODE_P_2] = {"P;2"}, + [IMAGING_RAWMODE_P_2L] = {"P;2L"}, + [IMAGING_RAWMODE_P_4] = {"P;4"}, + [IMAGING_RAWMODE_P_4L] = {"P;4L"}, + [IMAGING_RAWMODE_P_R] = {"P;R"}, + [IMAGING_RAWMODE_R] = {"R"}, + [IMAGING_RAWMODE_RGBAX] = {"RGBAX"}, + [IMAGING_RAWMODE_RGBAXX] = {"RGBAXX"}, + [IMAGING_RAWMODE_RGBA_15] = {"RGBA;15"}, + [IMAGING_RAWMODE_RGBA_16B] = {"RGBA;16B"}, + [IMAGING_RAWMODE_RGBA_16L] = {"RGBA;16L"}, + [IMAGING_RAWMODE_RGBA_16N] = {"RGBA;16N"}, + [IMAGING_RAWMODE_RGBA_4B] = {"RGBA;4B"}, + [IMAGING_RAWMODE_RGBA_I] = {"RGBA;I"}, + [IMAGING_RAWMODE_RGBA_L] = {"RGBA;L"}, + [IMAGING_RAWMODE_RGBXX] = {"RGBXX"}, + [IMAGING_RAWMODE_RGBXXX] = {"RGBXXX"}, + [IMAGING_RAWMODE_RGBX_16B] = {"RGBX;16B"}, + [IMAGING_RAWMODE_RGBX_16L] = {"RGBX;16L"}, + [IMAGING_RAWMODE_RGBX_16N] = {"RGBX;16N"}, + [IMAGING_RAWMODE_RGBX_L] = {"RGBX;L"}, + [IMAGING_RAWMODE_RGB_15] = {"RGB;15"}, + [IMAGING_RAWMODE_RGB_16] = {"RGB;16"}, + [IMAGING_RAWMODE_RGB_16B] = {"RGB;16B"}, + [IMAGING_RAWMODE_RGB_16L] = {"RGB;16L"}, + [IMAGING_RAWMODE_RGB_16N] = {"RGB;16N"}, + [IMAGING_RAWMODE_RGB_4B] = {"RGB;4B"}, + [IMAGING_RAWMODE_RGB_L] = {"RGB;L"}, + [IMAGING_RAWMODE_RGB_R] = {"RGB;R"}, + [IMAGING_RAWMODE_RGBaX] = {"RGBaX"}, + [IMAGING_RAWMODE_RGBaXX] = {"RGBaXX"}, + [IMAGING_RAWMODE_RGBa_16B] = {"RGBa;16B"}, + [IMAGING_RAWMODE_RGBa_16L] = {"RGBa;16L"}, + [IMAGING_RAWMODE_RGBa_16N] = {"RGBa;16N"}, + [IMAGING_RAWMODE_R_16B] = {"R;16B"}, + [IMAGING_RAWMODE_R_16L] = {"R;16L"}, + [IMAGING_RAWMODE_R_16N] = {"R;16N"}, + [IMAGING_RAWMODE_S] = {"S"}, + [IMAGING_RAWMODE_V] = {"V"}, + [IMAGING_RAWMODE_X] = {"X"}, + [IMAGING_RAWMODE_XBGR] = {"XBGR"}, + [IMAGING_RAWMODE_XRGB] = {"XRGB"}, + [IMAGING_RAWMODE_Y] = {"Y"}, + [IMAGING_RAWMODE_YCCA_P] = {"YCCA;P"}, + [IMAGING_RAWMODE_YCC_P] = {"YCC;P"}, + [IMAGING_RAWMODE_YCbCrK] = {"YCbCrK"}, + [IMAGING_RAWMODE_YCbCrX] = {"YCbCrX"}, + [IMAGING_RAWMODE_YCbCr_L] = {"YCbCr;L"}, + [IMAGING_RAWMODE_Y_I] = {"Y;I"}, + [IMAGING_RAWMODE_aBGR] = {"aBGR"}, + [IMAGING_RAWMODE_aRGB] = {"aRGB"}, +}; + +const RawModeID +findRawModeID(const char *const name) { + if (name == NULL) { + return IMAGING_RAWMODE_UNKNOWN; + } + for (size_t i = 0; i < sizeof(RAWMODES) / sizeof(*RAWMODES); i++) { +#ifdef NDEBUG + if (RAWMODES[i].name == NULL) { + fprintf(stderr, "Rawmode ID %zu is not defined.\n", (size_t)i); + } else +#endif + if (strcmp(RAWMODES[i].name, name) == 0) { + return (RawModeID)i; + } + } + return IMAGING_RAWMODE_UNKNOWN; +} + +const RawModeData *const +getRawModeData(const RawModeID id) { + if (id < 0 || id > sizeof(RAWMODES) / sizeof(*RAWMODES)) { + return &RAWMODES[IMAGING_RAWMODE_UNKNOWN]; + } + return &RAWMODES[id]; +} + +int +isModeI16(const ModeID mode) { + return mode == IMAGING_MODE_I_16 || mode == IMAGING_MODE_I_16L || + mode == IMAGING_MODE_I_16B || mode == IMAGING_MODE_I_16N; +} diff --git a/src/libImaging/Mode.h b/src/libImaging/Mode.h new file mode 100644 index 00000000000..39c0eb91994 --- /dev/null +++ b/src/libImaging/Mode.h @@ -0,0 +1,229 @@ +#ifndef __MODE_H__ +#define __MODE_H__ + +typedef enum { + IMAGING_MODE_UNKNOWN, + + IMAGING_MODE_1, + IMAGING_MODE_CMYK, + IMAGING_MODE_F, + IMAGING_MODE_HSV, + IMAGING_MODE_I, + IMAGING_MODE_L, + IMAGING_MODE_LA, + IMAGING_MODE_LAB, + IMAGING_MODE_La, + IMAGING_MODE_P, + IMAGING_MODE_PA, + IMAGING_MODE_RGB, + IMAGING_MODE_RGBA, + IMAGING_MODE_RGBX, + IMAGING_MODE_RGBa, + IMAGING_MODE_YCbCr, + + IMAGING_MODE_I_16, + IMAGING_MODE_I_16L, + IMAGING_MODE_I_16B, + IMAGING_MODE_I_16N, +} ModeID; + +typedef struct { + const char *const name; +} ModeData; + +const ModeID +findModeID(const char *const name); +const ModeData *const +getModeData(const ModeID id); + +typedef enum { + IMAGING_RAWMODE_UNKNOWN, + + // Non-rawmode aliases. + IMAGING_RAWMODE_1, + IMAGING_RAWMODE_CMYK, + IMAGING_RAWMODE_F, + IMAGING_RAWMODE_HSV, + IMAGING_RAWMODE_I, + IMAGING_RAWMODE_L, + IMAGING_RAWMODE_LA, + IMAGING_RAWMODE_LAB, + IMAGING_RAWMODE_La, + IMAGING_RAWMODE_P, + IMAGING_RAWMODE_PA, + IMAGING_RAWMODE_RGB, + IMAGING_RAWMODE_RGBA, + IMAGING_RAWMODE_RGBX, + IMAGING_RAWMODE_RGBa, + IMAGING_RAWMODE_YCbCr, + + // I;* modes. + IMAGING_RAWMODE_I_16, + IMAGING_RAWMODE_I_16L, + IMAGING_RAWMODE_I_16B, + IMAGING_RAWMODE_I_16N, + + // Rawmodes + IMAGING_RAWMODE_1_8, + IMAGING_RAWMODE_1_I, + IMAGING_RAWMODE_1_IR, + IMAGING_RAWMODE_1_R, + IMAGING_RAWMODE_A, + IMAGING_RAWMODE_ABGR, + IMAGING_RAWMODE_ARGB, + IMAGING_RAWMODE_A_16B, + IMAGING_RAWMODE_A_16L, + IMAGING_RAWMODE_A_16N, + IMAGING_RAWMODE_B, + IMAGING_RAWMODE_BGAR, + IMAGING_RAWMODE_BGR, + IMAGING_RAWMODE_BGRA, + IMAGING_RAWMODE_BGRA_15, + IMAGING_RAWMODE_BGRA_15Z, + IMAGING_RAWMODE_BGRA_16B, + IMAGING_RAWMODE_BGRA_16L, + IMAGING_RAWMODE_BGRX, + IMAGING_RAWMODE_BGR_5, + IMAGING_RAWMODE_BGR_15, + IMAGING_RAWMODE_BGR_16, + IMAGING_RAWMODE_BGRa, + IMAGING_RAWMODE_BGXR, + IMAGING_RAWMODE_B_16B, + IMAGING_RAWMODE_B_16L, + IMAGING_RAWMODE_B_16N, + IMAGING_RAWMODE_C, + IMAGING_RAWMODE_CMYKX, + IMAGING_RAWMODE_CMYKXX, + IMAGING_RAWMODE_CMYK_16B, + IMAGING_RAWMODE_CMYK_16L, + IMAGING_RAWMODE_CMYK_16N, + IMAGING_RAWMODE_CMYK_I, + IMAGING_RAWMODE_CMYK_L, + IMAGING_RAWMODE_C_I, + IMAGING_RAWMODE_Cb, + IMAGING_RAWMODE_Cr, + IMAGING_RAWMODE_I_32B, + IMAGING_RAWMODE_F_16, + IMAGING_RAWMODE_F_16B, + IMAGING_RAWMODE_F_16BS, + IMAGING_RAWMODE_F_16N, + IMAGING_RAWMODE_F_16NS, + IMAGING_RAWMODE_F_16S, + IMAGING_RAWMODE_F_32, + IMAGING_RAWMODE_F_32B, + IMAGING_RAWMODE_F_32BF, + IMAGING_RAWMODE_F_32BS, + IMAGING_RAWMODE_F_32F, + IMAGING_RAWMODE_F_32N, + IMAGING_RAWMODE_F_32NF, + IMAGING_RAWMODE_F_32NS, + IMAGING_RAWMODE_F_32S, + IMAGING_RAWMODE_F_64BF, + IMAGING_RAWMODE_F_64F, + IMAGING_RAWMODE_F_64NF, + IMAGING_RAWMODE_F_8, + IMAGING_RAWMODE_F_8S, + IMAGING_RAWMODE_G, + IMAGING_RAWMODE_G_16B, + IMAGING_RAWMODE_G_16L, + IMAGING_RAWMODE_G_16N, + IMAGING_RAWMODE_H, + IMAGING_RAWMODE_I_12, + IMAGING_RAWMODE_I_16BS, + IMAGING_RAWMODE_I_16NS, + IMAGING_RAWMODE_I_16R, + IMAGING_RAWMODE_I_16S, + IMAGING_RAWMODE_I_32, + IMAGING_RAWMODE_I_32BS, + IMAGING_RAWMODE_I_32N, + IMAGING_RAWMODE_I_32NS, + IMAGING_RAWMODE_I_32S, + IMAGING_RAWMODE_I_8, + IMAGING_RAWMODE_I_8S, + IMAGING_RAWMODE_K, + IMAGING_RAWMODE_K_I, + IMAGING_RAWMODE_LA_16B, + IMAGING_RAWMODE_LA_L, + IMAGING_RAWMODE_L_16, + IMAGING_RAWMODE_L_16B, + IMAGING_RAWMODE_L_2, + IMAGING_RAWMODE_L_2I, + IMAGING_RAWMODE_L_2IR, + IMAGING_RAWMODE_L_2R, + IMAGING_RAWMODE_L_4, + IMAGING_RAWMODE_L_4I, + IMAGING_RAWMODE_L_4IR, + IMAGING_RAWMODE_L_4R, + IMAGING_RAWMODE_L_I, + IMAGING_RAWMODE_L_R, + IMAGING_RAWMODE_M, + IMAGING_RAWMODE_M_I, + IMAGING_RAWMODE_PA_L, + IMAGING_RAWMODE_PX, + IMAGING_RAWMODE_P_1, + IMAGING_RAWMODE_P_2, + IMAGING_RAWMODE_P_2L, + IMAGING_RAWMODE_P_4, + IMAGING_RAWMODE_P_4L, + IMAGING_RAWMODE_P_R, + IMAGING_RAWMODE_R, + IMAGING_RAWMODE_RGBAX, + IMAGING_RAWMODE_RGBAXX, + IMAGING_RAWMODE_RGBA_15, + IMAGING_RAWMODE_RGBA_16B, + IMAGING_RAWMODE_RGBA_16L, + IMAGING_RAWMODE_RGBA_16N, + IMAGING_RAWMODE_RGBA_4B, + IMAGING_RAWMODE_RGBA_I, + IMAGING_RAWMODE_RGBA_L, + IMAGING_RAWMODE_RGBXX, + IMAGING_RAWMODE_RGBXXX, + IMAGING_RAWMODE_RGBX_16B, + IMAGING_RAWMODE_RGBX_16L, + IMAGING_RAWMODE_RGBX_16N, + IMAGING_RAWMODE_RGBX_L, + IMAGING_RAWMODE_RGB_15, + IMAGING_RAWMODE_RGB_16, + IMAGING_RAWMODE_RGB_16B, + IMAGING_RAWMODE_RGB_16L, + IMAGING_RAWMODE_RGB_16N, + IMAGING_RAWMODE_RGB_4B, + IMAGING_RAWMODE_RGB_L, + IMAGING_RAWMODE_RGB_R, + IMAGING_RAWMODE_RGBaX, + IMAGING_RAWMODE_RGBaXX, + IMAGING_RAWMODE_RGBa_16B, + IMAGING_RAWMODE_RGBa_16L, + IMAGING_RAWMODE_RGBa_16N, + IMAGING_RAWMODE_R_16B, + IMAGING_RAWMODE_R_16L, + IMAGING_RAWMODE_R_16N, + IMAGING_RAWMODE_S, + IMAGING_RAWMODE_V, + IMAGING_RAWMODE_X, + IMAGING_RAWMODE_XBGR, + IMAGING_RAWMODE_XRGB, + IMAGING_RAWMODE_Y, + IMAGING_RAWMODE_YCCA_P, + IMAGING_RAWMODE_YCC_P, + IMAGING_RAWMODE_YCbCrK, + IMAGING_RAWMODE_YCbCrX, + IMAGING_RAWMODE_YCbCr_L, + IMAGING_RAWMODE_Y_I, + IMAGING_RAWMODE_aBGR, + IMAGING_RAWMODE_aRGB, +} RawModeID; + +typedef struct { + const char *const name; +} RawModeData; + +const RawModeID +findRawModeID(const char *const name); +const RawModeData *const +getRawModeData(const RawModeID id); + +int +isModeI16(const ModeID mode); + +#endif // __MODE_H__ diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index f3b71421595..161d82f2e5f 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -258,16 +258,6 @@ void ImagingPackRGB(UINT8 *out, const UINT8 *in, int pixels) { int i = 0; /* RGB triplets */ -#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; @@ -278,7 +268,6 @@ ImagingPackRGB(UINT8 *out, const UINT8 *in, int pixels) { out[2] = in[i * 4 + B]; out += 3; } -#endif } void @@ -336,6 +325,19 @@ ImagingPackXBGR(UINT8 *out, const UINT8 *in, int pixels) { } } +void +ImagingPackCMYK2RGB(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)); + out += 3; + in += 4; + } +} + void ImagingPackBGRA(UINT8 *out, const UINT8 *in, int pixels) { int i; @@ -482,12 +484,6 @@ copy2(UINT8 *out, const UINT8 *in, int pixels) { memcpy(out, in, pixels * 2); } -static void -copy3(UINT8 *out, const UINT8 *in, int pixels) { - /* BGR;24, etc */ - memcpy(out, in, pixels * 3); -} - static void copy4(UINT8 *out, const UINT8 *in, int pixels) { /* RGBA, CMYK quadruples */ @@ -536,153 +532,146 @@ band3(UINT8 *out, const UINT8 *in, int pixels) { } static struct { - const char *mode; - const char *rawmode; + const ModeID mode; + const RawModeID 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}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1, 1, pack1}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_I, 1, pack1I}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_R, 1, pack1R}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_IR, 1, pack1IR}, + {IMAGING_MODE_1, IMAGING_RAWMODE_L, 8, pack1L}, /* grayscale */ - {"L", "L", 8, copy1}, - {"L", "L;16", 16, packL16}, - {"L", "L;16B", 16, packL16B}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L, 8, copy1}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_16, 16, packL16}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_16B, 16, packL16B}, /* grayscale w. alpha */ - {"LA", "LA", 16, packLA}, - {"LA", "LA;L", 16, packLAL}, + {IMAGING_MODE_LA, IMAGING_RAWMODE_LA, 16, packLA}, + {IMAGING_MODE_LA, IMAGING_RAWMODE_LA_L, 16, packLAL}, /* grayscale w. alpha premultiplied */ - {"La", "La", 16, packLA}, + {IMAGING_MODE_La, IMAGING_RAWMODE_La, 16, packLA}, /* palette */ - {"P", "P;1", 1, pack1}, - {"P", "P;2", 2, packP2}, - {"P", "P;4", 4, packP4}, - {"P", "P", 8, copy1}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_1, 1, pack1}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_2, 2, packP2}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_4, 4, packP4}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P, 8, copy1}, /* palette w. alpha */ - {"PA", "PA", 16, packLA}, - {"PA", "PA;L", 16, packLAL}, + {IMAGING_MODE_PA, IMAGING_RAWMODE_PA, 16, packLA}, + {IMAGING_MODE_PA, IMAGING_RAWMODE_PA_L, 16, packLAL}, /* true colour */ - {"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}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB, 24, ImagingPackRGB}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX, 32, copy4}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBA, 32, copy4}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_XRGB, 32, ImagingPackXRGB}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR, 24, ImagingPackBGR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGRX, 32, ImagingPackBGRX}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_XBGR, 32, ImagingPackXBGR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_L, 24, packRGBL}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_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}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA, 32, copy4}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_L, 32, packRGBXL}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGB, 24, ImagingPackRGB}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGR, 24, ImagingPackBGR}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA, 32, ImagingPackBGRA}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_ABGR, 32, ImagingPackABGR}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRa, 32, ImagingPackBGRa}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B, 8, band2}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A, 8, band3}, /* true colour w. alpha premultiplied */ - {"RGBa", "RGBa", 32, copy4}, - {"RGBa", "BGRa", 32, ImagingPackBGRA}, - {"RGBa", "aBGR", 32, ImagingPackABGR}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_RGBa, 32, copy4}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_BGRa, 32, ImagingPackBGRA}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_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}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX, 32, copy4}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_L, 32, packRGBXL}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB, 24, ImagingPackRGB}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGR, 24, ImagingPackBGR}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGRX, 32, ImagingPackBGRX}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_XBGR, 32, ImagingPackXBGR}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_B, 8, band2}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_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}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK, 32, copy4}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_I, 32, copy4I}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_L, 32, packRGBXL}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_C, 8, band0}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_M, 8, band1}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_Y, 8, band2}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_K, 8, band3}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_RGB, 24, ImagingPackCMYK2RGB}, /* 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}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr, 24, ImagingPackRGB}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr_L, 24, packRGBL}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCrX, 32, copy4}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCrK, 32, copy4}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_Y, 8, band0}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_Cb, 8, band1}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_Cr, 8, band2}, /* LAB Color */ - {"LAB", "LAB", 24, ImagingPackLAB}, - {"LAB", "L", 8, band0}, - {"LAB", "A", 8, band1}, - {"LAB", "B", 8, band2}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_LAB, 24, ImagingPackLAB}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_L, 8, band0}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_A, 8, band1}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_B, 8, band2}, /* HSV */ - {"HSV", "HSV", 24, ImagingPackRGB}, - {"HSV", "H", 8, band0}, - {"HSV", "S", 8, band1}, - {"HSV", "V", 8, band2}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_HSV, 24, ImagingPackRGB}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_H, 8, band0}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_S, 8, band1}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_V, 8, band2}, /* integer */ - {"I", "I", 32, copy4}, - {"I", "I;16B", 16, packI16B}, - {"I", "I;32S", 32, packI32S}, - {"I", "I;32NS", 32, copy4}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I, 32, copy4}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16B, 16, packI16B}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32S, 32, packI32S}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32NS, 32, copy4}, /* floating point */ - {"F", "F", 32, copy4}, - {"F", "F;32F", 32, packI32S}, - {"F", "F;32NF", 32, copy4}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F, 32, copy4}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32F, 32, packI32S}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32NF, 32, copy4}, /* storage modes */ - {"I;16", "I;16", 16, copy2}, + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16, 16, copy2}, #ifdef WORDS_BIGENDIAN - {"I;16", "I;16B", 16, packI16N_I16}, + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16B, 16, packI16N_I16}, #else - {"I;16", "I;16B", 16, packI16N_I16B}, + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16B, 16, packI16N_I16B}, #endif - {"I;16B", "I;16B", 16, copy2}, - {"I;16L", "I;16L", 16, copy2}, - {"I;16N", "I;16N", 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 */ + {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16B, 16, copy2}, + {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16L, 16, copy2}, + {IMAGING_MODE_I_16N, IMAGING_RAWMODE_I_16N, 16, copy2}, + // LibTiff native->image endian. + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16N, 16, packI16N_I16}, + {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16N, 16, packI16N_I16}, + {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16N, 16, packI16N_I16B} }; ImagingShuffler -ImagingFindPacker(const char *mode, const char *rawmode, int *bits_out) { - int i; - - /* find a suitable pixel packer */ - for (i = 0; packers[i].rawmode; i++) { - if (strcmp(packers[i].mode, mode) == 0 && - strcmp(packers[i].rawmode, rawmode) == 0) { +ImagingFindPacker(const ModeID mode, const RawModeID rawmode, int *bits_out) { + for (size_t i = 0; i < sizeof(packers) / sizeof(*packers); i++) { + if (packers[i].mode == mode && packers[i].rawmode == rawmode) { if (bits_out) { *bits_out = packers[i].bits; } diff --git a/src/libImaging/PackDecode.c b/src/libImaging/PackDecode.c index 7dd432b91c2..52f1ac502bd 100644 --- a/src/libImaging/PackDecode.c +++ b/src/libImaging/PackDecode.c @@ -17,7 +17,8 @@ int ImagingPackbitsDecode( - Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { + Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes +) { UINT8 n; UINT8 *ptr; int i; @@ -79,7 +80,8 @@ ImagingPackbitsDecode( (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, state->buffer, - state->xsize); + state->xsize + ); state->x = 0; diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c index 78916bca52b..b2dacf656b5 100644 --- a/src/libImaging/Palette.c +++ b/src/libImaging/Palette.c @@ -21,13 +21,14 @@ #include ImagingPalette -ImagingPaletteNew(const char *mode) { +ImagingPaletteNew(const ModeID mode) { /* Create a palette object */ int i; ImagingPalette palette; - if (strcmp(mode, "RGB") && strcmp(mode, "RGBA")) { + if (mode != IMAGING_MODE_RGB && mode != IMAGING_MODE_RGBA && + mode != IMAGING_MODE_CMYK) { return (ImagingPalette)ImagingError_ModeError(); } @@ -36,8 +37,7 @@ ImagingPaletteNew(const char *mode) { return (ImagingPalette)ImagingError_MemoryError(); } - strncpy(palette->mode, mode, IMAGING_MODE_LENGTH - 1); - palette->mode[IMAGING_MODE_LENGTH - 1] = 0; + palette->mode = mode; palette->size = 0; for (i = 0; i < 256; i++) { @@ -54,7 +54,7 @@ ImagingPaletteNewBrowser(void) { int i, r, g, b; ImagingPalette palette; - palette = ImagingPaletteNew("RGB"); + palette = ImagingPaletteNew(IMAGING_MODE_RGB); if (!palette) { return NULL; } @@ -148,7 +148,7 @@ ImagingPaletteDelete(ImagingPalette palette) { #define BOX 8 -#define BOXVOLUME BOX *BOX *BOX +#define BOXVOLUME BOX * BOX * BOX void ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) { diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index dc67cb41d2f..f4b72c5d814 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -23,6 +23,18 @@ #include "Imaging.h" +#define PREPARE_PASTE_LOOP() \ + int y, y_end, offset; \ + if (imOut == imIn && dy > sy) { \ + y = ysize - 1; \ + y_end = -1; \ + offset = -1; \ + } else { \ + y = 0; \ + y_end = ysize; \ + offset = 1; \ + } + static inline void paste( Imaging imOut, @@ -33,17 +45,17 @@ paste( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* paste opaque region */ - int y; - dx *= pixelsize; sx *= pixelsize; xsize *= pixelsize; - for (y = 0; y < ysize; y++) { + PREPARE_PASTE_LOOP(); + for (; y != y_end; y += offset) { memcpy(imOut->image[y + dy] + dx, imIn->image[y + sy] + sx, xsize); } } @@ -59,15 +71,17 @@ paste_mask_1( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* paste with mode "1" mask */ - int x, y; + int x; + PREPARE_PASTE_LOOP(); if (imOut->image8) { - int in_i16 = strncmp(imIn->mode, "I;16", 4) == 0; - int out_i16 = strncmp(imOut->mode, "I;16", 4) == 0; - for (y = 0; y < ysize; y++) { + int in_i16 = isModeI16(imIn->mode); + int out_i16 = isModeI16(imOut->mode); + for (; y != y_end; y += offset) { UINT8 *out = imOut->image8[y + dy] + dx; if (out_i16) { out += dx; @@ -95,7 +109,7 @@ paste_mask_1( } } else { - for (y = 0; y < ysize; y++) { + for (; y != y_end; y += offset) { INT32 *out = imOut->image32[y + dy] + dx; INT32 *in = imIn->image32[y + sy] + sx; UINT8 *mask = imMask->image8[y + sy] + sx; @@ -120,14 +134,16 @@ paste_mask_L( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* paste with mode "L" matte */ - int x, y; + int x; unsigned int tmp1; + PREPARE_PASTE_LOOP(); if (imOut->image8) { - for (y = 0; y < ysize; y++) { + for (; y != y_end; y += offset) { UINT8 *out = imOut->image8[y + dy] + dx; UINT8 *in = imIn->image8[y + sy] + sx; UINT8 *mask = imMask->image8[y + sy] + sx; @@ -138,7 +154,7 @@ paste_mask_L( } } else { - for (y = 0; y < ysize; y++) { + for (; y != y_end; y += offset) { UINT8 *out = (UINT8 *)(imOut->image32[y + dy] + dx); UINT8 *in = (UINT8 *)(imIn->image32[y + sy] + sx); UINT8 *mask = (UINT8 *)(imMask->image8[y + sy] + sx); @@ -167,14 +183,16 @@ paste_mask_RGBA( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* paste with mode "RGBA" matte */ - int x, y; + int x; unsigned int tmp1; + PREPARE_PASTE_LOOP(); if (imOut->image8) { - for (y = 0; y < ysize; y++) { + for (; y != y_end; y += offset) { UINT8 *out = imOut->image8[y + dy] + dx; UINT8 *in = imIn->image8[y + sy] + sx; UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx * 4 + 3; @@ -185,7 +203,7 @@ paste_mask_RGBA( } } else { - for (y = 0; y < ysize; y++) { + for (; y != y_end; y += offset) { UINT8 *out = (UINT8 *)(imOut->image32[y + dy] + dx); UINT8 *in = (UINT8 *)(imIn->image32[y + sy] + sx); UINT8 *mask = (UINT8 *)(imMask->image32[y + sy] + sx); @@ -214,14 +232,16 @@ paste_mask_RGBa( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* paste with mode "RGBa" matte */ - int x, y; + int x; unsigned int tmp1; + PREPARE_PASTE_LOOP(); if (imOut->image8) { - for (y = 0; y < ysize; y++) { + for (; y != y_end; y += offset) { UINT8 *out = imOut->image8[y + dy] + dx; UINT8 *in = imIn->image8[y + sy] + sx; UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx * 4 + 3; @@ -232,7 +252,7 @@ paste_mask_RGBa( } } else { - for (y = 0; y < ysize; y++) { + for (; y != y_end; y += offset) { UINT8 *out = (UINT8 *)(imOut->image32[y + dy] + dx); UINT8 *in = (UINT8 *)(imIn->image32[y + sy] + sx); UINT8 *mask = (UINT8 *)(imMask->image32[y + sy] + sx); @@ -252,7 +272,8 @@ paste_mask_RGBa( int ImagingPaste( - Imaging imOut, Imaging imIn, Imaging imMask, int dx0, int dy0, int dx1, int dy1) { + Imaging imOut, Imaging imIn, Imaging imMask, int dx0, int dy0, int dx1, int dy1 +) { int xsize, ysize; int pixelsize; int sx0, sy0; @@ -301,29 +322,26 @@ ImagingPaste( ImagingSectionEnter(&cookie); paste(imOut, imIn, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "1") == 0) { + } else if (imMask->mode == IMAGING_MODE_1) { ImagingSectionEnter(&cookie); paste_mask_1(imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "L") == 0) { + } else if (imMask->mode == IMAGING_MODE_L) { ImagingSectionEnter(&cookie); paste_mask_L(imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "LA") == 0 || strcmp(imMask->mode, "RGBA") == 0) { + } else if (imMask->mode == IMAGING_MODE_LA || imMask->mode == IMAGING_MODE_RGBA) { ImagingSectionEnter(&cookie); paste_mask_RGBA( - imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); + imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize + ); ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "RGBa") == 0) { + } else if (imMask->mode == IMAGING_MODE_RGBa) { ImagingSectionEnter(&cookie); paste_mask_RGBa( - imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); + imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize + ); ImagingSectionLeave(&cookie); - } else { (void)ImagingError_ValueError("bad transparency mask"); return -1; @@ -334,21 +352,16 @@ ImagingPaste( static inline void fill( - Imaging imOut, - const void *ink_, - int dx, - int dy, - int xsize, - int ysize, - int pixelsize) { + Imaging imOut, const UINT8 *ink, int dx, int dy, int xsize, int ysize, int pixelsize +) { /* fill opaque region */ int x, y; UINT8 ink8 = 0; INT32 ink32 = 0L; - memcpy(&ink32, ink_, pixelsize); - memcpy(&ink8, ink_, sizeof(ink8)); + memcpy(&ink32, ink, pixelsize); + memcpy(&ink8, ink, sizeof(ink8)); if (imOut->image8 || ink32 == 0L) { dx *= pixelsize; @@ -358,12 +371,25 @@ fill( } } else { +#if defined _WIN32 && !defined _WIN64 + int i; + dx *= pixelsize; + for (y = 0; y < ysize; y++) { + UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx; + for (x = 0; x < xsize; x++) { + for (i = 0; i < pixelsize; i++) { + *out++ = ink[i]; + } + } + } +#else for (y = 0; y < ysize; y++) { INT32 *out = imOut->image32[y + dy] + dx; for (x = 0; x < xsize; x++) { out[x] = ink32; } } +#endif } } @@ -378,7 +404,8 @@ fill_mask_1( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* fill with mode "1" mask */ int x, y; @@ -425,14 +452,15 @@ fill_mask_L( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* fill with mode "L" matte */ int x, y, i; unsigned int tmp1; if (imOut->image8) { - int i16 = strncmp(imOut->mode, "I;16", 4) == 0; + int i16 = isModeI16(imOut->mode); for (y = 0; y < ysize; y++) { UINT8 *out = imOut->image8[y + dy] + dx; if (i16) { @@ -451,9 +479,9 @@ fill_mask_L( } else { int alpha_channel = - strcmp(imOut->mode, "RGBa") == 0 || strcmp(imOut->mode, "RGBA") == 0 || - strcmp(imOut->mode, "La") == 0 || strcmp(imOut->mode, "LA") == 0 || - strcmp(imOut->mode, "PA") == 0; + imOut->mode == IMAGING_MODE_RGBa || imOut->mode == IMAGING_MODE_RGBA || + imOut->mode == IMAGING_MODE_La || imOut->mode == IMAGING_MODE_LA || + imOut->mode == IMAGING_MODE_PA; for (y = 0; y < ysize; y++) { UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx * pixelsize; UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx; @@ -484,7 +512,8 @@ fill_mask_RGBA( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* fill with mode "RGBA" matte */ int x, y, i; @@ -529,7 +558,8 @@ fill_mask_RGBa( int sy, int xsize, int ysize, - int pixelsize) { + int pixelsize +) { /* fill with mode "RGBa" matte */ int x, y, i; @@ -565,13 +595,8 @@ fill_mask_RGBa( int ImagingFill2( - Imaging imOut, - const void *ink, - Imaging imMask, - int dx0, - int dy0, - int dx1, - int dy1) { + Imaging imOut, const void *ink, Imaging imMask, int dx0, int dy0, int dx1, int dy1 +) { ImagingSectionCookie cookie; int xsize, ysize; int pixelsize; @@ -615,27 +640,22 @@ ImagingFill2( ImagingSectionEnter(&cookie); fill(imOut, ink, dx0, dy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "1") == 0) { + } else if (imMask->mode == IMAGING_MODE_1) { ImagingSectionEnter(&cookie); fill_mask_1(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "L") == 0) { + } else if (imMask->mode == IMAGING_MODE_L) { ImagingSectionEnter(&cookie); fill_mask_L(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "RGBA") == 0) { + } else if (imMask->mode == IMAGING_MODE_RGBA) { ImagingSectionEnter(&cookie); fill_mask_RGBA(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - - } else if (strcmp(imMask->mode, "RGBa") == 0) { + } else if (imMask->mode == IMAGING_MODE_RGBa) { ImagingSectionEnter(&cookie); fill_mask_RGBa(imOut, ink, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - } else { (void)ImagingError_ValueError("bad transparency mask"); return -1; diff --git a/src/libImaging/PcxDecode.c b/src/libImaging/PcxDecode.c index c95ffc8692c..a65952fb1da 100644 --- a/src/libImaging/PcxDecode.c +++ b/src/libImaging/PcxDecode.c @@ -60,15 +60,26 @@ ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt } 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 bands; + int xsize = 0; + int stride = 0; + if (state->bits == 2 || state->bits == 4) { + xsize = (state->xsize + 7) / 8; + bands = state->bits; + stride = state->bytes / state->bits; + } else { + xsize = state->xsize; + bands = state->bytes / state->xsize; + if (bands != 0) { + stride = state->bytes / bands; + } + } + if (stride > xsize) { int i; for (i = 1; i < bands; i++) { // note -- skipping first band memmove( - &state->buffer[i * state->xsize], - &state->buffer[i * stride], - state->xsize); + &state->buffer[i * xsize], &state->buffer[i * stride], xsize + ); } } /* Got a full line, unpack it */ @@ -76,7 +87,8 @@ ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, state->buffer, - state->xsize); + state->xsize + ); state->x = 0; diff --git a/src/libImaging/PcxEncode.c b/src/libImaging/PcxEncode.c index 549614bfd39..625cf7ffa72 100644 --- a/src/libImaging/PcxEncode.c +++ b/src/libImaging/PcxEncode.c @@ -71,7 +71,8 @@ ImagingPcxEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { state->buffer, (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); + state->xsize + ); state->y += 1; diff --git a/src/libImaging/Point.c b/src/libImaging/Point.c index dd06f3940d1..8f6d47c770f 100644 --- a/src/libImaging/Point.c +++ b/src/libImaging/Point.c @@ -128,7 +128,7 @@ im_point_32_8(Imaging imOut, Imaging imIn, im_point_context *context) { } Imaging -ImagingPoint(Imaging imIn, const char *mode, const void *table) { +ImagingPoint(Imaging imIn, ModeID mode, const void *table) { /* lookup table transform */ ImagingSectionCookie cookie; @@ -140,15 +140,15 @@ ImagingPoint(Imaging imIn, const char *mode, const void *table) { return (Imaging)ImagingError_ModeError(); } - if (!mode) { + if (mode == IMAGING_MODE_UNKNOWN) { 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 || mode != IMAGING_MODE_L) { goto mode_mismatch; } - } else if (!imIn->image8 && strcmp(imIn->mode, mode) != 0) { + } else if (!imIn->image8 && imIn->mode != mode) { goto mode_mismatch; } @@ -198,7 +198,8 @@ ImagingPoint(Imaging imIn, const char *mode, const void *table) { mode_mismatch: return (Imaging)ImagingError_ValueError( - "point operation not supported for this mode"); + "point operation not supported for this mode" + ); } Imaging @@ -209,8 +210,8 @@ ImagingPointTransform(Imaging imIn, double scale, double offset) { Imaging imOut; int x, y; - if (!imIn || (strcmp(imIn->mode, "I") != 0 && strcmp(imIn->mode, "I;16") != 0 && - strcmp(imIn->mode, "F") != 0)) { + if (!imIn || (imIn->mode != IMAGING_MODE_I && imIn->mode != IMAGING_MODE_I_16 && + imIn->mode != IMAGING_MODE_F)) { return (Imaging)ImagingError_ModeError(); } @@ -244,7 +245,7 @@ ImagingPointTransform(Imaging imIn, double scale, double offset) { ImagingSectionLeave(&cookie); break; case IMAGING_TYPE_SPECIAL: - if (strcmp(imIn->mode, "I;16") == 0) { + if (imIn->mode == IMAGING_MODE_I_16) { ImagingSectionEnter(&cookie); for (y = 0; y < imIn->ysize; y++) { char *in = (char *)imIn->image[y]; diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index cdc614536da..7e3df180823 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -36,7 +36,8 @@ #define UINT32_MAX 0xffffffff #endif -#define NO_OUTPUT +// #define DEBUG +// #define TEST_NEAREST_NEIGHBOUR typedef struct { uint32_t scale; @@ -103,7 +104,8 @@ 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.c.r >> d->scale, pixel.c.g >> d->scale, pixel.c.b >> d->scale + ); } static int @@ -111,9 +113,11 @@ 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); + 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); + pixel2.c.r >> d->scale, pixel2.c.g >> d->scale, pixel2.c.b >> d->scale + ); return (A == B) ? 0 : ((A < B) ? -1 : 1); } @@ -129,7 +133,8 @@ new_count_func(const HashTable *h, const Pixel key, uint32_t *val) { static void rehash_collide( - const HashTable *h, Pixel *keyp, uint32_t *valp, Pixel newkey, uint32_t newval) { + const HashTable *h, Pixel *keyp, uint32_t *valp, Pixel newkey, uint32_t newval +) { *valp += newval; } @@ -140,7 +145,7 @@ create_pixel_hash(Pixel *pixelData, uint32_t nPixels) { PixelHashData *d; HashTable *hash; uint32_t i; -#ifndef NO_OUTPUT +#ifdef DEBUG uint32_t timer, timer2, timer3; #endif @@ -152,32 +157,33 @@ create_pixel_hash(Pixel *pixelData, uint32_t nPixels) { hash = hashtable_new(pixel_hash, pixel_cmp); hashtable_set_user_data(hash, d); d->scale = 0; -#ifndef NO_OUTPUT +#ifdef DEBUG timer = timer3 = clock(); #endif for (i = 0; i < nPixels; i++) { if (!hashtable_insert_or_update_computed( - hash, pixelData[i], new_count_func, exists_count_func)) { + hash, pixelData[i], new_count_func, exists_count_func + )) { ; } while (hashtable_get_count(hash) > MAX_HASH_ENTRIES) { d->scale++; -#ifndef NO_OUTPUT +#ifdef DEBUG printf("rehashing - new scale: %d\n", (int)d->scale); timer2 = clock(); #endif hashtable_rehash_compute(hash, rehash_collide); -#ifndef NO_OUTPUT +#ifdef DEBUG timer2 = clock() - timer2; printf("rehash took %f sec\n", timer2 / (double)CLOCKS_PER_SEC); timer += timer2; #endif } } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("inserts took %f sec\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif -#ifndef NO_OUTPUT +#ifdef DEBUG printf("total %f sec\n", (clock() - timer3) / (double)CLOCKS_PER_SEC); #endif return hash; @@ -299,18 +305,18 @@ mergesort_pixels(PixelList *head, int i) { return head; } -#if defined(TEST_MERGESORT) || defined(TEST_SORTED) +#ifdef DEBUG static int test_sorted(PixelList *pl[3]) { - int i, n, l; + int i, 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]) + if (l < t->p.a.v[i]) { return 0; + } l = t->p.a.v[i]; } } @@ -335,18 +341,19 @@ splitlists( PixelList *nt[2][3], uint32_t nCount[2], int axis, - uint32_t pixelCount) { + uint32_t pixelCount +) { uint32_t left; PixelList *l, *r, *c, *n; int i; int nRight; -#ifndef NO_OUTPUT +#ifdef DEBUG int nLeft; #endif int splitColourVal; -#ifdef TEST_SPLIT +#ifdef DEBUG { PixelList *_prevTest, *_nextTest; int _i, _nextCount[3], _prevCount[3]; @@ -387,7 +394,8 @@ splitlists( _prevCount[2], _nextCount[0], _nextCount[1], - _nextCount[2]); + _nextCount[2] + ); exit(1); } } @@ -395,14 +403,14 @@ splitlists( #endif nCount[0] = nCount[1] = 0; nRight = 0; -#ifndef NO_OUTPUT +#ifdef DEBUG nLeft = 0; #endif for (left = 0, c = h[axis]; c;) { left = left + c->count; nCount[0] += c->count; c->flag = 0; -#ifndef NO_OUTPUT +#ifdef DEBUG nLeft++; #endif c = c->next[axis]; @@ -417,7 +425,7 @@ splitlists( break; } c->flag = 0; -#ifndef NO_OUTPUT +#ifdef DEBUG nLeft++; #endif nCount[0] += c->count; @@ -435,14 +443,14 @@ splitlists( } c->flag = 1; nRight++; -#ifndef NO_OUTPUT +#ifdef DEBUG nLeft--; #endif nCount[0] -= c->count; nCount[1] += c->count; } } -#ifndef NO_OUTPUT +#ifdef DEBUG 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); @@ -504,7 +512,7 @@ split(BoxNode *node) { gl = node->tail[1]->p.c.g; bh = node->head[2]->p.c.b; bl = node->tail[2]->p.c.b; -#ifdef TEST_SPLIT +#ifdef DEBUG printf("splitting node [%d %d %d] [%d %d %d] ", rl, gl, bl, rh, gh, bh); #endif f[0] = (rh - rl) * 77; @@ -519,11 +527,8 @@ split(BoxNode *node) { axis = i; } } -#ifdef TEST_SPLIT +#ifdef DEBUG printf("along axis %d\n", axis + 1); -#endif - -#ifdef TEST_SPLIT { PixelList *_prevTest, *_nextTest; int _i, _nextCount[3], _prevCount[3]; @@ -531,12 +536,14 @@ split(BoxNode *node) { 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]); + "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]); + "node->head[%d]->prev[%d]=%p\n", _i, _i, node->head[_i]->prev[_i] + ); } } @@ -573,20 +580,22 @@ split(BoxNode *node) { _prevCount[2], _nextCount[0], _nextCount[1], - _nextCount[2]); + _nextCount[2] + ); } } } #endif node->axis = axis; if (!splitlists( - node->head, node->tail, heads, tails, newCounts, axis, node->pixelCount)) { -#ifndef NO_OUTPUT + node->head, node->tail, heads, tails, newCounts, axis, node->pixelCount + )) { +#ifdef DEBUG printf("list split failed.\n"); #endif return 0; } -#ifdef TEST_SPLIT +#ifdef DEBUG if (!test_sorted(heads[0])) { printf("bug in split"); exit(1); @@ -612,7 +621,7 @@ split(BoxNode *node) { node->head[i] = NULL; node->tail[i] = NULL; } -#ifdef TEST_SPLIT +#ifdef DEBUG if (left->head[0]) { rh = left->head[0]->p.c.r; rl = left->tail[0]->p.c.r; @@ -676,7 +685,7 @@ median_cut(PixelList *hl[3], uint32_t imPixelCount, int nPixels) { } } while (compute_box_volume(thisNode) == 1); if (!split(thisNode)) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("Oops, split failed...\n"); #endif exit(1); @@ -705,16 +714,14 @@ free_box_tree(BoxNode *n) { free(n); } -#ifdef TEST_SPLIT_INTEGRITY +#ifdef DEBUG 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) { -#ifndef NO_OUTPUT 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 && @@ -735,7 +742,7 @@ annotate_hash_table(BoxNode *n, HashTable *h, uint32_t *box) { return annotate_hash_table(n->l, h, box) && annotate_hash_table(n->r, h, box); } if (n->l || n->r) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("box tree is dead\n"); #endif return 0; @@ -743,7 +750,7 @@ annotate_hash_table(BoxNode *n, HashTable *h, uint32_t *box) { 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 +#ifdef DEBUG printf("hashtable insert failed\n"); #endif return 0; @@ -772,7 +779,8 @@ _distance_index_cmp(const void *a, const void *b) { static int resort_distance_tables( - uint32_t *avgDist, uint32_t **avgDistSortKey, Pixel *p, uint32_t nEntries) { + uint32_t *avgDist, uint32_t **avgDistSortKey, Pixel *p, uint32_t nEntries +) { uint32_t i, j, k; uint32_t **skRow; uint32_t *skElt; @@ -801,7 +809,8 @@ resort_distance_tables( static int build_distance_tables( - uint32_t *avgDist, uint32_t **avgDistSortKey, Pixel *p, uint32_t nEntries) { + uint32_t *avgDist, uint32_t **avgDistSortKey, Pixel *p, uint32_t nEntries +) { uint32_t i, j; DistanceWithIndex *dwi; @@ -841,7 +850,8 @@ map_image_pixels( uint32_t nPaletteEntries, uint32_t *avgDist, uint32_t **avgDistSortKey, - uint32_t *pixelArray) { + uint32_t *pixelArray +) { uint32_t *aD, **aDSK; uint32_t idx; uint32_t i, j; @@ -888,7 +898,8 @@ map_image_pixels_from_quantized_pixels( uint32_t **avgDistSortKey, uint32_t *pixelArray, uint32_t *avg[3], - uint32_t *count) { + uint32_t *count +) { uint32_t *aD, **aDSK; uint32_t idx; uint32_t i, j; @@ -946,7 +957,8 @@ map_image_pixels_from_median_box( HashTable *medianBoxHash, uint32_t *avgDist, uint32_t **avgDistSortKey, - uint32_t *pixelArray) { + uint32_t *pixelArray +) { uint32_t *aD, **aDSK; uint32_t idx; uint32_t i, j; @@ -962,7 +974,7 @@ map_image_pixels_from_median_box( continue; } if (!hashtable_lookup(medianBoxHash, pixelData[i], &pixelVal)) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("pixel lookup failed\n"); #endif return 0; @@ -998,7 +1010,9 @@ compute_palette_from_median_cut( uint32_t nPixels, HashTable *medianBoxHash, Pixel **palette, - uint32_t nPaletteEntries) { + uint32_t nPaletteEntries, + BoxNode *root +) { uint32_t i; uint32_t paletteEntry; Pixel *p; @@ -1026,7 +1040,7 @@ compute_palette_from_median_cut( } } for (i = 0; i < nPixels; i++) { -#ifdef TEST_SPLIT_INTEGRITY +#ifdef DEBUG if (!(i % 100)) { printf("%05d\r", i); fflush(stdout); @@ -1041,7 +1055,7 @@ compute_palette_from_median_cut( } #endif if (!hashtable_lookup(medianBoxHash, pixelData[i], &paletteEntry)) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("pixel lookup failed\n"); #endif for (i = 0; i < 3; i++) { @@ -1051,11 +1065,12 @@ compute_palette_from_median_cut( return 0; } if (paletteEntry >= nPaletteEntries) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf( "panic - paletteEntry>=nPaletteEntries (%d>=%d)\n", (int)paletteEntry, - (int)nPaletteEntries); + (int)nPaletteEntries + ); #endif for (i = 0; i < 3; i++) { free(avg[i]); @@ -1092,7 +1107,8 @@ compute_palette_from_median_cut( static int recompute_palette_from_averages( - Pixel *palette, uint32_t nPaletteEntries, uint32_t *avg[3], uint32_t *count) { + Pixel *palette, uint32_t nPaletteEntries, uint32_t *avg[3], uint32_t *count +) { uint32_t i; for (i = 0; i < nPaletteEntries; i++) { @@ -1111,7 +1127,8 @@ compute_palette_from_quantized_pixels( uint32_t nPaletteEntries, uint32_t *avg[3], uint32_t *count, - uint32_t *qp) { + uint32_t *qp +) { uint32_t i; memset(count, 0, sizeof(uint32_t) * nPaletteEntries); @@ -1120,7 +1137,7 @@ compute_palette_from_quantized_pixels( } for (i = 0; i < nPixels; i++) { if (qp[i] >= nPaletteEntries) { -#ifndef NO_OUTPUT +#ifdef DEBUG printf("scream\n"); #endif return 0; @@ -1145,7 +1162,8 @@ k_means( Pixel *paletteData, uint32_t nPaletteEntries, uint32_t *qp, - int threshold) { + int threshold +) { uint32_t *avg[3]; uint32_t *count; uint32_t i; @@ -1187,23 +1205,26 @@ k_means( goto error_2; } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("["); fflush(stdout); #endif while (1) { if (!built) { compute_palette_from_quantized_pixels( - pixelData, nPixels, paletteData, nPaletteEntries, avg, count, qp); + pixelData, nPixels, paletteData, nPaletteEntries, avg, count, qp + ); if (!build_distance_tables( - avgDist, avgDistSortKey, paletteData, nPaletteEntries)) { + 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); + avgDist, avgDistSortKey, paletteData, nPaletteEntries + ); } changes = map_image_pixels_from_quantized_pixels( pixelData, @@ -1214,11 +1235,12 @@ k_means( avgDistSortKey, qp, avg, - count); + count + ); if (changes < 0) { goto error_3; } -#ifndef NO_OUTPUT +#ifdef DEBUG printf(".(%d)", changes); fflush(stdout); #endif @@ -1226,7 +1248,7 @@ k_means( break; } } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("]\n"); #endif if (avgDistSortKey) { @@ -1273,7 +1295,8 @@ quantize( Pixel **palette, uint32_t *paletteLength, uint32_t **quantizedPixels, - int kmeans) { + int kmeans +) { PixelList *hl[3]; HashTable *h; BoxNode *root; @@ -1285,32 +1308,32 @@ quantize( uint32_t **avgDistSortKey; Pixel *p; -#ifndef NO_OUTPUT +#ifdef DEBUG uint32_t timer, timer2; #endif -#ifndef NO_OUTPUT +#ifdef DEBUG timer2 = clock(); printf("create hash table..."); fflush(stdout); timer = clock(); #endif h = create_pixel_hash(pixelData, nPixels); -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif if (!h) { goto error_0; } -#ifndef NO_OUTPUT +#ifdef DEBUG 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); -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif @@ -1318,7 +1341,7 @@ quantize( goto error_1; } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("mergesort lists..."); fflush(stdout); timer = clock(); @@ -1326,47 +1349,47 @@ quantize( for (i = 0; i < 3; i++) { hl[i] = mergesort_pixels(hl[i], i); } -#ifdef TEST_MERGESORT +#ifdef DEBUG 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); #endif -#ifndef NO_OUTPUT +#ifdef DEBUG printf("median cut..."); fflush(stdout); timer = clock(); #endif root = median_cut(hl, nPixels, nQuantPixels); -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif if (!root) { goto error_1; } nPaletteEntries = 0; -#ifndef NO_OUTPUT +#ifdef DEBUG printf("median cut tree to hash table..."); fflush(stdout); timer = clock(); #endif annotate_hash_table(root, h, &nPaletteEntries); -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif -#ifndef NO_OUTPUT +#ifdef DEBUG printf("compute palette...\n"); fflush(stdout); timer = clock(); #endif - if (!compute_palette_from_median_cut(pixelData, nPixels, h, &p, nPaletteEntries)) { + if (!compute_palette_from_median_cut( + pixelData, nPixels, h, &p, nPaletteEntries, root + )) { goto error_3; } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif @@ -1399,7 +1422,8 @@ quantize( } if (!map_image_pixels_from_median_box( - pixelData, nPixels, p, nPaletteEntries, h, avgDist, avgDistSortKey, qp)) { + pixelData, nPixels, p, nPaletteEntries, h, avgDist, avgDistSortKey, qp + )) { goto error_7; } @@ -1445,13 +1469,14 @@ quantize( _SQR(pixelData[i].c.b - p[qp[i]].c.b))), sqrt((double)(_SQR(pixelData[i].c.r - p[bestmatch].c.r) + _SQR(pixelData[i].c.g - p[bestmatch].c.g) + - _SQR(pixelData[i].c.b - p[bestmatch].c.b)))); + _SQR(pixelData[i].c.b - p[bestmatch].c.b))) + ); } } hashtable_free(h2); } #endif -#ifndef NO_OUTPUT +#ifdef DEBUG printf("k means...\n"); fflush(stdout); timer = clock(); @@ -1459,7 +1484,7 @@ quantize( if (kmeans > 0) { k_means(pixelData, nPixels, p, nPaletteEntries, qp, kmeans - 1); } -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); #endif @@ -1467,7 +1492,7 @@ quantize( *palette = p; *paletteLength = nPaletteEntries; -#ifndef NO_OUTPUT +#ifdef DEBUG printf("cleanup..."); fflush(stdout); timer = clock(); @@ -1479,7 +1504,7 @@ quantize( free(avgDistSortKey); } destroy_pixel_hash(h); -#ifndef NO_OUTPUT +#ifdef DEBUG printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); printf("-----\ntotal time %f\n", (clock() - timer2) / (double)CLOCKS_PER_SEC); #endif @@ -1545,7 +1570,8 @@ quantize2( Pixel **palette, uint32_t *paletteLength, uint32_t **quantizedPixels, - int kmeans) { + int kmeans +) { HashTable *h; uint32_t i; uint32_t mean[3]; @@ -1609,7 +1635,8 @@ quantize2( } if (!map_image_pixels( - pixelData, nPixels, p, nQuantPixels, avgDist, avgDistSortKey, qp)) { + pixelData, nPixels, p, nQuantPixels, avgDist, avgDistSortKey, qp + )) { goto error_4; } if (kmeans > 0) { @@ -1657,13 +1684,13 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { 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) { + if (im->mode != IMAGING_MODE_L && im->mode != IMAGING_MODE_P && + im->mode != IMAGING_MODE_RGB && im->mode != IMAGING_MODE_RGBA) { return ImagingError_ModeError(); } /* only octree and imagequant supports RGBA */ - if (!strcmp(im->mode, "RGBA") && mode != 2 && mode != 3) { + if (im->mode == IMAGING_MODE_RGBA && mode != 2 && mode != 3) { return ImagingError_ModeError(); } @@ -1681,7 +1708,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { /* FIXME: maybe we could load the hash tables directly from the image data? */ - if (!strcmp(im->mode, "L")) { + if (im->mode == IMAGING_MODE_L) { /* grayscale */ /* FIXME: converting a "L" image to "P" with 256 colors @@ -1694,7 +1721,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { } } - } else if (!strcmp(im->mode, "P")) { + } else if (im->mode == IMAGING_MODE_P) { /* palette */ pp = im->palette->palette; @@ -1709,28 +1736,32 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { } } - } else if (!strcmp(im->mode, "RGB") || !strcmp(im->mode, "RGBA")) { + } else if (im->mode == IMAGING_MODE_RGB || im->mode == IMAGING_MODE_RGBA) { /* true colour */ - withAlpha = !strcmp(im->mode, "RGBA"); + withAlpha = im->mode == IMAGING_MODE_RGBA; int transparency = 0; unsigned char r = 0, g = 0, b = 0; for (i = y = 0; y < im->ysize; y++) { for (x = 0; x < im->xsize; x++, i++) { p[i].v = im->image32[y][x]; - 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; + if (withAlpha) { + if (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 { + p[i].c.a = 255; } } } @@ -1752,7 +1783,8 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { &palette, &paletteLength, &newData, - kmeans); + kmeans + ); break; case 1: /* maximum coverage */ @@ -1763,7 +1795,8 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { &palette, &paletteLength, &newData, - kmeans); + kmeans + ); break; case 2: result = quantize_octree( @@ -1773,7 +1806,8 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { &palette, &paletteLength, &newData, - withAlpha); + withAlpha + ); break; case 3: #ifdef HAVE_LIBIMAGEQUANT @@ -1785,7 +1819,8 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { &palette, &paletteLength, &newData, - withAlpha); + withAlpha + ); #else result = -1; #endif @@ -1799,7 +1834,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { ImagingSectionLeave(&cookie); if (result > 0) { - imOut = ImagingNewDirty("P", im->xsize, im->ysize); + imOut = ImagingNewDirty(IMAGING_MODE_P, im->xsize, im->ysize); ImagingSectionEnter(&cookie); for (i = y = 0; y < im->ysize; y++) { @@ -1824,7 +1859,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { } if (withAlpha) { - strcpy(imOut->palette->mode, "RGBA"); + imOut->palette->mode = IMAGING_MODE_RGBA; } free(palette); @@ -1836,7 +1871,8 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { if (result == -1) { return (Imaging)ImagingError_ValueError( "dependency required by this method was not " - "enabled at compile time"); + "enabled at compile time" + ); } return (Imaging)ImagingError_ValueError("quantization error"); diff --git a/src/libImaging/QuantHash.c b/src/libImaging/QuantHash.c index ea75d6037f9..bf2f29fde85 100644 --- a/src/libImaging/QuantHash.c +++ b/src/libImaging/QuantHash.c @@ -132,7 +132,8 @@ _hashtable_resize(HashTable *h) { static int _hashtable_insert_node( - HashTable *h, HashNode *node, int resize, int update, CollisionFunc cf) { + 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; @@ -207,7 +208,8 @@ _hashtable_insert(HashTable *h, HashKey_t key, HashVal_t val, int resize, int up int hashtable_insert_or_update_computed( - HashTable *h, HashKey_t key, ComputeFunc newFunc, ComputeFunc existsFunc) { + HashTable *h, HashKey_t key, ComputeFunc newFunc, ComputeFunc existsFunc +) { HashNode **n, *nv; HashNode *t; int i; diff --git a/src/libImaging/QuantHash.h b/src/libImaging/QuantHash.h index fc1a9900376..d75d55ce0e4 100644 --- a/src/libImaging/QuantHash.h +++ b/src/libImaging/QuantHash.h @@ -21,12 +21,15 @@ 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 *); + const HashTable *, const HashKey_t, const HashVal_t, void * +); typedef void (*IteratorUpdateFunc)( - const HashTable *, const HashKey_t, HashVal_t *, void *); + 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); + const HashTable *, HashKey_t *, HashVal_t *, HashKey_t, HashVal_t +); HashTable * hashtable_new(HashFunc hf, HashCmpFunc cf); @@ -42,7 +45,8 @@ 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); + HashTable *h, HashKey_t key, ComputeFunc newFunc, ComputeFunc existsFunc +); void * hashtable_set_user_data(HashTable *h, void *data); void * diff --git a/src/libImaging/QuantOctree.c b/src/libImaging/QuantOctree.c index 1331a30ad45..ec60b214ef1 100644 --- a/src/libImaging/QuantOctree.c +++ b/src/libImaging/QuantOctree.c @@ -107,11 +107,8 @@ free_color_cube(ColorCube cube) { static long color_bucket_offset_pos( - const ColorCube cube, - unsigned int r, - unsigned int g, - unsigned int b, - unsigned int a) { + 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; } @@ -191,7 +188,8 @@ create_sorted_color_palette(const ColorCube cube) { buckets, cube->size, sizeof(struct _ColorBucket), - (int (*)(void const *, void const *)) & compare_bucket_count); + (int (*)(void const *, void const *))&compare_bucket_count + ); return buckets; } @@ -212,7 +210,8 @@ copy_color_cube( unsigned int rBits, unsigned int gBits, unsigned int bBits, - unsigned int aBits) { + unsigned int aBits +) { unsigned int r, g, b, a; long src_pos, dst_pos; unsigned int src_reduce[4] = {0}, dst_reduce[4] = {0}; @@ -262,15 +261,18 @@ copy_color_cube( r >> src_reduce[0], g >> src_reduce[1], b >> src_reduce[2], - a >> src_reduce[3]); + 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]); + a >> dst_reduce[3] + ); add_bucket_values( - &cube->buckets[src_pos], &result->buckets[dst_pos]); + &cube->buckets[src_pos], &result->buckets[dst_pos] + ); } } } @@ -328,7 +330,8 @@ combined_palette( ColorBucket bucketsA, unsigned long nBucketsA, ColorBucket bucketsB, - unsigned long nBucketsB) { + unsigned long nBucketsB +) { ColorBucket result; if (nBucketsA > LONG_MAX - nBucketsB || (nBucketsA + nBucketsB) > LONG_MAX / sizeof(struct _ColorBucket)) { @@ -366,7 +369,8 @@ map_image_pixels( const Pixel *pixelData, uint32_t nPixels, const ColorCube lookupCube, - uint32_t *pixelArray) { + uint32_t *pixelArray +) { long i; for (i = 0; i < nPixels; i++) { pixelArray[i] = lookup_color(lookupCube, &pixelData[i]); @@ -384,7 +388,8 @@ quantize_octree( Pixel **palette, uint32_t *paletteLength, uint32_t **quantizedPixels, - int withAlpha) { + int withAlpha +) { ColorCube fineCube = NULL; ColorCube coarseCube = NULL; ColorCube lookupCube = NULL; @@ -461,7 +466,8 @@ quantize_octree( subtract_color_buckets( coarseCube, &paletteBucketsFine[nAlreadySubtracted], - nFineColors - nAlreadySubtracted); + nFineColors - nAlreadySubtracted + ); } /* create our palette buckets with fine and coarse combined */ @@ -470,7 +476,8 @@ quantize_octree( goto error; } paletteBuckets = combined_palette( - paletteBucketsCoarse, nCoarseColors, paletteBucketsFine, nFineColors); + paletteBucketsCoarse, nCoarseColors, paletteBucketsFine, nFineColors + ); free(paletteBucketsFine); paletteBucketsFine = NULL; @@ -491,7 +498,8 @@ quantize_octree( /* 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]); + coarseLookupCube, cubeBits[0], cubeBits[1], cubeBits[2], cubeBits[3] + ); if (!lookupCube) { goto error; } diff --git a/src/libImaging/QuantPngQuant.c b/src/libImaging/QuantPngQuant.c index 7a36300e408..a2258c3a289 100644 --- a/src/libImaging/QuantPngQuant.c +++ b/src/libImaging/QuantPngQuant.c @@ -26,7 +26,8 @@ quantize_pngquant( Pixel **palette, uint32_t *paletteLength, uint32_t **quantizedPixels, - int withAlpha) { + int withAlpha +) { int result = 0; liq_image *image = NULL; liq_attr *attr = NULL; diff --git a/src/libImaging/QuantPngQuant.h b/src/libImaging/QuantPngQuant.h index d65e42590ca..ae96a52f371 100644 --- a/src/libImaging/QuantPngQuant.h +++ b/src/libImaging/QuantPngQuant.h @@ -12,6 +12,7 @@ quantize_pngquant( Pixel **, uint32_t *, uint32_t **, - int); + int +); #endif diff --git a/src/libImaging/RankFilter.c b/src/libImaging/RankFilter.c index 73a6baecbb2..899b1fd3a29 100644 --- a/src/libImaging/RankFilter.c +++ b/src/libImaging/RankFilter.c @@ -102,7 +102,8 @@ MakeRankFunction(UINT8) MakeRankFunction(INT32) MakeRankFunction(FLOAT32) memcpy( \ buf + i * size, \ &IMAGING_PIXEL_##type(im, x, y + i), \ - size * sizeof(type)); \ + size * sizeof(type) \ + ); \ } \ IMAGING_PIXEL_##type(imOut, x, y) = Rank##type(buf, size2, rank); \ } \ diff --git a/src/libImaging/RawDecode.c b/src/libImaging/RawDecode.c index 24abe48041f..80ed0d6880a 100644 --- a/src/libImaging/RawDecode.c +++ b/src/libImaging/RawDecode.c @@ -74,7 +74,8 @@ ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt state->shuffle( (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, ptr, - state->xsize); + state->xsize + ); ptr += state->bytes; bytes -= state->bytes; diff --git a/src/libImaging/RawEncode.c b/src/libImaging/RawEncode.c index 50de8d98275..5e60e1106b9 100644 --- a/src/libImaging/RawEncode.c +++ b/src/libImaging/RawEncode.c @@ -65,7 +65,8 @@ ImagingRawEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { state->shuffle( ptr, (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); + state->xsize + ); if (state->bytes > state->count) { /* zero-pad the buffer, if necessary */ diff --git a/src/libImaging/Reduce.c b/src/libImaging/Reduce.c index 61566f0c506..a4e58ced81b 100644 --- a/src/libImaging/Reduce.c +++ b/src/libImaging/Reduce.c @@ -82,7 +82,8 @@ ImagingReduceNxN(Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale } } v = MAKE_UINT32( - (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24); + (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else if (imIn->bands == 3) { @@ -124,7 +125,8 @@ ImagingReduceNxN(Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - 0); + 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -171,7 +173,8 @@ ImagingReduceNxN(Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); + (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -226,7 +229,8 @@ ImagingReduce1xN(Imaging imOut, Imaging imIn, int box[4], int yscale) { ss3 += line[xx * 4 + 3]; } v = MAKE_UINT32( - (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24); + (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else if (imIn->bands == 3) { @@ -251,7 +255,8 @@ ImagingReduce1xN(Imaging imOut, Imaging imIn, int box[4], int yscale) { (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - 0); + 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -278,7 +283,8 @@ ImagingReduce1xN(Imaging imOut, Imaging imIn, int box[4], int yscale) { (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); + (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -329,7 +335,8 @@ ImagingReduceNx1(Imaging imOut, Imaging imIn, int box[4], int xscale) { ss3 += line[xx * 4 + 3]; } v = MAKE_UINT32( - (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24); + (ss0 * multiplier) >> 24, 0, 0, (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else if (imIn->bands == 3) { @@ -351,7 +358,8 @@ ImagingReduceNx1(Imaging imOut, Imaging imIn, int box[4], int xscale) { (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - 0); + 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -375,7 +383,8 @@ ImagingReduceNx1(Imaging imOut, Imaging imIn, int box[4], int xscale) { (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); + (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -425,7 +434,8 @@ ImagingReduce1x2(Imaging imOut, Imaging imIn, int box[4]) { 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); + (ss0 + amend) >> 1, (ss1 + amend) >> 1, (ss2 + amend) >> 1, 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -440,7 +450,8 @@ ImagingReduce1x2(Imaging imOut, Imaging imIn, int box[4]) { (ss0 + amend) >> 1, (ss1 + amend) >> 1, (ss2 + amend) >> 1, - (ss3 + amend) >> 1); + (ss3 + amend) >> 1 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -488,7 +499,8 @@ ImagingReduce2x1(Imaging imOut, Imaging imIn, int box[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); + (ss0 + amend) >> 1, (ss1 + amend) >> 1, (ss2 + amend) >> 1, 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -503,7 +515,8 @@ ImagingReduce2x1(Imaging imOut, Imaging imIn, int box[4]) { (ss0 + amend) >> 1, (ss1 + amend) >> 1, (ss2 + amend) >> 1, - (ss3 + amend) >> 1); + (ss3 + amend) >> 1 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -558,7 +571,8 @@ ImagingReduce2x2(Imaging imOut, Imaging imIn, int box[4]) { 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); + (ss0 + amend) >> 2, (ss1 + amend) >> 2, (ss2 + amend) >> 2, 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -577,7 +591,8 @@ ImagingReduce2x2(Imaging imOut, Imaging imIn, int box[4]) { (ss0 + amend) >> 2, (ss1 + amend) >> 2, (ss2 + amend) >> 2, - (ss3 + amend) >> 2); + (ss3 + amend) >> 2 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -623,7 +638,8 @@ ImagingReduce1x3(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, 0, 0, - ((ss3 + amend) * multiplier) >> 24); + ((ss3 + amend) * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else if (imIn->bands == 3) { @@ -637,7 +653,8 @@ ImagingReduce1x3(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, ((ss2 + amend) * multiplier) >> 24, - 0); + 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -652,7 +669,8 @@ ImagingReduce1x3(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, ((ss2 + amend) * multiplier) >> 24, - ((ss3 + amend) * multiplier) >> 24); + ((ss3 + amend) * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -694,7 +712,8 @@ ImagingReduce3x1(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, 0, 0, - ((ss3 + amend) * multiplier) >> 24); + ((ss3 + amend) * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else if (imIn->bands == 3) { @@ -708,7 +727,8 @@ ImagingReduce3x1(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, ((ss2 + amend) * multiplier) >> 24, - 0); + 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -723,7 +743,8 @@ ImagingReduce3x1(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, ((ss2 + amend) * multiplier) >> 24, - ((ss3 + amend) * multiplier) >> 24); + ((ss3 + amend) * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -775,7 +796,8 @@ ImagingReduce3x3(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, 0, 0, - ((ss3 + amend) * multiplier) >> 24); + ((ss3 + amend) * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else if (imIn->bands == 3) { @@ -795,7 +817,8 @@ ImagingReduce3x3(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, ((ss2 + amend) * multiplier) >> 24, - 0); + 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -818,7 +841,8 @@ ImagingReduce3x3(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, ((ss2 + amend) * multiplier) >> 24, - ((ss3 + amend) * multiplier) >> 24); + ((ss3 + amend) * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -900,7 +924,8 @@ ImagingReduce4x4(Imaging imOut, Imaging imIn, int box[4]) { 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); + (ss0 + amend) >> 4, (ss1 + amend) >> 4, (ss2 + amend) >> 4, 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -935,7 +960,8 @@ ImagingReduce4x4(Imaging imOut, Imaging imIn, int box[4]) { (ss0 + amend) >> 4, (ss1 + amend) >> 4, (ss2 + amend) >> 4, - (ss3 + amend) >> 4); + (ss3 + amend) >> 4 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -1007,7 +1033,8 @@ ImagingReduce5x5(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, 0, 0, - ((ss3 + amend) * multiplier) >> 24); + ((ss3 + amend) * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else if (imIn->bands == 3) { @@ -1045,7 +1072,8 @@ ImagingReduce5x5(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, ((ss2 + amend) * multiplier) >> 24, - 0); + 0 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } else { // bands == 4 @@ -1092,7 +1120,8 @@ ImagingReduce5x5(Imaging imOut, Imaging imIn, int box[4]) { ((ss0 + amend) * multiplier) >> 24, ((ss1 + amend) * multiplier) >> 24, ((ss2 + amend) * multiplier) >> 24, - ((ss3 + amend) * multiplier) >> 24); + ((ss3 + amend) * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -1181,7 +1210,8 @@ ImagingReduceCorners(Imaging imOut, Imaging imIn, int box[4], int xscale, int ys (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); + (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -1207,7 +1237,8 @@ ImagingReduceCorners(Imaging imOut, Imaging imIn, int box[4], int xscale, int ys (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); + (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -1232,7 +1263,8 @@ ImagingReduceCorners(Imaging imOut, Imaging imIn, int box[4], int xscale, int ys (ss0 * multiplier) >> 24, (ss1 * multiplier) >> 24, (ss2 * multiplier) >> 24, - (ss3 * multiplier) >> 24); + (ss3 * multiplier) >> 24 + ); memcpy(imOut->image[y] + x * sizeof(v), &v, sizeof(v)); } } @@ -1240,7 +1272,8 @@ ImagingReduceCorners(Imaging imOut, Imaging imIn, int box[4], int xscale, int ys void ImagingReduceNxN_32bpc( - Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) { + 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; @@ -1313,7 +1346,8 @@ ImagingReduceNxN_32bpc( void ImagingReduceCorners_32bpc( - Imaging imOut, Imaging imIn, int box[4], int xscale, int yscale) { + 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; @@ -1418,7 +1452,7 @@ 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) { + if (imIn->mode == IMAGING_MODE_P || imIn->mode == IMAGING_MODE_1) { return (Imaging)ImagingError_ModeError(); } @@ -1427,7 +1461,8 @@ ImagingReduce(Imaging imIn, int xscale, int yscale, int box[4]) { } imOut = ImagingNewDirty( - imIn->mode, (box[2] + xscale - 1) / xscale, (box[3] + yscale - 1) / yscale); + imIn->mode, (box[2] + xscale - 1) / xscale, (box[3] + yscale - 1) / yscale + ); if (!imOut) { return NULL; } diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index 59c27b3f421..cbd18d0c116 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -186,7 +186,8 @@ precompute_coeffs( int outSize, struct filter *filterp, int **boundsp, - double **kkp) { + double **kkp +) { double support, scale, filterscale; double center, ww, ss; int xx, x, ksize, xmin, xmax; @@ -284,7 +285,8 @@ normalize_coeffs_8bpc(int outSize, int ksize, double *prekk) { void ImagingResampleHorizontal_8bpc( - Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *prekk) { + 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; @@ -376,7 +378,8 @@ ImagingResampleHorizontal_8bpc( void ImagingResampleVertical_8bpc( - Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *prekk) { + 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; @@ -457,9 +460,89 @@ ImagingResampleVertical_8bpc( ImagingSectionLeave(&cookie); } +void +ImagingResampleHorizontal_16bpc( + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk +) { + ImagingSectionCookie cookie; + double ss; + int xx, yy, x, xmin, xmax, ss_int; + double *k; + + int bigendian = 0; + if ( + imIn->mode == IMAGING_MODE_I_16N +#ifdef WORDS_BIGENDIAN + || imIn->mode == IMAGING_MODE_I_16B +#endif + ) { + bigendian = 1; + } + + ImagingSectionEnter(&cookie); + for (yy = 0; yy < imOut->ysize; yy++) { + for (xx = 0; xx < imOut->xsize; xx++) { + xmin = bounds[xx * 2 + 0]; + xmax = bounds[xx * 2 + 1]; + k = &kk[xx * ksize]; + ss = 0.0; + for (x = 0; x < xmax; x++) { + ss += (imIn->image8[yy + offset][(x + xmin) * 2 + (bigendian ? 1 : 0)] + + (imIn->image8[yy + offset][(x + xmin) * 2 + (bigendian ? 0 : 1)] + << 8)) * + k[x]; + } + ss_int = ROUND_UP(ss); + imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256); + imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8); + } + } + ImagingSectionLeave(&cookie); +} + +void +ImagingResampleVertical_16bpc( + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk +) { + ImagingSectionCookie cookie; + double ss; + int xx, yy, y, ymin, ymax, ss_int; + double *k; + + int bigendian = 0; + if ( + imIn->mode == IMAGING_MODE_I_16N +#ifdef WORDS_BIGENDIAN + || imIn->mode == IMAGING_MODE_I_16B +#endif + ) { + bigendian = 1; + } + + ImagingSectionEnter(&cookie); + for (yy = 0; yy < imOut->ysize; yy++) { + ymin = bounds[yy * 2 + 0]; + ymax = bounds[yy * 2 + 1]; + k = &kk[yy * ksize]; + for (xx = 0; xx < imOut->xsize; xx++) { + ss = 0.0; + for (y = 0; y < ymax; y++) { + ss += (imIn->image8[y + ymin][xx * 2 + (bigendian ? 1 : 0)] + + (imIn->image8[y + ymin][xx * 2 + (bigendian ? 0 : 1)] << 8)) * + k[y]; + } + ss_int = ROUND_UP(ss); + imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256); + imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8); + } + } + ImagingSectionLeave(&cookie); +} + void ImagingResampleHorizontal_32bpc( - Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk) { + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk +) { ImagingSectionCookie cookie; double ss; int xx, yy, x, xmin, xmax; @@ -502,7 +585,8 @@ ImagingResampleHorizontal_32bpc( void ImagingResampleVertical_32bpc( - Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk) { + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk +) { ImagingSectionCookie cookie; double ss; int xx, yy, y, ymin, ymax; @@ -544,7 +628,8 @@ ImagingResampleVertical_32bpc( } typedef void (*ResampleFunction)( - Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk); + Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk +); Imaging ImagingResampleInner( @@ -554,7 +639,8 @@ ImagingResampleInner( struct filter *filterp, float box[4], ResampleFunction ResampleHorizontal, - ResampleFunction ResampleVertical); + ResampleFunction ResampleVertical +); Imaging ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]) { @@ -562,12 +648,17 @@ ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]) { ResampleFunction ResampleHorizontal; ResampleFunction ResampleVertical; - if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "1") == 0) { + if (imIn->mode == IMAGING_MODE_P || imIn->mode == IMAGING_MODE_1) { return (Imaging)ImagingError_ModeError(); } if (imIn->type == IMAGING_TYPE_SPECIAL) { - return (Imaging)ImagingError_ModeError(); + if (isModeI16(imIn->mode)) { + ResampleHorizontal = ImagingResampleHorizontal_16bpc; + ResampleVertical = ImagingResampleVertical_16bpc; + } else { + return (Imaging)ImagingError_ModeError(); + } } else if (imIn->image8) { ResampleHorizontal = ImagingResampleHorizontal_8bpc; ResampleVertical = ImagingResampleVertical_8bpc; @@ -609,7 +700,8 @@ ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]) { } return ImagingResampleInner( - imIn, xsize, ysize, filterp, box, ResampleHorizontal, ResampleVertical); + imIn, xsize, ysize, filterp, box, ResampleHorizontal, ResampleVertical + ); } Imaging @@ -620,7 +712,8 @@ ImagingResampleInner( struct filter *filterp, float box[4], ResampleFunction ResampleHorizontal, - ResampleFunction ResampleVertical) { + ResampleFunction ResampleVertical +) { Imaging imTemp = NULL; Imaging imOut = NULL; @@ -634,13 +727,15 @@ ImagingResampleInner( 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); + 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); + imIn->ysize, box[1], box[3], ysize, filterp, &bounds_vert, &kk_vert + ); if (!ksize_vert) { free(bounds_horiz); free(kk_horiz); @@ -662,7 +757,8 @@ ImagingResampleInner( imTemp = ImagingNewDirty(imIn->mode, xsize, ybox_last - ybox_first); if (imTemp) { ResampleHorizontal( - imTemp, imIn, ybox_first, ksize_horiz, bounds_horiz, kk_horiz); + imTemp, imIn, ybox_first, ksize_horiz, bounds_horiz, kk_horiz + ); } free(bounds_horiz); free(kk_horiz); diff --git a/src/libImaging/SgiRleDecode.c b/src/libImaging/SgiRleDecode.c index 89dedb5252f..2f5268b80b8 100644 --- a/src/libImaging/SgiRleDecode.c +++ b/src/libImaging/SgiRleDecode.c @@ -22,7 +22,8 @@ static void read4B(UINT32 *dest, UINT8 *buf) { - *dest = (UINT32)((buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]); + *dest = ((UINT32)buf[0] << 24) | ((UINT32)buf[1] << 16) | ((UINT32)buf[2] << 8) | + buf[3]; } /* @@ -114,7 +115,8 @@ expandrow(UINT8 *dest, UINT8 *src, int n, int z, int xsize, UINT8 *end_of_buffer static int expandrow2( - UINT8 *dest, const UINT8 *src, int n, int z, int xsize, UINT8 *end_of_buffer) { + UINT8 *dest, const UINT8 *src, int n, int z, int xsize, UINT8 *end_of_buffer +) { UINT8 pixel, count; int x = 0; @@ -173,8 +175,15 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t /* Get all data from File descriptor */ c = (SGISTATE *)state->context; - _imaging_seek_pyFd(state->fd, 0L, SEEK_END); + if (_imaging_seek_pyFd(state->fd, 0L, SEEK_END) == -1) { + state->errcode = IMAGING_CODEC_UNKNOWN; + return -1; + } c->bufsize = _imaging_tell_pyFd(state->fd); + if (c->bufsize == -1) { + state->errcode = IMAGING_CODEC_UNKNOWN; + return -1; + } c->bufsize -= SGI_HEADER_SIZE; c->tablen = im->bands * im->ysize; @@ -182,7 +191,7 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t each with 4 bytes per element of tablen Check here before we allocate any memory */ - if (c->bufsize < 8 * c->tablen) { + if (c->bufsize < 8 * (int64_t)c->tablen) { state->errcode = IMAGING_CODEC_OVERRUN; return -1; } @@ -192,8 +201,9 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t state->errcode = IMAGING_CODEC_MEMORY; return -1; } - _imaging_seek_pyFd(state->fd, SGI_HEADER_SIZE, SEEK_SET); - if (_imaging_read_pyFd(state->fd, (char *)ptr, c->bufsize) != c->bufsize) { + if (_imaging_seek_pyFd(state->fd, SGI_HEADER_SIZE, SEEK_SET) == -1 || + _imaging_read_pyFd(state->fd, (char *)ptr, c->bufsize) != c->bufsize) { + free(ptr); state->errcode = IMAGING_CODEC_UNKNOWN; return -1; } @@ -252,7 +262,8 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t c->rlelength, im->bands, im->xsize, - &ptr[c->bufsize - 1]); + &ptr[c->bufsize - 1] + ); } else { status = expandrow2( &state->buffer[c->channo * 2], @@ -260,7 +271,8 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t c->rlelength, im->bands, im->xsize, - &ptr[c->bufsize - 1]); + &ptr[c->bufsize - 1] + ); } if (status == -1) { state->errcode = IMAGING_CODEC_OVERRUN; diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index b27195a3587..c8175612e35 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -42,7 +42,7 @@ */ Imaging -ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { +ImagingNewPrologueSubtype(const ModeID mode, int xsize, int ysize, int size) { Imaging im; /* linesize overflow check, roughly the current largest space req'd */ @@ -58,137 +58,165 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { /* Setup image descriptor */ im->xsize = xsize; im->ysize = ysize; - + im->refcount = 1; im->type = IMAGING_TYPE_UINT8; + strcpy(im->arrow_band_format, "C"); - if (strcmp(mode, "1") == 0) { + if (mode == IMAGING_MODE_1) { /* 1-bit images */ im->bands = im->pixelsize = 1; im->linesize = xsize; + strcpy(im->band_names[0], "1"); - } else if (strcmp(mode, "P") == 0) { + } else if (mode == IMAGING_MODE_P) { /* 8-bit palette mapped images */ im->bands = im->pixelsize = 1; im->linesize = xsize; - im->palette = ImagingPaletteNew("RGB"); + im->palette = ImagingPaletteNew(IMAGING_MODE_RGB); + strcpy(im->band_names[0], "P"); - } else if (strcmp(mode, "PA") == 0) { + } else if (mode == IMAGING_MODE_PA) { /* 8-bit palette with alpha */ im->bands = 2; im->pixelsize = 4; /* store in image32 memory */ im->linesize = xsize * 4; - im->palette = ImagingPaletteNew("RGB"); + im->palette = ImagingPaletteNew(IMAGING_MODE_RGB); + strcpy(im->band_names[0], "P"); + strcpy(im->band_names[1], "X"); + strcpy(im->band_names[2], "X"); + strcpy(im->band_names[3], "A"); - } else if (strcmp(mode, "L") == 0) { + } else if (mode == IMAGING_MODE_L) { /* 8-bit grayscale (luminance) images */ im->bands = im->pixelsize = 1; im->linesize = xsize; + strcpy(im->band_names[0], "L"); - } else if (strcmp(mode, "LA") == 0) { + } else if (mode == IMAGING_MODE_LA) { /* 8-bit grayscale (luminance) with alpha */ im->bands = 2; im->pixelsize = 4; /* store in image32 memory */ im->linesize = xsize * 4; + strcpy(im->band_names[0], "L"); + strcpy(im->band_names[1], "X"); + strcpy(im->band_names[2], "X"); + strcpy(im->band_names[3], "A"); - } else if (strcmp(mode, "La") == 0) { + } else if (mode == IMAGING_MODE_La) { /* 8-bit grayscale (luminance) with premultiplied alpha */ im->bands = 2; im->pixelsize = 4; /* store in image32 memory */ im->linesize = xsize * 4; + strcpy(im->band_names[0], "L"); + strcpy(im->band_names[1], "X"); + strcpy(im->band_names[2], "X"); + strcpy(im->band_names[3], "a"); - } else if (strcmp(mode, "F") == 0) { + } else if (mode == IMAGING_MODE_F) { /* 32-bit floating point images */ im->bands = 1; im->pixelsize = 4; im->linesize = xsize * 4; im->type = IMAGING_TYPE_FLOAT32; + strcpy(im->arrow_band_format, "f"); + strcpy(im->band_names[0], "F"); - } else if (strcmp(mode, "I") == 0) { + } else if (mode == IMAGING_MODE_I) { /* 32-bit integer images */ im->bands = 1; im->pixelsize = 4; im->linesize = xsize * 4; im->type = IMAGING_TYPE_INT32; + strcpy(im->arrow_band_format, "i"); + strcpy(im->band_names[0], "I"); - } else if ( - strcmp(mode, "I;16") == 0 || strcmp(mode, "I;16L") == 0 || - strcmp(mode, "I;16B") == 0 || strcmp(mode, "I;16N") == 0) { + } else if (isModeI16(mode)) { /* EXPERIMENTAL */ /* 16-bit raw integer images */ im->bands = 1; im->pixelsize = 2; im->linesize = xsize * 2; im->type = IMAGING_TYPE_SPECIAL; + strcpy(im->arrow_band_format, "s"); + strcpy(im->band_names[0], "I"); - } else if (strcmp(mode, "RGB") == 0) { + } else if (mode == IMAGING_MODE_RGB) { /* 24-bit true colour images */ im->bands = 3; im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "R"); + strcpy(im->band_names[1], "G"); + strcpy(im->band_names[2], "B"); + strcpy(im->band_names[3], "X"); - } else if (strcmp(mode, "BGR;15") == 0) { - /* EXPERIMENTAL */ - /* 15-bit reversed true colour */ - im->bands = 3; - im->pixelsize = 2; - im->linesize = (xsize * 2 + 3) & -4; - im->type = IMAGING_TYPE_SPECIAL; - - } else if (strcmp(mode, "BGR;16") == 0) { - /* EXPERIMENTAL */ - /* 16-bit reversed true colour */ - im->bands = 3; - im->pixelsize = 2; - im->linesize = (xsize * 2 + 3) & -4; - im->type = IMAGING_TYPE_SPECIAL; - - } else if (strcmp(mode, "BGR;24") == 0) { - /* EXPERIMENTAL */ - /* 24-bit reversed true colour */ - im->bands = 3; - im->pixelsize = 3; - im->linesize = (xsize * 3 + 3) & -4; - im->type = IMAGING_TYPE_SPECIAL; - - } else if (strcmp(mode, "RGBX") == 0) { + } else if (mode == IMAGING_MODE_RGBX) { /* 32-bit true colour images with padding */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "R"); + strcpy(im->band_names[1], "G"); + strcpy(im->band_names[2], "B"); + strcpy(im->band_names[3], "X"); - } else if (strcmp(mode, "RGBA") == 0) { + } else if (mode == IMAGING_MODE_RGBA) { /* 32-bit true colour images with alpha */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "R"); + strcpy(im->band_names[1], "G"); + strcpy(im->band_names[2], "B"); + strcpy(im->band_names[3], "A"); - } else if (strcmp(mode, "RGBa") == 0) { + } else if (mode == IMAGING_MODE_RGBa) { /* 32-bit true colour images with premultiplied alpha */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "R"); + strcpy(im->band_names[1], "G"); + strcpy(im->band_names[2], "B"); + strcpy(im->band_names[3], "a"); - } else if (strcmp(mode, "CMYK") == 0) { + } else if (mode == IMAGING_MODE_CMYK) { /* 32-bit colour separation */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "C"); + strcpy(im->band_names[1], "M"); + strcpy(im->band_names[2], "Y"); + strcpy(im->band_names[3], "K"); - } else if (strcmp(mode, "YCbCr") == 0) { + } else if (mode == IMAGING_MODE_YCbCr) { /* 24-bit video format */ im->bands = 3; im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "Y"); + strcpy(im->band_names[1], "Cb"); + strcpy(im->band_names[2], "Cr"); + strcpy(im->band_names[3], "X"); - } else if (strcmp(mode, "LAB") == 0) { + } else if (mode == IMAGING_MODE_LAB) { /* 24-bit color, luminance, + 2 color channels */ /* L is uint8, a,b are int8 */ im->bands = 3; im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "L"); + strcpy(im->band_names[1], "a"); + strcpy(im->band_names[2], "b"); + strcpy(im->band_names[3], "X"); - } else if (strcmp(mode, "HSV") == 0) { + } else if (mode == IMAGING_MODE_HSV) { /* 24-bit color, luminance, + 2 color channels */ /* L is uint8, a,b are int8 */ im->bands = 3; im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "H"); + strcpy(im->band_names[1], "S"); + strcpy(im->band_names[2], "V"); + strcpy(im->band_names[3], "X"); } else { free(im); @@ -196,7 +224,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { } /* Setup image descriptor */ - strcpy(im->mode, mode); + im->mode = mode; /* Pointer array (allocate at least one line, to avoid MemoryError exceptions on platforms where calloc(0, x) returns NULL) */ @@ -219,15 +247,19 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { break; } + // UNDONE -- not accurate for arrow + MUTEX_LOCK(&ImagingDefaultArena.mutex); ImagingDefaultArena.stats_new_count += 1; + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); return im; } Imaging -ImagingNewPrologue(const char *mode, int xsize, int ysize) { +ImagingNewPrologue(const ModeID mode, int xsize, int ysize) { return ImagingNewPrologueSubtype( - mode, xsize, ysize, sizeof(struct ImagingMemoryInstance)); + mode, xsize, ysize, sizeof(struct ImagingMemoryInstance) + ); } void @@ -236,8 +268,18 @@ ImagingDelete(Imaging im) { return; } + MUTEX_LOCK(&im->mutex); + im->refcount--; + + if (im->refcount > 0) { + MUTEX_UNLOCK(&im->mutex); + return; + } + MUTEX_UNLOCK(&im->mutex); + if (im->palette) { ImagingPaletteDelete(im->palette); + im->palette = NULL; } if (im->destroy) { @@ -267,7 +309,11 @@ struct ImagingMemoryArena ImagingDefaultArena = { 0, 0, 0, - 0 // Stats + 0, // Stats + 0, // use_block_allocator +#ifdef Py_GIL_DISABLED + {0}, +#endif }; int @@ -297,6 +343,11 @@ ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max) { return 1; } +void +ImagingMemorySetBlockAllocator(ImagingMemoryArena arena, int use_block_allocator) { + arena->use_block_allocator = use_block_allocator; +} + void ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size) { while (arena->blocks_cached > new_size) { @@ -364,18 +415,19 @@ ImagingDestroyArray(Imaging im) { int y = 0; if (im->blocks) { + MUTEX_LOCK(&ImagingDefaultArena.mutex); while (im->blocks[y].ptr) { memory_return_block(&ImagingDefaultArena, im->blocks[y]); y += 1; } + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); free(im->blocks); } } Imaging -ImagingAllocateArray(Imaging im, int dirty, int block_size) { +ImagingAllocateArray(Imaging im, ImagingMemoryArena arena, int dirty, int block_size) { int y, line_in_block, current_block; - ImagingMemoryArena arena = &ImagingDefaultArena; ImagingMemoryBlock block = {NULL, 0}; int aligned_linesize, lines_per_block, blocks_count; char *aligned_ptr = NULL; @@ -390,11 +442,13 @@ ImagingAllocateArray(Imaging im, int dirty, int block_size) { if (lines_per_block == 0) { lines_per_block = 1; } + im->lines_per_block = lines_per_block; 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 pointer is always NULL */ + im->blocks_count = blocks_count; im->blocks = calloc(sizeof(*im->blocks), blocks_count + 1); if (!im->blocks) { return (Imaging)ImagingError_MemoryError(); @@ -481,12 +535,65 @@ ImagingAllocateBlock(Imaging im) { return im; } +/* Borrowed Arrow Storage Type */ +/* --------------------------- */ +/* Don't allocate the image. */ + +static void +ImagingDestroyArrow(Imaging im) { + // Rely on the internal Python destructor for the array capsule. + if (im->arrow_array_capsule) { + Py_DECREF(im->arrow_array_capsule); + im->arrow_array_capsule = NULL; + } +} + +Imaging +ImagingBorrowArrow( + Imaging im, + struct ArrowArray *external_array, + int offset_width, + PyObject *arrow_capsule +) { + // offset_width is the number of char* for a single offset from arrow + Py_ssize_t y, i; + + char *borrowed_buffer = NULL; + struct ArrowArray *arr = external_array; + + if (arr->n_children == 1) { + arr = arr->children[0]; + } + if (arr->n_buffers == 2) { + // buffer 0 is the null list + // buffer 1 is the data + borrowed_buffer = (char *)arr->buffers[1] + (offset_width * arr->offset); + } + + if (!borrowed_buffer) { + return (Imaging)ImagingError_ValueError( + "Arrow Array, exactly 2 buffers required" + ); + } + + for (y = i = 0; y < im->ysize; y++) { + im->image[y] = borrowed_buffer + i; + i += im->linesize; + } + im->read_only = 1; + Py_INCREF(arrow_capsule); + im->arrow_array_capsule = arrow_capsule; + im->destroy = ImagingDestroyArrow; + + return im; +} + /* -------------------------------------------------------------------- * Create a new, internally allocated, image. */ Imaging -ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { +ImagingNewInternal(const ModeID mode, int xsize, int ysize, int dirty) { Imaging im; if (xsize < 0 || ysize < 0) { @@ -498,14 +605,22 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { return NULL; } - if (ImagingAllocateArray(im, dirty, ImagingDefaultArena.block_size)) { + MUTEX_LOCK(&ImagingDefaultArena.mutex); + Imaging tmp = ImagingAllocateArray( + im, &ImagingDefaultArena, dirty, ImagingDefaultArena.block_size + ); + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + if (tmp) { return im; } - ImagingError_Clear(); + PyErr_Clear(); // Try to allocate the image once more with smallest possible block size - if (ImagingAllocateArray(im, dirty, IMAGING_PAGE_SIZE)) { + MUTEX_LOCK(&ImagingDefaultArena.mutex); + tmp = ImagingAllocateArray(im, &ImagingDefaultArena, dirty, IMAGING_PAGE_SIZE); + MUTEX_UNLOCK(&ImagingDefaultArena.mutex); + if (tmp) { return im; } @@ -514,17 +629,23 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { } Imaging -ImagingNew(const char *mode, int xsize, int ysize) { +ImagingNew(const ModeID mode, int xsize, int ysize) { + if (ImagingDefaultArena.use_block_allocator) { + return ImagingNewBlock(mode, xsize, ysize); + } return ImagingNewInternal(mode, xsize, ysize, 0); } Imaging -ImagingNewDirty(const char *mode, int xsize, int ysize) { +ImagingNewDirty(const ModeID mode, int xsize, int ysize) { + if (ImagingDefaultArena.use_block_allocator) { + return ImagingNewBlock(mode, xsize, ysize); + } return ImagingNewInternal(mode, xsize, ysize, 1); } Imaging -ImagingNewBlock(const char *mode, int xsize, int ysize) { +ImagingNewBlock(const ModeID mode, int xsize, int ysize) { Imaging im; if (xsize < 0 || ysize < 0) { @@ -545,12 +666,92 @@ ImagingNewBlock(const char *mode, int xsize, int ysize) { } Imaging -ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn) { +ImagingNewArrow( + const ModeID mode, + int xsize, + int ysize, + PyObject *schema_capsule, + PyObject *array_capsule +) { + /* A borrowed arrow array */ + Imaging im; + struct ArrowSchema *schema = + (struct ArrowSchema *)PyCapsule_GetPointer(schema_capsule, "arrow_schema"); + if (!schema) { + return NULL; + } + + struct ArrowArray *external_array = + (struct ArrowArray *)PyCapsule_GetPointer(array_capsule, "arrow_array"); + if (!external_array) { + return NULL; + } + + if (xsize < 0 || ysize < 0) { + return (Imaging)ImagingError_ValueError("bad image size"); + } + + im = ImagingNewPrologue(mode, xsize, ysize); + if (!im) { + return NULL; + } + + int64_t pixels = (int64_t)xsize * (int64_t)ysize; + + // fmt:off // don't reformat this + // stored as a single array, one element per pixel, either single band + // or multiband, where each pixel is an I32. + if (((strcmp(schema->format, "I") == 0 // int32 + && im->pixelsize == 4 // 4xchar* storage + && im->bands >= 2) // INT32 into any INT32 Storage mode + || // (()||()) && + (strcmp(schema->format, im->arrow_band_format) == 0 // same mode + && im->bands == 1)) // Single band match + && pixels == external_array->length) { + // one arrow element per, and it matches a pixelsize*char + if (ImagingBorrowArrow(im, external_array, im->pixelsize, array_capsule)) { + return im; + } + } + // Stored as [[r,g,b,a],...] + if (strcmp(schema->format, "+w:4") == 0 // 4 up array + && im->pixelsize == 4 // storage as 32 bpc + && schema->n_children > 0 // make sure schema is well formed. + && schema->children // make sure schema is well formed + && strcmp(schema->children[0]->format, "C") == 0 // Expected format + && strcmp(im->arrow_band_format, "C") == 0 // Expected Format + && pixels == external_array->length // expected length + && external_array->n_children == 1 // array is well formed + && external_array->children // array is well formed + && 4 * pixels == external_array->children[0]->length) { + // 4 up element of char into pixelsize == 4 + if (ImagingBorrowArrow(im, external_array, 1, array_capsule)) { + return im; + } + } + // Stored as [r,g,b,a,r,g,b,a,...] + if (strcmp(schema->format, "C") == 0 // uint8 + && im->pixelsize == 4 // storage as 32 bpc + && schema->n_children == 0 // make sure schema is well formed. + && strcmp(im->arrow_band_format, "C") == 0 // expected format + && 4 * pixels == external_array->length) { // expected length + // single flat array, interleaved storage. + if (ImagingBorrowArrow(im, external_array, 1, array_capsule)) { + return im; + } + } + // fmt: on + ImagingDelete(im); + return NULL; +} + +Imaging +ImagingNew2Dirty(const ModeID 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 || + if (imOut->mode != mode || imOut->xsize != imIn->xsize || imOut->ysize != imIn->ysize) { return ImagingError_Mismatch(); } diff --git a/src/libImaging/SunRleDecode.c b/src/libImaging/SunRleDecode.c index 9d8e1292a47..d3231ad901f 100644 --- a/src/libImaging/SunRleDecode.c +++ b/src/libImaging/SunRleDecode.c @@ -107,7 +107,8 @@ ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, state->buffer, - state->xsize); + state->xsize + ); state->x = 0; diff --git a/src/libImaging/TgaRleDecode.c b/src/libImaging/TgaRleDecode.c index 95ae9b62228..fbf29452c8e 100644 --- a/src/libImaging/TgaRleDecode.c +++ b/src/libImaging/TgaRleDecode.c @@ -93,7 +93,8 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, state->buffer, - state->xsize); + state->xsize + ); state->x = 0; diff --git a/src/libImaging/TgaRleEncode.c b/src/libImaging/TgaRleEncode.c index aa7e7b96d81..dde4766143a 100644 --- a/src/libImaging/TgaRleEncode.c +++ b/src/libImaging/TgaRleEncode.c @@ -63,7 +63,8 @@ ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) state->buffer, (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); + state->xsize + ); } row = state->buffer; @@ -146,7 +147,8 @@ ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) } memcpy( - dst, state->buffer + (state->x * bytesPerPixel - state->count), flushCount); + dst, state->buffer + (state->x * bytesPerPixel - state->count), flushCount + ); dst += flushCount; bytes -= flushCount; diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index abffdeabcd5..72e0d7b309a 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -44,7 +44,8 @@ dump_state(const TIFFSTATE *state) { (int)state->size, (uint)state->eof, state->data, - state->ifd)); + state->ifd) + ); } /* @@ -64,7 +65,8 @@ _tiffReadProc(thandle_t hdata, tdata_t buf, tsize_t size) { "_tiffReadProc", "Invalid Read at loc %" PRIu64 ", eof: %" PRIu64, state->loc, - state->eof); + state->eof + ); return 0; } to_read = min(size, min(state->size, (tsize_t)state->eof) - (tsize_t)state->loc); @@ -200,13 +202,15 @@ ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset) { state->state, state->x, state->y, - state->ystep)); + state->ystep) + ); TRACE( ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", state->xsize, state->ysize, state->xoff, - state->yoff)); + state->yoff) + ); TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); TRACE(("State: context %p \n", state->context)); @@ -226,7 +230,8 @@ _pickUnpackers( ImagingCodecState state, TIFF *tiff, uint16_t planarconfig, - ImagingShuffler *unpackers) { + 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; @@ -241,14 +246,26 @@ _pickUnpackers( // 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); + unpackers[0] = ImagingFindUnpacker( + IMAGING_MODE_RGBA, + bits_per_sample == 16 ? IMAGING_RAWMODE_R_16N : IMAGING_RAWMODE_R, + NULL + ); + unpackers[1] = ImagingFindUnpacker( + IMAGING_MODE_RGBA, + bits_per_sample == 16 ? IMAGING_RAWMODE_G_16N : IMAGING_RAWMODE_G, + NULL + ); + unpackers[2] = ImagingFindUnpacker( + IMAGING_MODE_RGBA, + bits_per_sample == 16 ? IMAGING_RAWMODE_B_16N : IMAGING_RAWMODE_B, + NULL + ); + unpackers[3] = ImagingFindUnpacker( + IMAGING_MODE_RGBA, + bits_per_sample == 16 ? IMAGING_RAWMODE_A_16N : IMAGING_RAWMODE_A, + NULL + ); return im->bands; } else { @@ -294,6 +311,7 @@ _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) { return -1; } + img.orientation = ORIENTATION_TOPLEFT; img.req_orientation = ORIENTATION_TOPLEFT; img.col_offset = 0; @@ -356,7 +374,8 @@ _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) { (UINT8 *)im->image[state->y + state->yoff + current_row] + state->xoff * im->pixelsize, state->buffer + current_row * row_byte_size, - state->xsize); + state->xsize + ); } } @@ -374,7 +393,8 @@ _decodeTile( ImagingCodecState state, TIFF *tiff, int planes, - ImagingShuffler *unpackers) { + 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; @@ -453,7 +473,8 @@ _decodeTile( ("Writing tile data at %dx%d using tile_width: %d; \n", tile_y + y, x, - current_tile_width)); + 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], @@ -462,7 +483,8 @@ _decodeTile( shuffler( (UINT8 *)im->image[tile_y + y] + x * im->pixelsize, state->buffer + tile_y * row_byte_size, - current_tile_width); + current_tile_width + ); } } } @@ -477,7 +499,8 @@ _decodeStrip( ImagingCodecState state, TIFF *tiff, int planes, - ImagingShuffler *unpackers) { + ImagingShuffler *unpackers +) { INT32 strip_row = 0; UINT8 *new_data; UINT32 rows_per_strip; @@ -544,9 +567,11 @@ _decodeStrip( tiff, TIFFComputeStrip(tiff, state->y, plane), (tdata_t)state->buffer, - strip_size) == -1) { + strip_size + ) == -1) { TRACE( - ("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0))); + ("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0)) + ); state->errcode = IMAGING_CODEC_BROKEN; return -1; } @@ -567,7 +592,8 @@ _decodeStrip( (UINT8 *)im->image[state->y + state->yoff + strip_row] + state->xoff * im->pixelsize, state->buffer + strip_row * row_byte_size, - state->xsize); + state->xsize + ); } } } @@ -577,7 +603,8 @@ _decodeStrip( int ImagingLibTiffDecode( - Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes) { + Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes +) { TIFFSTATE *clientstate = (TIFFSTATE *)state->context; char *filename = "tempfile.tif"; char *mode = "rC"; @@ -602,13 +629,15 @@ ImagingLibTiffDecode( state->state, state->x, state->y, - state->ystep)); + state->ystep) + ); TRACE( ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", state->xsize, state->ysize, state->xoff, - state->yoff)); + state->yoff) + ); TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); TRACE( ("Buffer: %p: %c%c%c%c\n", @@ -616,26 +645,30 @@ ImagingLibTiffDecode( (char)buffer[0], (char)buffer[1], (char)buffer[2], - (char)buffer[3])); + (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])); + (char)state->buffer[3]) + ); TRACE( ("Image: mode %s, type %d, bands: %d, xsize %d, ysize %d \n", - im->mode, + getModeData(im->mode)->name, im->type, im->bands, im->xsize, - im->ysize)); + im->ysize) + ); TRACE( ("Image: image8 %p, image32 %p, image %p, block %p \n", im->image8, im->image32, im->image, - im->block)); + im->block) + ); TRACE(("Image: pixelsize: %d, linesize %d \n", im->pixelsize, im->linesize)); dump_state(clientstate); @@ -665,7 +698,8 @@ ImagingLibTiffDecode( _tiffCloseProc, _tiffSizeProc, _tiffMapProc, - _tiffUnmapProc); + _tiffUnmapProc + ); } if (!tiff) { @@ -694,7 +728,8 @@ ImagingLibTiffDecode( state->xsize, img_width, state->ysize, - img_height)); + img_height) + ); state->errcode = IMAGING_CODEC_BROKEN; goto decode_err; } @@ -732,18 +767,21 @@ ImagingLibTiffDecode( 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) { + if (planes > 3 && im->mode == IMAGING_MODE_RGBA) { uint16_t extrasamples; uint16_t *sampleinfo; ImagingShuffler shuffle; INT32 y; TIFFGetFieldDefaulted( - tiff, TIFFTAG_EXTRASAMPLES, &extrasamples, &sampleinfo); + tiff, TIFFTAG_EXTRASAMPLES, &extrasamples, &sampleinfo + ); if (extrasamples >= 1 && (sampleinfo[0] == EXTRASAMPLE_UNSPECIFIED || sampleinfo[0] == EXTRASAMPLE_ASSOCALPHA)) { - shuffle = ImagingFindUnpacker("RGBA", "RGBa", NULL); + shuffle = ImagingFindUnpacker( + IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa, NULL + ); for (y = state->yoff; y < state->ysize; y++) { UINT8 *ptr = (UINT8 *)im->image[y + state->yoff] + @@ -758,7 +796,7 @@ ImagingLibTiffDecode( decode_err: // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup if (clientstate->fp) { - // Pillow will manage the closing of the file rather than libtiff + // Python will manage the closing of the file rather than libtiff // So only call TIFFCleanup TIFFCleanup(tiff); } else { @@ -793,13 +831,15 @@ ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp) { state->state, state->x, state->y, - state->ystep)); + state->ystep) + ); TRACE( ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", state->xsize, state->ysize, state->xoff, - state->yoff)); + state->yoff) + ); TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); TRACE(("State: context %p \n", state->context)); @@ -839,7 +879,8 @@ ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp) { _tiffCloseProc, _tiffSizeProc, _tiffNullMapProc, - _tiffUnmapProc); /*force no mmap*/ + _tiffUnmapProc + ); /*force no mmap*/ } if (!clientstate->tiff) { @@ -852,11 +893,11 @@ ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp) { int ImagingLibTiffMergeFieldInfo( - ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length) { + 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; @@ -874,18 +915,12 @@ ImagingLibTiffMergeFieldInfo( FIELD_CUSTOM, 1, passcount, - "CustomField"}}; + "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; + return TIFFMergeFieldInfo(clientstate->tiff, info, n); } int @@ -901,6 +936,27 @@ ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...) { return status; } +int +ImagingLibTiffEncodeCleanup(ImagingCodecState state) { + TIFFSTATE *clientstate = (TIFFSTATE *)state->context; + TIFF *tiff = clientstate->tiff; + + if (!tiff) { + return 0; + } + // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup + if (clientstate->fp) { + // Python will manage the closing of the file rather than libtiff + // So only call TIFFCleanup + TIFFCleanup(tiff); + } else { + // When tif_closeproc refers to our custom _tiffCloseProc though, + // that is fine, as it does not close the file + TIFFClose(tiff); + } + return 0; +} + int ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes) { /* One shot encoder. Encode everything to the tiff in the clientstate. @@ -922,13 +978,15 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt state->state, state->x, state->y, - state->ystep)); + state->ystep) + ); TRACE( ("State: xsize %d, ysize %d, xoff %d, yoff %d \n", state->xsize, state->ysize, state->xoff, - state->yoff)); + state->yoff) + ); TRACE(("State: bits %d, bytes %d \n", state->bits, state->bytes)); TRACE( ("Buffer: %p: %c%c%c%c\n", @@ -936,26 +994,30 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt (char)buffer[0], (char)buffer[1], (char)buffer[2], - (char)buffer[3])); + (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])); + (char)state->buffer[3]) + ); TRACE( ("Image: mode %s, type %d, bands: %d, xsize %d, ysize %d \n", - im->mode, + getModeData(im->mode)->name, im->type, im->bands, im->xsize, - im->ysize)); + im->ysize) + ); TRACE( ("Image: image8 %p, image32 %p, image %p, block %p \n", im->image8, im->image32, im->image, - im->block)); + im->block) + ); TRACE(("Image: pixelsize: %d, linesize %d \n", im->pixelsize, im->linesize)); dump_state(clientstate); @@ -967,14 +1029,19 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt state->buffer, (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); + state->xsize + ); if (TIFFWriteScanline( - tiff, (tdata_t)(state->buffer), (uint32_t)state->y, 0) == -1) { + tiff, (tdata_t)(state->buffer), (uint32_t)state->y, 0 + ) == -1) { TRACE(("Encode Error, row %d\n", state->y)); state->errcode = IMAGING_CODEC_BROKEN; - TIFFClose(tiff); - if (!clientstate->fp) { + + if (clientstate->fp) { + TIFFCleanup(tiff); + clientstate->tiff = NULL; + } else { free(clientstate->data); } return -1; @@ -990,14 +1057,11 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt 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? @@ -1013,7 +1077,8 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt (char)buffer[0], (char)buffer[1], (char)buffer[2], - (char)buffer[3])); + (char)buffer[3]) + ); if (clientstate->loc == clientstate->eof) { TRACE(("Hit EOF, calling an end, freeing data")); state->errcode = IMAGING_CODEC_END; diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h index 212b7dee6b0..77808b543fc 100644 --- a/src/libImaging/TiffDecode.h +++ b/src/libImaging/TiffDecode.h @@ -40,8 +40,11 @@ ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset); extern int ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp); extern int +ImagingLibTiffEncodeCleanup(ImagingCodecState state); +extern int ImagingLibTiffMergeFieldInfo( - ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length); + ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length +); extern int ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...); diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index eaa4374e3e9..203bcac2ca9 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -104,7 +104,8 @@ static UINT8 BITFLIP[] = { 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}; + 15, 143, 79, 207, 47, 175, 111, 239, 31, 159, 95, 223, 63, 191, 127, 255 +}; /* Unpack to "1" image */ @@ -882,7 +883,8 @@ unpackRGBa16L(UINT8 *_out, const UINT8 *in, int pixels) { CLIP8(in[1] * 255 / a), CLIP8(in[3] * 255 / a), CLIP8(in[5] * 255 / a), - a); + a + ); } memcpy(_out, &iv, sizeof(iv)); in += 8; @@ -906,7 +908,8 @@ unpackRGBa16B(UINT8 *_out, const UINT8 *in, int pixels) { CLIP8(in[0] * 255 / a), CLIP8(in[2] * 255 / a), CLIP8(in[4] * 255 / a), - a); + a + ); } memcpy(_out, &iv, sizeof(iv)); in += 8; @@ -930,7 +933,8 @@ unpackRGBa(UINT8 *_out, const UINT8 *in, int pixels) { CLIP8(in[0] * 255 / a), CLIP8(in[1] * 255 / a), CLIP8(in[2] * 255 / a), - a); + a + ); } memcpy(_out, &iv, sizeof(iv)); in += 4; @@ -954,7 +958,8 @@ unpackRGBaskip1(UINT8 *_out, const UINT8 *in, int pixels) { CLIP8(in[0] * 255 / a), CLIP8(in[1] * 255 / a), CLIP8(in[2] * 255 / a), - a); + a + ); } in += 5; } @@ -976,7 +981,8 @@ unpackRGBaskip2(UINT8 *_out, const UINT8 *in, int pixels) { CLIP8(in[0] * 255 / a), CLIP8(in[1] * 255 / a), CLIP8(in[2] * 255 / a), - a); + a + ); } in += 6; } @@ -998,7 +1004,8 @@ unpackBGRa(UINT8 *_out, const UINT8 *in, int pixels) { CLIP8(in[2] * 255 / a), CLIP8(in[1] * 255 / a), CLIP8(in[0] * 255 / a), - a); + a + ); } memcpy(_out, &iv, sizeof(iv)); in += 4; @@ -1029,7 +1036,8 @@ unpackRGBAL(UINT8 *_out, const UINT8 *in, int pixels) { in[i], in[i + pixels], in[i + pixels + pixels], - in[i + pixels + pixels + pixels]); + in[i + pixels + pixels + pixels] + ); memcpy(_out, &iv, sizeof(iv)); } } @@ -1276,12 +1284,6 @@ copy2(UINT8 *out, const UINT8 *in, int pixels) { memcpy(out, in, pixels * 2); } -static void -copy3(UINT8 *out, const UINT8 *in, int pixels) { - /* BGR;24 */ - memcpy(out, in, pixels * 3); -} - static void copy4(UINT8 *out, const UINT8 *in, int pixels) { /* RGBA, CMYK quadruples */ @@ -1540,12 +1542,11 @@ band316L(UINT8 *out, const UINT8 *in, int pixels) { } static struct { - const char *mode; - const char *rawmode; + const ModeID mode; + const RawModeID rawmode; int bits; ImagingShuffler unpack; } unpackers[] = { - /* raw mode syntax is ";" where "bits" defaults depending on mode (1 for "1", 8 for "P" and "L", etc), and "flags" should be given in alphabetical order. if both bits @@ -1558,300 +1559,295 @@ 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", 8, unpack18}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1, 1, unpack1}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_I, 1, unpack1I}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_R, 1, unpack1R}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_IR, 1, unpack1IR}, + {IMAGING_MODE_1, IMAGING_RAWMODE_1_8, 8, unpack18}, /* grayscale */ - {"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}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_2, 2, unpackL2}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_2I, 2, unpackL2I}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_2R, 2, unpackL2R}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_2IR, 2, unpackL2IR}, + + {IMAGING_MODE_L, IMAGING_RAWMODE_L_4, 4, unpackL4}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_4I, 4, unpackL4I}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_4R, 4, unpackL4R}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_4IR, 4, unpackL4IR}, + + {IMAGING_MODE_L, IMAGING_RAWMODE_L, 8, copy1}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_I, 8, unpackLI}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_R, 8, unpackLR}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_16, 16, unpackL16}, + {IMAGING_MODE_L, IMAGING_RAWMODE_L_16B, 16, unpackL16B}, /* grayscale w. alpha */ - {"LA", "LA", 16, unpackLA}, - {"LA", "LA;L", 16, unpackLAL}, + {IMAGING_MODE_LA, IMAGING_RAWMODE_LA, 16, unpackLA}, + {IMAGING_MODE_LA, IMAGING_RAWMODE_LA_L, 16, unpackLAL}, /* grayscale w. alpha premultiplied */ - {"La", "La", 16, unpackLA}, + {IMAGING_MODE_La, IMAGING_RAWMODE_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", "L", 8, copy1}, - {"P", "PX", 16, unpackL16B}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_1, 1, unpackP1}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_2, 2, unpackP2}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_2L, 2, unpackP2L}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_4, 4, unpackP4}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_4L, 4, unpackP4L}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P, 8, copy1}, + {IMAGING_MODE_P, IMAGING_RAWMODE_P_R, 8, unpackLR}, + {IMAGING_MODE_P, IMAGING_RAWMODE_L, 8, copy1}, + {IMAGING_MODE_P, IMAGING_RAWMODE_PX, 16, unpackL16B}, /* palette w. alpha */ - {"PA", "PA", 16, unpackLA}, - {"PA", "PA;L", 16, unpackLAL}, - {"PA", "LA", 16, unpackLA}, + {IMAGING_MODE_PA, IMAGING_RAWMODE_PA, 16, unpackLA}, + {IMAGING_MODE_PA, IMAGING_RAWMODE_PA_L, 16, unpackLAL}, + {IMAGING_MODE_PA, IMAGING_RAWMODE_LA, 16, unpackLA}, /* 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", "RGBX;16L", 64, unpackRGBA16L}, - {"RGB", "RGBX;16B", 64, unpackRGBA16B}, - {"RGB", "RGB;4B", 16, ImagingUnpackRGB4B}, - {"RGB", "BGR;5", 16, ImagingUnpackBGR15}, /* compat */ - {"RGB", "RGBX", 32, copy4}, - {"RGB", "RGBX;L", 32, unpackRGBAL}, - {"RGB", "RGBXX", 40, copy4skip1}, - {"RGB", "RGBXXX", 48, copy4skip2}, - {"RGB", "RGBA;L", 32, unpackRGBAL}, - {"RGB", "RGBA;15", 16, ImagingUnpackRGBA15}, - {"RGB", "BGRX", 32, ImagingUnpackBGRX}, - {"RGB", "BGXR", 32, ImagingUnpackBGXR}, - {"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}, - {"RGB", "CMYK", 32, cmyk2rgb}, - - {"BGR;15", "BGR;15", 16, copy2}, - {"BGR;16", "BGR;16", 16, copy2}, - {"BGR;24", "BGR;24", 24, copy3}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB, 24, ImagingUnpackRGB}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_L, 24, unpackRGBL}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_R, 24, unpackRGBR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16L, 48, unpackRGB16L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16B, 48, unpackRGB16B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR, 24, ImagingUnpackBGR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_15, 16, ImagingUnpackRGB15}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR_15, 16, ImagingUnpackBGR15}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16, 16, ImagingUnpackRGB16}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR_16, 16, ImagingUnpackBGR16}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX_16L, 64, unpackRGBA16L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX_16B, 64, unpackRGBA16B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_4B, 16, ImagingUnpackRGB4B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGR_5, 16, ImagingUnpackBGR15}, /* compat */ + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX, 32, copy4}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX_L, 32, unpackRGBAL}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBXX, 40, copy4skip1}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBXXX, 48, copy4skip2}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBA_L, 32, unpackRGBAL}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBA_15, 16, ImagingUnpackRGBA15}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGRX, 32, ImagingUnpackBGRX}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_BGXR, 32, ImagingUnpackBGXR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_XRGB, 32, ImagingUnpackXRGB}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_XBGR, 32, ImagingUnpackXBGR}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_YCC_P, 24, ImagingUnpackYCC}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_B, 8, band2}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R_16L, 16, band016L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G_16L, 16, band116L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_B_16L, 16, band216L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R_16B, 16, band016B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G_16B, 16, band116B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_B_16B, 16, band216B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_CMYK, 32, cmyk2rgb}, /* true colour w. alpha */ - {"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", "BGRA;15Z", 16, ImagingUnpackBGRA15Z}, - {"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", "BGAR", 32, unpackBGAR}, - {"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}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_LA, 16, unpackRGBALA}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_LA_16B, 32, unpackRGBALA16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA, 32, copy4}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBAX, 40, copy4skip1}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBAXX, 48, copy4skip2}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa, 32, unpackRGBa}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBaX, 40, unpackRGBaskip1}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBaXX, 48, unpackRGBaskip2}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa_16L, 64, unpackRGBa16L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa_16B, 64, unpackRGBa16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGR, 24, ImagingUnpackBGR}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRa, 32, unpackBGRa}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_I, 32, unpackRGBAI}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_L, 32, unpackRGBAL}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_15, 16, ImagingUnpackRGBA15}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA_15, 16, ImagingUnpackBGRA15}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA_15Z, 16, ImagingUnpackBGRA15Z}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_4B, 16, ImagingUnpackRGBA4B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_16L, 64, unpackRGBA16L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_16B, 64, unpackRGBA16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA, 32, unpackBGRA}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA_16L, 64, unpackBGRA16L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGRA_16B, 64, unpackBGRA16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_BGAR, 32, unpackBGAR}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_ARGB, 32, unpackARGB}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_ABGR, 32, unpackABGR}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_YCCA_P, 32, ImagingUnpackYCCA}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B, 8, band2}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A, 8, band3}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R_16L, 16, band016L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G_16L, 16, band116L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B_16L, 16, band216L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A_16L, 16, band316L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R_16B, 16, band016B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G_16B, 16, band116B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B_16B, 16, band216B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A_16B, 16, band316B}, #ifdef WORDS_BIGENDIAN - {"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}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16N, 48, unpackRGB16B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX_16N, 64, unpackRGBA16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa_16N, 64, unpackRGBa16B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_16N, 64, unpackRGBA16B}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_16N, 64, unpackRGBA16B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R_16N, 16, band016B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G_16N, 16, band116B}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_B_16N, 16, band216B}, + + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R_16N, 16, band016B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G_16N, 16, band116B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B_16N, 16, band216B}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_A_16N, 16, band316B}, #else - {"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}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGB_16N, 48, unpackRGB16L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_RGBX_16N, 64, unpackRGBA16L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBa_16N, 64, unpackRGBa16L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_RGBA_16N, 64, unpackRGBA16L}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_16N, 64, unpackRGBA16L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_R_16N, 16, band016L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_G_16N, 16, band116L}, + {IMAGING_MODE_RGB, IMAGING_RAWMODE_B_16N, 16, band216L}, + + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_R_16N, 16, band016L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_G_16N, 16, band116L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_B_16N, 16, band216L}, + {IMAGING_MODE_RGBA, IMAGING_RAWMODE_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}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_RGBa, 32, copy4}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_BGRa, 32, unpackBGRA}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_aRGB, 32, unpackARGB}, + {IMAGING_MODE_RGBa, IMAGING_RAWMODE_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", "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}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB, 24, ImagingUnpackRGB}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB_L, 24, unpackRGBL}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB_16B, 48, unpackRGB16B}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGR, 24, ImagingUnpackBGR}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB_15, 16, ImagingUnpackRGB15}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGR_15, 16, ImagingUnpackBGR15}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGB_4B, 16, ImagingUnpackRGB4B}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGR_5, 16, ImagingUnpackBGR15}, /* compat */ + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX, 32, copy4}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBXX, 40, copy4skip1}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBXXX, 48, copy4skip2}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_L, 32, unpackRGBAL}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_16L, 64, unpackRGBA16L}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_RGBX_16B, 64, unpackRGBA16B}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_BGRX, 32, ImagingUnpackBGRX}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_XRGB, 32, ImagingUnpackXRGB}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_XBGR, 32, ImagingUnpackXBGR}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_YCC_P, 24, ImagingUnpackYCC}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_R, 8, band0}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_G, 8, band1}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_B, 8, band2}, + {IMAGING_MODE_RGBX, IMAGING_RAWMODE_X, 8, band3}, /* colour separation */ - {"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}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK, 32, copy4}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYKX, 40, copy4skip1}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYKXX, 48, copy4skip2}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_I, 32, unpackCMYKI}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_L, 32, unpackRGBAL}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_16L, 64, unpackRGBA16L}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_16B, 64, unpackRGBA16B}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_C, 8, band0}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_M, 8, band1}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_Y, 8, band2}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_K, 8, band3}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_C_I, 8, band0I}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_M_I, 8, band1I}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_Y_I, 8, band2I}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_K_I, 8, band3I}, #ifdef WORDS_BIGENDIAN - {"CMYK", "CMYK;16N", 64, unpackRGBA16B}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_16N, 64, unpackRGBA16B}, #else - {"CMYK", "CMYK;16N", 64, unpackRGBA16L}, + {IMAGING_MODE_CMYK, IMAGING_RAWMODE_CMYK_16N, 64, unpackRGBA16L}, #endif /* video (YCbCr) */ - {"YCbCr", "YCbCr", 24, ImagingUnpackRGB}, - {"YCbCr", "YCbCr;L", 24, unpackRGBL}, - {"YCbCr", "YCbCrX", 32, copy4}, - {"YCbCr", "YCbCrK", 32, copy4}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr, 24, ImagingUnpackRGB}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr_L, 24, unpackRGBL}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCrX, 32, copy4}, + {IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCrK, 32, copy4}, /* LAB Color */ - {"LAB", "LAB", 24, ImagingUnpackLAB}, - {"LAB", "L", 8, band0}, - {"LAB", "A", 8, band1}, - {"LAB", "B", 8, band2}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_LAB, 24, ImagingUnpackLAB}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_L, 8, band0}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_A, 8, band1}, + {IMAGING_MODE_LAB, IMAGING_RAWMODE_B, 8, band2}, /* HSV Color */ - {"HSV", "HSV", 24, ImagingUnpackRGB}, - {"HSV", "H", 8, band0}, - {"HSV", "S", 8, band1}, - {"HSV", "V", 8, band2}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_HSV, 24, ImagingUnpackRGB}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_H, 8, band0}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_S, 8, band1}, + {IMAGING_MODE_HSV, IMAGING_RAWMODE_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}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I, 32, copy4}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_8, 8, unpackI8}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_8S, 8, unpackI8S}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16, 16, unpackI16}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16S, 16, unpackI16S}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16B, 16, unpackI16B}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16BS, 16, unpackI16BS}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16N, 16, unpackI16N}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_16NS, 16, unpackI16NS}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32, 32, unpackI32}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32S, 32, unpackI32S}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32B, 32, unpackI32B}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32BS, 32, unpackI32BS}, + {IMAGING_MODE_I, IMAGING_RAWMODE_I_32N, 32, unpackI32N}, + {IMAGING_MODE_I, IMAGING_RAWMODE_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}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F, 32, copy4}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_8, 8, unpackF8}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_8S, 8, unpackF8S}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16, 16, unpackF16}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16S, 16, unpackF16S}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16B, 16, unpackF16B}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16BS, 16, unpackF16BS}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16N, 16, unpackF16N}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_16NS, 16, unpackF16NS}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32, 32, unpackF32}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32S, 32, unpackF32S}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32B, 32, unpackF32B}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32BS, 32, unpackF32BS}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32N, 32, unpackF32N}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32NS, 32, unpackF32NS}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32F, 32, unpackF32F}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32BF, 32, unpackF32BF}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_32NF, 32, unpackF32NF}, #ifdef FLOAT64 - {"F", "F;64F", 64, unpackF64F}, - {"F", "F;64BF", 64, unpackF64BF}, - {"F", "F;64NF", 64, unpackF64NF}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_64F, 64, unpackF64F}, + {IMAGING_MODE_F, IMAGING_RAWMODE_F_64BF, 64, unpackF64BF}, + {IMAGING_MODE_F, IMAGING_RAWMODE_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;16N", "I;16N", 16, copy2}, - - {"I;16", "I;16B", 16, unpackI16B_I16}, - {"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}, + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16, 16, copy2}, + {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16B, 16, copy2}, + {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16L, 16, copy2}, + {IMAGING_MODE_I_16N, IMAGING_RAWMODE_I_16N, 16, copy2}, - {"I;16", "I;16R", 16, unpackI16R_I16}, + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16B, 16, unpackI16B_I16}, + {IMAGING_MODE_I_16B, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16B}, + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16R, 16, unpackI16R_I16}, - {"I;16", "I;12", 12, unpackI12_I16}, // 12 bit Tiffs stored in 16bits. + // LibTiff native->image endian. + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16}, + {IMAGING_MODE_I_16L, IMAGING_RAWMODE_I_16N, 16, unpackI16N_I16}, - {NULL} /* sentinel */ + // 12 bit Tiffs stored in 16bits. + {IMAGING_MODE_I_16, IMAGING_RAWMODE_I_12, 12, unpackI12_I16} }; ImagingShuffler -ImagingFindUnpacker(const char *mode, const char *rawmode, int *bits_out) { - int i; - - /* find a suitable pixel unpacker */ - for (i = 0; unpackers[i].rawmode; i++) { - if (strcmp(unpackers[i].mode, mode) == 0 && - strcmp(unpackers[i].rawmode, rawmode) == 0) { +ImagingFindUnpacker(const ModeID mode, const RawModeID rawmode, int *bits_out) { + for (size_t i = 0; i < sizeof(unpackers) / sizeof(*unpackers); i++) { + if (unpackers[i].mode == mode && unpackers[i].rawmode == rawmode) { if (bits_out) { *bits_out = unpackers[i].bits; } diff --git a/src/libImaging/UnpackYCC.c b/src/libImaging/UnpackYCC.c index 0b177bdd4f5..35b0c3b694d 100644 --- a/src/libImaging/UnpackYCC.c +++ b/src/libImaging/UnpackYCC.c @@ -34,7 +34,8 @@ static INT16 L[] = { 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}; + 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, @@ -55,7 +56,8 @@ static INT16 CB[] = { 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}; + 213, 215, 217, 220 +}; static INT16 GB[] = { 67, 67, 66, 66, 65, 65, 65, 64, 64, 63, 63, 62, 62, 62, 61, 61, @@ -73,7 +75,8 @@ static INT16 GB[] = { -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}; + -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, @@ -94,7 +97,8 @@ static INT16 CR[] = { 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}; + 209, 211, 213, 215 +}; static INT16 GR[] = { 127, 126, 125, 124, 123, 122, 121, 121, 120, 119, 118, 117, 116, 115, 114, @@ -114,7 +118,8 @@ static INT16 GR[] = { -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}; + -108 +}; #define R 0 #define G 1 diff --git a/src/libImaging/UnsharpMask.c b/src/libImaging/UnsharpMask.c index 2853ce903fc..e714749effd 100644 --- a/src/libImaging/UnsharpMask.c +++ b/src/libImaging/UnsharpMask.c @@ -23,7 +23,8 @@ clip8(int in) { Imaging ImagingUnsharpMask( - Imaging imOut, Imaging imIn, float radius, int percent, int threshold) { + Imaging imOut, Imaging imIn, float radius, int percent, int threshold +) { ImagingSectionCookie cookie; Imaging result; diff --git a/src/libImaging/XbmEncode.c b/src/libImaging/XbmEncode.c index eec4c0d8462..65cc3c6333c 100644 --- a/src/libImaging/XbmEncode.c +++ b/src/libImaging/XbmEncode.c @@ -40,7 +40,8 @@ ImagingXbmEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { state->shuffle( state->buffer, (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); + state->xsize + ); if (state->y < state->ysize - 1) { /* any line but the last */ diff --git a/src/libImaging/ZipDecode.c b/src/libImaging/ZipDecode.c index 8749678341e..d964ff2ca7d 100644 --- a/src/libImaging/ZipDecode.c +++ b/src/libImaging/ZipDecode.c @@ -217,7 +217,8 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt state->shuffle( (UINT8 *)im->image[state->y] + col * im->pixelsize, state->buffer + context->prefix + i, - 1); + 1 + ); col += COL_INCREMENT[context->pass]; } } else { @@ -229,7 +230,8 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt UINT8 byte = *(state->buffer + context->prefix + (i / 8)); byte <<= (i % 8); state->shuffle( - (UINT8 *)im->image[state->y] + col * im->pixelsize, &byte, 1); + (UINT8 *)im->image[state->y] + col * im->pixelsize, &byte, 1 + ); col += COL_INCREMENT[context->pass]; } } @@ -253,7 +255,8 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, state->buffer + context->prefix, - state->xsize); + state->xsize + ); state->y++; } diff --git a/src/libImaging/ZipEncode.c b/src/libImaging/ZipEncode.c index edbce36822c..44f2629cc12 100644 --- a/src/libImaging/ZipEncode.c +++ b/src/libImaging/ZipEncode.c @@ -98,7 +98,8 @@ ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { 15, 9, /* compression strategy (image data are filtered)*/ - compress_type); + compress_type + ); if (err < 0) { state->errcode = IMAGING_CODEC_CONFIG; return -1; @@ -108,7 +109,8 @@ ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { err = deflateSetDictionary( &context->z_stream, (unsigned char *)context->dictionary, - context->dictionary_size); + context->dictionary_size + ); if (err < 0) { state->errcode = IMAGING_CODEC_CONFIG; return -1; @@ -163,7 +165,8 @@ ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { state->buffer + 1, (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, - state->xsize); + state->xsize + ); state->y++; diff --git a/src/libImaging/codec_fd.c b/src/libImaging/codec_fd.c index 5261681107b..c5614e6039b 100644 --- a/src/libImaging/codec_fd.c +++ b/src/libImaging/codec_fd.c @@ -12,6 +12,9 @@ _imaging_read_pyFd(PyObject *fd, char *dest, Py_ssize_t bytes) { int bytes_result; result = PyObject_CallMethod(fd, "read", "n", bytes); + if (result == NULL) { + goto err; + } bytes_result = PyBytes_AsStringAndSize(result, &buffer, &length); if (bytes_result == -1) { @@ -28,7 +31,7 @@ _imaging_read_pyFd(PyObject *fd, char *dest, Py_ssize_t bytes) { return length; err: - Py_DECREF(result); + Py_XDECREF(result); return -1; } @@ -38,9 +41,16 @@ _imaging_write_pyFd(PyObject *fd, char *src, Py_ssize_t bytes) { PyObject *byteObj; byteObj = PyBytes_FromStringAndSize(src, bytes); + if (!byteObj) { + return -1; + } result = PyObject_CallMethod(fd, "write", "O", byteObj); Py_DECREF(byteObj); + if (result == NULL) { + return -1; + } + Py_DECREF(result); return bytes; @@ -51,6 +61,9 @@ _imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence) { PyObject *result; result = PyObject_CallMethod(fd, "seek", "ni", offset, whence); + if (result == NULL) { + return -1; + } Py_DECREF(result); return 0; @@ -62,6 +75,9 @@ _imaging_tell_pyFd(PyObject *fd) { Py_ssize_t location; result = PyObject_CallMethod(fd, "tell", NULL); + if (result == NULL) { + return -1; + } location = PyLong_AsSsize_t(result); Py_DECREF(result); diff --git a/src/map.c b/src/map.c index c298bd1482a..6f66b0cc57b 100644 --- a/src/map.c +++ b/src/map.c @@ -55,7 +55,7 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { PyObject *target; Py_buffer view; - char *mode; + char *mode_name; char *codec; Py_ssize_t offset; int xsize, ysize; @@ -70,9 +70,10 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { &ysize, &codec, &offset, - &mode, + &mode_name, &stride, - &ystep)) { + &ystep + )) { return NULL; } @@ -81,10 +82,12 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { return NULL; } + const ModeID mode = findModeID(mode_name); + if (stride <= 0) { - if (!strcmp(mode, "L") || !strcmp(mode, "P")) { + if (mode == IMAGING_MODE_L || mode == IMAGING_MODE_P) { stride = xsize; - } else if (!strncmp(mode, "I;16", 4)) { + } else if (isModeI16(mode)) { stride = xsize * 2; } else { stride = xsize * 4; @@ -136,6 +139,7 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { } } + im->read_only = view.readonly; im->destroy = mapping_destroy_buffer; Py_INCREF(target); diff --git a/src/outline.c b/src/outline.c index 27cc255cf84..32ab9109c15 100644 --- a/src/outline.c +++ b/src/outline.c @@ -89,8 +89,7 @@ _outline_move(OutlineObject *self, PyObject *args) { ImagingOutlineMove(self->outline, x0, y0); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -102,8 +101,7 @@ _outline_line(OutlineObject *self, PyObject *args) { ImagingOutlineLine(self->outline, x1, y1); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -115,8 +113,7 @@ _outline_curve(OutlineObject *self, PyObject *args) { ImagingOutlineCurve(self->outline, x1, y1, x2, y2, x3, y3); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -127,8 +124,7 @@ _outline_close(OutlineObject *self, PyObject *args) { ImagingOutlineClose(self->outline); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -140,8 +136,7 @@ _outline_transform(OutlineObject *self, PyObject *args) { ImagingOutlineTransform(self->outline, a); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static struct PyMethodDef _outline_methods[] = { @@ -154,34 +149,8 @@ static struct PyMethodDef _outline_methods[] = { }; static PyTypeObject OutlineType = { - PyVarObject_HEAD_INIT(NULL, 0) "Outline", /*tp_name*/ - sizeof(OutlineObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_outline_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 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).tp_name = "Outline", + .tp_basicsize = sizeof(OutlineObject), + .tp_dealloc = (destructor)_outline_dealloc, + .tp_methods = _outline_methods, }; diff --git a/src/path.c b/src/path.c index 6bc90abed86..b88346d5f8f 100644 --- a/src/path.c +++ b/src/path.c @@ -26,6 +26,7 @@ */ #include "Python.h" +#include "thirdparty/pythoncapi_compat.h" #include "libImaging/Imaging.h" #include @@ -43,7 +44,7 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view); typedef struct { PyObject_HEAD Py_ssize_t count; double *xy; - int index; /* temporary use, e.g. in decimate */ + int mapping; } PyPathObject; static PyTypeObject PyPathType; @@ -91,6 +92,7 @@ path_new(Py_ssize_t count, double *xy, int duplicate) { path->count = count; path->xy = xy; + path->mapping = 0; return path; } @@ -107,6 +109,52 @@ path_dealloc(PyPathObject *path) { #define PyPath_Check(op) (Py_TYPE(op) == &PyPathType) +static int +assign_item_to_array(double *xy, Py_ssize_t j, PyObject *op) { + 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 (PyList_Check(op)) { + if (PyList_GET_SIZE(op) != 2) { + PyErr_SetString( + PyExc_ValueError, "coordinate list must contain exactly 2 coordinates" + ); + return -1; + } + for (int k = 0; k < 2; k++) { + PyObject *op1 = PyList_GetItemRef(op, k); + if (op1 == NULL) { + return -1; + } + if (PyFloat_Check(op1) || PyLong_Check(op1) || PyNumber_Check(op1)) { + j = assign_item_to_array(xy, j, op1); + } else { + j = -1; + } + Py_DECREF(op1); + if (j == -1) { + PyErr_SetString( + PyExc_ValueError, "coordinate list must contain numbers" + ); + return -1; + } + } + } else { + double x, y; + if (PyArg_ParseTuple(op, "dd", &x, &y)) { + xy[j++] = x; + xy[j++] = y; + } else { + PyErr_SetString(PyExc_ValueError, "incorrect coordinate type"); + return -1; + } + } + return j; +} + Py_ssize_t PyPath_Flatten(PyObject *data, double **pxy) { Py_ssize_t i, j, n; @@ -162,41 +210,32 @@ PyPath_Flatten(PyObject *data, double **pxy) { 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); - assign_item_to_array(op, 0); + PyObject *op = PyList_GetItemRef(data, i); + if (op == NULL) { + free(xy); + return -1; + } + j = assign_item_to_array(xy, j, op); + Py_DECREF(op); + if (j == -1) { + free(xy); + return -1; + } } } else if (PyTuple_Check(data)) { for (i = 0; i < n; i++) { - double x, y; PyObject *op = PyTuple_GET_ITEM(data, i); - assign_item_to_array(op, 0); + j = assign_item_to_array(xy, j, op); + if (j == -1) { + free(xy); + return -1; + } } } else { for (i = 0; i < n; i++) { - double x, y; PyObject *op = PySequence_GetItem(data, i); if (!op) { /* treat IndexError as end of sequence */ @@ -208,8 +247,12 @@ PyPath_Flatten(PyObject *data, double **pxy) { return -1; } } - assign_item_to_array(op, 1); + j = assign_item_to_array(xy, j, op); Py_DECREF(op); + if (j == -1) { + free(xy); + return -1; + } } } @@ -270,6 +313,10 @@ path_compact(PyPathObject *self, PyObject *args) { double cityblock = 2.0; + if (self->mapping) { + PyErr_SetString(PyExc_ValueError, "Path compacted during mapping"); + return NULL; + } if (!PyArg_ParseTuple(args, "|d:compact", &cityblock)) { return NULL; } @@ -387,11 +434,13 @@ path_map(PyPathObject *self, PyObject *args) { xy = self->xy; /* apply function to coordinate set */ + self->mapping = 1; 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); if (!item || !PyArg_ParseTuple(item, "dd", &x, &y)) { + self->mapping = 0; Py_XDECREF(item); return NULL; } @@ -399,9 +448,9 @@ path_map(PyPathObject *self, PyObject *args) { xy[i + i + 1] = y; Py_DECREF(item); } + self->mapping = 0; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static int @@ -482,7 +531,8 @@ path_transform(PyPathObject *self, PyObject *args) { double wrap = 0.0; if (!PyArg_ParseTuple( - args, "(dddddd)|d:transform", &a, &b, &c, &d, &e, &f, &wrap)) { + args, "(dddddd)|d:transform", &a, &b, &c, &d, &e, &f, &wrap + )) { return NULL; } @@ -512,8 +562,7 @@ path_transform(PyPathObject *self, PyObject *args) { } } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static struct PyMethodDef methods[] = { @@ -563,7 +612,8 @@ path_subscript(PyPathObject *self, PyObject *item) { PyErr_Format( PyExc_TypeError, "Path indices must be integers, not %.200s", - Py_TYPE(item)->tp_name); + Py_TYPE(item)->tp_name + ); return NULL; } } @@ -579,37 +629,15 @@ static PySequenceMethods path_as_sequence = { }; 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_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)path_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 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*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "Path", + .tp_basicsize = sizeof(PyPathObject), + .tp_dealloc = (destructor)path_dealloc, + .tp_as_sequence = &path_as_sequence, + .tp_as_mapping = &path_as_mapping, + .tp_methods = methods, + .tp_getset = getsetters, }; diff --git a/src/thirdparty/pythoncapi_compat.h b/src/thirdparty/pythoncapi_compat.h new file mode 100644 index 00000000000..04fcf61e0ef --- /dev/null +++ b/src/thirdparty/pythoncapi_compat.h @@ -0,0 +1,2206 @@ +// Header file providing new C API functions to old Python versions. +// +// File distributed under the Zero Clause BSD (0BSD) license. +// Copyright Contributors to the pythoncapi_compat project. +// +// Homepage: +// https://github.com/python/pythoncapi_compat +// +// Latest version: +// https://raw.githubusercontent.com/python/pythoncapi-compat/main/pythoncapi_compat.h +// +// This file was vendored from the following commit: +// https://github.com/python/pythoncapi-compat/commit/c84545f0e1e21757d4901f75c47333d25a3fcff0 +// +// SPDX-License-Identifier: 0BSD + +#ifndef PYTHONCAPI_COMPAT +#define PYTHONCAPI_COMPAT + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include // offsetof() + +// Python 3.11.0b4 added PyFrame_Back() to Python.h +#if PY_VERSION_HEX < 0x030b00B4 && !defined(PYPY_VERSION) +# include "frameobject.h" // PyFrameObject, PyFrame_GetBack() +#endif +#if PY_VERSION_HEX < 0x030C00A3 +# include // T_SHORT, READONLY +#endif + + +#ifndef _Py_CAST +# define _Py_CAST(type, expr) ((type)(expr)) +#endif + +// Static inline functions should use _Py_NULL rather than using directly NULL +// to prevent C++ compiler warnings. On C23 and newer and on C++11 and newer, +// _Py_NULL is defined as nullptr. +#if (defined (__STDC_VERSION__) && __STDC_VERSION__ > 201710L) \ + || (defined(__cplusplus) && __cplusplus >= 201103) +# define _Py_NULL nullptr +#else +# define _Py_NULL NULL +#endif + +// Cast argument to PyObject* type. +#ifndef _PyObject_CAST +# define _PyObject_CAST(op) _Py_CAST(PyObject*, op) +#endif + +#ifndef Py_BUILD_ASSERT +# define Py_BUILD_ASSERT(cond) \ + do { \ + (void)sizeof(char [1 - 2 * !(cond)]); \ + } while(0) +#endif + + +// bpo-42262 added Py_NewRef() to Python 3.10.0a3 +#if PY_VERSION_HEX < 0x030A00A3 && !defined(Py_NewRef) +static inline PyObject* _Py_NewRef(PyObject *obj) +{ + Py_INCREF(obj); + return obj; +} +#define Py_NewRef(obj) _Py_NewRef(_PyObject_CAST(obj)) +#endif + + +// bpo-42262 added Py_XNewRef() to Python 3.10.0a3 +#if PY_VERSION_HEX < 0x030A00A3 && !defined(Py_XNewRef) +static inline PyObject* _Py_XNewRef(PyObject *obj) +{ + Py_XINCREF(obj); + return obj; +} +#define Py_XNewRef(obj) _Py_XNewRef(_PyObject_CAST(obj)) +#endif + + +// bpo-39573 added Py_SET_REFCNT() to Python 3.9.0a4 +#if PY_VERSION_HEX < 0x030900A4 && !defined(Py_SET_REFCNT) +static inline void _Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) +{ + ob->ob_refcnt = refcnt; +} +#define Py_SET_REFCNT(ob, refcnt) _Py_SET_REFCNT(_PyObject_CAST(ob), refcnt) +#endif + + +// Py_SETREF() and Py_XSETREF() were added to Python 3.5.2. +// It is excluded from the limited C API. +#if (PY_VERSION_HEX < 0x03050200 && !defined(Py_SETREF)) && !defined(Py_LIMITED_API) +#define Py_SETREF(dst, src) \ + do { \ + PyObject **_tmp_dst_ptr = _Py_CAST(PyObject**, &(dst)); \ + PyObject *_tmp_dst = (*_tmp_dst_ptr); \ + *_tmp_dst_ptr = _PyObject_CAST(src); \ + Py_DECREF(_tmp_dst); \ + } while (0) + +#define Py_XSETREF(dst, src) \ + do { \ + PyObject **_tmp_dst_ptr = _Py_CAST(PyObject**, &(dst)); \ + PyObject *_tmp_dst = (*_tmp_dst_ptr); \ + *_tmp_dst_ptr = _PyObject_CAST(src); \ + Py_XDECREF(_tmp_dst); \ + } while (0) +#endif + + +// bpo-43753 added Py_Is(), Py_IsNone(), Py_IsTrue() and Py_IsFalse() +// to Python 3.10.0b1. +#if PY_VERSION_HEX < 0x030A00B1 && !defined(Py_Is) +# define Py_Is(x, y) ((x) == (y)) +#endif +#if PY_VERSION_HEX < 0x030A00B1 && !defined(Py_IsNone) +# define Py_IsNone(x) Py_Is(x, Py_None) +#endif +#if (PY_VERSION_HEX < 0x030A00B1 || defined(PYPY_VERSION)) && !defined(Py_IsTrue) +# define Py_IsTrue(x) Py_Is(x, Py_True) +#endif +#if (PY_VERSION_HEX < 0x030A00B1 || defined(PYPY_VERSION)) && !defined(Py_IsFalse) +# define Py_IsFalse(x) Py_Is(x, Py_False) +#endif + + +// bpo-39573 added Py_SET_TYPE() to Python 3.9.0a4 +#if PY_VERSION_HEX < 0x030900A4 && !defined(Py_SET_TYPE) +static inline void _Py_SET_TYPE(PyObject *ob, PyTypeObject *type) +{ + ob->ob_type = type; +} +#define Py_SET_TYPE(ob, type) _Py_SET_TYPE(_PyObject_CAST(ob), type) +#endif + + +// bpo-39573 added Py_SET_SIZE() to Python 3.9.0a4 +#if PY_VERSION_HEX < 0x030900A4 && !defined(Py_SET_SIZE) +static inline void _Py_SET_SIZE(PyVarObject *ob, Py_ssize_t size) +{ + ob->ob_size = size; +} +#define Py_SET_SIZE(ob, size) _Py_SET_SIZE((PyVarObject*)(ob), size) +#endif + + +// bpo-40421 added PyFrame_GetCode() to Python 3.9.0b1 +#if PY_VERSION_HEX < 0x030900B1 || defined(PYPY_VERSION) +static inline PyCodeObject* PyFrame_GetCode(PyFrameObject *frame) +{ + assert(frame != _Py_NULL); + assert(frame->f_code != _Py_NULL); + return _Py_CAST(PyCodeObject*, Py_NewRef(frame->f_code)); +} +#endif + +static inline PyCodeObject* _PyFrame_GetCodeBorrow(PyFrameObject *frame) +{ + PyCodeObject *code = PyFrame_GetCode(frame); + Py_DECREF(code); + return code; +} + + +// bpo-40421 added PyFrame_GetBack() to Python 3.9.0b1 +#if PY_VERSION_HEX < 0x030900B1 && !defined(PYPY_VERSION) +static inline PyFrameObject* PyFrame_GetBack(PyFrameObject *frame) +{ + assert(frame != _Py_NULL); + return _Py_CAST(PyFrameObject*, Py_XNewRef(frame->f_back)); +} +#endif + +#if !defined(PYPY_VERSION) +static inline PyFrameObject* _PyFrame_GetBackBorrow(PyFrameObject *frame) +{ + PyFrameObject *back = PyFrame_GetBack(frame); + Py_XDECREF(back); + return back; +} +#endif + + +// bpo-40421 added PyFrame_GetLocals() to Python 3.11.0a7 +#if PY_VERSION_HEX < 0x030B00A7 && !defined(PYPY_VERSION) +static inline PyObject* PyFrame_GetLocals(PyFrameObject *frame) +{ +#if PY_VERSION_HEX >= 0x030400B1 + if (PyFrame_FastToLocalsWithError(frame) < 0) { + return NULL; + } +#else + PyFrame_FastToLocals(frame); +#endif + return Py_NewRef(frame->f_locals); +} +#endif + + +// bpo-40421 added PyFrame_GetGlobals() to Python 3.11.0a7 +#if PY_VERSION_HEX < 0x030B00A7 && !defined(PYPY_VERSION) +static inline PyObject* PyFrame_GetGlobals(PyFrameObject *frame) +{ + return Py_NewRef(frame->f_globals); +} +#endif + + +// bpo-40421 added PyFrame_GetBuiltins() to Python 3.11.0a7 +#if PY_VERSION_HEX < 0x030B00A7 && !defined(PYPY_VERSION) +static inline PyObject* PyFrame_GetBuiltins(PyFrameObject *frame) +{ + return Py_NewRef(frame->f_builtins); +} +#endif + + +// bpo-40421 added PyFrame_GetLasti() to Python 3.11.0b1 +#if PY_VERSION_HEX < 0x030B00B1 && !defined(PYPY_VERSION) +static inline int PyFrame_GetLasti(PyFrameObject *frame) +{ +#if PY_VERSION_HEX >= 0x030A00A7 + // bpo-27129: Since Python 3.10.0a7, f_lasti is an instruction offset, + // not a bytes offset anymore. Python uses 16-bit "wordcode" (2 bytes) + // instructions. + if (frame->f_lasti < 0) { + return -1; + } + return frame->f_lasti * 2; +#else + return frame->f_lasti; +#endif +} +#endif + + +// gh-91248 added PyFrame_GetVar() to Python 3.12.0a2 +#if PY_VERSION_HEX < 0x030C00A2 && !defined(PYPY_VERSION) +static inline PyObject* PyFrame_GetVar(PyFrameObject *frame, PyObject *name) +{ + PyObject *locals, *value; + + locals = PyFrame_GetLocals(frame); + if (locals == NULL) { + return NULL; + } +#if PY_VERSION_HEX >= 0x03000000 + value = PyDict_GetItemWithError(locals, name); +#else + value = _PyDict_GetItemWithError(locals, name); +#endif + Py_DECREF(locals); + + if (value == NULL) { + if (PyErr_Occurred()) { + return NULL; + } +#if PY_VERSION_HEX >= 0x03000000 + PyErr_Format(PyExc_NameError, "variable %R does not exist", name); +#else + PyErr_SetString(PyExc_NameError, "variable does not exist"); +#endif + return NULL; + } + return Py_NewRef(value); +} +#endif + + +// gh-91248 added PyFrame_GetVarString() to Python 3.12.0a2 +#if PY_VERSION_HEX < 0x030C00A2 && !defined(PYPY_VERSION) +static inline PyObject* +PyFrame_GetVarString(PyFrameObject *frame, const char *name) +{ + PyObject *name_obj, *value; +#if PY_VERSION_HEX >= 0x03000000 + name_obj = PyUnicode_FromString(name); +#else + name_obj = PyString_FromString(name); +#endif + if (name_obj == NULL) { + return NULL; + } + value = PyFrame_GetVar(frame, name_obj); + Py_DECREF(name_obj); + return value; +} +#endif + + +// bpo-39947 added PyThreadState_GetInterpreter() to Python 3.9.0a5 +#if PY_VERSION_HEX < 0x030900A5 || (defined(PYPY_VERSION) && PY_VERSION_HEX < 0x030B0000) +static inline PyInterpreterState * +PyThreadState_GetInterpreter(PyThreadState *tstate) +{ + assert(tstate != _Py_NULL); + return tstate->interp; +} +#endif + + +// bpo-40429 added PyThreadState_GetFrame() to Python 3.9.0b1 +#if PY_VERSION_HEX < 0x030900B1 && !defined(PYPY_VERSION) +static inline PyFrameObject* PyThreadState_GetFrame(PyThreadState *tstate) +{ + assert(tstate != _Py_NULL); + return _Py_CAST(PyFrameObject *, Py_XNewRef(tstate->frame)); +} +#endif + +#if !defined(PYPY_VERSION) +static inline PyFrameObject* +_PyThreadState_GetFrameBorrow(PyThreadState *tstate) +{ + PyFrameObject *frame = PyThreadState_GetFrame(tstate); + Py_XDECREF(frame); + return frame; +} +#endif + + +// bpo-39947 added PyInterpreterState_Get() to Python 3.9.0a5 +#if PY_VERSION_HEX < 0x030900A5 || defined(PYPY_VERSION) +static inline PyInterpreterState* PyInterpreterState_Get(void) +{ + PyThreadState *tstate; + PyInterpreterState *interp; + + tstate = PyThreadState_GET(); + if (tstate == _Py_NULL) { + Py_FatalError("GIL released (tstate is NULL)"); + } + interp = tstate->interp; + if (interp == _Py_NULL) { + Py_FatalError("no current interpreter"); + } + return interp; +} +#endif + + +// bpo-39947 added PyInterpreterState_Get() to Python 3.9.0a6 +#if 0x030700A1 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030900A6 && !defined(PYPY_VERSION) +static inline uint64_t PyThreadState_GetID(PyThreadState *tstate) +{ + assert(tstate != _Py_NULL); + return tstate->id; +} +#endif + +// bpo-43760 added PyThreadState_EnterTracing() to Python 3.11.0a2 +#if PY_VERSION_HEX < 0x030B00A2 && !defined(PYPY_VERSION) +static inline void PyThreadState_EnterTracing(PyThreadState *tstate) +{ + tstate->tracing++; +#if PY_VERSION_HEX >= 0x030A00A1 + tstate->cframe->use_tracing = 0; +#else + tstate->use_tracing = 0; +#endif +} +#endif + +// bpo-43760 added PyThreadState_LeaveTracing() to Python 3.11.0a2 +#if PY_VERSION_HEX < 0x030B00A2 && !defined(PYPY_VERSION) +static inline void PyThreadState_LeaveTracing(PyThreadState *tstate) +{ + int use_tracing = (tstate->c_tracefunc != _Py_NULL + || tstate->c_profilefunc != _Py_NULL); + tstate->tracing--; +#if PY_VERSION_HEX >= 0x030A00A1 + tstate->cframe->use_tracing = use_tracing; +#else + tstate->use_tracing = use_tracing; +#endif +} +#endif + + +// bpo-37194 added PyObject_CallNoArgs() to Python 3.9.0a1 +// PyObject_CallNoArgs() added to PyPy 3.9.16-v7.3.11 +#if !defined(PyObject_CallNoArgs) && PY_VERSION_HEX < 0x030900A1 +static inline PyObject* PyObject_CallNoArgs(PyObject *func) +{ + return PyObject_CallFunctionObjArgs(func, NULL); +} +#endif + + +// bpo-39245 made PyObject_CallOneArg() public (previously called +// _PyObject_CallOneArg) in Python 3.9.0a4 +// PyObject_CallOneArg() added to PyPy 3.9.16-v7.3.11 +#if !defined(PyObject_CallOneArg) && PY_VERSION_HEX < 0x030900A4 +static inline PyObject* PyObject_CallOneArg(PyObject *func, PyObject *arg) +{ + return PyObject_CallFunctionObjArgs(func, arg, NULL); +} +#endif + + +// bpo-1635741 added PyModule_AddObjectRef() to Python 3.10.0a3 +#if PY_VERSION_HEX < 0x030A00A3 +static inline int +PyModule_AddObjectRef(PyObject *module, const char *name, PyObject *value) +{ + int res; + + if (!value && !PyErr_Occurred()) { + // PyModule_AddObject() raises TypeError in this case + PyErr_SetString(PyExc_SystemError, + "PyModule_AddObjectRef() must be called " + "with an exception raised if value is NULL"); + return -1; + } + + Py_XINCREF(value); + res = PyModule_AddObject(module, name, value); + if (res < 0) { + Py_XDECREF(value); + } + return res; +} +#endif + + +// bpo-40024 added PyModule_AddType() to Python 3.9.0a5 +#if PY_VERSION_HEX < 0x030900A5 +static inline int PyModule_AddType(PyObject *module, PyTypeObject *type) +{ + const char *name, *dot; + + if (PyType_Ready(type) < 0) { + return -1; + } + + // inline _PyType_Name() + name = type->tp_name; + assert(name != _Py_NULL); + dot = strrchr(name, '.'); + if (dot != _Py_NULL) { + name = dot + 1; + } + + return PyModule_AddObjectRef(module, name, _PyObject_CAST(type)); +} +#endif + + +// bpo-40241 added PyObject_GC_IsTracked() to Python 3.9.0a6. +// bpo-4688 added _PyObject_GC_IS_TRACKED() to Python 2.7.0a2. +#if PY_VERSION_HEX < 0x030900A6 && !defined(PYPY_VERSION) +static inline int PyObject_GC_IsTracked(PyObject* obj) +{ + return (PyObject_IS_GC(obj) && _PyObject_GC_IS_TRACKED(obj)); +} +#endif + +// bpo-40241 added PyObject_GC_IsFinalized() to Python 3.9.0a6. +// bpo-18112 added _PyGCHead_FINALIZED() to Python 3.4.0 final. +#if PY_VERSION_HEX < 0x030900A6 && PY_VERSION_HEX >= 0x030400F0 && !defined(PYPY_VERSION) +static inline int PyObject_GC_IsFinalized(PyObject *obj) +{ + PyGC_Head *gc = _Py_CAST(PyGC_Head*, obj) - 1; + return (PyObject_IS_GC(obj) && _PyGCHead_FINALIZED(gc)); +} +#endif + + +// bpo-39573 added Py_IS_TYPE() to Python 3.9.0a4 +#if PY_VERSION_HEX < 0x030900A4 && !defined(Py_IS_TYPE) +static inline int _Py_IS_TYPE(PyObject *ob, PyTypeObject *type) { + return Py_TYPE(ob) == type; +} +#define Py_IS_TYPE(ob, type) _Py_IS_TYPE(_PyObject_CAST(ob), type) +#endif + + +// bpo-46906 added PyFloat_Pack2() and PyFloat_Unpack2() to Python 3.11a7. +// bpo-11734 added _PyFloat_Pack2() and _PyFloat_Unpack2() to Python 3.6.0b1. +// Python 3.11a2 moved _PyFloat_Pack2() and _PyFloat_Unpack2() to the internal +// C API: Python 3.11a2-3.11a6 versions are not supported. +#if 0x030600B1 <= PY_VERSION_HEX && PY_VERSION_HEX <= 0x030B00A1 && !defined(PYPY_VERSION) +static inline int PyFloat_Pack2(double x, char *p, int le) +{ return _PyFloat_Pack2(x, (unsigned char*)p, le); } + +static inline double PyFloat_Unpack2(const char *p, int le) +{ return _PyFloat_Unpack2((const unsigned char *)p, le); } +#endif + + +// bpo-46906 added PyFloat_Pack4(), PyFloat_Pack8(), PyFloat_Unpack4() and +// PyFloat_Unpack8() to Python 3.11a7. +// Python 3.11a2 moved _PyFloat_Pack4(), _PyFloat_Pack8(), _PyFloat_Unpack4() +// and _PyFloat_Unpack8() to the internal C API: Python 3.11a2-3.11a6 versions +// are not supported. +#if PY_VERSION_HEX <= 0x030B00A1 && !defined(PYPY_VERSION) +static inline int PyFloat_Pack4(double x, char *p, int le) +{ return _PyFloat_Pack4(x, (unsigned char*)p, le); } + +static inline int PyFloat_Pack8(double x, char *p, int le) +{ return _PyFloat_Pack8(x, (unsigned char*)p, le); } + +static inline double PyFloat_Unpack4(const char *p, int le) +{ return _PyFloat_Unpack4((const unsigned char *)p, le); } + +static inline double PyFloat_Unpack8(const char *p, int le) +{ return _PyFloat_Unpack8((const unsigned char *)p, le); } +#endif + + +// gh-92154 added PyCode_GetCode() to Python 3.11.0b1 +#if PY_VERSION_HEX < 0x030B00B1 && !defined(PYPY_VERSION) +static inline PyObject* PyCode_GetCode(PyCodeObject *code) +{ + return Py_NewRef(code->co_code); +} +#endif + + +// gh-95008 added PyCode_GetVarnames() to Python 3.11.0rc1 +#if PY_VERSION_HEX < 0x030B00C1 && !defined(PYPY_VERSION) +static inline PyObject* PyCode_GetVarnames(PyCodeObject *code) +{ + return Py_NewRef(code->co_varnames); +} +#endif + +// gh-95008 added PyCode_GetFreevars() to Python 3.11.0rc1 +#if PY_VERSION_HEX < 0x030B00C1 && !defined(PYPY_VERSION) +static inline PyObject* PyCode_GetFreevars(PyCodeObject *code) +{ + return Py_NewRef(code->co_freevars); +} +#endif + +// gh-95008 added PyCode_GetCellvars() to Python 3.11.0rc1 +#if PY_VERSION_HEX < 0x030B00C1 && !defined(PYPY_VERSION) +static inline PyObject* PyCode_GetCellvars(PyCodeObject *code) +{ + return Py_NewRef(code->co_cellvars); +} +#endif + + +// Py_UNUSED() was added to Python 3.4.0b2. +#if PY_VERSION_HEX < 0x030400B2 && !defined(Py_UNUSED) +# if defined(__GNUC__) || defined(__clang__) +# define Py_UNUSED(name) _unused_ ## name __attribute__((unused)) +# else +# define Py_UNUSED(name) _unused_ ## name +# endif +#endif + + +// gh-105922 added PyImport_AddModuleRef() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A0 +static inline PyObject* PyImport_AddModuleRef(const char *name) +{ + return Py_XNewRef(PyImport_AddModule(name)); +} +#endif + + +// gh-105927 added PyWeakref_GetRef() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D0000 +static inline int PyWeakref_GetRef(PyObject *ref, PyObject **pobj) +{ + PyObject *obj; + if (ref != NULL && !PyWeakref_Check(ref)) { + *pobj = NULL; + PyErr_SetString(PyExc_TypeError, "expected a weakref"); + return -1; + } + obj = PyWeakref_GetObject(ref); + if (obj == NULL) { + // SystemError if ref is NULL + *pobj = NULL; + return -1; + } + if (obj == Py_None) { + *pobj = NULL; + return 0; + } + *pobj = Py_NewRef(obj); + return 1; +} +#endif + + +// bpo-36974 added PY_VECTORCALL_ARGUMENTS_OFFSET to Python 3.8b1 +#ifndef PY_VECTORCALL_ARGUMENTS_OFFSET +# define PY_VECTORCALL_ARGUMENTS_OFFSET (_Py_CAST(size_t, 1) << (8 * sizeof(size_t) - 1)) +#endif + +// bpo-36974 added PyVectorcall_NARGS() to Python 3.8b1 +#if PY_VERSION_HEX < 0x030800B1 +static inline Py_ssize_t PyVectorcall_NARGS(size_t n) +{ + return n & ~PY_VECTORCALL_ARGUMENTS_OFFSET; +} +#endif + + +// gh-105922 added PyObject_Vectorcall() to Python 3.9.0a4 +#if PY_VERSION_HEX < 0x030900A4 +static inline PyObject* +PyObject_Vectorcall(PyObject *callable, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ +#if PY_VERSION_HEX >= 0x030800B1 && !defined(PYPY_VERSION) + // bpo-36974 added _PyObject_Vectorcall() to Python 3.8.0b1 + return _PyObject_Vectorcall(callable, args, nargsf, kwnames); +#else + PyObject *posargs = NULL, *kwargs = NULL; + PyObject *res; + Py_ssize_t nposargs, nkwargs, i; + + if (nargsf != 0 && args == NULL) { + PyErr_BadInternalCall(); + goto error; + } + if (kwnames != NULL && !PyTuple_Check(kwnames)) { + PyErr_BadInternalCall(); + goto error; + } + + nposargs = (Py_ssize_t)PyVectorcall_NARGS(nargsf); + if (kwnames) { + nkwargs = PyTuple_GET_SIZE(kwnames); + } + else { + nkwargs = 0; + } + + posargs = PyTuple_New(nposargs); + if (posargs == NULL) { + goto error; + } + if (nposargs) { + for (i=0; i < nposargs; i++) { + PyTuple_SET_ITEM(posargs, i, Py_NewRef(*args)); + args++; + } + } + + if (nkwargs) { + kwargs = PyDict_New(); + if (kwargs == NULL) { + goto error; + } + + for (i = 0; i < nkwargs; i++) { + PyObject *key = PyTuple_GET_ITEM(kwnames, i); + PyObject *value = *args; + args++; + if (PyDict_SetItem(kwargs, key, value) < 0) { + goto error; + } + } + } + else { + kwargs = NULL; + } + + res = PyObject_Call(callable, posargs, kwargs); + Py_DECREF(posargs); + Py_XDECREF(kwargs); + return res; + +error: + Py_DECREF(posargs); + Py_XDECREF(kwargs); + return NULL; +#endif +} +#endif + + +// gh-106521 added PyObject_GetOptionalAttr() and +// PyObject_GetOptionalAttrString() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyObject_GetOptionalAttr(PyObject *obj, PyObject *attr_name, PyObject **result) +{ + // bpo-32571 added _PyObject_LookupAttr() to Python 3.7.0b1 +#if PY_VERSION_HEX >= 0x030700B1 && !defined(PYPY_VERSION) + return _PyObject_LookupAttr(obj, attr_name, result); +#else + *result = PyObject_GetAttr(obj, attr_name); + if (*result != NULL) { + return 1; + } + if (!PyErr_Occurred()) { + return 0; + } + if (PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_Clear(); + return 0; + } + return -1; +#endif +} + +static inline int +PyObject_GetOptionalAttrString(PyObject *obj, const char *attr_name, PyObject **result) +{ + PyObject *name_obj; + int rc; +#if PY_VERSION_HEX >= 0x03000000 + name_obj = PyUnicode_FromString(attr_name); +#else + name_obj = PyString_FromString(attr_name); +#endif + if (name_obj == NULL) { + *result = NULL; + return -1; + } + rc = PyObject_GetOptionalAttr(obj, name_obj, result); + Py_DECREF(name_obj); + return rc; +} +#endif + + +// gh-106307 added PyObject_GetOptionalAttr() and +// PyMapping_GetOptionalItemString() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyMapping_GetOptionalItem(PyObject *obj, PyObject *key, PyObject **result) +{ + *result = PyObject_GetItem(obj, key); + if (*result) { + return 1; + } + if (!PyErr_ExceptionMatches(PyExc_KeyError)) { + return -1; + } + PyErr_Clear(); + return 0; +} + +static inline int +PyMapping_GetOptionalItemString(PyObject *obj, const char *key, PyObject **result) +{ + PyObject *key_obj; + int rc; +#if PY_VERSION_HEX >= 0x03000000 + key_obj = PyUnicode_FromString(key); +#else + key_obj = PyString_FromString(key); +#endif + if (key_obj == NULL) { + *result = NULL; + return -1; + } + rc = PyMapping_GetOptionalItem(obj, key_obj, result); + Py_DECREF(key_obj); + return rc; +} +#endif + +// gh-108511 added PyMapping_HasKeyWithError() and +// PyMapping_HasKeyStringWithError() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyMapping_HasKeyWithError(PyObject *obj, PyObject *key) +{ + PyObject *res; + int rc = PyMapping_GetOptionalItem(obj, key, &res); + Py_XDECREF(res); + return rc; +} + +static inline int +PyMapping_HasKeyStringWithError(PyObject *obj, const char *key) +{ + PyObject *res; + int rc = PyMapping_GetOptionalItemString(obj, key, &res); + Py_XDECREF(res); + return rc; +} +#endif + + +// gh-108511 added PyObject_HasAttrWithError() and +// PyObject_HasAttrStringWithError() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyObject_HasAttrWithError(PyObject *obj, PyObject *attr) +{ + PyObject *res; + int rc = PyObject_GetOptionalAttr(obj, attr, &res); + Py_XDECREF(res); + return rc; +} + +static inline int +PyObject_HasAttrStringWithError(PyObject *obj, const char *attr) +{ + PyObject *res; + int rc = PyObject_GetOptionalAttrString(obj, attr, &res); + Py_XDECREF(res); + return rc; +} +#endif + + +// gh-106004 added PyDict_GetItemRef() and PyDict_GetItemStringRef() +// to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyDict_GetItemRef(PyObject *mp, PyObject *key, PyObject **result) +{ +#if PY_VERSION_HEX >= 0x03000000 + PyObject *item = PyDict_GetItemWithError(mp, key); +#else + PyObject *item = _PyDict_GetItemWithError(mp, key); +#endif + if (item != NULL) { + *result = Py_NewRef(item); + return 1; // found + } + if (!PyErr_Occurred()) { + *result = NULL; + return 0; // not found + } + *result = NULL; + return -1; +} + +static inline int +PyDict_GetItemStringRef(PyObject *mp, const char *key, PyObject **result) +{ + int res; +#if PY_VERSION_HEX >= 0x03000000 + PyObject *key_obj = PyUnicode_FromString(key); +#else + PyObject *key_obj = PyString_FromString(key); +#endif + if (key_obj == NULL) { + *result = NULL; + return -1; + } + res = PyDict_GetItemRef(mp, key_obj, result); + Py_DECREF(key_obj); + return res; +} +#endif + + +// gh-106307 added PyModule_Add() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyModule_Add(PyObject *mod, const char *name, PyObject *value) +{ + int res = PyModule_AddObjectRef(mod, name, value); + Py_XDECREF(value); + return res; +} +#endif + + +// gh-108014 added Py_IsFinalizing() to Python 3.13.0a1 +// bpo-1856 added _Py_Finalizing to Python 3.2.1b1. +// _Py_IsFinalizing() was added to PyPy 7.3.0. +#if (0x030201B1 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030D00A1) \ + && (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x7030000) +static inline int Py_IsFinalizing(void) +{ +#if PY_VERSION_HEX >= 0x030700A1 + // _Py_IsFinalizing() was added to Python 3.7.0a1. + return _Py_IsFinalizing(); +#else + return (_Py_Finalizing != NULL); +#endif +} +#endif + + +// gh-108323 added PyDict_ContainsString() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int PyDict_ContainsString(PyObject *op, const char *key) +{ + PyObject *key_obj = PyUnicode_FromString(key); + if (key_obj == NULL) { + return -1; + } + int res = PyDict_Contains(op, key_obj); + Py_DECREF(key_obj); + return res; +} +#endif + + +// gh-108445 added PyLong_AsInt() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int PyLong_AsInt(PyObject *obj) +{ +#ifdef PYPY_VERSION + long value = PyLong_AsLong(obj); + if (value == -1 && PyErr_Occurred()) { + return -1; + } + if (value < (long)INT_MIN || (long)INT_MAX < value) { + PyErr_SetString(PyExc_OverflowError, + "Python int too large to convert to C int"); + return -1; + } + return (int)value; +#else + return _PyLong_AsInt(obj); +#endif +} +#endif + + +// gh-107073 added PyObject_VisitManagedDict() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyObject_VisitManagedDict(PyObject *obj, visitproc visit, void *arg) +{ + PyObject **dict = _PyObject_GetDictPtr(obj); + if (dict == NULL || *dict == NULL) { + return -1; + } + Py_VISIT(*dict); + return 0; +} + +static inline void +PyObject_ClearManagedDict(PyObject *obj) +{ + PyObject **dict = _PyObject_GetDictPtr(obj); + if (dict == NULL || *dict == NULL) { + return; + } + Py_CLEAR(*dict); +} +#endif + +// gh-108867 added PyThreadState_GetUnchecked() to Python 3.13.0a1 +// Python 3.5.2 added _PyThreadState_UncheckedGet(). +#if PY_VERSION_HEX >= 0x03050200 && PY_VERSION_HEX < 0x030D00A1 +static inline PyThreadState* +PyThreadState_GetUnchecked(void) +{ + return _PyThreadState_UncheckedGet(); +} +#endif + +// gh-110289 added PyUnicode_EqualToUTF8() and PyUnicode_EqualToUTF8AndSize() +// to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyUnicode_EqualToUTF8AndSize(PyObject *unicode, const char *str, Py_ssize_t str_len) +{ + Py_ssize_t len; + const void *utf8; + PyObject *exc_type, *exc_value, *exc_tb; + int res; + + // API cannot report errors so save/restore the exception + PyErr_Fetch(&exc_type, &exc_value, &exc_tb); + + // Python 3.3.0a1 added PyUnicode_AsUTF8AndSize() +#if PY_VERSION_HEX >= 0x030300A1 + if (PyUnicode_IS_ASCII(unicode)) { + utf8 = PyUnicode_DATA(unicode); + len = PyUnicode_GET_LENGTH(unicode); + } + else { + utf8 = PyUnicode_AsUTF8AndSize(unicode, &len); + if (utf8 == NULL) { + // Memory allocation failure. The API cannot report error, + // so ignore the exception and return 0. + res = 0; + goto done; + } + } + + if (len != str_len) { + res = 0; + goto done; + } + res = (memcmp(utf8, str, (size_t)len) == 0); +#else + PyObject *bytes = PyUnicode_AsUTF8String(unicode); + if (bytes == NULL) { + // Memory allocation failure. The API cannot report error, + // so ignore the exception and return 0. + res = 0; + goto done; + } + +#if PY_VERSION_HEX >= 0x03000000 + len = PyBytes_GET_SIZE(bytes); + utf8 = PyBytes_AS_STRING(bytes); +#else + len = PyString_GET_SIZE(bytes); + utf8 = PyString_AS_STRING(bytes); +#endif + if (len != str_len) { + Py_DECREF(bytes); + res = 0; + goto done; + } + + res = (memcmp(utf8, str, (size_t)len) == 0); + Py_DECREF(bytes); +#endif + +done: + PyErr_Restore(exc_type, exc_value, exc_tb); + return res; +} + +static inline int +PyUnicode_EqualToUTF8(PyObject *unicode, const char *str) +{ + return PyUnicode_EqualToUTF8AndSize(unicode, str, (Py_ssize_t)strlen(str)); +} +#endif + + +// gh-111138 added PyList_Extend() and PyList_Clear() to Python 3.13.0a2 +#if PY_VERSION_HEX < 0x030D00A2 +static inline int +PyList_Extend(PyObject *list, PyObject *iterable) +{ + return PyList_SetSlice(list, PY_SSIZE_T_MAX, PY_SSIZE_T_MAX, iterable); +} + +static inline int +PyList_Clear(PyObject *list) +{ + return PyList_SetSlice(list, 0, PY_SSIZE_T_MAX, NULL); +} +#endif + +// gh-111262 added PyDict_Pop() and PyDict_PopString() to Python 3.13.0a2 +#if PY_VERSION_HEX < 0x030D00A2 +static inline int +PyDict_Pop(PyObject *dict, PyObject *key, PyObject **result) +{ + PyObject *value; + + if (!PyDict_Check(dict)) { + PyErr_BadInternalCall(); + if (result) { + *result = NULL; + } + return -1; + } + + // bpo-16991 added _PyDict_Pop() to Python 3.5.0b2. + // Python 3.6.0b3 changed _PyDict_Pop() first argument type to PyObject*. + // Python 3.13.0a1 removed _PyDict_Pop(). +#if defined(PYPY_VERSION) || PY_VERSION_HEX < 0x030500b2 || PY_VERSION_HEX >= 0x030D0000 + value = PyObject_CallMethod(dict, "pop", "O", key); +#elif PY_VERSION_HEX < 0x030600b3 + value = _PyDict_Pop(_Py_CAST(PyDictObject*, dict), key, NULL); +#else + value = _PyDict_Pop(dict, key, NULL); +#endif + if (value == NULL) { + if (result) { + *result = NULL; + } + if (PyErr_Occurred() && !PyErr_ExceptionMatches(PyExc_KeyError)) { + return -1; + } + PyErr_Clear(); + return 0; + } + if (result) { + *result = value; + } + else { + Py_DECREF(value); + } + return 1; +} + +static inline int +PyDict_PopString(PyObject *dict, const char *key, PyObject **result) +{ + PyObject *key_obj = PyUnicode_FromString(key); + if (key_obj == NULL) { + if (result != NULL) { + *result = NULL; + } + return -1; + } + + int res = PyDict_Pop(dict, key_obj, result); + Py_DECREF(key_obj); + return res; +} +#endif + + +#if PY_VERSION_HEX < 0x030200A4 +// Python 3.2.0a4 added Py_hash_t type +typedef Py_ssize_t Py_hash_t; +#endif + + +// gh-111545 added Py_HashPointer() to Python 3.13.0a3 +#if PY_VERSION_HEX < 0x030D00A3 +static inline Py_hash_t Py_HashPointer(const void *ptr) +{ +#if PY_VERSION_HEX >= 0x030900A4 && !defined(PYPY_VERSION) + return _Py_HashPointer(ptr); +#else + return _Py_HashPointer(_Py_CAST(void*, ptr)); +#endif +} +#endif + + +// Python 3.13a4 added a PyTime API. +// Use the private API added to Python 3.5. +#if PY_VERSION_HEX < 0x030D00A4 && PY_VERSION_HEX >= 0x03050000 +typedef _PyTime_t PyTime_t; +#define PyTime_MIN _PyTime_MIN +#define PyTime_MAX _PyTime_MAX + +static inline double PyTime_AsSecondsDouble(PyTime_t t) +{ return _PyTime_AsSecondsDouble(t); } + +static inline int PyTime_Monotonic(PyTime_t *result) +{ return _PyTime_GetMonotonicClockWithInfo(result, NULL); } + +static inline int PyTime_Time(PyTime_t *result) +{ return _PyTime_GetSystemClockWithInfo(result, NULL); } + +static inline int PyTime_PerfCounter(PyTime_t *result) +{ +#if PY_VERSION_HEX >= 0x03070000 && !defined(PYPY_VERSION) + return _PyTime_GetPerfCounterWithInfo(result, NULL); +#elif PY_VERSION_HEX >= 0x03070000 + // Call time.perf_counter_ns() and convert Python int object to PyTime_t. + // Cache time.perf_counter_ns() function for best performance. + static PyObject *func = NULL; + if (func == NULL) { + PyObject *mod = PyImport_ImportModule("time"); + if (mod == NULL) { + return -1; + } + + func = PyObject_GetAttrString(mod, "perf_counter_ns"); + Py_DECREF(mod); + if (func == NULL) { + return -1; + } + } + + PyObject *res = PyObject_CallNoArgs(func); + if (res == NULL) { + return -1; + } + long long value = PyLong_AsLongLong(res); + Py_DECREF(res); + + if (value == -1 && PyErr_Occurred()) { + return -1; + } + + Py_BUILD_ASSERT(sizeof(value) >= sizeof(PyTime_t)); + *result = (PyTime_t)value; + return 0; +#else + // Call time.perf_counter() and convert C double to PyTime_t. + // Cache time.perf_counter() function for best performance. + static PyObject *func = NULL; + if (func == NULL) { + PyObject *mod = PyImport_ImportModule("time"); + if (mod == NULL) { + return -1; + } + + func = PyObject_GetAttrString(mod, "perf_counter"); + Py_DECREF(mod); + if (func == NULL) { + return -1; + } + } + + PyObject *res = PyObject_CallNoArgs(func); + if (res == NULL) { + return -1; + } + double d = PyFloat_AsDouble(res); + Py_DECREF(res); + + if (d == -1.0 && PyErr_Occurred()) { + return -1; + } + + // Avoid floor() to avoid having to link to libm + *result = (PyTime_t)(d * 1e9); + return 0; +#endif +} + +#endif + +// gh-111389 added hash constants to Python 3.13.0a5. These constants were +// added first as private macros to Python 3.4.0b1 and PyPy 7.3.8. +#if (!defined(PyHASH_BITS) \ + && ((!defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x030400B1) \ + || (defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x03070000 \ + && PYPY_VERSION_NUM >= 0x07030800))) +# define PyHASH_BITS _PyHASH_BITS +# define PyHASH_MODULUS _PyHASH_MODULUS +# define PyHASH_INF _PyHASH_INF +# define PyHASH_IMAG _PyHASH_IMAG +#endif + + +// gh-111545 added Py_GetConstant() and Py_GetConstantBorrowed() +// to Python 3.13.0a6 +#if PY_VERSION_HEX < 0x030D00A6 && !defined(Py_CONSTANT_NONE) + +#define Py_CONSTANT_NONE 0 +#define Py_CONSTANT_FALSE 1 +#define Py_CONSTANT_TRUE 2 +#define Py_CONSTANT_ELLIPSIS 3 +#define Py_CONSTANT_NOT_IMPLEMENTED 4 +#define Py_CONSTANT_ZERO 5 +#define Py_CONSTANT_ONE 6 +#define Py_CONSTANT_EMPTY_STR 7 +#define Py_CONSTANT_EMPTY_BYTES 8 +#define Py_CONSTANT_EMPTY_TUPLE 9 + +static inline PyObject* Py_GetConstant(unsigned int constant_id) +{ + static PyObject* constants[Py_CONSTANT_EMPTY_TUPLE + 1] = {NULL}; + + if (constants[Py_CONSTANT_NONE] == NULL) { + constants[Py_CONSTANT_NONE] = Py_None; + constants[Py_CONSTANT_FALSE] = Py_False; + constants[Py_CONSTANT_TRUE] = Py_True; + constants[Py_CONSTANT_ELLIPSIS] = Py_Ellipsis; + constants[Py_CONSTANT_NOT_IMPLEMENTED] = Py_NotImplemented; + + constants[Py_CONSTANT_ZERO] = PyLong_FromLong(0); + if (constants[Py_CONSTANT_ZERO] == NULL) { + goto fatal_error; + } + + constants[Py_CONSTANT_ONE] = PyLong_FromLong(1); + if (constants[Py_CONSTANT_ONE] == NULL) { + goto fatal_error; + } + + constants[Py_CONSTANT_EMPTY_STR] = PyUnicode_FromStringAndSize("", 0); + if (constants[Py_CONSTANT_EMPTY_STR] == NULL) { + goto fatal_error; + } + + constants[Py_CONSTANT_EMPTY_BYTES] = PyBytes_FromStringAndSize("", 0); + if (constants[Py_CONSTANT_EMPTY_BYTES] == NULL) { + goto fatal_error; + } + + constants[Py_CONSTANT_EMPTY_TUPLE] = PyTuple_New(0); + if (constants[Py_CONSTANT_EMPTY_TUPLE] == NULL) { + goto fatal_error; + } + // goto dance to avoid compiler warnings about Py_FatalError() + goto init_done; + +fatal_error: + // This case should never happen + Py_FatalError("Py_GetConstant() failed to get constants"); + } + +init_done: + if (constant_id <= Py_CONSTANT_EMPTY_TUPLE) { + return Py_NewRef(constants[constant_id]); + } + else { + PyErr_BadInternalCall(); + return NULL; + } +} + +static inline PyObject* Py_GetConstantBorrowed(unsigned int constant_id) +{ + PyObject *obj = Py_GetConstant(constant_id); + Py_XDECREF(obj); + return obj; +} +#endif + + +// gh-114329 added PyList_GetItemRef() to Python 3.13.0a4 +#if PY_VERSION_HEX < 0x030D00A4 +static inline PyObject * +PyList_GetItemRef(PyObject *op, Py_ssize_t index) +{ + PyObject *item = PyList_GetItem(op, index); + Py_XINCREF(item); + return item; +} +#endif + + +// gh-114329 added PyList_GetItemRef() to Python 3.13.0a4 +#if PY_VERSION_HEX < 0x030D00A4 +static inline int +PyDict_SetDefaultRef(PyObject *d, PyObject *key, PyObject *default_value, + PyObject **result) +{ + PyObject *value; + if (PyDict_GetItemRef(d, key, &value) < 0) { + // get error + if (result) { + *result = NULL; + } + return -1; + } + if (value != NULL) { + // present + if (result) { + *result = value; + } + else { + Py_DECREF(value); + } + return 1; + } + + // missing: set the item + if (PyDict_SetItem(d, key, default_value) < 0) { + // set error + if (result) { + *result = NULL; + } + return -1; + } + if (result) { + *result = Py_NewRef(default_value); + } + return 0; +} +#endif + +#if PY_VERSION_HEX < 0x030D00B3 +# define Py_BEGIN_CRITICAL_SECTION(op) { +# define Py_END_CRITICAL_SECTION() } +# define Py_BEGIN_CRITICAL_SECTION2(a, b) { +# define Py_END_CRITICAL_SECTION2() } +#endif + +#if PY_VERSION_HEX < 0x030E0000 && PY_VERSION_HEX >= 0x03060000 && !defined(PYPY_VERSION) +typedef struct PyUnicodeWriter PyUnicodeWriter; + +static inline void PyUnicodeWriter_Discard(PyUnicodeWriter *writer) +{ + _PyUnicodeWriter_Dealloc((_PyUnicodeWriter*)writer); + PyMem_Free(writer); +} + +static inline PyUnicodeWriter* PyUnicodeWriter_Create(Py_ssize_t length) +{ + if (length < 0) { + PyErr_SetString(PyExc_ValueError, + "length must be positive"); + return NULL; + } + + const size_t size = sizeof(_PyUnicodeWriter); + PyUnicodeWriter *pub_writer = (PyUnicodeWriter *)PyMem_Malloc(size); + if (pub_writer == _Py_NULL) { + PyErr_NoMemory(); + return _Py_NULL; + } + _PyUnicodeWriter *writer = (_PyUnicodeWriter *)pub_writer; + + _PyUnicodeWriter_Init(writer); + if (_PyUnicodeWriter_Prepare(writer, length, 127) < 0) { + PyUnicodeWriter_Discard(pub_writer); + return NULL; + } + writer->overallocate = 1; + return pub_writer; +} + +static inline PyObject* PyUnicodeWriter_Finish(PyUnicodeWriter *writer) +{ + PyObject *str = _PyUnicodeWriter_Finish((_PyUnicodeWriter*)writer); + assert(((_PyUnicodeWriter*)writer)->buffer == NULL); + PyMem_Free(writer); + return str; +} + +static inline int +PyUnicodeWriter_WriteChar(PyUnicodeWriter *writer, Py_UCS4 ch) +{ + if (ch > 0x10ffff) { + PyErr_SetString(PyExc_ValueError, + "character must be in range(0x110000)"); + return -1; + } + + return _PyUnicodeWriter_WriteChar((_PyUnicodeWriter*)writer, ch); +} + +static inline int +PyUnicodeWriter_WriteStr(PyUnicodeWriter *writer, PyObject *obj) +{ + PyObject *str = PyObject_Str(obj); + if (str == NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} + +static inline int +PyUnicodeWriter_WriteRepr(PyUnicodeWriter *writer, PyObject *obj) +{ + PyObject *str = PyObject_Repr(obj); + if (str == NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} + +static inline int +PyUnicodeWriter_WriteUTF8(PyUnicodeWriter *writer, + const char *str, Py_ssize_t size) +{ + if (size < 0) { + size = (Py_ssize_t)strlen(str); + } + + PyObject *str_obj = PyUnicode_FromStringAndSize(str, size); + if (str_obj == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str_obj); + Py_DECREF(str_obj); + return res; +} + +static inline int +PyUnicodeWriter_WriteWideChar(PyUnicodeWriter *writer, + const wchar_t *str, Py_ssize_t size) +{ + if (size < 0) { + size = (Py_ssize_t)wcslen(str); + } + + PyObject *str_obj = PyUnicode_FromWideChar(str, size); + if (str_obj == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str_obj); + Py_DECREF(str_obj); + return res; +} + +static inline int +PyUnicodeWriter_WriteSubstring(PyUnicodeWriter *writer, PyObject *str, + Py_ssize_t start, Py_ssize_t end) +{ + if (!PyUnicode_Check(str)) { + PyErr_Format(PyExc_TypeError, "expect str, not %T", str); + return -1; + } + if (start < 0 || start > end) { + PyErr_Format(PyExc_ValueError, "invalid start argument"); + return -1; + } + if (end > PyUnicode_GET_LENGTH(str)) { + PyErr_Format(PyExc_ValueError, "invalid end argument"); + return -1; + } + + return _PyUnicodeWriter_WriteSubstring((_PyUnicodeWriter*)writer, str, + start, end); +} + +static inline int +PyUnicodeWriter_Format(PyUnicodeWriter *writer, const char *format, ...) +{ + va_list vargs; + va_start(vargs, format); + PyObject *str = PyUnicode_FromFormatV(format, vargs); + va_end(vargs); + if (str == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} +#endif // PY_VERSION_HEX < 0x030E0000 + +// gh-116560 added PyLong_GetSign() to Python 3.14.0a0 +#if PY_VERSION_HEX < 0x030E00A0 +static inline int PyLong_GetSign(PyObject *obj, int *sign) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expect int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + + *sign = _PyLong_Sign(obj); + return 0; +} +#endif + +// gh-126061 added PyLong_IsPositive/Negative/Zero() to Python in 3.14.0a2 +#if PY_VERSION_HEX < 0x030E00A2 +static inline int PyLong_IsPositive(PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + return _PyLong_Sign(obj) == 1; +} + +static inline int PyLong_IsNegative(PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + return _PyLong_Sign(obj) == -1; +} + +static inline int PyLong_IsZero(PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + return _PyLong_Sign(obj) == 0; +} +#endif + + +// gh-124502 added PyUnicode_Equal() to Python 3.14.0a0 +#if PY_VERSION_HEX < 0x030E00A0 +static inline int PyUnicode_Equal(PyObject *str1, PyObject *str2) +{ + if (!PyUnicode_Check(str1)) { + PyErr_Format(PyExc_TypeError, "first argument must be str, not %s", + Py_TYPE(str1)->tp_name); + return -1; + } + if (!PyUnicode_Check(str2)) { + PyErr_Format(PyExc_TypeError, "second argument must be str, not %s", + Py_TYPE(str2)->tp_name); + return -1; + } + +#if PY_VERSION_HEX >= 0x030d0000 && !defined(PYPY_VERSION) + PyAPI_FUNC(int) _PyUnicode_Equal(PyObject *str1, PyObject *str2); + + return _PyUnicode_Equal(str1, str2); +#elif PY_VERSION_HEX >= 0x03060000 && !defined(PYPY_VERSION) + return _PyUnicode_EQ(str1, str2); +#elif PY_VERSION_HEX >= 0x03090000 && defined(PYPY_VERSION) + return _PyUnicode_EQ(str1, str2); +#else + return (PyUnicode_Compare(str1, str2) == 0); +#endif +} +#endif + + +// gh-121645 added PyBytes_Join() to Python 3.14.0a0 +#if PY_VERSION_HEX < 0x030E00A0 +static inline PyObject* PyBytes_Join(PyObject *sep, PyObject *iterable) +{ + return _PyBytes_Join(sep, iterable); +} +#endif + + +#if PY_VERSION_HEX < 0x030E00A0 +static inline Py_hash_t Py_HashBuffer(const void *ptr, Py_ssize_t len) +{ +#if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) + PyAPI_FUNC(Py_hash_t) _Py_HashBytes(const void *src, Py_ssize_t len); + + return _Py_HashBytes(ptr, len); +#else + Py_hash_t hash; + PyObject *bytes = PyBytes_FromStringAndSize((const char*)ptr, len); + if (bytes == NULL) { + return -1; + } + hash = PyObject_Hash(bytes); + Py_DECREF(bytes); + return hash; +#endif +} +#endif + + +#if PY_VERSION_HEX < 0x030E00A0 +static inline int PyIter_NextItem(PyObject *iter, PyObject **item) +{ + iternextfunc tp_iternext; + + assert(iter != NULL); + assert(item != NULL); + + tp_iternext = Py_TYPE(iter)->tp_iternext; + if (tp_iternext == NULL) { + *item = NULL; + PyErr_Format(PyExc_TypeError, "expected an iterator, got '%s'", + Py_TYPE(iter)->tp_name); + return -1; + } + + if ((*item = tp_iternext(iter))) { + return 1; + } + if (!PyErr_Occurred()) { + return 0; + } + if (PyErr_ExceptionMatches(PyExc_StopIteration)) { + PyErr_Clear(); + return 0; + } + return -1; +} +#endif + + +#if PY_VERSION_HEX < 0x030E00A0 +static inline PyObject* PyLong_FromInt32(int32_t value) +{ + Py_BUILD_ASSERT(sizeof(long) >= 4); + return PyLong_FromLong(value); +} + +static inline PyObject* PyLong_FromInt64(int64_t value) +{ + Py_BUILD_ASSERT(sizeof(long long) >= 8); + return PyLong_FromLongLong(value); +} + +static inline PyObject* PyLong_FromUInt32(uint32_t value) +{ + Py_BUILD_ASSERT(sizeof(unsigned long) >= 4); + return PyLong_FromUnsignedLong(value); +} + +static inline PyObject* PyLong_FromUInt64(uint64_t value) +{ + Py_BUILD_ASSERT(sizeof(unsigned long long) >= 8); + return PyLong_FromUnsignedLongLong(value); +} + +static inline int PyLong_AsInt32(PyObject *obj, int32_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(int) == 4); + int value = PyLong_AsInt(obj); + if (value == -1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (int32_t)value; + return 0; +} + +static inline int PyLong_AsInt64(PyObject *obj, int64_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long long) == 8); + long long value = PyLong_AsLongLong(obj); + if (value == -1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (int64_t)value; + return 0; +} + +static inline int PyLong_AsUInt32(PyObject *obj, uint32_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long) >= 4); + unsigned long value = PyLong_AsUnsignedLong(obj); + if (value == (unsigned long)-1 && PyErr_Occurred()) { + return -1; + } +#if SIZEOF_LONG > 4 + if ((unsigned long)UINT32_MAX < value) { + PyErr_SetString(PyExc_OverflowError, + "Python int too large to convert to C uint32_t"); + return -1; + } +#endif + *pvalue = (uint32_t)value; + return 0; +} + +static inline int PyLong_AsUInt64(PyObject *obj, uint64_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long long) == 8); + unsigned long long value = PyLong_AsUnsignedLongLong(obj); + if (value == (unsigned long long)-1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (uint64_t)value; + return 0; +} +#endif + + +// gh-102471 added import and export API for integers to 3.14.0a2. +#if PY_VERSION_HEX < 0x030E00A2 && PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) +// Helpers to access PyLongObject internals. +static inline void +_PyLong_SetSignAndDigitCount(PyLongObject *op, int sign, Py_ssize_t size) +{ +#if PY_VERSION_HEX >= 0x030C0000 + op->long_value.lv_tag = (uintptr_t)(1 - sign) | ((uintptr_t)(size) << 3); +#elif PY_VERSION_HEX >= 0x030900A4 + Py_SET_SIZE(op, sign * size); +#else + Py_SIZE(op) = sign * size; +#endif +} + +static inline Py_ssize_t +_PyLong_DigitCount(const PyLongObject *op) +{ +#if PY_VERSION_HEX >= 0x030C0000 + return (Py_ssize_t)(op->long_value.lv_tag >> 3); +#else + return _PyLong_Sign((PyObject*)op) < 0 ? -Py_SIZE(op) : Py_SIZE(op); +#endif +} + +static inline digit* +_PyLong_GetDigits(const PyLongObject *op) +{ +#if PY_VERSION_HEX >= 0x030C0000 + return (digit*)(op->long_value.ob_digit); +#else + return (digit*)(op->ob_digit); +#endif +} + +typedef struct PyLongLayout { + uint8_t bits_per_digit; + uint8_t digit_size; + int8_t digits_order; + int8_t digit_endianness; +} PyLongLayout; + +typedef struct PyLongExport { + int64_t value; + uint8_t negative; + Py_ssize_t ndigits; + const void *digits; + Py_uintptr_t _reserved; +} PyLongExport; + +typedef struct PyLongWriter PyLongWriter; + +static inline const PyLongLayout* +PyLong_GetNativeLayout(void) +{ + static const PyLongLayout PyLong_LAYOUT = { + PyLong_SHIFT, + sizeof(digit), + -1, // least significant first + PY_LITTLE_ENDIAN ? -1 : 1, + }; + + return &PyLong_LAYOUT; +} + +static inline int +PyLong_Export(PyObject *obj, PyLongExport *export_long) +{ + if (!PyLong_Check(obj)) { + memset(export_long, 0, sizeof(*export_long)); + PyErr_Format(PyExc_TypeError, "expected int, got %s", + Py_TYPE(obj)->tp_name); + return -1; + } + + // Fast-path: try to convert to a int64_t + PyLongObject *self = (PyLongObject*)obj; + int overflow; +#if SIZEOF_LONG == 8 + long value = PyLong_AsLongAndOverflow(obj, &overflow); +#else + // Windows has 32-bit long, so use 64-bit long long instead + long long value = PyLong_AsLongLongAndOverflow(obj, &overflow); +#endif + Py_BUILD_ASSERT(sizeof(value) == sizeof(int64_t)); + // the function cannot fail since obj is a PyLongObject + assert(!(value == -1 && PyErr_Occurred())); + + if (!overflow) { + export_long->value = value; + export_long->negative = 0; + export_long->ndigits = 0; + export_long->digits = 0; + export_long->_reserved = 0; + } + else { + export_long->value = 0; + export_long->negative = _PyLong_Sign(obj) < 0; + export_long->ndigits = _PyLong_DigitCount(self); + if (export_long->ndigits == 0) { + export_long->ndigits = 1; + } + export_long->digits = _PyLong_GetDigits(self); + export_long->_reserved = (Py_uintptr_t)Py_NewRef(obj); + } + return 0; +} + +static inline void +PyLong_FreeExport(PyLongExport *export_long) +{ + PyObject *obj = (PyObject*)export_long->_reserved; + + if (obj) { + export_long->_reserved = 0; + Py_DECREF(obj); + } +} + +static inline PyLongWriter* +PyLongWriter_Create(int negative, Py_ssize_t ndigits, void **digits) +{ + if (ndigits <= 0) { + PyErr_SetString(PyExc_ValueError, "ndigits must be positive"); + return NULL; + } + assert(digits != NULL); + + PyLongObject *obj = _PyLong_New(ndigits); + if (obj == NULL) { + return NULL; + } + _PyLong_SetSignAndDigitCount(obj, negative?-1:1, ndigits); + + *digits = _PyLong_GetDigits(obj); + return (PyLongWriter*)obj; +} + +static inline void +PyLongWriter_Discard(PyLongWriter *writer) +{ + PyLongObject *obj = (PyLongObject *)writer; + + assert(Py_REFCNT(obj) == 1); + Py_DECREF(obj); +} + +static inline PyObject* +PyLongWriter_Finish(PyLongWriter *writer) +{ + PyObject *obj = (PyObject *)writer; + PyLongObject *self = (PyLongObject*)obj; + Py_ssize_t j = _PyLong_DigitCount(self); + Py_ssize_t i = j; + int sign = _PyLong_Sign(obj); + + assert(Py_REFCNT(obj) == 1); + + // Normalize and get singleton if possible + while (i > 0 && _PyLong_GetDigits(self)[i-1] == 0) { + --i; + } + if (i != j) { + if (i == 0) { + sign = 0; + } + _PyLong_SetSignAndDigitCount(self, sign, i); + } + if (i <= 1) { + long val = sign * (long)(_PyLong_GetDigits(self)[0]); + Py_DECREF(obj); + return PyLong_FromLong(val); + } + + return obj; +} +#endif + + +#if PY_VERSION_HEX < 0x030C00A3 +# define Py_T_SHORT T_SHORT +# define Py_T_INT T_INT +# define Py_T_LONG T_LONG +# define Py_T_FLOAT T_FLOAT +# define Py_T_DOUBLE T_DOUBLE +# define Py_T_STRING T_STRING +# define _Py_T_OBJECT T_OBJECT +# define Py_T_CHAR T_CHAR +# define Py_T_BYTE T_BYTE +# define Py_T_UBYTE T_UBYTE +# define Py_T_USHORT T_USHORT +# define Py_T_UINT T_UINT +# define Py_T_ULONG T_ULONG +# define Py_T_STRING_INPLACE T_STRING_INPLACE +# define Py_T_BOOL T_BOOL +# define Py_T_OBJECT_EX T_OBJECT_EX +# define Py_T_LONGLONG T_LONGLONG +# define Py_T_ULONGLONG T_ULONGLONG +# define Py_T_PYSSIZET T_PYSSIZET + +# if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) +# define _Py_T_NONE T_NONE +# endif + +# define Py_READONLY READONLY +# define Py_AUDIT_READ READ_RESTRICTED +# define _Py_WRITE_RESTRICTED PY_WRITE_RESTRICTED +#endif + + +// gh-127350 added Py_fopen() and Py_fclose() to Python 3.14a4 +#if PY_VERSION_HEX < 0x030E00A4 +static inline FILE* Py_fopen(PyObject *path, const char *mode) +{ +#if 0x030400A2 <= PY_VERSION_HEX && !defined(PYPY_VERSION) + PyAPI_FUNC(FILE*) _Py_fopen_obj(PyObject *path, const char *mode); + + return _Py_fopen_obj(path, mode); +#else + FILE *f; + PyObject *bytes; +#if PY_VERSION_HEX >= 0x03000000 + if (!PyUnicode_FSConverter(path, &bytes)) { + return NULL; + } +#else + if (!PyString_Check(path)) { + PyErr_SetString(PyExc_TypeError, "except str"); + return NULL; + } + bytes = Py_NewRef(path); +#endif + const char *path_bytes = PyBytes_AS_STRING(bytes); + + f = fopen(path_bytes, mode); + Py_DECREF(bytes); + + if (f == NULL) { + PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, path); + return NULL; + } + return f; +#endif +} + +static inline int Py_fclose(FILE *file) +{ + return fclose(file); +} +#endif + + +#if 0x03090000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030E0000 && !defined(PYPY_VERSION) +static inline PyObject* +PyConfig_Get(const char *name) +{ + typedef enum { + _PyConfig_MEMBER_INT, + _PyConfig_MEMBER_UINT, + _PyConfig_MEMBER_ULONG, + _PyConfig_MEMBER_BOOL, + _PyConfig_MEMBER_WSTR, + _PyConfig_MEMBER_WSTR_OPT, + _PyConfig_MEMBER_WSTR_LIST, + } PyConfigMemberType; + + typedef struct { + const char *name; + size_t offset; + PyConfigMemberType type; + const char *sys_attr; + } PyConfigSpec; + +#define PYTHONCAPI_COMPAT_SPEC(MEMBER, TYPE, sys_attr) \ + {#MEMBER, offsetof(PyConfig, MEMBER), \ + _PyConfig_MEMBER_##TYPE, sys_attr} + + static const PyConfigSpec config_spec[] = { + PYTHONCAPI_COMPAT_SPEC(argv, WSTR_LIST, "argv"), + PYTHONCAPI_COMPAT_SPEC(base_exec_prefix, WSTR_OPT, "base_exec_prefix"), + PYTHONCAPI_COMPAT_SPEC(base_executable, WSTR_OPT, "_base_executable"), + PYTHONCAPI_COMPAT_SPEC(base_prefix, WSTR_OPT, "base_prefix"), + PYTHONCAPI_COMPAT_SPEC(bytes_warning, UINT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(exec_prefix, WSTR_OPT, "exec_prefix"), + PYTHONCAPI_COMPAT_SPEC(executable, WSTR_OPT, "executable"), + PYTHONCAPI_COMPAT_SPEC(inspect, BOOL, _Py_NULL), +#if 0x030C0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(int_max_str_digits, UINT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(interactive, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(module_search_paths, WSTR_LIST, "path"), + PYTHONCAPI_COMPAT_SPEC(optimization_level, UINT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(parser_debug, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(platlibdir, WSTR, "platlibdir"), + PYTHONCAPI_COMPAT_SPEC(prefix, WSTR_OPT, "prefix"), + PYTHONCAPI_COMPAT_SPEC(pycache_prefix, WSTR_OPT, "pycache_prefix"), + PYTHONCAPI_COMPAT_SPEC(quiet, BOOL, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(stdlib_dir, WSTR_OPT, "_stdlib_dir"), +#endif + PYTHONCAPI_COMPAT_SPEC(use_environment, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(verbose, UINT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(warnoptions, WSTR_LIST, "warnoptions"), + PYTHONCAPI_COMPAT_SPEC(write_bytecode, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(xoptions, WSTR_LIST, "_xoptions"), + PYTHONCAPI_COMPAT_SPEC(buffered_stdio, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(check_hash_pycs_mode, WSTR, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(code_debug_ranges, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(configure_c_stdio, BOOL, _Py_NULL), +#if 0x030D0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(cpu_count, INT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(dev_mode, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(dump_refs, BOOL, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(dump_refs_file, WSTR_OPT, _Py_NULL), +#endif +#ifdef Py_GIL_DISABLED + PYTHONCAPI_COMPAT_SPEC(enable_gil, INT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(faulthandler, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(filesystem_encoding, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(filesystem_errors, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(hash_seed, ULONG, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(home, WSTR_OPT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(import_time, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(install_signal_handlers, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(isolated, BOOL, _Py_NULL), +#ifdef MS_WINDOWS + PYTHONCAPI_COMPAT_SPEC(legacy_windows_stdio, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(malloc_stats, BOOL, _Py_NULL), +#if 0x030A0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(orig_argv, WSTR_LIST, "orig_argv"), +#endif + PYTHONCAPI_COMPAT_SPEC(parse_argv, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(pathconfig_warnings, BOOL, _Py_NULL), +#if 0x030C0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(perf_profiling, UINT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(program_name, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(run_command, WSTR_OPT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(run_filename, WSTR_OPT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(run_module, WSTR_OPT, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(safe_path, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(show_ref_count, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(site_import, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(skip_source_first_line, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(stdio_encoding, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(stdio_errors, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(tracemalloc, UINT, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(use_frozen_modules, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(use_hash_seed, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(user_site_directory, BOOL, _Py_NULL), +#if 0x030A0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(warn_default_encoding, BOOL, _Py_NULL), +#endif + }; + +#undef PYTHONCAPI_COMPAT_SPEC + + const PyConfigSpec *spec; + int found = 0; + for (size_t i=0; i < sizeof(config_spec) / sizeof(config_spec[0]); i++) { + spec = &config_spec[i]; + if (strcmp(spec->name, name) == 0) { + found = 1; + break; + } + } + if (found) { + if (spec->sys_attr != NULL) { + PyObject *value = PySys_GetObject(spec->sys_attr); + if (value == NULL) { + PyErr_Format(PyExc_RuntimeError, "lost sys.%s", spec->sys_attr); + return NULL; + } + return Py_NewRef(value); + } + + PyAPI_FUNC(const PyConfig*) _Py_GetConfig(void); + + const PyConfig *config = _Py_GetConfig(); + void *member = (char *)config + spec->offset; + switch (spec->type) { + case _PyConfig_MEMBER_INT: + case _PyConfig_MEMBER_UINT: + { + int value = *(int *)member; + return PyLong_FromLong(value); + } + case _PyConfig_MEMBER_BOOL: + { + int value = *(int *)member; + return PyBool_FromLong(value != 0); + } + case _PyConfig_MEMBER_ULONG: + { + unsigned long value = *(unsigned long *)member; + return PyLong_FromUnsignedLong(value); + } + case _PyConfig_MEMBER_WSTR: + case _PyConfig_MEMBER_WSTR_OPT: + { + wchar_t *wstr = *(wchar_t **)member; + if (wstr != NULL) { + return PyUnicode_FromWideChar(wstr, -1); + } + else { + return Py_NewRef(Py_None); + } + } + case _PyConfig_MEMBER_WSTR_LIST: + { + const PyWideStringList *list = (const PyWideStringList *)member; + PyObject *tuple = PyTuple_New(list->length); + if (tuple == NULL) { + return NULL; + } + + for (Py_ssize_t i = 0; i < list->length; i++) { + PyObject *item = PyUnicode_FromWideChar(list->items[i], -1); + if (item == NULL) { + Py_DECREF(tuple); + return NULL; + } + PyTuple_SET_ITEM(tuple, i, item); + } + return tuple; + } + default: + Py_UNREACHABLE(); + } + } + + PyErr_Format(PyExc_ValueError, "unknown config option name: %s", name); + return NULL; +} + +static inline int +PyConfig_GetInt(const char *name, int *value) +{ + PyObject *obj = PyConfig_Get(name); + if (obj == NULL) { + return -1; + } + + if (!PyLong_Check(obj)) { + Py_DECREF(obj); + PyErr_Format(PyExc_TypeError, "config option %s is not an int", name); + return -1; + } + + int as_int = PyLong_AsInt(obj); + Py_DECREF(obj); + if (as_int == -1 && PyErr_Occurred()) { + PyErr_Format(PyExc_OverflowError, + "config option %s value does not fit into a C int", name); + return -1; + } + + *value = as_int; + return 0; +} +#endif // PY_VERSION_HEX > 0x03090000 && !defined(PYPY_VERSION) + + +#ifdef __cplusplus +} +#endif +#endif // PYTHONCAPI_COMPAT diff --git a/src/thirdparty/raqm/COPYING b/src/thirdparty/raqm/COPYING index 97e2489b779..964318a8ae7 100644 --- a/src/thirdparty/raqm/COPYING +++ b/src/thirdparty/raqm/COPYING @@ -1,7 +1,7 @@ The MIT License (MIT) Copyright © 2015 Information Technology Authority (ITA) -Copyright © 2016-2023 Khaled Hosny +Copyright © 2016-2025 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 diff --git a/src/thirdparty/raqm/NEWS b/src/thirdparty/raqm/NEWS index e8bf32e0bbb..fb432cffb0e 100644 --- a/src/thirdparty/raqm/NEWS +++ b/src/thirdparty/raqm/NEWS @@ -1,3 +1,19 @@ +Overview of changes leading to 0.10.3 +Tuesday, August 5, 2025 +==================================== + +Fix raqm_set_text_utf8/utf16 reading beyond len for multibyte. + +Support building against SheenBidi 2.9. + +Fix deprecation warning with latest HarfBuzz. + +Overview of changes leading to 0.10.2 +Sunday, September 22, 2024 +==================================== + +Fix Unicode codepoint conversion from UTF-16. + Overview of changes leading to 0.10.1 Wednesday, April 12, 2023 ==================================== diff --git a/src/thirdparty/raqm/raqm-version.h b/src/thirdparty/raqm/raqm-version.h index 62d2d206459..f2dd61cf6a1 100644 --- a/src/thirdparty/raqm/raqm-version.h +++ b/src/thirdparty/raqm/raqm-version.h @@ -33,9 +33,9 @@ #define RAQM_VERSION_MAJOR 0 #define RAQM_VERSION_MINOR 10 -#define RAQM_VERSION_MICRO 1 +#define RAQM_VERSION_MICRO 3 -#define RAQM_VERSION_STRING "0.10.1" +#define RAQM_VERSION_STRING "0.10.3" #define RAQM_VERSION_ATLEAST(major,minor,micro) \ ((major)*10000+(minor)*100+(micro) <= \ diff --git a/src/thirdparty/raqm/raqm.c b/src/thirdparty/raqm/raqm.c index 2b331e1afb0..9ecc5cac8cf 100644 --- a/src/thirdparty/raqm/raqm.c +++ b/src/thirdparty/raqm/raqm.c @@ -30,7 +30,11 @@ #include #ifdef RAQM_SHEENBIDI +#ifdef RAQM_SHEENBIDI_GT_2_9 +#include +#else #include +#endif #else #ifdef HAVE_FRIBIDI_SYSTEM #include @@ -546,34 +550,32 @@ raqm_set_text (raqm_t *rq, return true; } -static void * -_raqm_get_utf8_codepoint (const void *str, +static const char * +_raqm_get_utf8_codepoint (const char *str, uint32_t *out_codepoint) { - const char *s = (const char *)str; - - if (0xf0 == (0xf8 & s[0])) + if (0xf0 == (0xf8 & str[0])) { - *out_codepoint = ((0x07 & s[0]) << 18) | ((0x3f & s[1]) << 12) | ((0x3f & s[2]) << 6) | (0x3f & s[3]); - s += 4; + *out_codepoint = ((0x07 & str[0]) << 18) | ((0x3f & str[1]) << 12) | ((0x3f & str[2]) << 6) | (0x3f & str[3]); + str += 4; } - else if (0xe0 == (0xf0 & s[0])) + else if (0xe0 == (0xf0 & str[0])) { - *out_codepoint = ((0x0f & s[0]) << 12) | ((0x3f & s[1]) << 6) | (0x3f & s[2]); - s += 3; + *out_codepoint = ((0x0f & str[0]) << 12) | ((0x3f & str[1]) << 6) | (0x3f & str[2]); + str += 3; } - else if (0xc0 == (0xe0 & s[0])) + else if (0xc0 == (0xe0 & str[0])) { - *out_codepoint = ((0x1f & s[0]) << 6) | (0x3f & s[1]); - s += 2; + *out_codepoint = ((0x1f & str[0]) << 6) | (0x3f & str[1]); + str += 2; } else { - *out_codepoint = s[0]; - s += 1; + *out_codepoint = str[0]; + str += 1; } - return (void *)s; + return str; } static size_t @@ -585,42 +587,41 @@ _raqm_u8_to_u32 (const char *text, size_t len, uint32_t *unicode) while ((*in_utf8 != '\0') && (in_len < len)) { - in_utf8 = _raqm_get_utf8_codepoint (in_utf8, out_utf32); + const char *out_utf8 = _raqm_get_utf8_codepoint (in_utf8, out_utf32); + in_len += out_utf8 - in_utf8; + in_utf8 = out_utf8; ++out_utf32; - ++in_len; } return (out_utf32 - unicode); } -static void * -_raqm_get_utf16_codepoint (const void *str, - uint32_t *out_codepoint) +static const uint16_t * +_raqm_get_utf16_codepoint (const uint16_t *str, + uint32_t *out_codepoint) { - const uint16_t *s = (const uint16_t *)str; - - if (s[0] > 0xD800 && s[0] < 0xDBFF) + if (str[0] >= 0xD800 && str[0] <= 0xDBFF) { - if (s[1] > 0xDC00 && s[1] < 0xDFFF) + if (str[1] >= 0xDC00 && str[1] <= 0xDFFF) { - uint32_t X = ((s[0] & ((1 << 6) -1)) << 10) | (s[1] & ((1 << 10) -1)); - uint32_t W = (s[0] >> 6) & ((1 << 5) - 1); + uint32_t X = ((str[0] & ((1 << 6) -1)) << 10) | (str[1] & ((1 << 10) -1)); + uint32_t W = (str[0] >> 6) & ((1 << 5) - 1); *out_codepoint = (W+1) << 16 | X; - s += 2; + str += 2; } else { /* A single high surrogate, this is an error. */ - *out_codepoint = s[0]; - s += 1; + *out_codepoint = str[0]; + str += 1; } } else { - *out_codepoint = s[0]; - s += 1; + *out_codepoint = str[0]; + str += 1; } - return (void *)s; + return str; } static size_t @@ -632,9 +633,10 @@ _raqm_u16_to_u32 (const uint16_t *text, size_t len, uint32_t *unicode) while ((*in_utf16 != '\0') && (in_len < len)) { - in_utf16 = _raqm_get_utf16_codepoint (in_utf16, out_utf32); + const uint16_t *out_utf16 = _raqm_get_utf16_codepoint (in_utf16, out_utf32); + in_len += (out_utf16 - in_utf16); + in_utf16 = out_utf16; ++out_utf32; - ++in_len; } return (out_utf32 - unicode); @@ -1114,12 +1116,12 @@ _raqm_set_spacing (raqm_t *rq, { if (_raqm_allowed_grapheme_boundary (rq->text[i], rq->text[i+1])) { - /* CSS word seperators, word spacing is only applied on these.*/ + /* CSS word separators, word spacing is only applied on these.*/ if (rq->text[i] == 0x0020 || /* Space */ rq->text[i] == 0x00A0 || /* No Break Space */ rq->text[i] == 0x1361 || /* Ethiopic Word Space */ - rq->text[i] == 0x10100 || /* Aegean Word Seperator Line */ - rq->text[i] == 0x10101 || /* Aegean Word Seperator Dot */ + rq->text[i] == 0x10100 || /* Aegean Word Separator Line */ + rq->text[i] == 0x10101 || /* Aegean Word Separator Dot */ rq->text[i] == 0x1039F || /* Ugaric Word Divider */ rq->text[i] == 0x1091F) /* Phoenician Word Separator */ { @@ -2167,6 +2169,10 @@ _raqm_ft_transform (int *x, *y = vector.y; } +#if !HB_VERSION_ATLEAST (10, 4, 0) +# define hb_ft_font_get_ft_face hb_ft_font_get_face +#endif + static bool _raqm_shape (raqm_t *rq) { @@ -2199,7 +2205,7 @@ _raqm_shape (raqm_t *rq) hb_glyph_position_t *pos; unsigned int len; - FT_Get_Transform (hb_ft_font_get_face (run->font), &matrix, NULL); + FT_Get_Transform (hb_ft_font_get_ft_face (run->font), &matrix, NULL); pos = hb_buffer_get_glyph_positions (run->buffer, &len); info = hb_buffer_get_glyph_infos (run->buffer, &len); diff --git a/tox.ini b/tox.ini index 85a2020d6d0..aede5fcdc54 100644 --- a/tox.ini +++ b/tox.ini @@ -3,46 +3,37 @@ requires = tox>=4.2 env_list = lint - py{py3, 312, 311, 310, 39, 38} + mypy + py{py3, 315, 314, 313, 312, 311, 310} [testenv] deps = - cffi numpy + pytest-sugar extras = tests commands = - make clean - {envpython} -m pip install . {envpython} selftest.py - {envpython} -m pytest -W always {posargs} -allowlist_externals = - make + {envpython} -m pytest \ + --dist worksteal \ + --numprocesses auto \ + -W always \ + {posargs} [testenv:lint] skip_install = true deps = check-manifest - pre-commit + prek pass_env = - PRE_COMMIT_COLOR + PREK_COLOR commands = - pre-commit run --all-files --show-diff-on-failure + prek run --all-files --show-diff-on-failure check-manifest [testenv:mypy] skip_install = true deps = -r .ci/requirements-mypy.txt - IceSpringPySideStubs-PyQt6 - IceSpringPySideStubs-PySide6 - ipython - numpy - packaging - types-cffi - types-defusedxml - types-olefile -extras = - typing commands = - mypy src {posargs} + mypy conftest.py selftest.py setup.py checks docs src winbuild Tests {posargs} diff --git a/wheels/README.md b/wheels/README.md index 8b412b7fe32..1175616e147 100644 --- a/wheels/README.md +++ b/wheels/README.md @@ -1,11 +1,11 @@ README ------ -[cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build macOS and Linux -wheels for tagged versions of Pillow. +[cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build wheels for tagged +versions of Pillow. This directory contains [multibuild](https://github.com/multi-build/multibuild) to -build dependencies for the wheels, and dependency licenses to be included. +build dependencies for macOS and Linux wheels, and dependency licenses to be included. Archives -------- @@ -30,6 +30,3 @@ Wheels Wheels are [GitHub Actions artifacts created for tags, relevant changes or manual builds](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml). - -Windows wheels are created separately. They are -[GitHub Actions artifacts created on each run of the Windows workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml?query=branch%3Amain). diff --git a/wheels/dependency_licenses/AOM.txt b/wheels/dependency_licenses/AOM.txt new file mode 100644 index 00000000000..3a2e46c264b --- /dev/null +++ b/wheels/dependency_licenses/AOM.txt @@ -0,0 +1,26 @@ +Copyright (c) 2016, Alliance for Open Media. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/DAV1D.txt b/wheels/dependency_licenses/DAV1D.txt new file mode 100644 index 00000000000..875b138ecf6 --- /dev/null +++ b/wheels/dependency_licenses/DAV1D.txt @@ -0,0 +1,23 @@ +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/FREETYPE2.txt b/wheels/dependency_licenses/FREETYPE2.txt index 93efc612676..8f2345992b0 100644 --- a/wheels/dependency_licenses/FREETYPE2.txt +++ b/wheels/dependency_licenses/FREETYPE2.txt @@ -211,351 +211,6 @@ Legal Terms --- end of FTL.TXT --- --------------------------------------------------------------------------- - - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Library General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Library General -Public License instead of this License. - --------------------------------------------------------------------------- - The following license details are part of `src/bdf/README`: ``` diff --git a/wheels/dependency_licenses/LIBAVIF.txt b/wheels/dependency_licenses/LIBAVIF.txt new file mode 100644 index 00000000000..350eb9d15ce --- /dev/null +++ b/wheels/dependency_licenses/LIBAVIF.txt @@ -0,0 +1,387 @@ +Copyright 2019 Joe Drago. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: src/obu.c + +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: third_party/iccjpeg/* + +In plain English: + +1. We don't promise that this software works. (But if you find any bugs, + please let us know!) +2. You can use this software for whatever you want. You don't have to pay us. +3. You may not pretend that you wrote this software. If you use it in a + program, you must acknowledge somewhere in your documentation that + you've used the IJG code. + +In legalese: + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2013, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +The Unix configuration script "configure" was produced with GNU Autoconf. +It is copyright by the Free Software Foundation but is freely distributable. +The same holds for its supporting scripts (config.guess, config.sub, +ltmain.sh). Another support script, install-sh, is copyright by X Consortium +but is also freely distributable. + +The IJG distribution formerly included code to read and write GIF files. +To avoid entanglement with the Unisys LZW patent, GIF reading support has +been removed altogether, and the GIF writer has been simplified to produce +"uncompressed GIFs". This technique does not use the LZW algorithm; the +resulting GIF files are larger than usual, but are readable by all standard +GIF decoders. + +We are required to state that + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + +------------------------------------------------------------------------------ + +Files: contrib/gdk-pixbuf/* + +Copyright 2020 Emmanuel Gil Peyrot. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: android_jni/gradlew* + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +------------------------------------------------------------------------------ + +Files: third_party/libyuv/* + +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/LIBYUV.txt b/wheels/dependency_licenses/LIBYUV.txt new file mode 100644 index 00000000000..c911747a6b5 --- /dev/null +++ b/wheels/dependency_licenses/LIBYUV.txt @@ -0,0 +1,29 @@ +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/ZSTD.txt b/wheels/dependency_licenses/ZSTD.txt new file mode 100644 index 00000000000..75800288cc2 --- /dev/null +++ b/wheels/dependency_licenses/ZSTD.txt @@ -0,0 +1,30 @@ +BSD License + +For Zstandard software + +Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook, nor Meta, nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/multibuild b/wheels/multibuild index 452dd2d1705..64739327166 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 452dd2d1705f6b2375369a6570c415beb3163f70 +Subproject commit 64739327166fcad1fa41ad9b23fa910fa244c84f diff --git a/winbuild/README.md b/winbuild/README.md index 7e81abcb0e5..b1c9262c2e4 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -11,12 +11,13 @@ For more extensive info, see the [Windows build instructions](build.rst). * 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.15 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). +* Tested on Windows Server 2025 and 2022 with Visual Studio 2022 Enterprise (GitHub + Actions). + +Here's an example script to build on Windows: -The following is a simplified version of the script used on AppVeyor: ``` -set PYTHON=C:\Python38\bin +set PYTHON=C:\Python310\bin cd /D C:\Pillow\winbuild %PYTHON%\python.exe build_prepare.py -v --depends=C:\pillow-depends build\build_dep_all.cmd @@ -24,6 +25,6 @@ cd .. %PYTHON%\python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . 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 +%PYTHON%\python.exe -m pytest -vv -x --cov PIL --cov Tests --cov-report term --cov-report xml Tests %PYTHON%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . ``` diff --git a/winbuild/build.rst b/winbuild/build.rst index d0be2943eb4..23b26c42252 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -6,7 +6,7 @@ Building Pillow on Windows be sufficient. This page describes the steps necessary to build Pillow using the same -scripts used on GitHub Actions and AppVeyor CIs. +scripts used on GitHub Actions CI. Prerequisites ------------- @@ -61,6 +61,7 @@ Run ``build_prepare.py`` to configure the build:: --no-imagequant skip GPL-licensed optional dependency libimagequant --no-fribidi, --no-raqm skip LGPL-licensed optional dependency FriBiDi + --no-avif skip optional dependency libavif Arguments can also be supplied using the environment variables PILLOW_BUILD, PILLOW_DEPS, ARCHITECTURE. See winbuild\build.rst for more information. @@ -112,9 +113,9 @@ directory. Example ------- -The following is a simplified version of the script used on AppVeyor:: +Here's an example script to build on Windows:: - set PYTHON=C:\Python38\bin + set PYTHON=C:\Python310\bin cd /D C:\Pillow\winbuild %PYTHON%\python.exe build_prepare.py -v --depends C:\pillow-depends build\build_dep_all.cmd @@ -123,5 +124,5 @@ The following is a simplified version of the script used on AppVeyor:: %PYTHON%\python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . 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 + %PYTHON%\python.exe -m pytest -vv -x --cov PIL --cov Tests --cov-report term --cov-report xml Tests %PYTHON%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9837589b2b1..3b16da58aef 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -7,6 +7,8 @@ import shutil import struct import subprocess +import sys +from typing import Any def cmd_cd(path: str) -> str: @@ -43,21 +45,22 @@ def cmd_nmake( target: str = "", params: list[str] | None = None, ) -> str: - params = "" if params is None else " ".join(params) - return " ".join( [ "{nmake}", "-nologo", f'-f "{makefile}"' if makefile is not None else "", - f"{params}", + f'{" ".join(params)}' if params is not None else "", f'"{target}"', ] ) def cmds_cmake( - target: str | tuple[str, ...] | list[str], *params, build_dir: str = "." + target: str | tuple[str, ...] | list[str], + *params: str, + build_dir: str = ".", + build_type: str = "Release", ) -> list[str]: if not isinstance(target, str): target = " ".join(target) @@ -66,7 +69,7 @@ def cmds_cmake( " ".join( [ "{cmake}", - "-DCMAKE_BUILD_TYPE=Release", + f"-DCMAKE_BUILD_TYPE={build_type}", "-DCMAKE_VERBOSE_MAKEFILE=ON", "-DCMAKE_RULE_MESSAGES:BOOL=OFF", # for NMake "-DCMAKE_C_COMPILER=cl.exe", # for Ninja @@ -110,31 +113,29 @@ def cmd_msbuild( } V = { - "BROTLI": "1.1.0", - "FREETYPE": "2.13.2", - "FRIBIDI": "1.0.15", - "HARFBUZZ": "8.5.0", - "JPEGTURBO": "3.0.3", - "LCMS2": "2.16", - "LIBPNG": "1.6.43", - "LIBWEBP": "1.4.0", - "OPENJPEG": "2.5.2", - "TIFF": "4.6.0", - "XZ": "5.4.5", - "ZLIB": "1.3.1", + "BROTLI": "1.2.0", + "FREETYPE": "2.14.3", + "FRIBIDI": "1.0.16", + "HARFBUZZ": "13.2.1", + "JPEGTURBO": "3.1.4.1", + "LCMS2": "2.18", + "LIBAVIF": "1.4.1", + "LIBIMAGEQUANT": "4.4.1", + "LIBPNG": "1.6.56", + "LIBWEBP": "1.6.0", + "OPENJPEG": "2.5.4", + "TIFF": "4.7.1", + "XZ": "5.8.3", + "ZLIBNG": "2.3.3", } -V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) -V["ZLIB_DOTLESS"] = V["ZLIB"].replace(".", "") # dependencies, listed in order of compilation -DEPS = { +DEPS: dict[str, dict[str, Any]] = { "libjpeg": { - "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/" - f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz/download", + "url": f"https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/{V['JPEGTURBO']}/libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", "filename": f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", - "dir": f"libjpeg-turbo-{V['JPEGTURBO']}", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" @@ -148,37 +149,38 @@ def cmd_msbuild( }, "build": [ *cmds_cmake( - ("jpeg-static", "cjpeg-static", "djpeg-static"), + ("jpeg-static", "djpeg-static"), "-DENABLE_SHARED:BOOL=FALSE", "-DWITH_JPEG8:BOOL=TRUE", "-DWITH_CRT_DLL:BOOL=TRUE", ), cmd_copy("jpeg-static.lib", "libjpeg.lib"), - cmd_copy("cjpeg-static.exe", "cjpeg.exe"), cmd_copy("djpeg-static.exe", "djpeg.exe"), ], - "headers": ["j*.h"], + "headers": ["jconfig.h", r"src\j*.h"], "libs": ["libjpeg.lib"], - "bins": ["cjpeg.exe", "djpeg.exe"], + "bins": ["djpeg.exe"], }, "zlib": { - "url": f"https://zlib.net/zlib{V['ZLIB_DOTLESS']}.zip", - "filename": f"zlib{V['ZLIB_DOTLESS']}.zip", - "dir": f"zlib-{V['ZLIB']}", - "license": "README", - "license_pattern": "Copyright notice:\n\n(.+)$", + "url": f"https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.tar.gz", + "filename": f"zlib-ng-{V['ZLIBNG']}.tar.gz", + "license": "LICENSE.md", + "patch": { + r"CMakeLists.txt": { + "set_target_properties(zlib-ng PROPERTIES OUTPUT_NAME zlibstatic${{SUFFIX}})": "set_target_properties(zlib-ng PROPERTIES OUTPUT_NAME zlib)", # noqa: E501 + }, + }, "build": [ - cmd_nmake(r"win32\Makefile.msc", "clean"), - cmd_nmake(r"win32\Makefile.msc", "zlib.lib"), - cmd_copy("zlib.lib", "z.lib"), + *cmds_cmake( + "zlib-ng", "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DZLIB_COMPAT:BOOL=ON" + ), ], "headers": [r"z*.h"], - "libs": [r"*.lib"], + "libs": [r"zlib.lib"], }, "xz": { - "url": f"{SF_PROJECTS}/lzmautils/files/xz-{V['XZ']}.tar.gz/download", + "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/FILENAME", "filename": f"xz-{V['XZ']}.tar.gz", - "dir": f"xz-{V['XZ']}", "license": "COPYING", "build": [ *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -186,12 +188,11 @@ def cmd_msbuild( cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"), ], "headers": [r"src\liblzma\api\lzma.h"], - "libs": [r"liblzma.lib"], + "libs": [r"lzma.lib"], }, "libwebp": { - "url": f"http://downloads.webmproject.org/releases/webp/libwebp-{V['LIBWEBP']}.tar.gz", + "url": "http://downloads.webmproject.org/releases/webp/FILENAME", "filename": f"libwebp-{V['LIBWEBP']}.tar.gz", - "dir": f"libwebp-{V['LIBWEBP']}", "license": "COPYING", "patch": { r"src\enc\picture_csp_enc.c": { @@ -201,7 +202,7 @@ def cmd_msbuild( }, "build": [ *cmds_cmake( - "webp webpdemux webpmux", + "webp webpmux webpdemux", "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DWEBP_LINK_STATIC:BOOL=OFF", ), @@ -211,25 +212,18 @@ def cmd_msbuild( "libs": [r"libsharpyuv.lib", r"libwebp*.lib"], }, "libtiff": { - "url": f"https://download.osgeo.org/libtiff/tiff-{V['TIFF']}.tar.gz", + "url": "https://download.osgeo.org/libtiff/FILENAME", "filename": f"tiff-{V['TIFF']}.tar.gz", - "dir": f"tiff-{V['TIFF']}", "license": "LICENSE.md", "patch": { r"libtiff\tif_lzma.c": { - # link against liblzma.lib - "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501 + # link against lzma.lib + "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "lzma.lib")', # noqa: E501 }, r"libtiff\tif_webp.c": { # link against libwebp.lib "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "libwebp.lib")', # noqa: E501 }, - r"test\CMakeLists.txt": { - "add_executable(test_write_read_tags ../placeholder.h)": "", - "target_sources(test_write_read_tags PRIVATE test_write_read_tags.c)": "", # noqa: E501 - "target_link_libraries(test_write_read_tags PRIVATE tiff)": "", - "list(APPEND simple_tests test_write_read_tags)": "", - }, }, "build": [ *cmds_cmake( @@ -244,9 +238,8 @@ def cmd_msbuild( }, "libpng": { "url": f"{SF_PROJECTS}/libpng/files/libpng{V['LIBPNG_XY']}/{V['LIBPNG']}/" - f"lpng{V['LIBPNG_DOTLESS']}.zip/download", - "filename": f"lpng{V['LIBPNG_DOTLESS']}.zip", - "dir": f"lpng{V['LIBPNG_DOTLESS']}", + f"FILENAME/download", + "filename": f"libpng-{V['LIBPNG']}.tar.gz", "license": "LICENSE", "build": [ *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), @@ -260,7 +253,6 @@ def cmd_msbuild( "brotli": { "url": f"https://github.com/google/brotli/archive/refs/tags/v{V['BROTLI']}.tar.gz", "filename": f"brotli-{V['BROTLI']}.tar.gz", - "dir": f"brotli-{V['BROTLI']}", "license": "LICENSE", "build": [ *cmds_cmake(("brotlicommon", "brotlidec"), "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -269,9 +261,8 @@ def cmd_msbuild( "libs": ["*.lib"], }, "freetype": { - "url": f"https://download.savannah.gnu.org/releases/freetype/freetype-{V['FREETYPE']}.tar.gz", + "url": "https://download.savannah.gnu.org/releases/freetype/FILENAME", "filename": f"freetype-{V['FREETYPE']}.tar.gz", - "dir": f"freetype-{V['FREETYPE']}", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { @@ -294,19 +285,18 @@ def cmd_msbuild( "build": [ cmd_rmdir("objs"), cmd_msbuild( - r"builds\windows\vc2010\freetype.sln", "Release Static", "Clean" + r"builds\windows\vc2010\freetype.vcxproj", "Release Static", "Clean" ), cmd_msbuild( - r"builds\windows\vc2010\freetype.sln", "Release Static", "Build" + r"builds\windows\vc2010\freetype.vcxproj", "Release Static", "Build" ), cmd_xcopy("include", "{inc_dir}"), ], "libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"], }, "lcms2": { - "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/lcms2-{V['LCMS2']}.tar.gz/download", # noqa: E501 + "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/FILENAME/download", "filename": f"lcms2-{V['LCMS2']}.tar.gz", - "dir": f"lcms2-{V['LCMS2']}", "license": "LICENSE", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { @@ -332,7 +322,6 @@ def cmd_msbuild( "openjpeg": { "url": f"https://github.com/uclouvain/openjpeg/archive/v{V['OPENJPEG']}.tar.gz", "filename": f"openjpeg-{V['OPENJPEG']}.tar.gz", - "dir": f"openjpeg-{V['OPENJPEG']}", "license": "LICENSE", "build": [ *cmds_cmake( @@ -344,30 +333,19 @@ def cmd_msbuild( "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", - "filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip", - "dir": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab", + "url": "https://github.com/ImageOptim/libimagequant/archive/{V['LIBIMAGEQUANT']}.tar.gz", + "filename": f"libimagequant-{V['LIBIMAGEQUANT']}.tar.gz", "license": "COPYRIGHT", - "patch": { - "CMakeLists.txt": { - "if(OPENMP_FOUND)": "if(false)", - "install": "#install", - # libimagequant does not detect MSVC x86_arm64 cross-compiler correctly - "if(${{CMAKE_SYSTEM_PROCESSOR}} STREQUAL ARM64)": "if({architecture} STREQUAL ARM64)", # noqa: E501 - } - }, "build": [ - *cmds_cmake("imagequant_a"), - cmd_copy("imagequant_a.lib", "imagequant.lib"), + cmd_cd("imagequant-sys"), + "cargo build --release", ], - "headers": [r"*.h"], - "libs": [r"imagequant.lib"], + "headers": ["libimagequant.h"], + "libs": [r"..\target\release\imagequant_sys.lib"], }, "harfbuzz": { - "url": f"https://github.com/harfbuzz/harfbuzz/archive/{V['HARFBUZZ']}.zip", - "filename": f"harfbuzz-{V['HARFBUZZ']}.zip", - "dir": f"harfbuzz-{V['HARFBUZZ']}", + "url": f"https://github.com/harfbuzz/harfbuzz/releases/download/{V['HARFBUZZ']}/FILENAME", + "filename": f"harfbuzz-{V['HARFBUZZ']}.tar.xz", "license": "COPYING", "build": [ *cmds_cmake( @@ -382,7 +360,6 @@ def cmd_msbuild( "fribidi": { "url": f"https://github.com/fribidi/fribidi/archive/v{V['FRIBIDI']}.zip", "filename": f"fribidi-{V['FRIBIDI']}.zip", - "dir": f"fribidi-{V['FRIBIDI']}", "license": "COPYING", "build": [ cmd_copy(r"COPYING", rf"{{bin_dir}}\fribidi-{V['FRIBIDI']}-COPYING"), @@ -398,6 +375,29 @@ def cmd_msbuild( ], "bins": [r"*.dll"], }, + "libavif": { + "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.tar.gz", + "filename": f"libavif-{V['LIBAVIF']}.tar.gz", + "license": "LICENSE", + "build": [ + "rustup update", + f"{sys.executable} -m pip install meson", + *cmds_cmake( + "avif_static", + "-DBUILD_SHARED_LIBS=OFF", + "-DAVIF_LIBSHARPYUV=LOCAL", + "-DAVIF_LIBYUV=LOCAL", + "-DAVIF_CODEC_AOM=LOCAL", + "-DCONFIG_AV1_HIGHBITDEPTH=0", + "-DAVIF_CODEC_AOM_DECODE=OFF", + "-DAVIF_CODEC_DAV1D=LOCAL", + "-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON", + build_type="MinSizeRel", + ), + cmd_xcopy("include", "{inc_dir}"), + ], + "libs": ["avif.lib"], + }, } @@ -498,7 +498,7 @@ def extract_dep(url: str, filename: str, prefs: dict[str, str]) -> None: except RuntimeError as exc: # Otherwise try upstream print(exc) - download_dep(url, file) + download_dep(url.replace("FILENAME", filename), file) print("Extracting " + filename) sources_dir_abs = os.path.abspath(sources_dir) @@ -511,15 +511,18 @@ def extract_dep(url: str, filename: str, prefs: dict[str, str]) -> None: msg = "Attempted Path Traversal in Zip File" raise RuntimeError(msg) zf.extractall(sources_dir) - elif filename.endswith((".tar.gz", ".tgz")): - with tarfile.open(file, "r:gz") as tgz: + elif filename.endswith((".tar.gz", ".tar.xz")): + with tarfile.open(file, "r:xz" if filename.endswith(".xz") else "r:gz") as tgz: for member in tgz.getnames(): member_abspath = os.path.abspath(os.path.join(sources_dir, member)) member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) if sources_dir_abs != member_prefix: msg = "Attempted Path Traversal in Tar File" raise RuntimeError(msg) - tgz.extractall(sources_dir) + if sys.version_info >= (3, 12): + tgz.extractall(sources_dir, filter="data") + else: + tgz.extractall(sources_dir) else: msg = "Unknown archive type: " + filename raise RuntimeError(msg) @@ -538,15 +541,12 @@ def write_script( print(" " + line) -def get_footer(dep: dict) -> list[str]: - 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 get_footer(dep: dict[str, Any]) -> list[str]: + return ( + [cmd_copy(out, "{inc_dir}") for out in dep.get("headers", [])] + + [cmd_copy(out, "{lib_dir}") for out in dep.get("libs", [])] + + [cmd_copy(out, "{bin_dir}") for out in dep.get("bins", [])] + ) def build_env(prefs: dict[str, str], verbose: bool) -> None: @@ -583,6 +583,7 @@ def build_dep(name: str, prefs: dict[str, str], verbose: bool) -> str: license_text += f.read() if "license_pattern" in dep: match = re.search(dep["license_pattern"], license_text, re.DOTALL) + assert match is not None license_text = "\n".join(match.groups()) assert len(license_text) > 50 with open(os.path.join(license_dir, f"{directory}.txt"), "w") as f: @@ -699,6 +700,11 @@ def main() -> None: action="store_true", help="skip LGPL-licensed optional dependency FriBiDi", ) + parser.add_argument( + "--no-avif", + action="store_true", + help="skip optional dependency libavif", + ) args = parser.parse_args() arch_prefs = ARCHITECTURES[args.architecture] @@ -739,6 +745,8 @@ def main() -> None: disabled += ["libimagequant"] if args.no_fribidi: disabled += ["fribidi"] + if args.no_avif or args.architecture == "ARM64": + disabled += ["libavif"] prefs = { "architecture": args.architecture, @@ -761,6 +769,8 @@ def main() -> None: } for k, v in DEPS.items(): + if "dir" not in v: + v["dir"] = re.sub(r"\.(tar\.gz|tar\.xz|zip)", "", v["filename"]) prefs[f"dir_{k}"] = os.path.join(sources_dir, v["dir"]) print()