diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 781ad4a4b..000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,99 +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:/Python313 - ARCHITECTURE: x86 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - - PYTHON: C:/Python39-x64 - ARCHITECTURE: AMD64 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - - -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.4.0 -- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.04.0\bin;%PATH% -- cd c:\pillow\winbuild\ -- ps: | - c:\python39\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 ipython numpy olefile pyroma' -- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% -- path %PYTHON%;%PATH% -- .ci\test.cmd - -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 c71546f00..6da27b975 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 e678f68ec..ae10cb671 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 e85e6bdc5..acb84f046 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -2,12 +2,12 @@ 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 @@ -20,10 +20,10 @@ fi set -e if [[ $(uname) != CYGWIN* ]]; then - sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ - ghostscript libjpeg-turbo-progs libopenjp2-7-dev\ + 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 + sway wl-clipboard libopenblas-dev nasm fi python3 -m pip install --upgrade pip @@ -36,6 +36,9 @@ 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 dependency, only install if there's a binary package. +# fails on beta 3.14 and PyPy +python3 -m pip install --only-binary=:all: pyarrow || true if [[ $(uname) != CYGWIN* ]]; then python3 -m pip install numpy @@ -50,7 +53,7 @@ if [[ $(uname) != CYGWIN* ]]; then # Pyroma uses non-isolated build and fails with old setuptools if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then # To match pyproject.toml - python3 -m pip install "setuptools>=67.8" + python3 -m pip install "setuptools>=77" fi # webp @@ -62,6 +65,9 @@ if [[ $(uname) != CYGWIN* ]]; then # 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 diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index c4511439c..520b6e320 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.21.3 +cibuildwheel==3.0.0 diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index dcb3996e2..44b5badab 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,9 +1,10 @@ -mypy==1.11.2 +mypy==1.16.1 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython numpy packaging +pyarrow-stubs pytest sphinx types-atheris diff --git a/.ci/test.cmd b/.ci/test.cmd index aafc9290c..acfac3d1a 100644 --- a/.ci/test.cmd +++ b/.ci/test.cmd @@ -1,3 +1,3 @@ python.exe -c "from PIL import Image" IF ERRORLEVEL 1 EXIT /B -python.exe -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests +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 3f0ddc350..87a605d84 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 --cov-report xml 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 143dde82c..1871d1f7a 100644 --- a/.clang-format +++ b/.clang-format @@ -1,5 +1,26 @@ # A clang-format style that approximates Python's PEP 7 # Useful for IDE integration +Language: C +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 +SpaceBeforeParens: ControlStatements +SpacesInParentheses: false +TabWidth: 4 +UseTab: Never +--- +Language: Cpp BasedOnStyle: Google AlwaysBreakAfterReturnType: All AllowShortIfStatementsOnASingleLine: false @@ -11,7 +32,6 @@ ColumnLimit: 88 DerivePointerAlignment: false IndentGotoLabels: false IndentWidth: 4 -Language: Cpp PointerAlignment: Right ReflowComments: true SortIncludes: false diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d03fcf0d9..c098e32eb 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 000000000..20d1ac79b --- /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 3c2066137..9bb089615 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -9,7 +9,6 @@ pull_request_rules: - 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 3711d91f0..de0ab4805 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 d1d824335..91fa0426f 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,7 +1,7 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:base" + "config:recommended" ], "labels": [ "Dependency" @@ -9,9 +9,13 @@ "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/docs.yml b/.github/workflows/docs.yml index 92e860cb5..626824f38 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -33,6 +33,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python uses: actions/setup-python@v5 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cc4760288..8e789a734 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,6 +21,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: pre-commit cache uses: actions/cache@v4 diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index ddb421230..94e3d5d08 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -6,19 +6,19 @@ if [[ "$ImageOS" == "macos13" ]]; then brew uninstall gradle maven fi 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" python3 -m pip install coverage @@ -30,6 +30,12 @@ python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma python3 -m pip install numpy +# optional test dependency, only install if there's a binary package. +# fails on beta 3.14 and PyPy +python3 -m pip install --only-binary=:all: pyarrow || true + +# libavif +pushd depends && ./install_libavif.sh && popd # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 545c2e364..61ccf58e2 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: permissions: - issues: write + contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -15,6 +15,8 @@ concurrency: jobs: stale: if: github.repository_owner == 'python-pillow' + permissions: + issues: write runs-on: ubuntu-latest diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 0aa79e423..abfeaa77f 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -48,9 +48,11 @@ jobs: - name: Checkout Pillow uses: actions/checkout@v4 + with: + persist-credentials: false - name: Install Cygwin - uses: cygwin/cygwin-install-action@v4 + uses: cygwin/cygwin-install-action@v5 with: packages: > gcc-g++ @@ -131,11 +133,12 @@ jobs: - name: After success run: | bash.exe .ci/after_success.sh + rm C:\cygwin\bin\bash.EXE - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: - file: ./coverage.xml + files: ./coverage.xml flags: GHA_Cygwin name: Cygwin Python 3.${{ matrix.python-minor-version }} token: ${{ secrets.CODECOV_ORG_TOKEN }} diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 21e1275e7..0b90732eb 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -29,13 +29,13 @@ concurrency: 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,35 +44,40 @@ jobs: amazon-2023-amd64, arch, centos-stream-9-amd64, + centos-stream-10-amd64, debian-12-bookworm-x86, debian-12-bookworm-amd64, - fedora-39-amd64, - fedora-40-amd64, + fedora-41-amd64, + fedora-42-amd64, gentoo, 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 + 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: | @@ -87,22 +92,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 c7a73439c..5a83c16c3 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -46,6 +46,8 @@ jobs: steps: - name: Checkout Pillow uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up shell run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH @@ -58,24 +60,25 @@ 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-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: | @@ -83,9 +86,9 @@ jobs: .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 000000000..e6a5f6e77 --- /dev/null +++ b/.github/workflows/test-valgrind-memory.yml @@ -0,0 +1,60 @@ +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 + +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@v4 + 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 63aec586b..8818b3b23 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -40,6 +40,8 @@ jobs: steps: - uses: actions/checkout@v4 + 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 c8842e37b..6d8acc44f 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -35,25 +35,33 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"] + architecture: ["x64"] + include: + # Test the oldest Python on 32-bit + - { python-version: "3.9", architecture: "x86" } - 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 + with: + persist-credentials: false - name: Checkout cached dependencies uses: actions/checkout@v4 with: + persist-credentials: false repository: python-pillow/pillow-depends path: winbuild\depends - name: Checkout extra test images uses: actions/checkout@v4 with: + persist-credentials: false repository: python-pillow/test-images path: Tests\test-images @@ -63,22 +71,25 @@ jobs: 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') && !contains(matrix.python-version, '3.14') && 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 +97,8 @@ jobs: choco install nasm --no-progress echo "C:\Program Files\NASM" >> $env:GITHUB_PATH - choco install ghostscript --version=10.4.0 --no-progress - echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH + choco install ghostscript --version=10.5.1 --no-progress + echo "C:\Program Files\gs\gs10.05.1\bin" >> $env:GITHUB_PATH # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images @@ -137,6 +148,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' @@ -178,7 +193,7 @@ jobs: - 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 @@ -213,9 +228,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 6576292b5..b4b516228 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,11 @@ jobs: "ubuntu-latest", ] python-version: [ + "pypy3.11", "pypy3.10", + "3.14t", + "3.14", + "3.13t", "3.13", "3.12", "3.11", @@ -52,21 +56,23 @@ jobs: - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - { python-version: "3.10", PYTHONOPTIMIZE: 2 } # Free-threaded - - { os: "ubuntu-latest", python-version: "3.13-dev", disable-gil: true } + - { python-version: "3.14t", disable-gil: true } + - { python-version: "3.13t", disable-gil: true } # M1 only available for 3.10+ - { os: "macos-13", python-version: "3.9" } exclude: - { os: "macos-latest", python-version: "3.9" } runs-on: ${{ matrix.os }} - name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }} + name: ${{ matrix.os }} Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 - if: "${{ !matrix.disable-gil }}" with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -75,13 +81,6 @@ jobs: ".ci/*.sh" "pyproject.toml" - - name: Set up Python ${{ matrix.python-version }} (free-threaded) - uses: deadsnakes/action@v3.2.0 - if: "${{ matrix.disable-gil }}" - with: - python-version: ${{ matrix.python-version }} - nogil: ${{ matrix.disable-gil }} - - name: Set PYTHON_GIL if: "${{ matrix.disable-gil }}" run: | @@ -114,7 +113,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.13'" run: echo "::add-matcher::.github/problem-matchers/gcc.json" - name: Build @@ -154,11 +153,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 97c1adf09..5384a74c0 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -1,11 +1,33 @@ #!/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} + +# Setup that needs to be done before multibuild utils are invoked +PROJECTDIR=$(pwd) +if [[ "$(uname -s)" == "Darwin" ]]; then + # Safety check - macOS builds require that CIBW_ARCHS is set, and that it + # only contains a single value (even though cibuildwheel allows multiple + # values in CIBW_ARCHS). + if [[ -z "$CIBW_ARCHS" ]]; then + echo "ERROR: Pillow macOS builds require CIBW_ARCHS be defined." + exit 1 + fi + if [[ "$CIBW_ARCHS" == *" "* ]]; then + echo "ERROR: Pillow macOS builds only support a single architecture in CIBW_ARCHS." + exit 1 + fi + + # Build macOS dependencies in `build/darwin` + # Install them into `build/deps/darwin` + WORKDIR=$(pwd)/build/darwin + BUILD_PREFIX=$(pwd)/build/deps/darwin +else + # Build prefix will default to /usr/local + WORKDIR=$(pwd)/build + MB_ML_LIBC=${AUDITWHEEL_POLICY::9} + MB_ML_VER=${AUDITWHEEL_POLICY:9} fi -export PLAT=$CIBW_ARCHS +PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}" + +# Define custom utilities source wheels/multibuild/common_utils.sh source wheels/multibuild/library_builders.sh if [ -z "$IS_MACOS" ]; then @@ -15,93 +37,167 @@ fi ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds -FREETYPE_VERSION=2.13.2 -HARFBUZZ_VERSION=10.0.1 -LIBPNG_VERSION=1.6.44 -JPEGTURBO_VERSION=3.0.4 -OPENJPEG_VERSION=2.5.2 -XZ_VERSION=5.6.3 -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 +FREETYPE_VERSION=2.13.3 +HARFBUZZ_VERSION=11.2.1 +LIBPNG_VERSION=1.6.49 +JPEGTURBO_VERSION=3.1.1 +OPENJPEG_VERSION=2.5.3 +XZ_VERSION=5.8.1 +TIFF_VERSION=4.7.0 +LCMS2_VERSION=2.17 +ZLIB_VERSION=1.3.1 +ZLIB_NG_VERSION=2.2.4 +LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 +LIBAVIF_VERSION=1.3.0 + +function build_pkg_config { + if [ -e pkg-config-stamp ]; then return; fi + # This essentially duplicates the Homebrew recipe + CFLAGS="$CFLAGS -Wno-int-conversion" 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 + export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config + touch pkg-config-stamp +} + +function build_zlib_ng { + if [ -e zlib-stamp ]; then return; fi + build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat + + if [ -n "$IS_MACOS" ]; then + # Ensure that on macOS, the library name is an absolute path, not an + # @rpath, so that delocate picks up the right library (and doesn't need + # DYLD_LIBRARY_PATH to be set). The default Makefile doesn't have an + # option to control the install_name. + install_name_tool -id $BUILD_PREFIX/lib/libz.1.dylib $BUILD_PREFIX/lib/libz.1.dylib + fi + touch zlib-stamp +} function build_brotli { - local cmake=$(get_modern_cmake) + 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_NAME_DIR=$BUILD_PREFIX/lib . \ + && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -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 - fi + 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_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) + 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 \ - && meson setup build --buildtype=release -Dfreetype=enabled -Dglib=disabled) + && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=minsize -Dfreetype=enabled -Dglib=disabled -Dtests=disabled) (cd $out_dir/build \ && meson install) - if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp /usr/local/lib64/libharfbuzz* /usr/local/lib + touch harfbuzz-stamp +} + +function build_libavif { + if [ -e libavif-stamp ]; then return; fi + + python3 -m pip install meson ninja + + if [[ "$PLAT" == "x86_64" ]] || [ -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 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" \ + ) + 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 + + 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=ON \ + -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=$lto \ + -DCMAKE_C_VISIBILITY_PRESET=hidden \ + -DCMAKE_CXX_VISIBILITY_PRESET=hidden \ + -DCMAKE_BUILD_TYPE=$build_type \ + "${libavif_cmake_flags[@]}" \ + . \ + && make install) + touch libavif-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" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then + build_new_zlib + 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 + 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 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_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 - build_libwebp - CFLAGS=$ORIGINAL_CFLAGS + CFLAGS="$CFLAGS $webp_cflags" build_simple libwebp $LIBWEBP_VERSION \ + https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \ + --enable-libwebpmux --enable-libwebpdemux build_brotli @@ -115,31 +211,47 @@ function build { build_harfbuzz } +# 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 + # Homebrew (or similar packaging environments) install can contain some of + # the libraries that we're going to build. However, they may be compiled + # with a MACOSX_DEPLOYMENT_TARGET that doesn't match what we want to use, + # and they may bring in other dependencies that we don't want. The same will + # be true of any other locations on the path. To avoid conflicts, strip the + # path down to the bare minimum (which, on macOS, won't include any + # development dependencies). + export PATH="$BUILD_PREFIX/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" + export CMAKE_PREFIX_PATH=$BUILD_PREFIX - brew install pkg-config + # 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 + build_pkg_config + # Ensure cmake is available + python3 -m pip install cmake fi wrap_wheel_builder build +# 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 f593c7228..9f5561c46 100644 --- a/.github/workflows/wheels-test.ps1 +++ b/.github/workflows/wheels-test.ps1 @@ -9,14 +9,21 @@ 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 +if ("$venv" -like "*\cibw-run-*-win_amd64\*") { + & $venv\Scripts\$python -m pip install numpy +} 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 Tests\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 b30b1725f..94dbb4679 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -1,12 +1,24 @@ #!/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 @@ -23,5 +35,5 @@ fi # Runs tests python3 selftest.py -python3 -m pytest Tests/check_wheel.py -python3 -m pytest +python3 -m pytest -vv -x Tests/check_wheel.py +python3 -m pytest -vv -x diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 837e5ea91..16c350a14 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -13,6 +13,7 @@ on: paths: - ".ci/requirements-cibw.txt" - ".github/workflows/wheel*" + - "pyproject.toml" - "setup.py" - "wheels/*" - "winbuild/build_prepare.py" @@ -23,6 +24,7 @@ on: paths: - ".ci/requirements-cibw.txt" - ".github/workflows/wheel*" + - "pyproject.toml" - "setup.py" - "wheels/*" - "winbuild/build_prepare.py" @@ -40,61 +42,7 @@ env: FORCE_COLOR: 1 jobs: - build-1-QEMU-emulated-wheels: - if: github.event_name != 'schedule' - name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: - - pp310 - - cp3{9,10,11} - - cp3{12,13} - spec: - - manylinux2014 - - manylinux_2_28 - - musllinux - exclude: - - { 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 }} @@ -110,12 +58,12 @@ jobs: - name: "macOS 10.13 x86_64" os: macos-13 cibw_arch: x86_64 - build: "cp3{12,13}*" + build: "cp3{12,13,14}*" macosx_deployment_target: "10.13" - name: "macOS 10.15 x86_64" os: macos-13 cibw_arch: x86_64 - build: "pp310*" + build: "pp3*" macosx_deployment_target: "10.15" - name: "macOS arm64" os: macos-latest @@ -129,9 +77,18 @@ jobs: cibw_arch: x86_64 build: "*manylinux*" manylinux: "manylinux_2_28" + - name: "manylinux2014 and musllinux aarch64" + os: ubuntu-24.04-arm + cibw_arch: aarch64 + - name: "manylinux_2_28 aarch64" + os: ubuntu-24.04-arm + cibw_arch: aarch64 + build: "*manylinux*" + manylinux: "manylinux_2_28" steps: - uses: actions/checkout@v4 with: + persist-credentials: false submodules: true - uses: actions/setup-python@v5 @@ -148,10 +105,11 @@ jobs: env: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BUILD: ${{ matrix.build }} - CIBW_FREE_THREADED_SUPPORT: True + 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 MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - uses: actions/upload-artifact@v4 @@ -162,20 +120,26 @@ jobs: 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 + with: + persist-credentials: false - name: Checkout extra test images uses: actions/checkout@v4 with: + persist-credentials: false repository: python-pillow/test-images path: Tests\test-images @@ -222,8 +186,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_CACHE_PATH: "C:\\cibw" - CIBW_FREE_THREADED_SUPPORT: True - CIBW_PRERELEASE_PYTHONS: True + CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm -v {project}:C:\pillow @@ -251,13 +214,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.x" - cache: pip - cache-dependency-path: "Makefile" - run: make sdist @@ -268,7 +231,7 @@ jobs: scientific-python-nightly-wheels-publish: if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') - needs: [build-2-native-wheels, windows] + needs: [build-native-wheels, windows] runs-on: ubuntu-latest name: Upload wheels to scientific-python-nightly-wheels steps: @@ -278,14 +241,14 @@ jobs: path: dist merge-multiple: true - name: Upload wheels to scientific-python-nightly-wheels - uses: scientific-python/upload-nightly-action@82396a2ed4269ba06c6b2988bb4fd568ef3c3d6b # 0.6.1 + uses: scientific-python/upload-nightly-action@b36e8c0c10dbcfd2e05bf95f17ef8c14fd708dbf # 0.6.2 with: artifacts_path: dist anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} pypi-publish: if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist] + needs: [build-native-wheels, windows, sdist] runs-on: ubuntu-latest name: Upload release to PyPI environment: diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 000000000..5bdc48c30 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,7 @@ +# Configuration for the zizmor static analysis tool, run via pre-commit in CI +# https://woodruffw.github.io/zizmor/configuration/ +rules: + unpinned-uses: + config: + policies: + "*": ref-pin diff --git a/.gitignore b/.gitignore index 1dd6c9175..3033c2ea7 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 6254b8941..6abb732bb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,17 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.9 + rev: v0.12.0 hooks: - - id: ruff + - id: ruff-check args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.8.0 + rev: 25.1.0 hooks: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.7.10 + rev: 1.8.5 hooks: - id: bandit args: [--severity-level=high] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v19.1.1 + rev: v20.1.6 hooks: - id: clang-format types: [c] @@ -44,36 +44,42 @@ repos: - 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)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.3 + rev: 0.33.1 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate + - repo: https://github.com/woodruffw/zizmor-pre-commit + rev: v1.9.0 + hooks: + - id: zizmor + - repo: https://github.com/sphinx-contrib/sphinx-lint rev: v1.0.0 hooks: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 2.2.4 + rev: v2.6.0 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.20.2 + rev: v0.24.1 hooks: - id: validate-pyproject additional_dependencies: [trove-classifiers>=2024.10.12] - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.4.1 + rev: 1.5.0 hooks: - id: tox-ini-fmt diff --git a/.readthedocs.yml b/.readthedocs.yml index def6282dd..3e03c76ea 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 0a1703668..dfbbd24b3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,9 +2,19 @@ Changelog (Pillow) ================== -11.0.0 (unreleased) +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] diff --git a/LICENSE b/LICENSE index 7990a6e57..10dd42d9e 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 A. 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 af25dfd2d..48085b82e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -20,7 +20,6 @@ graft docs graft _custom_build # build/src control detritus -exclude .appveyor.yml exclude .clang-format exclude .coveragerc exclude .editorconfig diff --git a/Makefile b/Makefile index 53164b08a..ce780acbc 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,10 @@ doc html: htmlview: $(MAKE) -C docs htmlview +.PHONY: htmllive +htmllive: + $(MAKE) -C docs htmllive + .PHONY: doccheck doccheck: $(MAKE) doc @@ -43,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" @@ -92,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 diff --git a/README.md b/README.md index 5bbebaccb..365d356a0 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,6 @@ As of 2019, Pillow development is GitHub Actions build status (Test Docker) - AppVeyor CI build status (Windows) GitHub Actions build status (Wheels) @@ -98,7 +95,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 +104,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 9e6ec5dd4..3c6188c82 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 2d014e5a4..a955ec4fa 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/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index dbdd5a4f5..58566c4b2 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -9,6 +9,6 @@ from PIL import Image 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_large_memory.py b/Tests/check_large_memory.py index a9ce79e57..c9feda3b1 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -32,7 +32,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit sy 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/Tests/check_large_memory_numpy.py index f4ca8d0aa..458b0ab72 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -28,7 +28,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit sy 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_png_dos.py b/Tests/check_png_dos.py index 63d6657bc..1bfb94ab7 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -3,26 +3,25 @@ from __future__ import annotations 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_wheel.py b/Tests/check_wheel.py index 4b91984f5..a78fb09b0 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -1,20 +1,28 @@ from __future__ import annotations +import platform import sys from PIL import features +from .helper import is_pypy + def test_wheel_modules() -> None: - expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} + expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"} - # tkinter is not available in cibuildwheel installed CPython on Windows - try: - import tkinter + 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") + 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") assert set(features.get_supported_modules()) == expected_modules @@ -34,10 +42,13 @@ def test_wheel_features() -> None: "fribidi", "harfbuzz", "libjpeg_turbo", + "zlib_ng", "xcb", } if sys.platform == "win32": expected_features.remove("xcb") + elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm": + expected_features.remove("zlib_ng") assert set(features.get_supported_features()) == expected_features diff --git a/Tests/helper.py b/Tests/helper.py index d6a93a803..e71b4665b 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -9,11 +9,11 @@ import os import shutil import subprocess import sys -import sysconfig import tempfile from collections.abc import Sequence from functools import lru_cache from io import BytesIO +from pathlib import Path from typing import Any, Callable import pytest @@ -96,7 +96,10 @@ 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: @@ -137,21 +140,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 @@ -165,6 +161,12 @@ 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) @@ -270,7 +272,7 @@ def _cached_hopper(mode: str) -> Image.Image: else: im = hopper() if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="BGR;"): im = im.convert(mode) else: try: @@ -327,16 +329,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 @@ -358,10 +351,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 000000000..07964487f 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 000000000..c793ade1c 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 000000000..87e4394f0 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 000000000..f6dbd1b47 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 000000000..a47a0562b 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 000000000..658cfec17 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 000000000..c73e70a3a 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 000000000..f57203093 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 000000000..8a9fb5e54 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 000000000..0c7c7620f 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 000000000..0181b088c 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 000000000..ddaa02f3f 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 000000000..63326b86c 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 000000000..ed5dbe3d0 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 000000000..ccd8861d5 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 000000000..f2753395f 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 000000000..52076cafd 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 000000000..468dcde00 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 000000000..f808357fc 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 000000000..41faa6050 Binary files /dev/null and b/Tests/images/avif/xmp_tags_orientation.avif 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 000000000..000377b6c Binary files /dev/null and b/Tests/images/drawing_emf_ref_72_144.png differ diff --git a/Tests/images/full_gimp_palette.gpl b/Tests/images/full_gimp_palette.gpl new file mode 100644 index 000000000..004217210 --- /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 000000000..fe97b83ba --- /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 000000000..063833b3a --- /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 509c42b26..8992a1657 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 9b9eda883..f0cb534b6 100644 Binary files a/Tests/images/imagedraw_rectangle_I.tiff and b/Tests/images/imagedraw_rectangle_I.tiff differ diff --git a/Tests/images/jfif_unit_cm.jpg b/Tests/images/jfif_unit_cm.jpg new file mode 100644 index 000000000..78b50e60a Binary files /dev/null and b/Tests/images/jfif_unit_cm.jpg differ diff --git a/Tests/images/multiline_text_justify.png b/Tests/images/multiline_text_justify.png new file mode 100644 index 000000000..32eed34cd 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 000000000..6d3fb421d Binary files /dev/null and b/Tests/images/multiline_text_justify_anchor.png differ diff --git a/Tests/images/op_index.qoi b/Tests/images/op_index.qoi new file mode 100644 index 000000000..e626aafe6 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 000000000..8c5743a98 Binary files /dev/null and b/Tests/images/p_4_planes.pcx differ diff --git a/Tests/oss-fuzz/python.supp b/Tests/oss-fuzz/python.supp index 36385d672..4803497ad 100644 --- a/Tests/oss-fuzz/python.supp +++ b/Tests/oss-fuzz/python.supp @@ -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 90eb8713a..e42ec90aa 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -7,7 +7,7 @@ import fuzzers 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"): @@ -32,21 +32,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_arrow.py b/Tests/test_arrow.py new file mode 100644 index 000000000..b86c77b9a --- /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 7f8487921..82cab39c6 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() diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 36ab187f2..26945ae1a 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -19,7 +19,7 @@ except ImportError: 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 @@ class TestColorLut3DCoreAPI: ] return ( channels, - size_1d, - size_2d, - size_3d, + (size_1d, size_2d, size_3d), [item for sublist in table for item in sublist], ) @@ -89,21 +87,21 @@ class TestColorLut3DCoreAPI: 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) + im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), 16) @pytest.mark.parametrize( "lut_mode, table_channels, table_size", @@ -264,7 +262,7 @@ class TestColorLut3DCoreAPI: 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, @@ -286,7 +284,7 @@ class TestColorLut3DCoreAPI: # 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, @@ -307,7 +305,7 @@ class TestColorLut3DCoreAPI: # 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, @@ -388,10 +386,12 @@ class TestColorLut3DFilter: 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 2c1de8bc3..2a22f805d 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -188,5 +188,5 @@ class TestEnvVars: ), ) 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 c140156f9..98d833736 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -12,19 +12,16 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS 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 @@ class TestDecompressionBomb: 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 @@ class TestDecompressionBomb: 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 82ff14181..88479ff0d 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -47,7 +47,6 @@ 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) diff --git a/Tests/test_features.py b/Tests/test_features.py index ed7929973..d06fb4d84 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -19,7 +19,7 @@ def test_check() -> None: assert features.check_codec(codec) == features.check(codec) for feature in features.features: if "webp" in feature: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="webp"): assert features.check_feature(feature) == features.check(feature) else: assert features.check_feature(feature) == features.check(feature) @@ -36,10 +36,11 @@ def test_version() -> 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: @@ -48,24 +49,24 @@ def test_version() -> None: test(codec, features.version_codec) for feature in features.features: if "webp" in feature: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="webp"): test(feature, features.version_feature) else: test(feature, features.version_feature) def test_webp_transparency() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="transp_webp"): assert (features.check("transp_webp") or False) == features.check_module("webp") def test_webp_mux() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="webp_mux"): assert (features.check("webp_mux") or False) == features.check_module("webp") def test_webp_anim() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="webp_anim"): assert (features.check("webp_anim") or False) == features.check_module("webp") @@ -94,10 +95,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: diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index ee6c867c3..12204b5b7 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -12,6 +12,7 @@ from PIL import Image, ImageSequence, PngImagePlugin # (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 +21,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 +36,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 +54,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 +62,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 +100,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 +145,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 +159,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 +197,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,24 +253,28 @@ 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") @@ -255,6 +282,7 @@ def test_apng_mode() -> None: assert im.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") @@ -262,6 +290,7 @@ def test_apng_mode() -> None: assert im.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") @@ -271,59 +300,68 @@ def test_apng_mode() -> None: 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, 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): - 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_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 +379,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 +406,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 +418,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 +436,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 +446,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 +489,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 +501,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 +512,13 @@ 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_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 +619,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 +641,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 +709,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))]) @@ -693,7 +733,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 +747,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 000000000..3fac51ac6 --- /dev/null +++ b/Tests/test_file_avif.py @@ -0,0 +1,781 @@ +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, + 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" + image.getdata() + + # 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" + reloaded.getdata() + + # avifdec hopper.avif avif/hopper_avif_write.png + assert_image_similar_tofile( + reloaded, "Tests/images/avif/hopper_avif_write.png", 6.02 + ) + + # 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, 8.62) + + 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)) + + # 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)) + 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 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", + [ + { + "aq-mode": "1", + "enable-chroma-deltaq": "1", + }, + (("aq-mode", "1"), ("enable-chroma-deltaq", "1")), + [("aq-mode", "1"), ("enable-chroma-deltaq", "1")], + ], + ) + 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 im.n_frames == 1 + assert not im.is_animated + + with Image.open("Tests/images/avif/star.avifs") as im: + 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 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 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 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 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 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 1e2f20c40..5f6b263a1 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -4,12 +4,11 @@ from pathlib import Path 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 2ff4160bd..746b2e180 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -15,25 +15,19 @@ from .helper import ( ) -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: @@ -66,7 +60,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 +68,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 +90,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 +106,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: @@ -152,7 +146,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) @@ -196,9 +190,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,9 +214,9 @@ 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() + with Image.open(io.BytesIO(data)) as im: + with pytest.raises(ValueError): + im.load() def test_offset() -> None: @@ -230,3 +224,13 @@ def test_offset() -> None: # 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 77ee5b0ea..362578c56 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): @@ -79,8 +79,8 @@ def test_handler(tmp_path: Path) -> 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 237045acc..597ab5083 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -4,8 +4,6 @@ import pytest from PIL import ContainerIO, Image -from .helper import hopper - TEST_FILE = "Tests/images/dummy.container" @@ -15,15 +13,15 @@ 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 hopper() as im: - container = ContainerIO.ContainerIO(im, 0, 0) + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 0, 0) assert container.seekable() is True diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 65337cad9..e9d88dd39 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 9a826ebe8..5c7a943b1 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -9,7 +9,13 @@ import pytest 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" @@ -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", ( @@ -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 @@ -366,9 +400,9 @@ def test_not_implemented(test_file: str) -> None: 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) @@ -382,10 +416,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 = hopper("L") + with pytest.raises(OSError, match="only RGB mode can be written as BC5"): + im.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 672c04a4d..d94de7287 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -15,6 +15,7 @@ from .helper import ( is_win32, mark_if_feature_version, skip_unless_feature, + timeout_unless_slower_valgrind, ) HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript() @@ -86,6 +87,8 @@ simple_eps_file_with_long_binary_data = ( 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 @@ -95,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: @@ -223,6 +230,8 @@ def test_showpage() -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_transparency() -> None: 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" @@ -235,7 +244,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") @@ -270,7 +279,7 @@ def test_1(filename: str) -> None: 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) @@ -304,6 +313,7 @@ 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") @@ -312,6 +322,7 @@ def test_render_scale2() -> None: # 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") @@ -388,7 +399,7 @@ 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/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index f86fb8d09..0fadd01d0 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 @@ animated_test_file_with_prefix_chunk = "Tests/images/2422.flc" 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,35 @@ 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 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[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 +90,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 +121,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 +145,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 +178,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 +194,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 e32f30a01..8d8064692 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 0c544245a..fdd7b3757 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: diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index d512df284..806532c17 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -4,6 +4,8 @@ import pytest from PIL import GdImageFile, UnidentifiedImageError +from .helper import assert_image_similar_tofile + TEST_GD_FILE = "Tests/images/hopper.gd" @@ -11,6 +13,7 @@ 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_bad_mode() -> None: diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 16c8466f3..ed70de15b 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -4,6 +4,7 @@ import warnings from collections.abc import Generator from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -21,9 +22,6 @@ from .helper import ( # 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: @@ -104,7 +106,7 @@ def test_palette_not_needed_for_second_frame() -> None: 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 +116,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 +224,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 +250,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 +259,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 +269,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,7 +277,7 @@ 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: @@ -281,7 +285,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None: 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]) @@ -304,8 +308,9 @@ 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 @@ -313,10 +318,10 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None: im = im.convert("RGB") # Check a color only from the old palette - assert im.load()[0, 0] == original_color + assert im.getpixel((0, 0)) == original_color # Check a color from the new palette - assert im.load()[24, 24] not in first_frame_colors + assert im.getpixel((24, 24)) not in first_frame_colors def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: @@ -325,7 +330,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: @@ -341,7 +346,7 @@ def test_palette_handling(tmp_path: Path) -> None: im = im.resize((100, 100), Image.Resampling.LANCZOS) im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) - f = str(tmp_path / "temp.gif") + f = tmp_path / "temp.gif" im2.save(f, optimize=True) with Image.open(f) as reloaded: @@ -352,7 +357,7 @@ 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") + out = tmp_path / "temp.gif" im.copy().save(out, "GIF", **kwargs) reloaded = Image.open(out) @@ -398,6 +403,7 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None: 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 +412,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 +448,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 +463,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 +471,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 +498,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 +527,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 +540,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 +570,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 +612,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 +635,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 +676,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 +718,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 +744,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 +758,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 +781,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 +827,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 +845,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 +859,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 +894,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 +909,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 +927,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 +940,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 +962,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 +971,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 +981,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 +991,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 +1007,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,7 +1016,7 @@ 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"): @@ -993,7 +1034,7 @@ 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") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") im.info["comment"] = b"Test comment text" im.save(out) @@ -1010,7 +1051,7 @@ def test_comment(tmp_path: Path) -> None: 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 +1077,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 +1111,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 +1121,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 +1151,7 @@ def test_version(tmp_path: Path) -> 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 +1159,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 +1176,7 @@ def test_append_images(tmp_path: Path) -> 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 +1185,12 @@ def test_append_images(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 == 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 +1217,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 +1225,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 +1242,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 +1256,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 +1264,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 +1274,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 +1301,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 +1313,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 +1324,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 +1340,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,7 +1355,7 @@ 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:] ) @@ -1308,6 +1363,8 @@ def test_palette_save_all_P(tmp_path: Path) -> None: with Image.open(out) as im: # Assert that the frames are correct, and each frame has the same palette assert_image_equal(im.convert("RGB"), frames[0].convert("RGB")) + assert im.palette is not None + assert im.global_palette is not None assert im.palette.palette == im.global_palette.palette im.seek(1) @@ -1322,7 +1379,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,45 +1392,45 @@ 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) - import pickle + h = GifImagePlugin.getheader(im, passed_palette) + d = GifImagePlugin.getdata(im) - # 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) + import pickle - assert h == h_target - assert d == d_target - finally: - GifImagePlugin._FORCE_OPTIMIZE = False + # 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 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() @@ -1393,24 +1450,24 @@ def test_lzw_bits() -> None: ), ) def test_extents( - test_file: str, loading_strategy: GifImagePlugin.LoadingStrategy + test_file: str, + loading_strategy: GifImagePlugin.LoadingStrategy, + monkeypatch: pytest.MonkeyPatch, ) -> None: - GifImagePlugin.LOADING_STRATEGY = loading_strategy - try: - with Image.open("Tests/images/" + test_file) as im: - assert im.size == (100, 100) + 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 - assert im.n_frames == 2 - assert im.size == (100, 100) + # Check that n_frames does not change the size + assert im.n_frames == 2 + assert im.size == (100, 100) - im.seek(1) - assert im.size == (150, 150) + im.seek(1) + assert im.size == (150, 150) - im.load() - assert im.im.size == (150, 150) - finally: - GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST + im.load() + assert im.im.size == (150, 150) def test_missing_background() -> None: @@ -1422,17 +1479,22 @@ 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 -def test_optimizing_p_rgba(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") +@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) @@ -1443,7 +1505,8 @@ def test_optimizing_p_rgba(tmp_path: Path) -> None: im2 = Image.new("P", (100, 100)) im2.putpalette(data, "RGBA") - im1.save(out, save_all=True, append_images=[im2]) + 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 e8d5f1705..08862113b 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 aba473d24..960e5f4be 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): @@ -79,8 +79,8 @@ def test_handler(tmp_path: Path) -> 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 8275bd0d8..e4f09a09c 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): @@ -81,8 +81,8 @@ def test_handler(tmp_path: Path) -> 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 16f6b3651..8ff59161f 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,6 +69,7 @@ def test_save_append_images(tmp_path: Path) -> None: assert_image_similar_tofile(im, temp_file, 1) with Image.open(temp_file) as reread: + assert isinstance(reread, IcnsImagePlugin.IcnsImageFile) reread.size = (16, 16) reread.load(2) assert_image_equal(reread, provided_im) @@ -84,10 +91,13 @@ 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 - with pytest.warns(DeprecationWarning): + with pytest.warns( + DeprecationWarning, match=r"Setting size to \(width, height, scale\)" + ): im.size = (w, h, r) im.load() assert im.mode == "RGBA" @@ -112,6 +122,7 @@ def test_older_icon() -> None: wr = w * r hr = h * r with Image.open("Tests/images/pillow2.icns") as im2: + assert isinstance(im2, IcnsImagePlugin.IcnsImageFile) im2.size = (w, h) im2.load(r) assert im2.mode == "RGBA" @@ -129,6 +140,7 @@ def test_jp2_icon() -> None: wr = w * r hr = h * r with Image.open("Tests/images/pillow3.icns") as im2: + assert isinstance(im2, IcnsImagePlugin.IcnsImageFile) im2.size = (w, h) im2.load(r) assert im2.mode == "RGBA" diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 37770498a..36b608a0a 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -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") @@ -243,27 +251,23 @@ def test_draw_reloaded(tmp_path: Path) -> None: assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico") -def test_truncated_mask() -> None: +def test_truncated_mask(monkeypatch: pytest.MonkeyPatch) -> None: # 1 bpp with open("Tests/images/hopper_mask.ico", "rb") as fp: data = fp.read() - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) data = data[:-3] - try: - with Image.open(io.BytesIO(data)) as im: - with Image.open("Tests/images/hopper_mask.png") as expected: - assert im.mode == "1" + 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") + # 32 bpp + output = io.BytesIO() + expected = hopper("RGBA") + expected.save(output, "ico", bitmap_format="bmp") - data = output.getvalue()[:-1] + data = output.getvalue()[:-1] - with Image.open(io.BytesIO(data)) as im: - assert im.mode == "RGB" - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + 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 036965bf5..55c6b7305 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 8a7c59fb1..0ac325d91 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -23,6 +23,9 @@ def test_open() -> None: assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")] assert_image_equal(im, expected) + with Image.open(f) as im: + assert im.load() is not None + def test_getiptcinfo_jpg_none() -> None: # Arrange @@ -58,10 +61,7 @@ def test_getiptcinfo_fotostation() -> None: # Assert assert iptc is not None - for tag in iptc.keys(): - if tag[0] == 240: - return - pytest.fail("FotoStation tag not found") + assert 240 in (tag[0] for tag in iptc.keys()), "FotoStation tag not found" def test_getiptcinfo_zero_padding() -> None: @@ -102,7 +102,7 @@ def test_i() -> None: c = b"a" # Act - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="IptcImagePlugin.i"): ret = IptcImagePlugin.i(c) # Assert @@ -117,7 +117,7 @@ def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(sys, "stdout", mystdout) # Act - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="IptcImagePlugin.dump"): IptcImagePlugin.dump(c) # Assert @@ -125,5 +125,5 @@ def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: def test_pad_deprecation() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="IptcImagePlugin.PAD"): assert IptcImagePlugin.PAD == b"\0\0\0\0" diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index cde951395..6dab418bf 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -32,6 +32,7 @@ from .helper import ( is_win32, mark_if_feature_version, skip_unless_feature, + timeout_unless_slower_valgrind, ) ElementTree: ModuleType | None @@ -83,7 +84,7 @@ class TestFileJpeg: @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): im.save(f) @@ -91,6 +92,7 @@ class TestFileJpeg: 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,30 +130,26 @@ class TestFileJpeg: 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, ...]: @@ -181,6 +179,10 @@ class TestFileJpeg: 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 +192,7 @@ class TestFileJpeg: 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 +236,7 @@ class TestFileJpeg: # 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 +248,11 @@ class TestFileJpeg: ) 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 +266,7 @@ class TestFileJpeg: 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 +279,20 @@ class TestFileJpeg: 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 +305,7 @@ class TestFileJpeg: 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 +314,8 @@ class TestFileJpeg: 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() @@ -328,7 +335,7 @@ class TestFileJpeg: assert exif[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) @@ -349,7 +356,6 @@ class TestFileJpeg: assert exif.get_ifd(0x8825) == {} transposed = ImageOps.exif_transpose(im) - assert transposed is not None exif = transposed.getexif() assert exif.get_ifd(0x8825) == {} @@ -420,8 +426,12 @@ class TestFileJpeg: 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") @@ -490,20 +500,21 @@ class TestFileJpeg: 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 +531,13 @@ class TestFileJpeg: @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 +553,20 @@ class TestFileJpeg: @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 @@ -652,6 +666,7 @@ class TestFileJpeg: 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 @@ -694,6 +709,7 @@ class TestFileJpeg: @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) @@ -715,7 +731,7 @@ class TestFileJpeg: 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 +746,13 @@ class TestFileJpeg: # 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 +770,7 @@ class TestFileJpeg: 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 +781,7 @@ class TestFileJpeg: 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)) @@ -848,8 +867,10 @@ class TestFileJpeg: 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: @@ -896,6 +917,7 @@ class TestFileJpeg: 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 +943,7 @@ class TestFileJpeg: 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 @@ -992,22 +1014,27 @@ class TestFileJpeg: assert im.getxmp() == {"xmpmeta": None} def test_save_xmp(self, tmp_path: Path) -> None: - f = str(tmp_path / "temp.jpg") + 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" - im.info["xmp"] = b"1" * 65504 - im.save(f) + # 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) - @pytest.mark.timeout(timeout=1) - def test_eof(self) -> None: + @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): @@ -1020,11 +1047,10 @@ class TestFileJpeg: 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() @@ -1038,10 +1064,16 @@ class TestFileJpeg: for marker in b"\xff\xd8", b"\xff\xd9": assert marker in data[1] assert marker in data[2] - # DHT, DQT - for marker in b"\xff\xc4", b"\xff\xdb": + + # 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] @@ -1067,9 +1099,10 @@ class TestFileJpeg: def test_deprecation(self) -> None: with Image.open(TEST_FILE) as im: - with pytest.warns(DeprecationWarning): + assert isinstance(im, JpegImagePlugin.JpegImageFile) + with pytest.warns(DeprecationWarning, match="huffman_ac"): assert im.huffman_ac == {} - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="huffman_dc"): assert im.huffman_dc == {} @@ -1077,7 +1110,7 @@ class TestFileJpeg: @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) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 26b085601..a5365a90d 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -2,6 +2,7 @@ from __future__ import annotations 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 @@ EXTRA_DIR = "Tests/images/jpeg2000" 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,76 @@ 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() -> None: - im = roundtrip(test_card, irreversible=True, quality_layers=[20]) - assert_image_similar(im, test_card, 2.0) +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() -> None: - im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") - assert_image_similar(im, test_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() -> None: - im = roundtrip(test_card, num_resolutions=8, progression="RLCP") - assert_image_equal(im, test_card) +def test_prog_res_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, num_resolutions=8, progression="RLCP") + assert_image_equal(im, card) @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) @@ -172,14 +182,11 @@ 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( @@ -205,31 +212,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( @@ -245,24 +254,30 @@ def test_layers() -> None: (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): @@ -276,7 +291,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) @@ -300,6 +315,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" ) @@ -310,6 +337,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: @@ -397,6 +436,7 @@ 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 @@ -404,37 +444,39 @@ def test_pclr() -> None: f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2" ) as im: assert im.mode == "P" + assert im.palette is not None 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( @@ -457,15 +499,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 62f8719af..1ec39eba5 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -36,14 +36,11 @@ class LibTiffTestCase: 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 +81,7 @@ class TestFileLibTiff(LibTiffTestCase): 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) @@ -127,7 +124,7 @@ class TestFileLibTiff(LibTiffTestCase): """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) @@ -155,8 +152,9 @@ class TestFileLibTiff(LibTiffTestCase): @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: @@ -174,6 +172,7 @@ class TestFileLibTiff(LibTiffTestCase): ] with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) if legacy_api: reloaded = loaded.tag.named() else: @@ -216,6 +215,7 @@ class TestFileLibTiff(LibTiffTestCase): # 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] @@ -251,7 +251,7 @@ class TestFileLibTiff(LibTiffTestCase): # 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) @@ -313,14 +313,15 @@ class TestFileLibTiff(LibTiffTestCase): } 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 ( @@ -351,14 +352,16 @@ class TestFileLibTiff(LibTiffTestCase): ) 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 @@ -369,17 +372,18 @@ class TestFileLibTiff(LibTiffTestCase): ) -> 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: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) if 700 in reloaded.tag_v2: 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: @@ -387,7 +391,7 @@ class TestFileLibTiff(LibTiffTestCase): 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: @@ -404,7 +408,7 @@ class TestFileLibTiff(LibTiffTestCase): 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: @@ -424,7 +428,7 @@ class TestFileLibTiff(LibTiffTestCase): 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"] @@ -434,12 +438,15 @@ class TestFileLibTiff(LibTiffTestCase): """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] @@ -461,7 +468,7 @@ class TestFileLibTiff(LibTiffTestCase): 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") @@ -474,7 +481,7 @@ class TestFileLibTiff(LibTiffTestCase): # 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) @@ -498,7 +505,7 @@ class TestFileLibTiff(LibTiffTestCase): 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: @@ -506,7 +513,7 @@ class TestFileLibTiff(LibTiffTestCase): 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: @@ -514,7 +521,7 @@ class TestFileLibTiff(LibTiffTestCase): 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) @@ -529,7 +536,7 @@ class TestFileLibTiff(LibTiffTestCase): 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) @@ -538,19 +545,20 @@ class TestFileLibTiff(LibTiffTestCase): 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) @@ -576,6 +584,7 @@ class TestFileLibTiff(LibTiffTestCase): 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) @@ -595,6 +604,7 @@ class TestFileLibTiff(LibTiffTestCase): # 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): @@ -614,6 +624,7 @@ class TestFileLibTiff(LibTiffTestCase): 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 @@ -690,25 +701,29 @@ class TestFileLibTiff(LibTiffTestCase): 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: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) if Image.core.libtiff_support_custom_tags: assert reloaded.tag_v2[34665] == 125456 @@ -717,7 +732,7 @@ class TestFileLibTiff(LibTiffTestCase): ) -> 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 @@ -728,7 +743,7 @@ class TestFileLibTiff(LibTiffTestCase): # 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 @@ -740,7 +755,7 @@ class TestFileLibTiff(LibTiffTestCase): 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()) @@ -783,13 +798,14 @@ class TestFileLibTiff(LibTiffTestCase): 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) @@ -806,7 +822,7 @@ class TestFileLibTiff(LibTiffTestCase): 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 @@ -868,7 +884,7 @@ class TestFileLibTiff(LibTiffTestCase): 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) @@ -1012,7 +1028,7 @@ class TestFileLibTiff(LibTiffTestCase): @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, @@ -1030,6 +1046,17 @@ class TestFileLibTiff(LibTiffTestCase): 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 = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + assert_image_equal_tofile(im, "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" @@ -1083,6 +1110,7 @@ class TestFileLibTiff(LibTiffTestCase): 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() @@ -1098,6 +1126,27 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_similar(base_im, im, 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: # Assert that a TIFF image with SampleFormat=UINT tag is not corrupted @@ -1123,16 +1172,14 @@ class TestFileLibTiff(LibTiffTestCase): 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: @@ -1141,34 +1188,33 @@ class TestFileLibTiff(LibTiffTestCase): 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, "TIFF", **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") + out = tmp_path / "temp.tif" with pytest.raises(SystemError): 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 617e1e89c..65ba80c20 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -32,7 +32,7 @@ class TestFileLibTiffSmall(LibTiffTestCase): 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_mic.py b/Tests/test_file_mic.py index 9a6f13ea3..9aeb306e4 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -30,11 +30,13 @@ def test_sanity() -> None: 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 e0f42a266..13ace9246 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -6,7 +6,7 @@ from typing import Any import pytest -from PIL import Image, ImageFile, MpoImagePlugin +from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin from .helper import ( assert_image_equal, @@ -29,25 +29,32 @@ def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile: @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 +70,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 +80,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 @@ -146,6 +156,7 @@ def test_reload_exif_after_seek() -> None: def test_mp(test_file: str) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() + assert mpinfo is not None assert mpinfo[45056] == b"0100" assert mpinfo[45057] == 2 @@ -155,6 +166,7 @@ def test_mp_offset() -> None: # in APP2 data, in contrast to normal 8 with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im: mpinfo = im._getmp() + assert mpinfo is not None assert mpinfo[45056] == b"0100" assert mpinfo[45057] == 2 @@ -171,6 +183,7 @@ def test_mp_no_data() -> None: def test_mp_attribute(test_file: str) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() + assert mpinfo is not None for frame_number, mpentry in enumerate(mpinfo[0xB002]): mpattr = mpentry["Attribute"] if frame_number: @@ -211,12 +224,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 @@ -230,6 +245,8 @@ def test_eoferror() -> None: 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) @@ -293,3 +310,18 @@ def test_save_all() -> None: # Test that a single frame image will not be saved as an MPO jpg = roundtrip(im, save_all=True) assert "mp" not in jpg.info + + +def test_save_xmp() -> None: + im = Image.new("RGB", (1, 1)) + im2 = Image.new("RGB", (1, 1), "#f00") + im2.encoderinfo = {"xmp": b"Second frame"} + im_reloaded = roundtrip(im, xmp=b"First frame", save_all=True, append_images=[im2]) + + # Test that encoderinfo is unchanged + assert im2.encoderinfo == {"xmp": b"Second frame"} + + assert im_reloaded.info["xmp"] == b"First frame" + + im_reloaded.seek(1) + assert im_reloaded.info["xmp"] == b"Second frame" diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index b0964aabe..8c91922bd 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -15,7 +15,7 @@ YA_EXTRA_DIR = "Tests/images/msp" 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 194f39b30..58208ba99 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -14,7 +14,7 @@ from .helper import assert_image_equal, hopper, magick_command 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_pcx.py b/Tests/test_file_pcx.py index b3f38c3e5..2e999eff6 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 @@ from .helper import assert_image_equal, hopper 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,39 @@ 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) +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" diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 1d5001b1a..a2218673b 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -13,7 +13,12 @@ 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: @@ -55,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) @@ -80,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) @@ -89,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: @@ -117,7 +122,7 @@ 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") + outfile = tmp_path / "temp.pdf" im.save(outfile, "PDF", **params) with open(outfile, "rb") as fp: @@ -144,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) @@ -177,7 +182,7 @@ def test_save_all(tmp_path: Path) -> 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) @@ -264,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() @@ -292,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) @@ -339,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 0abf9866f..0a51fd493 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -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 @@ class TestFilePng: 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,11 +100,11 @@ class TestFilePng: assert im.format == "PNG" assert im.get_format_mimetype() == "image/png" - for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]: + for mode in ["1", "L", "P", "RGB", "I;16", "I;16B"]: im = hopper(mode) im.save(test_file) with Image.open(test_file) as reloaded: - if mode in ("I", "I;16B"): + if mode == "I;16B": reloaded = reloaded.convert(mode) assert_image_equal(reloaded, im) @@ -250,7 +250,7 @@ class TestFilePng: # 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 @@ -271,7 +271,7 @@ class TestFilePng: 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 @@ -294,7 +294,7 @@ class TestFilePng: 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 @@ -315,7 +315,7 @@ class TestFilePng: im_rgba = im.convert("RGBA") assert im_rgba.getchannel("A").getcolors()[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: @@ -329,7 +329,7 @@ class TestFilePng: 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 +338,8 @@ class TestFilePng: 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 +363,7 @@ class TestFilePng: 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 +373,20 @@ class TestFilePng: 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 @@ -490,7 +488,7 @@ class TestFilePng: 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: @@ -551,7 +549,7 @@ class TestFilePng: 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) @@ -578,6 +576,7 @@ class TestFilePng: 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: @@ -598,8 +597,9 @@ class TestFilePng: (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", @@ -609,22 +609,25 @@ class TestFilePng: # 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 a UnicodeDecodeError in load_end - with Image.open("Tests/images/truncated_image.png") as im: - # The file is truncated - with pytest.raises(OSError): - im.text() - ImageFile.LOAD_TRUNCATED_IMAGES = True - assert isinstance(im.text, dict) - ImageFile.LOAD_TRUNCATED_IMAGES = False - # Raises an EOFError in load_end with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: + assert 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 + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + assert isinstance(im.text, dict) + def test_unknown_compression_method(self) -> None: with pytest.raises(SyntaxError, match="Unknown compression method"): PngImagePlugin.PngImageFile("Tests/images/unknown_compression_method.png") @@ -649,21 +652,22 @@ class TestFilePng: @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: @@ -673,8 +677,8 @@ class TestFilePng: 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 len(reloaded.png.im_palette[1]) == 3 @@ -723,11 +727,12 @@ class TestFilePng: 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 @@ -743,7 +748,7 @@ class TestFilePng: ) 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: @@ -752,7 +757,7 @@ class TestFilePng: 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: @@ -770,38 +775,41 @@ class TestFilePng: 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 + monkeypatch.setattr(sys, "stdout", mystdout) with Image.open(TEST_PNG_FILE) as im: im.save(sys.stdout, "PNG") - # Reset stdout - sys.stdout = old_stdout - 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") @@ -810,11 +818,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() @@ -822,7 +830,4 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase): 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 fb08d613a..68f2f9468 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 @@ -93,7 +94,7 @@ def test_16bit_pgm() -> None: 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) @@ -105,7 +106,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) @@ -116,7 +117,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) @@ -127,7 +128,7 @@ 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) @@ -193,8 +194,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) @@ -206,7 +207,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) @@ -217,7 +218,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) @@ -234,7 +235,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) @@ -244,7 +245,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") @@ -254,7 +255,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") @@ -269,7 +270,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") @@ -278,7 +279,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") @@ -287,30 +288,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: @@ -319,7 +318,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") @@ -330,16 +329,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 @@ -353,7 +352,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") @@ -367,22 +366,18 @@ 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 + monkeypatch.setattr(sys, "stdout", mystdout) with Image.open(TEST_FILE) as im: im.save(sys.stdout, "PPM") - # Reset stdout - sys.stdout = old_stdout - if isinstance(mystdout, MyStdOut): mystdout = mystdout.buffer with Image.open(mystdout) as reloaded: diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index e6c79e40b..38a88cd17 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -25,16 +25,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 +44,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 +59,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 @@ -115,11 +123,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 @@ -171,5 +181,6 @@ 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: + assert isinstance(im, PsdImagePlugin.PsdImageFile) with pytest.raises(SyntaxError): im.layers diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py index fd4b981ce..b9becb24f 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 e13a8019e..abf424dbf 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 - for mode in ("L", "RGB", "RGBA"): - roundtrip(hopper(mode)) +@pytest.mark.parametrize("mode", ("L", "RGB", "RGBA")) +def test_write(mode: str, tmp_path: Path) -> None: + roundtrip(hopper(mode), tmp_path) - # 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 66c88e9d8..3b3c3b4a5 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -7,7 +7,7 @@ from pathlib import Path 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 @@ -92,6 +96,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 +154,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 6cfff8730..c2f162cf9 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 = Image.merge("RGB", (b, g, r)) + assert_image_equal_tofile(im, 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 6217ebedd..084d0f288 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 a03a6a6e1..bd39de2e1 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -1,8 +1,6 @@ from __future__ import annotations import os -from glob import glob -from itertools import product from pathlib import Path import pytest @@ -15,16 +13,29 @@ _TGA_DIR = os.path.join("Tests", "images", "tga") _TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") -_MODES = ("L", "LA", "P", "RGB", "RGBA") _ORIGINS = ("tl", "bl") _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} -@pytest.mark.parametrize("mode", _MODES) -def test_sanity(mode: 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", _ORIGINS) +@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 +47,40 @@ def test_sanity(mode: str, tmp_path: Path) -> None: assert_image_equal(saved_im, original_im) - png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png")) + 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 - for png_path in png_paths: - with Image.open(png_path) as reference_im: - assert reference_im.mode == mode + path_no_ext = os.path.splitext(png_path)[0] + tga_path = "{}_{}_{}.tga".format(path_no_ext, origin, "rle" if rle else "raw") - path_no_ext = os.path.splitext(png_path)[0] - for origin, rle in product(_ORIGINS, (True, False)): - tga_path = "{}_{}_{}.tga".format( - path_no_ext, origin, "rle" if rle else "raw" - ) + with Image.open(tga_path) as original_im: + assert original_im.format == "TGA" + assert original_im.get_format_mimetype() == "image/x-tga" + if rle: + assert original_im.info["compression"] == "tga_rle" + assert original_im.info["orientation"] == _ORIGIN_TO_ORIENTATION[origin] + if mode == "P": + assert original_im.getpalette() == reference_im.getpalette() - 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") @@ -121,7 +126,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 +145,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 +159,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 +176,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 +190,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 +208,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 +220,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 +239,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) @@ -261,7 +276,7 @@ def test_save_l_transparency(tmp_path: Path) -> None: assert im.mode == "LA" assert im.getchannel("A").getcolors()[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: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index af766022b..046a9f1f1 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -9,7 +9,14 @@ from types import ModuleType import pytest -from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError +from PIL import ( + Image, + ImageFile, + JpegImagePlugin, + TiffImagePlugin, + TiffTags, + UnidentifiedImageError, +) from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION from .helper import ( @@ -20,6 +27,7 @@ from .helper import ( hopper, is_pypy, is_win32, + timeout_unless_slower_valgrind, ) ElementTree: ModuleType | None @@ -31,7 +39,7 @@ except ImportError: 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,37 +49,24 @@ class TestFileTiff: 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() @@ -88,6 +83,8 @@ class TestFileTiff: def test_context_manager(self) -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open("Tests/images/multipage.tiff") as im: im.load() @@ -108,22 +105,39 @@ class TestFileTiff: assert_image_equal_tofile(im, "Tests/images/hopper.tif") with Image.open("Tests/images/hopper_bigtiff.tif") as im: - 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) @@ -137,6 +151,8 @@ class TestFileTiff: 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) @@ -151,6 +167,8 @@ class TestFileTiff: 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 @@ -165,11 +183,12 @@ class TestFileTiff: 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) @@ -182,6 +201,7 @@ class TestFileTiff: 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 @@ -197,19 +217,21 @@ class TestFileTiff: 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) @@ -291,11 +313,13 @@ class TestFileTiff: ) 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 @@ -339,19 +363,24 @@ class TestFileTiff: 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) @@ -362,6 +391,8 @@ class TestFileTiff: # Arrange filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # v2 interface v2_tags = { 256: 55, @@ -401,6 +432,7 @@ class TestFileTiff: 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)) @@ -433,6 +465,7 @@ class TestFileTiff: 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: @@ -469,14 +502,14 @@ class TestFileTiff: 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) @@ -488,7 +521,7 @@ class TestFileTiff: 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 @@ -517,10 +550,11 @@ class TestFileTiff: @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) @@ -596,9 +630,11 @@ class TestFileTiff: 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") + 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 @@ -614,14 +650,14 @@ class TestFileTiff: 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") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/hopper.tif") as im: im.load() assert isinstance(im, TiffImagePlugin.TiffImageFile) @@ -636,7 +672,7 @@ class TestFileTiff: 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}) @@ -644,6 +680,18 @@ class TestFileTiff: assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2[278] == 256 + im = hopper() + im2 = Image.new("L", (128, 128)) + im2.encoderinfo = {"tiffinfo": {278: 256}} + im.save(outfile, save_all=True, append_images=[im2]) + + with Image.open(outfile) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + assert im.tag_v2[278] == 128 + + im.seek(1) + assert im.tag_v2[278] == 256 + def test_strip_raw(self) -> None: infile = "Tests/images/tiff_strip_raw.tif" with Image.open(infile) as im: @@ -673,9 +721,10 @@ class TestFileTiff: 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: @@ -690,7 +739,7 @@ class TestFileTiff: @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) @@ -705,6 +754,7 @@ class TestFileTiff: 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 @@ -715,6 +765,7 @@ class TestFileTiff: 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 @@ -726,6 +777,7 @@ class TestFileTiff: 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: @@ -742,6 +794,39 @@ class TestFileTiff: 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 @@ -751,7 +836,7 @@ class TestFileTiff: 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"] @@ -760,7 +845,7 @@ class TestFileTiff: 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) @@ -771,11 +856,11 @@ class TestFileTiff: 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 @@ -801,8 +886,32 @@ class TestFileTiff: 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, @@ -828,7 +937,7 @@ class TestFileTiff: ] 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: @@ -850,7 +959,7 @@ class TestFileTiff: 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) @@ -862,7 +971,7 @@ class TestFileTiff: 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) @@ -889,13 +998,12 @@ class TestFileTiff: 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", @@ -903,10 +1011,10 @@ class TestFileTiff: "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 @@ -914,7 +1022,7 @@ class TestFileTiff: @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: diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 36aabf4f8..36ad8cee9 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,8 +168,9 @@ 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] @@ -178,6 +184,7 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG @@ -210,7 +217,7 @@ def test_no_duplicate_50741_tag() -> None: 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) @@ -227,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 @@ -244,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" @@ -263,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" @@ -290,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) @@ -311,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) @@ -332,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 @@ -351,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 @@ -363,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 @@ -381,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 @@ -393,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 @@ -406,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 @@ -420,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 @@ -444,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) @@ -461,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 @@ -480,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 b34975e83..b15d79d61 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -21,7 +21,11 @@ def test_open() -> None: 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 79f6bb4e0..5456adf59 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -28,19 +28,16 @@ except ImportError: 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: @@ -157,9 +154,8 @@ class TestFileWebp: @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_message(self, tmp_path: Path) -> None: im = Image.new("RGB", (15000, 15000)) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="encoding error 6"): im.save(tmp_path / "temp.webp", method=0) - assert str(e.value) == "encoding error 6" @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None: @@ -191,6 +187,8 @@ class TestFileWebp: file_path = "Tests/images/hopper.webp" with Image.open(file_path) as image: with warnings.catch_warnings(): + warnings.simplefilter("error") + image.save(tmp_path / "temp.webp") def test_file_pointer_could_be_reused(self) -> None: @@ -221,6 +219,7 @@ class TestFileWebp: # 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 im.save(out_webp, save_all=True) @@ -232,7 +231,8 @@ class TestFileWebp: 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 def test_duration(self, tmp_path: Path) -> None: diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index c88fe3589..c573390c4 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -42,7 +42,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") @@ -71,7 +71,7 @@ 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) @@ -104,7 +104,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: @@ -123,7 +123,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) @@ -142,10 +142,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 967a0aae8..503761374 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -6,7 +6,7 @@ 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, features from .helper import ( assert_image_equal, @@ -22,10 +22,12 @@ 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 @@ -37,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 @@ -67,8 +71,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 @@ -87,7 +92,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None: 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 ) @@ -99,7 +104,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None: ) -> 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 +121,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 +132,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 +147,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 +165,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 +176,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 +203,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 80429715e..5eaa4f599 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -13,7 +13,7 @@ RGB_MODE = "RGB" def test_write_lossless_rgb(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" hopper(RGB_MODE).save(temp_file, lossless=True) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index d9a834c75..7543d22da 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -6,7 +6,7 @@ from types import ModuleType import pytest -from PIL import Image +from PIL import Image, WebPImagePlugin from .helper import mark_if_feature_version, skip_unless_feature @@ -40,7 +40,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)" @@ -110,6 +110,7 @@ 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() @@ -146,7 +147,7 @@ def test_write_animated_metadata(tmp_path: Path) -> None: 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 79e707263..dcf5f000f 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 @@ import pytest 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: @@ -31,7 +32,25 @@ 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_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 +68,7 @@ def test_register_handler(tmp_path: Path) -> 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 +80,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 +98,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 44dd2541f..154f3dcc0 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 26afe93f4..86d86602f 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 136070f9e..b4155c879 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 @@ filename = "Tests/images/courB08.bdf" 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_fontfile.py b/Tests/test_fontfile.py index 206499a04..575dada86 100644 --- a/Tests/test_fontfile.py +++ b/Tests/test_fontfile.py @@ -4,7 +4,20 @@ from pathlib import Path import pytest -from PIL import FontFile +from PIL import FontFile, Image + + +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: diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index c07024a2c..9cbf18566 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -22,28 +22,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)) + r.paste(im, (0, 0)) + r.paste(im90, (px, 0)) - g.paste(w90, (0, 0)) - g.paste(w, (2 * px, 0)) + g.paste(im90, (0, 0)) + g.paste(im, (2 * px, 0)) - b.paste(w, (px, 0)) - b.paste(w90, (2 * px, 0)) + b.paste(im, (px, 0)) + b.paste(im90, (2 * px, 0)) - img = Image.merge("RGB", (r, g, b)) - - return img + return Image.merge("RGB", (r, g, b)) def to_xxx_colorsys( @@ -79,8 +77,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_image.py b/Tests/test_image.py index 9b65041f4..069083b19 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -34,6 +34,7 @@ from .helper import ( is_win32, mark_if_feature_version, skip_unless_feature, + timeout_unless_slower_valgrind, ) ElementTree: ModuleType | None @@ -52,7 +53,7 @@ except ImportError: # Deprecation helper def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="BGR;"): return Image.new(mode, size) else: return Image.new(mode, size) @@ -65,21 +66,20 @@ class TestImage: @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: + 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" @@ -188,16 +195,13 @@ class TestImage: 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 @@ -227,10 +231,10 @@ class TestImage: 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)) @@ -248,13 +252,22 @@ class TestImage: 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")) @@ -560,10 +573,7 @@ class TestImage: 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) @@ -580,9 +590,7 @@ class TestImage: 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 is not None - assert px[0, 0] == 5 + assert im.getpixel((0, 0)) == 5 def test_linear_gradient_wrong_mode(self) -> None: # Arrange @@ -662,12 +670,14 @@ class TestImage: im.putpalette(list(range(256)) * 4, "RGBA") im_remapped = im.remap_palette(list(range(256))) assert_image_equal(im, im_remapped) + assert im.palette is not None + assert im_remapped.palette is not None assert im.palette.palette == im_remapped.palette.palette # Test illegal image mode with hopper() as im: with pytest.raises(ValueError): - im.remap_palette(None) + im.remap_palette([]) def test_remap_palette_transparency(self) -> None: im = Image.new("P", (1, 2), (0, 0, 0)) @@ -732,15 +742,17 @@ class TestImage: # 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): @@ -768,7 +780,7 @@ class TestImage: 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 @@ -791,6 +803,10 @@ class TestImage: 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" ) @@ -803,7 +819,7 @@ class TestImage: 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] @@ -825,7 +841,7 @@ class TestImage: 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 @@ -844,7 +860,7 @@ class TestImage: 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" @@ -866,7 +882,7 @@ class TestImage: 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 @@ -958,6 +974,11 @@ class TestImage: 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: if ElementTree is None: @@ -974,7 +995,7 @@ class TestImage: 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( @@ -985,6 +1006,11 @@ class TestImage: 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() == [] + @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) @@ -1113,7 +1139,7 @@ class TestImage: assert im.fp is None def test_deprecation(self) -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="Image.isImageType"): assert not Image.isImageType(None) @@ -1124,7 +1150,7 @@ class TestImageBytes: source_bytes = im.tobytes() if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match=mode): reloaded = Image.frombytes(mode, im.size, source_bytes) else: reloaded = Image.frombytes(mode, im.size, source_bytes) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index bb30b462d..66412a035 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -193,7 +193,7 @@ class TestImageGetPixel: @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) def test_deprecated(self, mode: str) -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="BGR;"): self.check(mode) def test_list(self) -> None: @@ -271,13 +271,25 @@ class TestImagePutPixelError: 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( @@ -305,13 +317,6 @@ int main(int argc, char* argv[]) """ ) - 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") diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index eb2309e0f..ecbce3d6f 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 @@ numpy = pytest.importorskip("numpy", reason="NumPy not installed") im = hopper().resize((128, 100)) +TYPE_CHECKING = False if TYPE_CHECKING: import numpy.typing as npt @@ -47,7 +48,7 @@ def test_toarray() -> None: with pytest.raises(OSError): numpy.array(im_truncated) else: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="__array_interface__"): numpy.array(im_truncated) @@ -101,7 +102,8 @@ def test_fromarray_strides_without_tobytes() -> None: with pytest.raises(ValueError): wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)}) - Image.fromarray(wrapped, "L") + with pytest.warns(DeprecationWarning, match="'mode' parameter"): + Image.fromarray(wrapped, "L") def test_fromarray_palette() -> None: @@ -110,7 +112,8 @@ def test_fromarray_palette() -> None: a = numpy.array(i) # Act - out = Image.fromarray(a, "P") + with pytest.warns(DeprecationWarning, match="'mode' parameter"): + out = Image.fromarray(a, "P") # Assert that the Python and C palettes match assert out.palette is not None diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 6a925975e..33f844437 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -118,7 +118,7 @@ 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 +154,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 +177,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 +203,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) @@ -222,9 +225,7 @@ def test_l_macro_rounding(convert_mode: str) -> None: im.palette.getcolor((0, 1, 2)) converted_im = im.convert(convert_mode) - px = converted_im.load() - assert px is not None - 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] @@ -236,6 +237,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_getim.py b/Tests/test_image_getim.py index fa58492fc..7b5f7a589 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -11,9 +11,9 @@ def test_sanity() -> None: type_repr = repr(type(im.getim())) assert "PyCapsule" in type_repr - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="id property"): assert isinstance(im.im.id, int) - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="unsafe_ptrs property"): ptrs = dict(im.im.unsafe_ptrs) assert ptrs.keys() == {"image8", "image32", "image"} diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 27cb7c59d..34c1763b8 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -81,7 +81,7 @@ def test_mode_F() -> None: @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): + with pytest.warns(DeprecationWarning, match=mode): im = Image.new(mode, (1, 2)) im.putdata(data) diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 7c564d967..6d313cb8c 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -70,6 +70,7 @@ def test_quantize_no_dither() -> None: converted = image.quantize(dither=Image.Dither.NONE, palette=palette) assert converted.mode == "P" assert converted.palette is not None + assert palette.palette is not None assert converted.palette.palette == palette.palette.palette @@ -148,10 +149,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 is not None assert converted.palette is not None - assert converted_px[0, 0] == converted.palette.colors[color] + assert converted.getpixel((0, 0)) == converted.palette.colors[color] def test_small_palette() -> None: diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index ce6209c0d..73b25ed51 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -462,7 +462,7 @@ class TestCoreResampleBox: 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 8548fb5da..f700d20c0 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -10,7 +10,7 @@ from pathlib import Path import pytest -from PIL import Image +from PIL import Image, ImageFile from .helper import ( assert_image_equal, @@ -171,7 +171,7 @@ class TestImagingCoreResize: # 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: @@ -179,7 +179,7 @@ class TestImagingCoreResize: @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: @@ -189,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 ) @@ -210,7 +210,7 @@ class TestReducingGapResize: ) def test_reducing_gap_1( self, - gradients_image: Image.Image, + gradients_image: ImageFile.ImageFile, box: tuple[float, float, float, float], epsilon: float, ) -> None: @@ -230,7 +230,7 @@ class TestReducingGapResize: ) def test_reducing_gap_2( self, - gradients_image: Image.Image, + gradients_image: ImageFile.ImageFile, box: tuple[float, float, float, float], epsilon: float, ) -> None: @@ -250,7 +250,7 @@ class TestReducingGapResize: ) def test_reducing_gap_3( self, - gradients_image: Image.Image, + gradients_image: ImageFile.ImageFile, box: tuple[float, float, float, float], epsilon: float, ) -> None: @@ -266,7 +266,9 @@ class TestReducingGapResize: @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( @@ -281,7 +283,7 @@ class TestReducingGapResize: ) def test_box_filter( self, - gradients_image: Image.Image, + gradients_image: ImageFile.ImageFile, box: tuple[float, float, float, float], epsilon: float, ) -> None: @@ -307,7 +309,7 @@ class TestImageResize: # 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_transposed(self) -> None: diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index 3385f81f5..43068535e 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 01bd4b1d7..1181f6fca 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -104,20 +104,20 @@ def test_transposed() -> 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)) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 7e83396de..0429eb99d 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -47,6 +47,8 @@ class TestImageTransform: 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: diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index f062651f0..b6db0ab5c 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -54,7 +54,7 @@ def skip_missing() -> None: def test_sanity() -> None: # basic smoke test. # this mostly follows the cms_test outline. - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="PIL.ImageCms.versions"): v = ImageCms.versions() # should return four strings assert v[0] == "1.0.0 pil" assert list(map(type, v)) == [str, str, str, str] @@ -679,7 +679,7 @@ def test_auxiliary_channels_isolated() -> None: def test_long_modes() -> None: p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc") - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ABCDEFGHI"): ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI") @@ -703,15 +703,15 @@ def test_cmyk_lab() -> None: def test_deprecation() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageCms.DESCRIPTION"): assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageCms.VERSION"): assert ImageCms.VERSION == "1.0.0 pil" - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageCms.FLAGS"): assert isinstance(ImageCms.FLAGS, dict) profile = ImageCmsProfile(ImageCms.createProfile("sRGB")) - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="RGBA;16B"): ImageCms.ImageCmsTransform(profile, profile, "RGBA;16B", "RGB") - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="RGBA;16B"): ImageCms.ImageCmsTransform(profile, profile, "RGB", "RGBA;16B") diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index ed50dfbed..881f9c85d 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -39,6 +39,8 @@ BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X 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 +48,8 @@ POINTS = ( 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]], ) @@ -448,7 +452,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 +473,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 +491,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 +783,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 +814,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)) @@ -1047,8 +1049,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) @@ -1396,6 +1398,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 @@ -1504,7 +1528,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) @@ -1608,7 +1631,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, @@ -1622,7 +1645,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, @@ -1647,9 +1670,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: @@ -1674,13 +1696,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: @@ -1710,5 +1735,5 @@ def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None: def test_getdraw() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="'hints' parameter"): ImageDraw.getdraw(None, []) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 1ee684926..a9444c26d 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -93,6 +93,19 @@ class TestImageFile: 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") def test_incremental_webp(self) -> None: with ImageFile.Parser() as p: @@ -118,8 +131,28 @@ class TestImageFile: assert_image_equal(im1, im2) + 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_oserror(self) -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="raise_oserror"): with pytest.raises(OSError): ImageFile.raise_oserror(1) @@ -163,9 +196,8 @@ class TestImageFile: 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: @@ -178,13 +210,10 @@ class TestImageFile: 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: @@ -193,13 +222,12 @@ class TestImageFile: 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): diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 6a0a940b9..9334d30e4 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -124,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: @@ -254,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 @@ -266,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) @@ -461,6 +479,20 @@ 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_when_image_not_found() -> None: with tempfile.NamedTemporaryFile(delete=False) as tmp: pass @@ -543,7 +575,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( @@ -1012,7 +1044,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 @@ -1029,7 +1061,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 @@ -1160,15 +1192,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]) @@ -1187,5 +1219,5 @@ def test_freetype_deprecation(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(features, "version_module", fake_version_module) # Act / Assert - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="FreeType 2.9.0"): ImageFont.truetype(FONT_PATH, FONT_SIZE) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 24c7b871a..c85eb499c 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -229,7 +229,7 @@ def test_getlength( @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: @@ -272,27 +272,27 @@ def test_anchor_ttb(anchor: str) -> None: 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), @@ -346,7 +346,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) diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 695aecbde..3eb98d379 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -7,7 +7,7 @@ import pytest 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"): @@ -72,7 +72,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 5cd510751..01fa090dc 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -40,8 +40,11 @@ class TestImageGrab: @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,6 +60,13 @@ class TestImageGrab: ImageGrab.grab(xdisplay="error.test:0.0") assert str(e.value).startswith("X connection failed") + @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") + def test_grab_invalid_handle(self) -> None: + 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"]) diff --git a/Tests/test_imagemath_lambda_eval.py b/Tests/test_imagemath_lambda_eval.py index 360325780..eec76118a 100644 --- a/Tests/test_imagemath_lambda_eval.py +++ b/Tests/test_imagemath_lambda_eval.py @@ -56,7 +56,7 @@ def test_sanity() -> None: def test_options_deprecated() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageMath.lambda_eval options"): assert ImageMath.lambda_eval(lambda args: 1, images) == 1 diff --git a/Tests/test_imagemath_unsafe_eval.py b/Tests/test_imagemath_unsafe_eval.py index b7ac84691..60ad6aafa 100644 --- a/Tests/test_imagemath_unsafe_eval.py +++ b/Tests/test_imagemath_unsafe_eval.py @@ -36,12 +36,12 @@ def test_sanity() -> None: def test_eval_deprecated() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageMath.eval"): assert ImageMath.eval("1") == 1 def test_options_deprecated() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageMath.unsafe_eval options"): assert ImageMath.unsafe_eval("1", images) == 1 diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 6180a7b5d..515e29cea 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -80,15 +80,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 @@ -238,15 +235,12 @@ def test_incorrect_mode() -> None: im = hopper("RGB") mop = ImageMorph.MorphOp(op_name="erosion8") - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="Image mode must be L"): mop.apply(im) - assert str(e.value) == "Image mode must be L" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="Image mode must be L"): mop.match(im) - assert str(e.value) == "Image mode must be L" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="Image mode must be L"): mop.get_on_pixels(im) - assert str(e.value) == "Image mode must be L" def test_add_patterns() -> None: @@ -279,9 +273,10 @@ def test_pattern_syntax_error() -> None: lb.add_patterns(new_patterns) # Act / Assert - with pytest.raises(Exception) as e: + with pytest.raises( + Exception, match='Syntax error in pattern "a pattern with a syntax error"' + ): lb.build_lut() - assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"' def test_load_invalid_mrl() -> None: @@ -290,9 +285,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: diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 2fb2a60b6..9f2fd5ba2 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -165,14 +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)) - px = new_im.load() - assert px is not None - assert px[2, 0] == 1 + assert new_im.getpixel((2, 0)) == 1 new_im = ImageOps.pad(im, (1, 4)) - px = new_im.load() - assert px is not None - assert px[0, 2] == 1 + assert new_im.getpixel((0, 2)) == 1 @pytest.mark.parametrize("mode", ("P", "PA")) @@ -405,7 +401,6 @@ def test_exif_transpose() -> 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 @@ -417,7 +412,6 @@ def test_exif_transpose() -> 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) @@ -433,7 +427,6 @@ def test_exif_transpose() -> 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() @@ -446,17 +439,24 @@ def test_exif_transpose() -> 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 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 @@ -464,7 +464,6 @@ def test_exif_transpose_xml_without_xmp() -> None: del im.info["xmp"] transposed_im = ImageOps.exif_transpose(im) - assert transposed_im is not None assert 0x0112 not in transposed_im.getexif() diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 6cf0079dd..782022f51 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -17,6 +17,7 @@ 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")) @@ -111,7 +112,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,7 +190,7 @@ 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) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 76bdf1e5f..ad8acde49 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -68,25 +68,10 @@ def test_path_constructors( assert list(p) == [(0.0, 1.0)] -@pytest.mark.parametrize( - "coords", - ( - ("a", "b"), - ([0, 1],), - [[0, 1]], - ([0.0, 1.0],), - [[0.0, 1.0]], - ), -) -def test_invalid_path_constructors( - coords: tuple[str, str] | Sequence[Sequence[int]] -) -> None: - # Act - with pytest.raises(ValueError) as e: - 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 +84,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", diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 22cd674ce..2d7ca0ae0 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 9b37435eb..7b9ac80bc 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -4,13 +4,13 @@ from pathlib import Path 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) diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index c23a5c690..e8468e59f 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -88,7 +88,7 @@ if is_win32(): def test_pointer(tmp_path: Path) -> None: im = hopper() (width, height) = im.size - opath = str(tmp_path / "temp.png") + 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 a66bf9708..5762e69c1 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -820,7 +820,7 @@ def test_unpack( def test_unpack_deprecated( mode: str, rawmode: str, data: int | bytes, pixels: list[float | tuple[int, ...]] ) -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match=mode): test_unpack(mode, rawmode, data, pixels) diff --git a/Tests/test_map.py b/Tests/test_map.py index 93140f6e5..1278ba3a6 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -7,36 +7,30 @@ import pytest 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 e26f5d283..b78b7984f 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -44,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_numpy.py b/Tests/test_numpy.py index 040472d69..ef54deeeb 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 PIL import Image, _typing 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 @@ -141,9 +141,7 @@ def test_save_tiff_uint16() -> None: a.shape = TEST_IMAGE_SIZE img = Image.fromarray(a) - img_px = img.load() - assert img_px is not None - assert img_px[0, 0] == pixel_value + assert img.getpixel((0, 0)) == pixel_value @pytest.mark.parametrize( @@ -264,4 +262,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 f6b12cb20..d85fb1212 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -20,10 +20,10 @@ from PIL.PdfParser import ( 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,7 +118,7 @@ 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: diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index d250ba369..54cef00ad 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -18,7 +18,7 @@ def helper_pickle_file( ) -> None: # Arrange with Image.open(test_file) as im: - filename = str(tmp_path / "temp.pkl") + filename = tmp_path / "temp.pkl" if mode: im = im.convert(mode) @@ -65,7 +65,7 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non ("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,14 +74,26 @@ 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") # Act / Assert - for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): im._mode = "LA" with open(filename, "wb") as f: pickle.dump(im, f, protocol) @@ -122,7 +134,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 +148,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 +162,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 a743d831f..78f5632c5 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -35,7 +35,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) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py new file mode 100644 index 000000000..8dad94fe0 --- /dev/null +++ b/Tests/test_pyarrow.py @@ -0,0 +1,246 @@ +from __future__ import annotations + +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 reloaded + + 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) diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index c2f7fe22e..8235daf32 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -23,5 +23,11 @@ def test_pyroma() -> None: ) else: - # Should have a perfect score - assert rating == (10, []) + # Should have a perfect score, but pyroma does not support PEP 639 yet. + assert rating == ( + 9, + [ + "Your package does neither have a license field " + "nor any license classifiers." + ], + ) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 0ed9fbfa5..82a3e0741 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,7 +1,7 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING, Union +from typing import Union import pytest @@ -9,6 +9,7 @@ from PIL import Image, ImageQt from .helper import assert_image_equal_tofile, assert_image_similar, hopper +TYPE_CHECKING = False if TYPE_CHECKING: import PyQt6 import PySide6 diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index dd4fc46c3..03e92b5b9 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -35,10 +35,11 @@ class TestShellInjection: @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") diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py index 073e5415c..976f62384 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 13f1f9c80..42d06b896 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -65,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/codecov.yml b/codecov.yml index 8646576bb..84920238f 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 diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh new file mode 100755 index 000000000..f0d1d851d --- /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/install_imagequant.sh b/depends/install_imagequant.sh index 8d62d5ac7..88756f8f9 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -2,7 +2,7 @@ # install libimagequant archive_name=libimagequant -archive_version=4.3.3 +archive_version=4.3.4 archive=$archive_name-$archive_version diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh new file mode 100755 index 000000000..26af8a36c --- /dev/null +++ b/depends/install_libavif.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -eo pipefail + +version=1.3.0 + +./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 + +popd diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index 8c2967bc2..1f8d78193 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.3 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/depends/install_webp.sh b/depends/install_webp.sh index c47fb35f1..9d2977715 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.4.0 +archive=libwebp-1.5.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/COPYING b/docs/COPYING index d5ee19f81..17fba5b87 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 A. 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 16a891a73..000000000 --- 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 e90af0519..8c1019294 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,43 +3,34 @@ # 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: @@ -51,159 +42,28 @@ install-sphinx: .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 bdbf1373d..5225e9644 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 98cdd8e5a..7df895b8f 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 b81d86c6c..040301433 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ import PIL # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = "8.1" +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,7 +55,7 @@ master_doc = "index" project = "Pillow (PIL Fork)" copyright = ( "1995-2011 Fredrik Lundh and contributors, " - "2010-2024 Jeffrey A. Clark and contributors." + "2010 Jeffrey A. Clark and contributors." ) author = "Fredrik Lundh (PIL), Jeffrey A. Clark (Pillow)" @@ -121,7 +121,7 @@ nitpicky = True # 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 = [("py:class", "_io.BytesIO"), ("py:class", "_CmsProfileCompatible")] +nitpick_ignore = [("py:class", "_CmsProfileCompatible")] # -- Options for HTML output ---------------------------------------------- @@ -229,97 +229,6 @@ html_js_files = [ # 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/.*", diff --git a/docs/dater.py b/docs/dater.py index f9fb0c1da..c0302b55c 100644 --- a/docs/dater.py +++ b/docs/dater.py @@ -8,8 +8,8 @@ from __future__ import annotations import re import subprocess -from typing import TYPE_CHECKING +TYPE_CHECKING = False if TYPE_CHECKING: from sphinx.application import Sphinx diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 25607e27c..def98b80a 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -155,7 +155,7 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc 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 +Specific WebP feature checks ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 11.0.0 @@ -175,6 +175,46 @@ deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obt raw pointers to ``ImagingCore`` internals. To interact with C code, you can use ``Image.Image.getim()``, which returns a ``Capsule`` object. +ExifTags.IFD.Makernote +^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.1.0 + +``ExifTags.IFD.Makernote`` has been deprecated. Instead, use +``ExifTags.IFD.MakerNote``. + +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. + +Image.fromarray mode parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.3.0 + +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. + +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") + Removed features ---------------- diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 10828bff0..e4b6b9c01 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -285,7 +285,7 @@ Image.register_decoder("DXT5", DXT5Decoder) 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/docs/handbook/appendices.rst b/docs/handbook/appendices.rst index 347a8848b..c20d8bc8b 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 7da1078c1..c9d3f5e91 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) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 8183473e4..83df2bd5c 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. @@ -454,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 @@ -572,10 +657,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 @@ -692,6 +786,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 ^^^ @@ -894,7 +1012,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. @@ -963,6 +1082,26 @@ Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well. +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 ^^^ @@ -1103,7 +1242,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 @@ -1123,15 +1262,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 @@ -1175,6 +1313,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 @@ -1270,8 +1413,8 @@ Saving sequences 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 @@ -1435,30 +1578,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 ^^^ @@ -1479,15 +1598,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 ^^^ @@ -1551,7 +1661,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: @@ -1565,6 +1676,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 ------------------ @@ -1597,15 +1713,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 @@ -1671,11 +1786,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/overview.rst b/docs/handbook/overview.rst index 17964d1c5..ab22b9807 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/third-party-plugins.rst b/docs/handbook/third-party-plugins.rst new file mode 100644 index 000000000..a189a5773 --- /dev/null +++ b/docs/handbook/third-party-plugins.rst @@ -0,0 +1,18 @@ +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:`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-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 implemetation. 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-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/tutorial.rst b/docs/handbook/tutorial.rst index 3df8e0d20..28c0abe44 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -122,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 ^^^^^^^^^^^^^^^^^^^^ :: @@ -399,7 +399,7 @@ Applying filters .. image:: enhanced_hopper.webp :align: center -Point Operations +Point operations ^^^^^^^^^^^^^^^^ The :py:meth:`~PIL.Image.Image.point` method can be used to translate the pixel @@ -534,7 +534,6 @@ You can create animated GIFs with Pillow, e.g. # Save the images as an animated GIF images[0].save( "animated_hopper.gif", - save_all=True, append_images=images[1:], duration=500, # duration of each frame in milliseconds loop=0, # loop forever @@ -678,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)) diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 2e853224d..21a9124d7 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 @@ -54,7 +54,7 @@ true color. def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"SPAM" + return prefix.startswith(b"SPAM") class SpamImageFile(ImageFile.ImageFile): @@ -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 18f5c3d13..689088d48 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -33,10 +33,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 @@ -178,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:: @@ -193,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 @@ -210,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. @@ -259,7 +271,7 @@ 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 @@ -272,14 +284,16 @@ Build Options * Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, ``-C lcms=disable``, ``-C webp=disable``, - ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=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 jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=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. Tcl and Tk must be used together. @@ -305,7 +319,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/platform-support.rst b/docs/installation/platform-support.rst index 00dec41d1..a56f94316 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. @@ -23,45 +23,45 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Amazon Linux 2023 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Arch | 3.12 | x86-64 | +| Arch | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ | CentOS Stream 9 | 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 | +| Fedora 41 | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Fedora 40 | 3.12 | x86-64 | +| Fedora 42 | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 13 Ventura | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 | -| | PyPy3 | | +| | 3.14, PyPy3 | | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.9, 3.10, 3.11, | x86-64 | -| | 3.12, 3.13, PyPy3 | | +| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 24.04 LTS (Noble) | 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, 3.13, 3.14, PyPy3 | | | +----------------------------+---------------------+ -| | 3.10 | arm64v8 | -+----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, | +| | 3.12 | arm64v8, ppc64le, | | | | s390x | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2019 | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.9, 3.10, 3.11, | x86-64 | -| | 3.12, 3.13, PyPy3 | | +| Windows Server 2022 | 3.9 | x86 | | +----------------------------+---------------------+ -| | 3.13 | x86 | +| | 3.10, 3.11, 3.12, 3.13, | x86-64 | +| | 3.14, PyPy3 | | | +----------------------------+---------------------+ -| | 3.9 (MinGW) | x86-64 | +| | 3.12 (MinGW) | x86-64 | | +----------------------------+---------------------+ | | 3.9 (Cygwin) | x86-64 | +----------------------------------+----------------------------+---------------------+ -Other Platforms +Other platforms --------------- These platforms have been reported to work at the versions mentioned. @@ -75,7 +75,9 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+============================+==================+==============+ -| macOS 15 Sequoia | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm | +| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.2.1 |arm | +| +----------------------------+------------------+ | +| | 3.8 | 10.4.0 | | +----------------------------------+----------------------------+------------------+--------------+ | macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm | +----------------------------------+----------------------------+------------------+--------------+ @@ -148,7 +150,7 @@ These platforms have been reported to work at the versions mentioned. +----------------------------------+----------------------------+------------------+--------------+ | 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 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 | +----------------------------------+----------------------------+------------------+--------------+ diff --git a/docs/installation/python-support.rst b/docs/installation/python-support.rst index dd5765b6b..7daee8afc 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 0ed5ee1a5..9d15537fb 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 06965ead3..e6bcd9d59 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 bc3758218..e68722900 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 @@ -260,7 +261,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: @@ -370,6 +371,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 9519361a7..505181db6 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 96bd14dd3..238390e75 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 @@ -286,6 +286,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 31faeac78..68e228dba 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 3e9aa73f8..6e73233a1 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 @@ -54,7 +54,7 @@ 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. @@ -75,7 +75,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 +102,7 @@ Example: Draw Partial Opacity Text out.show() -Example: Draw Multiline Text +Example: Draw multiline text ---------------------------- :: @@ -387,8 +387,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 +458,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. @@ -599,8 +605,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. @@ -650,8 +659,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 529acad4a..334d1d4b2 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 fdfeb60f9..043559352 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() diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index 5f2b6af7c..1c201cacc 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 d9d9cac6e..aac55fe6b 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -1,7 +1,7 @@ .. 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 @@ -18,6 +18,9 @@ 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 diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index db2987eb0..f6a2ec5bc 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,15 +9,16 @@ 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) @@ -39,6 +40,11 @@ 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: + HWND, to capture a single window. Windows only. + + .. versionadded:: 11.2.1 :return: An image .. py:function:: grabclipboard() diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index f4e1081e6..0ee49b150 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 @@ -86,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 (-), @@ -102,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 @@ -116,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 @@ -128,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 d4522a06a..30b89a54d 100644 --- a/docs/reference/ImageMorph.rst +++ b/docs/reference/ImageMorph.rst @@ -1,7 +1,7 @@ .. 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. diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst index fcaa3c8f6..1ecff09f0 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 72ccfac7d..42ce5cb13 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 23544b613..5f5606349 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 7e67a44d3..88d7b8a20 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 a27b2fb4e..0d6f394dd 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 5cedede69..12c8741ce 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 f69466382..ede119920 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/ImageTk.rst b/docs/reference/ImageTk.rst index 134ef5651..3ab72b83d 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 5b0a5ce49..530279934 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 4151be4a7..c0b9bd2ba 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 aafae44cf..b0a3ba8b5 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 3e8512e7a..9eed775fc 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 1ac3d034b..9d7cf83b6 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 diff --git a/docs/reference/TiffTags.rst b/docs/reference/TiffTags.rst index 7cb7d16ae..d75a48478 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 000000000..063046d8c --- /dev/null +++ b/docs/reference/arrow_support.rst @@ -0,0 +1,88 @@ +.. _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 +**except** for ``BGR;15``, ``BGR;16`` and ``BGR;24``. This is due to +line-length packing in these modes making for non-continuous memory. + +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 1abe5280f..5ad9d9fd1 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 5e8586905..12dca6cf2 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 fcff96735..381d7830a 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,6 +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. +* ``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/internal_design.rst b/docs/reference/internal_design.rst index 99a18e9ea..6bba673b9 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 93fd82cf9..19f78864d 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 @@ -58,7 +58,7 @@ on some Python versions. See :py:obj:`typing.TypeGuard`. -:mod:`~PIL._util` Module +:mod:`~PIL._util` module ------------------------ .. automodule:: PIL._util @@ -66,7 +66,7 @@ on some Python versions. :undoc-members: :show-inheritance: -:mod:`~PIL._version` Module +:mod:`~PIL._version` module --------------------------- .. module:: PIL._version @@ -78,7 +78,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 diff --git a/docs/reference/limits.rst b/docs/reference/limits.rst index a71b514b5..d2f8f7d1f 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 730c8da5b..0d43cbc73 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 454b94d8c..243d4f353 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,15 +193,15 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.MpoImagePlugin` Module ----------------------------------- +:mod:`~PIL.MpoImagePlugin` module +--------------------------------- .. automodule:: PIL.MpoImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`~PIL.MspImagePlugin` Module +:mod:`~PIL.MspImagePlugin` module --------------------------------- .. automodule:: PIL.MspImagePlugin @@ -201,7 +209,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PalmImagePlugin` Module +:mod:`~PIL.PalmImagePlugin` module ---------------------------------- .. automodule:: PIL.PalmImagePlugin @@ -209,7 +217,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PcdImagePlugin` Module +:mod:`~PIL.PcdImagePlugin` module --------------------------------- .. automodule:: PIL.PcdImagePlugin @@ -217,7 +225,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PcxImagePlugin` Module +:mod:`~PIL.PcxImagePlugin` module --------------------------------- .. automodule:: PIL.PcxImagePlugin @@ -225,7 +233,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PdfImagePlugin` Module +:mod:`~PIL.PdfImagePlugin` module --------------------------------- .. automodule:: PIL.PdfImagePlugin @@ -233,7 +241,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PixarImagePlugin` Module +:mod:`~PIL.PixarImagePlugin` module ----------------------------------- .. automodule:: PIL.PixarImagePlugin @@ -241,7 +249,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PngImagePlugin` Module +:mod:`~PIL.PngImagePlugin` module --------------------------------- .. automodule:: PIL.PngImagePlugin @@ -252,7 +260,7 @@ Plugin reference :member-order: groupwise -:mod:`~PIL.PpmImagePlugin` Module +:mod:`~PIL.PpmImagePlugin` module --------------------------------- .. automodule:: PIL.PpmImagePlugin @@ -260,7 +268,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PsdImagePlugin` Module +:mod:`~PIL.PsdImagePlugin` module --------------------------------- .. automodule:: PIL.PsdImagePlugin @@ -268,7 +276,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.SgiImagePlugin` Module +:mod:`~PIL.SgiImagePlugin` module --------------------------------- .. automodule:: PIL.SgiImagePlugin @@ -276,7 +284,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.SpiderImagePlugin` Module +:mod:`~PIL.SpiderImagePlugin` module ------------------------------------ .. automodule:: PIL.SpiderImagePlugin @@ -284,7 +292,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.SunImagePlugin` Module +:mod:`~PIL.SunImagePlugin` module --------------------------------- .. automodule:: PIL.SunImagePlugin @@ -292,7 +300,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.TgaImagePlugin` Module +:mod:`~PIL.TgaImagePlugin` module --------------------------------- .. automodule:: PIL.TgaImagePlugin @@ -300,7 +308,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.TiffImagePlugin` Module +:mod:`~PIL.TiffImagePlugin` module ---------------------------------- .. automodule:: PIL.TiffImagePlugin @@ -308,7 +316,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.WebPImagePlugin` Module +:mod:`~PIL.WebPImagePlugin` module ---------------------------------- .. automodule:: PIL.WebPImagePlugin @@ -316,7 +324,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.WmfImagePlugin` Module +:mod:`~PIL.WmfImagePlugin` module --------------------------------- .. automodule:: PIL.WmfImagePlugin @@ -324,7 +332,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.XVThumbImagePlugin` Module +:mod:`~PIL.XVThumbImagePlugin` module ------------------------------------- .. automodule:: PIL.XVThumbImagePlugin @@ -332,7 +340,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.XbmImagePlugin` Module +:mod:`~PIL.XbmImagePlugin` module --------------------------------- .. automodule:: PIL.XbmImagePlugin @@ -340,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 2ea973c5c..3e2aa84b1 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 @@ -164,7 +164,7 @@ Since Pillow's C API is now faster than PyAccess on PyPy, ``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 02189d514..aa17a62e0 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 fd556bdf1..996e6a3ed 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 1c6b78b08..337748785 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 2f0437d94..6c7d8ea0a 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 8d3706be6..84a6091c9 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 index c3f18140f..020fbf7df 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -1,7 +1,7 @@ 11.0.0 ------ -Backwards Incompatible Changes +Backwards incompatible changes ============================== Python 3.8 @@ -103,7 +103,7 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc 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 +Specific WebP feature checks ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 11.0.0 @@ -113,7 +113,7 @@ Specific WebP Feature Checks ``True`` if the WebP module is installed, until they are removed in Pillow 12.0.0 (2025-10-15). -API Changes +API changes =========== Default resampling filter for I;16* image modes @@ -122,7 +122,7 @@ 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 +API additions ============= Writing XMP bytes to JPEG and MPO @@ -138,7 +138,7 @@ either JPEG or MPO images:: im.info["xmp"] = b"test" im.save("out.jpg") -Other Changes +Other changes ============= Python 3.13 @@ -154,7 +154,7 @@ 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 +C-level flags ^^^^^^^^^^^^^ Some compiling flags like ``WITH_THREADING``, ``WITH_IMAGECHOPS``, and other diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst new file mode 100644 index 000000000..4888ddf56 --- /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 000000000..f55b0d7d7 --- /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 000000000..2d35d8228 --- /dev/null +++ b/docs/releasenotes/11.3.0.rst @@ -0,0 +1,79 @@ +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. + +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. libaom is available as an encoder and dav1d as a decoder. + +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/2.7.0.rst b/docs/releasenotes/2.7.0.rst index e9b0995bb..a1ddd1178 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 8bc477f70..dcd8031f5 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 951819f19..90f77ff61 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 3ed8fae57..20d7d073e 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 cd6f7e2f9..9447245c4 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 73156a65d..60ffbdcba 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 8a5a7efe3..01ec77a58 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 625f237e8..dd97463f6 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 80ad9b9fb..1f809ad18 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 8c8055bfa..1cbd3853b 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 bc2a45f02..0ea3de399 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 2061f6467..617d51e52 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 ea81fc45e..87a57799f 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 be00a45cd..2b93e0322 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 4e3d10ac5..4b80e8521 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 d9b8f0fb7..d18337820 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 8f276da24..6adce95b2 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 6d7277c70..13b540d60 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 5e69f0b6b..b788b2eeb 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 ce3edc5fa..761f435f3 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 b851c56fc..b37cd7160 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 372298fbc..0ede05917 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 ed6026593..9504c974a 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 0dd8669a5..c2aeb0f74 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 91e54da19..12bafa8ce 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 1fc245c9a..d0dde756f 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 5c3993318..06e6d9974 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 690421c2a..b8ad5a898 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 50fe9aa19..a59560695 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 4ef914f64..c46240854 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 34ba703f7..e26a6ceda 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 bdc8e8020..3bdf77d56 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 fee66b6d0..660e5514c 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 f65e3bcc2..5326afe78 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 5b83d1e9c..72749ce8c 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 6e0647343..a3c9800b6 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 e5987ce08..bb1e731fd 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 37f26a22c..3b202157d 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 501479bb6..6bf2079c8 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -37,7 +37,7 @@ 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 641cda4ef..a85f1e075 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,9 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 11.3.0 + 11.2.1 + 11.1.0 11.0.0 10.4.0 10.3.0 diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst index cfc7221a3..a453d2a43 100644 --- a/docs/releasenotes/template.rst +++ b/docs/releasenotes/template.rst @@ -14,7 +14,7 @@ TODO TODO -Backwards Incompatible Changes +Backwards incompatible changes ============================== TODO @@ -28,7 +28,7 @@ TODO TODO -API Changes +API changes =========== TODO @@ -36,7 +36,7 @@ TODO TODO -API Additions +API additions ============= TODO @@ -44,7 +44,7 @@ TODO TODO -Other Changes +Other changes ============= TODO diff --git a/pyproject.toml b/pyproject.toml index c55be7693..683ab24ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "backend" requires = [ - "setuptools>=67.8", + "setuptools>=77", ] backend-path = [ "_custom_build", @@ -14,14 +14,14 @@ readme = "README.md" keywords = [ "Imaging", ] -license = { text = "MIT-CMU" } +license = "MIT-CMU" +license-files = [ "LICENSE" ] authors = [ { name = "Jeffrey A. Clark", email = "aclark@aclark.net" }, ] requires-python = ">=3.9" classifiers = [ "Development Status :: 6 - Mature", - "License :: OSI Approved :: CMU License (MIT-CMU)", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -43,7 +43,8 @@ dynamic = [ optional-dependencies.docs = [ "furo", "olefile", - "sphinx>=8.1", + "sphinx>=8.2", + "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph", @@ -54,9 +55,13 @@ optional-dependencies.fpx = [ optional-dependencies.mic = [ "olefile", ] +optional-dependencies.test-arrow = [ + "pyarrow", +] + optional-dependencies.tests = [ "check-manifest", - "coverage", + "coverage>=7.4.2", "defusedxml", "markdown2", "olefile", @@ -65,17 +70,20 @@ optional-dependencies.tests = [ "pytest", "pytest-cov", "pytest-timeout", + "pytest-xdist", + "trove-classifiers>=2024.10.12", ] + optional-dependencies.typing = [ "typing-extensions; python_version<'3.10'", ] optional-dependencies.xmp = [ "defusedxml", ] -urls.Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst" +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.org" +urls.Homepage = "https://python-pillow.github.io" urls.Mastodon = "https://fosstodon.org/@pillow" urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" urls.Source = "https://github.com/python-pillow/Pillow" @@ -93,10 +101,17 @@ version = { attr = "PIL.__version__" } [tool.cibuildwheel] before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 + config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" +# Disable platform guessing on macOS +macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" + test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" +[tool.cibuildwheel.macos.environment] +PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" + [tool.black] exclude = "wheels/multibuild" @@ -113,6 +128,7 @@ lint.select = [ "ISC", # flake8-implicit-str-concat "LOG", # flake8-logging "PGH", # pygrep-hooks + "PIE", # flake8-pie "PT", # flake8-pytest-style "PYI", # flake8-pyi "RUF100", # unused noqa (yesqa) @@ -125,6 +141,7 @@ lint.ignore = [ "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 diff --git a/setup.py b/setup.py index 60707083f..354e09f85 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,6 @@ import subprocess import sys import warnings from collections.abc import Iterator -from typing import Any from setuptools import Extension, setup from setuptools.command.build_ext import build_ext @@ -32,6 +31,7 @@ configuration: dict[str, list[str]] = {} PILLOW_VERSION = get_version() +AVIF_ROOT = None FREETYPE_ROOT = None HARFBUZZ_ROOT = None FRIBIDI_ROOT = None @@ -45,7 +45,7 @@ WEBP_ROOT = None ZLIB_ROOT = None FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ -if sys.platform == "win32" and sys.version_info >= (3, 14): +if sys.platform == "win32" and sys.version_info >= (3, 15): import atexit atexit.register( @@ -64,10 +64,12 @@ _IMAGING = ("decode", "encode", "map", "display", "outline", "path") _LIB_IMAGING = ( "Access", "AlphaComposite", + "Arrow", "Resample", "Reduce", "Bands", "BcnDecode", + "BcnEncode", "BitDecode", "Blend", "Chops", @@ -145,7 +147,7 @@ class RequiredDependencyException(Exception): PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version -def _dbg(s: str, tp: Any = None) -> None: +def _dbg(s: str, tp: str | tuple[str, ...] | None = None) -> None: if DEBUG: if tp: print(s % tp) @@ -160,7 +162,7 @@ def _find_library_dirs_ldconfig() -> list[str]: args: list[str] env: dict[str, str] expr: str - if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): + if sys.platform.startswith(("linux", "gnu")): if struct.calcsize("l") == 4: machine = os.uname()[4] + "-32" else: @@ -221,13 +223,14 @@ def _add_directory( path.insert(where, subdir) -def _find_include_file(self: pil_build_ext, include: str) -> int: +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: pil_build_ext, library: str) -> str | None: @@ -305,6 +308,7 @@ class pil_build_ext(build_ext): "jpeg2000", "imagequant", "xcb", + "avif", ] required = {"jpeg", "zlib"} @@ -344,7 +348,7 @@ class pil_build_ext(build_ext): 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")] @@ -389,17 +393,18 @@ class pil_build_ext(build_ext): 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) @@ -407,15 +412,17 @@ class pil_build_ext(build_ext): _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) @@ -448,7 +455,7 @@ class pil_build_ext(build_ext): 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") ) @@ -477,6 +484,7 @@ class pil_build_ext(build_ext): # # add configured kits for root_name, lib_name in { + "AVIF_ROOT": "avif", "JPEG_ROOT": "libjpeg", "JPEG2K_ROOT": "libopenjp2", "TIFF_ROOT": ("libtiff-5", "libtiff-4"), @@ -500,11 +508,11 @@ class pil_build_ext(build_ext): if root is None and pkg_config: if isinstance(lib_name, str): - _dbg(f"Looking for `{lib_name}` using pkg-config.") + _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 @@ -606,6 +614,7 @@ class pil_build_ext(build_ext): _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")) @@ -613,11 +622,7 @@ class pil_build_ext(build_ext): 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.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"): @@ -690,6 +695,8 @@ class pil_build_ext(build_ext): feature.set("zlib", "z") elif sys.platform == "win32" and _find_library_file(self, "zlib"): 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") @@ -724,7 +731,7 @@ class pil_build_ext(build_ext): 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"): @@ -746,12 +753,12 @@ class pil_build_ext(build_ext): if feature.want("tiff"): _dbg("Looking for tiff") if _find_include_file(self, "tiff.h"): - if _find_library_file(self, "tiff"): - feature.set("tiff", "tiff") if sys.platform in ["win32", "darwin"] and _find_library_file( self, "libtiff" ): feature.set("tiff", "libtiff") + elif _find_library_file(self, "tiff"): + feature.set("tiff", "tiff") if feature.want("freetype"): _dbg("Looking for freetype") @@ -839,6 +846,16 @@ class pil_build_ext(build_ext): if _find_library_file(self, "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 feature.get(f) and feature.require(f): if f in ("jpeg", "zlib"): @@ -927,6 +944,14 @@ class pil_build_ext(build_ext): 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) @@ -969,6 +994,7 @@ class pil_build_ext(build_ext): (feature.get("lcms"), "LITTLECMS2"), (feature.get("webp"), "WEBP"), (feature.get("xcb"), "XCB (X protocol)"), + (feature.get("avif"), "LIBAVIF"), ] all = 1 @@ -1001,7 +1027,7 @@ def debug_build() -> bool: return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD -files = ["src/_imaging.c"] +files: list[str | os.PathLike[str]] = ["src/_imaging.c"] for src_file in _IMAGING: files.append("src/" + src_file + ".c") for src_file in _LIB_IMAGING: @@ -1011,6 +1037,7 @@ ext_modules = [ 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._imagingmorph", ["src/_imagingmorph.c"]), @@ -1044,7 +1071,7 @@ except DependencyException as err: 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 000000000..366e0c864 --- /dev/null +++ b/src/PIL/AvifImagePlugin.py @@ -0,0 +1,291 @@ +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) + 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 bc1416c74..f175e2f4f 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -26,17 +26,6 @@ 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 +43,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 +51,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 +60,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 +96,7 @@ class BdfFontFile(FontFile.FontFile): 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 +105,7 @@ class BdfFontFile(FontFile.FontFile): 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 e5605635e..f7be7746d 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): @@ -259,29 +259,44 @@ class BlpImageFile(ImageFile.ImageFile): def _open(self) -> None: self.magic = self.fp.read(4) - - self.fp.seek(5, os.SEEK_CUR) - (self._blp_alpha_depth,) = struct.unpack(" tuple[int, int]: try: - self._read_blp_header() + self._read_header() self._load() except struct.error as e: msg = "Truncated BLP file" @@ -292,25 +307,9 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): 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 @@ -326,9 +325,11 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): 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(" 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: @@ -365,65 +368,61 @@ class BLP1Decoder(_BLPBaseDecoder): (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) @@ -472,10 +471,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 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: @@ -242,7 +244,9 @@ class BmpImageFile(ImageFile.ImageFile): msg = "Unsupported BMP bitfields layout" raise OSError(msg) elif file_info["compression"] == self.COMPRESSIONS["RAW"]: - if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset + 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.COMPRESSIONS["RLE8"], @@ -441,9 +445,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 diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 0ee2f653b..8c5da14f5 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,13 +41,11 @@ class BufrStubImageFile(ImageFile.StubImageFile): format_description = "BUFR" def _open(self) -> None: - offset = self.fp.tell() - 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" diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index c4be0ceca..b817dbc87 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -26,7 +26,7 @@ from ._binary import i32le as i32 def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"\0\0\2\0" + return prefix.startswith(b"\0\0\2\0") ## diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index f67f27d73..aea661b9c 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -24,6 +24,7 @@ from __future__ import annotations 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? @@ -66,6 +67,8 @@ class DcxImageFile(PcxImageFile): 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 1b6408237..f9ade18f9 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: @@ -419,6 +419,14 @@ class DdsImageFile(ImageFile.ImageFile): 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" @@ -518,30 +526,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] - else: - rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000] - else: - pixel_flags = DDPF.RGB - rawmode = 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: - pixel_flags |= DDPF.ALPHAPIXELS - rgba_mask.append(0xFF000000 if alpha else 0) - - flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PITCH | DDSD.PIXELFORMAT + flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT bitcount = len(im.getbands()) * 8 - pitch = (im.width * bitcount + 7) // 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: + 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: + 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: + pixel_flags |= DDPF.ALPHAPIXELS + rgba_mask.append(0xFF000000 if alpha else 0) + + fourcc = D3DFMT.UNKNOWN fp.write( o32(DDS_MAGIC) + struct.pack( @@ -556,17 +602,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 fb1e301c0..5e2ddad99 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -170,7 +170,9 @@ def Ghostscript( 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 + ) ## @@ -295,7 +297,7 @@ class EpsImageFile(ImageFile.ImageFile): 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] = "" @@ -454,7 +456,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, [ImageFile._Tile("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 39b4aa552..2280d5ce8 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -303,38 +303,38 @@ TAGS = { 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 +342,41 @@ GPSTAGS = {i.value: i.name for i in GPS} 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 6bbd2641a..a3fdc0efe 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -17,7 +17,7 @@ from . import Image, ImageFile def _accept(prefix: bytes) -> bool: - return prefix[:6] == b"SIMPLE" + return prefix.startswith(b"SIMPLE") class FitsImageFile(ImageFile.ImageFile): diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 666390be9..7c5bfeefa 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -22,6 +22,7 @@ from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 from ._binary import i32le as i32 from ._binary import o8 +from ._util import DeferredError # # decoder @@ -134,6 +135,8 @@ class FliImageFile(ImageFile.ImageFile): 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) @@ -159,7 +162,7 @@ class FliImageFile(ImageFile.ImageFile): framesize = i32(s) self.decodermaxblock = framesize - self.tile = [ImageFile._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/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 8fef51076..fd992cd9e 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -42,7 +42,7 @@ MODES = { def _accept(prefix: bytes) -> bool: - return prefix[:8] == olefile.MAGIC + return prefix.startswith(olefile.MAGIC) ## @@ -170,7 +170,7 @@ class FpxImageFile(ImageFile.ImageFile): "raw", (x, y, x1, y1), i32(s, i) + 28, - (self.rawmode,), + self.rawmode, ) ) diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index ddb469bc3..d60e75bb6 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -79,8 +79,6 @@ class FtexImageFile(ImageFile.ImageFile): 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 @@ -95,7 +93,8 @@ class FtexImageFile(ImageFile.ImageFile): self._mode = "RGBA" self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))] elif format == Format.UNCOMPRESSED: - self.tile = [ImageFile._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 +107,7 @@ class FtexImageFile(ImageFile.ImageFile): 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/GdImageFile.py b/src/PIL/GdImageFile.py index f1b4969f2..891225ce2 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -56,7 +56,7 @@ class GdImageFile(ImageFile.ImageFile): 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 +68,15 @@ class GdImageFile(ImageFile.ImageFile): 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 57c291792..c98e02f69 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -31,7 +31,7 @@ import os import subprocess from enum import IntEnum from functools import cached_property -from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union +from typing import IO, Any, Literal, NamedTuple, Union, cast from . import ( Image, @@ -45,7 +45,9 @@ from . import ( 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 . import _imaging from ._typing import Buffer @@ -67,7 +69,7 @@ LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST def _accept(prefix: bytes) -> bool: - return prefix[:6] in [b"GIF87a", b"GIF89a"] + return prefix.startswith((b"GIF87a", b"GIF89a")) ## @@ -103,7 +105,6 @@ class GifImageFile(ImageFile.ImageFile): self.info["version"] = s[:6] self._size = i16(s, 6), i16(s, 8) - self.tile = [] flags = s[10] bits = (flags & 7) + 1 @@ -168,6 +169,8 @@ class GifImageFile(ImageFile.ImageFile): 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 @@ -258,7 +261,7 @@ class GifImageFile(ImageFile.ImageFile): # 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) @@ -347,12 +350,15 @@ class GifImageFile(ImageFile.ImageFile): 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: @@ -690,15 +696,21 @@ 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) - assert im_frames[0].im.palette is not None - 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 @@ -764,7 +776,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 diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index 220eac57e..ec62f8e4e 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -116,7 +116,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 4cad0ebee..379ffd739 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -16,24 +16,27 @@ from __future__ import annotations import re +from io import BytesIO from typing import IO -from ._binary import o8 - class GimpPaletteFile: """File handler for GIMP's palette format.""" 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 +44,29 @@ class GimpPaletteFile: # 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 = b"".join(palette) + self.palette = bytes(palette) + + def __init__(self, fp: IO[bytes]) -> None: + self._read(fp) + + @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 e9aa084b2..439fc5a3e 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 prefix.startswith(b"GRIB") and prefix[7] == 1 class GribStubImageFile(ImageFile.StubImageFile): @@ -40,13 +41,11 @@ class GribStubImageFile(ImageFile.StubImageFile): format_description = "GRIB" def _open(self) -> None: - offset = self.fp.tell() - 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" diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index cc9e73deb..76e640f15 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,13 +41,11 @@ class HDF5StubImageFile(ImageFile.StubImageFile): format_description = "HDF5" def _open(self) -> None: - offset = self.fp.tell() - 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" diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 9757b2b14..5a88429e5 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -117,14 +117,13 @@ def read_png_or_jpeg2000( sig = fobj.read(12) im: Image.Image - if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a": + 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: @@ -387,7 +386,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 e879f1801..bd35ac890 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -118,7 +118,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) class IconHeader(NamedTuple): @@ -362,7 +362,7 @@ class IcoImageFile(ImageFile.ImageFile): self.info["sizes"] = set(sizes) self.size = im.size - return None + 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 f9f47348c..71b999678 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -31,6 +31,7 @@ import re from typing import IO, Any from . import Image, ImageFile, ImagePalette +from ._util import DeferredError # -------------------------------------------------------------------- # Standard tags @@ -145,7 +146,7 @@ class ImImageFile(ImageFile.ImageFile): 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 +156,9 @@ class ImImageFile(ImageFile.ImageFile): 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 +210,7 @@ class ImImageFile(ImageFile.ImageFile): 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,7 +248,7 @@ class ImImageFile(ImageFile.ImageFile): self._fp = self.fp # FIXME: hack - if self.rawmode[:2] == "F;": + if self.rawmode.startswith("F;"): # ifunc95 formats try: # use bit decoder (if necessary) @@ -290,6 +291,8 @@ class ImImageFile(ImageFile.ImageFile): 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 @@ -357,7 +360,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") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 44270392c..b17441aa7 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,14 +41,7 @@ import warnings from collections.abc import Callable, Iterator, MutableMapping, Sequence from enum import IntEnum from types import ModuleType -from typing import ( - IO, - TYPE_CHECKING, - Any, - Literal, - Protocol, - cast, -) +from typing import IO, Any, Literal, Protocol, cast # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -218,6 +211,7 @@ if hasattr(core, "DEFAULT_STRATEGY"): # -------------------------------------------------------------------- # Registries +TYPE_CHECKING = False if TYPE_CHECKING: import mmap from xml.etree.ElementTree import Element @@ -514,7 +508,7 @@ class ImagePointTransform: def _getscaleoffset( - expr: Callable[[ImagePointTransform], ImagePointTransform | float] + 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) @@ -548,7 +542,6 @@ class Image: def __init__(self) -> None: # FIXME: take "new" parameters / other image? - # FIXME: turn mode and size into delegating properties? self._im: core.ImagingCore | DeferredError | None = None self._mode = "" self._size = (0, 0) @@ -584,6 +577,14 @@ class Image: 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 @@ -603,24 +604,16 @@ class Image: def __enter__(self): 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"): + from . import ImageFile + + if isinstance(self, ImageFile.ImageFile): 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. @@ -629,14 +622,9 @@ class Image: :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): + 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 @@ -692,13 +680,10 @@ class Image: ) 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: PrettyPrinter, cycle: bool) -> None: @@ -707,14 +692,8 @@ class Image: # 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: @@ -757,13 +736,23 @@ class Image: new["shape"], new["typestr"] = _conv_type_shape(self) return new + 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: 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 @@ -778,18 +767,20 @@ class Image: .. 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. The default is to - use the standard "raw" encoder. + :param encoder_name: What encoder to use. - 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. + 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. :param args: Extra arguments to the encoder. :returns: A :py:class:`bytes` object. """ @@ -811,7 +802,9 @@ class Image: e = _getencoder(self.mode, encoder_name, encoder_args) e.setimage(self.im) - 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: @@ -1025,7 +1018,7 @@ class Image: 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 @@ -1518,7 +1511,7 @@ class Image: 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: @@ -1549,8 +1542,11 @@ class Image: # 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]) @@ -1563,50 +1559,10 @@ class Image: 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): - assert exif._info is not None - ifds.append((ifd1, exif._info.next)) + from . import ImageFile - offset = None - for ifd, ifd_offset in ifds: - current_offset = self.fp.tell() - if offset is None: - offset = current_offset - - fp = self.fp - if ifd is not None: - 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 - - 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) -> CapsuleType: """ @@ -2539,7 +2495,21 @@ class Image: 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. @@ -2550,7 +2520,7 @@ class Image: 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: @@ -2559,14 +2529,7 @@ class Image: 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() @@ -2582,9 +2545,28 @@ class Image: 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) + encoderinfo = getattr(self, "encoderinfo", {}) + self.encoderinfo = {**encoderinfo, **params} + 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()] @@ -2612,6 +2594,8 @@ class Image: except PermissionError: pass raise + finally: + self.encoderinfo = encoderinfo if open_fp: fp.close() @@ -3025,7 +3009,7 @@ class Image: # Abstract handlers. -class ImagePointHandler: +class ImagePointHandler(abc.ABC): """ Used as a mixin by point transforms (for use with :py:meth:`~PIL.Image.Image.point`) @@ -3036,7 +3020,7 @@ class ImagePointHandler: pass -class ImageTransformHandler: +class ImageTransformHandler(abc.ABC): """ Used as a mixin by geometry transforms (for use with :py:meth:`~PIL.Image.Image.transform`) @@ -3055,15 +3039,6 @@ class ImageTransformHandler: # -------------------------------------------------------------------- # Factories -# -# Debugging - - -def _wedge() -> Image: - """Create grayscale wedge (for debugging only)""" - - return Image()._new(core.wedge("L")) - def _check_size(size: Any) -> None: """ @@ -3254,6 +3229,18 @@ class SupportsArrayInterface(Protocol): 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 @@ -3283,7 +3270,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: :param obj: Object with array interface :param mode: Optional mode to use when reading ``obj``. Will be determined from - type if ``None``. + type if ``None``. Deprecated. This will not be used to convert the data after reading, but will be used to change how the data is read:: @@ -3318,6 +3305,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: msg = f"Cannot handle this data type: {typekey_shape}, {typestr}" raise TypeError(msg) from e else: + deprecate("'mode' parameter", 13) rawmode = mode if mode in ["1", "L", "I", "P", "F"]: ndmax = 2 @@ -3342,6 +3330,58 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) +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 @@ -3463,7 +3503,7 @@ def open( exclusive_fp = False filename: str | bytes = "" if is_path(fp): - filename = os.path.realpath(os.fspath(fp)) + filename = os.fspath(fp) if filename: fp = builtins.open(filename, "rb") @@ -3893,7 +3933,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:: @@ -3943,7 +3983,7 @@ class Exif(_ExifBase): return self._fixup_dict(dict(info)) def _get_head(self) -> bytes: - version = b"\x2B" if self.bigtiff else b"\x2A" + version = b"\x2b" if self.bigtiff else b"\x2a" if self.endian == "<": head = b"II" + version + b"\x00" + o32le(8) else: @@ -4027,6 +4067,9 @@ class Exif(_ExifBase): 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, @@ -4056,19 +4099,19 @@ class Exif(_ExifBase): 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]: + 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": + 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("H", tag_data[:2])[0]): + for i in range(struct.unpack(">H", tag_data[:2])[0]): ifd_tag, typ, count, data = struct.unpack( ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2] ) @@ -4147,7 +4190,7 @@ class Exif(_ExifBase): 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 diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index fdfbee789..a1584f111 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -248,6 +248,9 @@ class ImageCmsProfile: low-level profile object """ + self.filename = None + self.product_name = None # profile.product_name + self.product_info = None # profile.product_info if isinstance(profile, str): if sys.platform == "win32": @@ -256,23 +259,18 @@ class ImageCmsProfile: 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 tobytes(self) -> bytes: """ Returns the profile in a format suitable for embedding in diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index d8e4c0c60..d7a739e4e 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -35,19 +35,16 @@ import math import struct from collections.abc import Sequence from types import ModuleType -from typing import TYPE_CHECKING, Any, AnyStr, Callable, Union, cast +from typing import Any, AnyStr, Callable, Union, 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 +Outline: Callable[[], Image.core._Outline] = Image.core.outline +TYPE_CHECKING = False if TYPE_CHECKING: from . import ImageDraw2, ImageFont @@ -368,22 +365,10 @@ class ImageDraw: # 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, @@ -557,21 +542,6 @@ class ImageDraw: 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: ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont, - spacing: float, - stroke_width: float, - ) -> float: - return ( - self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] - + stroke_width - + spacing - ) - def text( self, xy: tuple[float, float], @@ -643,6 +613,7 @@ class ImageDraw: features=features, language=language, stroke_width=stroke_width, + stroke_filled=True, anchor=anchor, ink=ink, start=start, @@ -692,11 +663,132 @@ class ImageDraw: draw_text(stroke_ink, stroke_width) # Draw normal text - draw_text(ink, 0) + if ink != stroke_ink: + draw_text(ink) else: # Only draw normal text draw_text(ink) + def _prepare_multiline_text( + self, + xy: tuple[float, float], + text: AnyStr, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ), + anchor: str | None, + spacing: float, + align: str, + direction: str | None, + features: list[str] | None, + language: str | None, + stroke_width: float, + embedded_color: bool, + font_size: float | None, + ) -> tuple[ + ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont, + list[tuple[tuple[float, float], str, AnyStr]], + ]: + 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 = text.split("\n" if isinstance(text, str) else b"\n") + line_spacing = ( + self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] + + stroke_width + + spacing + ) + + 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 + + parts = [] + for idx, line in enumerate(lines): + left = xy[0] + 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(text, str) else 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.textlength( + word, + font, + direction=direction, + features=features, + language=language, + embedded_color=embedded_color, + ) + for word in words + ] + word_anchor = "l" + anchor[1] + width_difference = max_width - sum(word_widths) + for i, word in enumerate(words): + parts.append(((left, top), word_anchor, word)) + left += word_widths[i] + width_difference / (len(words) - 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(((left, top), anchor, line)) + top += line_spacing + + return font, parts + def multiline_text( self, xy: tuple[float, float], @@ -720,62 +812,24 @@ class ImageDraw: *, 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) + font, lines = self._prepare_multiline_text( + xy, + text, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + embedded_color, + font_size, + ) + for xy, anchor, line in lines: self.text( - (left, top), + xy, line, fill, font, @@ -787,7 +841,6 @@ class ImageDraw: stroke_fill=stroke_fill, embedded_color=embedded_color, ) - top += line_spacing def textlength( self, @@ -889,69 +942,26 @@ class ImageDraw: *, font_size: float | None = None, ) -> tuple[float, float, float, float]: - 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 + font, lines = self._prepare_multiline_text( + xy, + text, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + embedded_color, + font_size, + ) bbox: tuple[float, float, float, float] | 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) - + for xy, anchor, line in lines: bbox_line = self.textbbox( - (left, top), + xy, line, font, anchor, @@ -971,8 +981,6 @@ class ImageDraw: max(bbox[3], bbox_line[3]), ) - top += line_spacing - if bbox is None: return xy[0], xy[1], xy[0], xy[1] return bbox @@ -1192,7 +1200,7 @@ def _compute_regular_polygon_vertices( 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: @@ -1215,4 +1223,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/ImageFile.py b/src/PIL/ImageFile.py index d69d84568..bf556a2c6 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -31,18 +31,21 @@ from __future__ import annotations import abc import io import itertools +import logging import os import struct -import sys -from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast +from typing import IO, Any, NamedTuple, cast -from . import Image +from . import ExifTags, Image from ._deprecate import deprecate -from ._util import is_path +from ._util import DeferredError, is_path +TYPE_CHECKING = False if TYPE_CHECKING: from ._typing import StrOrBytesPath +logger = logging.getLogger(__name__) + MAXBLOCK = 65536 SAFEBLOCK = 1024 * 1024 @@ -98,8 +101,8 @@ def _tilesort(t: _Tile) -> int: class _Tile(NamedTuple): codec_name: str extents: tuple[int, int, int, int] | None - offset: int - args: tuple[Any, ...] | str | None + offset: int = 0 + args: tuple[Any, ...] | str | None = None # @@ -120,7 +123,7 @@ class ImageFile(Image.Image): self.custom_mimetype: str | None = None self.tile: list[_Tile] = [] - """ A list of tile descriptors, or ``None`` """ + """ A list of tile descriptors """ self.readonly = 1 # until we know better @@ -130,7 +133,7 @@ class ImageFile(Image.Image): if is_path(fp): # filename self.fp = open(fp, "rb") - self.filename = os.path.realpath(os.fspath(fp)) + self.filename = os.fspath(fp) self._exclusive_fp = True else: # stream @@ -163,6 +166,85 @@ class ImageFile(Image.Image): def _open(self) -> None: pass + def _close_fp(self): + 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 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,) + 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(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 @@ -170,8 +252,13 @@ class ImageFile(Image.Image): return Image.MIME.get(self.format.upper()) return None + 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: @@ -196,8 +283,6 @@ class ImageFile(Image.Image): 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") readonly = 0 @@ -263,7 +348,7 @@ class ImageFile(Image.Image): 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 @@ -276,8 +361,13 @@ class ImageFile(Image.Image): 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: @@ -356,7 +446,7 @@ class ImageFile(Image.Image): return self.tell() != frame -class StubHandler: +class StubHandler(abc.ABC): def open(self, im: StubImageFile) -> None: pass @@ -365,7 +455,7 @@ class StubHandler: pass -class StubImageFile(ImageFile): +class StubImageFile(ImageFile, metaclass=abc.ABCMeta): """ Base class for stub image loaders. @@ -373,9 +463,9 @@ 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) -> Image.core.PixelAccess | None: loader = self._load() @@ -389,10 +479,10 @@ class StubImageFile(ImageFile): 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: diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 8b0974b2c..b9ed54ab2 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -20,14 +20,15 @@ import abc import functools from collections.abc import Sequence from types import ModuleType -from typing import TYPE_CHECKING, Any, Callable, cast +from typing import Any, Callable, cast +TYPE_CHECKING = False if TYPE_CHECKING: from . import _imaging from ._typing import NumpyArray -class Filter: +class Filter(abc.ABC): @abc.abstractmethod def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: pass @@ -553,7 +554,7 @@ class Color3DLUT(MultibandFilter): 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): @@ -598,8 +599,6 @@ class Color3DLUT(MultibandFilter): 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 b694b817e..329c463ff 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -34,12 +34,13 @@ import warnings from enum import IntEnum from io import BytesIO from types import ModuleType -from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast +from typing import IO, Any, BinaryIO, TypedDict, cast from . import Image, features from ._typing import StrOrBytesPath from ._util import DeferredError, is_path +TYPE_CHECKING = False if TYPE_CHECKING: from . import ImageFile from ._imaging import ImagingFont @@ -270,7 +271,7 @@ class FreeTypeFont: ) 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: @@ -644,10 +645,10 @@ class FreeTypeFont: features, language, stroke_width, + kwargs.get("stroke_filled", False), anchor, ink, - start[0], - start[1], + start, ) def font_variant( @@ -1092,7 +1093,7 @@ w7IkEbzhVQAAAABJRU5ErkJggg== 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. diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index e27ca7e50..1eb450734 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -25,12 +25,17 @@ import tempfile 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: @@ -51,8 +56,12 @@ def grab( 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 +86,18 @@ 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]) + subprocess.call(args + [filepath]) im = Image.open(filepath) im.load() os.unlink(filepath) @@ -104,38 +117,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) + p = subprocess.run( + ["osascript", "-e", "get the clipboard as «class PNGf»"], + capture_output=True, + ) + if p.returncode != 0: + return None - im = None - if os.stat(filepath).st_size != 0: - im = Image.open(filepath) - im.load() - os.unlink(filepath) - return im + 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 15464947d..484797f91 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -173,10 +173,10 @@ class _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: diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 44aad0c3c..da28854b5 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -22,7 +22,7 @@ import functools import operator import re from collections.abc import Sequence -from typing import Protocol, cast +from typing import Literal, Protocol, cast, overload from . import ExifTags, Image, ImagePalette @@ -213,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( @@ -235,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( @@ -256,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]) @@ -673,6 +673,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 @@ -698,10 +708,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._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() @@ -718,11 +729,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 183f85526..103697117 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -19,10 +19,11 @@ from __future__ import annotations import array from collections.abc import Sequence -from typing import IO, TYPE_CHECKING +from typing import IO from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile +TYPE_CHECKING = False if TYPE_CHECKING: from . import Image diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index a3d647138..df7a57b65 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -19,11 +19,12 @@ from __future__ import annotations import sys from io import BytesIO -from typing import TYPE_CHECKING, Any, Callable, Union +from typing import Any, Callable, Union from . import Image from ._util import is_path +TYPE_CHECKING = False if TYPE_CHECKING: import PyQt6 import PySide6 @@ -213,4 +214,7 @@ def toqimage(im: Image.Image | str | QByteArray) -> ImageQt: def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap: qimage = toqimage(im) - return getattr(QPixmap, "fromImage")(qimage) + pixmap = getattr(QPixmap, "fromImage")(qimage) + if qt_version == "6": + pixmap.detach() + return pixmap diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index d62893d9c..7705608e3 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -175,7 +175,9 @@ class MacViewer(Viewer): 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( [ @@ -192,7 +194,7 @@ if sys.platform == "darwin": register(MacViewer) -class UnixViewer(Viewer): +class UnixViewer(abc.ABC, Viewer): format = "PNG" options = {"compress_level": 1, "save_all": True} diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index bf29fdba5..3a4cb81e9 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -28,10 +28,11 @@ from __future__ import annotations import tkinter from io import BytesIO -from typing import TYPE_CHECKING, Any, cast +from typing import Any from . import Image, ImageFile +TYPE_CHECKING = False if TYPE_CHECKING: from ._typing import CapsuleType @@ -263,28 +264,3 @@ def getimage(photo: PhotoImage) -> Image.Image: _pyimagingtkcall("PyImagingPhotoGet", photo, im.getim()) return im - - -def _show(image: Image.Image, title: str | None) -> None: - """Helper for the Image.show method.""" - - class UI(tkinter.Label): - def __init__(self, master: tkinter.Toplevel, im: Image.Image) -> None: - self.image: BitmapImage | PhotoImage - if im.mode == "1": - self.image = BitmapImage(im, foreground="white", master=master) - else: - self.image = PhotoImage(im, master=master) - if TYPE_CHECKING: - image = cast(tkinter._Image, self.image) - else: - image = self.image - super().__init__(master, image=image, bg="black", bd=0) - - if not getattr(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 a3d8f441a..fb144ff38 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -48,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. @@ -58,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/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index 594c56513..c4eccee34 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -55,14 +55,14 @@ class ImtImageFile(ImageFile.ImageFile): 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 60ab7c83f..637f67810 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -179,7 +179,8 @@ class IptcImageFile(ImageFile.ImageFile): with Image.open(o) as _im: _im.load() self.im = _im.im - return None + self.tile = [] + return Image.Image.load(self) Image.register_open(IptcImageFile.format, IptcImageFile) diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index b6ebd562b..e0f4ecae5 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -252,6 +252,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): 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) @@ -262,6 +263,9 @@ class Jpeg2KImageFile(ImageFile.ImageFile): 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" @@ -296,10 +300,6 @@ class Jpeg2KImageFile(ImageFile.ImageFile): ] def _parse_comment(self) -> None: - hdr = self.fp.read(2) - length = _binary.i16be(hdr) - self.fp.seek(length - 2, os.SEEK_CUR) - while True: marker = self.fp.read(2) if not marker: @@ -352,9 +352,8 @@ class Jpeg2KImageFile(ImageFile.ImageFile): 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") ) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 6510e072e..defe9f773 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -42,7 +42,7 @@ import subprocess import sys import tempfile import warnings -from typing import IO, TYPE_CHECKING, Any +from typing import IO, Any from . import Image, ImageFile from ._binary import i16be as i16 @@ -52,6 +52,7 @@ from ._binary import o16be as o16 from ._deprecate import deprecate from .JpegPresets import presets +TYPE_CHECKING = False if TYPE_CHECKING: from .MpoImagePlugin import MpoImageFile @@ -72,12 +73,12 @@ def APP(self: JpegImageFile, marker: int) -> 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) @@ -90,21 +91,24 @@ def APP(self: JpegImageFile, marker: int) -> None: 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 @@ -117,7 +121,7 @@ def APP(self: JpegImageFile, marker: int) -> None: # 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", {}) @@ -150,7 +154,7 @@ def APP(self: JpegImageFile, marker: int) -> None: except struct.error: break # 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: @@ -159,7 +163,7 @@ def APP(self: JpegImageFile, marker: int) -> None: 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 @@ -322,7 +326,7 @@ MARKER = { 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") ## @@ -339,7 +343,7 @@ class JpegImageFile(ImageFile.ImageFile): 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 @@ -395,6 +399,13 @@ class JpegImageFile(ImageFile.ImageFile): return getattr(self, "_" + name) raise AttributeError(name) + 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 @@ -407,7 +418,7 @@ class JpegImageFile(ImageFile.ImageFile): # Premature EOF. # Pretend file is finished adding EOI marker self._ended = True - return b"\xFF\xD9" + return b"\xff\xd9" return s @@ -537,7 +548,7 @@ def _getmp(self: JpegImageFile) -> dict[int, Any] | None: 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 @@ -559,7 +570,7 @@ def _getmp(self: JpegImageFile) -> dict[int, Any] | None: 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 ) @@ -702,7 +713,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 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 @@ -751,18 +762,16 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: extra = info.get("extra", b"") MAX_BYTES_IN_MARKER = 65533 - xmp = info.get("xmp", im.info.get("xmp")) - if xmp: + 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 + extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp - icc_profile = info.get("icc_profile") - if icc_profile: + 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 = [] @@ -773,7 +782,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: for marker in markers: size = o16(2 + overhead_len + len(marker)) extra += ( - b"\xFF\xE2" + b"\xff\xe2" + size + b"ICC_PROFILE\0" + o8(i) @@ -806,8 +815,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 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), @@ -821,7 +829,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # 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": @@ -838,7 +845,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 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, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 5dd031be3..b4460a9a5 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -23,7 +23,7 @@ from . import Image, ImageFile 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") ## diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 5f23a34b9..9ce38c427 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -26,7 +26,7 @@ from . import Image, TiffImagePlugin def _accept(prefix: bytes) -> bool: - return prefix[:8] == olefile.MAGIC + return prefix.startswith(olefile.MAGIC) ## @@ -54,7 +54,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): 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 @@ -73,12 +73,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): 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 ad4d3e937..47ebe9d62 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -33,11 +33,7 @@ class BitStream: 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 @@ class BitStream: 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 71f89a09a..f7393eac0 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -32,6 +32,7 @@ from . import ( TiffImagePlugin, ) from ._binary import o32le +from ._util import DeferredError def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: @@ -51,7 +52,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if not offsets: # APP2 marker im_frame.encoderinfo["extra"] = ( - b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 + b"\xff\xe2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 ) exif = im_frame.encoderinfo.get("exif") if isinstance(exif, Image.Exif): @@ -84,7 +85,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) @@ -125,11 +126,15 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): 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] diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index f3460a787..277087a86 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -37,7 +37,7 @@ from ._binary import o16le as o16 def _accept(prefix: bytes) -> bool: - return prefix[:4] in [b"DanM", b"LinS"] + return prefix.startswith((b"DanM", b"LinS")) ## @@ -69,10 +69,10 @@ class MspImageFile(ImageFile.ImageFile): self._mode = "1" self._size = i16(s, 4), i16(s, 6) - if s[:4] == b"DanM": - self.tile = [ImageFile._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 = [ImageFile._Tile("MSP", (0, 0) + self.size, 32, None)] + self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)] class MspDecoder(ImageFile.PyDecoder): @@ -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, [ImageFile._Tile("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 02939d26b..7fd4c5c94 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -17,10 +17,13 @@ from __future__ import annotations import sys -from typing import IO, TYPE_CHECKING +from typing import IO from . import EpsImagePlugin +TYPE_CHECKING = False + + ## # Simple PostScript graphics interface. diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index 81652e5ee..2a26e5d4e 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -32,7 +32,7 @@ class PaletteFile: 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 b33245376..15f712908 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -116,9 +116,6 @@ _COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00} 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,12 +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: - assert im.palette is not None - 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 @@ -196,22 +192,11 @@ 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( diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index e8ea800a4..3aa249988 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -34,7 +34,7 @@ class PcdImageFile(ImageFile.ImageFile): self.fp.seek(2048) s = self.fp.read(2048) - if s[:4] != b"PCD_": + if not s.startswith(b"PCD_"): msg = "not a PCD file" raise SyntaxError(msg) @@ -47,7 +47,7 @@ class PcdImageFile(ImageFile.ImageFile): self._mode = "RGB" self._size = 768, 512 # FIXME: not correct for rotated images! - self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048, None)] + self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)] def load_end(self) -> None: if self.tile_post_rotate: diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 8445d5cc7..458d586c4 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -54,7 +54,7 @@ class PcxImageFile(ImageFile.ImageFile): # 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 @@ class PcxImageFile(ImageFile.ImageFile): 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 @@ class PcxImageFile(ImageFile.ImageFile): 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 @@ class PcxImageFile(ImageFile.ImageFile): 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,9 +129,7 @@ class PcxImageFile(ImageFile.ImageFile): bbox = (0, 0) + self.size logger.debug("size: %sx%s", *self.size) - self.tile = [ - ImageFile._Tile("pcx", bbox, self.fp.tell(), (rawmode, planes * stride)) - ] + self.tile = [ImageFile._Tile("pcx", bbox, offset, (rawmode, planes * stride))] # -------------------------------------------------------------------- @@ -188,7 +187,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) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 7cb2d241b..73d8c21c0 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -8,7 +8,7 @@ import os import re import time import zlib -from typing import IO, TYPE_CHECKING, Any, NamedTuple, Union +from typing import IO, Any, NamedTuple, Union # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set @@ -19,14 +19,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,29 +36,29 @@ PDFDocEncoding = { 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", } @@ -251,6 +251,7 @@ class PdfArray(list[Any]): return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" +TYPE_CHECKING = False if TYPE_CHECKING: _DictBase = collections.UserDict[Union[str, bytes], Any] else: diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 36f565f1c..d2b6d0a97 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -28,7 +28,7 @@ from ._binary import i16le as i16 def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"\200\350\000\000" + return prefix.startswith(b"\200\350\000\000") ## @@ -61,9 +61,7 @@ class PixarImageFile(ImageFile.ImageFile): # FIXME: to be continued... # create tile descriptor (assuming "dumped") - self.tile = [ - ImageFile._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 4e1227204..1b9a89aef 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -40,7 +40,7 @@ import warnings import zlib from collections.abc import Callable from enum import IntEnum -from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn, cast +from typing import IO, Any, NamedTuple, NoReturn, cast from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from ._binary import i16be as i16 @@ -48,7 +48,10 @@ from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 from ._binary import o32be as o32 +from ._deprecate import deprecate +from ._util import DeferredError +TYPE_CHECKING = False if TYPE_CHECKING: from . import _imaging @@ -523,7 +526,7 @@ class PngStream(ChunkStream): 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 @@ -740,7 +743,7 @@ class PngStream(ChunkStream): def _accept(prefix: bytes) -> bool: - return prefix[:8] == _MAGIC + return prefix.startswith(_MAGIC) ## @@ -869,6 +872,8 @@ class PngImageFile(ImageFile.ImageFile): 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 @@ -1364,6 +1369,8 @@ def _save( 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 @@ -1382,7 +1389,7 @@ def _save( 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: @@ -1433,7 +1440,7 @@ def _save( 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)) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 4e779df17..db34d107a 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -47,7 +47,7 @@ MODES = { def _accept(prefix: bytes) -> bool: - return prefix[0:1] == b"P" and prefix[1] in b"0123456fy" + return prefix.startswith(b"P") and prefix[1] in b"0123456fy" ## @@ -94,8 +94,8 @@ class PpmImageFile(ImageFile.ImageFile): 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: @@ -230,7 +230,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): 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: diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 8ff5e3908..f49aaeeb1 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -27,6 +27,7 @@ from ._binary import i16be as i16 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) @@ -47,7 +48,7 @@ MODES = { def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"8BPS" + return prefix.startswith(b"8BPS") ## @@ -148,6 +149,8 @@ class PsdImageFile(ImageFile.ImageFile): ) -> 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) @@ -167,17 +170,15 @@ class PsdImageFile(ImageFile.ImageFile): 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 + _, 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) diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index 010d3f941..dba5d809f 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): @@ -32,7 +35,7 @@ class QoiImageFile(ImageFile.ImageFile): self._mode = "RGB" if channels == 3 else "RGBA" self.fp.seek(1, os.SEEK_CUR) # colorspace - self.tile = [ImageFile._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): @@ -51,7 +54,7 @@ class QoiDecoder(ImageFile.PyDecoder): 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 +113,122 @@ class QoiDecoder(ImageFile.PyDecoder): 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 44254b7a4..853022150 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -82,17 +82,10 @@ class SgiImageFile(ImageFile.ImageFile): # 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) @@ -156,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 @@ -188,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)) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 075073f9f..868019e80 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -37,9 +37,12 @@ from __future__ import annotations import os import struct import sys -from typing import IO, TYPE_CHECKING, Any, cast +from typing import IO, Any, cast from . import Image, ImageFile +from ._util import DeferredError + +TYPE_CHECKING = False def isInt(f: Any) -> int: @@ -154,9 +157,7 @@ class SpiderImageFile(ImageFile.ImageFile): self.rawmode = "F;32F" self._mode = "F" - self.tile = [ - ImageFile._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 @@ -180,6 +181,8 @@ class SpiderImageFile(ImageFile.ImageFile): 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) @@ -211,26 +214,27 @@ class SpiderImageFile(ImageFile.ImageFile): # 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 # -------------------------------------------------------------------- @@ -268,7 +272,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) @@ -280,9 +284,7 @@ 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, [ImageFile._Tile("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: diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index 779288b1c..86490a496 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -35,12 +35,16 @@ class TarIO(ContainerIO.ContainerIO[bytes]): 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: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ff5a6f9e9..146b01e5f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -50,7 +50,7 @@ import warnings from collections.abc import Iterator, MutableMapping from fractions import Fraction from numbers import Number, Rational -from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn, cast +from typing import IO, Any, Callable, NoReturn, cast from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 @@ -58,9 +58,10 @@ from ._binary import i32be as i32 from ._binary import o8 from ._deprecate import deprecate from ._typing import StrOrBytesPath -from ._util import is_path +from ._util import DeferredError, is_path from .TiffTags import TYPES +TYPE_CHECKING = False if TYPE_CHECKING: from ._typing import Buffer, IntegralLike @@ -275,12 +276,12 @@ OPEN_INFO = { 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): @@ -288,13 +289,13 @@ if not getattr(Image.core, "libtiff_support_custom_tags", True): def _accept(prefix: bytes) -> bool: - return prefix[:4] in PREFIXES + return prefix.startswith(tuple(PREFIXES)) def _limit_rational( val: float | Fraction | IFDRational, max_val: int ) -> tuple[IntegralLike, IntegralLike]: - inv = abs(float(val)) > 1 + 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 @@ -404,7 +405,7 @@ class IFDRational(Rational): 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: @@ -582,7 +583,7 @@ class ImageFileDirectory_v2(_IFDv2Base): 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: @@ -685,22 +686,33 @@ class ImageFileDirectory_v2(_IFDv2Base): 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 - ) - elif all(isinstance(v, int) for v in values): - if all(0 <= v < 2**16 for v in values): - self.tagtype[tag] = TiffTags.SHORT - elif all(-(2**15) < v < 2**15 for v in values): - self.tagtype[tag] = TiffTags.SIGNED_SHORT + for v in values: + assert isinstance(v, IFDRational) + if v < 0: + self.tagtype[tag] = TiffTags.SIGNED_RATIONAL + break else: - self.tagtype[tag] = ( - TiffTags.LONG - if all(v >= 0 for v in values) - else TiffTags.SIGNED_LONG - ) + self.tagtype[tag] = TiffTags.RATIONAL + elif all(isinstance(v, int) 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 signed_short: + self.tagtype[tag] = TiffTags.SIGNED_SHORT + elif long: + self.tagtype[tag] = TiffTags.LONG + else: + 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): @@ -718,7 +730,10 @@ class ImageFileDirectory_v2(_IFDv2Base): 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 @@ -921,9 +936,9 @@ class ImageFileDirectory_v2(_IFDv2Base): 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,) = ( @@ -935,12 +950,25 @@ class ImageFileDirectory_v2(_IFDv2Base): warnings.warn(str(msg)) return + 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: list[tuple[int, int, int, bytes, bytes]] = [] - offset = offset + len(result) + len(self._tags_v2) * 12 + 4 + + 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 @@ -952,11 +980,7 @@ class ImageFileDirectory_v2(_IFDv2Base): 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 @@ -967,10 +991,8 @@ class ImageFileDirectory_v2(_IFDv2Base): 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 @@ -981,10 +1003,10 @@ class ImageFileDirectory_v2(_IFDv2Base): 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 @@ -995,16 +1017,18 @@ class ImageFileDirectory_v2(_IFDv2Base): values = [val + offset for val in handler(self, data, self.legacy_api)] data = self._write_dispatch[typ](self, *values) else: - value = self._pack("L", self._unpack("L", value)[0] + offset) + 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: @@ -1016,8 +1040,7 @@ class ImageFileDirectory_v2(_IFDv2Base): 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) @@ -1194,18 +1217,17 @@ class TiffImageFile(ImageFile.ImageFile): return self._seek(frame) if self._im is not None and ( - self.im.size != self._tile_size or self.im.mode != self.mode + self.im.size != self._tile_size + or self.im.mode != self.mode + or self.readonly ): - # The core image will no longer be used 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" @@ -1238,7 +1260,10 @@ class TiffImageFile(ImageFile.ImageFile): 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() @@ -1262,7 +1287,7 @@ class TiffImageFile(ImageFile.ImageFile): blocks = {} val = self.tag_v2.get(ExifTags.Base.ImageResources) if val: - while val[:4] == b"8BIM": + while val.startswith(b"8BIM"): id = i16(val[4:6]) n = math.ceil((val[6] + 1) / 2) * 2 size = i32(val[6 + n : 10 + n]) @@ -1289,10 +1314,6 @@ class TiffImageFile(ImageFile.ImageFile): 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: @@ -1367,8 +1388,17 @@ class TiffImageFile(ImageFile.ImageFile): 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") @@ -1386,7 +1416,8 @@ class TiffImageFile(ImageFile.ImageFile): 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) @@ -1419,8 +1450,12 @@ class TiffImageFile(ImageFile.ImageFile): logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) # size - xsize = self.tag_v2.get(IMAGEWIDTH) - ysize = self.tag_v2.get(IMAGELENGTH) + 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) @@ -1542,17 +1577,6 @@ class TiffImageFile(ImageFile.ImageFile): # 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 ( @@ -1561,6 +1585,14 @@ class TiffImageFile(ImageFile.ImageFile): 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 @@ -1583,6 +1615,10 @@ class TiffImageFile(ImageFile.ImageFile): 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: stride = w * sum(bps_tuple) / 8 # bytes per line @@ -1605,11 +1641,11 @@ class TiffImageFile(ImageFile.ImageFile): args, ) ) - x = x + w + x += w if x >= xsize: x, y = 0, y + h if y >= ysize: - x = y = 0 + y = 0 layer += 1 else: logger.debug("- unsupported data organization") @@ -1644,7 +1680,7 @@ SAVE_INFO = { "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), @@ -1652,10 +1688,7 @@ SAVE_INFO = { "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), } @@ -1666,10 +1699,13 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 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: @@ -1901,7 +1937,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 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 @@ -1926,7 +1964,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # 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. @@ -2017,20 +2055,21 @@ class AppendingTiffWriter(io.BytesIO): 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() @@ -2050,11 +2089,13 @@ class AppendingTiffWriter(io.BytesIO): 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() @@ -2100,18 +2141,20 @@ class AppendingTiffWriter(io.BytesIO): 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: Buffer, /) -> int: return self.f.write(data) @@ -2141,17 +2184,19 @@ class AppendingTiffWriter(io.BytesIO): msg = f"wrote only {bytes_written} bytes but wanted {expected}" raise RuntimeError(msg) - def rewriteLastShortToLong(self, value: int) -> None: - self.f.seek(-2, os.SEEK_CUR) - bytes_written = self.f.write(struct.pack(self.longFmt, value)) - self._verify_bytes_written(bytes_written, 4) - - def _rewriteLast(self, value: int, field_size: int) -> None: + 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(field_size), value) + struct.pack(self.endian + self._fmt(new_field_size), value) ) - self._verify_bytes_written(bytes_written, field_size) + 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: return self._rewriteLast(value, 2) @@ -2159,13 +2204,17 @@ class AppendingTiffWriter(io.BytesIO): def rewriteLastLong(self, value: int) -> None: 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)) - self._verify_bytes_written(bytes_written, 2) + self._write(value, 2) def writeLong(self, value: int) -> None: - bytes_written = self.f.write(struct.pack(self.longFmt, value)) - self._verify_bytes_written(bytes_written, 4) + self._write(value, 4) def close(self) -> None: self.finalize() @@ -2173,24 +2222,37 @@ class AppendingTiffWriter(io.BytesIO): 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 + 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, field_size) - self.f.seek(cur_pos + 4) + self.f.seek(cur_pos + fmt_size) else: self.f.seek(offset) self._fixOffsets(count, field_size) @@ -2198,24 +2260,33 @@ class AppendingTiffWriter(io.BytesIO): elif is_local: # skip the locally stored value that is not an offset - self.f.seek(4, os.SEEK_CUR) + self.f.seek(fmt_size, os.SEEK_CUR) def _fixOffsets(self, count: int, field_size: int) -> None: for i in range(count): offset = self._read(field_size) offset += self.offsetOfNewPage - if field_size == 2 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) + 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._rewriteLast(offset, field_size) @@ -2232,9 +2303,7 @@ class AppendingTiffWriter(io.BytesIO): 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) @@ -2242,12 +2311,11 @@ 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 + if not hasattr(ims, "encoderinfo"): + ims.encoderinfo = {} + if not hasattr(ims, "encoderconfig"): + ims.encoderconfig = () + nfr = getattr(ims, "n_frames", 1) for idx in range(nfr): ims.seek(idx) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 64188f28c..1716a18cc 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -21,7 +21,7 @@ _VP8_MODES_BY_IDENTIFIER = { 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 @@ -46,8 +46,7 @@ class WebPImageFile(ImageFile.ImageFile): 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._size, loop_count, bgcolor, frame_count, mode = self._decoder.get_info() self.info["loop"] = loop_count bg_a, bg_r, bg_g, bg_b = ( (bgcolor >> 24) & 0xFF, @@ -60,7 +59,6 @@ class WebPImageFile(ImageFile.ImageFile): self.is_animated = self.n_frames > 1 self._mode = "RGB" if mode == "RGBX" else mode self.rawmode = mode - self.tile = [] # Attempt to read ICC / EXIF / XMP chunks from file icc_profile = self._decoder.get_chunk("ICCP") @@ -224,8 +222,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, @@ -241,7 +238,7 @@ 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): diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 68f8a74f5..d569cb4b8 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -68,9 +68,7 @@ if hasattr(Image.core, "drawwmf"): 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,18 @@ class WmfStubImageFile(ImageFile.StubImageFile): format_description = "Windows Metafile" def _open(self) -> None: - self._inch = None - # check placable header - s = self.fp.read(80) + s = self.fp.read(44) - 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 +102,8 @@ class WmfStubImageFile(ImageFile.StubImageFile): # 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 +113,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): 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 +128,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): 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 +137,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): self.info["dpi"] = xdpi else: self.info["dpi"] = xdpi, ydpi + self._inch = xdpi, ydpi else: msg = "Unsupported file format" @@ -152,13 +153,17 @@ class WmfStubImageFile(ImageFile.StubImageFile): def _load(self) -> ImageFile.StubHandler | None: return _handler - def load(self, dpi: int | None = None) -> Image.core.PixelAccess | 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 5d1f201a4..cde28388f 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -34,7 +34,7 @@ for r in range(8): def _accept(prefix: bytes) -> bool: - return prefix[:6] == _MAGIC + return prefix.startswith(_MAGIC) ## @@ -74,9 +74,7 @@ class XVThumbImageFile(ImageFile.ImageFile): self.palette = ImagePalette.raw("RGB", PALETTE) self.tile = [ - ImageFile._Tile( - "raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1) - ) + 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 f3d490a84..1e57aa162 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -38,7 +38,7 @@ xbm_head = re.compile( def _accept(prefix: bytes) -> bool: - return prefix.lstrip()[:7] == b"#define" + return prefix.lstrip().startswith(b"#define") ## @@ -67,7 +67,7 @@ class XbmImageFile(ImageFile.ImageFile): self._mode = "1" self._size = xsize, ysize - self.tile = [ImageFile._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, [ImageFile._Tile("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 1fc6c0c39..3be240fbc 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -25,7 +25,7 @@ xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)') 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 @@ class XpmImageFile(ImageFile.ImageFile): 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,12 +92,16 @@ class XpmImageFile(ImageFile.ImageFile): 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 = [ - ImageFile._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: # @@ -111,16 +109,48 @@ class XpmImageFile(ImageFile.ImageFile): 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 09546fe63..6e4c23f89 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -25,6 +25,7 @@ del _version _plugins = [ + "AvifImagePlugin", "BlpImagePlugin", "BmpImagePlugin", "BufrStubImagePlugin", diff --git a/src/PIL/_avif.pyi b/src/PIL/_avif.pyi new file mode 100644 index 000000000..e27843e53 --- /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/_deprecate.py b/src/PIL/_deprecate.py index 83952b397..170d44490 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. @@ -47,6 +48,8 @@ def deprecate( raise RuntimeError(msg) elif when == 12: removed = "Pillow 12 (2025-10-15)" + elif when == 13: + removed = "Pillow 13 (2026-10-15)" else: msg = f"Unknown removal version: {when}. Update {__name__}?" raise ValueError(msg) @@ -65,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/_imagingft.pyi b/src/PIL/_imagingft.pyi index 9cc9822f5..1cb1429d6 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -28,10 +28,10 @@ class Font: features: list[str] | None, lang: str | None, stroke_width: float, + stroke_filled: bool, anchor: str | None, foreground_ink_long: int, - x_start: float, - y_start: float, + start: tuple[float, float], /, ) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ... def getsize( diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index beddfb062..9c0143003 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 0a7d87cc2..373938e71 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -3,8 +3,9 @@ from __future__ import annotations import os import sys from collections.abc import Sequence -from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union +from typing import Any, Protocol, TypeVar, Union +TYPE_CHECKING = False if TYPE_CHECKING: from numbers import _IntegralLike as IntegralLike @@ -44,10 +45,10 @@ _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 = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] __all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"] diff --git a/src/PIL/_version.py b/src/PIL/_version.py index c4a72ad7e..ac678c7d2 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "11.0.0.dev0" +__version__ = "11.3.0.dev0" diff --git a/src/PIL/features.py b/src/PIL/features.py index 75d59e01c..573f1d412 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -17,6 +17,7 @@ modules = { "freetype2": ("PIL._imagingft", "freetype2_version"), "littlecms2": ("PIL._imagingcms", "littlecms_version"), "webp": ("PIL._webp", "webpdecoder_version"), + "avif": ("PIL._avif", "libavif_version"), } @@ -127,6 +128,8 @@ features: dict[str, tuple[str, str | bool, str | None]] = { "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), } @@ -286,6 +289,7 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: ("freetype2", "FREETYPE2"), ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), + ("avif", "AVIF"), ("jpg", "JPEG"), ("jpg_2000", "OPENJPEG (JPEG2000)"), ("zlib", "ZLIB (PNG/ZIP)"), @@ -299,7 +303,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: @@ -308,7 +313,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/_avif.c b/src/_avif.c new file mode 100644 index 000000000..3585297fe --- /dev/null +++ b/src/_avif.c @@ -0,0 +1,902 @@ +#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; +} + +PyObject * +_encoder_dealloc(AvifEncoderObject *self) { + if (self->encoder) { + avifEncoderDestroy(self->encoder); + } + if (self->image) { + avifImageDestroy(self->image); + } + Py_RETURN_NONE; +} + +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 (image == 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; +} + +PyObject * +_decoder_dealloc(AvifDecoderObject *self) { + if (self->decoder) { + avifDecoderDestroy(self->decoder); + } + PyBuffer_Release(&self->buffer); + Py_RETURN_NONE; +} + +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 (image->exif.size) { + exif = + PyBytes_FromStringAndSize((const char *)image->exif.data, image->exif.size); + } + + if (image->icc.size) { + icc = PyBytes_FromStringAndSize((const char *)image->icc.data, image->icc.size); + } + + 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"); + return NULL; + } + + size = rgb.rowBytes * rgb.height; + + bytes = PyBytes_FromStringAndSize((char *)rgb.pixels, size); + avifRGBImageFreePixels(&rgb); + + 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 2db4486b2..6f13834a9 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 @@ -223,6 +230,93 @@ 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->release != NULL) { + schema->release(schema); + } + free(schema); +} + +PyObject * +ExportArrowSchemaPyCapsule(ImagingObject *self) { + struct ArrowSchema *schema = + (struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema)); + 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->release != NULL) { + array->release(array); + } + free(array); +} + +PyObject * +ExportArrowArrayPyCapsule(ImagingObject *self) { + struct ArrowArray *array = + (struct ArrowArray *)calloc(1, sizeof(struct ArrowArray)); + 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; + 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; + } + + // ImagingBorrowArrow is responsible for retaining the array_capsule + ret = PyImagingNew( + ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule) + ); + if (!ret) { + return ImagingError_ValueError("Invalid Arrow array mode or size mismatch"); + } + return ret; +} + /* -------------------------------------------------------------------- */ /* EXCEPTION REROUTING */ /* -------------------------------------------------------------------- */ @@ -244,12 +338,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(); @@ -275,11 +363,6 @@ ImagingError_ValueError(const char *message) { return NULL; } -void -ImagingError_Clear(void) { - PyErr_Clear(); -} - /* -------------------------------------------------------------------- */ /* HELPERS */ /* -------------------------------------------------------------------- */ @@ -466,8 +549,7 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) { } /* unknown type */ - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static char * @@ -860,7 +942,7 @@ _color_lut_3d(ImagingObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "siiiiiO:color_lut_3d", + "sii(iii)O:color_lut_3d", &mode, &filter, &table_channels, @@ -958,8 +1040,7 @@ _convert2(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1008,10 +1089,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)); } @@ -1207,8 +1284,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); @@ -1410,8 +1486,7 @@ _paste(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1579,7 +1654,8 @@ _putdata(ImagingObject *self, PyObject *args) { int bigendian = 0; if (image->type == IMAGING_TYPE_SPECIAL) { // I;16* - if (strcmp(image->mode, "I;16B") == 0 + if ( + strcmp(image->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN || strcmp(image->mode, "I;16N") == 0 #endif @@ -1684,8 +1760,7 @@ _putdata(ImagingObject *self, PyObject *args) { Py_XDECREF(seq); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1745,8 +1820,7 @@ _putpalette(ImagingObject *self, PyObject *args) { 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 * @@ -1770,8 +1844,7 @@ _putpalettealpha(ImagingObject *self, PyObject *args) { strcpy(self->image->palette->mode, "RGBA"); self->image->palette->palette[index * 4 + 3] = (UINT8)alpha; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1798,8 +1871,7 @@ _putpalettealphas(ImagingObject *self, PyObject *args) { self->image->palette->palette[i * 4 + 3] = (UINT8)values[i]; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1835,8 +1907,7 @@ _putpixel(ImagingObject *self, PyObject *args) { self->access->put_pixel(im, x, y, ink); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2003,8 +2074,7 @@ im_setmode(ImagingObject *self, PyObject *args) { } self->access = ImagingAccessNew(im); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2067,8 +2137,7 @@ _transform(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2147,6 +2216,7 @@ _unsharp_mask(ImagingObject *self, PyObject *args) { } if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) { + ImagingDelete(imOut); return NULL; } @@ -2195,8 +2265,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]); @@ -2276,8 +2345,7 @@ _getextrema(ImagingObject *self) { } } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2340,8 +2408,7 @@ _fillband(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2356,8 +2423,7 @@ _putband(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2943,8 +3009,7 @@ _draw_arc(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2981,8 +3046,7 @@ _draw_bitmap(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3038,8 +3102,7 @@ _draw_chord(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3093,8 +3156,7 @@ _draw_ellipse(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3147,7 +3209,8 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) { (int)p[3], &ink, width, - self->blend + self->blend, + NULL ) < 0) { free(xy); return NULL; @@ -3157,8 +3220,7 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) { free(xy); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3189,8 +3251,7 @@ _draw_points(ImagingDrawObject *self, PyObject *args) { free(xy); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /* from outline.c */ @@ -3218,8 +3279,7 @@ _draw_outline(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3275,8 +3335,7 @@ _draw_pieslice(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3289,7 +3348,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; } @@ -3319,16 +3381,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 * @@ -3382,8 +3451,7 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static struct PyMethodDef _draw_methods[] = { @@ -3588,8 +3656,7 @@ _save_ppm(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /* -------------------------------------------------------------------- */ @@ -3678,6 +3745,10 @@ static struct PyMethodDef methods[] = { /* Misc. */ {"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 */ }; @@ -3745,6 +3816,11 @@ _getattr_unsafe_ptrs(ImagingObject *self, void *closure) { ); } +static PyObject * +_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}, @@ -3752,6 +3828,7 @@ static struct PyGetSetDef getsetters[] = { {"id", (getter)_getattr_id}, {"ptr", (getter)_getattr_ptr}, {"unsafe_ptrs", (getter)_getattr_unsafe_ptrs}, + {"readonly", (getter)_getattr_readonly}, {NULL} }; @@ -3792,102 +3869,26 @@ 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, }; 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, }; static PyMappingMethods pixel_access_as_mapping = { @@ -3899,20 +3900,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, }; /* -------------------------------------------------------------------- */ @@ -3977,8 +3968,7 @@ _reset_stats(PyObject *self, PyObject *args) { arena->stats_freed_blocks = 0; MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -4038,8 +4028,7 @@ _set_alignment(PyObject *self, PyObject *args) { ImagingDefaultArena.alignment = alignment; MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -4063,8 +4052,7 @@ _set_block_size(PyObject *self, PyObject *args) { ImagingDefaultArena.block_size = block_size; MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -4092,8 +4080,22 @@ _set_blocks_max(PyObject *self, PyObject *args) { 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 * @@ -4108,8 +4110,7 @@ _clear_cache(PyObject *self, PyObject *args) { ImagingMemoryClearCache(&ImagingDefaultArena, i); MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /* -------------------------------------------------------------------- */ @@ -4155,6 +4156,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); @@ -4216,6 +4219,7 @@ static PyMethodDef functions[] = { {"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 */ @@ -4223,6 +4227,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}, @@ -4284,7 +4289,6 @@ static PyMethodDef functions[] = { {"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 */ /* Drawing support stuff */ {"font", (PyCFunction)_font_new, METH_VARARGS}, @@ -4302,9 +4306,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 */ @@ -4367,6 +4373,15 @@ setup_module(PyObject *m) { Py_INCREF(have_libjpegturbo); PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo); + PyObject *have_mozjpeg; +#ifdef JPEG_C_PARAM_SUPPORTED + have_mozjpeg = Py_True; +#else + have_mozjpeg = Py_False; +#endif + Py_INCREF(have_mozjpeg); + PyModule_AddObject(m, "HAVE_MOZJPEG", have_mozjpeg); + PyObject *have_libimagequant; #ifdef HAVE_LIBIMAGEQUANT have_libimagequant = Py_True; @@ -4397,6 +4412,20 @@ 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 + Py_INCREF(have_zlibng); + PyModule_AddObject(m, "HAVE_ZLIBNG", have_zlibng); + #ifdef HAVE_LIBTIFF { extern const char *ImagingTiffVersion(void); @@ -4434,28 +4463,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__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; - } - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 1823bcf03..e2f29d1b7 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -346,10 +346,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); } @@ -362,9 +362,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 @@ -378,17 +378,17 @@ _buildTransform( ) { 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; @@ -412,19 +412,19 @@ _buildProofTransform( ) { 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; @@ -654,8 +654,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 @@ -672,20 +671,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); @@ -723,14 +719,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); @@ -780,14 +774,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); @@ -801,14 +793,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 @@ -835,21 +825,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++) { @@ -858,8 +845,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); } @@ -926,8 +912,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); @@ -957,8 +942,7 @@ _is_intent_supported(CmsProfileObject *self, int clut) { 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); @@ -1042,8 +1026,7 @@ 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( @@ -1141,8 +1124,7 @@ cms_profile_getattr_saturation_rendering_intent_gamut( 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); } @@ -1150,8 +1132,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); } @@ -1159,8 +1140,7 @@ 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); } @@ -1176,21 +1156,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); } @@ -1229,8 +1206,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); @@ -1245,8 +1221,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); @@ -1261,8 +1236,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); @@ -1321,14 +1295,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) { @@ -1362,14 +1334,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( @@ -1432,44 +1402,19 @@ static struct PyGetSetDef cms_profile_getsetters[] = { {"colorant_table_out", (getter)cms_profile_getattr_colorant_table_out}, {"intent_supported", (getter)cms_profile_getattr_is_intent_supported}, {"clut", (getter)cms_profile_getattr_is_clut}, - {"icc_measurement_condition", (getter)cms_profile_getattr_icc_measurement_condition - }, + {"icc_measurement_condition", + (getter)cms_profile_getattr_icc_measurement_condition}, {"icc_viewing_condition", (getter)cms_profile_getattr_icc_viewing_condition}, {NULL} }; static PyTypeObject CmsProfile_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "PIL.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[] = { @@ -1477,36 +1422,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 @@ -1544,29 +1463,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; - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingft.c b/src/_imagingft.c index 7f2464124..29d8e9e71 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -82,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; @@ -187,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 */ @@ -197,6 +202,7 @@ 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, @@ -204,6 +210,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { index, &self->face ); + MUTEX_UNLOCK(&ft_library_mutex); } } @@ -268,6 +275,7 @@ 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); @@ -332,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); } @@ -424,6 +426,7 @@ text_layout_fallback( "setting text direction, language or font features is not supported " "without libraqm" ); + return 0; } if (PyUnicode_Check(string)) { @@ -833,6 +836,7 @@ font_render(FontObject *self, PyObject *args) { int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ int color = 0; /* is FT_LOAD_COLOR enabled? */ float stroke_width = 0; + int stroke_filled = 0; PY_LONG_LONG foreground_ink_long = 0; unsigned int foreground_ink; const char *mode = NULL; @@ -852,7 +856,7 @@ font_render(FontObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "OO|zzOzfzLffO:render", + "OO|zzOzfpzL(ff):render", &string, &fill, &mode, @@ -860,6 +864,7 @@ font_render(FontObject *self, PyObject *args) { &features, &lang, &stroke_width, + &stroke_filled, &anchor, &foreground_ink_long, &x_start, @@ -1004,7 +1009,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}; @@ -1370,8 +1376,7 @@ font_setvarname(FontObject *self, PyObject *args) { return geterror(error); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1425,15 +1430,16 @@ font_setvaraxes(FontObject *self, PyObject *args) { return geterror(error); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } #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); @@ -1514,36 +1520,11 @@ static struct PyGetSetDef font_getsetters[] = { }; 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[] = { @@ -1620,27 +1601,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; - } - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingmath.c b/src/_imagingmath.c index dbe636707..48c395900 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -192,8 +192,7 @@ _unop(PyObject *self, PyObject *args) { unop(out, im1); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -226,8 +225,7 @@ _binop(PyObject *self, PyObject *args) { binop(out, im1, im2); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyMethodDef _functions[] = { @@ -304,27 +302,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__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; - } - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index b763e3a6f..5995f9d42 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -246,23 +246,22 @@ static PyMethodDef functions[] = { {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); - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingtk.c b/src/_imagingtk.c index c70d044bb..68d7bf4cd 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,25 +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; - } -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_webp.c b/src/_webp.c index dfda7048d..e84e786ed 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -164,7 +164,7 @@ _anim_encoder_new(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "iiIiiiiii", + "(ii)Iiiiiii", &width, &height, &bgcolor, @@ -449,7 +449,7 @@ _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, @@ -530,36 +530,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 @@ -573,36 +547,10 @@ 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, }; /* -------------------------------------------------------------------- */ @@ -693,6 +641,10 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { ImagingSectionLeave(&cookie); WebPPictureFree(&pic); + + output = writer.mem; + ret_size = writer.size; + if (!ok) { int error_code = (&pic)->error_code; char message[50] = ""; @@ -704,10 +656,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { ); } PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); + free(output); return NULL; } - output = writer.mem; - ret_size = writer.size; { /* I want to truncate the *_size items that get passed into WebP @@ -829,27 +780,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; - } - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/decode.c b/src/decode.c index 51d0aced2..03db1ce35 100644 --- a/src/decode.c +++ b/src/decode.c @@ -213,8 +213,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 * @@ -231,8 +230,7 @@ _setfd(ImagingDecoderObject *decoder, PyObject *args) { Py_XINCREF(fd); state->fd = fd; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -258,36 +256,11 @@ 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, }; /* -------------------------------------------------------------------- */ diff --git a/src/display.c b/src/display.c index b4e2e3899..3215f6691 100644 --- a/src/display.c +++ b/src/display.c @@ -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 * @@ -112,8 +111,7 @@ _draw(ImagingDisplayObject *display, PyObject *args) { ImagingDrawDIB(display->dib, hdc, dst, src); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } extern Imaging @@ -143,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 * @@ -190,8 +187,7 @@ _releasedc(ImagingDisplayObject *display, PyObject *args) { ReleaseDC(window, dc); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -211,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 * @@ -253,36 +248,11 @@ static struct PyGetSetDef getsetters[] = { }; 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 * @@ -316,46 +286,72 @@ PyImaging_DisplayModeWin32(PyObject *self, PyObject *args) { /* -------------------------------------------------------------------- */ /* 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); @@ -367,6 +363,10 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { FreeLibrary(user32); + if (width == -1) { + goto error; + } + bitmap = CreateCompatibleBitmap(screen, width, height); if (!bitmap) { goto error; @@ -412,7 +412,11 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { 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); @@ -420,7 +424,11 @@ error: PyErr_SetString(PyExc_OSError, "screen grab failed"); DeleteDC(screen_copy); - DeleteDC(screen); + if (screens == -1) { + ReleaseDC(wnd, screen); + } else { + DeleteDC(screen); + } return NULL; } @@ -690,25 +698,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; } /* -------------------------------------------------------------------- */ @@ -716,6 +725,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; @@ -796,10 +813,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 */ diff --git a/src/encode.c b/src/encode.c index 1a4cd489d..e56494036 100644 --- a/src/encode.c +++ b/src/encode.c @@ -27,6 +27,7 @@ #include "thirdparty/pythoncapi_compat.h" #include "libImaging/Imaging.h" +#include "libImaging/Bcn.h" #include "libImaging/Gif.h" #ifdef HAVE_UNISTD_H @@ -278,8 +279,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 * @@ -296,8 +296,7 @@ _setfd(ImagingEncoderObject *encoder, PyObject *args) { Py_XINCREF(fd); state->fd = fd; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -325,36 +324,11 @@ 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, }; /* -------------------------------------------------------------------- */ @@ -377,6 +351,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 */ /* -------------------------------------------------------------------- */ @@ -704,6 +703,8 @@ 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_GetItemRef(tags, pos); @@ -736,7 +737,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { } 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; } } @@ -929,7 +930,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { ); } else if (type == TIFF_LONG) { 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( @@ -959,6 +960,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { 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, (uint64_t)PyLong_AsLongLong(value) + ); } else { TRACE( ("Unhandled type for key %d : %s \n", @@ -1095,7 +1100,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|nnnnpnnnnnnOz#y#y#", + "ss|nnnnpn(nn)nnnOz#y#y#", &mode, &rawmode, &quality, @@ -1251,7 +1256,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"; @@ -1265,7 +1270,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|OOOsOnOOOssbbnz#p", + "ss|OOOsOnOOpssbbnz#p", &mode, &format, &offset, @@ -1400,7 +1405,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { 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; diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c new file mode 100644 index 000000000..ccafe33b9 --- /dev/null +++ b/src/libImaging/Arrow.c @@ -0,0 +1,301 @@ + +#include "Arrow.h" +#include "Imaging.h" +#include + +/* struct ArrowSchema* */ +/* _arrow_schema_channel(char* channel, char* format) { */ + +/* } */ + +static void +ReleaseExportedSchema(struct ArrowSchema *array) { + // This should not be called on already released array + // assert(array->release != NULL); + + 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 + for (int64_t i = 0; i < array->n_children; ++i) { + struct ArrowSchema *child = array->children[i]; + if (child->release != NULL) { + child->release(child); + child->release = NULL; + } + free(array->children[i]); + } + if (array->children) { + free(array->children); + } + + // Release dictionary + struct ArrowSchema *dict = array->dictionary; + if (dict != NULL && dict->release != NULL) { + dict->release(dict); + dict->release = NULL; + } + + // TODO here: release and/or deallocate all data directly owned by + // the ArrowArray struct, such as the private_data. + + // Mark array released + array->release = NULL; +} + +int +export_named_type(struct ArrowSchema *schema, char *format, 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; + + 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) { + return export_named_type(schema, im->arrow_band_format, im->band_names[0]); + } + + 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 *)); + schema->children[0] = (struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema)); + retval = export_named_type(schema->children[0], im->arrow_band_format, "pixel"); + if (retval != 0) { + free(schema->children[0]); + free(schema->children); + schema->release(schema); + return retval; + } + 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) { + // undone -- does arrow release all the children recursively? + for (int i = 0; i < array->n_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 **)malloc(sizeof(void *) * array->n_buffers); + // assert(array->buffers != NULL); + 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 (im->block) { + array->children[0]->buffers[1] = im->block; + } else { + array->children[0]->buffers[1] = im->blocks[0].ptr; + } + return 0; + +err: + if (array->children[0]) { + free(array->children[0]); + } + if (array->children) { + free(array->children); + } + if (array->buffers) { + free(array->buffers); + } + 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 000000000..0b285fe80 --- /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/BcnDecode.c b/src/libImaging/BcnDecode.c index 9a41febc7..7b3d8f908 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]; + } } } diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c new file mode 100644 index 000000000..7a5072dde --- /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) { + UINT8 r, g, b; + r = item.color[0] >> (8 - 5); + g = item.color[1] >> (8 - 6); + 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], 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 = + strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "LA") == 0; + + UINT8 *dst = buf; + + for (;;) { + // Loop writes a max of 16 bytes per iteration + if (dst + 16 >= 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/Draw.c b/src/libImaging/Draw.c index f1c8ffcff..27cac687e 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -63,7 +63,7 @@ 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) { @@ -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,16 +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 (strncmp(im->mode, "I;16", 4) == 0) { + bigendian = + ( +#ifdef WORDS_BIGENDIAN + strcmp(im->mode, "I;16") == 0 || strcmp(im->mode, "I;16L") == 0 +#else + strcmp(im->mode, "I;16B") == 0 +#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) { @@ -143,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) { @@ -167,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; } @@ -407,7 +435,14 @@ 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++) { @@ -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,7 +475,7 @@ 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; @@ -461,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; @@ -470,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; } @@ -501,56 +537,49 @@ 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( + 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 { - xx[k] = - fmin( + } 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; - } - } else { - if (current->dx > 0) { - xx[k] = fmin( - adjacent_line_x, adjacent_line_x_other_edge - ); - } else { - xx[k] = - fmax( - adjacent_line_x, adjacent_line_x_other_edge - ) + + )) - 1; } + break; } - break; } } } @@ -565,7 +594,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; @@ -581,13 +610,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); } } } @@ -597,21 +626,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); */ @@ -646,14 +660,13 @@ 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 */ @@ -698,7 +711,15 @@ 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; @@ -738,7 +759,7 @@ ImagingDrawWideLine( 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; } @@ -781,7 +802,7 @@ ImagingDrawRectangle( } for (y = y0; y <= y1; y++) { - draw->hline(im, x0, y, x1, ink); + draw->hline(im, x0, y, x1, ink, NULL); } } else { @@ -790,8 +811,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); } @@ -802,7 +823,14 @@ 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; @@ -846,7 +874,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 { @@ -868,11 +896,12 @@ 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 ); } } @@ -1543,7 +1572,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; } @@ -1578,7 +1609,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; @@ -1996,7 +2029,7 @@ ImagingDrawOutline( 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/Except.c b/src/libImaging/Except.c deleted file mode 100644 index f42ff9aec..000000000 --- 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 76d0abccc..901fe83ad 100644 --- a/src/libImaging/File.c +++ b/src/libImaging/File.c @@ -54,7 +54,7 @@ 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; } diff --git a/src/libImaging/Fill.c b/src/libImaging/Fill.c index 8fb481e7e..28f427370 100644 --- a/src/libImaging/Fill.c +++ b/src/libImaging/Fill.c @@ -118,8 +118,9 @@ ImagingFillRadialGradient(const char *mode) { for (y = 0; y < 256; y++) { for (x = 0; x < 256; x++) { - d = (int - )sqrt((double)((x - 128) * (x - 128) + (y - 128) * (y - 128)) * 2.0); + d = (int)sqrt( + (double)((x - 128) * (x - 128) + (y - 128) * (y - 128)) * 2.0 + ); if (d >= 255) { d = 255; } diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index 7b7b2e429..c46dd3cd1 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -155,7 +155,8 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { } else { int bigendian = 0; if (im->type == IMAGING_TYPE_SPECIAL) { - if (strcmp(im->mode, "I;16B") == 0 + if ( + strcmp(im->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN || strcmp(im->mode, "I;16N") == 0 #endif @@ -308,7 +309,8 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { } else { int bigendian = 0; if (im->type == IMAGING_TYPE_SPECIAL) { - if (strcmp(im->mode, "I;16B") == 0 + if ( + strcmp(im->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN || strcmp(im->mode, "I;16N") == 0 #endif diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index c9b7e43b4..2ce282241 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -44,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 @@ -55,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 31052c68a..bfe67d462 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -20,6 +20,8 @@ extern "C" { #define M_PI 3.1415926535897932384626433832795 #endif +#include "Arrow.h" + /* -------------------------------------------------------------------- */ /* @@ -104,6 +106,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)]) @@ -161,6 +178,7 @@ 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 @@ -174,6 +192,8 @@ 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); @@ -187,6 +207,15 @@ ImagingDelete(Imaging im); extern Imaging ImagingNewBlock(const char *mode, int xsize, int ysize); +extern Imaging +ImagingNewArrow( + const char *mode, + int xsize, + int ysize, + PyObject *schema_capsule, + PyObject *array_capsule +); + extern Imaging ImagingNewPrologue(const char *mode, int xsize, int ysize); extern Imaging @@ -241,8 +270,6 @@ ImagingSectionLeave(ImagingSectionCookie *cookie); /* Exceptions */ /* ---------- */ -extern void * -ImagingError_OSError(void); extern void * ImagingError_MemoryError(void); extern void * @@ -251,8 +278,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 */ /* ------------------- */ @@ -481,7 +506,15 @@ 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( @@ -501,7 +534,14 @@ 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( @@ -567,6 +607,8 @@ typedef int (*ImagingCodec)( 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); @@ -609,10 +651,6 @@ ImagingLibTiffDecode( 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 @@ -702,6 +740,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 @@ -709,6 +754,8 @@ _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; diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index fc927d2f0..cc6955ca5 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -615,6 +615,7 @@ static const struct j2k_decode_unpacker j2k_unpackers[] = { {"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_GRAY, 4, 1, j2ku_srgba_rgba}, {"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}, @@ -640,7 +641,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; @@ -870,7 +871,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) { @@ -883,7 +884,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( diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index d30ccde60..61e095ad6 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -207,8 +207,8 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) { if (params->cp_cinema == OPJ_CINEMA4K_24) { float max_rate = - ((float)(components * im->xsize * im->ysize * 8) / (CINEMA_24_CS_LENGTH * 8) - ); + ((float)(components * im->xsize * im->ysize * 8) / + (CINEMA_24_CS_LENGTH * 8)); params->POC[0].tile = 1; params->POC[0].resno0 = 0; @@ -243,8 +243,8 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) { params->max_comp_size = COMP_24_CS_MAX_LENGTH; } else { float max_rate = - ((float)(components * im->xsize * im->ysize * 8) / (CINEMA_48_CS_LENGTH * 8) - ); + ((float)(components * im->xsize * im->ysize * 8) / + (CINEMA_48_CS_LENGTH * 8)); for (n = 0; n < params->tcp_numlayers; ++n) { rate = 0; @@ -330,6 +330,13 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { 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 (strcmp(im->mode, "CMYK") == 0) { + components = 4; + color_space = OPJ_CLRSPC_CMYK; + pack = j2k_pack_rgba; +#endif } else { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 4372d51d5..79a38e12f 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -131,10 +131,20 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { 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); diff --git a/src/libImaging/PcxDecode.c b/src/libImaging/PcxDecode.c index 942c8dc22..a65952fb1 100644 --- a/src/libImaging/PcxDecode.c +++ b/src/libImaging/PcxDecode.c @@ -60,15 +60,25 @@ 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 ); } } diff --git a/src/libImaging/Point.c b/src/libImaging/Point.c index 6a4060b4b..b11ea62ed 100644 --- a/src/libImaging/Point.c +++ b/src/libImaging/Point.c @@ -197,8 +197,9 @@ ImagingPoint(Imaging imIn, const char *mode, const void *table) { return imOut; mode_mismatch: - return (Imaging - )ImagingError_ValueError("point operation not supported for this mode"); + return (Imaging)ImagingError_ValueError( + "point operation not supported for this mode" + ); } Imaging diff --git a/src/libImaging/QuantHash.h b/src/libImaging/QuantHash.h index 0462cfd49..d75d55ce0 100644 --- a/src/libImaging/QuantHash.h +++ b/src/libImaging/QuantHash.h @@ -20,8 +20,12 @@ typedef uint32_t HashVal_t; typedef uint32_t (*HashFunc)(const HashTable *, const HashKey_t); typedef int (*HashCmpFunc)(const HashTable *, const HashKey_t, const HashKey_t); -typedef void (*IteratorFunc)(const HashTable *, const HashKey_t, const HashVal_t, void *); -typedef void (*IteratorUpdateFunc)(const HashTable *, const HashKey_t, HashVal_t *, void *); +typedef void (*IteratorFunc)( + const HashTable *, const HashKey_t, const HashVal_t, void * +); +typedef void (*IteratorUpdateFunc)( + const HashTable *, const HashKey_t, HashVal_t *, void * +); typedef void (*ComputeFunc)(const HashTable *, const HashKey_t, HashVal_t *); typedef void (*CollisionFunc)( const HashTable *, HashKey_t *, HashVal_t *, HashKey_t, HashVal_t diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index f5e386dc2..b114e0023 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -470,7 +470,8 @@ ImagingResampleHorizontal_16bpc( double *k; int bigendian = 0; - if (strcmp(imIn->mode, "I;16N") == 0 + if ( + strcmp(imIn->mode, "I;16N") == 0 #ifdef WORDS_BIGENDIAN || strcmp(imIn->mode, "I;16B") == 0 #endif @@ -509,7 +510,8 @@ ImagingResampleVertical_16bpc( double *k; int bigendian = 0; - if (strcmp(imIn->mode, "I;16N") == 0 + if ( + strcmp(imIn->mode, "I;16N") == 0 #ifdef WORDS_BIGENDIAN || strcmp(imIn->mode, "I;16B") == 0 #endif diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 522e9f375..6fe26e1bd 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -58,19 +58,22 @@ 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) { /* 1-bit images */ im->bands = im->pixelsize = 1; im->linesize = xsize; + strcpy(im->band_names[0], "1"); } else if (strcmp(mode, "P") == 0) { /* 8-bit palette mapped images */ im->bands = im->pixelsize = 1; im->linesize = xsize; im->palette = ImagingPaletteNew("RGB"); + strcpy(im->band_names[0], "P"); } else if (strcmp(mode, "PA") == 0) { /* 8-bit palette with alpha */ @@ -78,23 +81,36 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 4; /* store in image32 memory */ im->linesize = xsize * 4; im->palette = ImagingPaletteNew("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) { /* 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) { /* 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) { /* 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) { /* 32-bit floating point images */ @@ -102,6 +118,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { 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) { /* 32-bit integer images */ @@ -109,6 +127,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { 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) { @@ -118,12 +138,18 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { 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) { /* 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 */ @@ -132,6 +158,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 2; im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; + /* not allowing arrow due to line length packing */ + strcpy(im->arrow_band_format, ""); } else if (strcmp(mode, "BGR;16") == 0) { /* EXPERIMENTAL */ @@ -140,6 +168,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 2; im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; + /* not allowing arrow due to line length packing */ + strcpy(im->arrow_band_format, ""); } else if (strcmp(mode, "BGR;24") == 0) { /* EXPERIMENTAL */ @@ -148,32 +178,54 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 3; im->linesize = (xsize * 3 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; + /* not allowing arrow due to line length packing */ + strcpy(im->arrow_band_format, ""); } else if (strcmp(mode, "RGBX") == 0) { /* 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) { /* 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) { /* 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) { /* 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) { /* 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) { /* 24-bit color, luminance, + 2 color channels */ @@ -181,6 +233,10 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { 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) { /* 24-bit color, luminance, + 2 color channels */ @@ -188,6 +244,10 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { 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); @@ -218,6 +278,7 @@ 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); @@ -238,8 +299,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) { @@ -270,6 +341,7 @@ struct ImagingMemoryArena ImagingDefaultArena = { 0, 0, 0, // Stats + 0, // use_block_allocator #ifdef Py_GIL_DISABLED {0}, #endif @@ -302,6 +374,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) { @@ -396,11 +473,13 @@ ImagingAllocateArray(Imaging im, ImagingMemoryArena arena, int dirty, int block_ 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(); @@ -487,6 +566,59 @@ 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. */ @@ -513,7 +645,7 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { return im; } - ImagingError_Clear(); + PyErr_Clear(); // Try to allocate the image once more with smallest possible block size MUTEX_LOCK(&ImagingDefaultArena.mutex); @@ -529,11 +661,17 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { Imaging ImagingNew(const char *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) { + if (ImagingDefaultArena.use_block_allocator) { + return ImagingNewBlock(mode, xsize, ysize); + } return ImagingNewInternal(mode, xsize, ysize, 1); } @@ -558,6 +696,80 @@ ImagingNewBlock(const char *mode, int xsize, int ysize) { return NULL; } +Imaging +ImagingNewArrow( + const char *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"); + + struct ArrowArray *external_array = + (struct ArrowArray *)PyCapsule_GetPointer(array_capsule, "arrow_array"); + + 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 char *mode, Imaging imOut, Imaging imIn) { /* allocate or validate output image */ diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index e4da9162d..e289ce405 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -299,6 +299,7 @@ _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) { return -1; } + img.orientation = ORIENTATION_TOPLEFT; img.req_orientation = ORIENTATION_TOPLEFT; img.col_offset = 0; @@ -556,7 +557,8 @@ _decodeStrip( (tdata_t)state->buffer, strip_size ) == -1) { - TRACE(("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0)) + TRACE( + ("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0)) ); state->errcode = IMAGING_CODEC_BROKEN; return -1; @@ -928,6 +930,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. @@ -1009,17 +1032,10 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt TRACE(("Encode Error, row %d\n", state->y)); state->errcode = IMAGING_CODEC_BROKEN; - // 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); + clientstate->tiff = NULL; } else { - // When tif_closeproc refers to our custom _tiffCloseProc though, - // that is fine, as it does not close the file - TIFFClose(tiff); - } - if (!clientstate->fp) { free(clientstate->data); } return -1; @@ -1035,22 +1051,11 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt TRACE(("Error flushing the tiff")); // likely reason is memory. state->errcode = IMAGING_CODEC_MEMORY; - if (clientstate->fp) { - TIFFCleanup(tiff); - } else { - TIFFClose(tiff); - } if (!clientstate->fp) { free(clientstate->data); } return -1; } - TRACE(("Closing \n")); - if (clientstate->fp) { - TIFFCleanup(tiff); - } else { - TIFFClose(tiff); - } // reset the clientstate metadata to use it to read out the buffer. clientstate->loc = 0; clientstate->size = clientstate->eof; // redundant? diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h index 22361210d..77808b543 100644 --- a/src/libImaging/TiffDecode.h +++ b/src/libImaging/TiffDecode.h @@ -40,6 +40,8 @@ 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 ); diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index c23d5d889..9c3ee2665 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1664,6 +1664,7 @@ static struct { {"RGBA", "RGBaXX", 48, unpackRGBaskip2}, {"RGBA", "RGBa;16L", 64, unpackRGBa16L}, {"RGBA", "RGBa;16B", 64, unpackRGBa16B}, + {"RGBA", "BGR", 24, ImagingUnpackBGR}, {"RGBA", "BGRa", 32, unpackBGRa}, {"RGBA", "RGBA;I", 32, unpackRGBAI}, {"RGBA", "RGBA;L", 32, unpackRGBAL}, @@ -1695,6 +1696,7 @@ static struct { #ifdef WORDS_BIGENDIAN {"RGB", "RGB;16N", 48, unpackRGB16B}, + {"RGB", "RGBX;16N", 64, unpackRGBA16B}, {"RGBA", "RGBa;16N", 64, unpackRGBa16B}, {"RGBA", "RGBA;16N", 64, unpackRGBA16B}, {"RGBX", "RGBX;16N", 64, unpackRGBA16B}, @@ -1708,6 +1710,7 @@ static struct { {"RGBA", "A;16N", 16, band316B}, #else {"RGB", "RGB;16N", 48, unpackRGB16L}, + {"RGB", "RGBX;16N", 64, unpackRGBA16L}, {"RGBA", "RGBa;16N", 64, unpackRGBa16L}, {"RGBA", "RGBA;16N", 64, unpackRGBA16L}, {"RGBX", "RGBX;16N", 64, unpackRGBA16L}, diff --git a/src/map.c b/src/map.c index c66702981..9a3144ab9 100644 --- a/src/map.c +++ b/src/map.c @@ -137,6 +137,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 27cc255cf..32ab9109c 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 067f42f62..38300547c 100644 --- a/src/path.c +++ b/src/path.c @@ -109,6 +109,39 @@ 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)) { + for (int k = 0; k < 2; k++) { + PyObject *op1 = PyList_GetItemRef(op, k); + if (op1 == NULL) { + return -1; + } + j = assign_item_to_array(xy, j, op1); + Py_DECREF(op1); + if (j == -1) { + 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; @@ -164,48 +197,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; \ - } \ - if (decref) { \ - Py_DECREF(op); \ - } - /* Copy table to path array */ if (PyList_Check(data)) { for (i = 0; i < n; i++) { - double x, y; PyObject *op = PyList_GetItemRef(data, i); if (op == NULL) { free(xy); 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; + } } } 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 */ @@ -217,7 +234,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; + } } } @@ -415,8 +437,7 @@ path_map(PyPathObject *self, PyObject *args) { } self->mapping = 0; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static int @@ -528,8 +549,7 @@ path_transform(PyPathObject *self, PyObject *args) { } } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static struct PyMethodDef methods[] = { @@ -600,34 +620,11 @@ static PyMappingMethods path_as_mapping = { }; 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 index 51e8c0de7..04fcf61e0 100644 --- a/src/thirdparty/pythoncapi_compat.h +++ b/src/thirdparty/pythoncapi_compat.h @@ -7,7 +7,10 @@ // https://github.com/python/pythoncapi_compat // // Latest version: -// https://raw.githubusercontent.com/python/pythoncapi_compat/master/pythoncapi_compat.h +// 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 @@ -19,11 +22,15 @@ 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 @@ -45,6 +52,13 @@ extern "C" { # 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) @@ -280,7 +294,7 @@ PyFrame_GetVarString(PyFrameObject *frame, const char *name) // bpo-39947 added PyThreadState_GetInterpreter() to Python 3.9.0a5 -#if PY_VERSION_HEX < 0x030900A5 || defined(PYPY_VERSION) +#if PY_VERSION_HEX < 0x030900A5 || (defined(PYPY_VERSION) && PY_VERSION_HEX < 0x030B0000) static inline PyInterpreterState * PyThreadState_GetInterpreter(PyThreadState *tstate) { @@ -573,7 +587,7 @@ static inline int PyWeakref_GetRef(PyObject *ref, PyObject **pobj) return 0; } *pobj = Py_NewRef(obj); - return (*pobj != NULL); + return 1; } #endif @@ -911,7 +925,7 @@ static inline int PyObject_VisitManagedDict(PyObject *obj, visitproc visit, void *arg) { PyObject **dict = _PyObject_GetDictPtr(obj); - if (*dict == NULL) { + if (dict == NULL || *dict == NULL) { return -1; } Py_VISIT(*dict); @@ -922,7 +936,7 @@ static inline void PyObject_ClearManagedDict(PyObject *obj) { PyObject **dict = _PyObject_GetDictPtr(obj); - if (*dict == NULL) { + if (dict == NULL || *dict == NULL) { return; } Py_CLEAR(*dict); @@ -1197,11 +1211,11 @@ static inline int PyTime_PerfCounter(PyTime_t *result) #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.9. +// 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 >= 0x07090000))) + && PYPY_VERSION_NUM >= 0x07030800))) # define PyHASH_BITS _PyHASH_BITS # define PyHASH_MODULUS _PyHASH_MODULUS # define PyHASH_INF _PyHASH_INF @@ -1338,6 +1352,166 @@ PyDict_SetDefaultRef(PyObject *d, PyObject *key, PyObject *default_value, } #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 @@ -1353,6 +1527,678 @@ static inline int PyLong_GetSign(PyObject *obj, int *sign) } #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 } diff --git a/tox.ini b/tox.ini index e79d88500..967d4b537 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ requires = tox>=4.2 env_list = lint - py{py3, 313, 312, 311, 310, 39} + py{py3, 314, 313, 312, 311, 310, 39} [testenv] deps = @@ -11,12 +11,8 @@ deps = extras = tests commands = - make clean - {envpython} -m pip install . {envpython} selftest.py {envpython} -m pytest -W always {posargs} -allowlist_externals = - make [testenv:lint] skip_install = true diff --git a/wheels/dependency_licenses/AOM.txt b/wheels/dependency_licenses/AOM.txt new file mode 100644 index 000000000..3a2e46c26 --- /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 000000000..875b138ec --- /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 93efc6126..8f2345992 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 000000000..350eb9d15 --- /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 000000000..c911747a6 --- /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/multibuild b/wheels/multibuild index 452dd2d17..42d761728 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 452dd2d1705f6b2375369a6570c415beb3163f70 +Subproject commit 42d761728d141d8462cd9943f4329f12fe62b155 diff --git a/winbuild/README.md b/winbuild/README.md index f6111c79b..62345af60 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -11,10 +11,10 @@ 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 2019 with Visual Studio 2019 Community and Visual Studio 2022 Community (AppVeyor). * Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). -The following is a simplified version of the script used on AppVeyor: +Here's an example script to build on Windows: + ``` set PYTHON=C:\Python39\bin cd /D C:\Pillow\winbuild @@ -24,6 +24,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 96b8803b4..aa4677ad5 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,7 +113,7 @@ 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:\Python39\bin cd /D C:\Pillow\winbuild @@ -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 a21fbef91..187d07b20 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -7,6 +7,7 @@ import re import shutil import struct import subprocess +import sys from typing import Any @@ -56,7 +57,10 @@ def cmd_nmake( def cmds_cmake( - target: str | tuple[str, ...] | list[str], *params: str, 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) @@ -65,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 @@ -112,28 +116,26 @@ V = { "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", - "HARFBUZZ": "10.0.1", - "JPEGTURBO": "3.0.4", - "LCMS2": "2.16", - "LIBPNG": "1.6.44", - "LIBWEBP": "1.4.0", - "OPENJPEG": "2.5.2", - "TIFF": "4.6.0", - "XZ": "5.6.3", - "ZLIB": "1.3.1", + "HARFBUZZ": "11.2.1", + "JPEGTURBO": "3.1.1", + "LCMS2": "2.17", + "LIBAVIF": "1.3.0", + "LIBIMAGEQUANT": "4.3.4", + "LIBPNG": "1.6.49", + "LIBWEBP": "1.5.0", + "OPENJPEG": "2.5.3", + "TIFF": "4.7.0", + "XZ": "5.8.1", + "ZLIBNG": "2.2.4", } -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: 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==========" @@ -156,31 +158,37 @@ DEPS: dict[str, dict[str, Any]] = { 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"], }, "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 PROPERTIES OUTPUT_NAME zlibstatic${{SUFFIX}})": "set_target_properties(zlib 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", "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DZLIB_COMPAT:BOOL=ON" + ), ], "headers": [r"z*.h"], - "libs": [r"*.lib"], + "libs": [r"zlib.lib"], }, "xz": { - "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/xz-{V['XZ']}.tar.gz", + "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"), + *cmds_cmake( + "liblzma", + "-DBUILD_SHARED_LIBS:BOOL=OFF" + + (" -DXZ_CLMUL_CRC:BOOL=OFF" if struct.calcsize("P") == 4 else ""), + ), cmd_mkdir(r"{inc_dir}\lzma"), cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"), ], @@ -188,9 +196,8 @@ DEPS: dict[str, dict[str, Any]] = { "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": { @@ -210,9 +217,8 @@ DEPS: dict[str, dict[str, Any]] = { "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": { @@ -236,6 +242,7 @@ DEPS: dict[str, dict[str, Any]] = { "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DWebP_LIBRARY=libwebp", '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", ) ], "headers": [r"libtiff\tiff*.h"], @@ -243,9 +250,8 @@ DEPS: dict[str, dict[str, Any]] = { }, "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"), @@ -259,7 +265,6 @@ DEPS: dict[str, dict[str, Any]] = { "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"), @@ -268,9 +273,8 @@ DEPS: dict[str, dict[str, Any]] = { "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": { @@ -303,9 +307,8 @@ DEPS: dict[str, dict[str, Any]] = { "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": { @@ -331,7 +334,6 @@ DEPS: dict[str, dict[str, Any]] = { "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( @@ -343,30 +345,19 @@ DEPS: dict[str, dict[str, Any]] = { "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( @@ -381,7 +372,6 @@ DEPS: dict[str, dict[str, Any]] = { "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"), @@ -397,6 +387,29 @@ DEPS: dict[str, dict[str, Any]] = { ], "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"], + }, } @@ -497,7 +510,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) @@ -510,15 +523,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) @@ -699,6 +715,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 +760,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 +784,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()